Skip to content
This repository was archived by the owner on May 30, 2026. It is now read-only.

feat(build): restore string externals and add build.ignore option - #82

Merged
C. Spencer Beggs (spencerbeggs) merged 6 commits into
mainfrom
bug/bundling
May 16, 2026
Merged

C. Spencer Beggs (spencerbeggs) merged 6 commits into
mainfrom
bug/bundling

Conversation

@spencerbeggs

Copy link
Copy Markdown
Contributor

Summary

  • Regression fix (0.6.4 regressed string externals — build hard-fails on modules that 0.6.3 externalized #81): build.externals string entries were silently ignored in v0.6.4. Leading the rspack externals config with a function caused rspack to stop consulting trailing string entries; configured packages were bundled and hard-failed to resolve. Fixed by folding the entire externalization decision into a single function.
  • New feature — build.ignore: A string[] config option that replaces listed modules with a throwing ESM stub. Use it for optional transitive dependencies the action never exercises (native modules, optional plugins). Unlike build.externals (excluded because they are available at runtime), ignored modules are absent at runtime — code that wraps their require() calls in try/catch correctly detects them as unavailable. build.ignore takes precedence over build.externals.

Test plan

  • pnpm test — 97 unit + 4 integration tests pass (includes two new regression tests: string-externals.int.test.ts guards the externals fix; ignore-modules.int.test.ts guards the build.ignore feature end-to-end)
  • pnpm run typecheck — clean
  • pnpm run lint — clean

Closes #81.

Signed-off-by: C. Spencer Beggs spencer@beggs.codes

Fixes a regression from v0.6.4 in which user-configured string externals
were silently ignored because leading the externals array with a function
caused rspack to stop consulting the trailing string entries. The entire
externalization decision is now folded into a single function.

Adds a new build.ignore config option: a string array of module names to
exclude from the bundle and replace with a throwing ESM stub. Use it for
optional transitive dependencies the action never exercises (e.g. native
modules pulled in as optional plugins). Modules in build.ignore throw at
runtime so try/catch-guarded optional require calls detect them as absent.
build.ignore takes precedence over build.externals.

Closes #81.

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>

@savvy-web-bot savvy-web-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Current Commit: a4f3e01

Summary

This is a clean, well-scoped PR. The regression fix is correct, the new feature is semantically well-designed, and the integration tests are exactly the right kind of evidence for both. All CI checks pass (Tests, Code Quality, Conventional Commits, PR Title Validation, Markdown, CodeQL — all ✅).


Checks

Check Status
Tests ✅ success
Code Quality ✅ success
Conventional Commits ✅ success
PR Title Validation ✅ success
Markdown ✅ success
CodeQL ✅ success

The externals regression fix

The root cause analysis is correct: when rspack externals is an array and the first entry is a function, rspack's array fall-through stops after the function returns a non-false value for any request — trailing string entries never get consulted. Folding everything into a single function eliminates the dependency on that undocumented array-traversal behavior entirely. The fix is minimal and clearly explained in the inline comment.

The guard !config.build.ignore.includes(request) on line 174 is necessary and correct. Because rspack's externals check runs before alias resolution, without that guard a module in both ignore and externals would be externalized (skipping alias substitution entirely), rather than stubbed. The behavior matches what the docs describe.

The build.ignore feature

The semantic distinction between externals (available at runtime) and ignore (absent at runtime, replaced with a throwing stub) is well-drawn and the docs' comparison table makes it immediately clear. The fixture vendor/optional-loader.cjs mirrors the real-world CycloneDX/optional-native-dep pattern closely, making the integration test a meaningful proof of the end-to-end behavior. The stub error message ("A module excluded via the build 'ignore' option was loaded at runtime.") is descriptive and actionable.

Changeset is correctly typed as minor — regression fix + new feature, no breaking changes.

Issues

Two inline comments are attached. In brief:

  1. Subpath imports (line 132) — the $-suffixed alias only stubs exact module-name imports; require("bufferutil/fallback") would still fail the build. The stated use cases (top-level optional native modules) almost certainly never use subpath imports, so this may never bite anyone in practice. But the docs should at least mention the constraint so users who hit it aren't left guessing.

  2. Stub written per entry (line 135) — minor I/O redundancy in multi-entry builds; nit-level and safe to address in a follow-up if desired.

Comment thread src/services/build-live.ts Resolved
Comment thread src/services/build-live.ts Resolved

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a bundling regression where string entries in build.externals were ignored after the node: interop fix, and introduces a new build.ignore option to intentionally stub out optional (runtime-absent) dependencies while keeping builds resolvable.

Changes:

  • Fixes build.externals handling by consolidating externalization logic into a single rspack externals function (restoring string externals behavior).
  • Adds build.ignore: string[] to config schema + docs, implemented via resolve.alias to a throwing stub module (with ignore taking precedence over externals).
  • Adds integration + unit tests and updates documentation/spec artifacts and release changeset.

Reviewed changes

Copilot reviewed 15 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/services/build-live.ts Implements consolidated externals decision function and build.ignore stub/alias mechanism.
src/schemas/config.ts Adds build.ignore to resolved and input config schemas; updates example.
src/schemas/config.test.ts Unit tests for build.ignore defaults and decoding.
README.md Updates feature bullets/headings and mentions externals/ignore.
docs/superpowers/specs/2026-05-15-build-ignore-option-design.md New/updated design spec describing build.ignore semantics and mechanism.
docs/superpowers/plans/2026-05-15-build-ignore-option.md Implementation plan write-up for the feature and regression fix.
docs/configuration.md Documents build.ignore and clarifies externals semantics; adds examples and comparison table.
.claude/design/github-action-builder/architecture.md Updates architecture/config documentation to include ignore and the new bundler behavior.
.changeset/quiet-roads-return.md Changeset for the externals regression fix + new build.ignore feature.
__test__/integration/string-externals.int.test.ts New integration regression test for issue #81 (string externals).
__test__/integration/ignore-modules.int.test.ts New integration test verifying build.ignore stubbing behavior end-to-end.
__test__/integration/fixtures/string-externals/action.yml Fixture manifest for string externals integration test.
__test__/integration/fixtures/string-externals/src/main.ts Fixture source that imports a missing external module to force the regression case.
__test__/integration/fixtures/string-externals/uninstalled-peer-dep.d.ts Ambient type declaration for the missing external module used by the fixture.
__test__/integration/fixtures/ignore-modules/action.yml Fixture manifest for ignore-modules integration test.
__test__/integration/fixtures/ignore-modules/src/main.ts Fixture entry importing a CJS loader that conditionally requires an ignored module.
__test__/integration/fixtures/ignore-modules/vendor/optional-loader.cjs CJS optional-dependency loader using try/catch around require().
__test__/integration/fixtures/ignore-modules/vendor/optional-loader.d.cts Types for the CJS loader fixture module.

Comment thread src/services/build-live.ts Resolved
Comment thread src/services/build-live.ts Resolved
Comment thread docs/configuration.md
Comment on lines 74 to 81
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `minify` | `boolean` | `true` | Minify output for smaller bundles |
| `target` | `string` | `"es2022"` | ECMAScript target version |
| `sourceMap` | `boolean` | `false` | Generate source maps |
| `externals` | `string[]` | `[]` | Packages to exclude from bundle |
| `externals` | `string[]` | `[]` | Packages to leave out of the bundle; must be available at runtime |
| `ignore` | `string[]` | `[]` | Packages to leave out of the bundle and replace with a stub that throws if loaded |
| `quiet` | `boolean` | `false` | Suppress build output |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: target and quiet are not currently in BuildOptionsSchema (checked src/schemas/config.ts). This is a pre-existing documentation gap — not introduced by this PR. Leaving open as a follow-up candidate.

- Write the build.ignore stub into a private mkdtemp directory instead of
  a predictable shared temp path, removing a symlink and race risk, and
  remove the directory after the bundle is written
- Document the exact-match limitation of build.ignore (subpath imports of
  an ignored module are not stubbed) in the configuration docs
- Precompute Sets for the externals and ignore lookups in the externals
  callback
- Correct the design spec wording for the empty-ignore case

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
@savvy-web-bot

savvy-web-bot Bot commented May 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review

Current Commit: f9f31bc

Summary

Third-pass review. The new commit (f9f31bc) is a pure documentation normalization — no functional changes. All threads from previous reviews are resolved or addressed. The PR remains ready to merge.


Validation Results

Check Status
PR Title Validation ✅ success
Conventional Commits ✅ success
Code Quality ✅ success
Markdown ✅ success
Tests ✅ success

(Results from commit c13921d; CI at f9f31bc was not yet complete at review time — the commit is docs-only so no functional regression is possible.)


What Changed Since the Second Review

Commit f9f31bc — "docs: normalize user-facing documentation"

  • Docs pages renamed to a numbered NN-slug.md convention (getting-started.md → 01-getting-started.md, etc.)
  • docs/README.md added as a table-of-contents page
  • Headings converted from Title Case to Sentence case throughout
  • Multi-line source paragraphs joined onto single lines
  • Cross-references updated to new filenames
  • Badge block added to the top-level README.md
  • Example output comments added to programmatic examples; fabricated build numbers replaced with placeholders

All changes are presentation-only and look clean.


Thread Status

All four threads from the first-pass review are resolved:

Thread Issue Status
#3251485701 (mine) $ exact-match — subpath imports not stubbed ✅ Resolved — documented in ignore section
#3251485975 (mine) Stub written per-entry + shared temp path ✅ Resolved — mkdtempSync per-build dir + cleanup
#3251497192 (Copilot) Predictable temp path security ✅ Resolved — same mkdtempSync fix
#3251497204 (Copilot) Array.includes in hot externals callback ✅ Resolved — precomputed Set in c13921d

Two Copilot threads remain open:

Thread Issue Assessment
#3251497220 (Copilot) target/quiet listed in docs but absent from BuildOptionsSchema Pre-existing; not introduced by this PR. Good candidate for a follow-up issue.
#3251497230 (Copilot) Spec wording: "no resolve.alias" vs actual behavior Fixed in c13921d — spec now reads "resolve.alias is given no entries and no stub is written." Thread can be resolved.

Overall Assessment

Nothing to add on top of the second review. The implementation is solid, both threads I flagged are cleanly resolved, and the docs normalization commit is tidy. The Copilot target/quiet schema gap is real but predates this PR and should be tracked separately.

This PR is ready to merge.

- Rename docs/ pages to the NN-slug.md convention and add docs/README.md
  as a table-of-contents page
- Rewrite title-case headings to sentence case across the docs
- Join multi-line paragraphs onto single source lines
- Add the standard badge block to the README
- Add expected-output comments to programmatic examples and replace
  fabricated build-summary numbers with placeholders

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Rewrite AI-tell prose (promotional phrasing, filler, forced rule-of-three,
sycophantic pleasantries, copula avoidance) into more direct, specific
wording across the README and all six docs pages. Code blocks, headings,
badges, and examples are unchanged.

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
Signed-off-by: C. Spencer Beggs <spencer@savvyweb.systems>
Signed-off-by: C. Spencer Beggs <spencer@savvyweb.systems>
@spencerbeggs
C. Spencer Beggs (spencerbeggs) merged commit aa802d6 into main May 16, 2026
6 checks passed
@spencerbeggs
C. Spencer Beggs (spencerbeggs) deleted the bug/bundling branch May 16, 2026 00:15
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0.6.4 regressed string externals — build hard-fails on modules that 0.6.3 externalized

2 participants