Skip to content

Commit

Permalink
improve node_modules resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
timsuchanek committed Oct 8, 2019
1 parent f1160b8 commit e363e74
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 46 deletions.
18 changes: 18 additions & 0 deletions cli/generator-helper/src/GeneratorProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ChildProcessByStdio, spawn } from 'child_process'
import byline from './byline'
import { GeneratorManifest, GeneratorOptions, JsonRPC } from './types'
import fs from 'fs'
import path from 'path'
import chalk from 'chalk'

let globalMessageId = 1
Expand All @@ -17,6 +18,16 @@ export class GeneratorProcess {
if (!fs.existsSync(executablePath)) {
throw new Error(`Can't find executable ${executablePath}`)
}

if (!hasChmodX(executablePath)) {
throw new Error(
`${chalk.bold(
executablePath,
)} is not executable. Please run ${chalk.greenBright(
`chmod +x ${path.relative(process.cwd(), executablePath)}`,
)}`,
)
}
}
async init() {
if (!this.initPromise) {
Expand Down Expand Up @@ -159,3 +170,10 @@ export class GeneratorProcess {
})
}
}

function hasChmodX(file: string): boolean {
const s = fs.statSync(file)
// tslint:disable-next-line
const newMode = s.mode | 64 | 8 | 1
return s.mode === newMode
}
10 changes: 6 additions & 4 deletions cli/generator-helper/src/__tests__/generatorHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ const stubOptions: GeneratorOptions = {

describe('generatorHandler', () => {
test('not executable', async () => {
const generator = new GeneratorProcess(
path.join(__dirname, 'not-executable'),
)
expect(generator.init()).rejects.toThrow('lacks the right chmod')
expect(() => {
const generator = new GeneratorProcess(
path.join(__dirname, 'not-executable'),
)
}).toThrow('is not executable')
// expect(generator.init()).rejects.toThrow('is not executable')
})
test('parsing error', async () => {
const generator = new GeneratorProcess(
Expand Down
21 changes: 16 additions & 5 deletions cli/sdk/src/Generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import {
export class Generator {
private generatorProcess: GeneratorProcess
public manifest: GeneratorManifest | null = null
constructor(
private executablePath: string,
public options: GeneratorOptions,
) {
public options?: GeneratorOptions
constructor(private executablePath: string) {
this.generatorProcess = new GeneratorProcess(this.executablePath)
}
async init() {
Expand All @@ -22,9 +20,22 @@ export class Generator {
this.generatorProcess.stop()
}
generate(): Promise<void> {
return this.generatorProcess.generate(this.options)
if (!this.options) {
throw new Error(
`Please first run .setOptions() on the Generator to initialize the options`,
)
}
return this.generatorProcess.generate(this.options!)
}
setOptions(options: GeneratorOptions) {
this.options = options
}
setBinaryPaths(binaryPaths: BinaryPaths) {
if (!this.options) {
throw new Error(
`Please first run .setOptions() on the Generator to initialize the options`,
)
}
this.options.binaryPaths = binaryPaths
}
}
20 changes: 10 additions & 10 deletions cli/sdk/src/__tests__/getGenerators/getGenerators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ describe('getGenerators', () => {
'predefined-generator': path.join(__dirname, 'generator'),
}

const generators = await getGenerators(
path.join(__dirname, 'valid-minimal-schema.prisma'),
const generators = await getGenerators({
schemaPath: path.join(__dirname, 'valid-minimal-schema.prisma'),
aliases,
)
})

expect(generators.map(g => g.manifest)).toMatchInlineSnapshot(`
Array [
Expand All @@ -33,7 +33,7 @@ describe('getGenerators', () => {
`)

expect(
pick(generators[0].options, [
pick(generators[0].options!, [
'generator',
'datamodel',
'datasources',
Expand Down Expand Up @@ -71,10 +71,10 @@ describe('getGenerators', () => {
}

expect(
getGenerators(
path.join(__dirname, 'invalid-platforms-schema.prisma'),
getGenerators({
schemaPath: path.join(__dirname, 'invalid-platforms-schema.prisma'),
aliases,
),
}),
).rejects.toThrow('deprecated')
})

Expand All @@ -84,10 +84,10 @@ describe('getGenerators', () => {
}

expect(
getGenerators(
path.join(__dirname, 'invalid-binary-target-schema.prisma'),
getGenerators({
schemaPath: path.join(__dirname, 'invalid-binary-target-schema.prisma'),
aliases,
),
}),
).rejects.toThrow('Unknown')
})
})
82 changes: 55 additions & 27 deletions cli/sdk/src/getGenerators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,38 @@ import {
EngineType,
} from '@prisma/generator-helper'
import 'flat-map-polyfill'
import chalk from 'chalk'
import { BinaryDownloadConfiguration } from '@prisma/fetch-engine/dist/download'

import { getConfig, getDMMF } from './engineCommands'
import { download } from '@prisma/fetch-engine'
import { unique } from './unique'
import { pick } from './pick'
import { Generator } from './Generator'
import chalk from 'chalk'
import { BinaryDownloadConfiguration } from '@prisma/fetch-engine/dist/download'
import { resolveOutput } from './resolveOutput'

export type GetGeneratorOptions = {
schemaPath: string
aliases?: { [alias: string]: string }
version?: string
printDownloadProgress?: boolean
baseDir?: string // useful in tests to resolve the base dir from which `output` is resolved
}

/**
* Makes sure that all generators have the binaries they deserve and returns a
* `Generator` class per generator defined in the schema.prisma file.
* In other words, this is basically a generator factory function.
* @param schemaPath Path to schema.prisma
* @param generatorAliases Aliases like `photonjs` -> `node_modules/photonjs/gen.js`
* @param aliases Aliases like `photonjs` -> `node_modules/photonjs/gen.js`
*/
export async function getGenerators(
schemaPath: string,
generatorAliases?: { [alias: string]: string },
version?: string,
printDownloadProgress?: boolean,
): Promise<Generator[]> {
export async function getGenerators({
schemaPath,
aliases,
version,
printDownloadProgress,
baseDir = path.dirname(schemaPath),
}: GetGeneratorOptions): Promise<Generator[]> {
if (!fs.existsSync(schemaPath)) {
throw new Error(`${schemaPath} does not exist`)
}
Expand All @@ -46,17 +56,43 @@ export async function getGenerators(
config.generators,
async (generator, index) => {
let generatorPath = generator.provider
if (generatorAliases && generatorAliases[generator.provider]) {
generatorPath = generatorAliases[generator.provider]
if (aliases && aliases[generator.provider]) {
generatorPath = aliases[generator.provider]
if (!fs.existsSync(generatorPath)) {
throw new Error(
`Could not find generator executable ${
generatorAliases[generator.provider]
aliases[generator.provider]
} for generator ${generator.provider}`,
)
}
}

const generatorInstance = new Generator(generatorPath)

await generatorInstance.init()

// resolve output path
if (generator.output) {
generator.output = path.resolve(baseDir, generator.output)
} else {
if (
!generatorInstance.manifest ||
!generatorInstance.manifest.defaultOutput
) {
throw new Error(
`Can't resolve output dir for generator ${chalk.bold(
generator.name,
)} with provider ${chalk.bold(generator.provider)}.
The generator needs to either define the \`defaultOutput\` path in the manifest or you need to define \`output\` in the schema.prisma file.`,
)
}

generator.output = await resolveOutput({
defaultOutput: generatorInstance.manifest.defaultOutput,
baseDir,
})
}

const options: GeneratorOptions = {
datamodel: schema,
datasources: config.datasources,
Expand All @@ -66,9 +102,9 @@ export async function getGenerators(
schemaPath,
}

const generatorInstance = new Generator(generatorPath, options)

await generatorInstance.init()
// we set the options here a bit later after instantiating the Generator,
// as we need the generator manifest to resolve the `output` dir
generatorInstance.setOptions(options)

runningGenerators.push(generatorInstance)

Expand Down Expand Up @@ -126,24 +162,16 @@ export async function getGenerators(
}

/**
* Shortcut for getGenerators, if there is only one generator defined. Useful for testing
* Shortcut for getGenerators, if there is only one generator defined. Useful for testing.
* @param schemaPath path to schema.prisma
* @param generatorAliases Aliases like `photonjs` -> `node_modules/photonjs/gen.js`
* @param aliases Aliases like `photonjs` -> `node_modules/photonjs/gen.js`
* @param version Version of the binary, commit hash of https://github.com/prisma/prisma-engine/commits/master
* @param printDownloadProgress `boolean` to print download progress or not
*/
export async function getGenerator(
schemaPath: string,
generatorAliases?: { [alias: string]: string },
version?: string,
printDownloadProgress?: boolean,
options: GetGeneratorOptions,
): Promise<Generator> {
const generators = await getGenerators(
schemaPath,
generatorAliases,
version,
printDownloadProgress,
)
const generators = await getGenerators(options)
return generators[0]
}

Expand Down
51 changes: 51 additions & 0 deletions cli/sdk/src/resolveOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'fs'
import path from 'path'
import { promisify } from 'util'
const exists = promisify(fs.exists)

async function resolveNodeModulesBase(cwd: string) {
if (await exists(path.resolve(process.cwd(), 'prisma/schema.prisma'))) {
return process.cwd()
}
if (
path.relative(process.cwd(), cwd) === 'prisma' &&
(await exists(path.resolve(process.cwd(), 'package.json')))
) {
return process.cwd()
}
if (await exists(path.resolve(cwd, 'node_modules'))) {
return cwd
}
if (await exists(path.resolve(cwd, '../node_modules'))) {
return path.join(cwd, '../')
}
if (await exists(path.resolve(cwd, 'package.json'))) {
return cwd
}
if (await exists(path.resolve(cwd, '../package.json'))) {
return path.join(cwd, '../')
}
return cwd
}

export type ResolveOutputOptions = {
defaultOutput: string
baseDir: string // normally `schemaDir`, the dir containing the schema.prisma file
}

export async function resolveOutput(options: ResolveOutputOptions) {
const defaultOutput = stripRelativePath(options.defaultOutput)
if (defaultOutput.startsWith('node_modules')) {
const nodeModulesBase = await resolveNodeModulesBase(options.defaultOutput)
return path.resolve(nodeModulesBase, defaultOutput)
}

return path.resolve(options.baseDir, defaultOutput)
}

function stripRelativePath(pathString: string) {
if (pathString.startsWith('./')) {
return pathString.slice(2)
}
return pathString
}

0 comments on commit e363e74

Please sign in to comment.