@exodus/stasis 1.0.0-beta.1 → 1.0.0-beta.10
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 +32 -8
- package/bin/stasis.js +386 -213
- package/package.json +52 -12
- package/src/audit-corrections.js +96 -0
- package/src/audit.js +289 -64
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +14 -22
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +96 -128
- package/src/cmd/bundle.js +1121 -654
- package/src/cmd/diff.js +2 -19
- package/src/cmd/github-bundle.js +39 -0
- package/src/cmd/output.js +36 -0
- package/src/cmd/sbom.js +7 -19
- package/src/diff.js +80 -129
- package/src/installed-commits.js +182 -0
- package/src/json-stream.js +283 -0
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +49 -116
- package/src/loaders/cargo.js +1716 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +375 -402
- package/src/loaders/rust.js +3254 -242
- package/src/loaders/solidity-ownership.js +531 -0
- package/src/loaders/solidity.js +412 -162
- package/src/loaders/toml.js +68 -0
- package/src/metro-resolver.js +150 -0
- package/src/metro-transformer.js +3 -2
- package/src/mock.js +57 -165
- package/src/parse.js +4 -16
- package/src/resolve-fields.js +370 -0
- package/src/resolve-node.js +419 -0
- package/src/resolve-typescript.js +541 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +120 -97
- package/src/scan.js +348 -180
- package/src/vfs-bundle/entries.js +144 -0
- package/src/vfs-bundle/github.js +202 -0
- package/src/vfs-bundle/prisma.js +439 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +647 -0
- package/src/vfs-bundle.js +80 -0
- package/src/why.js +434 -0
- package/src/apis/npm/index.js +0 -36
- package/src/apis/npm/semver.cjs +0 -44
- package/src/cmd/extract.js +0 -193
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { isTomlTable, readToml } from '../loaders/toml.js'
|
|
4
|
+
|
|
5
|
+
// What `soldeer install` (0.12.0) writes beside the dependencies folder after installing it
|
|
6
|
+
// (soldeer-core's update_config_libs and edit_remappings with RemappingsAction::Update): the
|
|
7
|
+
// remappings.txt it would write is written into the tree, which then serves it in place of the
|
|
8
|
+
// project's; a foundry.toml it would edit is refused, as is what this can't tell it would leave as
|
|
9
|
+
// it is. `view` is the project's root by paths from `/`, `root` its real path, for messages.
|
|
10
|
+
|
|
11
|
+
const SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?(?:\+[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?$/u
|
|
12
|
+
// A requirement Soldeer takes as exact: a bare version (its `^` made `=`), or one written `=`.
|
|
13
|
+
const EXACT = /^=?\s*(\S+)$/u
|
|
14
|
+
const withoutBuild = (version) => version.replace(/\+.*$/u, '')
|
|
15
|
+
|
|
16
|
+
// A file's text as Soldeer reads it (a byte order mark kept), or undefined.
|
|
17
|
+
function textOf(view, path) {
|
|
18
|
+
try {
|
|
19
|
+
return view.stat(path).type === 'file' ? view.readFile(path).toString('utf8') : undefined
|
|
20
|
+
} catch {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Rust's `Path::components` of a remapping's target, `.` kept only at the start.
|
|
26
|
+
function components(path) {
|
|
27
|
+
const out = path.startsWith('/') ? ['/'] : []
|
|
28
|
+
for (const [i, part] of path.split('/').entries()) {
|
|
29
|
+
if (part !== '' && (part !== '.' || i === 0)) out.push(part)
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Whether the folder `name` of a remapping's target (its dependencies folder and the one in it) is
|
|
35
|
+
// `dep`'s, as soldeer's path_matches tells, or null where only Rust's semver could tell.
|
|
36
|
+
function folderMatches(dep, name) {
|
|
37
|
+
if (!name.startsWith(`${dep.name}-`)) return false
|
|
38
|
+
const version = name.slice(dep.name.length + 1)
|
|
39
|
+
if (version === dep.version) return true
|
|
40
|
+
if (!SEMVER.test(version)) return version === dep.req
|
|
41
|
+
const exact = EXACT.exec(dep.req.trim())?.[1]
|
|
42
|
+
if (exact !== undefined && SEMVER.test(exact)) return withoutBuild(exact) === withoutBuild(version)
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The remappings Soldeer's update leaves, of `existing` (`[name, target]` pairs) and the config's
|
|
47
|
+
// dependencies, sorted as it sorts them (by their UTF-8 bytes).
|
|
48
|
+
function updatedRemappings(existing, deps, { regenerate, where }) {
|
|
49
|
+
const items = deps.map((dep) => ({ dep, line: `${dep.remapped}=dependencies/${dep.folder}/` }))
|
|
50
|
+
let lines
|
|
51
|
+
if (regenerate || existing.length === 0) {
|
|
52
|
+
lines = items.map(({ line }) => line)
|
|
53
|
+
} else {
|
|
54
|
+
lines = []
|
|
55
|
+
let rest = existing
|
|
56
|
+
for (const { dep, line } of items) {
|
|
57
|
+
let found = false
|
|
58
|
+
rest = rest.filter(([name, target]) => {
|
|
59
|
+
const path = components(target).slice(0, 2)
|
|
60
|
+
const folder = path.at(-1)
|
|
61
|
+
const matches = folder === undefined || ['/', '.', '..'].includes(folder) ? false : folderMatches(dep, folder)
|
|
62
|
+
if (matches === null) throw new Error(`${where}: "${name}=${target}" names ${folder}, which \`soldeer install\` may rewrite to dependencies/${dep.folder}`)
|
|
63
|
+
if (!matches) return true
|
|
64
|
+
lines.push(`${name}=${target.replaceAll(path.join('/').replace(/^\/\//u, '/'), `dependencies/${dep.folder}`)}`)
|
|
65
|
+
found = true
|
|
66
|
+
return false
|
|
67
|
+
})
|
|
68
|
+
if (!found) lines.push(line)
|
|
69
|
+
}
|
|
70
|
+
lines.push(...rest.map(([name, target]) => `${name}=${target}`))
|
|
71
|
+
}
|
|
72
|
+
return lines.toSorted((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// `[name, target]` of each remapping in `lines`, as Soldeer reads them: split at the first `=`, any
|
|
76
|
+
// line without one passed over.
|
|
77
|
+
const pairs = (lines) => lines.flatMap((line) => {
|
|
78
|
+
const at = line.indexOf('=')
|
|
79
|
+
return at === -1 ? [] : [[line.slice(0, at), line.slice(at + 1)]]
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export function settleSoldeer(view, vfs, root) {
|
|
83
|
+
const foundryText = textOf(view, '/foundry.toml')
|
|
84
|
+
const foundry = foundryText === undefined ? undefined : readToml(foundryText)
|
|
85
|
+
const configFile = isTomlTable(foundry?.dependencies) ? 'foundry.toml' : 'soldeer.toml'
|
|
86
|
+
const config = configFile === 'foundry.toml' ? foundry : readToml(textOf(view, '/soldeer.toml') ?? '')
|
|
87
|
+
const where = (file) => join(root, file)
|
|
88
|
+
|
|
89
|
+
if (configFile === 'foundry.toml') {
|
|
90
|
+
const libs = foundry.profile?.default?.libs
|
|
91
|
+
if (!Array.isArray(libs) || !libs.includes('dependencies')) {
|
|
92
|
+
throw new Error(`${where('foundry.toml')}: [profile.default] libs holds no "dependencies", which \`soldeer install\` adds`)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const settings = { remappings_generate: true, remappings_regenerate: false, remappings_version: true, remappings_prefix: '', remappings_location: 'txt', ...config.soldeer }
|
|
97
|
+
if (!settings.remappings_generate) return
|
|
98
|
+
const locked = new Map(readToml(textOf(view, '/soldeer.lock') ?? '').dependencies?.map((entry) => [entry.name, entry.version]) ?? [])
|
|
99
|
+
const deps = Object.entries(config.dependencies ?? {}).map(([name, value]) => {
|
|
100
|
+
const req = typeof value === 'string' ? value : value.version
|
|
101
|
+
const version = locked.get(name)
|
|
102
|
+
const remapped = `${settings.remappings_prefix}${name}${settings.remappings_version ? `-${req.replaceAll('=', '')}` : ''}/`
|
|
103
|
+
return { name, req, version, folder: `${name}-${version}`, remapped }
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const remappingsTxt = textOf(view, '/remappings.txt')
|
|
107
|
+
if (configFile === 'foundry.toml' && settings.remappings_location === 'config') {
|
|
108
|
+
if (remappingsTxt !== undefined) throw new Error(`${where('remappings.txt')}: \`soldeer install\` removes it, as remappings_location is "config"`)
|
|
109
|
+
for (const [name, profile] of Object.entries(isTomlTable(foundry.profile) ? foundry.profile : {})) {
|
|
110
|
+
const current = Array.isArray(profile?.remappings) ? profile.remappings : undefined
|
|
111
|
+
if (current === undefined && name !== 'default') continue
|
|
112
|
+
const updated = updatedRemappings(pairs((current ?? []).filter((r) => typeof r === 'string')), deps, { regenerate: settings.remappings_regenerate, where: where('foundry.toml') })
|
|
113
|
+
if (current === undefined || updated.length !== current.length || updated.some((r, i) => r !== current[i])) {
|
|
114
|
+
throw new Error(`${where('foundry.toml')}: [profile.${name}] remappings are not as \`soldeer install\` leaves them`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lines = (settings.remappings_regenerate ? '' : remappingsTxt ?? '').split('\n')
|
|
121
|
+
if (lines.at(-1) === '') lines.pop()
|
|
122
|
+
const existing = pairs(lines.map((line) => line.replace(/\r$/u, '')))
|
|
123
|
+
const text = updatedRemappings(existing, deps, { regenerate: settings.remappings_regenerate, where: where('remappings.txt') }).map((line) => `${line}\n`).join('')
|
|
124
|
+
if (text !== remappingsTxt) vfs.writeFile('/remappings.txt', text, { mode: remappingsTxt === undefined ? 0o644 : view.lstat('/remappings.txt').mode })
|
|
125
|
+
}
|