@exodus/stasis 1.0.0-beta.2 → 1.0.0-beta.5
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 +23 -3
- package/bin/stasis.js +375 -170
- package/package.json +44 -11
- package/src/audit-corrections.js +68 -0
- package/src/audit.js +243 -59
- package/src/babel-worker.cjs +65 -0
- package/src/babel.js +131 -0
- package/src/bundle-from-lockfile.js +12 -18
- package/src/bundle-reader.js +77 -0
- package/src/cmd/build.js +95 -128
- package/src/cmd/bundle.js +877 -641
- 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 +4 -18
- package/src/diff.js +67 -124
- 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 +1699 -0
- package/src/loaders/foundry.js +923 -0
- package/src/loaders/paths.js +33 -0
- package/src/loaders/php.js +339 -394
- 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/mock.js +57 -165
- package/src/parse.js +4 -16
- package/src/resolve-fields.js +331 -0
- package/src/resolve-node.js +416 -0
- package/src/resolve-typescript.js +427 -0
- package/src/rollup.js +5 -0
- package/src/sbom.js +64 -92
- package/src/scan.js +234 -156
- package/src/vfs-bundle/entries.js +140 -0
- package/src/vfs-bundle/github.js +193 -0
- package/src/vfs-bundle/soldeer.js +125 -0
- package/src/vfs-bundle/tree.js +606 -0
- package/src/vfs-bundle.js +74 -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
package/src/cmd/build.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
2
|
import { createRequire } from 'node:module'
|
|
3
3
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
@@ -7,42 +7,30 @@ 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'
|
|
13
|
+
import { writeFile } from './output.js'
|
|
12
14
|
|
|
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
15
|
export { bundleFromLockfile }
|
|
16
16
|
|
|
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
17
|
const JS_EXTS = new Set(['.js', '.cjs', '.mjs', '.ts', '.cts', '.mts', '.jsx', '.tsx'])
|
|
22
18
|
|
|
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.
|
|
19
|
+
// Allowlist (not blocklist) of per-file formats `stasis build` accepts, so a bundle
|
|
20
|
+
// format added later fails closed here rather than in a cryptic esbuild error.
|
|
29
21
|
const BUILDABLE_FORMATS = new Set([
|
|
30
22
|
'module', 'commonjs', 'json', 'module-typescript', 'commonjs-typescript',
|
|
31
23
|
'resource', 'resource:base64', 'directory', 'stat:file', 'stat:directory',
|
|
32
24
|
])
|
|
33
25
|
|
|
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.
|
|
26
|
+
// esbuild is an optional peer dep; resolve it from the local install, else fall back
|
|
27
|
+
// to a global one.
|
|
39
28
|
async function importEsbuild() {
|
|
40
29
|
let mod
|
|
41
30
|
try {
|
|
42
31
|
mod = await import('esbuild')
|
|
43
32
|
} 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.
|
|
33
|
+
// Only a genuine "not installed" miss falls through to the global lookup; surface other failures.
|
|
46
34
|
if (cause?.code !== 'ERR_MODULE_NOT_FOUND' && cause?.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
|
|
47
35
|
throw cause
|
|
48
36
|
}
|
|
@@ -57,14 +45,10 @@ async function importEsbuild() {
|
|
|
57
45
|
}
|
|
58
46
|
mod = await import(pathToFileURL(globalMain).href)
|
|
59
47
|
}
|
|
60
|
-
//
|
|
61
|
-
// interop surfaces them directly. A globally-imported CJS copy may instead land its
|
|
62
|
-
// exports on `default` -- accept both shapes.
|
|
48
|
+
// A globally-imported CJS copy may land its exports on `default` -- accept both shapes.
|
|
63
49
|
return typeof mod.build === 'function' ? mod : mod.default
|
|
64
50
|
}
|
|
65
51
|
|
|
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
52
|
function resolveGlobalEsbuild() {
|
|
69
53
|
const probes = [
|
|
70
54
|
['npm', ['root', '-g']],
|
|
@@ -76,7 +60,7 @@ function resolveGlobalEsbuild() {
|
|
|
76
60
|
try {
|
|
77
61
|
out = spawnSync(cmd, args, { encoding: 'utf8' })
|
|
78
62
|
} catch {
|
|
79
|
-
continue
|
|
63
|
+
continue
|
|
80
64
|
}
|
|
81
65
|
if (!out || out.status !== 0 || !out.stdout) continue
|
|
82
66
|
const base = out.stdout.trim()
|
|
@@ -88,61 +72,77 @@ function resolveGlobalEsbuild() {
|
|
|
88
72
|
try {
|
|
89
73
|
return createRequire(join(nm, 'noop.js')).resolve('esbuild')
|
|
90
74
|
} catch {
|
|
91
|
-
// keep probing
|
|
92
75
|
}
|
|
93
76
|
}
|
|
94
77
|
}
|
|
95
78
|
return undefined
|
|
96
79
|
}
|
|
97
80
|
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
function assertBuildable(artifact, label) {
|
|
81
|
+
// Preconditions independent of which entry we build. Per-entry language checks are deferred
|
|
82
|
+
// to assertBuildableEntry, once selectEntry has picked one.
|
|
83
|
+
function assertBuildableArtifact(artifact, label) {
|
|
102
84
|
if (artifact.config.scope !== 'full') {
|
|
103
85
|
throw new Error(
|
|
104
86
|
`stasis build requires a full-scope artifact; ${label} is scope='${artifact.config.scope}'. ` +
|
|
105
87
|
`node_modules-scope artifacts omit the entry and workspace code, so there is nothing to build.`
|
|
106
88
|
)
|
|
107
89
|
}
|
|
108
|
-
|
|
109
|
-
if (entries.length === 0) {
|
|
90
|
+
if (artifact.entries.size === 0) {
|
|
110
91
|
throw new Error(`stasis build: ${label} declares no entries to build`)
|
|
111
92
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The entry plus its transitive imports in the recorded graph -- the exact set esbuild loads
|
|
96
|
+
// (all conditions buckets; a --metro edge's per-platform targets included).
|
|
97
|
+
function reachableFiles(entry, imports) {
|
|
98
|
+
const seen = new Set([entry])
|
|
99
|
+
const stack = [entry]
|
|
100
|
+
while (stack.length > 0) {
|
|
101
|
+
const parent = stack.pop()
|
|
102
|
+
for (const [, byParent] of imports) {
|
|
103
|
+
const specifiers = byParent.get(parent)
|
|
104
|
+
if (specifiers === undefined) continue
|
|
105
|
+
for (const [, target] of specifiers) {
|
|
106
|
+
// A plain edge is a file string; a --metro edge is a {platform: file} Map.
|
|
107
|
+
const targets = typeof target === 'string' ? [target] : [...target.values()]
|
|
108
|
+
for (const file of targets) {
|
|
109
|
+
if (!seen.has(file)) {
|
|
110
|
+
seen.add(file)
|
|
111
|
+
stack.push(file)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
return seen
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Language checks scoped to the built entry: it must be JS/TS, and every file reachable from
|
|
121
|
+
// it must be buildable. A non-JS file elsewhere (another entry, or a `stasis add` attachment)
|
|
122
|
+
// isn't reached, so it doesn't block the build. imports/formats are null on a legacy lockfile.
|
|
123
|
+
function assertBuildableEntry(artifact, entry, label) {
|
|
124
|
+
if (!JS_EXTS.has(extname(entry))) {
|
|
125
|
+
throw new Error(`stasis build only builds JavaScript/TypeScript entries (esbuild); got non-JS entry '${entry}' in ${label}`)
|
|
126
|
+
}
|
|
127
|
+
const reachable = reachableFiles(entry, artifact.imports ?? new Map())
|
|
128
|
+
// A reachable resource (plugin captures DO record asset edges) stays allowed via BUILDABLE_FORMATS;
|
|
129
|
+
// an entry that imports an unbuildable asset fails later at esbuild, not here.
|
|
126
130
|
for (const [file, format] of artifact.formats ?? new Map()) {
|
|
131
|
+
if (!reachable.has(file)) continue
|
|
127
132
|
if (!BUILDABLE_FORMATS.has(format)) {
|
|
128
133
|
throw new Error(`stasis build only supports JavaScript/TypeScript bundles (esbuild); ${label} carries a '${format}' file (${file})`)
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
}
|
|
132
137
|
|
|
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.
|
|
138
|
+
// Pick which recorded entry to build; required only when several are recorded. Matched
|
|
139
|
+
// against the recorded project-relative entries (verbatim, `./`-stripped, or cwd-relative).
|
|
140
140
|
function selectEntry(entryArg, entries, { cwd, root, label }) {
|
|
141
141
|
const recorded = [...entries]
|
|
142
142
|
const list = recorded.map((e) => ` ${e}`).join('\n')
|
|
143
143
|
if (entryArg === undefined) {
|
|
144
144
|
if (recorded.length === 1) return recorded[0]
|
|
145
|
-
// length === 0 is impossible here (
|
|
145
|
+
// length === 0 is impossible here (assertBuildableArtifact required >= 1 entry).
|
|
146
146
|
throw new Error(`stasis build: ${label} has ${recorded.length} entry points; pass one as the second argument:\n${list}`)
|
|
147
147
|
}
|
|
148
148
|
const candidates = new Set([
|
|
@@ -157,13 +157,8 @@ function selectEntry(entryArg, entries, { cwd, root, label }) {
|
|
|
157
157
|
return match
|
|
158
158
|
}
|
|
159
159
|
|
|
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`.
|
|
160
|
+
// Build a bundle-load State that serves an in-memory Bundle. skipDiscovery does NO
|
|
161
|
+
// filesystem discovery (no package.json walk, no on-disk config, no stray stasis.code.br).
|
|
167
162
|
function loadStateForBundle(root, bundle) {
|
|
168
163
|
const state = new State(root, { bundle: 'load', lock: 'ignore', scope: bundle.config.scope, skipDiscovery: true })
|
|
169
164
|
const sources = new Map()
|
|
@@ -177,50 +172,23 @@ function loadStateForBundle(root, bundle) {
|
|
|
177
172
|
state.formats = bundle.formats
|
|
178
173
|
state.imports = bundle.imports
|
|
179
174
|
state.entries = new Set(bundle.entries)
|
|
180
|
-
// Not state.
|
|
181
|
-
//
|
|
175
|
+
// Not state.executable either: like state.modules below, the load path never reads it, and every
|
|
176
|
+
// consumer narrows against state.modules -- which is empty here -- so copying it would be inert.
|
|
177
|
+
// Not state.modules: the load path never reads it (only the capture paths write it).
|
|
182
178
|
return state
|
|
183
179
|
}
|
|
184
180
|
|
|
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.
|
|
181
|
+
// Run `stasis build`: run esbuild over one recorded entry, following the artifact's
|
|
182
|
+
// recorded import graph EXACTLY (not esbuild's resolver). A specifier the artifact records
|
|
183
|
+
// is always followed from the artifact; `external` only steers the specifiers it doesn't.
|
|
217
184
|
export async function buildCommand({
|
|
218
185
|
cwd = process.cwd(),
|
|
219
186
|
artifact,
|
|
220
187
|
entry,
|
|
221
188
|
output,
|
|
222
|
-
format
|
|
189
|
+
format,
|
|
223
190
|
platform = 'node',
|
|
191
|
+
babel = false,
|
|
224
192
|
minify = false,
|
|
225
193
|
sourcemap = false,
|
|
226
194
|
define = {},
|
|
@@ -230,36 +198,43 @@ export async function buildCommand({
|
|
|
230
198
|
if (!artifact) throw new Error('stasis build: an artifact (bundle or lockfile) is required')
|
|
231
199
|
if (!output) throw new Error('stasis build: --output is required (a .js/.cjs/.mjs file, or a directory)')
|
|
232
200
|
if (!Array.isArray(external)) throw new Error('stasis build: external must be an array of esbuild patterns')
|
|
201
|
+
// --platform=hermes is a stasis-level target, not an esbuild platform (esbuild gets
|
|
202
|
+
// 'neutral'): every code file is downleveled to Hermes' dialect before the final bundle,
|
|
203
|
+
// and the bundle itself must avoid ESM -- Hermes has no import/export.
|
|
204
|
+
const hermes = platform === 'hermes'
|
|
205
|
+
format ??= hermes ? 'iife' : 'esm'
|
|
206
|
+
if (hermes && format === 'esm') {
|
|
207
|
+
throw new Error('stasis build: --platform=hermes cannot emit ESM (Hermes has no import/export); use --format=iife (the hermes default) or cjs')
|
|
208
|
+
}
|
|
233
209
|
const artifactAbs = resolve(cwd, artifact)
|
|
234
210
|
const { kind, artifact: parsed } = parseFileWithKind(artifactAbs)
|
|
235
|
-
|
|
211
|
+
assertBuildableArtifact(parsed, artifact)
|
|
236
212
|
|
|
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.
|
|
213
|
+
// Anchor artifact-relative paths at the artifact's own directory; a lockfile's sources
|
|
214
|
+
// are read + verified from there.
|
|
241
215
|
const root = dirname(artifactAbs)
|
|
242
216
|
const selectedEntry = selectEntry(entry, parsed.entries, { cwd, root, label: artifact })
|
|
217
|
+
assertBuildableEntry(parsed, selectedEntry, artifact)
|
|
243
218
|
|
|
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
219
|
const bundle = kind === 'lockfile' ? bundleFromLockfile(parsed, { root }) : parsed
|
|
248
220
|
const state = loadStateForBundle(root, bundle)
|
|
249
|
-
//
|
|
250
|
-
// State's root so esbuild's entry resolution is independent of the process cwd.
|
|
221
|
+
// Make the entry absolute against the State's root so resolution is independent of process cwd.
|
|
251
222
|
const entryPoint = resolve(state.root, selectedEntry)
|
|
252
223
|
|
|
253
224
|
const esbuild = await importEsbuild()
|
|
225
|
+
// Per-file code rewrites the plugin applies before esbuild parses a served file:
|
|
226
|
+
// --babel runs the project's own babel.config.* over the original source (no esbuild
|
|
227
|
+
// pre-transform), and otherwise --platform=hermes uses the built-in downlevel pipeline.
|
|
228
|
+
// Both resolve Babel from the project (cwd), never from stasis's own dependencies.
|
|
229
|
+
let transform
|
|
230
|
+
if (babel) transform = createBabelConfigTransform({ projectDir: cwd })
|
|
231
|
+
else if (hermes) transform = createHermesTransform({ projectDir: cwd, esbuild })
|
|
232
|
+
|
|
254
233
|
const outAbs = resolve(cwd, output)
|
|
255
|
-
// A `.js`/`.cjs`/`.mjs` --output names a single output file; anything else is a directory.
|
|
256
234
|
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.
|
|
235
|
+
// Never overwrite a file the artifact attests: esbuild's own overwrite check fires only
|
|
236
|
+
// after writing (corrupting a lockfile's hash), so we build write:false and refuse to
|
|
237
|
+
// clobber any recorded file before touching disk.
|
|
263
238
|
const attested = new Set([...state.sources.keys(), ...state.resources.keys()].map((f) => resolve(state.root, f)))
|
|
264
239
|
|
|
265
240
|
let result
|
|
@@ -269,24 +244,21 @@ export async function buildCommand({
|
|
|
269
244
|
absWorkingDir: state.root,
|
|
270
245
|
bundle: true,
|
|
271
246
|
write: false,
|
|
272
|
-
platform,
|
|
247
|
+
platform: hermes ? 'neutral' : platform,
|
|
273
248
|
format,
|
|
274
249
|
logLevel: 'silent',
|
|
250
|
+
...(hermes ? { supported: HERMES_UNSUPPORTED } : {}),
|
|
275
251
|
...(minify ? { minify: true } : {}),
|
|
276
252
|
...(sourcemap ? { sourcemap: true } : {}),
|
|
277
253
|
...(external.length > 0 ? { external } : {}),
|
|
278
254
|
...(Object.keys(define).length > 0 ? { define } : {}),
|
|
279
255
|
...(asFile ? { outfile: outAbs } : { outdir: outAbs }),
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
plugins: [new StasisEsbuild(state, { loaders: new Map(Object.entries(loader)) })],
|
|
256
|
+
// Loader overrides go through the plugin: it serves every in-bundle file, so esbuild's
|
|
257
|
+
// own `loader` option never sees them. e.g. { '.js': 'jsx' } to parse JSX in .js files.
|
|
258
|
+
plugins: [new StasisEsbuild(state, { loaders: new Map(Object.entries(loader)), transform })],
|
|
284
259
|
})
|
|
285
260
|
} 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.
|
|
261
|
+
// Under logLevel:'silent' esbuild puts the real errors on err.errors; surface them, then fail.
|
|
290
262
|
if (Array.isArray(err?.errors) && err.errors.length > 0) {
|
|
291
263
|
const formatted = await esbuild.formatMessages(err.errors, { kind: 'error', color: false })
|
|
292
264
|
for (const line of formatted) process.stderr.write(line)
|
|
@@ -295,9 +267,7 @@ export async function buildCommand({
|
|
|
295
267
|
throw err
|
|
296
268
|
}
|
|
297
269
|
|
|
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.
|
|
270
|
+
// Refuse to clobber an attested source, checked against esbuild's actual output paths before any write.
|
|
301
271
|
for (const file of result.outputFiles) {
|
|
302
272
|
if (attested.has(file.path)) {
|
|
303
273
|
throw new Error(
|
|
@@ -312,10 +282,7 @@ export async function buildCommand({
|
|
|
312
282
|
for (const line of formatted) process.stderr.write(line)
|
|
313
283
|
}
|
|
314
284
|
|
|
315
|
-
for (const file of result.outputFiles)
|
|
316
|
-
mkdirSync(dirname(file.path), { recursive: true })
|
|
317
|
-
writeFileSync(file.path, file.contents)
|
|
318
|
-
}
|
|
285
|
+
for (const file of result.outputFiles) writeFile(file.path, file.contents)
|
|
319
286
|
|
|
320
287
|
console.warn(`[stasis] Built ${selectedEntry} from ${kind} ${artifact} to ${output}`)
|
|
321
288
|
return { entry: selectedEntry, dir: asFile ? undefined : outAbs, outfile: asFile ? outAbs : undefined }
|