@exodus/stasis-plugins 1.0.0-beta.0 → 1.0.0-beta.1
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/package.json +2 -2
- package/src/esbuild.js +30 -1
- package/src/metro.js +35 -8
- package/src/webpack.js +31 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exodus/stasis-plugins",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
4
|
"description": "Bundler plugins (webpack, esbuild, Metro) for @exodus/stasis-core",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
},
|
|
32
32
|
"homepage": "https://github.com/ExodusOSS/stasis#readme",
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@exodus/stasis-core": "1.0.0-beta.
|
|
34
|
+
"@exodus/stasis-core": "1.0.0-beta.1"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/src/esbuild.js
CHANGED
|
@@ -21,6 +21,11 @@ export class StasisEsbuild {
|
|
|
21
21
|
#resources
|
|
22
22
|
#loaders
|
|
23
23
|
|
|
24
|
+
// Build starts observed by this (capture-mode) instance, across rebuilds AND across separate
|
|
25
|
+
// esbuild.build()/context() calls sharing one instance. The second start is refused -- see
|
|
26
|
+
// the onStart hook in setup().
|
|
27
|
+
#captureBuildCount = 0
|
|
28
|
+
|
|
24
29
|
constructor(options = {}, { loaders } = {}) {
|
|
25
30
|
// A caller that owns a State (e.g. `stasis build`) can pass it directly to drive the plugin, bypassing resolvePluginState; mode follows that State's config, and a foreign-copy State fails closed via the instanceof miss.
|
|
26
31
|
const state = options instanceof State
|
|
@@ -49,9 +54,33 @@ export class StasisEsbuild {
|
|
|
49
54
|
return 'stasis'
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
setup = ({ onResolve, onLoad, onEnd, resolve }) => {
|
|
57
|
+
setup = ({ onResolve, onLoad, onStart, onEnd, resolve }) => {
|
|
53
58
|
if (!this.#state) return // noop plugin
|
|
54
59
|
|
|
60
|
+
// Watch/rebuild capture is EXPLICITLY UNSUPPORTED -- fail the second build loudly before it
|
|
61
|
+
// captures anything. Capture's dedupe is keyed by PATH, not content (#seen below, plus
|
|
62
|
+
// State.write's compare-and-skip caches), so on a rebuild where a file's bytes changed the
|
|
63
|
+
// plugin would skip re-capture: the emitted output would use the new bytes while the
|
|
64
|
+
// bundle/lockfile silently kept attesting the OLD ones. Rather than track content changes
|
|
65
|
+
// across rebuilds, refuse them outright. esbuild rebuilds (watch mode, context.rebuild())
|
|
66
|
+
// re-fire onStart/onEnd against the SAME setup registrations, so an instance-level counter
|
|
67
|
+
// sees every start; it also covers reusing one plugin instance across two esbuild.build()
|
|
68
|
+
// calls, which has the same staleness hazard. Returning errors from onStart is the
|
|
69
|
+
// esbuild-idiomatic hard failure (the build reports them and onEnd's clean-build gate then
|
|
70
|
+
// skips the write). Load mode registers nothing here: it serves immutable attested bytes,
|
|
71
|
+
// so rebuilds have no drift hazard.
|
|
72
|
+
if (!this.#state.config.loadBundle) {
|
|
73
|
+
onStart(() => {
|
|
74
|
+
this.#captureBuildCount += 1
|
|
75
|
+
if (this.#captureBuildCount === 1) return undefined
|
|
76
|
+
return {
|
|
77
|
+
errors: [{
|
|
78
|
+
text: 'StasisEsbuild: watch/rebuild is not supported for capture -- run a one-shot build (rebuilds would silently attest stale content)',
|
|
79
|
+
}],
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
// The stasis preload owns its own write via beforeExit/exit hooks (stasis-core/hooks.js).
|
|
56
85
|
// Standalone and sidecar States have no such hook, so the plugin writes them when the
|
|
57
86
|
// build finishes -- but only on a clean build, otherwise a partial bundle/lockfile
|
package/src/metro.js
CHANGED
|
@@ -108,13 +108,19 @@ function metroDefaultSerializer() {
|
|
|
108
108
|
// hook) is a lower-coverage fallback -- see its note.
|
|
109
109
|
//
|
|
110
110
|
// KNOWN LIMITATIONS (Metro-specific coverage gaps -- documented, not silently ignored):
|
|
111
|
-
// -
|
|
111
|
+
// - Capture is ONE-SHOT ONLY, enforced: a dev server (`metro start`) re-invokes the serializer
|
|
112
|
+
// per rebuild, and the SECOND invocation throws (see #run) -- capture's path-keyed dedupe
|
|
113
|
+
// would otherwise keep attesting a file's first-build bytes after an edit changed them.
|
|
114
|
+
// Metro gives the serializer no watch signal on the first build, so the first rebuild is the
|
|
115
|
+
// earliest refusal point. Load mode (the companion worker transformer) is unaffected and
|
|
116
|
+
// runs fine under a dev server -- it only reads immutable attested bytes.
|
|
117
|
+
// - Child-process capture is best-effort within that one shot. A worker KILLED by signal
|
|
112
118
|
// before its exit hook (jest-worker forceExit / pool overflow) loses its shard SILENTLY at
|
|
113
119
|
// capture time -- a later frozen run still catches the gap (fail-closed), but nothing warns
|
|
114
|
-
// during capture. And it relies on the loader's exit-time write
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
120
|
+
// during capture. And it relies on the loader's exit-time write, which standalone (no stasis
|
|
121
|
+
// loader) Metro never reaches -- it mints no shard dir and produces no toolchain-complete
|
|
122
|
+
// lockfile. Use a one-shot `metro build` under `stasis run --child-process` to capture, then
|
|
123
|
+
// verify with `--lock=frozen`.
|
|
118
124
|
// - Scaled asset variants: `import x from './logo.png'` is ONE module in the graph, keyed
|
|
119
125
|
// by the base path; Metro discovers `logo@2x.png` / `@3x` via its AssetData (parallel
|
|
120
126
|
// scales/files arrays), NOT as separate graph modules, so only the base file is attested
|
|
@@ -130,6 +136,11 @@ export class StasisMetro {
|
|
|
130
136
|
#state
|
|
131
137
|
#resources
|
|
132
138
|
|
|
139
|
+
// Whether this instance already served one capture (#run). The serializer fires once per
|
|
140
|
+
// one-shot `metro build`; a dev server re-invokes it per rebuild, which capture refuses --
|
|
141
|
+
// see the guard in #run.
|
|
142
|
+
#ran = false
|
|
143
|
+
|
|
133
144
|
constructor(options = {}) {
|
|
134
145
|
const { state } = resolvePluginState('StasisMetro', options, process.cwd())
|
|
135
146
|
this.#state = state // null when the plugin should be inert (Rule 7)
|
|
@@ -192,15 +203,31 @@ export class StasisMetro {
|
|
|
192
203
|
'serializer config in load mode, or use bundle=add/replace/frozen here to capture.'
|
|
193
204
|
)
|
|
194
205
|
}
|
|
206
|
+
// Watch/dev-server capture is EXPLICITLY UNSUPPORTED -- refuse the second serialization
|
|
207
|
+
// loudly before it captures anything. Capture's dedupe is keyed by PATH, not content
|
|
208
|
+
// (#seen below, plus State.write's compare-and-skip caches), so on a rebuild where a
|
|
209
|
+
// file's bytes changed the plugin would skip re-capture: the emitted bundle would use the
|
|
210
|
+
// new bytes while the stasis bundle/lockfile silently kept attesting the OLD ones. The
|
|
211
|
+
// serializer fires exactly once per one-shot `metro build`; only a dev server
|
|
212
|
+
// (`metro start`) re-invokes it, and Metro hands the serializer no watch-mode signal on
|
|
213
|
+
// the FIRST build, so the first rebuild is the earliest point capture can refuse. (Load
|
|
214
|
+
// mode has no such hazard and legitimately runs under a dev server -- it lives in the
|
|
215
|
+
// companion worker transformer, rejected above, which only READS immutable attested bytes.)
|
|
216
|
+
if (this.#ran) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
'StasisMetro: watch/dev-server rebuilds are not supported for capture -- use a one-shot `metro build`'
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
this.#ran = true
|
|
195
222
|
this.#capture(graph, preModules)
|
|
196
223
|
|
|
197
224
|
// The stasis preload owns its own write via beforeExit/exit hooks (stasis-core/hooks.js).
|
|
198
225
|
// Standalone and sidecar States have no such hook, so the plugin writes them here. The
|
|
199
226
|
// serializer only runs once Metro has successfully built + transformed the whole graph
|
|
200
227
|
// (a failed build throws before serialization), so this is the clean-build equivalent of
|
|
201
|
-
// webpack's `stats.hasErrors()` / esbuild's `result.errors.length` gate
|
|
202
|
-
//
|
|
203
|
-
//
|
|
228
|
+
// webpack's `stats.hasErrors()` / esbuild's `result.errors.length` gate -- and it runs at
|
|
229
|
+
// most once per instance (the rebuild guard above), so this write is the one-shot build's
|
|
230
|
+
// single flush.
|
|
204
231
|
if (this.#state !== State.preload) this.#state.write()
|
|
205
232
|
}
|
|
206
233
|
|
package/src/webpack.js
CHANGED
|
@@ -484,35 +484,45 @@ export class StasisWebpack {
|
|
|
484
484
|
})
|
|
485
485
|
})
|
|
486
486
|
|
|
487
|
+
// Watch-mode capture is EXPLICITLY UNSUPPORTED -- refuse it loudly before anything is
|
|
488
|
+
// captured. Capture's dedupe is keyed by PATH, not content (#seen above, plus State.write's
|
|
489
|
+
// compare-and-skip caches), so on a watch rebuild where a file's bytes changed the plugin
|
|
490
|
+
// would skip re-capture: the emitted output would use the new bytes while the bundle /
|
|
491
|
+
// lockfile silently kept attesting the OLD ones. Rather than track content changes across
|
|
492
|
+
// rebuilds, refuse watch outright. compiler.hooks.watchRun fires ONLY under
|
|
493
|
+
// compiler.watch() (both webpack 4 and 5) and BEFORE the first watch build's compilation,
|
|
494
|
+
// so this rejects the whole watch session up front, not just the second build. watchRun is
|
|
495
|
+
// an AsyncSeriesHook: a sync `.tap` callback that throws propagates through the hook's call
|
|
496
|
+
// chain and fails the watcher -- exactly the loud refusal we want, so keep it a plain tap.
|
|
497
|
+
// Load mode is deliberately untouched (see #applyLoadMode): it serves immutable attested
|
|
498
|
+
// bytes back into the build, so a dev-server rebuild has no drift hazard there.
|
|
499
|
+
compiler.hooks.watchRun.tap('Stasis', () => {
|
|
500
|
+
throw new Error(
|
|
501
|
+
'StasisWebpack: watch mode is not supported for capture -- ' +
|
|
502
|
+
'run a one-shot build (watch rebuilds would silently attest stale content)'
|
|
503
|
+
)
|
|
504
|
+
})
|
|
505
|
+
|
|
487
506
|
// The stasis preload owns its own write via beforeExit/exit hooks (stasis-core/hooks.js).
|
|
488
507
|
// Standalone and sidecar States have no such hook, so the plugin writes them when the
|
|
489
508
|
// compiler finishes -- but only on a clean build. A failed compilation has incomplete
|
|
490
509
|
// state, and lock=replace would otherwise overwrite a good lockfile with a partial one.
|
|
491
510
|
if (this.#state !== State.preload) {
|
|
492
|
-
// Watch-mode detection. compiler.hooks.watchRun fires ONLY under compiler.watch() (both
|
|
493
|
-
// webpack 4 and 5) and always before that build's `done`, so the flag is set in time.
|
|
494
|
-
// The flag is per-compiler (this closure), which is what we want: each compiler reports
|
|
495
|
-
// its own run kind even when one shared plugin instance drives several.
|
|
496
|
-
let watching = false
|
|
497
|
-
compiler.hooks.watchRun.tap('Stasis', () => { watching = true })
|
|
498
511
|
compiler.hooks.done.tap('Stasis', (stats) => {
|
|
499
512
|
if (stats.hasErrors()) return
|
|
500
|
-
// Write immediately
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// time compiler.run()'s callback resolves).
|
|
507
|
-
// Defer ONLY the multi-compiler one-shot case. compiler.hooks.done fires once PER CHILD
|
|
508
|
-
// COMPILER, so a MultiCompiler with N targets sharing this one State's growing bundle
|
|
509
|
-
// would otherwise pay N brotli + writeFileSync passes over an ever-larger payload (the
|
|
513
|
+
// Write immediately for a single compiler: only one `done`, so there is no N-write
|
|
514
|
+
// amplification and no reason to give up the old write-by-`done` durability (the
|
|
515
|
+
// bundle is on disk by the time compiler.run()'s callback resolves). Defer ONLY the
|
|
516
|
+
// multi-compiler one-shot case. compiler.hooks.done fires once PER CHILD COMPILER, so
|
|
517
|
+
// a MultiCompiler with N targets sharing this one State's growing bundle would
|
|
518
|
+
// otherwise pay N brotli + writeFileSync passes over an ever-larger payload (the
|
|
510
519
|
// dominant, super-linear cost). Deferral collapses those into ONE write of the final
|
|
511
|
-
// bundle at process exit -- mirroring how the preload writes (hooks.js).
|
|
520
|
+
// bundle at process exit -- mirroring how the preload writes (hooks.js). Watch mode
|
|
521
|
+
// never reaches this hook: capture refuses it at watchRun above. esbuild / metro
|
|
512
522
|
// are deliberately NOT changed: their write hook fires once per build (no per-child
|
|
513
523
|
// amplification), and stasis build reads esbuild output in-process, so their synchronous
|
|
514
524
|
// write is load-bearing in a way webpack's per-child `done` write is not.
|
|
515
|
-
if (
|
|
525
|
+
if (this.#captureApplyCount === 1) this.#state.write()
|
|
516
526
|
else this.#deferWrite()
|
|
517
527
|
})
|
|
518
528
|
}
|
|
@@ -527,8 +537,9 @@ export class StasisWebpack {
|
|
|
527
537
|
// a process that never exits normally -- one that stays alive after building and is then
|
|
528
538
|
// killed by a signal (SIGINT/SIGTERM/SIGKILL), which bypass both beforeExit and exit -- would
|
|
529
539
|
// not flush. That is the same capture-then-exit assumption the preload already relies on
|
|
530
|
-
// (hooks.js) and metro documents; the common build-and-exit
|
|
531
|
-
//
|
|
540
|
+
// (hooks.js) and metro documents; the common build-and-exit flow is unaffected, a
|
|
541
|
+
// single-compiler build never reaches here (it writes on `done`), and watch (`--watch`,
|
|
542
|
+
// dev-server) capture is refused outright at watchRun (see #applyCaptureMode).
|
|
532
543
|
#deferWrite() {
|
|
533
544
|
this.#deferredWriteDirty = true
|
|
534
545
|
if (this.#deferredWriteArmed) return
|