// @ts-check const childProcess = require('child_process') const { promisify } = require('util') const fs = require('fs') const path = require('path') const c = require('./colors') const exec = promisify(childProcess.exec) const copyFile = promisify(fs.copyFile) const mkdir = promisify(fs.mkdir) const stat = promisify(fs.stat) function debug(message, ...optionalParams) { if (process.env.DEBUG && process.env.DEBUG === 'prisma:postinstall') { console.log(message, ...optionalParams) } } /** * Adds `package.json` to the end of a path if it doesn't already exist' * @param {string} pth */ function addPackageJSON(pth) { if (pth.endsWith('package.json')) return pth return path.join(pth, 'package.json') } /** * Looks up for a `package.json` which is not `@prisma/cli` or `prisma` and returns the directory of the package * @param {string} startPath - Path to Start At * @param {number} limit - Find Up limit * @returns {string | null} */ function findPackageRoot(startPath, limit = 10) { if (!startPath || !fs.existsSync(startPath)) return null let currentPath = startPath // Limit traversal for (let i = 0; i < limit; i++) { const pkgPath = addPackageJSON(currentPath) if (fs.existsSync(pkgPath)) { try { const pkg = require(pkgPath) if (pkg.name && !['@prisma/cli', 'prisma'].includes(pkg.name)) { return pkgPath.replace('package.json', '') } } catch {} // eslint-disable-line no-empty } currentPath = path.join(currentPath, '../') } return null } /** * The `postinstall` hook of client sets up the ground and env vars for the `prisma generate` command, * and runs it, showing a warning if the schema is not found. * - initializes the ./node_modules/.prisma/client folder with the default index(-browser).js/index.d.ts, * which define a `PrismaClient` class stub that throws an error if instantiated before the `prisma generate` * command is successfully executed. * - sets the path of the root of the project (TODO: to verify) to the `process.env.PRISMA_GENERATE_IN_POSTINSTALL` * variable, or `'true'` if the project root cannot be found. * - runs `prisma generate`, passing through additional information about the command that triggered the generation, * which is useful for debugging/telemetry. It tries to use the local `prisma` package if it is installed, otherwise it * falls back to the global `prisma` package. If neither options are available, it warns the user to install `prisma` first. */ async function main() { if (process.env.INIT_CWD) { process.chdir(process.env.INIT_CWD) // necessary, because npm chooses __dirname as process.cwd() // in the postinstall hook } await createDefaultGeneratedThrowFiles() // TODO: consider using the `which` package const localPath = getLocalPackagePath() // Only execute if !localpath const installedGlobally = localPath ? undefined : await isInstalledGlobally() // this is needed, so we can find the correct schemas in yarn workspace projects const root = findPackageRoot(localPath) process.env.PRISMA_GENERATE_IN_POSTINSTALL = root ? root : 'true' debug({ localPath, installedGlobally, init_cwd: process.env.INIT_CWD, PRISMA_GENERATE_IN_POSTINSTALL: process.env.PRISMA_GENERATE_IN_POSTINSTALL, }) try { if (localPath) { await run('node', [localPath, 'generate', '--postinstall', doubleQuote(getPostInstallTrigger())]) return } if (installedGlobally) { await run('prisma', ['generate', '--postinstall', doubleQuote(getPostInstallTrigger())]) return } } catch (e) { // if exit code = 1 do not print if (e && e !== 1) { console.error(e) } debug(e) } if (!localPath && !installedGlobally) { console.error( `${c.yellow( 'warning', )} In order to use "@prisma/client", please install Prisma CLI. You can install it with "npm add -D prisma".`, ) } } function getLocalPackagePath() { try { const packagePath = require.resolve('prisma/package.json') if (packagePath) { return require.resolve('prisma') } } catch (e) {} // eslint-disable-line no-empty // TODO: consider removing this try { const packagePath = require.resolve('@prisma/cli/package.json') if (packagePath) { return require.resolve('@prisma/cli') } } catch (e) {} // eslint-disable-line no-empty return null } async function isInstalledGlobally() { try { const result = await exec('prisma -v') if (result.stdout.includes('@prisma/client')) { return true } else { console.error(`${c.yellow('warning')} You still have the ${c.bold('prisma')} cli (Prisma 1) installed globally. Please uninstall it with either ${c.green('npm remove -g prisma')} or ${c.green('yarn global remove prisma')}.`) } } catch (e) { return false } } if (!process.env.PRISMA_SKIP_POSTINSTALL_GENERATE) { main() .catch((e) => { if (e.stderr) { if (e.stderr.includes(`Can't find schema.prisma`)) { console.error( `${c.yellow('warning')} @prisma/client needs a ${c.bold('schema.prisma')} to function, but couldn't find it. Please either create one manually or use ${c.bold('prisma init')}. Once you created it, run ${c.bold('prisma generate')}. To keep Prisma related things separate, we recommend creating it in a subfolder called ${c.underline( './prisma', )} like so: ${c.underline('./prisma/schema.prisma')}\n`, ) } else { console.error(e.stderr) } } else { console.error(e) } process.exit(0) }) .finally(() => { debug(`postinstall trigger: ${getPostInstallTrigger()}`) }) } function run(cmd, params, cwd = process.cwd()) { const child = childProcess.spawn(cmd, params, { stdio: ['pipe', 'inherit', 'inherit'], cwd, }) return new Promise((resolve, reject) => { child.on('close', () => { resolve() }) child.on('exit', (code) => { if (code === 0) { resolve() } else { reject(code) } }) child.on('error', () => { reject() }) }) } /** * Copies our default "throw" files into the default generation folder. These * files are dummy and informative because they just throw an error to let the * user know that they have forgotten to run `prisma generate` or that they * don't have a a schema file yet. We only add these files at the default * location `node_modules/.prisma/client`. */ async function createDefaultGeneratedThrowFiles() { try { const dotPrismaClientDir = path.join(__dirname, '../../../.prisma/client') const defaultNodeIndexPath = path.join(dotPrismaClientDir, 'index.js') const defaultNodeIndexDtsPath = path.join(dotPrismaClientDir, 'index.d.ts') const defaultBrowserIndexPath = path.join(dotPrismaClientDir, 'index-browser.js') const defaultEdgeIndexPath = path.join(dotPrismaClientDir, 'edge.js') const defaultEdgeIndexDtsPath = path.join(dotPrismaClientDir, 'edge.d.ts') const defaultDenoClientDir = path.join(dotPrismaClientDir, 'deno') const defaultDenoEdgeIndexPath = path.join(defaultDenoClientDir, 'edge.ts') await makeDir(dotPrismaClientDir) await makeDir(defaultDenoClientDir) if (!fs.existsSync(defaultNodeIndexPath)) { await copyFile(path.join(__dirname, 'default-index.js'), defaultNodeIndexPath) } if (!fs.existsSync(defaultBrowserIndexPath)) { await copyFile(path.join(__dirname, 'default-index-browser.js'), defaultBrowserIndexPath) } if (!fs.existsSync(defaultNodeIndexDtsPath)) { await copyFile(path.join(__dirname, 'default-index.d.ts'), defaultNodeIndexDtsPath) } if (!fs.existsSync(defaultEdgeIndexPath)) { await copyFile(path.join(__dirname, 'default-edge.js'), defaultEdgeIndexPath) } if (!fs.existsSync(defaultEdgeIndexDtsPath)) { await copyFile(path.join(__dirname, 'default-index.d.ts'), defaultEdgeIndexDtsPath) } if (!fs.existsSync(defaultDenoEdgeIndexPath)) { await copyFile(path.join(__dirname, 'default-deno-edge.ts'), defaultDenoEdgeIndexPath) } } catch (e) { console.error(e) } } // TODO: can this be replaced some utility eg. mkdir function makeDir(input) { const make = async (pth) => { try { await mkdir(pth) return pth } catch (error) { if (error.code === 'EPERM') { throw error } if (error.code === 'ENOENT') { if (path.dirname(pth) === pth) { throw new Error(`operation not permitted, mkdir '${pth}'`) } if (error.message.includes('null bytes')) { throw error } await make(path.dirname(pth)) return make(pth) } try { const stats = await stat(pth) if (!stats.isDirectory()) { throw new Error('The path is not a directory') } } catch (_) { throw error } return pth } } return make(path.resolve(input)) } /** * Get the command that triggered this postinstall script being run. If there is * an error while attempting to get this value then the string constant * 'ERROR_WHILE_FINDING_POSTINSTALL_TRIGGER' is returned. * This information is just necessary for telemetry. * This is passed to `prisma generate` as a string like `--postinstall value`. */ function getPostInstallTrigger() { /* npm_config_argv` is not officially documented so here are our research notes `npm_config_argv` is available to the postinstall script when the containing package has been installed by npm into some project. An example of its value: ``` npm_config_argv: '{"remain":["../test"],"cooked":["add","../test"],"original":["add","../test"]}', ``` We are interesting in the data contained in the "original" field. Trivia/Note: `npm_config_argv` is not available when running e.g. `npm install` on the containing package itself (e.g. when working on it) Yarn mimics this data and environment variable. Here is an example following `yarn add` for the same package: ``` npm_config_argv: '{"remain":[],"cooked":["add"],"original":["add","../test"]}' ``` Other package managers like `pnpm` have not been tested. */ const maybe_npm_config_argv_string = process.env.npm_config_argv if (maybe_npm_config_argv_string === undefined) { return UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING } let npm_config_argv try { npm_config_argv = JSON.parse(maybe_npm_config_argv_string) } catch (e) { return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR}: ${maybe_npm_config_argv_string}` } if (typeof npm_config_argv !== 'object' || npm_config_argv === null) { return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR}: ${maybe_npm_config_argv_string}` } const npm_config_argv_original_arr = npm_config_argv.original if (!Array.isArray(npm_config_argv_original_arr)) { return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR}: ${maybe_npm_config_argv_string}` } const npm_config_argv_original = npm_config_argv_original_arr.filter((arg) => arg !== '').join(' ') const command = npm_config_argv_original === '' ? getPackageManagerName() : [getPackageManagerName(), npm_config_argv_original].join(' ') return command } /** * Wrap double quotes around the given string. */ function doubleQuote(x) { return `"${x}"` } /** * Get the package manager name currently being used. If parsing fails, then the following pattern is returned: * UNKNOWN_NPM_CONFIG_USER_AGENT(). */ function getPackageManagerName() { const userAgent = process.env.npm_config_user_agent if (!userAgent) return 'MISSING_NPM_CONFIG_USER_AGENT' const name = parsePackageManagerName(userAgent) if (!name) return `UNKNOWN_NPM_CONFIG_USER_AGENT(${userAgent})` return name } /** * Parse package manager name from useragent. If parsing fails, `null` is returned. */ function parsePackageManagerName(userAgent) { let packageManager = null // example: 'yarn/1.22.4 npm/? node/v13.11.0 darwin x64' // References: // - https://pnpm.io/only-allow-pnpm // - https://github.com/cameronhunter/npm-config-user-agent-parser if (userAgent) { const matchResult = userAgent.match(/^([^/]+)\/.+/) if (matchResult) { packageManager = matchResult[1].trim() } } return packageManager } // prettier-ignore const UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING' // prettier-ignore const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR' // prettier-ignore const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR' // expose for testing exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING = UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR = UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR = UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR exports.getPostInstallTrigger = getPostInstallTrigger