UNPKG

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