Skip to content

feat(v9): ship web packages ESM-first (type:module), drop node export condition#36327

Draft
Hotell wants to merge 9 commits into
microsoft:masterfrom
Hotell:feat/v9-valid-esm-drop-node-condition
Draft

feat(v9): ship web packages ESM-first (type:module), drop node export condition#36327
Hotell wants to merge 9 commits into
microsoft:masterfrom
Hotell:feat/v9-valid-esm-drop-node-condition

Conversation

@Hotell

@Hotell Hotell commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Draft. Stacks on #36324 (module-condition) and supersedes it — this branch drops the node condition entirely. Open for early review / CI.

Summary

Migrates converged v9 web packages to ESM-first packaging so bare-Node SSR can import real ESM (and tree-shake), while require still gets CommonJS — no consumer config, no dual-package hazard for single-format graphs.

// package.json
"type": "module",
"main": "lib-commonjs/index.cjs",
"exports": { ".": {
  "import":  { "types": "./dist/index.d.ts",  "default": "./lib/index.js" },
  "require": { "types": "./dist/index.d.cts", "default": "./lib-commonjs/index.cjs" }
}}
  • lib/ ships valid native ESM (fully-specified .js via swc resolveFully).
  • lib-commonjs/ ships .cjs (required under type: module).
  • The node condition is dropped: bare-Node import → ESM, require → CJS; bundlers resolve import → ESM and tree-shake.
  • Per-condition types: require points at a rolled dist/*.d.cts so node16/nodenext CJS consumers are @arethetypeswrong-clean.

Why (vs the module condition in #36324)

The module condition only helps bundled SSR. This unlocks bare-Node externalized ESM (edge/workers, externalized deps) too, by making lib/ genuinely Node-loadable ESM. Latest Griffel (type: module, no node) already ships this exact shape, so the ecosystem is aligned.

What's automated (no per-package manual steps)

  • Build executor: auto-renames lib-commonjs/*.js*.cjs (+ rewrites relative require/maps) and copies dist/*.d.ts*.d.ctsgated on type: module (no-op otherwise).
  • Generators (migrate-converged-pkg, react-library): emit the ESM-first shape, .cjs dev configs, nested-types exports, and dist/*.d.cts in files.
  • Plugin: recognizes jest.config.cjs; adds an optional attw target (dependsOn: build, not in CI gates).

Scope / exclusions

  • Included: 84 converged v9 web packages (type: module).
  • Excluded (kept CommonJS): platform:node packages, storybook addons / preset.js, and just-scripts packages (charts — just.config.* is incompatible with type: module).

Validation

  • react-text dependency chain (bottom-up: tokens, keyboard-keys, react-utilities, react-theme, react-jsx-runtime, react-shared-contexts, react-text): bare-Node require ✅ and import ✅ both load; functionally real.
  • attw (react-text, react-utilities): 🟢 node16-from-CJS, 🟢 node16-from-ESM, 🟢 bundler, 🟢 node10 (exit 0).
  • vNext build sweep: "Successfully ran target build for 91 projects", exit 0.
  • workspace-plugin tests: 37 suites / 239 ✅.

Known limitation (follow-up, not a regression)

Bare-Node import of icon-dependent packages currently fails inside @fluentui/react-icons — an external package whose ESM (lib/index.js) uses extensionless imports (not valid bare-Node ESM). Bundled SSR, client, and require() are unaffected. Needs an upstream fully-specified-ESM fix in react-icons (or temporary exclusion of icon-dependent packages).

Reviewer notes

  • The all-or-nothing nature: bare-Node import only "lights up" once a package's entire transitive @fluentui/* closure is migrated (CJS export * barrels break Node's named-import interop). Throughout rollout, require + bundled never regress.
  • Change-file messages for type: module packages updated to describe the ESM-first state; excluded packages keep the module-condition message.

Hotell added 2 commits June 18, 2026 19:00
…targeted bundlers

Adds a `module` condition nested in `node` to every converged v9 package's export map:
- node-targeted bundlers (webpack/rollup/vite/esbuild) resolve the ESM build and tree-shake
- bare Node ignores `module` and falls back to CommonJS, keeping SSR single-instance / dual-package-hazard free
- enables fully-specified .js import emit via swc `baseUrl` so lib/ is valid ESM

Updates the migrate-converged-pkg generator (source of truth) + swcrc template, and rolls the change out across all v9 packages with matching beachball change files.
…rt condition

Migrates converged v9 web packages to ESM-first packaging:
- `type: module` with valid ESM under lib/ (fully-specified .js) and CommonJS under lib-commonjs/*.cjs
- drop the `node` export condition; bare-Node `import` resolves ESM, `require` resolves CJS
- per-condition types: `require` points at a rolled `dist/*.d.cts` (attw-clean for node16/nodenext CJS)
- rename CJS dev configs to .cjs (jest/eslint/tests) for type:module compatibility

Build executor auto-emits lib-commonjs .cjs + dist .d.cts (gated on type:module).
Generators (migrate-converged-pkg, react-library) emit the new web shape; optional `attw` target added.
Excludes CJS-first (platform:node, storybook addons) and just-scripts (charts) packages.
@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

Hotell added 4 commits June 19, 2026 14:46
@fluentui/tokens is now type:module (ESM, named exports, no default). The generate-tokens script relied on CJS-interop default import; switch to a namespace import.
…e tests

- rit.config.js -> .cjs (react-provider) + teach rit loader (args.ts) and workspace-plugin rit-target inference to prefer .cjs
- .storybook/main.js -> .cjs (recipes), scripts/server.js -> .cjs (tokens)
- getDependencies.spec: react-text main is now lib-commonjs/index.cjs; refresh dep-tree order snapshot
- react-library spec: drop stale jest.config.js from scaffold snapshot
Graph traversal order is non-deterministic across machines; sort deps by name for a stable snapshot.
Comment thread .gitignore
@@ -153,3 +153,5 @@ gulp-cache
.cursor/rules/nx-rules.mdc

@github-actions github-actions Bot Jun 19, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 753 Changed
vr-tests-react-components/Positioning.Positioning end.chromium.png 733 Changed
vr-tests-react-components/ProgressBar converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 51 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 51 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 160 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled - RTL.chromium.png 635 Changed
vr-tests-web-components/Avatar 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-web-components/Avatar. - Dark Mode.normal.chromium_1.png 298 Changed
vr-tests-web-components/RadioGroup 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-web-components/RadioGroup. - Dark Mode.2nd selected.chromium_3.png 119 Changed
vr-tests/react-charting-LineChart 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests/react-charting-LineChart.Multiple - Dark Mode.default.chromium.png 181 Changed

There were 4 duplicate changes discarded. Check the build logs for more information.

Hotell added 3 commits June 19, 2026 17:04
Migrate @fluentui/scripts-cypress to type:module so its TS source loads
correctly when imported from migrated (type:module) cypress configs:

- package.json: type:module; rename node-run configs to .cjs (eslint/jest)
- base.config.ts: __dirname -> import.meta.dirname; add webpack resolve.extensionAlias (.js->.ts)
- index.ts: explicit .ts specifier for base.config re-export (resolves under native ESM via require(esm) from CommonJS cypress configs); explicit extension on browser type import
- browser/index.ts + support/*.js: fully-specified import specifiers; convert support require() to static ESM import
- tsconfig.lib.json: noEmit + allowImportingTsExtensions (source-only package)
- rit cypress.config template: __dirname -> process.cwd() for ESM/CJS sandbox parity

Validated: react-utilities/babel-preset-global-context/react-headless-components-preview e2e,
react-menu-grid-preview test-rit (17) e2e + test, scripts-cypress lint/test/type-check,
workspace-plugin (239) and react-integration-tester (37) unit tests.
…odule)

Complete the ESM-first migration for the one missed web package (it still
shipped the pre-migration shape: main=lib-commonjs/index.js, no type field):

- package.json: type:module, main->lib-commonjs/index.cjs, per-subpath exports
  rewritten to ESM-first nested shape (import.types=.d.ts/default=lib.js,
  require.types=.d.cts/default=lib-commonjs/*.cjs), dist/*.d.cts in files
- jest/eslint/test-setup configs renamed to .cjs
- generate-api: read subpath types from nested import.types (not just flat types)
  so exportSubpaths rollups work with the ESM-first export shape

Also simplify @fluentui/scripts-cypress: inline base.config into index.ts to
remove the only cross-file relative import. This fixes a type-check regression
where the prior explicit .ts specifier leaked an allowImportingTsExtensions
requirement into every cypress consumer, while still resolving under Node's
native ESM loader for CommonJS cypress configs.

Validated: react-headless build/attw(green)/lint/test(712)/type-check/e2e(185),
react-utilities & babel-preset-global-context e2e, react-menu-grid-preview
test-rit(17) e2e, scripts-cypress lint/type-check, workspace-plugin (239) tests.
- react-examples (v8, tsc --module commonjs) compiled FocusTrapZone.e2e.tsx,
  which imports @fluentui/scripts-cypress; the inlined base.config uses
  import.meta (required for type:module) and is invalid under module:commonjs.
  Exclude e2e cypress test files from the v8 library build (they're bundled by
  cypress at runtime, never part of the shipped lib).
- scripts-monorepo: annotate the getDependencies.spec sort comparator so checkJs
  doesn't flag implicit-any params (TS7006).
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.

1 participant