@exodus/stasis-plugins 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/src/plugins.js CHANGED
@@ -1,9 +1,5 @@
1
- // Plugin <-> preload coordination (resolvePluginState): decides which State a bundler
2
- // plugin drives, given its options and the ambient preload. The per-file classification
3
- // helpers (pathExt / parseResourcesOption / classifyExtension) live in util.js -- they're
4
- // pure and shared with State's `--fs` capture, so both paths classify against one source
5
- // of truth. Kept out of state.js because the coordination only consults State.preload +
6
- // Config helpers, never State internals.
1
+ // Plugin <-> preload coordination (resolvePluginState): decides which State a bundler plugin
2
+ // drives, given its options and the ambient preload.
7
3
 
8
4
  import assert from 'node:assert/strict'
9
5
 
@@ -12,58 +8,42 @@ import { ensureStateForLoader, isLoaderInstalled } from '@exodus/stasis-core/hoo
12
8
  import { State } from '@exodus/stasis-core/state'
13
9
  import { extSetsEqual, parseResourcesOption } from '@exodus/stasis-core/util'
14
10
 
15
- // Decides which State a bundler plugin (StasisWebpack / StasisEsbuild) should run against,
16
- // given its constructor options and the ambient preload (or lack thereof). Returns either
17
- // { state: null, isNoop: true } -- plugin should register no hooks and become inert -- or
18
- // { state, isNoop: false } -- plugin should drive the returned State.
19
- //
20
- // The lockfile is always unified: a plugin never specifies its own lockfile position
21
- // (validatePluginOptions rejects `lockFile`). When an ambient preload exists the plugin
22
- // MUST share it -- it either reuses the preload State outright or runs as a sidecar that
23
- // shares the preload's hashes/entries/modules + lockfile data. Only the BUNDLE may split
24
- // off per-plugin (via bundleFile, a sidecar). A process that genuinely needs a non-default
25
- // lockfile location sets it process-wide (Config / EXODUS_STASIS_LOCK_FILE), not per-plugin.
26
- //
27
- // Rules:
28
- // 1. Plugin asking for any lockfile mode other than 'none' / 'ignore' requires a preload.
29
- // Without one it's a hard throw -- the lockfile would otherwise only cover the bundled
30
- // app and silently miss every dependency the bundler itself pulled in.
31
- // 2. With a preload, the plugin's lock mode must agree with the preload's.
32
- // 3. Lockfile is unified: when plugin and preload align we reuse the preload State so a
33
- // single lockfile is written.
34
- // 4. Bundle may split: with a preload that already has bundle on, the plugin cannot disable
35
- // bundle and the bundle mode must match. The plugin's bundleFile, however, may differ
36
- // -- in which case we construct a sidecar State sharing the preload's lockfile data.
11
+ // Decides which State a bundler plugin runs against, given its options and the ambient preload.
12
+ // Returns { state: null, isNoop: true } (inert -- register no hooks) or { state, isNoop: false }.
13
+ // The lockfile is always unified (plugins can't set lockFile); only the BUNDLE may split per-plugin
14
+ // (bundleFile -> sidecar). Rules, referenced by number in the code below:
15
+ // 0. No preload, not under `stasis run`, no lock/bundle option -> inert (safe on a plain build).
16
+ // 1. A writing/verifying lock mode without a preload throws (it would miss the bundler's own deps).
17
+ // 2. With a preload, the plugin's lock mode must agree with it.
18
+ // 3. When plugin and preload align, reuse the preload State (single lockfile).
19
+ // 4. Preload with bundle on: plugin can't disable it and the mode must match; bundleFile may differ.
37
20
  // 5. Same bundleFile (or unspecified) reuses the preload State.
38
- // 6. Different bundleFile creates a sidecar.
39
- // 7. Plugin with lock='none' AND bundle='none' and no preload is a no-op (lets framework
40
- // plugins be env-controlled off without throwing on the lock=none/bundle=none invariant).
21
+ // 6. Different bundleFile creates a sidecar sharing the preload's lockfile data.
22
+ // 7. lock='none' AND bundle='none' with no preload is a no-op (env-controlled-off framework plugins).
41
23
  export function resolvePluginState(label, options, cwd) {
42
24
  validatePluginOptions(label, options)
43
- const { scope, lock, bundle, bundleFile, resourcesBundleFile, debug, childProcess, resources } = options
44
- // Same-instance fast path: the plugin's stasis-core IS the binary's, the
45
- // hooks are installed, but the lazy initState hasn't fired yet (plugin
46
- // ran AHEAD of the first user-file load -- webpack constructs plugins
47
- // during config-parse before user code executes). Force-init now so the
48
- // plugin lands in the ambient branch and inherits the loader's Config.
49
- // We deliberately do NOT do this when the loader is a SEPARATE module
50
- // instance (different stasis-core copy in the plugin's node_modules vs
51
- // the binary's): force-init there would create a phantom State.preload
52
- // with its own beforeExit/exit save handlers that race the real loader's
53
- // lockfile write. Cross-instance detection lives below via env vars.
25
+ const { scope, lock, bundle, bundleFile, resourcesBundleFile, debug, childProcess, packageJSON, resources } = options
26
+ // Same-instance fast path: loader installed but lazy initState hasn't fired (webpack builds plugins
27
+ // before user code). Force-init so the plugin lands in the ambient branch. NOT for a separate
28
+ // stasis-core copy -- that would mint a phantom State.preload racing the real loader's write.
54
29
  if (!State.preload && isLoaderInstalled()) ensureStateForLoader()
55
30
  const ambient = State.preload
56
31
 
57
32
  if (!ambient) {
33
+ // "Under stasis run" detection: the bin script always exports EXODUS_STASIS_LOCK, so it's a
34
+ // process-wide signal regardless of which stasis-core copy hosts this code.
35
+ const underStasisRun = process.env.EXODUS_STASIS_LOCK !== undefined
36
+
37
+ // Rule 0: no preload, not under `stasis run`, no lock/bundle option -> do nothing, so a bare
38
+ // plugin sits inertly in a plain build. Capture is governed elsewhere (env or explicit options).
39
+ if (!underStasisRun && lock === undefined && bundle === undefined) {
40
+ return { state: null, isNoop: true }
41
+ }
42
+
58
43
  if (lock === 'none' && bundle === 'none') return { state: null, isNoop: true } // Rule 7
59
- // Rule 7 needs BOTH to be 'none'. A partial opt-out -- one field is 'none' but
60
- // the other was simply omitted -- would otherwise fall through to either a
61
- // misleading Rule 1 throw about the default lockfile mode, or to Config's
62
- // generic "stasis needs a lockfile or a bundle" RangeError. Catch the partial
63
- // case explicitly. Only fires when the OTHER field is undefined: an active
64
- // bundle mode like `bundle:'load'` / `'frozen'` / `'add'` paired with
65
- // `lock:'none'` is a legitimate "bundle as trust root" config that Config
66
- // accepts (see config.js's bundle=load / bundle=frozen rules).
44
+ // A partial opt-out (one field 'none', the other omitted) would otherwise hit a misleading
45
+ // Rule 1 throw or Config's generic error; catch it explicitly. An active bundle mode paired
46
+ // with lock='none' is a legitimate "bundle as trust root" config, so only fire when the other is undefined.
67
47
  if ((lock === 'none' && bundle === undefined) || (bundle === 'none' && lock === undefined)) {
68
48
  throw new Error(
69
49
  `${label}: to opt out of stasis without a preload, pass BOTH lock='none' and bundle='none' ` +
@@ -71,18 +51,9 @@ export function resolvePluginState(label, options, cwd) {
71
51
  `bundle=${bundle === undefined ? 'default' : `'${bundle}'`})`
72
52
  )
73
53
  }
74
- // Cross-instance "running under stasis run" detection: `stasis run`'s
75
- // bin script always sets EXODUS_STASIS_LOCK on the spawned child (even
76
- // for the default `--lock=add`), so the env var is a reliable
77
- // process-wide signal regardless of which stasis-core copy is hosting
78
- // this code. When set, the runtime's loader is providing full lockfile
79
- // coverage for the process; the plugin's lockfile (at its own path,
80
- // typically a sidecar like `sources.stasis.lock.json`) is a sibling
81
- // artifact covering bundler-side resolutions, and the "lock=add needs
82
- // a preload" rationale doesn't apply. Skip that rule under stasis run;
83
- // Config below will pick up env-var defaults (lock/scope/bundle) for
84
- // the standalone plugin State.
85
- const underStasisRun = process.env.EXODUS_STASIS_LOCK !== undefined
54
+ // A writing/verifying lock (add|replace|frozen) still needs a preload unless under stasis run
55
+ // (whose loader gives full coverage). `lock` may be undefined here if only a writing `bundle`
56
+ // was passed, so the throw below distinguishes a defaulted mode from an explicit one.
86
57
  const effectiveLock = lock ?? DEFAULT_LOCK
87
58
  if (!underStasisRun && effectiveLock !== 'none' && effectiveLock !== 'ignore') {
88
59
  const phrasing = lock === undefined
@@ -96,16 +67,12 @@ export function resolvePluginState(label, options, cwd) {
96
67
  return { state: new State(cwd, options), isNoop: false }
97
68
  }
98
69
 
99
- // Preload is active. Since `lockFile` is not a plugin option, the lockfile is
100
- // necessarily the ambient's -- the plugin shares it (reuse or sidecar below).
101
- // Enforce coordination on the remaining fields before deciding which.
70
+ // Preload active: the lockfile is necessarily the ambient's. Enforce field coordination, then reuse or sidecar.
102
71
  const pc = ambient.config
103
72
  if (lock !== undefined && lock !== pc.lock) {
104
73
  throw new Error(`${label}: lock='${lock}' conflicts with active preload lock='${pc.lock}'`)
105
74
  }
106
- // scope/debug/childProcess must agree with the preload on every preload-coupled path -- reuse
107
- // adopts preload's config wholesale, and sidecar copies it field by field. Catch a
108
- // disagreement here rather than silently overriding the user's value.
75
+ // scope/debug/childProcess must agree with the preload (reuse adopts it, sidecar copies it).
109
76
  if (scope !== undefined && scope !== pc.scope) {
110
77
  throw new Error(`${label}: scope='${scope}' conflicts with active preload scope='${pc.scope}'`)
111
78
  }
@@ -115,10 +82,11 @@ export function resolvePluginState(label, options, cwd) {
115
82
  if (childProcess !== undefined && childProcess !== pc.childProcess) {
116
83
  throw new Error(`${label}: childProcess=${childProcess} conflicts with active preload childProcess=${pc.childProcess}`)
117
84
  }
118
- // resources is process-wide (a Config field), so a plugin's allowlist must agree with
119
- // the preload's -- reuse adopts it wholesale, a sidecar inherits it. Compare the parsed
120
- // sets (order/dupes/case don't matter), failing on a real disagreement rather than
121
- // letting one plugin silently widen the attested asset set.
85
+ if (packageJSON !== undefined && packageJSON !== pc.packageJSON) {
86
+ throw new Error(`${label}: packageJSON=${packageJSON} conflicts with active preload packageJSON=${pc.packageJSON}`)
87
+ }
88
+ // resources is process-wide, so a plugin's allowlist must agree with the preload's -- compare
89
+ // parsed sets (order/dupes/case-insensitive) to prevent one plugin silently widening the asset set.
122
90
  if (resources !== undefined) {
123
91
  const parsed = parseResourcesOption(label, resources)
124
92
  if (!extSetsEqual(parsed, pc.resources)) {
@@ -128,10 +96,8 @@ export function resolvePluginState(label, options, cwd) {
128
96
  }
129
97
  }
130
98
 
131
- // A plugin instance is attaching to the preload now (its constructor called us). Mark it so the
132
- // preload's bundle `reason` map can tell apart files run merely fs-READS during the build (the
133
- // plugin's modules, e.g. webpack+babel reading app source through --fs) from genuine run
134
- // dependencies recorded before any plugin existed. See State#bundleReason.
99
+ // Mark the plugin attached so the preload's bundle `reason` map can distinguish files merely
100
+ // fs-READ during the build from genuine run dependencies recorded before any plugin existed.
135
101
  ambient.markPluginAttached()
136
102
 
137
103
  if (pc.bundle) {
@@ -151,8 +117,7 @@ export function resolvePluginState(label, options, cwd) {
151
117
  assertOptionsMatchConfig(pc, options)
152
118
  return { state: ambient, isNoop: false }
153
119
  }
154
- // Rule 6: different path -- sidecar inherits preload's modes, overrides bundleFile
155
- // (and resourcesBundleFile if the plugin provided one for a split layout).
120
+ // Rule 6: different path -- sidecar inherits preload's modes, overrides bundleFile.
156
121
  const sidecar = new State(cwd, {
157
122
  parent: ambient,
158
123
  scope: pc.scope,
@@ -162,8 +127,9 @@ export function resolvePluginState(label, options, cwd) {
162
127
  resourcesBundleFile,
163
128
  debug: pc.debug,
164
129
  childProcess: pc.childProcess,
130
+ packageJSON: pc.packageJSON,
165
131
  resources: [...pc.resources],
166
- // carried over: sidecars skip stasis.config.json discovery, which may have set it
132
+ // carried over: sidecars skip stasis.config.json discovery
167
133
  brotliQuality: pc.brotliQuality,
168
134
  })
169
135
  return { state: sidecar, isNoop: false }
@@ -171,14 +137,9 @@ export function resolvePluginState(label, options, cwd) {
171
137
 
172
138
  // Preload exists but has no bundle.
173
139
  if (bundle === undefined || bundle === 'none' || bundle === 'ignore') {
174
- // Plugin doesn't want a bundle either: just reuse preload for lockfile coverage.
175
- // bundle='ignore' here legitimately disagrees with preload's bundleMode ('none'),
176
- // both mean "no bundle written by the plugin", so skip the bundle field in the
177
- // cross-check rather than letting assertOptionsMatchConfig flag the difference.
178
- // Defense-in-depth: the strip is only safe when the preload's bundleMode is
179
- // actually in the no-bundle equivalence class. If a future bundleMode reads as
180
- // `!pc.bundle` but means something else, the strip would silently mask a real
181
- // disagreement -- the assert pins the contract to today's two values.
140
+ // Plugin wants no bundle either: reuse preload for lockfile coverage. bundle='ignore' vs the
141
+ // preload's 'none' both mean "no plugin bundle", so strip the bundle field from the cross-check;
142
+ // the assert pins that strip to today's no-bundle values so a future mode can't silently mask a disagreement.
182
143
  assert.ok(pc.bundleMode === 'none' || pc.bundleMode === 'ignore',
183
144
  `unexpected preload bundleMode='${pc.bundleMode}' in the no-bundle branch`)
184
145
  const { bundle: _b, ...rest } = options
@@ -200,6 +161,7 @@ export function resolvePluginState(label, options, cwd) {
200
161
  resourcesBundleFile,
201
162
  debug: pc.debug,
202
163
  childProcess: pc.childProcess,
164
+ packageJSON: pc.packageJSON,
203
165
  resources: [...pc.resources],
204
166
  brotliQuality: pc.brotliQuality, // carried over, like the Rule 6 sidecar above
205
167
  })