Skip to content

docs(public-docsite-v9): add native ESM examples (browser import map + Node) - #36682

Open
Martin Hochel (Hotell) wants to merge 6 commits into
microsoft:masterfrom
Hotell:docs/native-esm-import-map-example
Open

docs(public-docsite-v9): add native ESM examples (browser import map + Node)#36682
Martin Hochel (Hotell) wants to merge 6 commits into
microsoft:masterfrom
Hotell:docs/native-esm-import-map-example

Conversation

@Hotell

@Hotell Martin Hochel (Hotell) commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Previous Behavior

There was no example showing that Fluent UI v9 can run without a bundler. All docsite demos are React stories compiled by webpack, so the ESM-first packaging ("type": "module" + dual lib/ / lib-commonjs/ output) had no user-facing proof point.

New Behavior

Adds two runnable, buildless examples — browser and Node — plus a docs page explaining both. Neither uses JSX (it can't be parsed without a compiler), so both build elements with const h = React.createElement.

Browser — public/examples/native-esm.html

A single static HTML file, served via staticDirs, that renders a themed Fluent UI Button with no bundler, no node_modules, and no build step:

  • <script type="importmap"> maps react, react-dom/client and @fluentui/react-components to esm.sh, which rewrites the bare specifiers inside the ~70 transitive @fluentui/* / @griffel/* deps so the browser can resolve them.
  • ?external=react,react-dom keeps the CDN from inlining a second React copy (which would break hooks and FluentProvider's context).

Node — public/examples/native-esm-node.mjs

A plain .mjs script that server-renders the same Button. Node needs no import map — it picks the ESM build through the import export condition — and the output is a full HTML document with Griffel styles extracted via renderToStyleElements.

Node ESM resolution gaps found

Node's resolution is stricter than any bundler's, and this example surfaced two issues. Both are already being addressed elsewhere — neither blocks this PR.

  1. tabster (via @fluentui/react-tabster) declares "type": "module" and ships ESM in dist/esm/, but has no exports map and a CommonJS main, so Node loads the CJS entry:

    SyntaxError: Named export 'createTabster' not found. The requested module 'tabster' is a CommonJS module
    

    Reproduces on the latest release, tabster@8.8.0. Fix in flight upstream: microsoft/tabster#588. Until it ships and the dependency is bumped, the example includes public/examples/esm-compat-hook.mjs, a small resolve hook redirecting the specifier to tabster's ESM build; the hook and the --import flag can then be deleted. Bundlers are unaffected since they honor the module field Node ignores.

  2. @fluentui/react-icons must be reasonably current. Older builds — including 2.0.311, the version this repo's lockfile currently pins — emit extensionless relative imports (export * from './icons/chunk-0') which Node rejects with ERR_MODULE_NOT_FOUND. Fixed in current releases (2.0.339 emits ./icons/chunk-0.js); the bump is coming in a separate PR. Until then the Node example cannot be run in place inside this monorepo — the file header says so and gives copy-into-an-empty-folder instructions.

Both are linked from the docs page so the workaround can be removed once the fixes land.

Docs

src/Concepts/NativeEsm.mdx (Concepts > Developer > Native ESM) covers both examples, with a live embed of the browser one, the duplicate-React pitfall, the gaps above, and caveats (request waterfall, no tree shaking, no build-time Griffel optimization, manual version pinning). Registered in the nav ordering in .storybook/preview.js.

Verification

  • Browser: served standalone and inside Storybook — Button renders fully themed, --colorBrandBackground resolves, state updates work, zero console errors. Network trace confirms exactly one React instance and per-package unbundled fetches for the whole v9 graph.
  • Node: verified in a clean scratch project outside the monorepo (npm i react react-dom @fluentui/react-components@fluentui/react-icons@2.0.339, tabster@8.8.0, use-sync-external-store@1.6.0; Node 22.21.1) — emits a valid document containing fui-Button markup and data-make-styles-bucket style tags. Without the hook it still fails on tabster, confirming the workaround is load-bearing.
  • Both .mjs files are reachable through staticDirs (200), so the docs links resolve in the deployed site.

No change file — @fluentui/public-docsite-v9 is a private app.

Related Issue(s)

Adds a standalone, bundler-free HTML page that loads @fluentui/react-components from esm.sh via a browser import map, plus a Concepts > Developer docs page that explains it and embeds it live.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

📊 Bundle size report

✅ No changes found

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Pull request demo site: URL

Complements the browser import map example with a runnable .mjs script that server-renders a Fluent UI Button via the package's import export condition, plus a resolve hook working around tabster missing an exports map.
@Hotell Martin Hochel (Hotell) changed the title docs(public-docsite-v9): add native ESM import map example docs(public-docsite-v9): add native ESM examples (browser import map + Node) Sep 2, 2026
Running the example in place inside the monorepo failed: it resolves to the workspace's older hoisted deps. Renames the hook to esm-compat-hook.mjs, adds a use-sync-external-store/shim redirect, and documents that a fresh install of published packages is required.
The repo resolves use-sync-external-store@1.6.0, which has a proper exports map covering ./shim; the earlier ERR_UNSUPPORTED_DIR_IMPORT was from a stale install, not a real packaging gap. Simplifies the hook back to the tabster redirect and documents react-icons as the reason the example cannot run in place inside the monorepo.
References microsoft/tabster#588 in the hook, the docs page, so the workaround can be removed once it ships. Icons bump is handled in a separate PR.
@Hotell
Martin Hochel (Hotell) marked this pull request as ready for review September 2, 2026 09:53
return h(
FluentProvider,
{ theme: webLightTheme },
h('p', null, `Clicked ${count} times`),

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.

Would it be worth making the count a live region (for example,  role="status" )?
Since focus remains on the button, I don’t think the updated count will be reliably announced by screen readers.
I realize this is a minimal demo, but it may also be copied as an example pattern.

return h(
FluentProvider,
{ theme: webLightTheme, style: { padding: 24 } },
h('p', null, `Clicked ${count} ${count === 1 ? 'time' : 'times'}`),

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.

Same pattern as in the documentation snippet.
If we add live-region semantics there, could we update this runnable example to match?

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 overall.
I left one non-blocking accessibility suggestion about announcing the updated count to screen readers. 🚀

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.

[Feature]: support native ESM modules from CDN

2 participants