Skip to content

feat(react-icons-atomic-webpack-loader): add export-level atomization - #1246

Draft
Martin Hochel (Hotell) wants to merge 14 commits into
microsoft:mainfrom
Hotell:feat/export-level-atomizer
Draft

Martin Hochel (Hotell) wants to merge 14 commits into
microsoft:mainfrom
Hotell:feat/export-level-atomizer

Conversation

@Hotell

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

Copy link
Copy Markdown
Collaborator

Summary

  • add opt-in moduleGranularity: 'icon' while preserving the existing 'family' default
  • rewrite selected exports to versioned query-addressed ESM modules without publishing one physical file per icon export
  • support static imports/re-exports, direct atomic imports, and statically provable dynamic-import groups
  • add the complete __fluentIcon=v1 compilation handshake required by font and SVG-sprite subsetting plugins
  • preserve and compose source maps only when the bundler requests them
  • cover Webpack and Rspack integration, system and brand icons, headless/font variants, exact queried identities, malformed selectors, and stale protocol contracts
  • document loader-rule coverage, compatibility, rollback, caching, and chunk-distribution considerations

Why icon granularity?

Family mode already tree-shakes unused exports. Icon mode changes module identity so exports from the same physical icon family can be placed in different chunks.

For example:

// main.js
import { AddRegular } from '@fluentui/react-icons';

// lazy.js
import { AddFilled } from '@fluentui/react-icons';

With the default family granularity, both imports resolve to the same generated family module:

main.js  ─┐
          ├─> lib/atoms/svg/add.js
lazy.js  ─┘

The bundler must assign that single module identity as a unit, which can move the family into a shared or otherwise less optimal chunk.

With moduleGranularity: 'icon', the loader emits independent virtual module identities:

main.js  ──> lib/atoms/svg/add.js?__fluentIcon=v1&export=416464526567756c6172
lazy.js  ──> lib/atoms/svg/add.js?__fluentIcon=v1&export=41646446696c6c6564

Each selected module contains only its declaration and required imports, allowing the bundler to keep AddRegular with the main chunk and AddFilled with the lazy chunk.

Configuration

{
  test: /\.[mc]?[jt]sx?$/,
  enforce: 'pre',
  use: [
    {
      loader: '@fluentui/react-icons-atomic-webpack-loader',
      options: {
        moduleGranularity: 'icon',
      },
    },
  ],
}

The rule must cover importers and the generated Fluent ESM atom directories because icon mode uses two loader passes:

  1. Rewrite the importer to a query-addressed family request.
  2. Process the resolved family file and emit the selected virtual module.

Font and SVG-sprite variants require their matching query-aware subsetting plugin. The compilation handshake fails closed when the plugin is missing or supports a different selector key/version. Family mode does not use this handshake.

Validation

  • builds, lint, unit tests, and Webpack/Rspack fixtures for the atomic loader, font plugin, and SVG-sprite plugin
  • default family-mode and explicit icon-mode plugin integration coverage
  • generated ESM declaration independence verification
  • repeatable benchmark covering 5,000 selected exports and 20 complete importer rewrites

Notes

Add versioned per-export selectors and dynamic grouping.

Make font and sprite subsetting query-aware, with coordinated integration coverage.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

📋 PR Validation Summary

Check the Build react library job summary for detailed reports:

  • 📦 Bundle Size — size comparison against the base branch
  • 📖 Docsite Preview — build artifact with local preview instructions

To view: click the link above → select the Build react library job → open the Summary tab.

Comment thread packages/react-icons-atomic-webpack-loader/test/selector-protocol.test.ts Outdated
Comment thread packages/react-icons-atomic-webpack-loader/test/transform.test.ts
Comment thread packages/react-icons-atomic-webpack-loader/src/selector-protocol.ts Outdated
Comment thread packages/react-icons-atomic-webpack-loader/test/make-configs.js
Comment thread packages/react-icons-atomic-webpack-loader/CHANGELOG.md Outdated
Comment thread packages/react-icons/build-verify.test.js Outdated
Comment thread packages/react-icons-atomic-webpack-loader/README.md
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
@Hotell

Copy link
Copy Markdown
Collaborator Author

Split the general SVG-sprite per-entrypoint attribution fix into #1247. This PR now remains focused on icon granularity and the selector protocol handshake.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
@Hotell Martin Hochel (Hotell) changed the title feat(react-icons): add export-level atomization feat(react-icons-atomic-webpack-loader): add export-level atomization Sep 17, 2026

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.

🟡 Changes recommended

Unresolved critical and moderate findings affect RSC behavior, source maps, selector validation, and benchmark reliability.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds opt-in icon-level atomization for React icons while preserving family granularity by default, with coordinated font/SVG-sprite subsetting support.

Changes:

  • Adds query-addressed ESM selection for static, direct, re-exported, and dynamic imports.
  • Extends subsetting plugin selector handshakes.
  • Adds source-map handling, integration tests, documentation, and benchmarking.

Final findings:

  • Critical, 1 vote: Grouped dynamic-import modules drop the "use client" directive.
  • Moderate, 2 votes: Pass-through processing can return source maps when maps are disabled.
  • Moderate, 1 vote each: Benchmark setup lacks its generated dependency prerequisite; malformed selectors do not fail closed; an intermediate source map lacks source identity.
File summaries
File Reviewed change
yarn.lock Locks the source-map dependency.
packages/react-icons/build-verify.test.js Verifies independent generated declarations.
packages/react-icons-svg-sprite-subsetting-webpack-plugin/test/webpack.config.js Adds icon-mode integration coverage.
packages/react-icons-svg-sprite-subsetting-webpack-plugin/src/selector-protocol.ts Defines SVG-sprite selector handling.
packages/react-icons-svg-sprite-subsetting-webpack-plugin/src/index.ts Supports queried sprite resources.
packages/react-icons-svg-sprite-subsetting-webpack-plugin/README.md Documents selector integration.
packages/react-icons-font-subsetting-webpack-plugin/test/plugin.test.ts Tests queried font resources and protocol errors.
packages/react-icons-font-subsetting-webpack-plugin/test/make-configs.js Adds icon-mode font fixtures.
packages/react-icons-font-subsetting-webpack-plugin/src/selector-protocol.ts Defines font selector handling.
packages/react-icons-font-subsetting-webpack-plugin/src/index.ts Supports queried font modules.
packages/react-icons-font-subsetting-webpack-plugin/README.md Documents query-aware subsetting.
packages/react-icons-atomic-webpack-loader/test/transform.test.ts Tests icon-level transformations.
packages/react-icons-atomic-webpack-loader/test/src/icon-granularity-svg.js Adds a static-import fixture.
packages/react-icons-atomic-webpack-loader/test/src/icon-granularity-dynamic.js Adds a dynamic-import fixture.
packages/react-icons-atomic-webpack-loader/test/selector-protocol.test.ts Tests selector parsing and emission.
packages/react-icons-atomic-webpack-loader/test/make-configs.js Adds Webpack/Rspack assertions.
packages/react-icons-atomic-webpack-loader/test/loader.test.ts Tests loader selection, handshakes, and maps.
packages/react-icons-atomic-webpack-loader/test/benchmark.js Adds atomization benchmarking.
packages/react-icons-atomic-webpack-loader/src/transform.ts Implements icon-level request rewriting.
packages/react-icons-atomic-webpack-loader/src/source-maps.ts Composes source maps.
packages/react-icons-atomic-webpack-loader/src/selector-protocol.ts Implements selector encoding and validation.
packages/react-icons-atomic-webpack-loader/src/select-export.ts Emits selected ESM declarations.
packages/react-icons-atomic-webpack-loader/src/modules.ts Provides icon and family resolution helpers.
packages/react-icons-atomic-webpack-loader/src/loader-context.ts Extends loader context metadata.
packages/react-icons-atomic-webpack-loader/src/index.ts Integrates selection and plugin checks.
packages/react-icons-atomic-webpack-loader/src/direct-path.ts Handles explicit family imports.
packages/react-icons-atomic-webpack-loader/README.md Documents icon granularity and configuration.
packages/react-icons-atomic-webpack-loader/package.json Adds the remapping dependency and benchmark script.
packages/docsite/stories/Icons/IconsBuildTransforms.mdx Documents icon-level chunk distribution.
Review details

Suppressed comments (4)

packages/react-icons-atomic-webpack-loader/package.json:13

  • This script builds only the loader, but test/benchmark.js immediately reads ../../react-icons/lib/atoms/svg; those generated lib files are not present after a clean install. The documented standalone benchmark therefore fails with a missing-directory error unless the contributor manually builds react-icons first; build that workspace target here (or document the prerequisite).
    "benchmark": "yarn build && node test/benchmark.js"

packages/react-icons-atomic-webpack-loader/src/index.ts:113

  • The no-op fast path has the same source-map leak: it returns inputSourceMap regardless of this.sourceMap. With source maps disabled, this should pass undefined rather than preserve the incoming map.
    return this.callback(null, sourceCode, inputSourceMap);

packages/react-icons-atomic-webpack-loader/src/selector-protocol.ts:11

  • A malformed selector that contains the reserved key without = (for example ?__fluentIcon or ?foo=1&__fluentIcon) does not match this pattern, so parseSelectorQuery returns null and the generated-resource bailout silently loads the full family. Match a bare key as well so the pair validation fails closed as documented.
const SELECTOR_QUERY_PATTERN = new RegExp(`(?:^\\?|&)${SELECTOR_QUERY_KEY}=`);

packages/react-icons-atomic-webpack-loader/src/transform.ts:513

  • This new composition path generates the intermediate map without a source (or embedded content), so when inputSourceMap is present the remapper has no reliable source identity for these mappings and can leave importer positions unmapped or tied to an empty source. Generate the map with the current path (and content), as the selector-emission path already does.
    map: sourceMap ? src.generateMap({ hires: true }) : undefined,
  • Files reviewed: 28/29 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/react-icons-atomic-webpack-loader/src/select-export.ts
Comment thread packages/react-icons-atomic-webpack-loader/src/index.ts Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: eb2f6b74-f69e-472a-a562-355785a6ffe3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants