Skip to content

chore(workspace-plugin): harden verify-packaging with export map check and gate attw in CI - #36617

Open
Martin Hochel (Hotell) wants to merge 5 commits into
microsoft:masterfrom
Hotell:chore/verify-packaging-export-map
Open

Martin Hochel (Hotell) wants to merge 5 commits into
microsoft:masterfrom
Hotell:chore/verify-packaging-export-map

Conversation

@Hotell

@Hotell Martin Hochel (Hotell) commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Previous Behavior

verify-packaging asserted on broad globs (lib/**/*.js, dist/*, …) but never cross-referenced the export map. An entry declaring a file that was never built passed unnoticed while resolving to nothing at runtime.

It also only ran for 2 projects (react-text, react-components), and attw — already inferred by the workspace plugin — was explicitly not a CI gate.

Two latent defects surfaced while working on this:

  • The ships cjs assertion has been vacuous since the native ESM migration. It used lib-commonjs/**/*.(js|map), which never matches a .cjs file. It only passed because sourcemaps (index.cjs.map) ship alongside — so it was effectively asserting "ships cjs sourcemaps". Any package built without maps would have failed spuriously.
  • executor.spec.ts pushed tags onto a shared context mock, so v8 / ships-amd / ships-bundle accumulated across tests.

New Behavior

Export map cross-check

Every file referenced by any condition in package.json#exports must appear in the npm pack --dry-run file list.

Paths under src/ are skipped: dev-only conditions (e.g. ./__dev in babel-preset-storybook-full-source) resolve to source, which the executor already asserts is never shipped — checking them would contradict an existing rule.

This is the complement to the sync generators in #36615 / #36616, which cannot cover it: those are pure Tree operations that run before the build, while this needs built output and npm pack. Sync answers "is the declaration correct?"; this answers "did the declared files actually get built and shipped?".

Wider, cheaper scope

verifyPackaging.include now covers the critical packages rather than 2:

react-components, react-headless-components-preview, react-utilities, react-button, react-charts

verify-packaging also moves from the default named input to production, so test-only PRs no longer re-pack. production is default minus test files, so this is a strict narrowing.

react, utilities, web-components and chart-web-components define their own verify-packaging npm scripts and resolve to nx:run-script, so they are unaffected by the plugin's include list and keep their existing behaviour.

attw is now a CI gate

buildAttwTarget is gated on packageJSON.type === 'module'. CommonJS-first packages always report inherent 🤨 CJS default export interop findings under the node16 profile, so gating them would be permanently red. This self-maintains — a package becomes gated the moment it migrates to ESM-first.

attw is added to the affected target list in pr.yml. 80 ESM-first packages, all green.

Also fixed

  • ships cjs pattern → lib-commonjs/**/*.(js|cjs) (sourcemaps alone no longer satisfy the assertion).
  • Tag leakage in executor.spec.ts (assigns instead of pushes).

Verification

  • Unit tests for the new assertion: passes when every entry ships, fails naming the missing paths when one does not, ignores dev-only src/ conditions.
  • Widening initially surfaced 1 real failure (babel-preset-storybook-full-source, the ./__dev case) — now handled; all covered projects pass.
  • production input verified both directions: touching Button.test.tsx → full cache hit (15/15); touching Button.tsx → correctly invalidated (13/15).
  • nx run-many -t attw → 80/80 green.
  • Initial, pre-restack workspace-plugin run: 317 tests / 42 suites green. Final combined-stack run: 385 tests / 44 suites / 225 snapshots green.

Performance

Scenario Time
Warm / cached 8.6s (103/103 cache hits)
npm pack --dry-run, per package 583ms median, 721ms mean

npm pack cost is dominated by subprocess startup rather than package size, which is why the project count matters more than package size.

Related Issue(s)


Note

Third of a 3 PR stack, branched off #36616 — earlier commits will show here until those merge.

Review Readiness Update

Updated head: 1c17895cdc4ad9cdf848e15bf40fdd0e1bb32d12.

CI follow-up: the initial restacked run exposed TS2353 in the new unmatched-wildcard regression fixture. The one-line repair uses the existing typed import condition; workspace-plugin:type-check and all 13 executor tests passed afterward. The earlier React 17 SwatchPicker focus timeout was in unchanged source; React-major integration and E2E both passed naturally on the final head. No gate was bypassed.

Reports For Owner Review

  • The current-head visual change report lists 12 changed screenshots. The bot review thread remains unresolved for owner review. Screenshot generation passing does not constitute image approval; no independent image comparison or baseline acceptance was performed.
  • The bundle report reports react-headless-components-preview entire-library increases of 340 B minified / 139 B gzip. Its content was verified against the monosize-report artifact from the successful exact-head bundle-size run. This combined-stack delta remains visible for owner review and has not been independently attributed to the packaging change.

Ready for review, not merged. #36616 must still land before this stack's final PR.

@github-actions

github-actions Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

📊 Bundle size report

✅ No changes found

@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

Comment thread .github/workflows/pr.yml
Generates bundle-size fixtures from source so they cannot silently fall behind
what a package actually exports. Only fixtures declared in
project.json#metadata.bundleSizeFixtures are generated; hand written monosize
fixtures in the same folder are left alone.

entryPoints kind namespace imports every non-root export subpath, resolved from
metadata.exportMap rather than package.json#exports so a drifted export map
cannot hide behind a matching drifted fixture. This immediately caught
AllComponents.fixture.js missing menu-button, positioning and utils.

baseHooks kind named imports every use*Base_unstable hook exported by the
project's workspace dependencies, enumerated by walking each dependency's
src/index.ts with the TypeScript parser. Source is used rather than the
etc/*.api.md rollups because api.md is generated by generate-api, which itself
reads package.json#exports - deriving fixtures from it would reintroduce the
coupling this harness exists to break. Regenerating BaseHooks.fixture.js
reproduces the hand maintained file byte for byte.
Adds an export map cross-check to verify-packaging: every file referenced by any
condition in package.json#exports must appear in the npm pack file list. The
existing assertions only matched broad globs and never looked at the export map,
so an entry pointing at a file that was never built passed unnoticed while
resolving to nothing at runtime. Paths under src/ are skipped because dev only
conditions (eg. ./__dev) resolve to source, which is already asserted as never
shipped.

Widens verify-packaging from a hardcoded two project include list to every non
private v9 library - 6 projects to 91, all passing.

Fixes the ships-cjs assertion, which used `lib-commonjs/**/*.(js|map)` and so
never matched a `.cjs` file - after the native ESM migration it was only ever
passing because sourcemaps ship alongside.

Scopes the attw target to esm first packages and adds it to the affected CI
gate. CommonJS-first packages always report inherent `CJS default export`
interop findings under the node16 profile, so gating them would be permanently
red. All 80 esm first packages pass.

Also fixes tag leakage in the verify-packaging spec, which pushed onto a shared
context mock so tags accumulated across tests.

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.

Looks good to me!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants