@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/audit.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { advisories } from './apis/npm/index.js'
|
|
2
|
+
import semver from './apis/npm/semver.cjs'
|
|
3
|
+
import { parseFile } from './parse.js'
|
|
4
|
+
|
|
5
|
+
// Only audit installed dependencies; workspace/first-party packages live under
|
|
6
|
+
// non-`node_modules` keys in .modules (Lockfile/Bundle merge sources in) and
|
|
7
|
+
// shouldn't be sent to the public registry — they leak names and produce noise.
|
|
8
|
+
export function collectPackagesFromFile(file) {
|
|
9
|
+
const out = []
|
|
10
|
+
for (const [dir, { name, version }] of parseFile(file).modules) {
|
|
11
|
+
if (!dir.includes('node_modules')) continue
|
|
12
|
+
if (name && version) out.push({ name, version })
|
|
13
|
+
}
|
|
14
|
+
return out
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function collectPackages(files) {
|
|
18
|
+
const seen = new Set()
|
|
19
|
+
const out = []
|
|
20
|
+
for (const file of files) {
|
|
21
|
+
for (const { name, version } of collectPackagesFromFile(file)) {
|
|
22
|
+
const key = `${name}@${version}`
|
|
23
|
+
if (seen.has(key)) continue
|
|
24
|
+
seen.add(key)
|
|
25
|
+
out.push({ name, version })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out.toSorted((a, b) => a.name.localeCompare(b.name) || semver.compare(a.version, b.version))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SEVERITY_ORDER = { critical: 0, high: 1, moderate: 2, low: 3, info: 4, none: 5 }
|
|
32
|
+
|
|
33
|
+
export function flattenAdvisories(result, packages = []) {
|
|
34
|
+
const installed = new Map()
|
|
35
|
+
for (const { name, version } of packages) {
|
|
36
|
+
if (!installed.has(name)) installed.set(name, [])
|
|
37
|
+
installed.get(name).push(version)
|
|
38
|
+
}
|
|
39
|
+
const rows = []
|
|
40
|
+
for (const [pkg, list] of Object.entries(result)) {
|
|
41
|
+
if (!Array.isArray(list)) continue
|
|
42
|
+
const installedVersions = installed.get(pkg) ?? []
|
|
43
|
+
for (const adv of list) {
|
|
44
|
+
const range = adv.vulnerable_versions ?? ''
|
|
45
|
+
const affected = range
|
|
46
|
+
? installedVersions.filter((v) => semver.satisfies(v, range))
|
|
47
|
+
: installedVersions
|
|
48
|
+
// npm occasionally returns advisories whose vulnerable range matches none
|
|
49
|
+
// of the versions we submitted; drop them so the table (and the CLI exit
|
|
50
|
+
// code) reflect only real hits.
|
|
51
|
+
if (installedVersions.length > 0 && affected.length === 0) continue
|
|
52
|
+
rows.push({
|
|
53
|
+
package: pkg,
|
|
54
|
+
installed: affected.join(', '),
|
|
55
|
+
vulnerable: range,
|
|
56
|
+
severity: adv.severity ?? '',
|
|
57
|
+
title: adv.title ?? '',
|
|
58
|
+
url: adv.url ?? '',
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
rows.sort((a, b) => {
|
|
63
|
+
const sa = SEVERITY_ORDER[a.severity] ?? 99
|
|
64
|
+
const sb = SEVERITY_ORDER[b.severity] ?? 99
|
|
65
|
+
if (sa !== sb) return sa - sb
|
|
66
|
+
if (a.package !== b.package) return a.package < b.package ? -1 : 1
|
|
67
|
+
return a.title < b.title ? -1 : 1
|
|
68
|
+
})
|
|
69
|
+
return rows
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Line breaks in npm advisory titles would otherwise break the box layout.
|
|
73
|
+
// Covers LF, bare CR (and CRLF), and U+2028 / U+2029 line/paragraph separators.
|
|
74
|
+
const cell = (v) => String(v ?? '').replace(/[\r\n\u2028\u2029]+/gu, ' ')
|
|
75
|
+
|
|
76
|
+
export function formatTable(rows, columns) {
|
|
77
|
+
if (rows.length === 0) return ''
|
|
78
|
+
const widths = columns.map((c) => Math.max(c.length, ...rows.map((r) => cell(r[c]).length)))
|
|
79
|
+
const pad = (s, w) => s.padEnd(w)
|
|
80
|
+
const line = (l, m, r, fill) => l + widths.map((w) => fill.repeat(w + 2)).join(m) + r
|
|
81
|
+
const row = (vals) => '│ ' + vals.map((v, i) => pad(cell(v), widths[i])).join(' │ ') + ' │'
|
|
82
|
+
return [
|
|
83
|
+
line('┌', '┬', '┐', '─'),
|
|
84
|
+
row(columns),
|
|
85
|
+
line('├', '┼', '┤', '─'),
|
|
86
|
+
...rows.map((r) => row(columns.map((c) => r[c]))),
|
|
87
|
+
line('└', '┴', '┘', '─'),
|
|
88
|
+
].join('\n')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function audit(files) {
|
|
92
|
+
const packages = collectPackages(files)
|
|
93
|
+
if (packages.length === 0) {
|
|
94
|
+
return { packages, advisories: {}, rows: [] }
|
|
95
|
+
}
|
|
96
|
+
const result = await advisories(packages)
|
|
97
|
+
const rows = flattenAdvisories(result, packages)
|
|
98
|
+
return { packages, advisories: result, rows }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function printAuditReport({ packages, rows }, { out = process.stdout, err = process.stderr } = {}) {
|
|
102
|
+
err.write(`Scanned ${packages.length} package${packages.length === 1 ? '' : 's'}\n`)
|
|
103
|
+
if (packages.length === 0) {
|
|
104
|
+
err.write('No node_modules entries found in the input files\n')
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
if (rows.length === 0) {
|
|
108
|
+
err.write('No advisories found\n')
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
out.write(formatTable(rows, ['severity', 'package', 'installed', 'vulnerable', 'title', 'url']) + '\n')
|
|
112
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { isUtf8 } from 'node:buffer'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
6
|
+
import { sha512integrity } from '@exodus/stasis-core/state-util'
|
|
7
|
+
import { moduleFileKey } from '@exodus/stasis-core/util'
|
|
8
|
+
|
|
9
|
+
// Reconstruct an in-memory Bundle from a lockfile by reading every file it attests
|
|
10
|
+
// from disk and verifying each against the lockfile's SRI digest -- the exact inverse
|
|
11
|
+
// of `extract`'s lockfileFromBundle. A changed or tampered file fails closed here (the
|
|
12
|
+
// digest won't match), so building from a lockfile is as trustworthy as a
|
|
13
|
+
// `stasis run --lock=frozen` replay: the build only ever sees bytes the lockfile
|
|
14
|
+
// vouches for. `root` is the project root the lockfile's paths are relative to.
|
|
15
|
+
//
|
|
16
|
+
// Directory captures (`stasis run --fs` readdir listings, format 'directory') are
|
|
17
|
+
// skipped: they key a listing at a directory's own path, not a file esbuild could load.
|
|
18
|
+
export function bundleFromLockfile(lockfile, { root }) {
|
|
19
|
+
if (!lockfile.imports) throw new Error('stasis: lockfile has no recorded import graph')
|
|
20
|
+
const formats = lockfile.formats ?? new Map()
|
|
21
|
+
const modules = new Map()
|
|
22
|
+
for (const [dir, { name, version, ecosystem, files }] of lockfile.modules) {
|
|
23
|
+
const out = Object.create(null)
|
|
24
|
+
for (const [rel, integrity] of Object.entries(files)) {
|
|
25
|
+
const file = moduleFileKey(dir, rel)
|
|
26
|
+
const format = formats.get(file)
|
|
27
|
+
if (format === 'directory') continue
|
|
28
|
+
const abs = resolve(root, file)
|
|
29
|
+
let bytes
|
|
30
|
+
try {
|
|
31
|
+
bytes = readFileSync(abs)
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
throw new Error(`stasis: file attested by the lockfile is missing on disk: ${file}`, { cause })
|
|
34
|
+
}
|
|
35
|
+
// The lockfile digests raw on-disk bytes; verify against them before trusting
|
|
36
|
+
// the file. (Code and 'resource' files hash identically whether viewed as a
|
|
37
|
+
// string or bytes, since Node hashes a string as its UTF-8 bytes.)
|
|
38
|
+
const actual = sha512integrity(bytes)
|
|
39
|
+
if (actual !== integrity) {
|
|
40
|
+
throw new Error(`stasis: ${file} does not match the lockfile (expected ${integrity}, got ${actual})`)
|
|
41
|
+
}
|
|
42
|
+
// Store content the way a Bundle does: 'resource:base64' as base64 text,
|
|
43
|
+
// everything else (code, 'resource') as a raw UTF-8 string. A non-base64 entry is
|
|
44
|
+
// digested AND served as UTF-8 text, so a non-UTF-8 byte would transcode lossily
|
|
45
|
+
// (U+FFFD), diverging the served content from the verified bytes -- mirror the
|
|
46
|
+
// capture side's isUtf8 guard and fail closed instead.
|
|
47
|
+
if (format === 'resource:base64') {
|
|
48
|
+
out[rel] = bytes.toString('base64')
|
|
49
|
+
} else {
|
|
50
|
+
if (!isUtf8(bytes)) throw new Error(`stasis: ${file} is recorded as '${format ?? 'code'}' but is not valid UTF-8`)
|
|
51
|
+
out[rel] = bytes.toString('utf8')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
modules.set(dir, ecosystem === undefined
|
|
55
|
+
? { name, version, files: out }
|
|
56
|
+
: { name, version, ecosystem, files: out })
|
|
57
|
+
}
|
|
58
|
+
return new Bundle({
|
|
59
|
+
config: lockfile.config,
|
|
60
|
+
entries: lockfile.entries,
|
|
61
|
+
modules,
|
|
62
|
+
imports: lockfile.imports,
|
|
63
|
+
formats,
|
|
64
|
+
})
|
|
65
|
+
}
|
package/src/bundle.js
CHANGED
|
@@ -1,217 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
fromEntries,
|
|
6
|
-
objectToMaps,
|
|
7
|
-
sortPaths,
|
|
8
|
-
splitNodeModulesPath,
|
|
9
|
-
} from './util.js'
|
|
10
|
-
|
|
11
|
-
const VERSION = 1
|
|
12
|
-
const LEGACY_VERSION = 0
|
|
13
|
-
|
|
14
|
-
const normalize = ({ name, version, files }) =>
|
|
15
|
-
({ name, version, files: fromEntries(Object.entries(files)) })
|
|
16
|
-
|
|
17
|
-
const inferModuleDir = (path) =>
|
|
18
|
-
splitNodeModulesPath(path) ?? { dir: '.', rel: path, name: null }
|
|
19
|
-
|
|
20
|
-
// Bundle handles the JSON shape of stasis.code.br / stasis.resources.br;
|
|
21
|
-
// callers (State) are responsible for the brotli wrapper around the JSON
|
|
22
|
-
// text. On-disk version:
|
|
23
|
-
// v0 — legacy flat layout: top-level `sources` (or `resources`) keyed by
|
|
24
|
-
// project-relative path. No entries/modules metadata.
|
|
25
|
-
// v1 — current layout, mirrors stasis.lock.json: per-package buckets under
|
|
26
|
-
// `sources` (workspace) and `modules` (node_modules) with name+version+files,
|
|
27
|
-
// plus `entries`. File payloads carry bytes (UTF-8 source strings or base64
|
|
28
|
-
// resource blobs) instead of SRI digests.
|
|
29
|
-
// We load both v0 and v1; we always serialize v1. v0 is converted on load to
|
|
30
|
-
// the same in-memory shape — paths are regrouped by package dir via
|
|
31
|
-
// `inferModuleDir` (best-effort, last-wins). name/version stay null since v0
|
|
32
|
-
// records neither, which State uses as a marker to skip metadata cross-checks.
|
|
33
|
-
export class Bundle {
|
|
34
|
-
static VERSION = VERSION
|
|
35
|
-
|
|
36
|
-
// The on-disk version this instance represents. Defaults to VERSION; parse*
|
|
37
|
-
// sets it to whatever the file declared (LEGACY_VERSION for v0 input).
|
|
38
|
-
// Serialization always writes the current VERSION regardless — loading v0 and
|
|
39
|
-
// re-saving promotes the bundle to v1.
|
|
40
|
-
version = VERSION
|
|
41
|
-
config
|
|
42
|
-
entries
|
|
43
|
-
modules
|
|
44
|
-
formats
|
|
45
|
-
imports
|
|
46
|
-
|
|
47
|
-
constructor({ config = { scope: 'full' }, entries, modules, formats, imports, version = VERSION } = {}) {
|
|
48
|
-
assert([LEGACY_VERSION, VERSION].includes(version))
|
|
49
|
-
assert(['node_modules', 'full'].includes(config.scope))
|
|
50
|
-
this.version = version
|
|
51
|
-
this.config = config
|
|
52
|
-
this.entries = entries ?? new Set()
|
|
53
|
-
this.modules = modules ?? new Map()
|
|
54
|
-
this.formats = formats ?? new Map()
|
|
55
|
-
this.imports = imports ?? new Map()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Flat project-relative view of file contents collected from this.modules.
|
|
59
|
-
// Values are UTF-8 source strings for code bundles, base64 for resource bundles.
|
|
60
|
-
get sources() {
|
|
61
|
-
const m = new Map()
|
|
62
|
-
for (const [dir, { files }] of this.modules) {
|
|
63
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
64
|
-
m.set(dir === '.' ? rel : `${dir}/${rel}`, content)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return m
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
static parseCode(text) {
|
|
71
|
-
const json = JSON.parse(text)
|
|
72
|
-
assert(json.version === VERSION || json.version === LEGACY_VERSION)
|
|
73
|
-
assert(['node_modules', 'full'].includes(json.config?.scope))
|
|
74
|
-
assert(json.formats)
|
|
75
|
-
assert(json.imports)
|
|
76
|
-
|
|
77
|
-
const modules = new Map()
|
|
78
|
-
let entries = new Set()
|
|
79
|
-
|
|
80
|
-
if (json.version === VERSION) {
|
|
81
|
-
const full = json.config.scope === 'full'
|
|
82
|
-
if (json.modules !== undefined) {
|
|
83
|
-
assert(typeof json.modules === 'object' && json.modules !== null)
|
|
84
|
-
for (const [dir, info] of Object.entries(json.modules)) {
|
|
85
|
-
assert(dir.includes('node_modules'))
|
|
86
|
-
assert(!dir.startsWith('..'))
|
|
87
|
-
assert(info?.name && info.version && info.files)
|
|
88
|
-
modules.set(dir, normalize(info))
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
if (full) {
|
|
92
|
-
assert(Array.isArray(json.entries))
|
|
93
|
-
// Empty entries in v1 + full would let `state.assertEntry` skip its check
|
|
94
|
-
// (it short-circuits on entries.size === 0 for v0+no-lockfile compatibility).
|
|
95
|
-
// Forbid the shape at parse time so tampered bundles can't exploit that path.
|
|
96
|
-
assert(json.entries.length > 0, 'v1 bundle with scope=full must have at least one entry')
|
|
97
|
-
assert(json.sources && typeof json.sources === 'object')
|
|
98
|
-
entries = new Set(json.entries)
|
|
99
|
-
for (const [dir, info] of Object.entries(json.sources)) {
|
|
100
|
-
assert(!dir.includes('node_modules'))
|
|
101
|
-
assert(!dir.startsWith('..'))
|
|
102
|
-
assert(info?.name && info.version && info.files)
|
|
103
|
-
modules.set(dir, normalize(info))
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
assert(json.entries === undefined)
|
|
107
|
-
assert(json.sources === undefined)
|
|
108
|
-
}
|
|
109
|
-
for (const [, { files }] of modules) {
|
|
110
|
-
for (const rel of Object.keys(files)) assert(!rel.startsWith('..'))
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
// v0 legacy: flat sources at top level, regrouped by inferred module dir.
|
|
114
|
-
assert(json.sources)
|
|
115
|
-
for (const [path, content] of Object.entries(json.sources)) {
|
|
116
|
-
assert(!path.startsWith('..'))
|
|
117
|
-
const { dir, rel, name } = inferModuleDir(path)
|
|
118
|
-
assert(!dir.startsWith('..') && !rel.startsWith('..'))
|
|
119
|
-
if (!modules.has(dir)) modules.set(dir, { name, version: null, files: Object.create(null) })
|
|
120
|
-
modules.get(dir).files[rel] = content
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return new Bundle({
|
|
125
|
-
version: json.version,
|
|
126
|
-
config: json.config,
|
|
127
|
-
entries,
|
|
128
|
-
modules,
|
|
129
|
-
formats: new Map(Object.entries(json.formats)),
|
|
130
|
-
imports: objectToMaps(json.imports),
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
static parseResources(text) {
|
|
135
|
-
const json = JSON.parse(text)
|
|
136
|
-
assert(json.version === VERSION || json.version === LEGACY_VERSION)
|
|
137
|
-
|
|
138
|
-
const modules = new Map()
|
|
139
|
-
let config = { scope: 'full' }
|
|
140
|
-
|
|
141
|
-
if (json.version === VERSION) {
|
|
142
|
-
assert(['node_modules', 'full'].includes(json.config?.scope))
|
|
143
|
-
config = json.config
|
|
144
|
-
const full = config.scope === 'full'
|
|
145
|
-
if (json.modules !== undefined) {
|
|
146
|
-
assert(typeof json.modules === 'object' && json.modules !== null)
|
|
147
|
-
for (const [dir, info] of Object.entries(json.modules)) {
|
|
148
|
-
assert(dir.includes('node_modules'))
|
|
149
|
-
assert(!dir.startsWith('..'))
|
|
150
|
-
assert(info?.name && info.version && info.files)
|
|
151
|
-
modules.set(dir, normalize(info))
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (full && json.sources !== undefined) {
|
|
155
|
-
assert(typeof json.sources === 'object' && json.sources !== null)
|
|
156
|
-
for (const [dir, info] of Object.entries(json.sources)) {
|
|
157
|
-
assert(!dir.includes('node_modules'))
|
|
158
|
-
assert(!dir.startsWith('..'))
|
|
159
|
-
assert(info?.name && info.version && info.files)
|
|
160
|
-
modules.set(dir, normalize(info))
|
|
161
|
-
}
|
|
162
|
-
} else if (!full) {
|
|
163
|
-
assert(json.sources === undefined)
|
|
164
|
-
}
|
|
165
|
-
for (const [, { files }] of modules) {
|
|
166
|
-
for (const rel of Object.keys(files)) assert(!rel.startsWith('..'))
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
// v0 legacy: flat resources at top level, regrouped by inferred module dir.
|
|
170
|
-
assert(json.resources)
|
|
171
|
-
for (const [path, content] of Object.entries(json.resources)) {
|
|
172
|
-
assert(!path.startsWith('..'))
|
|
173
|
-
const { dir, rel, name } = inferModuleDir(path)
|
|
174
|
-
assert(!dir.startsWith('..') && !rel.startsWith('..'))
|
|
175
|
-
if (!modules.has(dir)) modules.set(dir, { name, version: null, files: Object.create(null) })
|
|
176
|
-
modules.get(dir).files[rel] = content
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return new Bundle({ version: json.version, config, modules })
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
#groupedFromModules() {
|
|
184
|
-
const moduleEntries = []
|
|
185
|
-
const sourceEntries = []
|
|
186
|
-
for (const [dir, { name, version, files }] of this.modules) {
|
|
187
|
-
if (Object.keys(files).length === 0) continue
|
|
188
|
-
const inNodeModules = dir.includes('node_modules')
|
|
189
|
-
if (inNodeModules) assert(name && version && files)
|
|
190
|
-
const sorted = fromEntries(Object.entries(files).toSorted((a, b) => sortPaths(a[0], b[0])))
|
|
191
|
-
const target = inNodeModules ? moduleEntries : sourceEntries
|
|
192
|
-
target.push([dir, { name, version, files: sorted }])
|
|
193
|
-
}
|
|
194
|
-
moduleEntries.sort((a, b) => sortPaths(a[0], b[0]))
|
|
195
|
-
sourceEntries.sort((a, b) => sortPaths(a[0], b[0]))
|
|
196
|
-
return { modules: fromEntries(moduleEntries), sources: fromEntries(sourceEntries) }
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
serializeCode() {
|
|
200
|
-
const entries = fileSetToObject(this.entries)
|
|
201
|
-
const { modules, sources } = this.#groupedFromModules()
|
|
202
|
-
const formats = fileMapToObject(this.formats)
|
|
203
|
-
const imports = fileMapToObject(this.imports)
|
|
204
|
-
const data = { version: VERSION, config: this.config }
|
|
205
|
-
if (this.config.scope === 'full') Object.assign(data, { entries, sources })
|
|
206
|
-
Object.assign(data, { modules, formats, imports })
|
|
207
|
-
return JSON.stringify(data, undefined, 2)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
serializeResources() {
|
|
211
|
-
const { modules, sources } = this.#groupedFromModules()
|
|
212
|
-
const data = { version: VERSION, config: this.config }
|
|
213
|
-
if (this.config.scope === 'full') Object.assign(data, { sources })
|
|
214
|
-
Object.assign(data, { modules })
|
|
215
|
-
return JSON.stringify(data, undefined, 2)
|
|
216
|
-
}
|
|
217
|
-
}
|
|
1
|
+
// Public-export adapter: backs `@exodus/stasis/bundle`. The implementation lives in
|
|
2
|
+
// @exodus/stasis-core; internal tooling and tests import `@exodus/stasis-core/bundle`
|
|
3
|
+
// directly, so this file exists only to back the package's `exports` map.
|
|
4
|
+
export * from '@exodus/stasis-core/bundle'
|