UNPKG

11.4 kBJavaScriptView Raw
1// @ts-check
2const childProcess = require('child_process')
3const { promisify } = require('util')
4const fs = require('fs')
5const path = require('path')
6const c = require('./colors')
7
8const exec = promisify(childProcess.exec)
9const copyFile = promisify(fs.copyFile)
10const mkdir = promisify(fs.mkdir)
11const stat = promisify(fs.stat)
12
13function debug(message, ...optionalParams) {
14 if (process.env.DEBUG && process.env.DEBUG === 'prisma:postinstall') {
15 console.log(message, ...optionalParams)
16 }
17}
18/**
19 * Adds `package.json` to the end of a path if it doesn't already exist'
20 * @param {string} pth
21 */
22function addPackageJSON(pth) {
23 if (pth.endsWith('package.json')) return pth
24 return path.join(pth, 'package.json')
25}
26
27/**
28 * Looks up for a `package.json` which is not `@prisma/cli` or `prisma` and returns the directory of the package
29 * @param {string} startPath - Path to Start At
30 * @param {number} limit - Find Up limit
31 * @returns {string | null}
32 */
33function findPackageRoot(startPath, limit = 10) {
34 if (!startPath || !fs.existsSync(startPath)) return null
35 let currentPath = startPath
36 // Limit traversal
37 for (let i = 0; i < limit; i++) {
38 const pkgPath = addPackageJSON(currentPath)
39 if (fs.existsSync(pkgPath)) {
40 try {
41 const pkg = require(pkgPath)
42 if (pkg.name && !['@prisma/cli', 'prisma'].includes(pkg.name)) {
43 return pkgPath.replace('package.json', '')
44 }
45 } catch {}
46 }
47 currentPath = path.join(currentPath, '../')
48 }
49 return null
50}
51
52async function main() {
53 if (process.env.INIT_CWD) {
54 process.chdir(process.env.INIT_CWD) // necessary, because npm chooses __dirname as process.cwd()
55 // in the postinstall hook
56 }
57 await ensureEmptyDotPrisma()
58
59 const localPath = getLocalPackagePath()
60 // Only execute if !localpath
61 const installedGlobally = localPath ? undefined : await isInstalledGlobally()
62
63 // this is needed, so that the Generate command does not fail in postinstall
64
65 process.env.PRISMA_GENERATE_IN_POSTINSTALL = 'true'
66
67 // this is needed, so we can find the correct schemas in yarn workspace projects
68 const root = findPackageRoot(localPath)
69
70 process.env.PRISMA_GENERATE_IN_POSTINSTALL = root ? root : 'true'
71
72 debug({
73 localPath,
74 installedGlobally,
75 init_cwd: process.env.INIT_CWD,
76 PRISMA_GENERATE_IN_POSTINSTALL: process.env.PRISMA_GENERATE_IN_POSTINSTALL,
77 })
78 try {
79 if (localPath) {
80 await run('node', [
81 localPath,
82 'generate',
83 '--postinstall',
84 doubleQuote(getPostInstallTrigger()),
85 ])
86 return
87 }
88 if (installedGlobally) {
89 await run('prisma', [
90 'generate',
91 '--postinstall',
92 doubleQuote(getPostInstallTrigger()),
93 ])
94 return
95 }
96 } catch (e) {
97 // if exit code = 1 do not print
98 if (e && e !== 1) {
99 console.error(e)
100 }
101 debug(e)
102 }
103
104 if (!localPath && !installedGlobally) {
105 console.error(
106 `${c.yellow(
107 'warning',
108 )} In order to use "@prisma/client", please install Prisma CLI. You can install it with "npm add -D prisma".`,
109 )
110 }
111}
112
113function getLocalPackagePath() {
114 try {
115 const packagePath = require.resolve('prisma/package.json')
116 if (packagePath) {
117 return require.resolve('prisma')
118 }
119 } catch (e) {
120 //
121 }
122
123 try {
124 const packagePath = require.resolve('@prisma/cli/package.json')
125 if (packagePath) {
126 return require.resolve('@prisma/cli')
127 }
128 } catch (e) {}
129
130 return null
131}
132
133async function isInstalledGlobally() {
134 try {
135 const result = await exec('prisma -v')
136 if (result.stdout.includes('@prisma/client')) {
137 return true
138 } else {
139 console.error(`${c.yellow('warning')} You still have the ${c.bold(
140 'prisma',
141 )} cli (Prisma 1) installed globally.
142Please uninstall it with either ${c.green('npm remove -g prisma')} or ${c.green(
143 'yarn global remove prisma',
144 )}.`)
145 }
146 } catch (e) {
147 return false
148 }
149}
150
151if (!process.env.PRISMA_SKIP_POSTINSTALL_GENERATE) {
152 main()
153 .catch((e) => {
154 if (e.stderr) {
155 if (e.stderr.includes(`Can't find schema.prisma`)) {
156 console.error(
157 `${c.yellow('warning')} @prisma/client needs a ${c.bold(
158 'schema.prisma',
159 )} to function, but couldn't find it.
160 Please either create one manually or use ${c.bold('prisma init')}.
161 Once you created it, run ${c.bold('prisma generate')}.
162 To keep Prisma related things separate, we recommend creating it in a subfolder called ${c.underline(
163 './prisma',
164 )} like so: ${c.underline('./prisma/schema.prisma')}\n`,
165 )
166 } else {
167 console.error(e.stderr)
168 }
169 } else {
170 console.error(e)
171 }
172 process.exit(0)
173 })
174 .finally(() => {
175 debug(`postinstall trigger: ${getPostInstallTrigger()}`)
176 })
177}
178
179function run(cmd, params, cwd = process.cwd()) {
180 const child = childProcess.spawn(cmd, params, {
181 stdio: ['pipe', 'inherit', 'inherit'],
182 cwd,
183 })
184
185 return new Promise((resolve, reject) => {
186 child.on('close', () => {
187 resolve()
188 })
189 child.on('exit', (code) => {
190 if (code === 0) {
191 resolve()
192 } else {
193 reject(code)
194 }
195 })
196 child.on('error', () => {
197 reject()
198 })
199 })
200}
201
202async function ensureEmptyDotPrisma() {
203 try {
204 const dotPrismaClientDir = path.join(__dirname, '../../../.prisma/client')
205 await makeDir(dotPrismaClientDir)
206 const defaultIndexJsPath = path.join(dotPrismaClientDir, 'index.js')
207 const defaultIndexBrowserJSPath = path.join(
208 dotPrismaClientDir,
209 'index-browser.js',
210 )
211 const defaultIndexDTSPath = path.join(dotPrismaClientDir, 'index.d.ts')
212
213 if (!fs.existsSync(defaultIndexJsPath)) {
214 await copyFile(
215 path.join(__dirname, 'default-index.js'),
216 defaultIndexJsPath,
217 )
218 }
219 if (!fs.existsSync(defaultIndexBrowserJSPath)) {
220 await copyFile(
221 path.join(__dirname, 'default-index-browser.js'),
222 defaultIndexBrowserJSPath,
223 )
224 }
225
226 if (!fs.existsSync(defaultIndexDTSPath)) {
227 await copyFile(
228 path.join(__dirname, 'default-index.d.ts'),
229 defaultIndexDTSPath,
230 )
231 }
232 } catch (e) {
233 console.error(e)
234 }
235}
236
237async function makeDir(input) {
238 const make = async (pth) => {
239 try {
240 await mkdir(pth)
241
242 return pth
243 } catch (error) {
244 if (error.code === 'EPERM') {
245 throw error
246 }
247
248 if (error.code === 'ENOENT') {
249 if (path.dirname(pth) === pth) {
250 throw new Error(`operation not permitted, mkdir '${pth}'`)
251 }
252
253 if (error.message.includes('null bytes')) {
254 throw error
255 }
256
257 await make(path.dirname(pth))
258
259 return make(pth)
260 }
261
262 try {
263 const stats = await stat(pth)
264 if (!stats.isDirectory()) {
265 throw new Error('The path is not a directory')
266 }
267 } catch (_) {
268 throw error
269 }
270
271 return pth
272 }
273 }
274
275 return make(path.resolve(input))
276}
277
278/**
279 * Get the command that triggered this postinstall script being run. If there is
280 * an error while attempting to get this value then the string constant
281 * 'ERROR_WHILE_FINDING_POSTINSTALL_TRIGGER' is returned.
282 * This information is just necessary for telemetry.
283 * This get's passed in to Generate, which then automatically get's propagated to telemetry.
284 */
285function getPostInstallTrigger() {
286 /*
287 npm_config_argv` is not officially documented so here are our research notes
288
289 `npm_config_argv` is available to the postinstall script when the containing package has been installed by npm into some project.
290
291 An example of its value:
292
293 ```
294 npm_config_argv: '{"remain":["../test"],"cooked":["add","../test"],"original":["add","../test"]}',
295 ```
296
297 We are interesting in the data contained in the "original" field.
298
299 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)
300
301 Yarn mimics this data and environment variable. Here is an example following `yarn add` for the same package:
302
303 ```
304 npm_config_argv: '{"remain":[],"cooked":["add"],"original":["add","../test"]}'
305 ```
306
307 Other package managers like `pnpm` have not been tested.
308 */
309
310 const maybe_npm_config_argv_string = process.env.npm_config_argv
311
312 if (maybe_npm_config_argv_string === undefined) {
313 return UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING
314 }
315
316 let npm_config_argv
317 try {
318 npm_config_argv = JSON.parse(maybe_npm_config_argv_string)
319 } catch (e) {
320 return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR}: ${maybe_npm_config_argv_string}`
321 }
322
323 if (typeof npm_config_argv !== 'object' || npm_config_argv === null) {
324 return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR}: ${maybe_npm_config_argv_string}`
325 }
326
327 const npm_config_arv_original_arr = npm_config_argv.original
328
329 if (!Array.isArray(npm_config_arv_original_arr)) {
330 return `${UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR}: ${maybe_npm_config_argv_string}`
331 }
332
333 const npm_config_arv_original = npm_config_arv_original_arr
334 .filter((arg) => arg !== '')
335 .join(' ')
336
337 const command =
338 npm_config_arv_original === ''
339 ? getPackageManagerName()
340 : [getPackageManagerName(), npm_config_arv_original].join(' ')
341
342 return command
343}
344
345/**
346 * Wrap double quotes around the given string.
347 */
348function doubleQuote(x) {
349 return `"${x}"`
350}
351
352/**
353 * Get the package manager name currently being used. If parsing fails, then the following pattern is returned:
354 * UNKNOWN_NPM_CONFIG_USER_AGENT(<string received>).
355 */
356function getPackageManagerName() {
357 const userAgent = process.env.npm_config_user_agent
358 if (!userAgent) return 'MISSING_NPM_CONFIG_USER_AGENT'
359
360 const name = parsePackageManagerName(userAgent)
361 if (!name) return `UNKNOWN_NPM_CONFIG_USER_AGENT(${userAgent})`
362
363 return name
364}
365
366/**
367 * Parse package manager name from useragent. If parsing fails, `null` is returned.
368 */
369function parsePackageManagerName(userAgent) {
370 let packageManager = null
371
372 // example: 'yarn/1.22.4 npm/? node/v13.11.0 darwin x64'
373 // References:
374 // - https://pnpm.js.org/en/3.6/only-allow-pnpm
375 // - https://github.com/cameronhunter/npm-config-user-agent-parser
376 if (userAgent) {
377 const matchResult = userAgent.match(/^([^\/]+)\/.+/)
378 if (matchResult) {
379 packageManager = matchResult[1].trim()
380 }
381 }
382
383 return packageManager
384}
385
386// prettier-ignore
387const UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING'
388// prettier-ignore
389const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR'
390// prettier-ignore
391const UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR = 'UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR'
392
393// expose for testing
394
395exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING = UNABLE_TO_FIND_POSTINSTALL_TRIGGER__ENVAR_MISSING
396exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR = UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_PARSE_ERROR
397exports.UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR = UNABLE_TO_FIND_POSTINSTALL_TRIGGER_JSON_SCHEMA_ERROR
398exports.getPostInstallTrigger = getPostInstallTrigger