@exodus/stasis 1.0.0-beta.1 → 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 +173 -161
- 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 +93 -123
- package/src/cmd/bundle.js +470 -427
- 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/metro-transformer.js +3 -2
- 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,41 +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
|
-
// (resource/directory) that are inert unless the SELECTED entry imports them. An allowlist
|
|
26
|
-
// so a new language format added to `stasis bundle` later (e.g. a future `go`) is rejected
|
|
27
|
-
// 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.
|
|
28
20
|
const BUILDABLE_FORMATS = new Set([
|
|
29
21
|
'module', 'commonjs', 'json', 'module-typescript', 'commonjs-typescript',
|
|
30
|
-
'resource', 'resource:base64', 'directory',
|
|
22
|
+
'resource', 'resource:base64', 'directory', 'stat:file', 'stat:directory',
|
|
31
23
|
])
|
|
32
24
|
|
|
33
|
-
// esbuild is an
|
|
34
|
-
//
|
|
35
|
-
// from the local install first (a peer dep hoisted into the project's node_modules, or
|
|
36
|
-
// an npx-provided copy), then fall back to a global install, with an actionable error
|
|
37
|
-
// 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.
|
|
38
27
|
async function importEsbuild() {
|
|
39
28
|
let mod
|
|
40
29
|
try {
|
|
41
30
|
mod = await import('esbuild')
|
|
42
31
|
} catch (cause) {
|
|
43
|
-
// Only a genuine "not installed" miss falls through to the global lookup;
|
|
44
|
-
// 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.
|
|
45
33
|
if (cause?.code !== 'ERR_MODULE_NOT_FOUND' && cause?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
46
34
|
throw cause
|
|
47
35
|
}
|
|
@@ -56,14 +44,10 @@ async function importEsbuild() {
|
|
|
56
44
|
}
|
|
57
45
|
mod = await import(pathToFileURL(globalMain).href)
|
|
58
46
|
}
|
|
59
|
-
//
|
|
60
|
-
// interop surfaces them directly. A globally-imported CJS copy may instead land its
|
|
61
|
-
// exports on `default` -- accept both shapes.
|
|
47
|
+
// A globally-imported CJS copy may land its exports on `default` -- accept both shapes.
|
|
62
48
|
return typeof mod.build === 'function' ? mod : mod.default
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
// Locate a globally-installed esbuild via the package managers' "global root" commands.
|
|
66
|
-
// Returns the absolute path to esbuild's entry, or undefined when none is installed.
|
|
67
51
|
function resolveGlobalEsbuild() {
|
|
68
52
|
const probes = [
|
|
69
53
|
['npm', ['root', '-g']],
|
|
@@ -75,7 +59,7 @@ function resolveGlobalEsbuild() {
|
|
|
75
59
|
try {
|
|
76
60
|
out = spawnSync(cmd, args, { encoding: 'utf8' })
|
|
77
61
|
} catch {
|
|
78
|
-
continue
|
|
62
|
+
continue
|
|
79
63
|
}
|
|
80
64
|
if (!out || out.status !== 0 || !out.stdout) continue
|
|
81
65
|
const base = out.stdout.trim()
|
|
@@ -87,61 +71,77 @@ function resolveGlobalEsbuild() {
|
|
|
87
71
|
try {
|
|
88
72
|
return createRequire(join(nm, 'noop.js')).resolve('esbuild')
|
|
89
73
|
} catch {
|
|
90
|
-
// keep probing
|
|
91
74
|
}
|
|
92
75
|
}
|
|
93
76
|
}
|
|
94
77
|
return undefined
|
|
95
78
|
}
|
|
96
79
|
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
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) {
|
|
101
83
|
if (artifact.config.scope !== 'full') {
|
|
102
84
|
throw new Error(
|
|
103
85
|
`stasis build requires a full-scope artifact; ${label} is scope='${artifact.config.scope}'. ` +
|
|
104
86
|
`node_modules-scope artifacts omit the entry and workspace code, so there is nothing to build.`
|
|
105
87
|
)
|
|
106
88
|
}
|
|
107
|
-
|
|
108
|
-
if (entries.length === 0) {
|
|
89
|
+
if (artifact.entries.size === 0) {
|
|
109
90
|
throw new Error(`stasis build: ${label} declares no entries to build`)
|
|
110
91
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
125
129
|
for (const [file, format] of artifact.formats ?? new Map()) {
|
|
130
|
+
if (!reachable.has(file)) continue
|
|
126
131
|
if (!BUILDABLE_FORMATS.has(format)) {
|
|
127
132
|
throw new Error(`stasis build only supports JavaScript/TypeScript bundles (esbuild); ${label} carries a '${format}' file (${file})`)
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
// Pick which recorded entry to build
|
|
133
|
-
//
|
|
134
|
-
// entry is used); with several it is required, and an absent or unknown one is rejected
|
|
135
|
-
// with the available list. The selector is matched against the artifact's recorded
|
|
136
|
-
// (project-relative) entries -- only those are attested as graph roots, and the load
|
|
137
|
-
// plugin's assertEntry rejects anything else -- accepting the recorded path verbatim, a
|
|
138
|
-
// 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).
|
|
139
139
|
function selectEntry(entryArg, entries, { cwd, root, label }) {
|
|
140
140
|
const recorded = [...entries]
|
|
141
141
|
const list = recorded.map((e) => ` ${e}`).join('\n')
|
|
142
142
|
if (entryArg === undefined) {
|
|
143
143
|
if (recorded.length === 1) return recorded[0]
|
|
144
|
-
// length === 0 is impossible here (
|
|
144
|
+
// length === 0 is impossible here (assertBuildableArtifact required >= 1 entry).
|
|
145
145
|
throw new Error(`stasis build: ${label} has ${recorded.length} entry points; pass one as the second argument:\n${list}`)
|
|
146
146
|
}
|
|
147
147
|
const candidates = new Set([
|
|
@@ -156,13 +156,8 @@ function selectEntry(entryArg, entries, { cwd, root, label }) {
|
|
|
156
156
|
return match
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
// Build a bundle-load State that serves an in-memory Bundle
|
|
160
|
-
//
|
|
161
|
-
// state.entries). skipDiscovery anchors the State at `root` and does NO filesystem discovery:
|
|
162
|
-
// no package.json walk, no on-disk config, no stray stasis.code.br read. bundle=load +
|
|
163
|
-
// lock=ignore serve the bundle's own bytes (self-attesting -- the lockfile path already
|
|
164
|
-
// SHA-512-verified them in bundleFromLockfile). We mirror State's on-disk load (split
|
|
165
|
-
// 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).
|
|
166
161
|
function loadStateForBundle(root, bundle) {
|
|
167
162
|
const state = new State(root, { bundle: 'load', lock: 'ignore', scope: bundle.config.scope, skipDiscovery: true })
|
|
168
163
|
const sources = new Map()
|
|
@@ -176,50 +171,23 @@ function loadStateForBundle(root, bundle) {
|
|
|
176
171
|
state.formats = bundle.formats
|
|
177
172
|
state.imports = bundle.imports
|
|
178
173
|
state.entries = new Set(bundle.entries)
|
|
179
|
-
// Not state.
|
|
180
|
-
//
|
|
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).
|
|
181
177
|
return state
|
|
182
178
|
}
|
|
183
179
|
|
|
184
|
-
// Run
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
// recorded import graph EXACTLY rather than esbuild's own resolver.
|
|
188
|
-
//
|
|
189
|
-
// `entry` selects which recorded entry to build (see selectEntry): optional when the
|
|
190
|
-
// artifact records one, required when it records several. `output` is required: a path
|
|
191
|
-
// ending in `.js`/`.cjs`/`.mjs` writes a single file, anything else a directory.
|
|
192
|
-
//
|
|
193
|
-
// The build runs entirely in memory off the artifact -- no temp files, and no package.json
|
|
194
|
-
// required. The artifact's relative paths anchor at the artifact's OWN directory: a bundle is
|
|
195
|
-
// self-contained (root is just a keying prefix; nothing is read from disk), and a lockfile
|
|
196
|
-
// records its paths relative to where it sits (its project root), so its sources are read +
|
|
197
|
-
// verified from there. A project stasis.config.json is not consulted -- the build is driven
|
|
198
|
-
// by the artifact, not by `stasis run` config.
|
|
199
|
-
//
|
|
200
|
-
// The recorded graph is forced via the stasis esbuild plugin in bundle-load mode: each import
|
|
201
|
-
// resolves to the edge stasis recorded (a specifier the artifact doesn't record is handed back
|
|
202
|
-
// to esbuild, which externalizes it like a built-in/`external`), and each module's bytes come
|
|
203
|
-
// from the artifact -- so the output reflects what stasis attested, not what's on disk.
|
|
204
|
-
//
|
|
205
|
-
// `format` (esm/cjs/iife, default esm), `platform` (node/browser/neutral, default node),
|
|
206
|
-
// `minify`, `sourcemap`, `define` (an esbuild define map), `external` (an array of esbuild
|
|
207
|
-
// external patterns), and `loader` (an esbuild extension->loader map, e.g. { '.js': 'jsx' }
|
|
208
|
-
// to parse JSX in .js files) are forwarded to esbuild; the import graph is fixed by the artifact.
|
|
209
|
-
//
|
|
210
|
-
// `external` is esbuild's native option: it only steers specifiers esbuild itself would
|
|
211
|
-
// resolve -- i.e. the ones the bundle does NOT record (handed back to esbuild as a miss,
|
|
212
|
-
// typically the externals that existed at capture time). It re-declares those so esbuild
|
|
213
|
-
// leaves them as runtime imports instead of trying (and failing) to resolve them on disk.
|
|
214
|
-
// A specifier the artifact DOES record is still followed from the artifact -- the recorded
|
|
215
|
-
// 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.
|
|
216
183
|
export async function buildCommand({
|
|
217
184
|
cwd = process.cwd(),
|
|
218
185
|
artifact,
|
|
219
186
|
entry,
|
|
220
187
|
output,
|
|
221
|
-
format
|
|
188
|
+
format,
|
|
222
189
|
platform = 'node',
|
|
190
|
+
babel = false,
|
|
223
191
|
minify = false,
|
|
224
192
|
sourcemap = false,
|
|
225
193
|
define = {},
|
|
@@ -229,36 +197,43 @@ export async function buildCommand({
|
|
|
229
197
|
if (!artifact) throw new Error('stasis build: an artifact (bundle or lockfile) is required')
|
|
230
198
|
if (!output) throw new Error('stasis build: --output is required (a .js/.cjs/.mjs file, or a directory)')
|
|
231
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
|
+
}
|
|
232
208
|
const artifactAbs = resolve(cwd, artifact)
|
|
233
209
|
const { kind, artifact: parsed } = parseFileWithKind(artifactAbs)
|
|
234
|
-
|
|
210
|
+
assertBuildableArtifact(parsed, artifact)
|
|
235
211
|
|
|
236
|
-
// Anchor
|
|
237
|
-
//
|
|
238
|
-
// records its paths relative to where it sits -- its project root -- so its sources are
|
|
239
|
-
// 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.
|
|
240
214
|
const root = dirname(artifactAbs)
|
|
241
215
|
const selectedEntry = selectEntry(entry, parsed.entries, { cwd, root, label: artifact })
|
|
216
|
+
assertBuildableEntry(parsed, selectedEntry, artifact)
|
|
242
217
|
|
|
243
|
-
// The Bundle the load State serves, entirely in memory. A bundle artifact is already a
|
|
244
|
-
// parsed Bundle; a lockfile is reconstructed into one by reading + SHA-512-verifying its
|
|
245
|
-
// attested files from disk (bundleFromLockfile). The plugin drives off the in-memory State.
|
|
246
218
|
const bundle = kind === 'lockfile' ? bundleFromLockfile(parsed, { root }) : parsed
|
|
247
219
|
const state = loadStateForBundle(root, bundle)
|
|
248
|
-
//
|
|
249
|
-
// 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.
|
|
250
221
|
const entryPoint = resolve(state.root, selectedEntry)
|
|
251
222
|
|
|
252
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
|
+
|
|
253
232
|
const outAbs = resolve(cwd, output)
|
|
254
|
-
// A `.js`/`.cjs`/`.mjs` --output names a single output file; anything else is a directory.
|
|
255
233
|
const asFile = /\.[cm]?js$/u.test(output)
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
// overwrite input file" check fires only AFTER it has written, and for a lockfile that
|
|
260
|
-
// permanently breaks the file's hash. So we build with write:false and write the outputs
|
|
261
|
-
// 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.
|
|
262
237
|
const attested = new Set([...state.sources.keys(), ...state.resources.keys()].map((f) => resolve(state.root, f)))
|
|
263
238
|
|
|
264
239
|
let result
|
|
@@ -268,24 +243,21 @@ export async function buildCommand({
|
|
|
268
243
|
absWorkingDir: state.root,
|
|
269
244
|
bundle: true,
|
|
270
245
|
write: false,
|
|
271
|
-
platform,
|
|
246
|
+
platform: hermes ? 'neutral' : platform,
|
|
272
247
|
format,
|
|
273
248
|
logLevel: 'silent',
|
|
249
|
+
...(hermes ? { supported: HERMES_UNSUPPORTED } : {}),
|
|
274
250
|
...(minify ? { minify: true } : {}),
|
|
275
251
|
...(sourcemap ? { sourcemap: true } : {}),
|
|
276
252
|
...(external.length > 0 ? { external } : {}),
|
|
277
253
|
...(Object.keys(define).length > 0 ? { define } : {}),
|
|
278
254
|
...(asFile ? { outfile: outAbs } : { outdir: outAbs }),
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
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 })],
|
|
283
258
|
})
|
|
284
259
|
} catch (err) {
|
|
285
|
-
//
|
|
286
|
-
// logLevel:'silent'. Surface those (rather than the bare wrapper) so the user sees the
|
|
287
|
-
// actual resolve/load failure -- including an entry that imports an un-buildable asset
|
|
288
|
-
// -- then fail the command.
|
|
260
|
+
// Under logLevel:'silent' esbuild puts the real errors on err.errors; surface them, then fail.
|
|
289
261
|
if (Array.isArray(err?.errors) && err.errors.length > 0) {
|
|
290
262
|
const formatted = await esbuild.formatMessages(err.errors, { kind: 'error', color: false })
|
|
291
263
|
for (const line of formatted) process.stderr.write(line)
|
|
@@ -294,9 +266,7 @@ export async function buildCommand({
|
|
|
294
266
|
throw err
|
|
295
267
|
}
|
|
296
268
|
|
|
297
|
-
//
|
|
298
|
-
// ACTUAL output paths (so it covers both `--output` file and directory), BEFORE any write,
|
|
299
|
-
// 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.
|
|
300
270
|
for (const file of result.outputFiles) {
|
|
301
271
|
if (attested.has(file.path)) {
|
|
302
272
|
throw new Error(
|