@exodus/stasis 1.0.0-beta.5 → 1.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,439 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { createRequire } from 'node:module'
3
+ import { posix } from 'node:path'
4
+ import { compileFunction } from 'node:vm'
5
+
6
+ import { readJson } from '@exodus/stasis-core/bundle-util'
7
+ import { hasNodeModulesSegment } from '@exodus/stasis-core/util'
8
+ import { createVfs } from '@preventive/vfs'
9
+ import { isDir, isFile, loadTsconfigCompilerOptions } from '../resolve-typescript.js'
10
+ import { getParser, literalSpec, syntaxErrors } from '../scan.js'
11
+ import { holding, nearest, projectView } from './tree.js'
12
+
13
+ // buildVfsBundle's `generate: ['prisma']`: the client each project's `prisma generate` would write
14
+ // with its installed Prisma, without running anything of the repo: schema, config, tsconfig.json
15
+ // and package.json are read from the tree as data. The optional peer GENERATOR builds the client in
16
+ // memory; an older version's is 7.10.0's rewritten (DOWN), tested byte for byte against its own.
17
+
18
+ const GENERATOR = '@prisma/client-generator-ts'
19
+ const GENERATOR_VERSION = '7.10.0'
20
+ const PROVIDER = 'prisma-client'
21
+ const require = createRequire(import.meta.url)
22
+
23
+ // The engine hash each Prisma stamps its client with, newest first.
24
+ const ENGINES = {
25
+ '7.10.0': '0edf323efd1d98336f3f0a68684b56f689b900d3',
26
+ '7.9.1': 'e922089b7d7502aff4249d5da3420f6fa55fc6ad',
27
+ '7.9.0': 'e922089b7d7502aff4249d5da3420f6fa55fc6ad',
28
+ '7.8.0': '3c6e192761c0362d496ed980de936e2f3cebcd3a',
29
+ '7.7.0': '75cbdc1eb7150937890ad5465d861175c6624711',
30
+ '7.6.0': '75cbdc1eb7150937890ad5465d861175c6624711',
31
+ '7.5.0': '280c870be64f457428992c43c1f6d557fab6e29e',
32
+ '7.4.2': '94a226be1cf2967af2541cca5529f0f7ba866919',
33
+ '7.4.1': '55ae170b1ced7fc6ed07a15f110549408c501bb3',
34
+ '7.4.0': 'ab56fe763f921d033a6c195e7ddeb3e255bdbb57',
35
+ }
36
+ export const PRISMA_VERSIONS = Object.keys(ENGINES)
37
+ const newerThan = (version, than) => PRISMA_VERSIONS.indexOf(version) < PRISMA_VERSIONS.indexOf(than)
38
+
39
+ // --- the generator ---
40
+
41
+ // The bundle's readSourceFile, for the client `at` describes: an edge runtime's client copies the
42
+ // query compiler from `prisma generate`'s own build, which here is the tree's `prisma`.
43
+ const readQueryCompiler = (at) => (name) => {
44
+ const file = posix.join(at.prismaDir, 'build', name)
45
+ if (!isFile(file, at.host)) throw new Error(`prisma: ${file}, which a ${at.target} client carries, is not in the tree`)
46
+ return Buffer.from(at.host.readFile(file))
47
+ }
48
+
49
+ // The bundle's inferModuleFormatFromNearestPackageJson, for node16 and nodenext; ESM before 7.10.0.
50
+ const packageFormat = ({ host, version }) => (dir) => {
51
+ if (!newerThan(version, '7.9.1')) return 'esm'
52
+ const file = nearestFile(host, dir, 'package.json')
53
+ try { return file !== null && JSON.parse(host.readFile(file).toString('utf8')).type === 'module' ? 'esm' : 'cjs' } catch { return 'cjs' }
54
+ }
55
+
56
+ // The esbuild bundle at `main` as a function of `parameters` returning `roots`, kept to the top-level
57
+ // statements they reach by name (any word of a statement: more is kept than used, never less), and
58
+ // refused where it then requires anything `given` doesn't hold, or by no literal name.
59
+ function compiled(main, roots, parameters, given) {
60
+ const source = readFileSync(main, 'utf8')
61
+ const { body } = getParser().parseSync(main, source, { sourceType: 'script' }).program
62
+ const declaring = new Map(body.flatMap((node) => (node.declarations ?? [node]).flatMap(({ id }) => (id?.name ? [[id.name, node]] : []))))
63
+ const kept = new Set()
64
+ const keep = (name) => {
65
+ const node = declaring.get(name)
66
+ if (node === undefined || parameters.includes(name) || kept.has(node)) return
67
+ kept.add(node)
68
+ for (const [word] of source.slice(node.start, node.end).matchAll(/[\w$]+/gu)) keep(word)
69
+ }
70
+ roots.forEach(keep)
71
+ const reached = body.filter((node) => node.directive || kept.has(node)).map((node) => source.slice(node.start, node.end)).join('\n')
72
+ const denied = [...reached.matchAll(/\brequire\b(?:\("([^"]*)"\))?/gu)].filter(([, id]) => !Object.hasOwn(given, id)).map(([call]) => call)
73
+ if (denied.length > 0) throw new Error(`--generate=prisma: ${main}, stripped, holds ${[...new Set(denied)].join(', ')}, beyond what stasis lets it require`)
74
+ return compileFunction(`${reached}\nreturn { ${roots.join(', ')} }`, ['require', ...parameters], { filename: main }).bind(null, (id) => given[id])
75
+ }
76
+
77
+ // CommonJS files from `main` on, compiled requiring nothing outside their package but what `given`
78
+ // holds (none reads __filename or __dirname).
79
+ function commonjs(main, given) {
80
+ const modules = new Map()
81
+ const load = (file) => {
82
+ if (modules.has(file)) return modules.get(file).exports
83
+ const module = { exports: {} }
84
+ modules.set(file, module)
85
+ const requiring = (id) => {
86
+ if (id.startsWith('.')) return load(createRequire(file).resolve(id))
87
+ if (!Object.hasOwn(given, id)) throw new Error(`--generate=prisma: ${file} requires ${id}, which stasis doesn't let it`)
88
+ return given[id]
89
+ }
90
+ compileFunction(readFileSync(file, 'utf8'), ['exports', 'require', 'module'], { filename: file })(module.exports, requiring, module)
91
+ return module.exports
92
+ }
93
+ return load(main)
94
+ }
95
+
96
+ // The schema engine's refusals are JSON with a `message`; its panics aren't.
97
+ const engineMessage = (message) => { try { return JSON.parse(message).message ?? message } catch { return message } }
98
+
99
+ // What the compiled generator returns, is given, and requires as Node loads it (none of it the
100
+ // disk's); given, as readSourceFile and inferModuleFormatFromNearestPackageJson read the disk,
101
+ // `debug` would require @prisma/debug, and buildTypedSql is undefined: stasis passes no typedSql.
102
+ const EXPORTS = ['buildClient', 'validateDmmfAgainstDenylists', 'parseRuntimeTargetFromUnknown', 'parseGeneratedFileExtension', 'parseImportFileExtension', 'inferImportFileExtension', 'parseModuleFormatFromUnknown', 'inferModuleFormat', 'parseCompilerBuildFromUnknown']
103
+ const PARAMETERS = ['readSourceFile', 'inferModuleFormatFromNearestPackageJson', 'debug', 'buildTypedSql']
104
+ const REQUIRES = ['@prisma/client-common', '@prisma/dmmf', '@prisma/param-graph-builder', 'indent-string', 'klona', 'pluralize', 'ts-pattern']
105
+ let loaded
106
+
107
+ // The peer exports neither buildClient (generateClient's in-memory half), the reserved-names check
108
+ // generateClient runs, nor its generate()'s option parsing and inference, so its bundle is compiled
109
+ // to return them, stripped (cleanup, not a sandbox) of what they never reach. @prisma/internals is
110
+ // never loaded: loading it reads the host's npm and yarn config and platform, and patches fs and
111
+ // process.cwd. Its users get a stand-in: its parsing the schema engine's and @prisma/get-dmmf's,
112
+ // which it wraps, and its isValidJsIdentifier its own file's, whose Unicode tables aren't Node's.
113
+ function loadGenerator() {
114
+ if (loaded) return loaded
115
+ let main
116
+ try { main = require.resolve(GENERATOR) } catch (cause) { throw new Error(`--generate=prisma needs the optional '${GENERATOR}' ${GENERATOR_VERSION} dependency; install it (e.g. \`npm i ${GENERATOR}@${GENERATOR_VERSION}\`)`, { cause }) }
117
+ const own = createRequire(main)
118
+ const { version } = own('../package.json')
119
+ if (version !== GENERATOR_VERSION) throw new Error(`--generate=prisma needs ${GENERATOR} ${GENERATOR_VERSION}, not ${version}`)
120
+ // Prisma's packages beside the @prisma/internals the peer was built with.
121
+ const near = createRequire(own.resolve('@prisma/internals'))
122
+ const engine = near('@prisma/prisma-schema-wasm')
123
+ const getDmmf = near('@prisma/get-dmmf')
124
+ const internals = {
125
+ externalToInternalDmmf: getDmmf.externalToInternalDmmf,
126
+ hasOwnProperty: Object.hasOwn,
127
+ assertNever: (value, message) => { throw new Error(message) },
128
+ isValidJsIdentifier: commonjs(own.resolve('@prisma/internals/dist/utils/isValidJsIdentifier.js'), {}).isValidJsIdentifier,
129
+ // But for resolving binaryTargets, which the generator never reads.
130
+ getConfig: ({ datamodel }) => {
131
+ const { config, errors } = JSON.parse(engine.get_config(JSON.stringify({ prismaSchema: datamodel })))
132
+ if (errors.length > 0) throw new Error(errors.map((error) => error.message).join('\n'))
133
+ return config
134
+ },
135
+ getDMMF: (options) => {
136
+ const result = getDmmf.getDMMF(options)
137
+ if ('error' in result) throw new Error(engineMessage(result.error.message), { cause: result.error })
138
+ return result
139
+ },
140
+ mergeSchemas: ({ schemas }) => engine.merge_schemas(JSON.stringify({ schema: schemas })),
141
+ }
142
+ const given = { ...Object.fromEntries(REQUIRES.map((id) => [id, own(id)])), '@prisma/internals': internals, '@prisma/ts-builders': commonjs(own.resolve('@prisma/ts-builders'), { '@prisma/internals': internals }) }
143
+ const run = compiled(main, EXPORTS, PARAMETERS, given)
144
+ const { loadSchemaFiles } = compiled(near.resolve('@prisma/schema-files-loader'), ['loadSchemaFiles'], ['realFsResolver'], { 'node:path': posix })()
145
+ // `peer` is evaluated for each client (a millisecond), its functions given what they read.
146
+ loaded = { peer: (at) => run(readQueryCompiler(at), packageFormat(at), () => {}), internals, loadSchemaFiles }
147
+ return loaded
148
+ }
149
+
150
+ // --- what `prisma generate` reads ---
151
+
152
+ const nearestFile = (host, dir, name) => {
153
+ const at = nearest(dir, holding(host, name))
154
+ return at === null ? null : posix.join(at, name)
155
+ }
156
+
157
+ // The `prisma` Node resolves from `dir`, or where that is unsupported (Prisma 8, say), the one
158
+ // `@prisma/prisma7` runs as `prisma7` beside it.
159
+ function installedPrisma(host, dir) {
160
+ const installed = (from) => {
161
+ const file = nearestFile(host, from, 'node_modules/prisma/package.json')
162
+ return file === null ? null : { file, version: readJson(file, host)?.version }
163
+ }
164
+ const prisma = installed(dir)
165
+ const prisma7 = PRISMA_VERSIONS.includes(prisma?.version) ? null : nearestFile(host, dir, 'node_modules/@prisma/prisma7/package.json')
166
+ const beside = prisma7 && installed(host.realpath(posix.dirname(prisma7)))
167
+ return PRISMA_VERSIONS.includes(beside?.version) ? beside : prisma
168
+ }
169
+
170
+ // @prisma/config's search order; only 7.10.0 reads the first two (prisma7.config.*).
171
+ const CONFIG_BASES = ['prisma7.config', '.config/prisma7', ...['prisma.config', '.config/prisma', '.config/prisma.config'].flatMap((base) => [base, `${base}/index`])]
172
+
173
+ // The modules a config's top-level imports and requires may bind, as `name.export` (`name.*`: the module).
174
+ const MODULES = { path: 'path', 'node:path': 'path', 'prisma/config': 'config', '@prisma/config': 'config' }
175
+
176
+ // The `schema` a Prisma config names, read from its syntax tree: the config is never run, nor
177
+ // anything built from it. A string literal or path.join of them is taken, through defineConfig,
178
+ // `as`, `satisfies`, parentheses and top-level consts; anything else is refused.
179
+ function configSchema(file, text) {
180
+ const parsed = getParser().parseSync(file, text, { sourceType: 'unambiguous' })
181
+ const [error] = syntaxErrors(parsed)
182
+ if (error) throw new Error(`${file}: ${error.message}`)
183
+ const refuse = (what) => { throw new Error(`${file}: ${what}, which stasis reads without running the config`) }
184
+ const consts = new Map()
185
+ const bound = new Map()
186
+ const bind = (from, name, local) => bound.set(local, `${MODULES[from]}.${name}`)
187
+ let config
188
+ for (const node of parsed.program.body) {
189
+ if (node.type === 'ExportDefaultDeclaration') config = node.declaration
190
+ else if (node.type === 'ImportDeclaration') {
191
+ for (const { type, imported, local } of node.specifiers) bind(node.source.value, type === 'ImportSpecifier' ? imported.name ?? imported.value : '*', local.name)
192
+ } else if (node.type === 'VariableDeclaration' && node.kind === 'const') {
193
+ for (const { id, init } of node.declarations) {
194
+ const from = init?.type === 'CallExpression' && init.callee.name === 'require' && init.arguments.length === 1 ? literalSpec(init.arguments[0]) : null
195
+ if (id.type === 'Identifier' && init) consts.set(id.name, init)
196
+ if (from !== null && id.type === 'Identifier') bind(from, '*', id.name)
197
+ if (from !== null && id.type === 'ObjectPattern') for (const { type, computed, key, value } of id.properties) if (type === 'Property' && !computed && value.type === 'Identifier') bind(from, key.name, value.name)
198
+ }
199
+ } else if (node.type === 'ExpressionStatement' && node.expression.type === 'AssignmentExpression') {
200
+ const { left, right } = node.expression
201
+ if (left.type === 'MemberExpression' && left.object.name === 'module' && left.property.name === 'exports') config = right
202
+ }
203
+ }
204
+ if (config === undefined) refuse('no default export')
205
+ // Each const once, against cycles.
206
+ const peel = (node, seen = new Set()) => {
207
+ if (['TSAsExpression', 'TSSatisfiesExpression', 'ParenthesizedExpression'].includes(node.type)) return peel(node.expression, seen)
208
+ return node.type === 'Identifier' && consts.has(node.name) && !seen.has(node.name) ? peel(consts.get(node.name), seen.add(node.name)) : node
209
+ }
210
+ const called = ({ type, callee }) => type === 'CallExpression' && (callee.type === 'MemberExpression' && !callee.computed && bound.get(callee.object.name) === 'path.*' ? `path.${callee.property.name}` : bound.get(callee.name))
211
+ config = peel(config)
212
+ if (called(config) === 'config.defineConfig' && config.arguments.length === 1) config = peel(config.arguments[0])
213
+ if (config.type !== 'ObjectExpression') refuse('its default export is no object literal')
214
+ const spelled = (node) => {
215
+ node = peel(node)
216
+ return literalSpec(node) ?? (called(node) === 'path.join' ? posix.join(...node.arguments.map(spelled)) : refuse('its `schema` is no string literal, nor path.join of them'))
217
+ }
218
+ let schema
219
+ for (const property of config.properties) {
220
+ // A spread or a computed key may name a schema stasis can't see.
221
+ if (property.type !== 'Property' || property.computed) refuse('its config spreads or computes a key')
222
+ if ((property.key.name ?? property.key.value) === 'schema') schema = spelled(property.value)
223
+ }
224
+ return schema
225
+ }
226
+
227
+ // A schema directory is read by Prisma's own loader, over the tree.
228
+ function schemaFilesAt(host, path) {
229
+ if (isFile(path, host)) return [[path, host.readFile(path).toString('utf8')]]
230
+ const view = projectView(host, '/')
231
+ return loadGenerator().loadSchemaFiles(path, {
232
+ listDirContents: async (dir) => view.readdir(dir),
233
+ getEntryType: async (at) => ({ kind: view.lstat(at).type, realPath: host.realpath(at) }),
234
+ getFileContents: async (file) => host.readFile(file).toString('utf8'),
235
+ })
236
+ }
237
+
238
+ // The schema path `prisma generate` takes at `dir`: the config's `schema` (relative to the config),
239
+ // else ./schema.prisma or ./prisma/schema.prisma; null if none.
240
+ function projectSchema(host, dir, version) {
241
+ const first = (names) => names.map((name) => posix.join(dir, name)).find((file) => isFile(file, host))
242
+ const config = first(CONFIG_BASES.slice(newerThan(version, '7.9.1') ? 0 : 2).flatMap((base) => ['.js', '.ts', '.mjs', '.cjs', '.mts', '.cts'].map((ext) => `${base}${ext}`)))
243
+ const named = config && configSchema(config, host.readFile(config).toString('utf8'))
244
+ const path = named === undefined ? first(['schema.prisma', 'prisma/schema.prisma']) : posix.resolve(posix.dirname(config), named)
245
+ if (path === undefined) return null
246
+ if (!isFile(path, host) && !isDir(path, host)) throw new Error(`${config}: its schema ${named} is no file or directory`)
247
+ return path
248
+ }
249
+
250
+ // --- the generator's options, as its generate() parses and infers them, with its own functions ---
251
+
252
+ // The inference reads the nearest tsconfig.json as get-tsconfig 4.10 gives it, defaults included: an
253
+ // ES2015+ target's module, else preserve's bundler resolution.
254
+ const ES_TARGETS = new Set(['es6', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'es2021', 'es2022', 'es2023', 'es2024', 'esnext'])
255
+ function generatorOptions(peer, host, config, outputDir) {
256
+ const file = nearestFile(host, outputDir, 'tsconfig.json')
257
+ const o = file === null ? undefined : loadTsconfigCompilerOptions(file, host)
258
+ if (o?.target && ES_TARGETS.has(o.target.toLowerCase())) o.module ??= 'es6'
259
+ else if (o?.module && o.module.toLowerCase() === 'preserve') o.moduleResolution ??= 'bundler'
260
+ const tsconfig = o && { compilerOptions: o }
261
+ const target = config.runtime === undefined ? 'nodejs' : peer.parseRuntimeTargetFromUnknown(config.runtime)
262
+ const generatedFileExtension = config.generatedFileExtension === undefined ? 'ts' : peer.parseGeneratedFileExtension(config.generatedFileExtension)
263
+ const importFileExtension = config.importFileExtension === undefined ? peer.inferImportFileExtension({ tsconfig, generatedFileExtension, target }) : peer.parseImportFileExtension(config.importFileExtension)
264
+ const moduleFormat = config.moduleFormat === undefined ? peer.inferModuleFormat({ tsconfig, generatedFileExtension, importFileExtension, outputDir }) : peer.parseModuleFormatFromUnknown(config.moduleFormat)
265
+ const compilerBuild = peer.parseCompilerBuildFromUnknown(config.compilerBuild, target)
266
+ return { target, generatedFileExtension, importFileExtension, moduleFormat, compilerBuild }
267
+ }
268
+
269
+ // --- 7.10.0's client as each older version writes it ---
270
+
271
+ // A missing `from` means the client isn't the one these rewrites were written against: refused unless `optional` (null).
272
+ function swap(text, from, to, { path, all, optional }) {
273
+ if (typeof from === 'string' ? text.includes(from) : from.test(text)) return all ? text.replaceAll(from, () => to) : text.replace(from, () => to)
274
+ if (optional) return null
275
+ throw new Error(`prisma: ${path} of the ${GENERATOR_VERSION} client holds no ${JSON.stringify(String(from).slice(0, 80))}`)
276
+ }
277
+
278
+ // File stems, without the generated extension (MODELS: each of models/).
279
+ const CLASS = 'internal/class'
280
+ const NAMESPACE = 'internal/prismaNamespace'
281
+ const MODELS = 'models/*'
282
+
283
+ // 7.10.0 adds the `schema` of each model to the runtime data model.
284
+ function dropModelSchemas(text, path) {
285
+ const match = /^config\.runtimeDataModel = JSON\.parse\((".*")\)$/mu.exec(text)
286
+ if (match === null) throw new Error(`prisma: ${path} of the ${GENERATOR_VERSION} client holds no runtimeDataModel`)
287
+ const model = JSON.parse(JSON.parse(match[1]))
288
+ // Must round-trip, or the rewrite wouldn't match the generator's own serialization.
289
+ if (JSON.stringify(JSON.stringify(model)) !== match[1]) throw new Error(`prisma: ${path}: the runtimeDataModel does not serialize back as written`)
290
+ for (const each of Object.values(model.models)) delete each.schema
291
+ return text.replace(match[0], () => `config.runtimeDataModel = JSON.parse(${JSON.stringify(JSON.stringify(model))})`)
292
+ }
293
+
294
+ // 7.8.0's PrismaClientOptions, which 7.9.0 splits into interfaces.
295
+ const UNION_OPTIONS = `export type PrismaClientOptions = ({
296
+ /**
297
+ * Instance of a Driver Adapter, e.g., like one provided by \`@prisma/adapter-pg\`.
298
+ */
299
+ adapter: runtime.SqlDriverAdapterFactory
300
+ accelerateUrl?: never
301
+ } | {
302
+ /**
303
+ * Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
304
+ */
305
+ accelerateUrl: string
306
+ adapter?: never
307
+ }) & {`
308
+
309
+ // The constructor example in the docs: with an adapter from 7.4.2, without before.
310
+ const example = (indent) => [`${indent}* const prisma = new PrismaClient({\n${indent}* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })\n${indent}* })\n`, `${indent}* const prisma = new PrismaClient()\n`]
311
+ // Each version's rewrites of the next newer one's client, by file stem; none where only the stamps (STAMPS) differ.
312
+ const DOWN = {
313
+ '7.9.1': { [CLASS]: dropModelSchemas },
314
+ '7.8.0': {
315
+ [CLASS]: (text, path) => {
316
+ text = swap(text, '>(options: Prisma.PrismaClientConstructorArgs<Options>): PrismaClient<', '>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<', { path })
317
+ return swap(text, " in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'],", " in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,", { path })
318
+ },
319
+ // 7.9.0's constructor argument type, its PrismaClientOptions split into interfaces, and its XOR.
320
+ [NAMESPACE]: (text, path) => {
321
+ text = swap(text, /\/\*\*\n \* Resolved type of the argument passed to the `PrismaClient` constructor\.\n[^]*?, PrismaClientOptions>;\n\n/u, '', { path })
322
+ text = swap(text, ' ((Without<T, U> & U) | (Without<U, T> & T)) & object\n', ' (Without<T, U> & U) | (Without<U, T> & T)\n', { path })
323
+ text = swap(text, /\/\*\*\n \* Options common to all variants of `PrismaClientOptions`[^]*?\nexport interface PrismaClientBaseOptions \{/u, UNION_OPTIONS, { path })
324
+ return swap(text, /\n\n\/\*\*\n \* `PrismaClient` options for connecting [^]*?\nexport type PrismaClientOptions = PrismaClientOptionsWithAccelerateUrl \| PrismaClientOptionsWithAdapter/u, '', { path })
325
+ },
326
+ },
327
+ '7.7.0': {
328
+ // MongoDB's transactions take no isolation level.
329
+ [CLASS]: (text, path) => swap(text, '[...P], options?: { maxWait?: number, timeout?: number, ', '[...P], options?: { ', { path, optional: true })
330
+ ?? swap(text, '[...P], options?: { maxWait?: number, timeout?: number })', '[...P])', { path }),
331
+ // 7.8.0's query plan cache option.
332
+ [NAMESPACE]: (text, path) => swap(text, / \/\*\*\n \* Optional maximum size for the query plan cache\.[^]*?\n queryPlanCacheMaxSize\?: number\n/u, '', { path }),
333
+ },
334
+ '7.5.0': { [MODELS]: (text) => text.replace(/^export (type Get(\w+)GroupByPayload<T extends \2GroupByArgs> = )/mu, '$1') },
335
+ '7.4.2': {
336
+ // 7.5.0's distinct doc on each model's FindManyArgs, which 7.4.2 has on FindFirst(OrThrow)Args alone.
337
+ [MODELS]: (text) => text.replace(/^(export type \w+FindManyArgs<.* = \{\n(?:(?!\}\n).*\n)*?) \/\*\*\n \* \{@link https:\/\/www\.prisma\.io\/docs\/concepts\/components\/prisma-client\/distinct Distinct Docs\}\n \* \n \* Filter by unique combinations of .*\n \*\/\n(?= distinct\?: )/gmu, '$1'),
338
+ // From 7.5.0, MongoDB's interactive transactions take no nested $transaction.
339
+ [CLASS]: (text) => text.replace('ITXClientDenyList | "$transaction">', 'ITXClientDenyList>'), [NAMESPACE]: (text) => text.replace("ITXClientDenyList | '$transaction'>", 'ITXClientDenyList>'),
340
+ },
341
+ '7.4.1': { client: (text, path) => swap(text, ...example(' '), { path, all: true }), [CLASS]: (text, path) => swap(swap(text, ...example(' '), { path, all: true }), ...example(' '), { path, all: true }) },
342
+ '7.4.0': { [CLASS]: (text, path) => swap(text, 'https://www.prisma.io/docs/orm/prisma-client/queries/transactions', 'https://www.prisma.io/docs/concepts/components/prisma-client/transactions', { path }) },
343
+ }
344
+
345
+ // Where each client stamps its version and engine.
346
+ const STAMPS = [
347
+ [CLASS, (version) => `"clientVersion": "${version}",\n "engineVersion": "${ENGINES[version]}",`],
348
+ [NAMESPACE, (version) => ` * Prisma Client JS version: ${version}\n * Query Engine version: ${ENGINES[version]}\n`],
349
+ [NAMESPACE, (version) => ` client: "${version}",\n engine: "${ENGINES[version]}"\n`],
350
+ ]
351
+
352
+ // A 7.10.0 client file as `version` writes it.
353
+ function asVersion(text, path, version, extension) {
354
+ if (typeof text !== 'string') return text
355
+ const key = path.startsWith('models/') ? MODELS : path.slice(0, -extension.length - 1)
356
+ for (const each of PRISMA_VERSIONS.slice(1, PRISMA_VERSIONS.indexOf(version) + 1)) text = DOWN[each]?.[key]?.(text, path) ?? text
357
+ for (const [where, stamp] of STAMPS) if (key === where) text = swap(text, stamp(GENERATOR_VERSION), stamp(version), { path })
358
+ return text
359
+ }
360
+
361
+ // --- generating ---
362
+
363
+ // One generator's client: path in the output -> text (bytes for the query compiler's .wasm).
364
+ function generateClientFor({ schemaPath, datamodel, generator, dmmf, datasources, outputDir, prisma, host }) {
365
+ const client = { host, version: prisma.version, prismaDir: posix.dirname(prisma.file) }
366
+ const peer = loadGenerator().peer(client)
367
+ const options = generatorOptions(peer, host, generator.config, outputDir)
368
+ client.target = options.target
369
+ const built = peer.buildClient({
370
+ datamodel, schemaPath, runtimeBase: '@prisma/client/runtime', outputDir, generator, dmmf, datasources, binaryPaths: {},
371
+ engineVersion: ENGINES[GENERATOR_VERSION], clientVersion: GENERATOR_VERSION, activeProvider: datasources[0]?.activeProvider, tsNoCheckPreamble: true, ...options,
372
+ })
373
+ const denied = peer.validateDmmfAgainstDenylists(built.prismaClientDmmf)
374
+ if (denied) throw new Error(`prisma: ${schemaPath} contains reserved keywords, to rename: ${denied.map((error) => error.message).join(', ')}`)
375
+ // fileMap nests directories as objects.
376
+ const flat = (map, at) => Object.entries(map).flatMap(([name, content]) => (typeof content === 'string' || Buffer.isBuffer(content) ? [[at + name, content]] : flat(content, `${at}${name}/`)))
377
+ return new Map(flat(built.fileMap, '').map(([path, content]) => [path, asVersion(content, path, prisma.version, options.generatedFileExtension)]))
378
+ }
379
+
380
+ const readingSchema = (path, what) => Promise.try(what).catch((cause) => { throw new Error(`prisma: ${path}: ${cause.message}`, { cause }) })
381
+
382
+ // Each `{ dir, files }` output. Projects without a schema or a `prisma-client` generator are skipped
383
+ // quietly; ones stasis can't generate for with a warning, the scan then reporting what is missing.
384
+ /* eslint-disable no-await-in-loop -- one project at a time, in the order its warnings read */
385
+ export async function generatePrismaClients({ host, root, projects }) {
386
+ const outputs = []
387
+ const skip = (what, why) => { console.warn(`[stasis] prisma: not generating ${what}: ${why}`) }
388
+ for (const project of [...projects].toSorted()) {
389
+ const dir = posix.join(root, project)
390
+ const prisma = installedPrisma(host, dir)
391
+ const supported = PRISMA_VERSIONS.includes(prisma?.version)
392
+ const path = await Promise.try(projectSchema, host, dir, supported ? prisma.version : GENERATOR_VERSION).catch((error) => skip(`for ${dir}`, error.message))
393
+ // Checked before parsing: another Prisma's schema needn't satisfy 7.10.0.
394
+ if (path && !supported) skip(`for ${path}`, `stasis generates as Prisma ${PRISMA_VERSIONS.at(-1)} to ${PRISMA_VERSIONS[0]} do, and ${prisma === null ? 'none is installed' : `${prisma.file} is ${prisma.version}`}`)
395
+ if (!path || !supported) continue
396
+ const files = await readingSchema(path, () => schemaFilesAt(host, path))
397
+ const { internals } = loadGenerator()
398
+ const config = await readingSchema(path, () => internals.getConfig({ datamodel: files }))
399
+ const generators = config.generators.filter((generator) => generator.provider.value === PROVIDER && generator.provider.fromEnvVar === null)
400
+ for (const other of config.generators.filter((generator) => !generators.includes(generator))) skip(`${path}'s generator ${other.name}`, `stasis generates the ${JSON.stringify(PROVIDER)} provider's alone`)
401
+ if (generators.length === 0) continue
402
+ for (const warning of config.warnings) console.warn(`[stasis] prisma: ${warning}`)
403
+ if (config.datasources.length === 0) throw new Error(`prisma: ${path} defines no datasource`)
404
+ const dmmf = await readingSchema(path, () => internals.getDMMF({ datamodel: files }))
405
+ const datamodel = internals.mergeSchemas({ schemas: files })
406
+ for (const generator of generators) {
407
+ if (generator.output === null) throw new Error(`prisma: ${path}: generator ${generator.name} names no output`)
408
+ if (generator.output.fromEnvVar !== null) {
409
+ skip(`${path}'s generator ${generator.name}`, `its output is env("${generator.output.fromEnvVar}"), which stasis doesn't read`)
410
+ continue
411
+ }
412
+ const outputDir = posix.resolve(posix.dirname(generator.sourceFilePath ?? path), generator.output.value)
413
+ if (hasNodeModulesSegment(outputDir)) throw new Error(`prisma: ${path}: generator ${generator.name}'s output ${outputDir} is in node_modules, which is laid out from the lockfile alone`)
414
+ if (outputs.some((output) => output.dir === outputDir)) throw new Error(`prisma: ${outputDir} is the output of more than one generator`)
415
+ outputs.push({ dir: outputDir, files: generateClientFor({ schemaPath: path, datamodel, generator, dmmf, datasources: config.datasources, outputDir, prisma, host }) })
416
+ }
417
+ }
418
+ return outputs
419
+ }
420
+
421
+ // What deleteOutputDir clears before writing (its globs skip dot paths), in an output that must
422
+ // already look like a client.
423
+ const isStale = (path) => /(?:\.(?:js|ts|mts|cts|wasm|prisma)$|^package\.json$|^[^/]*\.node$|^(?:query|schema)-engine-[^/]*$)/u.test(path) && !path.split('/').some((name) => name.startsWith('.'))
424
+ const CLIENT_FILES = ['client.ts', 'client.mts', 'client.cts', 'client.d.ts']
425
+
426
+ // A copy of `vfs` with the outputs written over any earlier client, as `prisma generate` does.
427
+ export function withPrismaClients(vfs, outputs) {
428
+ const out = createVfs()
429
+ out.mount(vfs, '/')
430
+ for (const { dir, files } of outputs) {
431
+ if (out.isDirectory(dir)) {
432
+ const names = out.readdir(dir)
433
+ if (names.length > 0 && !CLIENT_FILES.some((name) => names.includes(name))) throw new Error(`prisma: ${dir} exists and is not empty but doesn't look like a generated Prisma Client`)
434
+ for (const { type, path } of out.walk(dir).toArray()) if (type === 'file' && isStale(posix.relative(dir, path))) out.unlink(path)
435
+ }
436
+ out.mount(createVfs(Object.fromEntries([...files].map(([path, text]) => [posix.join(dir, path), text]))), '/', { clash: 'replace' })
437
+ }
438
+ return out
439
+ }
@@ -20,7 +20,7 @@ import { isDir, isFile } from '../resolve-typescript.js'
20
20
  // reads the project through them. The project is read through a host it is given, and nothing else.
21
21
 
22
22
  // cwd or the nearest of its ancestors that `holds`, or null.
23
- function nearest(cwd, holds) {
23
+ export function nearest(cwd, holds) {
24
24
  for (let dir = cwd; ; dir = dirname(dir)) {
25
25
  if (holds(dir)) return dir
26
26
  if (dirname(dir) === dir) return null
@@ -28,7 +28,7 @@ function nearest(cwd, holds) {
28
28
  }
29
29
 
30
30
  // Whether a directory holds a file of one of `names`.
31
- const holding = (host, ...names) => (dir) => names.some((name) => isFile(join(dir, name), host))
31
+ export const holding = (host, ...names) => (dir) => names.some((name) => isFile(join(dir, name), host))
32
32
 
33
33
  // The directory of the package cwd is in that is none of `projects` (their directories from
34
34
  // `root`), or null: the nearest package.json with a name between cwd and `root`, one without being a
@@ -52,7 +52,7 @@ const naming = (file, promise) => promise.catch((cause) => {
52
52
  })
53
53
 
54
54
  // The directory `root` as `host` holds it, by paths from `/`, as deptree reads a project.
55
- function projectView(host, root) {
55
+ export function projectView(host, root) {
56
56
  const at = (p) => (p === '/' ? root : join(root, p))
57
57
  const typeOf = (st) => (st.isDirectory() ? 'directory' : st.isFile() ? 'file' : 'other')
58
58
  const stat = (p) => {
@@ -232,7 +232,7 @@ const PACKAGE_MANAGERS = {
232
232
  installs: 'dependencies',
233
233
  hides: 'node_modules',
234
234
  async build(view, soldeer, file, given) {
235
- const tree = await naming(file, buildSoldeerTree({ project: view, host: { soldeer, os: given?.os ?? process.platform } }))
235
+ const tree = await naming(file, buildSoldeerTree({ project: view, host: { soldeer, os: given?.os ?? process.platform }, github: given?.client }))
236
236
  settleSoldeer(view, tree.vfs, dirname(file))
237
237
  return tree
238
238
  },
@@ -242,14 +242,19 @@ const PACKAGE_MANAGERS = {
242
242
  // Their names.
243
243
  const PACKAGE_MANAGER_NAMES = Object.keys(PACKAGE_MANAGERS)
244
244
 
245
- // layOutTree's tree, with the host reading `project` through it: the tree serves what the package
246
- // manager installs, and any file it writes beside it at the root.
245
+ // layOutTree's tree, with the host reading `project` through it (treeHost).
247
246
  export async function loadTree(options) {
248
247
  const tree = await layOutTree(options)
249
- const pm = PACKAGE_MANAGERS[options.packageManager]
248
+ return { ...tree, host: treeHost(tree, options.project) }
249
+ }
250
+
251
+ // The host reading `project`, a host of the project's Vfs, through `tree` (layOutTree's): the tree
252
+ // serves what the package manager installs, and any file it writes beside it at the root.
253
+ export function treeHost(tree, project) {
254
+ const pm = PACKAGE_MANAGERS[tree.packageManager]
250
255
  const files = tree.vfs.readdir('/').filter((name) => tree.vfs.lstat(`/${name}`).type === 'file')
251
256
  const installs = [...[...tree.projects].map((dir) => join(dir, pm.installs)), ...files]
252
- return { ...tree, host: vfsHost(tree.vfs, { root: tree.root, outside: options.project, installs, hides: pm.hides }) }
257
+ return vfsHost(tree.vfs, { root: tree.root, outside: project, installs, hides: pm.hides })
253
258
  }
254
259
 
255
260
  // The lockfile `packageManager` installs from, e.g. 'pnpm-lock.yaml'.
@@ -343,8 +348,10 @@ export function checkVfs(name, vfs) {
343
348
  // path; a new Vfs holding the tree, rooted there; the directories of the projects it finds, from
344
349
  // there; deptree's counts; and the version reproduced, `packageManagerVersion` if given, else the one
345
350
  // the root package.json's packageManager pins, else the default. deptree reads the project through a
346
- // view of `project`, which nothing is written through.
347
- async function layOutTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc }) {
351
+ // view of `project`, which nothing is written through. `client`, a @preventive/upstream/github.js
352
+ // client, is what a Soldeer git dependency is fetched through (deptree's `github`); without one, a
353
+ // lockfile holding one is refused before anything is fetched.
354
+ async function layOutTree({ project, packageManager, cwd, packageManagerVersion, os, cpu, libc, client }) {
348
355
  const pm = PACKAGE_MANAGERS[packageManager]
349
356
  const found = pm.root(project, cwd, os)
350
357
  if (found === null) throw new Error(`no ${pm.lockfile} found in ${cwd} or any parent directory`)
@@ -357,7 +364,7 @@ async function layOutTree({ project, packageManager, cwd, packageManagerVersion,
357
364
  const projects = new Set(pm.projects(view, version, { os, cpu, libc }))
358
365
  const other = pm.kind === 'js' ? outsider(project, found, cwd, projects) : null
359
366
  if (other !== null) throw new Error(`${file} does not install ${other}: it is none of the lockfile's projects`)
360
- const { vfs, stats } = await pm.build(view, version, file, { os, cpu, libc })
367
+ const { vfs, stats } = await pm.build(view, version, file, { os, cpu, libc, client })
361
368
  return {
362
369
  root: project.realpath(found),
363
370
  vfs,
package/src/vfs-bundle.js CHANGED
@@ -12,10 +12,11 @@ import { NODE_MODULES_MANAGERS, checkTarget, checkVfs, detectPackageManager, loa
12
12
  // only read. The tree is laid out by @preventive/deptree into a Vfs of its own, and nothing is read
13
13
  // from disk or written there but the tarballs and zips: fetched from registry.npmjs.org and
14
14
  // Soldeer's registry, or read from npm's cache or ~/.audit's where one holds them, every copy held
15
- // to the lockfile's integrity before it is used; cached only where setCacheDir says.
15
+ // to the lockfile's integrity before it is used; cached only where setCacheDir says. A Soldeer git
16
+ // dependency is GitHub's tarball of its commit, fetched through the `client` buildVfsBundle is given.
16
17
  // buildGitHubBundle builds one from a GitHub repo at a commit, or the one a tag names (the default
17
18
  // branch's head without either), its tree fetched from GitHub and held to its git tree id, cached
18
- // there the same way.
19
+ // there the same way, and its Soldeer git dependencies through the same client.
19
20
 
20
21
  export { buildVfsBundle } from './cmd/bundle.js'
21
22
  export { buildGitHubBundle } from './vfs-bundle/github.js'
@@ -55,7 +56,7 @@ export async function loadNodeModules({ vfs, packageManager, cwd = '/', packageM
55
56
  // directory, of the kind its package manager builds: `packageManager` if given, else the one
56
57
  // detected as the build detects it. For pnpm and yarn 1, the entry points its package.json names,
57
58
  // resolved as the build resolves them with the same `conditions`, `mainFields`, `metro`,
58
- // `platforms`, `jsx` and `typescript` (packageEntries); for Soldeer, its .sol entry points by name
59
+ // `platforms` and `typescript` (packageEntries); for Soldeer, its .sol entry points by name
59
60
  // and layout (solidityEntries). The options are checked as the build checks them, and `os`, the
60
61
  // one the build is for, detects the package manager as there. Of the project held in `vfs`, from
61
62
  // `cwd`; or of a GitHub repo, `{ github, sha, tag, directory, client }` as buildGitHubBundle takes