Skip to content

Commit 3f43052

Browse files
authored
chore(react-icons): make build transform docs DRY and ship docs to npm (#1036)
1 parent 51ba031 commit 3f43052

7 files changed

Lines changed: 180 additions & 310 deletions

File tree

packages/react-icons/README.md

Lines changed: 2 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -336,122 +336,9 @@ Without this setting, TypeScript will not be able to resolve the individual icon
336336

337337
Migrating a larger codebase to the new performant atomic imports might be a daunting task. To make this migration more straightforward, you can leverage build-time import transforms to get all the benefits without modifying your actual code.
338338

339-
These transforms automatically rewrite imports from:
339+
Use `svg` as the target path. This transformation happens at build time, so your source code remains unchanged while your bundled output gets the full tree-shaking benefits.
340340

341-
```tsx
342-
import { AccessTime24Filled } from '@fluentui/react-icons';
343-
```
344-
345-
To the optimized atomic import:
346-
347-
```tsx
348-
import { AccessTime24Filled } from '@fluentui/react-icons/svg/access-time';
349-
```
350-
351-
This transformation happens at build time, so your source code remains unchanged while your bundled output gets the full tree-shaking benefits.
352-
353-
#### Babel
354-
355-
If you use Babel for transpilation, add [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports) with the following setup:
356-
357-
Copy the following helper into your project (e.g. as `fluent-icons-transform.js`):
358-
359-
```js
360-
// @filename fluent-icons-transform.js
361-
362-
/**
363-
* Resolves a @fluentui/react-icons import name to its atomic module path.
364-
* @param {string} importName - The named export being imported.
365-
* @returns {string} The resolved module path.
366-
*/
367-
function resolveFluentIconImport(importName) {
368-
if (importName === 'useIconContext' || importName === 'IconDirectionContextProvider') {
369-
return '@fluentui/react-icons/providers';
370-
}
371-
372-
const match = importName.match(/^(.+?)(\d+)?(Regular|Filled|Light|Color)$/);
373-
if (!match) {
374-
return '@fluentui/react-icons/utils';
375-
}
376-
377-
return `@fluentui/react-icons/svg/${kebabCase(match[1])}`;
378-
}
379-
380-
function kebabCase(str) {
381-
return str.replace(/[a-z\d](?=[A-Z])|[a-zA-Z](?=\d)|[A-Z](?=[A-Z][a-z])/g, '$&-').toLowerCase();
382-
}
383-
384-
module.exports = { resolveFluentIconImport };
385-
```
386-
387-
Then use it in your Babel config:
388-
389-
```js
390-
// @filename .babelrc.js
391-
const { resolveFluentIconImport } = require('./fluent-icons-transform');
392-
393-
module.exports = {
394-
presets: [
395-
// ... your preset configuration
396-
],
397-
plugins: [
398-
[
399-
'transform-imports',
400-
{
401-
'@fluentui/react-icons': {
402-
transform: resolveFluentIconImport,
403-
preventFullImport: false,
404-
skipDefaultConversion: true,
405-
},
406-
},
407-
],
408-
],
409-
};
410-
```
411-
412-
#### SWC
413-
414-
If you use SWC for transpilation, add [@swc/plugin-transform-imports](https://www.npmjs.com/package/@swc/plugin-transform-imports) with the following setup:
415-
416-
```jsonc
417-
// @filename .swcrc
418-
{
419-
"jsc": {
420-
"experimental": {
421-
"plugins": [
422-
[
423-
"@swc/plugin-transform-imports",
424-
{
425-
"@fluentui/react-icons": {
426-
"transform": [
427-
// Transform provider imports to /providers
428-
["^(useIconContext|IconDirectionContextProvider)$", "@fluentui/react-icons/providers"],
429-
// Transform icon imports to /svg/{icon-name}
430-
// (.+?) lazily captures the icon base name (may contain digits,
431-
// e.g. "Space3D", "Rotate315Right"), (\d+)? greedily strips any
432-
// trailing all-digit segment (size suffixes like 16/20/24, but
433-
// also level indicators like Battery0) — this mirrors the
434-
// normalizeBaseName logic used by the generation pipeline.
435-
// {{ kebabCase }} on group 1 mirrors lodash.kebabCase.
436-
[
437-
"(.+?)(\\d+)?(Regular|Filled|Light|Color)$",
438-
"@fluentui/react-icons/svg/{{ kebabCase memberMatches.[1] }}",
439-
],
440-
// Fallback: all other exports are utilities
441-
[".*", "@fluentui/react-icons/utils"],
442-
],
443-
"preventFullImport": false,
444-
"skipDefaultConversion": true,
445-
"handleDefaultImport": false,
446-
"handleNamespaceImport": false,
447-
},
448-
},
449-
],
450-
],
451-
},
452-
},
453-
}
454-
```
341+
👉 **[Build-Time Transform setup (Babel & SWC) →](./docs/build-transforms.md)**
455342

456343
## Atomic API (SVG Sprites) — ⚠️ Alpha
457344

packages/react-icons/build-transforms.test.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const STANDARD_ICON_CASES = [
8888

8989
const ALL_CASES = [...DIGIT_ICON_CASES, ...TRAILING_DIGIT_CASES, ...STANDARD_ICON_CASES];
9090

91-
describe('README build-transform kebab-case consistency', () => {
91+
describe('build-transforms kebab-case consistency', () => {
9292
describe('resolveFluentIconImport matches lodash.kebabCase (generation pipeline)', () => {
9393
it.each(ALL_CASES)('%s → %s', (importName, expected) => {
9494
expect(resolveFluentIconImport(importName)).toBe(`@fluentui/react-icons/svg/${expected}`);
@@ -108,6 +108,28 @@ describe('README build-transform kebab-case consistency', () => {
108108
});
109109
});
110110

111+
describe('resolveFluentIconImport target parameter', () => {
112+
it.each([
113+
['svg', '@fluentui/react-icons/svg/access-time'],
114+
['svg-sprite', '@fluentui/react-icons/svg-sprite/access-time'],
115+
['fonts', '@fluentui/react-icons/fonts/access-time'],
116+
['base/svg', '@fluentui/react-icons/base/svg/access-time'],
117+
['base/svg-sprite', '@fluentui/react-icons/base/svg-sprite/access-time'],
118+
['base/fonts', '@fluentui/react-icons/base/fonts/access-time'],
119+
])('target=%s → %s', (target, expected) => {
120+
expect(resolveFluentIconImport('AccessTime24Filled', target)).toBe(expected);
121+
});
122+
123+
it('defaults to svg when target is omitted', () => {
124+
expect(resolveFluentIconImport('AccessTime24Filled')).toBe('@fluentui/react-icons/svg/access-time');
125+
});
126+
127+
it('provider and utility imports are target-independent', () => {
128+
expect(resolveFluentIconImport('useIconContext', 'base/svg')).toBe('@fluentui/react-icons/providers');
129+
expect(resolveFluentIconImport('bundleIcon', 'base/svg')).toBe('@fluentui/react-icons/utils');
130+
});
131+
});
132+
111133
describe('SWC regex captures correct base name for kebabCase helper', () => {
112134
it.each(ALL_CASES)('%s → group1 kebab-cases to %s', (importName, expected) => {
113135
const group1 = swcCaptureGroup1(importName);
@@ -161,7 +183,7 @@ function collectAtomExports() {
161183
return pairs;
162184
}
163185

164-
describe('README build-transforms resolve every generated atom export', () => {
186+
describe('build-transforms resolve every generated atom export', () => {
165187
const atomExports = collectAtomExports();
166188

167189
it(`should have collected exports (sanity check)`, () => {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Build-Time Import Transforms
2+
3+
You can keep root-level barrel imports and leverage build-time transforms to automatically rewrite them to optimized atomic imports — no source code changes required.
4+
5+
These transforms rewrite imports from:
6+
7+
```tsx
8+
import { AccessTime24Filled } from '@fluentui/react-icons';
9+
```
10+
11+
To the optimized atomic import for your chosen target:
12+
13+
```tsx
14+
// SVG (standard)
15+
import { AccessTime24Filled } from '@fluentui/react-icons/svg/access-time';
16+
17+
// SVG sprites
18+
import { AccessTime24Filled } from '@fluentui/react-icons/svg-sprite/access-time';
19+
20+
// Font icons
21+
import { AccessTime24Filled } from '@fluentui/react-icons/fonts/access-time';
22+
23+
// Base API (SVG)
24+
import { AccessTime24Filled } from '@fluentui/react-icons/base/svg/access-time';
25+
26+
// Base API (SVG sprites)
27+
import { AccessTime24Filled } from '@fluentui/react-icons/base/svg-sprite/access-time';
28+
29+
// Base API (fonts)
30+
import { AccessTime24Filled } from '@fluentui/react-icons/base/fonts/access-time';
31+
```
32+
33+
The examples below use `svg` as the target path. Replace it with the appropriate path for your setup from the list above.
34+
35+
## Babel
36+
37+
Add [babel-plugin-transform-imports](https://www.npmjs.com/package/babel-plugin-transform-imports) with the following setup.
38+
39+
Copy the following helper into your project (e.g. as `fluent-icons-transform.js`):
40+
41+
```js
42+
// @filename fluent-icons-transform.js
43+
44+
/**
45+
* Resolves a @fluentui/react-icons import name to its atomic module path.
46+
* @param {string} importName - The named export being imported.
47+
* @param {string} [target='svg'] - The target subpath (e.g. 'svg', 'svg-sprite', 'fonts',
48+
* 'base/svg', 'base/svg-sprite', 'base/fonts').
49+
* @returns {string} The resolved module path.
50+
*/
51+
function resolveFluentIconImport(importName, target = 'svg') {
52+
if (importName === 'useIconContext' || importName === 'IconDirectionContextProvider') {
53+
return '@fluentui/react-icons/providers';
54+
}
55+
56+
const match = importName.match(/^(.+?)(\d+)?(Regular|Filled|Light|Color)$/);
57+
if (!match) {
58+
return '@fluentui/react-icons/utils';
59+
}
60+
61+
return `@fluentui/react-icons/${target}/${kebabCase(match[1])}`;
62+
}
63+
64+
function kebabCase(str) {
65+
return str.replace(/[a-z\d](?=[A-Z])|[a-zA-Z](?=\d)|[A-Z](?=[A-Z][a-z])/g, '$&-').toLowerCase();
66+
}
67+
68+
module.exports = { resolveFluentIconImport };
69+
```
70+
71+
Then use it in your Babel config:
72+
73+
```js
74+
// @filename .babelrc.js
75+
const { resolveFluentIconImport } = require('./fluent-icons-transform');
76+
77+
module.exports = {
78+
presets: [
79+
// ... your preset configuration
80+
],
81+
plugins: [
82+
[
83+
'transform-imports',
84+
{
85+
'@fluentui/react-icons': {
86+
// Change the second argument to match your target:
87+
// 'svg' | 'svg-sprite' | 'fonts' | 'base/svg' | 'base/svg-sprite' | 'base/fonts'
88+
transform: (importName) => resolveFluentIconImport(importName, 'svg'),
89+
preventFullImport: false,
90+
skipDefaultConversion: true,
91+
},
92+
},
93+
],
94+
],
95+
};
96+
```
97+
98+
## SWC
99+
100+
Add [@swc/plugin-transform-imports](https://www.npmjs.com/package/@swc/plugin-transform-imports) with the following setup.
101+
102+
Replace every `svg` segment in the target paths below with your chosen target (`svg`, `svg-sprite`, `fonts`, `base/svg`, `base/svg-sprite`, or `base/fonts`):
103+
104+
```jsonc
105+
// @filename .swcrc
106+
{
107+
"jsc": {
108+
"experimental": {
109+
"plugins": [
110+
[
111+
"@swc/plugin-transform-imports",
112+
{
113+
"@fluentui/react-icons": {
114+
"transform": [
115+
// Transform provider imports to /providers
116+
["^(useIconContext|IconDirectionContextProvider)$", "@fluentui/react-icons/providers"],
117+
// Transform icon imports to /{target}/{icon-name}
118+
// (.+?) lazily captures the icon base name (may contain digits,
119+
// e.g. "Space3D", "Rotate315Right"), (\d+)? greedily strips any
120+
// trailing all-digit segment (size suffixes like 16/20/24, but
121+
// also level indicators like Battery0) — this mirrors the
122+
// normalizeBaseName logic used by the generation pipeline.
123+
// {{ kebabCase }} on group 1 mirrors lodash.kebabCase.
124+
[
125+
"(.+?)(\\d+)?(Regular|Filled|Light|Color)$",
126+
"@fluentui/react-icons/svg/{{ kebabCase memberMatches.[1] }}",
127+
],
128+
// Fallback: all other exports are utilities
129+
[".*", "@fluentui/react-icons/utils"],
130+
],
131+
"preventFullImport": false,
132+
"skipDefaultConversion": true,
133+
"handleDefaultImport": false,
134+
"handleNamespaceImport": false,
135+
},
136+
},
137+
],
138+
],
139+
},
140+
},
141+
}
142+
```

0 commit comments

Comments
 (0)