@exodus/stasis 1.0.0-alpha.2 → 1.0.0-beta.0
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 +67 -3
- package/bin/stasis.js +322 -17
- package/logo.svg +12 -3
- package/package.json +39 -18
- package/src/audit.js +112 -0
- package/src/bundle-from-lockfile.js +65 -0
- package/src/bundle.js +4 -217
- package/src/cmd/build.js +321 -0
- package/src/cmd/bundle.js +950 -59
- package/src/cmd/diff.js +36 -0
- package/src/cmd/extract.js +193 -0
- package/src/cmd/sbom.js +35 -0
- package/src/diff.js +333 -0
- package/src/esbuild.js +5 -56
- package/src/loader-mock.js +19 -0
- package/src/loader.js +6 -132
- package/src/loaders/bash.js +257 -0
- package/src/loaders/php.js +1058 -0
- package/src/loaders/rust.js +336 -0
- package/src/loaders/solidity.js +70 -49
- package/src/lockfile.js +4 -80
- package/src/metro-transformer.js +6 -0
- package/src/metro.js +5 -0
- package/src/mock.js +310 -0
- package/src/parse.js +53 -0
- package/src/prune.js +4 -162
- package/src/sbom.js +266 -0
- package/src/scan.js +373 -0
- package/src/webpack.js +5 -48
- package/src/config.js +0 -205
- package/src/state.js +0 -440
- package/src/state.util.js +0 -25
- package/src/util.js +0 -60
package/src/state.js
DELETED
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { isUtf8 } from 'node:buffer'
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
-
import { join, resolve, relative, basename, dirname, extname } from 'node:path'
|
|
5
|
-
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
6
|
-
import { findPackageJSON } from 'node:module'
|
|
7
|
-
import { brotliCompressSync, brotliDecompressSync } from 'node:zlib'
|
|
8
|
-
|
|
9
|
-
import { Config } from './config.js'
|
|
10
|
-
import { Bundle } from './bundle.js'
|
|
11
|
-
import { Lockfile } from './lockfile.js'
|
|
12
|
-
import { sha512integrity, readFileSyncMaybe, noupsert } from './state.util.js'
|
|
13
|
-
import { splitNodeModulesPath } from './util.js'
|
|
14
|
-
|
|
15
|
-
const FILE_CONFIG = 'stasis.config.json'
|
|
16
|
-
const FILE_LOCK = 'stasis.lock.json'
|
|
17
|
-
const FILE_CODE = 'stasis.code.br'
|
|
18
|
-
const FILE_RESOURCES = 'stasis.resources.br'
|
|
19
|
-
|
|
20
|
-
function readPackageJSON(pkgAbsolute) {
|
|
21
|
-
const buf = readFileSync(pkgAbsolute)
|
|
22
|
-
assert.ok(isUtf8(buf))
|
|
23
|
-
return JSON.parse(buf.toString())
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// TODO: stricter format validation
|
|
27
|
-
|
|
28
|
-
let preload
|
|
29
|
-
const liveStates = new Set()
|
|
30
|
-
|
|
31
|
-
export class State {
|
|
32
|
-
hashes = new Map()
|
|
33
|
-
entries = new Set()
|
|
34
|
-
sources = new Map()
|
|
35
|
-
resources = new Map()
|
|
36
|
-
formats = new Map()
|
|
37
|
-
modules = new Map()
|
|
38
|
-
imports = new Map()
|
|
39
|
-
config
|
|
40
|
-
root
|
|
41
|
-
|
|
42
|
-
// options.preload (boolean) -- mark this instance as the unique preload State, exposed via
|
|
43
|
-
// State.preload. Only one preload State may exist at a time. Non-preload States can be
|
|
44
|
-
// constructed freely (e.g. a bundler plugin that emits a sidecar bundle in a process that
|
|
45
|
-
// already has a preload-owned lockfile). All other keys forward to Config.
|
|
46
|
-
constructor(root, options = {}) {
|
|
47
|
-
const { preload: isPreload = false, ...configOptions } = options
|
|
48
|
-
this.config = new Config(configOptions)
|
|
49
|
-
// Check the preload-uniqueness invariant up front so two simultaneous preload attempts
|
|
50
|
-
// can't both pass through the constructor; the actual `preload = this` registration
|
|
51
|
-
// happens after construction succeeds so a throw in setup can't leave a half-built
|
|
52
|
-
// preload reference behind.
|
|
53
|
-
if (isPreload) assert.ok(!preload, 'Only one preload Stasis instance is supported')
|
|
54
|
-
const potentialRoots = []
|
|
55
|
-
let cursor = root
|
|
56
|
-
while (cursor) {
|
|
57
|
-
if (existsSync(join(cursor, 'package.json'))) {
|
|
58
|
-
potentialRoots.push(cursor)
|
|
59
|
-
} else if (
|
|
60
|
-
existsSync(join(cursor, FILE_CONFIG)) ||
|
|
61
|
-
existsSync(join(cursor, FILE_LOCK)) ||
|
|
62
|
-
existsSync(join(cursor, FILE_CODE)) ||
|
|
63
|
-
existsSync(join(cursor, FILE_RESOURCES))) {
|
|
64
|
-
throw new Error('Unexpected stasis config without package.json')
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (cursor === process.env.PROJECT_CWD) break // e.g. yarn sets this
|
|
68
|
-
if (existsSync(join(cursor, '.git'))) break // don't go higher than the repo root
|
|
69
|
-
if (existsSync(join(cursor, 'pnpm-workspace.yaml'))) break // pnpm workspace root
|
|
70
|
-
const parent = dirname(cursor)
|
|
71
|
-
if (!parent || parent === cursor) break
|
|
72
|
-
cursor = parent
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// default root is top-level package.json, to opt-in to per-dir create stasis.config.json
|
|
76
|
-
this.root = potentialRoots.at(-1)
|
|
77
|
-
|
|
78
|
-
let loaded = false
|
|
79
|
-
for (const rootDir of potentialRoots) {
|
|
80
|
-
const config = readFileSyncMaybe(rootDir, FILE_CONFIG, 'utf-8')
|
|
81
|
-
const lock = readFileSyncMaybe(rootDir, FILE_LOCK, 'utf-8')
|
|
82
|
-
const sourcesPath = this.config.bundleFile || join(rootDir, FILE_CODE)
|
|
83
|
-
const sources = readFileSyncMaybe(dirname(sourcesPath), basename(sourcesPath))
|
|
84
|
-
const resources = readFileSyncMaybe(rootDir, FILE_RESOURCES)
|
|
85
|
-
if (config !== null || lock !== null || sources !== null || resources !== null) {
|
|
86
|
-
if (loaded) throw new Error('Stasis config already loaded')
|
|
87
|
-
loaded = true
|
|
88
|
-
this.root = rootDir
|
|
89
|
-
|
|
90
|
-
if (config) this.config.loadConfig(config)
|
|
91
|
-
|
|
92
|
-
if (lock && !this.config.useLockfile && !this.config.ignoreLockfile) {
|
|
93
|
-
throw new Error(`Unexpected ${join(rootDir, FILE_LOCK)} with config.lock = 'none'`)
|
|
94
|
-
}
|
|
95
|
-
if (sources && !this.config.writeBundle && !this.config.loadBundle && !this.config.ignoreBundle) {
|
|
96
|
-
throw new Error(`Unexpected ${sourcesPath} with config.bundle = 'none'`)
|
|
97
|
-
}
|
|
98
|
-
if (resources && !this.config.writeBundle && !this.config.loadBundle && !this.config.ignoreBundle) {
|
|
99
|
-
throw new Error(`Unexpected ${join(rootDir, FILE_RESOURCES)} with config.bundle = 'none'`)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (sources && !lock && this.config.useLockfile && !this.config.replaceLockfile) {
|
|
103
|
-
throw new Error('stasis.lock.json missing, can not use sources')
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (this.config.frozen) assert.ok(lock, 'No lockfile, but attempting to run in frozen mode')
|
|
107
|
-
let lockfileLoaded = false
|
|
108
|
-
if (lock && this.config.useLockfile && !this.config.replaceLockfile) {
|
|
109
|
-
const lockfile = Lockfile.parse(lock)
|
|
110
|
-
if (this.config.frozen) assert.equal(lockfile.config.scope, this.config.scope)
|
|
111
|
-
|
|
112
|
-
const includeSources = lockfile.config.scope === 'full' && this.config.full
|
|
113
|
-
for (const [dir, info] of lockfile.modules) {
|
|
114
|
-
if (!dir.includes('node_modules') && !includeSources) continue
|
|
115
|
-
this.modules.set(dir, info)
|
|
116
|
-
}
|
|
117
|
-
if (includeSources) this.entries = lockfile.entries
|
|
118
|
-
|
|
119
|
-
for (const [dir, { files }] of this.modules) {
|
|
120
|
-
for (const [name, hash] of Object.entries(files)) {
|
|
121
|
-
noupsert(this.hashes, join(dir, name), hash)
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
lockfileLoaded = true
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (sources && (this.config.writeBundle || this.config.loadBundle) && !this.config.replaceBundle) {
|
|
128
|
-
const bundle = Bundle.parseCode(brotliDecompressSync(sources).toString('utf-8'))
|
|
129
|
-
assert.equal(bundle.config.scope, this.config.scope)
|
|
130
|
-
this.#mergeBundleMetadata(bundle, { lockfileLoaded })
|
|
131
|
-
this.sources = bundle.sources
|
|
132
|
-
this.formats = bundle.formats
|
|
133
|
-
this.imports = bundle.imports
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (resources && (this.config.writeBundle || this.config.loadBundle) && !this.config.replaceBundle) {
|
|
137
|
-
const bundle = Bundle.parseResources(brotliDecompressSync(resources).toString('utf-8'))
|
|
138
|
-
// v0 resource bundles didn't record scope; only enforce on v1 where it's reliable.
|
|
139
|
-
if (bundle.version === Bundle.VERSION) {
|
|
140
|
-
assert.equal(bundle.config.scope, this.config.scope)
|
|
141
|
-
}
|
|
142
|
-
this.#mergeBundleMetadata(bundle, { lockfileLoaded })
|
|
143
|
-
this.resources = bundle.sources
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Register only after every fallible step succeeds, so a thrown error during config
|
|
149
|
-
// discovery / lockfile / bundle parsing leaves the static registry untouched.
|
|
150
|
-
if (isPreload) preload = this
|
|
151
|
-
liveStates.add(this)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Cross-check bundle metadata (entries/modules) with what the lockfile already
|
|
155
|
-
// loaded, or absorb it as the source of truth when no lockfile is present.
|
|
156
|
-
// v0 bundles infer `name` from path for node_modules buckets but carry no
|
|
157
|
-
// version; the workspace "." bucket has no inferable name. We skip entries
|
|
158
|
-
// we can't cross-check (no name with lockfile, or no version when populating
|
|
159
|
-
// state.modules — addFile later asserts strict name+version equality against
|
|
160
|
-
// disk's package.json, which would clash with a null we stashed earlier).
|
|
161
|
-
// Note: the path-inferred name for v0 will disagree with the lockfile when a
|
|
162
|
-
// dep is installed under an alias (`foo: npm:bar` writes `node_modules/foo`
|
|
163
|
-
// with `name: "bar"` in package.json); the strict equality below will fail
|
|
164
|
-
// in that case, which is the right call — the v0 bundle has no way to
|
|
165
|
-
// attest the real package identity, so we refuse to silently mismatch.
|
|
166
|
-
#mergeBundleMetadata(bundle, { lockfileLoaded }) {
|
|
167
|
-
if (lockfileLoaded) {
|
|
168
|
-
if (bundle.entries.size > 0) {
|
|
169
|
-
assert.deepStrictEqual(
|
|
170
|
-
[...bundle.entries].toSorted(),
|
|
171
|
-
[...this.entries].toSorted(),
|
|
172
|
-
'bundle/lockfile entries mismatch'
|
|
173
|
-
)
|
|
174
|
-
}
|
|
175
|
-
for (const [dir, info] of bundle.modules) {
|
|
176
|
-
if (!info.name) continue // workspace bucket or fallback: no inferable name
|
|
177
|
-
assert.ok(this.modules.has(dir), `bundle module ${dir} missing in lockfile`)
|
|
178
|
-
const lockModule = this.modules.get(dir)
|
|
179
|
-
assert.equal(info.name, lockModule.name)
|
|
180
|
-
if (info.version) assert.equal(info.version, lockModule.version)
|
|
181
|
-
for (const rel of Object.keys(info.files)) {
|
|
182
|
-
assert.ok(Object.hasOwn(lockModule.files, rel), `bundle file ${dir}/${rel} missing in lockfile`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
if (bundle.entries.size > 0) this.entries = new Set([...this.entries, ...bundle.entries])
|
|
187
|
-
for (const [dir, info] of bundle.modules) {
|
|
188
|
-
if (!info.name || !info.version) continue // partial metadata
|
|
189
|
-
if (this.modules.has(dir)) {
|
|
190
|
-
// Code and resources bundles both populate `state.modules` when no lockfile
|
|
191
|
-
// is loaded. They must agree on name/version for the same dir.
|
|
192
|
-
const existing = this.modules.get(dir)
|
|
193
|
-
assert.equal(info.name, existing.name, `bundle ${dir} name mismatch`)
|
|
194
|
-
assert.equal(info.version, existing.version, `bundle ${dir} version mismatch`)
|
|
195
|
-
} else {
|
|
196
|
-
this.modules.set(dir, { name: info.name, version: info.version, files: Object.create(null) })
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
assertEntry(url) {
|
|
203
|
-
// Skipped silently when no entries info is available (v0 bundle + lock=none/ignore).
|
|
204
|
-
// v1 bundles in full scope are required to declare at least one entry at parse time,
|
|
205
|
-
// so this fallback only ever triggers for the legacy path.
|
|
206
|
-
if (this.entries.size === 0) return
|
|
207
|
-
const file = this.relative(this.absolute(url))
|
|
208
|
-
assert.ok(this.entries.has(file), `Unknown entry point: ${file}`)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
static get preload() {
|
|
212
|
-
return preload
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Back-compat accessor: returns the preload State if one exists, else the sole live State.
|
|
216
|
-
// Throws when several non-preload States coexist -- callers in that situation must reach
|
|
217
|
-
// for State.preload (or hold their own reference). Returns undefined when no State is live.
|
|
218
|
-
static get instance() {
|
|
219
|
-
if (preload) return preload
|
|
220
|
-
if (liveStates.size === 0) return undefined
|
|
221
|
-
if (liveStates.size > 1) throw new Error('Multiple Stasis instances; use State.preload')
|
|
222
|
-
return [...liveStates][0]
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
absolute(url) {
|
|
226
|
-
const absolute = fileURLToPath(url)
|
|
227
|
-
assert.equal(pathToFileURL(absolute).toString(), url)
|
|
228
|
-
assert.equal(absolute, resolve(absolute))
|
|
229
|
-
return absolute
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
relative(absolute) {
|
|
233
|
-
assert.ok(absolute)
|
|
234
|
-
const file = relative(this.root, absolute)
|
|
235
|
-
assert.ok(!file.startsWith('..'))
|
|
236
|
-
return file
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
addFile(url, { source, format, isEntry, isBinary } = {}) {
|
|
240
|
-
const absolute = this.absolute(url)
|
|
241
|
-
assert.ok(existsSync(absolute))
|
|
242
|
-
const file = this.relative(absolute)
|
|
243
|
-
|
|
244
|
-
const closestPkgAbsolute = findPackageJSON(url)
|
|
245
|
-
const closestPkg = readPackageJSON(closestPkgAbsolute)
|
|
246
|
-
|
|
247
|
-
const closestType = closestPkg.type ?? 'commonjs'
|
|
248
|
-
assert.ok(closestType === 'module' || closestType === 'commonjs')
|
|
249
|
-
const extToFormat = { __proto__: null, '.json': 'json', '.mjs': 'module', '.cjs': 'commonjs', '.js': closestType }
|
|
250
|
-
const inferredFormat = extToFormat[extname(file)]
|
|
251
|
-
if (inferredFormat !== undefined) {
|
|
252
|
-
if (format != null) assert.equal(format, inferredFormat)
|
|
253
|
-
else format = inferredFormat
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// findPackageJSON may land on a `{"type":"module"}`-style sub-bucket
|
|
257
|
-
// marker that lacks name/version.
|
|
258
|
-
const nmRoot = splitNodeModulesPath(file)?.dir
|
|
259
|
-
let pkgAbsolute, name, version
|
|
260
|
-
if (nmRoot) {
|
|
261
|
-
pkgAbsolute = resolve(this.root, nmRoot, 'package.json')
|
|
262
|
-
const rootPkg = pkgAbsolute === closestPkgAbsolute ? closestPkg : readPackageJSON(pkgAbsolute)
|
|
263
|
-
;({ name, version } = rootPkg)
|
|
264
|
-
assert.ok(name, `Missing name in ${this.relative(pkgAbsolute)}`)
|
|
265
|
-
assert.ok(version, `Missing version in ${this.relative(pkgAbsolute)}`)
|
|
266
|
-
if (closestPkgAbsolute !== pkgAbsolute) {
|
|
267
|
-
if (closestPkg.name !== undefined) assert.equal(closestPkg.name, name)
|
|
268
|
-
if (closestPkg.version !== undefined) assert.equal(closestPkg.version, version)
|
|
269
|
-
}
|
|
270
|
-
} else {
|
|
271
|
-
pkgAbsolute = closestPkgAbsolute
|
|
272
|
-
let json = closestPkg
|
|
273
|
-
while (true) {
|
|
274
|
-
if (json.name !== undefined && json.version !== undefined) {
|
|
275
|
-
;({ name, version } = json)
|
|
276
|
-
break
|
|
277
|
-
}
|
|
278
|
-
assert.ok(Object.keys(json).every((k) => k === 'type'))
|
|
279
|
-
const next = findPackageJSON('..', pathToFileURL(pkgAbsolute))
|
|
280
|
-
assert.ok(
|
|
281
|
-
next && !relative(this.root, next).startsWith('..'),
|
|
282
|
-
`No package.json with name+version found for ${file}`
|
|
283
|
-
)
|
|
284
|
-
pkgAbsolute = next
|
|
285
|
-
json = readPackageJSON(pkgAbsolute)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const pkg = this.relative(pkgAbsolute)
|
|
289
|
-
assert.ok(pkg === 'package.json' || pkg.endsWith('/package.json'))
|
|
290
|
-
assert.equal(basename(pkg), 'package.json')
|
|
291
|
-
const dir = dirname(pkg)
|
|
292
|
-
if (nmRoot) assert.equal(dir, nmRoot)
|
|
293
|
-
if (!this.modules.has(dir)) this.modules.set(dir, { name, version, files: Object.create(null) })
|
|
294
|
-
const module = this.modules.get(dir)
|
|
295
|
-
assert.equal(module.name, name)
|
|
296
|
-
assert.equal(module.version, version)
|
|
297
|
-
|
|
298
|
-
if (typeof source === 'string') {
|
|
299
|
-
assert.ok(source.isWellFormed())
|
|
300
|
-
} else {
|
|
301
|
-
if (source === undefined || source === null) source = readFileSync(absolute)
|
|
302
|
-
assert.ok(Buffer.isBuffer(source))
|
|
303
|
-
if (!isBinary) assert.ok(isUtf8(source), `File is not UTF-8: ${file}`)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const buf = typeof source === 'string' ? Buffer.from(source) : source
|
|
307
|
-
assert.deepStrictEqual(readFileSync(absolute), buf)
|
|
308
|
-
|
|
309
|
-
if (isEntry) this.entries.add(file)
|
|
310
|
-
const integrity = sha512integrity(source)
|
|
311
|
-
noupsert(this.hashes, file, integrity)
|
|
312
|
-
const rel = relative(dir, file)
|
|
313
|
-
assert.ok(!rel.startsWith('..'))
|
|
314
|
-
|
|
315
|
-
if (this.config.frozen && (dir.includes('node_modules') || this.config.full)) {
|
|
316
|
-
assert.ok(Object.hasOwn(module.files, rel))
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!Object.hasOwn(module.files, rel)) module.files[rel] = integrity
|
|
320
|
-
assert.equal(module.files[rel], integrity)
|
|
321
|
-
|
|
322
|
-
if (this.config.bundle) {
|
|
323
|
-
if (isBinary) {
|
|
324
|
-
noupsert(this.resources, file, buf.toString('base64'))
|
|
325
|
-
} else {
|
|
326
|
-
noupsert(this.sources, file, typeof source === 'string' ? source : source.toString())
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (format) noupsert(this.formats, file, format)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
getFile(url) {
|
|
334
|
-
const file = this.relative(this.absolute(url))
|
|
335
|
-
const source = this.sources.get(file)
|
|
336
|
-
const format = this.formats.get(file) // might be undefined e.g. for some bundlers
|
|
337
|
-
assert.ok(source !== undefined)
|
|
338
|
-
// Without a lockfile the bundle is self-attesting: hashes would be derived
|
|
339
|
-
// from the same source bytes we'd then verify, which is a tautology.
|
|
340
|
-
if (this.config.useLockfile) assert.equal(this.hashes.get(file), sha512integrity(source))
|
|
341
|
-
return { source, format }
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
getFormat(url) {
|
|
345
|
-
return this.formats.get(this.relative(this.absolute(url)))
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Different conditions / import attributes can yield different URLs/formats for the same parent+specifier
|
|
349
|
-
#conditionsKey(conditions, importAttributes) {
|
|
350
|
-
const cond = conditions === '*' ? '*' : conditions.join(', ')
|
|
351
|
-
assert.ok(!cond.includes('(') && !cond.includes(')'), 'conditions must not contain "(" or ")"')
|
|
352
|
-
const attrs = importAttributes ? Object.entries(importAttributes) : []
|
|
353
|
-
if (attrs.length === 0) return cond
|
|
354
|
-
const sorted = Object.fromEntries(attrs.toSorted(([a], [b]) => (a < b ? -1 : 1)))
|
|
355
|
-
return `${cond} (with: ${JSON.stringify(sorted)})`
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
addImport(parentURL, specifier, url, { conditions = '*', format, importAttributes } = {}) {
|
|
359
|
-
if (conditions !== '*') assert.ok(Array.isArray(conditions))
|
|
360
|
-
assert.ok(parentURL, 'addImport requires a parent (entries go through addFile)')
|
|
361
|
-
const parent = this.relative(this.absolute(parentURL))
|
|
362
|
-
const file = this.relative(this.absolute(url))
|
|
363
|
-
const key = this.#conditionsKey(conditions, importAttributes)
|
|
364
|
-
|
|
365
|
-
if (!this.imports.has(key)) this.imports.set(key, new Map())
|
|
366
|
-
const imports = this.imports.get(key)
|
|
367
|
-
if (!imports.has(parent)) imports.set(parent, new Map())
|
|
368
|
-
const specifiers = imports.get(parent)
|
|
369
|
-
noupsert(specifiers, specifier, file)
|
|
370
|
-
if (format) noupsert(this.formats, file, format)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
getImport(parentURL, specifier, { conditions = '*', importAttributes } = {}) {
|
|
374
|
-
if (conditions !== '*') assert.ok(Array.isArray(conditions))
|
|
375
|
-
const parent = this.relative(this.absolute(parentURL))
|
|
376
|
-
const key = this.#conditionsKey(conditions, importAttributes)
|
|
377
|
-
const file = this.imports.get(key)?.get(parent)?.get(specifier)
|
|
378
|
-
assert.ok(file)
|
|
379
|
-
const url = pathToFileURL(resolve(this.root, file)).toString()
|
|
380
|
-
const format = this.formats.get(file) // might be undefined e.g. for some bundlers
|
|
381
|
-
return { url, format }
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
get lockData() {
|
|
385
|
-
return new Lockfile({
|
|
386
|
-
config: this.config.values,
|
|
387
|
-
entries: this.entries,
|
|
388
|
-
modules: this.modules,
|
|
389
|
-
}).serialize()
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Pair each module's recorded file list with the corresponding content from
|
|
393
|
-
// perFile (state.sources for code, state.resources for resources), dropping
|
|
394
|
-
// modules with no content. Bundle.serialize* then groups them into sources/
|
|
395
|
-
// modules buckets and writes the lockfile-shaped output.
|
|
396
|
-
#bundleModules(perFile) {
|
|
397
|
-
const modules = new Map()
|
|
398
|
-
for (const [dir, info] of this.modules) {
|
|
399
|
-
const files = {}
|
|
400
|
-
for (const rel of Object.keys(info.files)) {
|
|
401
|
-
const file = dir === '.' ? rel : `${dir}/${rel}`
|
|
402
|
-
const content = perFile.get(file)
|
|
403
|
-
if (content !== undefined) files[rel] = content
|
|
404
|
-
}
|
|
405
|
-
if (Object.keys(files).length > 0) {
|
|
406
|
-
modules.set(dir, { name: info.name, version: info.version, files })
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
return modules
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
get sourceData() {
|
|
413
|
-
return new Bundle({
|
|
414
|
-
config: this.config.values,
|
|
415
|
-
entries: this.entries,
|
|
416
|
-
modules: this.#bundleModules(this.sources),
|
|
417
|
-
formats: this.formats,
|
|
418
|
-
imports: this.imports,
|
|
419
|
-
}).serializeCode()
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
get resourceData() {
|
|
423
|
-
return new Bundle({
|
|
424
|
-
config: this.config.values,
|
|
425
|
-
modules: this.#bundleModules(this.resources),
|
|
426
|
-
}).serializeResources()
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
write() {
|
|
430
|
-
// writeFileSync(join(this.root, FILE_CONFIG), this.config.json)
|
|
431
|
-
if (this.config.writeLockfile) {
|
|
432
|
-
writeFileSync(join(this.root, FILE_LOCK), this.lockData)
|
|
433
|
-
}
|
|
434
|
-
if (this.config.writeBundle) {
|
|
435
|
-
const sourcesPath = this.config.bundleFile || join(this.root, FILE_CODE)
|
|
436
|
-
mkdirSync(dirname(sourcesPath), { recursive: true })
|
|
437
|
-
writeFileSync(sourcesPath, brotliCompressSync(this.sourceData))
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
package/src/state.util.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { hash } from 'node:crypto'
|
|
3
|
-
import { readFileSync } from 'node:fs'
|
|
4
|
-
import { join, sep } from 'node:path'
|
|
5
|
-
|
|
6
|
-
assert.equal(sep, '/', 'Not tested on Windows')
|
|
7
|
-
|
|
8
|
-
export const sha512integrity = (x) => `sha512-${hash('sha512', x, 'base64')}`
|
|
9
|
-
|
|
10
|
-
export function readFileSyncMaybe(dir, file, encoding) {
|
|
11
|
-
try {
|
|
12
|
-
return readFileSync(join(dir, file), encoding)
|
|
13
|
-
} catch (err) {
|
|
14
|
-
if (err.code === 'ENOENT') return null
|
|
15
|
-
throw err
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function noupsert(map, key, value) {
|
|
20
|
-
if (map.has(key)) {
|
|
21
|
-
assert.deepStrictEqual(map.get(key), value)
|
|
22
|
-
} else {
|
|
23
|
-
map.set(key, value)
|
|
24
|
-
}
|
|
25
|
-
}
|
package/src/util.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
const sep = '/'
|
|
2
|
-
|
|
3
|
-
export function assert(condition, msg) {
|
|
4
|
-
if (!condition) throw new Error(msg)
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function sortPaths(a, b) {
|
|
8
|
-
const [al, bl] = [a.split(sep), b.split(sep)]
|
|
9
|
-
while (al.length > 0 && al[0] === bl[0]) {
|
|
10
|
-
al.shift()
|
|
11
|
-
bl.shift()
|
|
12
|
-
}
|
|
13
|
-
if (al.length === 0 && bl.length === 0) return 0
|
|
14
|
-
|
|
15
|
-
// First process each file in dir, then subdirs
|
|
16
|
-
if (al.length < 2) return bl.length < 2 && al > bl ? 1 : -1
|
|
17
|
-
if (bl.length < 2) return 1
|
|
18
|
-
|
|
19
|
-
// * comes first
|
|
20
|
-
if (al[0] === '*') return -1
|
|
21
|
-
if (bl[0] === '*') return 1
|
|
22
|
-
|
|
23
|
-
// node_modules comes last
|
|
24
|
-
if (al[0] === 'node_modules') return 1
|
|
25
|
-
if (bl[0] === 'node_modules') return -1
|
|
26
|
-
|
|
27
|
-
// Prefer example/ over example-something/
|
|
28
|
-
const [an, bn] = [al, bl].map((list) => list.join(String.fromCodePoint(0)))
|
|
29
|
-
if (an < bn) return -1
|
|
30
|
-
if (an > bn) return 1
|
|
31
|
-
throw new Error('Unreachable')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export const isPlainObject = (x) => x && [null, Object.prototype].includes(Object.getPrototypeOf(x))
|
|
35
|
-
|
|
36
|
-
export const fromEntries = (entries) => Object.setPrototypeOf(Object.fromEntries(entries), null)
|
|
37
|
-
|
|
38
|
-
export const fileSetToObject = (set) => [...set].toSorted((a, b) => sortPaths(a, b))
|
|
39
|
-
|
|
40
|
-
export const fileMapToObject = (map) => fromEntries(
|
|
41
|
-
[...map]
|
|
42
|
-
.toSorted((a, b) => sortPaths(a[0], b[0]))
|
|
43
|
-
.map(([k, v]) => [k, v instanceof Map ? fileMapToObject(v) : v])
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
export const objectToMaps = (obj) => new Map(
|
|
47
|
-
Object.entries(obj).map(([k, v]) => [k, isPlainObject(v) ? objectToMaps(v) : v])
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
export function splitNodeModulesPath(path) {
|
|
51
|
-
const marker = 'node_modules/'
|
|
52
|
-
const idx = path.lastIndexOf(marker)
|
|
53
|
-
if (idx === -1) return null
|
|
54
|
-
const after = idx + marker.length
|
|
55
|
-
const parts = path.slice(after).split('/')
|
|
56
|
-
const pkgLen = parts[0].startsWith('@') ? 2 : 1
|
|
57
|
-
if (parts.length <= pkgLen || parts.slice(0, pkgLen).some((p) => !p)) return null
|
|
58
|
-
const name = parts.slice(0, pkgLen).join('/')
|
|
59
|
-
return { dir: path.slice(0, after) + name, rel: parts.slice(pkgLen).join('/'), name }
|
|
60
|
-
}
|