@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.
- package/README.md +7 -6
- package/bin/stasis.js +30 -17
- package/package.json +14 -9
- package/src/audit-corrections.js +6 -2
- package/src/audit.js +63 -22
- package/src/bundle-from-lockfile.js +3 -5
- package/src/cmd/bundle.js +149 -69
- package/src/diff.js +16 -8
- package/src/loaders/cargo.js +26 -9
- package/src/loaders/php.js +4 -3
- package/src/metro-resolver.js +1 -1
- package/src/resolve-fields.js +18 -11
- package/src/resolve-typescript.js +175 -61
- package/src/sbom.js +3 -1
- package/src/scan.js +158 -73
- package/src/vfs-bundle/entries.js +5 -5
- package/src/vfs-bundle/github.js +15 -9
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/tree.js +18 -11
- package/src/vfs-bundle.js +4 -3
|
@@ -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
|
+
}
|
package/src/vfs-bundle/tree.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|