feat(plugins): import tracking, options bag, and preload coordination#20
Open
exo-nikita wants to merge 2 commits into
Open
feat(plugins): import tracking, options bag, and preload coordination#20exo-nikita wants to merge 2 commits into
exo-nikita wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a comprehensive node:test suite for the Stasis webpack plugin (lock add/frozen/replace/ignore, bundle add/replace, scope variants, config conflicts, tamper detection) along with two fixture projects, and modernizes src/webpack.js to use the hooks.tap() API with node:assert/strict, recording imports via state.addImport and tagging entries via addFile(..., { isEntry }).
Changes:
- Rewrite
src/webpack.jsto usecompiler.hooks.normalModuleFactory/nmf.hooks.afterResolvetaps, trackisEntry, and callstate.addImport(parentURL, rawRequest, url). - Add
tests/webpack.test.jsandtests/webpack-run.helper.jsrunning webpack in a child process with a freshStateper test. - Add
webpack-fullandwebpack-nmfixtures (with committedstasis.config.jsonandstasis.lock.json).
Reviewed changes
Copilot reviewed 13 out of 17 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/webpack.js | Switch from legacy plugin() callbacks to hooks.tap(), record imports and entry flag |
| tests/webpack.test.js | New 300+ line test suite covering lock modes, bundle, tampering, scopes, conflicts |
| tests/webpack-run.helper.js | Child-process helper that constructs State, runs webpack with the plugin, then writes state |
| tests/fixtures/webpack-full/* | Full-scope fixture: entry, hello, package.json, committed config & lockfile, pnpm-workspace marker |
| tests/fixtures/webpack-nm/* | node_modules-scope fixture: entry/helper, fake-cjs-pkg under node_modules, committed config & lockfile |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
23c5b5f to
a55e84a
Compare
a55e84a to
344e72d
Compare
exo-nikita
pushed a commit
that referenced
this pull request
May 18, 2026
Implements the plugin↔preload rules described in PR #20: 1. Plugin lockfile mode (other than 'none' / 'ignore') without an active preload is a hard throw; the lockfile would otherwise silently miss every dependency the bundler itself pulled in. 2. With a preload, the plugin's lock must agree with the preload's. 3. Lockfile is unified: when plugin and preload agree, the plugin reuses the preload State so one lockfile is written. 4. With a preload that has bundle on, the plugin can't disable bundle and the bundle mode must match; only bundleFile may differ. 5. Same bundleFile (or unspecified) reuses the preload. 6. Different bundleFile constructs a sidecar State -- shares the preload's hashes/entries/modules by reference so lockfile coverage stays unified, but has its own sources/formats/imports/resources and writes only its own bundle file. 7. lock='none' AND bundle='none' with no preload is a no-op plugin (no hooks, no State) -- useful for env/flag-controlled builds. Surface: - src/state.js: State constructor accepts { parent: preload } for sidecar mode (mutually exclusive with preload: true). Sidecar shares hashes/entries/modules by reference; only the bundleFile differs. write() on a sidecar skips the lockfile. New exported helper resolvePluginState(label, options, cwd) encodes all seven rules and is the single entry point for plugin constructors. - src/{webpack,esbuild}.js: constructors delegate to resolvePluginState; no-op State means no hooks registered. - tests/{webpack,esbuild}-run.helper.js: by default construct a preload State (simulating loader-active) with options mirrored from the plugin's. STASIS_TEST_PRELOAD=0 opts a test out of preload to exercise standalone / noop / hard-throw paths. - tests/state-sidecar.test.js: 9 tests for sidecar mechanics + the rule 2/4/5/6 paths of resolvePluginState. - tests/state-resolve-no-preload.test.js: rule 1 (hard throw on every non-ignore lockfile mode) + rule 7 (noop). - tests/{webpack,esbuild}.test.js: rule 1 + rule 7 coverage through the full bundler harness. 275/275 tests pass, lint clean.
Ports the entire PR-20 series onto the post-restructure monorepo layout
(stasis-core/ + stasis/), retaining the seven plugin-preload rules and
the test surface that pinned them down. Replaces the old class-plugin
shape that asserted "Stasis preload is not active" on import.
stasis-core/src/state.js:
- State constructor accepts { parent: preload } for sidecar mode
(mutually exclusive with { preload: true }). Sidecar shares
hashes/entries/modules by reference (unified lockfile coverage) but
manages its own sources/formats/imports/resources and emits its own
bundle file. write() on a sidecar skips the lockfile -- the parent
owns it.
- New exports: TRACKED_EXTENSIONS (js/mjs/cjs/ts/jsx/tsx/json/mts/cts),
isTrackedPath(filePath), and resolvePluginState(label, options, cwd).
The resolver encodes the seven rules and is the single entry point
for plugin constructors.
stasis/src/{webpack,esbuild}.js:
- Both plugins accept { lock, bundle, bundleFile, scope, debug } options
with the same shape as the CLI flags, validated via
validatePluginOptions.
- Constructors delegate to resolvePluginState; setup/apply short-
circuits when state === null (rule 7 noop).
- StasisWebpack moves to the modern compiler.hooks.normalModuleFactory
/afterResolve API (webpack 4+), records addImport edges with
isEntry derived from issuer, and uses resourceResolveData.path
(not data.resource) to avoid query/inline-loader/synthetic resources.
- Both plugins filter to isTrackedPath before addFile/addImport so
untracked extensions (.css/.wasm/...) compile but stay out of the
lockfile.
- Both register a build-end hook (compiler.hooks.done /
build.onEnd) that calls state.write() only when
state !== State.preload and the build had no errors -- so
sidecar/standalone State output reaches disk without clobbering on
a failed compile.
- StasisEsbuild also: accepts the `type` import attribute,
tolerates warnings from sibling plugins, and falls back to the
default loader for unknown extensions.
stasis/tests/:
- webpack-run.helper.js (new) and esbuild-run.helper.js mirror plugin
options onto an auto-constructed preload by default. STASIS_TEST_
PLUGIN_OPTIONS / STASIS_TEST_PRELOAD_OPTIONS / STASIS_TEST_PRELOAD=0
let tests pick the coordination path under test.
- webpack.test.js (new) covers lock/bundle/scope basics + the plugin-
option, rule-1/7, sidecar (rule 6), failed-build no-clobber, and
untracked-extension coverage.
- esbuild.test.js gains parallel plugin-option / rule-1/7 / sidecar /
no-clobber / untracked-extension / import-attribute tests.
- state-sidecar.test.js, state-resolve-no-preload.test.js,
state-resolve-nobundle-preload.test.js cover the resolver and
sidecar mechanics directly.
- New fixtures: tests/fixtures/webpack-full and webpack-nm mirroring
the esbuild ones, adapted for webpack 4 + CJS entries.
All 52 new tests pass; 0 lint warnings.
37f716c to
bc2685b
Compare
The plugins are tightly coupled to State / resolvePluginState /
isTrackedPath, so they belong alongside them in stasis-core. They
import nothing from esbuild/webpack at runtime (they receive the
compiler/build object), so stasis-core stays a zero-dependency runtime.
- Move stasis/src/{esbuild,webpack}.js -> stasis-core/src/, switching
their State import from the @exodus/stasis-core/state package
specifier to the local ./state.js.
- Add ./esbuild and ./webpack to stasis-core exports + files.
- stasis/src/{esbuild,webpack}.js become thin re-export adapters
(export * from @exodus/stasis-core/{esbuild,webpack}), mirroring the
existing loader adapter, so @exodus/stasis/{esbuild,webpack} keeps
working.
- public-exports.test.js asserts the @exodus/stasis entry points
re-export the identical stasis-core classes.
Tests stay in stasis/tests where the test runner and esbuild/webpack
devDeps live; they import the plugins through the stasis adapter, which
also exercises the public surface. Full suite unchanged: 744 pass, 26
pre-existing failures (network-dependent popular-npm-modules + cli
matrix), 0 lint warnings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three threads of work, now sitting as a single commit on top of post-restructure
main.1.
StasisWebpack: record imports and entry statusMigrated to webpack's modern
compiler.hooks.normalModuleFactory.tap/nmf.hooks.afterResolve.tapAPI.afterResolvederivesisEntry = !issuerand records bothstate.addImport(parentURL, rawRequest, url)for every non-entry edge andstate.addFile(url, { source, isEntry, isBinary })once per file. Usesdata.resourceResolveData.path(notdata.resource) so query strings, inline loaders,data:URIs, andnull-loaderoutputs don't crash. New fixtures (webpack-full,webpack-nm) and a spawning helper.2. Both plugins accept a constructor options bag
new StasisWebpack({ lock, bundle, bundleFile, scope, debug })andnew StasisEsbuild({ ... })accept the same keys as the CLI flags. Validation goes through the existingvalidatePluginOptions/assertOptionsMatchConfighelpers instasis-core/config.3. Plugin↔preload coordination rules
The plugin used to assert "Stasis preload is not active" at module import, which blocked the standalone use case entirely. We now codify a richer contract via a new
resolvePluginState(label, options, cwd)helper instasis-core/state:lockother thannone/ignore, no preloadbundle: 'none'/'ignore'bundleFileunset or matches preload'sbundleFilediffers from preload'sState— shareshashes/entries/moduleswith preload by reference (unified lockfile), has its ownsources/formats/imports/resourcesand writes only its own bundle filelock: 'none'ANDbundle: 'none'with no preloadSidecar
Stateis enabled by a newparent: preloadoption on theStateconstructor (mutually exclusive withpreload: true);write()on a sidecar emits the bundle but skips the lockfile (the preload owns it). Both plugins also wirecompiler.hooks.done/build.onEndto callstate.write()for non-preload State (skipping on build failure to avoid clobbering).Hardening that came out of review
isTrackedPath(js/mjs/cjs/ts/jsx/tsx/json/mts/cts) beforeaddFile/addImport— untracked extensions (.css,.wasm, asset modules) compile but stay out of the lockfile, since the lockfile/loader semantics can't round-trip them.StasisEsbuildnow accepts thetypeimport attribute (with { type: 'json' }), tolerates benign warnings from sibling plugins, and falls back to thedefaultloader for unknown extensions instead of asserting.resolvePluginStatevalidatesscopeanddebugagainst the preload (previously it silently dropped plugin values on the sidecar paths).Test plan
node --run lint— cleannode --run test— 770/770 pass (excluding 26 pre-existing failures inpopular-npm-modules.test.js, unrelated)stasis/tests/state-sidecar.test.js— sidecar mechanics + rules 2/4/5/6 ofresolvePluginStatestasis/tests/state-resolve-no-preload.test.js— rule 1 (hard throw acrossadd/frozen/replace) + rule 7 (noop)stasis/tests/state-resolve-nobundle-preload.test.js— preload-with-no-bundle reuse and sidecar paths (incl.bundle: 'ignore'regression)stasis/tests/{webpack,esbuild}.test.js— env-driven, plugin-option, rule-1, rule-7, untracked-extension, failed-build-no-clobber, and rule-6 sidecar coverage end-to-end through the actual bundler harnessSTASIS_TEST_PRELOAD=0opts a test out of the helper's auto-preload to exercise standalone pathsSTASIS_TEST_PRELOAD_OPTIONSlets a test point preload and plugin at differentbundleFilepaths (for the sidecar test)