@exodus/stasis 1.0.0-beta.2 → 1.0.0-beta.3
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 +16 -2
- package/bin/stasis.js +153 -167
- package/package.json +11 -5
- package/src/apis/npm/semver.cjs +0 -2
- package/src/audit.js +161 -22
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +12 -18
- package/src/cmd/build.js +92 -123
- package/src/cmd/bundle.js +469 -428
- package/src/cmd/diff.js +2 -19
- package/src/cmd/sbom.js +1 -7
- package/src/diff.js +61 -118
- package/src/loader-mock.js +8 -15
- package/src/loaders/bash.js +33 -81
- package/src/loaders/php.js +143 -282
- package/src/loaders/rust.js +37 -93
- package/src/loaders/solidity.js +27 -52
- package/src/metro-resolver.js +165 -0
- package/src/mock.js +54 -160
- package/src/parse.js +4 -16
- package/src/sbom.js +37 -79
- package/src/scan.js +166 -124
- package/src/cmd/extract.js +0 -193
package/src/cmd/build.js
CHANGED
|
@@ -7,42 +7,29 @@ import { spawnSync } from 'node:child_process'
|
|
|
7
7
|
import { Bundle } from '@exodus/stasis-core/bundle'
|
|
8
8
|
import { State } from '@exodus/stasis-core/state'
|
|
9
9
|
import { StasisEsbuild } from '@exodus/stasis-plugins/esbuild'
|
|
10
|
+
import { HERMES_UNSUPPORTED, createBabelConfigTransform, createHermesTransform } from '../babel.js'
|
|
10
11
|
import { bundleFromLockfile } from '../bundle-from-lockfile.js'
|
|
11
12
|
import { parseFileWithKind } from '../parse.js'
|
|
12
13
|
|
|
13
|
-
// Re-exported so `@exodus/stasis/cmd/build` keeps surfacing the lockfile->bundle
|
|
14
|
-
// reconstruction alongside the command; the implementation lives in its own module.
|
|
15
14
|
export { bundleFromLockfile }
|
|
16
15
|
|
|
17
|
-
// JS/TS entry extensions esbuild can build. esbuild handles JSX/TSX natively (loader
|
|
18
|
-
// 'jsx'/'tsx', derived from the extension by the load plugin), and stasis-core already
|
|
19
|
-
// classifies .jsx/.tsx as code (CODE_EXTENSIONS), so a bundle/lockfile captured by the
|
|
20
|
-
// esbuild/webpack plugins can carry them as entries.
|
|
21
16
|
const JS_EXTS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'])
|
|
22
17
|
|
|
23
|
-
// Allowlist (
|
|
24
|
-
//
|
|
25
|
-
// directory payloads and the payload-free stat records (`--fs` lstat/stat captures) --
|
|
26
|
-
// that are inert unless the SELECTED entry imports them. An allowlist
|
|
27
|
-
// so a new language format added to `stasis bundle` later (e.g. a future `go`) is rejected
|
|
28
|
-
// here by default, rather than slipping through to a cryptic esbuild failure in the build.
|
|
18
|
+
// Allowlist (not blocklist) of per-file formats `stasis build` accepts, so a bundle
|
|
19
|
+
// format added later fails closed here rather than in a cryptic esbuild error.
|
|
29
20
|
const BUILDABLE_FORMATS = new Set([
|
|
30
21
|
'module', 'commonjs', 'json', 'module-typescript', 'commonjs-typescript',
|
|
31
22
|
'resource', 'resource:base64', 'directory', 'stat:file', 'stat:directory',
|
|
32
23
|
])
|
|
33
24
|
|
|
34
|
-
// esbuild is an
|
|
35
|
-
//
|
|
36
|
-
// from the local install first (a peer dep hoisted into the project's node_modules, or
|
|
37
|
-
// an npx-provided copy), then fall back to a global install, with an actionable error
|
|
38
|
-
// if neither is present.
|
|
25
|
+
// esbuild is an optional peer dep; resolve it from the local install, else fall back
|
|
26
|
+
// to a global one.
|
|
39
27
|
async function importEsbuild() {
|
|
40
28
|
let mod
|
|
41
29
|
try {
|
|
42
30
|
mod = await import('esbuild')
|
|
43
31
|
} catch (cause) {
|
|
44
|
-
// Only a genuine "not installed" miss falls through to the global lookup;
|
|
45
|
-
// different failure (a corrupt install, a syntax error in esbuild) is surfaced.
|
|
32
|
+
// Only a genuine "not installed" miss falls through to the global lookup; surface other failures.
|
|
46
33
|
if (cause?.code !== 'ERR_MODULE_NOT_FOUND' && cause?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
47
34
|
throw cause
|
|
48
35
|
}
|
|
@@ -57,14 +44,10 @@ async function importEsbuild() {
|
|
|
57
44
|
}
|
|
58
45
|
mod = await import(pathToFileURL(globalMain).href)
|
|
59
46
|
}
|
|
60
|
-
//
|
|
61
|
-
// interop surfaces them directly. A globally-imported CJS copy may instead land its
|
|
62
|
-
// exports on `default` -- accept both shapes.
|
|
47
|
+
// A globally-imported CJS copy may land its exports on `default` -- accept both shapes.
|
|
63
48
|
return typeof mod.build === 'function' ? mod : mod.default
|
|
64
49
|
}
|
|
65
50
|
|
|
66
|
-
// Locate a globally-installed esbuild via the package managers' "global root" commands.
|
|
67
|
-
// Returns the absolute path to esbuild's entry, or undefined when none is installed.
|
|
68
51
|
function resolveGlobalEsbuild() {
|
|
69
52
|
const probes = [
|
|
70
53
|
['npm', ['root', '-g']],
|
|
@@ -76,7 +59,7 @@ function resolveGlobalEsbuild() {
|
|
|
76
59
|
try {
|
|
77
60
|
out = spawnSync(cmd, args, { encoding: 'utf8' })
|
|
78
61
|
} catch {
|
|
79
|
-
continue
|
|
62
|
+
continue
|
|
80
63
|
}
|
|
81
64
|
if (!out || out.status !== 0 || !out.stdout) continue
|
|
82
65
|
const base = out.stdout.trim()
|
|
@@ -88,61 +71,77 @@ function resolveGlobalEsbuild() {
|
|
|
88
71
|
try {
|
|
89
72
|
return createRequire(join(nm, 'noop.js')).resolve('esbuild')
|
|
90
73
|
} catch {
|
|
91
|
-
// keep probing
|
|
92
74
|
}
|
|
93
75
|
}
|
|
94
76
|
}
|
|
95
77
|
return undefined
|
|
96
78
|
}
|
|
97
79
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
function assertBuildable(artifact, label) {
|
|
80
|
+
// Preconditions independent of which entry we build. Per-entry language checks are deferred
|
|
81
|
+
// to assertBuildableEntry, once selectEntry has picked one.
|
|
82
|
+
function assertBuildableArtifact(artifact, label) {
|
|
102
83
|
if (artifact.config.scope !== 'full') {
|
|
103
84
|
throw new Error(
|
|
104
85
|
`stasis build requires a full-scope artifact; ${label} is scope='${artifact.config.scope}'. ` +
|
|
105
86
|
`node_modules-scope artifacts omit the entry and workspace code, so there is nothing to build.`
|
|
106
87
|
)
|
|
107
88
|
}
|
|
108
|
-
|
|
109
|
-
if (entries.length === 0) {
|
|
89
|
+
if (artifact.entries.size === 0) {
|
|
110
90
|
throw new Error(`stasis build: ${label} declares no entries to build`)
|
|
111
91
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The entry plus its transitive imports in the recorded graph -- the exact set esbuild loads
|
|
95
|
+
// (all conditions buckets; a --metro edge's per-platform targets included).
|
|
96
|
+
function reachableFiles(entry, imports) {
|
|
97
|
+
const seen = new Set([entry])
|
|
98
|
+
const stack = [entry]
|
|
99
|
+
while (stack.length > 0) {
|
|
100
|
+
const parent = stack.pop()
|
|
101
|
+
for (const [, byParent] of imports) {
|
|
102
|
+
const specifiers = byParent.get(parent)
|
|
103
|
+
if (specifiers === undefined) continue
|
|
104
|
+
for (const [, target] of specifiers) {
|
|
105
|
+
// A plain edge is a file string; a --metro edge is a {platform: file} Map.
|
|
106
|
+
const targets = typeof target === 'string' ? [target] : [...target.values()]
|
|
107
|
+
for (const file of targets) {
|
|
108
|
+
if (!seen.has(file)) {
|
|
109
|
+
seen.add(file)
|
|
110
|
+
stack.push(file)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
115
114
|
}
|
|
116
115
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
116
|
+
return seen
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Language checks scoped to the built entry: it must be JS/TS, and every file reachable from
|
|
120
|
+
// it must be buildable. A non-JS file elsewhere (another entry, or a `stasis add` attachment)
|
|
121
|
+
// isn't reached, so it doesn't block the build. imports/formats are null on a legacy lockfile.
|
|
122
|
+
function assertBuildableEntry(artifact, entry, label) {
|
|
123
|
+
if (!JS_EXTS.has(extname(entry))) {
|
|
124
|
+
throw new Error(`stasis build only builds JavaScript/TypeScript entries (esbuild); got non-JS entry '${entry}' in ${label}`)
|
|
125
|
+
}
|
|
126
|
+
const reachable = reachableFiles(entry, artifact.imports ?? new Map())
|
|
127
|
+
// A reachable resource (plugin captures DO record asset edges) stays allowed via BUILDABLE_FORMATS;
|
|
128
|
+
// an entry that imports an unbuildable asset fails later at esbuild, not here.
|
|
126
129
|
for (const [file, format] of artifact.formats ?? new Map()) {
|
|
130
|
+
if (!reachable.has(file)) continue
|
|
127
131
|
if (!BUILDABLE_FORMATS.has(format)) {
|
|
128
132
|
throw new Error(`stasis build only supports JavaScript/TypeScript bundles (esbuild); ${label} carries a '${format}' file (${file})`)
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
// Pick which recorded entry to build
|
|
134
|
-
//
|
|
135
|
-
// entry is used); with several it is required, and an absent or unknown one is rejected
|
|
136
|
-
// with the available list. The selector is matched against the artifact's recorded
|
|
137
|
-
// (project-relative) entries -- only those are attested as graph roots, and the load
|
|
138
|
-
// plugin's assertEntry rejects anything else -- accepting the recorded path verbatim, a
|
|
139
|
-
// leading `./` stripped, or a cwd-relative path normalized into the project root.
|
|
137
|
+
// Pick which recorded entry to build; required only when several are recorded. Matched
|
|
138
|
+
// against the recorded project-relative entries (verbatim, `./`-stripped, or cwd-relative).
|
|
140
139
|
function selectEntry(entryArg, entries, { cwd, root, label }) {
|
|
141
140
|
const recorded = [...entries]
|
|
142
141
|
const list = recorded.map((e) => ` ${e}`).join('\n')
|
|
143
142
|
if (entryArg === undefined) {
|
|
144
143
|
if (recorded.length === 1) return recorded[0]
|
|
145
|
-
// length === 0 is impossible here (
|
|
144
|
+
// length === 0 is impossible here (assertBuildableArtifact required >= 1 entry).
|
|
146
145
|
throw new Error(`stasis build: ${label} has ${recorded.length} entry points; pass one as the second argument:\n${list}`)
|
|
147
146
|
}
|
|
148
147
|
const candidates = new Set([
|
|
@@ -157,13 +156,8 @@ function selectEntry(entryArg, entries, { cwd, root, label }) {
|
|
|
157
156
|
return match
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
// Build a bundle-load State that serves an in-memory Bundle
|
|
161
|
-
//
|
|
162
|
-
// state.entries). skipDiscovery anchors the State at `root` and does NO filesystem discovery:
|
|
163
|
-
// no package.json walk, no on-disk config, no stray stasis.code.br read. bundle=load +
|
|
164
|
-
// lock=ignore serve the bundle's own bytes (self-attesting -- the lockfile path already
|
|
165
|
-
// SHA-512-verified them in bundleFromLockfile). We mirror State's on-disk load (split
|
|
166
|
-
// sources/resources by format) and fill the maps from `bundle`.
|
|
159
|
+
// Build a bundle-load State that serves an in-memory Bundle. skipDiscovery does NO
|
|
160
|
+
// filesystem discovery (no package.json walk, no on-disk config, no stray stasis.code.br).
|
|
167
161
|
function loadStateForBundle(root, bundle) {
|
|
168
162
|
const state = new State(root, { bundle: 'load', lock: 'ignore', scope: bundle.config.scope, skipDiscovery: true })
|
|
169
163
|
const sources = new Map()
|
|
@@ -177,50 +171,23 @@ function loadStateForBundle(root, bundle) {
|
|
|
177
171
|
state.formats = bundle.formats
|
|
178
172
|
state.imports = bundle.imports
|
|
179
173
|
state.entries = new Set(bundle.entries)
|
|
180
|
-
// Not state.
|
|
181
|
-
//
|
|
174
|
+
// Not state.executable either: like state.modules below, the load path never reads it, and every
|
|
175
|
+
// consumer narrows against state.modules -- which is empty here -- so copying it would be inert.
|
|
176
|
+
// Not state.modules: the load path never reads it (only the capture paths write it).
|
|
182
177
|
return state
|
|
183
178
|
}
|
|
184
179
|
|
|
185
|
-
// Run
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// recorded import graph EXACTLY rather than esbuild's own resolver.
|
|
189
|
-
//
|
|
190
|
-
// `entry` selects which recorded entry to build (see selectEntry): optional when the
|
|
191
|
-
// artifact records one, required when it records several. `output` is required: a path
|
|
192
|
-
// ending in `.js`/`.cjs`/`.mjs` writes a single file, anything else a directory.
|
|
193
|
-
//
|
|
194
|
-
// The build runs entirely in memory off the artifact -- no temp files, and no package.json
|
|
195
|
-
// required. The artifact's relative paths anchor at the artifact's OWN directory: a bundle is
|
|
196
|
-
// self-contained (root is just a keying prefix; nothing is read from disk), and a lockfile
|
|
197
|
-
// records its paths relative to where it sits (its project root), so its sources are read +
|
|
198
|
-
// verified from there. A project stasis.config.json is not consulted -- the build is driven
|
|
199
|
-
// by the artifact, not by `stasis run` config.
|
|
200
|
-
//
|
|
201
|
-
// The recorded graph is forced via the stasis esbuild plugin in bundle-load mode: each import
|
|
202
|
-
// resolves to the edge stasis recorded (a specifier the artifact doesn't record is handed back
|
|
203
|
-
// to esbuild, which externalizes it like a built-in/`external`), and each module's bytes come
|
|
204
|
-
// from the artifact -- so the output reflects what stasis attested, not what's on disk.
|
|
205
|
-
//
|
|
206
|
-
// `format` (esm/cjs/iife, default esm), `platform` (node/browser/neutral, default node),
|
|
207
|
-
// `minify`, `sourcemap`, `define` (an esbuild define map), `external` (an array of esbuild
|
|
208
|
-
// external patterns), and `loader` (an esbuild extension->loader map, e.g. { '.js': 'jsx' }
|
|
209
|
-
// to parse JSX in .js files) are forwarded to esbuild; the import graph is fixed by the artifact.
|
|
210
|
-
//
|
|
211
|
-
// `external` is esbuild's native option: it only steers specifiers esbuild itself would
|
|
212
|
-
// resolve -- i.e. the ones the bundle does NOT record (handed back to esbuild as a miss,
|
|
213
|
-
// typically the externals that existed at capture time). It re-declares those so esbuild
|
|
214
|
-
// leaves them as runtime imports instead of trying (and failing) to resolve them on disk.
|
|
215
|
-
// A specifier the artifact DOES record is still followed from the artifact -- the recorded
|
|
216
|
-
// graph wins, so `external` never silently un-inlines an attested edge.
|
|
180
|
+
// Run `stasis build`: run esbuild over one recorded entry, following the artifact's
|
|
181
|
+
// recorded import graph EXACTLY (not esbuild's resolver). A specifier the artifact records
|
|
182
|
+
// is always followed from the artifact; `external` only steers the specifiers it doesn't.
|
|
217
183
|
export async function buildCommand({
|
|
218
184
|
cwd = process.cwd(),
|
|
219
185
|
artifact,
|
|
220
186
|
entry,
|
|
221
187
|
output,
|
|
222
|
-
format
|
|
188
|
+
format,
|
|
223
189
|
platform = 'node',
|
|
190
|
+
babel = false,
|
|
224
191
|
minify = false,
|
|
225
192
|
sourcemap = false,
|
|
226
193
|
define = {},
|
|
@@ -230,36 +197,43 @@ export async function buildCommand({
|
|
|
230
197
|
if (!artifact) throw new Error('stasis build: an artifact (bundle or lockfile) is required')
|
|
231
198
|
if (!output) throw new Error('stasis build: --output is required (a .js/.cjs/.mjs file, or a directory)')
|
|
232
199
|
if (!Array.isArray(external)) throw new Error('stasis build: external must be an array of esbuild patterns')
|
|
200
|
+
// --platform=hermes is a stasis-level target, not an esbuild platform (esbuild gets
|
|
201
|
+
// 'neutral'): every code file is downleveled to Hermes' dialect before the final bundle,
|
|
202
|
+
// and the bundle itself must avoid ESM -- Hermes has no import/export.
|
|
203
|
+
const hermes = platform === 'hermes'
|
|
204
|
+
format ??= hermes ? 'iife' : 'esm'
|
|
205
|
+
if (hermes && format === 'esm') {
|
|
206
|
+
throw new Error('stasis build: --platform=hermes cannot emit ESM (Hermes has no import/export); use --format=iife (the hermes default) or cjs')
|
|
207
|
+
}
|
|
233
208
|
const artifactAbs = resolve(cwd, artifact)
|
|
234
209
|
const { kind, artifact: parsed } = parseFileWithKind(artifactAbs)
|
|
235
|
-
|
|
210
|
+
assertBuildableArtifact(parsed, artifact)
|
|
236
211
|
|
|
237
|
-
// Anchor
|
|
238
|
-
//
|
|
239
|
-
// records its paths relative to where it sits -- its project root -- so its sources are
|
|
240
|
-
// read + verified from there. No package.json discovery, no project stasis.config.json.
|
|
212
|
+
// Anchor artifact-relative paths at the artifact's own directory; a lockfile's sources
|
|
213
|
+
// are read + verified from there.
|
|
241
214
|
const root = dirname(artifactAbs)
|
|
242
215
|
const selectedEntry = selectEntry(entry, parsed.entries, { cwd, root, label: artifact })
|
|
216
|
+
assertBuildableEntry(parsed, selectedEntry, artifact)
|
|
243
217
|
|
|
244
|
-
// The Bundle the load State serves, entirely in memory. A bundle artifact is already a
|
|
245
|
-
// parsed Bundle; a lockfile is reconstructed into one by reading + SHA-512-verifying its
|
|
246
|
-
// attested files from disk (bundleFromLockfile). The plugin drives off the in-memory State.
|
|
247
218
|
const bundle = kind === 'lockfile' ? bundleFromLockfile(parsed, { root }) : parsed
|
|
248
219
|
const state = loadStateForBundle(root, bundle)
|
|
249
|
-
//
|
|
250
|
-
// State's root so esbuild's entry resolution is independent of the process cwd.
|
|
220
|
+
// Make the entry absolute against the State's root so resolution is independent of process cwd.
|
|
251
221
|
const entryPoint = resolve(state.root, selectedEntry)
|
|
252
222
|
|
|
253
223
|
const esbuild = await importEsbuild()
|
|
224
|
+
// Per-file code rewrites the plugin applies before esbuild parses a served file:
|
|
225
|
+
// --babel runs the project's own babel.config.* over the original source (no esbuild
|
|
226
|
+
// pre-transform), and otherwise --platform=hermes uses the built-in downlevel pipeline.
|
|
227
|
+
// Both resolve Babel from the project (cwd), never from stasis's own dependencies.
|
|
228
|
+
let transform
|
|
229
|
+
if (babel) transform = createBabelConfigTransform({ projectDir: cwd })
|
|
230
|
+
else if (hermes) transform = createHermesTransform({ projectDir: cwd, esbuild })
|
|
231
|
+
|
|
254
232
|
const outAbs = resolve(cwd, output)
|
|
255
|
-
// A `.js`/`.cjs`/`.mjs` --output names a single output file; anything else is a directory.
|
|
256
233
|
const asFile = /\.[cm]?js$/u.test(output)
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// overwrite input file" check fires only AFTER it has written, and for a lockfile that
|
|
261
|
-
// permanently breaks the file's hash. So we build with write:false and write the outputs
|
|
262
|
-
// ourselves, refusing -- before touching disk -- to clobber any recorded file.
|
|
234
|
+
// Never overwrite a file the artifact attests: esbuild's own overwrite check fires only
|
|
235
|
+
// after writing (corrupting a lockfile's hash), so we build write:false and refuse to
|
|
236
|
+
// clobber any recorded file before touching disk.
|
|
263
237
|
const attested = new Set([...state.sources.keys(), ...state.resources.keys()].map((f) => resolve(state.root, f)))
|
|
264
238
|
|
|
265
239
|
let result
|
|
@@ -269,24 +243,21 @@ export async function buildCommand({
|
|
|
269
243
|
absWorkingDir: state.root,
|
|
270
244
|
bundle: true,
|
|
271
245
|
write: false,
|
|
272
|
-
platform,
|
|
246
|
+
platform: hermes ? 'neutral' : platform,
|
|
273
247
|
format,
|
|
274
248
|
logLevel: 'silent',
|
|
249
|
+
...(hermes ? { supported: HERMES_UNSUPPORTED } : {}),
|
|
275
250
|
...(minify ? { minify: true } : {}),
|
|
276
251
|
...(sourcemap ? { sourcemap: true } : {}),
|
|
277
252
|
...(external.length > 0 ? { external } : {}),
|
|
278
253
|
...(Object.keys(define).length > 0 ? { define } : {}),
|
|
279
254
|
...(asFile ? { outfile: outAbs } : { outdir: outAbs }),
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
plugins: [new StasisEsbuild(state, { loaders: new Map(Object.entries(loader)) })],
|
|
255
|
+
// Loader overrides go through the plugin: it serves every in-bundle file, so esbuild's
|
|
256
|
+
// own `loader` option never sees them. e.g. { '.js': 'jsx' } to parse JSX in .js files.
|
|
257
|
+
plugins: [new StasisEsbuild(state, { loaders: new Map(Object.entries(loader)), transform })],
|
|
284
258
|
})
|
|
285
259
|
} catch (err) {
|
|
286
|
-
//
|
|
287
|
-
// logLevel:'silent'. Surface those (rather than the bare wrapper) so the user sees the
|
|
288
|
-
// actual resolve/load failure -- including an entry that imports an un-buildable asset
|
|
289
|
-
// -- then fail the command.
|
|
260
|
+
// Under logLevel:'silent' esbuild puts the real errors on err.errors; surface them, then fail.
|
|
290
261
|
if (Array.isArray(err?.errors) && err.errors.length > 0) {
|
|
291
262
|
const formatted = await esbuild.formatMessages(err.errors, { kind: 'error', color: false })
|
|
292
263
|
for (const line of formatted) process.stderr.write(line)
|
|
@@ -295,9 +266,7 @@ export async function buildCommand({
|
|
|
295
266
|
throw err
|
|
296
267
|
}
|
|
297
268
|
|
|
298
|
-
//
|
|
299
|
-
// ACTUAL output paths (so it covers both `--output` file and directory), BEFORE any write,
|
|
300
|
-
// so a rejected build leaves every recorded file intact.
|
|
269
|
+
// Refuse to clobber an attested source, checked against esbuild's actual output paths before any write.
|
|
301
270
|
for (const file of result.outputFiles) {
|
|
302
271
|
if (attested.has(file.path)) {
|
|
303
272
|
throw new Error(
|