@fluentui/react-icons-atomic-webpack-loader 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -8
- package/lib/index.d.ts +41 -0
- package/lib/index.js +8 -2
- package/lib/modules.d.ts +52 -1
- package/lib/modules.js +85 -7
- package/lib/transform.d.ts +8 -0
- package/lib/transform.js +228 -25
- package/package.json +5 -8
package/README.md
CHANGED
|
@@ -63,17 +63,21 @@ module.exports = {
|
|
|
63
63
|
|
|
64
64
|
## Supported modules
|
|
65
65
|
|
|
66
|
-
| Module | Variants | Notes |
|
|
67
|
-
| ----------------------------- | ---------------------------- | ----------------------------- |
|
|
68
|
-
| `@fluentui/react-icons` | `svg`, `fonts`, `svg-sprite` | Has `/providers` and `/utils` |
|
|
69
|
-
| `@fluentui/react-brand-icons` | `svg` | Has `/utils`; no `/providers` |
|
|
66
|
+
| Module | Variants | Headless | Notes |
|
|
67
|
+
| ----------------------------- | ---------------------------- | -------------- | ----------------------------- |
|
|
68
|
+
| `@fluentui/react-icons` | `svg`, `fonts`, `svg-sprite` | `svg`, `fonts` | Has `/providers` and `/utils` |
|
|
69
|
+
| `@fluentui/react-brand-icons` | `svg` | `svg` | Has `/utils`; no `/providers` |
|
|
70
|
+
|
|
71
|
+
> **Color icons are SVG-only.** Color variants (`*Color`) rely on gradients that cannot be represented in an icon font, so they ship only in the `svg` and `svg-sprite` builds — never `fonts`. The loader reroutes color imports off font variants automatically (see [Color icons](#color-icons) below).
|
|
70
72
|
|
|
71
73
|
## Options
|
|
72
74
|
|
|
73
|
-
| Option
|
|
74
|
-
|
|
|
75
|
-
| `iconVariant`
|
|
76
|
-
| `fallbackVariant`
|
|
75
|
+
| Option | Type | Default | Description |
|
|
76
|
+
| --------------------- | -------------------------------------- | ----------- | --------------------------------------------------------------------------------------- |
|
|
77
|
+
| `iconVariant` | `'svg'` \| `'fonts'` \| `'svg-sprite'` | `'svg'` | Variant icons resolve to. Applied to every supported module. |
|
|
78
|
+
| `fallbackVariant` | `'svg'` \| `'fonts'` \| `'svg-sprite'` | `undefined` | Variant used for a module that does not support `iconVariant` (see below). |
|
|
79
|
+
| `headless` | `boolean` | `false` | Resolve to the headless (Griffel-free) build where the module ships one. |
|
|
80
|
+
| `allowDynamicImports` | `boolean` | `false` | Atomize a narrow, statically-provable subset of dynamic `import()` barrels (see below). |
|
|
77
81
|
|
|
78
82
|
### Variant resolution & `fallbackVariant`
|
|
79
83
|
|
|
@@ -97,6 +101,26 @@ Resolution is lazy and per file: only modules actually imported in a given file
|
|
|
97
101
|
}
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
### Color icons
|
|
105
|
+
|
|
106
|
+
Color variants (`*Color`, e.g. `AddCircleColor`) are **SVG-only** — their gradients cannot be represented in an icon font, so the `fonts` build ships no color glyphs. When a color icon is imported under a color-less variant (`iconVariant: 'fonts'`), the loader reroutes just that import to a color-capable variant, following the same precedence as above (constrained to `svg` / `svg-sprite`), and emits a warning:
|
|
107
|
+
|
|
108
|
+
1. If `iconVariant` is already color-capable (`svg` / `svg-sprite`), the color import is left on it — no reroute, no warning.
|
|
109
|
+
2. Otherwise, if `fallbackVariant` is set and color-capable, it is used (e.g. `svg-sprite`).
|
|
110
|
+
3. Otherwise the loader falls back to `svg`.
|
|
111
|
+
|
|
112
|
+
Rerouting is per specifier, so color and non-color icons in the same statement resolve independently:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
// iconVariant: 'fonts'
|
|
116
|
+
import { AddFilled, AddCircleColor } from '@fluentui/react-icons';
|
|
117
|
+
// →
|
|
118
|
+
import { AddFilled } from '@fluentui/react-icons/fonts/add';
|
|
119
|
+
import { AddCircleColor } from '@fluentui/react-icons/svg/add-circle';
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> Color icons are deprecated. See the [user guidance](https://microsoft.github.io/fluentui-system-icons/?path=/docs/icons-user-guidance--docs#color-variants-deprecated).
|
|
123
|
+
|
|
100
124
|
### Using font icons
|
|
101
125
|
|
|
102
126
|
```js
|
|
@@ -116,6 +140,40 @@ Resolution is lazy and per file: only modules actually imported in a given file
|
|
|
116
140
|
|
|
117
141
|
This changes icon resolution from `@fluentui/react-icons/svg/*` to `@fluentui/react-icons/fonts/*`. Non-icon exports (`utils`, `providers`) are unaffected.
|
|
118
142
|
|
|
143
|
+
> Color icons have no font build and are rerouted to `svg` (or `svg-sprite`) automatically — see [Color icons](#color-icons).
|
|
144
|
+
|
|
145
|
+
### Using the headless API
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
{
|
|
149
|
+
loader: '@fluentui/react-icons-atomic-webpack-loader',
|
|
150
|
+
options: {
|
|
151
|
+
iconVariant: 'fonts',
|
|
152
|
+
headless: true,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
With the example above:
|
|
158
|
+
|
|
159
|
+
| Import | Resolves to |
|
|
160
|
+
| ------------------- | ------------------------------------------ |
|
|
161
|
+
| `AddFilled` | `@fluentui/react-icons/headless/fonts/add` |
|
|
162
|
+
| `bundleIcon` (util) | `@fluentui/react-icons/headless/utils` |
|
|
163
|
+
| `useIconContext` | `@fluentui/react-icons/providers` (shared) |
|
|
164
|
+
|
|
165
|
+
Notes:
|
|
166
|
+
|
|
167
|
+
- **Best-effort per module:** a module without a headless build for the resolved variant degrades to its standard (Griffel) implementation with a warning rather than failing the build. This applies to headless `svg-sprite` (not generated yet).
|
|
168
|
+
- **Version requirement:** headless `@fluentui/react-brand-icons` requires `>= 2.0.206`. The loader rewrites imports statically and does not check the installed version, so an older brand-icons will fail to resolve the `/headless/*` entries at build time.
|
|
169
|
+
- **Context is shared:** `useIconContext` / `IconDirectionContextProvider` always resolve to `@fluentui/react-icons/providers` — it is framework-agnostic and reused by both APIs.
|
|
170
|
+
- **CSS is your responsibility:** the loader only rewrites component/utility imports. You must still import the headless CSS in your app entry point:
|
|
171
|
+
```js
|
|
172
|
+
import '@fluentui/react-icons/headless/styles.css';
|
|
173
|
+
// and, for font icons:
|
|
174
|
+
import '@fluentui/react-icons/headless/fonts/styles.css';
|
|
175
|
+
```
|
|
176
|
+
|
|
119
177
|
### Using SVG sprite icons
|
|
120
178
|
|
|
121
179
|
```js
|
|
@@ -139,6 +197,40 @@ This changes icon resolution from `@fluentui/react-icons/svg/*` to `@fluentui/re
|
|
|
139
197
|
|
|
140
198
|
The loader parses each module and rewrites import and re-export declarations that reference a supported module. Each named specifier is routed to an atomic subpath based on its name:
|
|
141
199
|
|
|
200
|
+
### Resolution flow
|
|
201
|
+
|
|
202
|
+
Each named specifier is resolved independently, so color and non-color icons — even within the same statement — can land on different variants.
|
|
203
|
+
|
|
204
|
+
```mermaid
|
|
205
|
+
flowchart TD
|
|
206
|
+
A["Named specifier from a supported module"] --> B{"Icon name? ends in Regular / Filled / Light / Color"}
|
|
207
|
+
B -->|"context / hook"| P["/providers"]
|
|
208
|
+
B -->|"utility"| U["/utils"]
|
|
209
|
+
B -->|"yes"| V{"Module supports iconVariant?"}
|
|
210
|
+
|
|
211
|
+
V -->|"yes"| R["variant = iconVariant"]
|
|
212
|
+
V -->|"no"| F{"fallbackVariant set?"}
|
|
213
|
+
F -->|"no"| ERR["Error: import left untouched, set fallbackVariant"]
|
|
214
|
+
F -->|"yes, supported"| R2["variant = fallbackVariant"]
|
|
215
|
+
F -->|"yes, unsupported"| RS["variant = svg (warning)"]
|
|
216
|
+
|
|
217
|
+
R --> C{"Color icon?"}
|
|
218
|
+
R2 --> C
|
|
219
|
+
RS --> C
|
|
220
|
+
|
|
221
|
+
C -->|"no"| H{"headless requested and available for variant?"}
|
|
222
|
+
C -->|"yes, already color-capable: svg / svg-sprite"| H
|
|
223
|
+
C -->|"yes, color-less variant: fonts"| CC["reroute to first color-capable of iconVariant, fallbackVariant, svg (warning)"]
|
|
224
|
+
CC --> H
|
|
225
|
+
|
|
226
|
+
H -->|"yes"| HP["prefix with /headless"]
|
|
227
|
+
H -->|"no / not available (warning)"| STD["standard build"]
|
|
228
|
+
HP --> OUT["resolved atomic path"]
|
|
229
|
+
STD --> OUT
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
> Steps marked "(warning)" emit a build warning — a best-effort degrade rather than a hard failure.
|
|
233
|
+
|
|
142
234
|
### `@fluentui/react-icons`
|
|
143
235
|
|
|
144
236
|
| Export type | Example | Resolved path |
|
|
@@ -156,8 +248,89 @@ The loader parses each module and rewrites import and re-export declarations tha
|
|
|
156
248
|
|
|
157
249
|
Files that don't reference a supported module are passed through untouched (fast pre-check).
|
|
158
250
|
|
|
251
|
+
## Limitations
|
|
252
|
+
|
|
253
|
+
### Dynamic imports are not atomized
|
|
254
|
+
|
|
255
|
+
The loader only rewrites **static** `import` / `export … from` declarations. A dynamic `import()` of a barrel cannot be atomized, because the returned module-namespace object is a runtime value whose usage the loader cannot statically prove:
|
|
256
|
+
|
|
257
|
+
```js
|
|
258
|
+
// ⚠️ Not rewritten — the ENTIRE icon set is pulled into the async chunk.
|
|
259
|
+
const { AddFilled } = await import('@fluentui/react-icons');
|
|
260
|
+
React.lazy(() => import('@fluentui/react-icons'));
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
When it detects a dynamic import of a supported barrel, the loader emits a warning. Import the atomic path directly instead — then you lazy-load only the icons you use:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
// ✅ Only this icon lands in the async chunk.
|
|
267
|
+
const { AddFilled } = await import('@fluentui/react-icons/svg/add');
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Alternatively, move the icons behind a local module that statically imports them; the loader atomizes that module, and only your lazy chunk pays for what it uses.
|
|
271
|
+
|
|
272
|
+
> The same applies to full-barrel subpaths (`@fluentui/react-icons/svg`, `@fluentui/react-icons/fonts`) — dynamically importing those also bundles the whole set. Always target a per-icon atomic path.
|
|
273
|
+
|
|
274
|
+
### Opt-in: `allowDynamicImports`
|
|
275
|
+
|
|
276
|
+
> **Prefer a dedicated module of static imports.** The most robust pattern is a
|
|
277
|
+
> small module that statically imports the icons you need and is itself lazy-loaded
|
|
278
|
+
> (`const { AddFilled } = await import('./lazy-icons')`). Static imports are
|
|
279
|
+
> atomized unconditionally, tree-shake predictably, and avoid every gotcha below.
|
|
280
|
+
> Reach for `allowDynamicImports` only when refactoring to that pattern isn't
|
|
281
|
+
> practical.
|
|
282
|
+
|
|
283
|
+
With `allowDynamicImports: true`, the loader additionally rewrites a **narrow,
|
|
284
|
+
statically-provable** subset of dynamic barrel imports into atomic ones. Only two
|
|
285
|
+
call-site shapes qualify — where the imported names are literals at the `import()`:
|
|
286
|
+
|
|
287
|
+
```js
|
|
288
|
+
// await + object destructure
|
|
289
|
+
const { AddFilled } = await import('@fluentui/react-icons');
|
|
290
|
+
// → const { AddFilled } = await import('@fluentui/react-icons/svg/add');
|
|
291
|
+
|
|
292
|
+
// .then + object-pattern callback param
|
|
293
|
+
import('@fluentui/react-icons').then(({ AddFilled }) => …);
|
|
294
|
+
// → import('@fluentui/react-icons/svg/add').then(({ AddFilled }) => …);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Names from the **same** atom are grouped into one import; names from **different**
|
|
298
|
+
atoms become a positional `Promise.all([...])`:
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
const { AddFilled, ArrowLeftRegular } = await import('@fluentui/react-icons');
|
|
302
|
+
// →
|
|
303
|
+
const [{ AddFilled }, { ArrowLeftRegular }] = await Promise.all([
|
|
304
|
+
import('@fluentui/react-icons/svg/add'),
|
|
305
|
+
import('@fluentui/react-icons/svg/arrow-left'),
|
|
306
|
+
]);
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
It honors `iconVariant` / `fallbackVariant` / `headless` and per-name color
|
|
310
|
+
rerouting exactly like static imports.
|
|
311
|
+
|
|
312
|
+
#### Gotchas — what is **not** rewritten (left untouched, still warns)
|
|
313
|
+
|
|
314
|
+
- **Namespace binding**: `const ns = await import('@fluentui/react-icons')` — `ns`
|
|
315
|
+
is a runtime object; usage isn't statically known, so the whole set ships.
|
|
316
|
+
- **`.then(m => m.AddFilled)`**: namespace parameter + member access — not a
|
|
317
|
+
destructure, so the names aren't visible at the call site.
|
|
318
|
+
- **Rest / computed / default / nested patterns**: `{ AddFilled, ...rest }`,
|
|
319
|
+
`{ [name]: icon }`, `{ AddFilled = fallback }`, `{ Add: { … } }`.
|
|
320
|
+
- **Non-literal specifiers**: `import(pkg)`, template interpolation, or a promise
|
|
321
|
+
stored in a variable and `.then`-ed elsewhere.
|
|
322
|
+
|
|
323
|
+
For any of these the loader leaves your code as-is and emits the standard
|
|
324
|
+
"cannot be atomized" warning — the safe default.
|
|
325
|
+
|
|
326
|
+
> ⚠️ The `Promise.all` rewrite changes the emitted runtime structure (parallel
|
|
327
|
+
> chunk loading, positional destructuring). It's semantically equivalent for the
|
|
328
|
+
> supported shapes, but if you depend on the exact expression shape, prefer the
|
|
329
|
+
> dedicated-module pattern above.
|
|
330
|
+
|
|
159
331
|
## Requirements
|
|
160
332
|
|
|
161
333
|
- `webpack` >= 5
|
|
162
334
|
- `@fluentui/react-icons` >= 2 (with atomic subpath exports)
|
|
163
335
|
- `@fluentui/react-brand-icons` (with atomic subpath exports), if used
|
|
336
|
+
- `>= 2.0.206` when using `headless: true` — earlier versions do not ship the `/headless/svg/*` and `/headless/utils` entries, so the loader's rewritten imports will fail to resolve.
|
package/lib/index.d.ts
CHANGED
|
@@ -8,13 +8,54 @@ export interface FluentIconsAtomicImportLoaderOptions {
|
|
|
8
8
|
* Not every module supports every variant (e.g. `@fluentui/react-brand-icons`
|
|
9
9
|
* only ships `svg`). When a referenced module does not support this variant,
|
|
10
10
|
* `fallbackVariant` is used instead.
|
|
11
|
+
*
|
|
12
|
+
* Color icons are an exception: they are SVG-only (gradients cannot live in an
|
|
13
|
+
* icon font), so a `*Color` import under `iconVariant: 'fonts'` is rerouted to
|
|
14
|
+
* a color-capable variant (`svg` / `svg-sprite`) following the same
|
|
15
|
+
* `iconVariant → fallbackVariant → svg` precedence, with a warning.
|
|
11
16
|
*/
|
|
12
17
|
iconVariant?: IconVariant;
|
|
13
18
|
/**
|
|
14
19
|
* The variant to use for a referenced module that does not support
|
|
15
20
|
* `iconVariant`. When omitted and a module cannot honor `iconVariant`, the
|
|
16
21
|
* loader fails with a descriptive error.
|
|
22
|
+
*
|
|
23
|
+
* Also used as the preferred target when rerouting SVG-only color icons off a
|
|
24
|
+
* color-less `iconVariant` (e.g. `fonts`), provided the fallback itself is
|
|
25
|
+
* color-capable; otherwise the loader degrades to `svg`.
|
|
17
26
|
*/
|
|
18
27
|
fallbackVariant?: IconVariant;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve atomic imports to the **headless** (Griffel-free) build where the
|
|
30
|
+
* referenced module ships one. Defaults to `false`.
|
|
31
|
+
*
|
|
32
|
+
* Headless is best-effort per module: a module without a headless build for
|
|
33
|
+
* the resolved variant (e.g. headless `svg-sprite` which isn't generated yet)
|
|
34
|
+
* degrades to its standard implementation with a warning instead of failing
|
|
35
|
+
* the build.
|
|
36
|
+
*
|
|
37
|
+
* NOTE: the loader only rewrites component/utility imports — you must still
|
|
38
|
+
* import the headless CSS (`@fluentui/react-icons/headless/styles.css`, plus
|
|
39
|
+
* `headless/fonts/styles.css` for font icons) in your app entry point.
|
|
40
|
+
*/
|
|
41
|
+
headless?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Rewrite a **narrow, statically-provable** subset of dynamic `import()` barrel
|
|
44
|
+
* calls into atomic dynamic imports. Defaults to `false`.
|
|
45
|
+
*
|
|
46
|
+
* Only two shapes are rewritten, where the imported names are known literals at
|
|
47
|
+
* the call site:
|
|
48
|
+
* - `const { AddFilled } = await import('@fluentui/react-icons')`
|
|
49
|
+
* - `import('@fluentui/react-icons').then(({ AddFilled }) => …)`
|
|
50
|
+
*
|
|
51
|
+
* Names from the same atom are grouped into one import; names from different
|
|
52
|
+
* atoms become a positional `Promise.all([...])`. Anything else (namespace
|
|
53
|
+
* binding `const ns = await import(…)`, `.then(m => m.X)`, rest/computed/default
|
|
54
|
+
* patterns, non-literal specifiers) is left untouched and still warns.
|
|
55
|
+
*
|
|
56
|
+
* Prefer a dedicated module of **static** atomic imports that you lazy-load
|
|
57
|
+
* (`import('./icons')`) over relying on this; see the README for the gotchas.
|
|
58
|
+
*/
|
|
59
|
+
allowDynamicImports?: boolean;
|
|
19
60
|
}
|
|
20
61
|
export default function fluentIconsAtomicImportLoader(this: LoaderContext<FluentIconsAtomicImportLoaderOptions>, sourceCode: string): void;
|
package/lib/index.js
CHANGED
|
@@ -9,12 +9,18 @@ function fluentIconsAtomicImportLoader(sourceCode) {
|
|
|
9
9
|
if (!modules_1.SUPPORTED_MODULE_NAMES.some((name) => sourceCode.includes(name))) {
|
|
10
10
|
return this.callback(null, sourceCode);
|
|
11
11
|
}
|
|
12
|
-
const { iconVariant = 'svg', fallbackVariant } = this.getOptions();
|
|
12
|
+
const { iconVariant = 'svg', fallbackVariant, headless = false, allowDynamicImports = false } = this.getOptions();
|
|
13
13
|
let code;
|
|
14
14
|
let map;
|
|
15
15
|
let diagnostics;
|
|
16
16
|
try {
|
|
17
|
-
({ code, map, diagnostics } = (0, transform_1.transformSource)(sourceCode, {
|
|
17
|
+
({ code, map, diagnostics } = (0, transform_1.transformSource)(sourceCode, {
|
|
18
|
+
iconVariant,
|
|
19
|
+
fallbackVariant,
|
|
20
|
+
headless,
|
|
21
|
+
allowDynamicImports,
|
|
22
|
+
path: resourcePath,
|
|
23
|
+
}));
|
|
18
24
|
}
|
|
19
25
|
catch (error) {
|
|
20
26
|
const reason = error instanceof Error ? error.message : String(error);
|
package/lib/modules.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export type IconVariant = 'svg' | 'fonts' | 'svg-sprite';
|
|
|
5
5
|
* Every module supports `svg`, so it is always a valid resolution target.
|
|
6
6
|
*/
|
|
7
7
|
export declare const DEFAULT_SAFETY_VARIANT: IconVariant;
|
|
8
|
+
/**
|
|
9
|
+
* Whether an import name is a *color* icon variant, i.e. its style suffix is
|
|
10
|
+
* `Color` (e.g. `AddCircleColor`, `AddCircle20Color`). Every icon export carries
|
|
11
|
+
* exactly one trailing style suffix, so a `Color` suffix unambiguously marks a
|
|
12
|
+
* color variant — icons whose base name merely contains the word "Color" (e.g.
|
|
13
|
+
* `TextColorRegular`) end in a different style suffix.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isColorIconName(importName: string): boolean;
|
|
8
16
|
/**
|
|
9
17
|
* Describes how a single supported module's barrel imports are rewritten into
|
|
10
18
|
* atomic deep paths.
|
|
@@ -14,13 +22,27 @@ export interface ModuleDescriptor {
|
|
|
14
22
|
name: string;
|
|
15
23
|
/** Icon variants this module ships atomic entry points for. */
|
|
16
24
|
supportedVariants: IconVariant[];
|
|
25
|
+
/**
|
|
26
|
+
* Icon variants for which this module ships a *headless* (Griffel-free) build.
|
|
27
|
+
* Empty when the module has no headless build at all.
|
|
28
|
+
*/
|
|
29
|
+
headlessVariants: IconVariant[];
|
|
30
|
+
/**
|
|
31
|
+
* Icon variants for which this module ships *color* icon atoms.
|
|
32
|
+
*
|
|
33
|
+
* Color icons are SVG-only by nature — their gradients cannot be represented
|
|
34
|
+
* in an icon font — so the font builds contain no color glyphs. Empty when the
|
|
35
|
+
* module has no color icons at all.
|
|
36
|
+
*/
|
|
37
|
+
colorVariants: IconVariant[];
|
|
17
38
|
/**
|
|
18
39
|
* Resolves the atomic subpath for a single named import.
|
|
19
40
|
*
|
|
20
41
|
* @param importName - The imported binding name (e.g. `AddFilled`, `bundleIcon`).
|
|
21
42
|
* @param variant - The already-resolved, supported icon variant for this module.
|
|
43
|
+
* @param headless - Whether to resolve to the headless (Griffel-free) build.
|
|
22
44
|
*/
|
|
23
|
-
resolve(importName: string, variant: IconVariant): string;
|
|
45
|
+
resolve(importName: string, variant: IconVariant, headless: boolean): string;
|
|
24
46
|
}
|
|
25
47
|
export declare const MODULES: ModuleDescriptor[];
|
|
26
48
|
export declare const SUPPORTED_MODULE_NAMES: string[];
|
|
@@ -45,3 +67,32 @@ export interface VariantResolution {
|
|
|
45
67
|
* policy logic; callers decide how to surface `error`/`warning`.
|
|
46
68
|
*/
|
|
47
69
|
export declare function resolveModuleVariant(descriptor: ModuleDescriptor, iconVariant: IconVariant, fallbackVariant: IconVariant | undefined): VariantResolution;
|
|
70
|
+
/**
|
|
71
|
+
* Refines an already module-resolved `variant` for a single *color* icon import.
|
|
72
|
+
*
|
|
73
|
+
* Color icons are SVG-only (gradients cannot live in an icon font), so the font
|
|
74
|
+
* builds ship no color glyphs. When the module-resolved `variant` has no color
|
|
75
|
+
* atoms, the color import is rerouted using the same
|
|
76
|
+
* `iconVariant → fallbackVariant → svg` precedence as {@link resolveModuleVariant},
|
|
77
|
+
* constrained to variants that are both supported *and* color-capable. `svg` is
|
|
78
|
+
* always both, so a resolution always exists.
|
|
79
|
+
*
|
|
80
|
+
* Callers must only invoke this for color imports (see {@link isColorIconName})
|
|
81
|
+
* and only after module resolution has succeeded.
|
|
82
|
+
*/
|
|
83
|
+
export declare function resolveColorVariant(descriptor: ModuleDescriptor, variant: IconVariant, iconVariant: IconVariant, fallbackVariant: IconVariant | undefined): {
|
|
84
|
+
variant: IconVariant;
|
|
85
|
+
warning?: string;
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Resolves whether to use a module's headless build, given the requested
|
|
89
|
+
* `headless` flag and the already-resolved `variant`.
|
|
90
|
+
*
|
|
91
|
+
* Headless is best-effort: when a module has no headless build for the resolved
|
|
92
|
+
* variant, the loader degrades to the standard (Griffel) implementation and
|
|
93
|
+
* records a warning rather than failing the build.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveModuleHeadless(descriptor: ModuleDescriptor, variant: IconVariant, headless: boolean): {
|
|
96
|
+
headless: boolean;
|
|
97
|
+
warning?: string;
|
|
98
|
+
};
|
package/lib/modules.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveModuleVariant = exports.getModuleDescriptor = exports.SUPPORTED_MODULE_NAMES = exports.MODULES = exports.DEFAULT_SAFETY_VARIANT = void 0;
|
|
3
|
+
exports.resolveModuleHeadless = exports.resolveColorVariant = exports.resolveModuleVariant = exports.getModuleDescriptor = exports.SUPPORTED_MODULE_NAMES = exports.MODULES = exports.isColorIconName = exports.DEFAULT_SAFETY_VARIANT = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* The variant used as the ultimate safety net when neither the requested
|
|
6
6
|
* `iconVariant` nor the configured `fallbackVariant` is supported by a module.
|
|
@@ -11,6 +11,17 @@ const ICON_SUFFIX_REGEX = /(\d*)?(Regular|Filled|Light|Color)$/;
|
|
|
11
11
|
function isIconName(importName) {
|
|
12
12
|
return ICON_SUFFIX_REGEX.test(importName);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Whether an import name is a *color* icon variant, i.e. its style suffix is
|
|
16
|
+
* `Color` (e.g. `AddCircleColor`, `AddCircle20Color`). Every icon export carries
|
|
17
|
+
* exactly one trailing style suffix, so a `Color` suffix unambiguously marks a
|
|
18
|
+
* color variant — icons whose base name merely contains the word "Color" (e.g.
|
|
19
|
+
* `TextColorRegular`) end in a different style suffix.
|
|
20
|
+
*/
|
|
21
|
+
function isColorIconName(importName) {
|
|
22
|
+
return ICON_SUFFIX_REGEX.exec(importName)?.[2] === 'Color';
|
|
23
|
+
}
|
|
24
|
+
exports.isColorIconName = isColorIconName;
|
|
14
25
|
function toKebabCase(value) {
|
|
15
26
|
return value.replace(/[a-z\d](?=[A-Z])|[a-zA-Z](?=\d)|[A-Z](?=[A-Z][a-z])/g, '$&-').toLowerCase();
|
|
16
27
|
}
|
|
@@ -20,24 +31,35 @@ function iconBaseName(importName) {
|
|
|
20
31
|
const reactIcons = {
|
|
21
32
|
name: '@fluentui/react-icons',
|
|
22
33
|
supportedVariants: ['svg', 'fonts', 'svg-sprite'],
|
|
23
|
-
|
|
34
|
+
// Headless ships svg + fonts today; headless svg-sprite is not generated yet.
|
|
35
|
+
headlessVariants: ['svg', 'fonts'],
|
|
36
|
+
// Color icons ship in svg + svg-sprite; the font build has no color glyphs.
|
|
37
|
+
colorVariants: ['svg', 'svg-sprite'],
|
|
38
|
+
resolve(importName, variant, headless) {
|
|
24
39
|
if (importName === 'useIconContext' || importName === 'IconDirectionContextProvider') {
|
|
40
|
+
// Context is framework-agnostic and shared by both APIs.
|
|
25
41
|
return '@fluentui/react-icons/providers';
|
|
26
42
|
}
|
|
43
|
+
const pkg = headless ? '@fluentui/react-icons/headless' : '@fluentui/react-icons';
|
|
27
44
|
if (!isIconName(importName)) {
|
|
28
|
-
return
|
|
45
|
+
return `${pkg}/utils`;
|
|
29
46
|
}
|
|
30
|
-
return
|
|
47
|
+
return `${pkg}/${variant}/${iconBaseName(importName)}`;
|
|
31
48
|
},
|
|
32
49
|
};
|
|
33
50
|
const reactBrandIcons = {
|
|
34
51
|
name: '@fluentui/react-brand-icons',
|
|
35
52
|
supportedVariants: ['svg'],
|
|
36
|
-
|
|
53
|
+
// Brand icons ship a headless (Griffel-free) svg build.
|
|
54
|
+
headlessVariants: ['svg'],
|
|
55
|
+
// Brand icons ship a single svg build, which already includes color icons.
|
|
56
|
+
colorVariants: ['svg'],
|
|
57
|
+
resolve(importName, _variant, headless) {
|
|
58
|
+
const pkg = headless ? '@fluentui/react-brand-icons/headless' : '@fluentui/react-brand-icons';
|
|
37
59
|
if (!isIconName(importName)) {
|
|
38
|
-
return
|
|
60
|
+
return `${pkg}/utils`;
|
|
39
61
|
}
|
|
40
|
-
return
|
|
62
|
+
return `${pkg}/svg/${iconBaseName(importName)}`;
|
|
41
63
|
},
|
|
42
64
|
};
|
|
43
65
|
exports.MODULES = [reactIcons, reactBrandIcons];
|
|
@@ -73,3 +95,59 @@ function resolveModuleVariant(descriptor, iconVariant, fallbackVariant) {
|
|
|
73
95
|
};
|
|
74
96
|
}
|
|
75
97
|
exports.resolveModuleVariant = resolveModuleVariant;
|
|
98
|
+
/**
|
|
99
|
+
* Refines an already module-resolved `variant` for a single *color* icon import.
|
|
100
|
+
*
|
|
101
|
+
* Color icons are SVG-only (gradients cannot live in an icon font), so the font
|
|
102
|
+
* builds ship no color glyphs. When the module-resolved `variant` has no color
|
|
103
|
+
* atoms, the color import is rerouted using the same
|
|
104
|
+
* `iconVariant → fallbackVariant → svg` precedence as {@link resolveModuleVariant},
|
|
105
|
+
* constrained to variants that are both supported *and* color-capable. `svg` is
|
|
106
|
+
* always both, so a resolution always exists.
|
|
107
|
+
*
|
|
108
|
+
* Callers must only invoke this for color imports (see {@link isColorIconName})
|
|
109
|
+
* and only after module resolution has succeeded.
|
|
110
|
+
*/
|
|
111
|
+
function resolveColorVariant(descriptor, variant, iconVariant, fallbackVariant) {
|
|
112
|
+
// Already color-capable — nothing to reroute (e.g. svg, svg-sprite).
|
|
113
|
+
if (descriptor.colorVariants.includes(variant)) {
|
|
114
|
+
return { variant };
|
|
115
|
+
}
|
|
116
|
+
const candidates = [iconVariant, fallbackVariant, exports.DEFAULT_SAFETY_VARIANT].filter((candidate) => candidate !== undefined);
|
|
117
|
+
const colorVariant = candidates.find((candidate) => descriptor.supportedVariants.includes(candidate) && descriptor.colorVariants.includes(candidate)) ?? exports.DEFAULT_SAFETY_VARIANT;
|
|
118
|
+
return {
|
|
119
|
+
variant: colorVariant,
|
|
120
|
+
warning: `"${descriptor.name}" has no color icons for variant "${variant}" ` +
|
|
121
|
+
`(color icons are SVG-only). Resolving Color imports to "${colorVariant}".`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
exports.resolveColorVariant = resolveColorVariant;
|
|
125
|
+
/**
|
|
126
|
+
* Resolves whether to use a module's headless build, given the requested
|
|
127
|
+
* `headless` flag and the already-resolved `variant`.
|
|
128
|
+
*
|
|
129
|
+
* Headless is best-effort: when a module has no headless build for the resolved
|
|
130
|
+
* variant, the loader degrades to the standard (Griffel) implementation and
|
|
131
|
+
* records a warning rather than failing the build.
|
|
132
|
+
*/
|
|
133
|
+
function resolveModuleHeadless(descriptor, variant, headless) {
|
|
134
|
+
if (!headless) {
|
|
135
|
+
return { headless: false };
|
|
136
|
+
}
|
|
137
|
+
if (descriptor.headlessVariants.includes(variant)) {
|
|
138
|
+
return { headless: true };
|
|
139
|
+
}
|
|
140
|
+
if (descriptor.headlessVariants.length === 0) {
|
|
141
|
+
return {
|
|
142
|
+
headless: false,
|
|
143
|
+
warning: `"${descriptor.name}" has no headless build; using its standard (Griffel) ` + `implementation for this import.`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
headless: false,
|
|
148
|
+
warning: `"${descriptor.name}" has no headless build for variant "${variant}" ` +
|
|
149
|
+
`(headless supports: ${descriptor.headlessVariants.join(', ')}); using its standard ` +
|
|
150
|
+
`(Griffel) implementation for this import.`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
exports.resolveModuleHeadless = resolveModuleHeadless;
|
package/lib/transform.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ interface TransformOptions {
|
|
|
5
5
|
iconVariant: IconVariant;
|
|
6
6
|
/** The variant to fall back to when a module does not support `iconVariant`. */
|
|
7
7
|
fallbackVariant?: IconVariant;
|
|
8
|
+
/** Resolve to the headless (Griffel-free) build where the module supports it. */
|
|
9
|
+
headless?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Rewrite a narrow, statically-provable subset of dynamic `import()` barrel
|
|
12
|
+
* calls into atomic dynamic imports (see {@link rewriteDynamicImports}).
|
|
13
|
+
* Defaults to `false`. Un-rewritable dynamic barrel imports still warn.
|
|
14
|
+
*/
|
|
15
|
+
allowDynamicImports?: boolean;
|
|
8
16
|
path: string;
|
|
9
17
|
}
|
|
10
18
|
export interface Diagnostic {
|
package/lib/transform.js
CHANGED
|
@@ -8,49 +8,90 @@ const oxc_parser_1 = require("oxc-parser");
|
|
|
8
8
|
const magic_string_1 = __importDefault(require("magic-string"));
|
|
9
9
|
const modules_1 = require("./modules");
|
|
10
10
|
function transformSource(source, options) {
|
|
11
|
-
const { iconVariant, fallbackVariant, path } = options;
|
|
11
|
+
const { iconVariant, fallbackVariant, headless = false, allowDynamicImports = false, path } = options;
|
|
12
12
|
const result = (0, oxc_parser_1.parseSync)(path, source, {
|
|
13
13
|
sourceType: 'module',
|
|
14
14
|
});
|
|
15
15
|
if (result.errors.length > 0) {
|
|
16
16
|
throw new Error(result.errors[0].message);
|
|
17
17
|
}
|
|
18
|
-
const { staticImports, staticExports } = result.module;
|
|
18
|
+
const { staticImports, staticExports, dynamicImports } = result.module;
|
|
19
19
|
const src = new magic_string_1.default(source);
|
|
20
20
|
const diagnostics = [];
|
|
21
|
-
//
|
|
22
|
-
|
|
21
|
+
// Dedupe diagnostics by message so a module's variant / color / headless
|
|
22
|
+
// concern surfaces at most once, even though resolution now runs per
|
|
23
|
+
// (module, color-ness) rather than per module.
|
|
24
|
+
const seenDiagnostics = new Set();
|
|
25
|
+
const pushDiagnostic = (diagnostic) => {
|
|
26
|
+
const key = `${diagnostic.level}:${diagnostic.message}`;
|
|
27
|
+
if (seenDiagnostics.has(key))
|
|
28
|
+
return;
|
|
29
|
+
seenDiagnostics.add(key);
|
|
30
|
+
diagnostics.push(diagnostic);
|
|
31
|
+
};
|
|
32
|
+
// Resolve each referenced module at most once per color-ness: color icons may
|
|
33
|
+
// route to a different variant than their non-color siblings, so the cache key
|
|
34
|
+
// is `${name}:${isColor}`. Resolution stays O(#modules × 2) regardless of how
|
|
35
|
+
// many icons a file imports.
|
|
36
|
+
const resolvedTargets = new Map();
|
|
23
37
|
/**
|
|
24
|
-
* Returns the variant to rewrite a referenced
|
|
25
|
-
* could not be resolved (an error
|
|
26
|
-
* should be left untouched).
|
|
38
|
+
* Returns the target (variant + headless) to rewrite a single referenced
|
|
39
|
+
* import with, or `null` when the module could not be resolved (an error
|
|
40
|
+
* diagnostic has been recorded and the import should be left untouched).
|
|
27
41
|
*/
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
const targetFor = (descriptor, isColor) => {
|
|
43
|
+
const cacheKey = `${descriptor.name}:${isColor}`;
|
|
44
|
+
if (resolvedTargets.has(cacheKey)) {
|
|
45
|
+
return resolvedTargets.get(cacheKey);
|
|
31
46
|
}
|
|
32
47
|
const resolution = (0, modules_1.resolveModuleVariant)(descriptor, iconVariant, fallbackVariant);
|
|
33
48
|
if (resolution.warning) {
|
|
34
|
-
|
|
49
|
+
pushDiagnostic({ level: 'warning', message: resolution.warning });
|
|
35
50
|
}
|
|
36
51
|
if (resolution.error) {
|
|
37
|
-
|
|
52
|
+
pushDiagnostic({ level: 'error', message: resolution.error });
|
|
53
|
+
}
|
|
54
|
+
if (!resolution.variant) {
|
|
55
|
+
resolvedTargets.set(cacheKey, null);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let variant = resolution.variant;
|
|
59
|
+
// Color icons are SVG-only; reroute them off any color-less variant (fonts)
|
|
60
|
+
// to a color-capable one, honoring the fallback precedence.
|
|
61
|
+
if (isColor) {
|
|
62
|
+
const colorResolution = (0, modules_1.resolveColorVariant)(descriptor, variant, iconVariant, fallbackVariant);
|
|
63
|
+
if (colorResolution.warning) {
|
|
64
|
+
pushDiagnostic({ level: 'warning', message: colorResolution.warning });
|
|
65
|
+
}
|
|
66
|
+
variant = colorResolution.variant;
|
|
67
|
+
}
|
|
68
|
+
const headlessResolution = (0, modules_1.resolveModuleHeadless)(descriptor, variant, headless);
|
|
69
|
+
if (headlessResolution.warning) {
|
|
70
|
+
pushDiagnostic({ level: 'warning', message: headlessResolution.warning });
|
|
38
71
|
}
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
return
|
|
72
|
+
const target = { variant, headless: headlessResolution.headless };
|
|
73
|
+
resolvedTargets.set(cacheKey, target);
|
|
74
|
+
return target;
|
|
42
75
|
};
|
|
43
76
|
for (const imp of staticImports) {
|
|
44
77
|
const moduleName = imp.moduleRequest.value;
|
|
45
78
|
const descriptor = (0, modules_1.getModuleDescriptor)(moduleName);
|
|
46
79
|
if (!descriptor)
|
|
47
80
|
continue;
|
|
48
|
-
const variant = variantFor(descriptor);
|
|
49
|
-
if (!variant)
|
|
50
|
-
continue;
|
|
51
81
|
const namedEntries = imp.entries.filter((e) => e.importName.kind === 'Name');
|
|
52
82
|
if (namedEntries.length === 0)
|
|
53
83
|
continue;
|
|
84
|
+
// Resolve each named specifier independently — color icons may route to a
|
|
85
|
+
// different variant than their non-color siblings in the same statement.
|
|
86
|
+
const resolvedEntries = namedEntries.map((entry) => ({
|
|
87
|
+
entry,
|
|
88
|
+
importedName: entry.importName.name,
|
|
89
|
+
target: targetFor(descriptor, (0, modules_1.isColorIconName)(entry.importName.name)),
|
|
90
|
+
}));
|
|
91
|
+
// A module-level resolution error is independent of color-ness, so if any
|
|
92
|
+
// specifier is unresolved they all are — leave the whole statement untouched.
|
|
93
|
+
if (resolvedEntries.some(({ target }) => !target))
|
|
94
|
+
continue;
|
|
54
95
|
const otherEntries = imp.entries.filter((e) => e.importName.kind !== 'Name');
|
|
55
96
|
const lines = [];
|
|
56
97
|
if (otherEntries.length > 0) {
|
|
@@ -59,10 +100,9 @@ function transformSource(source, options) {
|
|
|
59
100
|
.join(', ');
|
|
60
101
|
lines.push(`import ${names} from '${moduleName}';`);
|
|
61
102
|
}
|
|
62
|
-
for (const entry of
|
|
63
|
-
const importedName = entry.importName.name;
|
|
103
|
+
for (const { entry, importedName, target } of resolvedEntries) {
|
|
64
104
|
const localName = entry.localName.value;
|
|
65
|
-
const newSource = descriptor.resolve(importedName, variant);
|
|
105
|
+
const newSource = descriptor.resolve(importedName, target.variant, target.headless);
|
|
66
106
|
const spec = importedName === localName ? importedName : `${importedName} as ${localName}`;
|
|
67
107
|
lines.push(`import { ${spec} } from '${newSource}';`);
|
|
68
108
|
}
|
|
@@ -81,12 +121,12 @@ function transformSource(source, options) {
|
|
|
81
121
|
for (const entry of relevantEntries) {
|
|
82
122
|
const moduleName = entry.moduleRequest.value;
|
|
83
123
|
const descriptor = (0, modules_1.getModuleDescriptor)(moduleName);
|
|
84
|
-
const variant = variantFor(descriptor);
|
|
85
|
-
if (!variant)
|
|
86
|
-
continue;
|
|
87
124
|
const importedName = entry.importName.name;
|
|
125
|
+
const target = targetFor(descriptor, (0, modules_1.isColorIconName)(importedName));
|
|
126
|
+
if (!target)
|
|
127
|
+
continue;
|
|
88
128
|
const exportedName = entry.exportName.name;
|
|
89
|
-
const newSource = descriptor.resolve(importedName, variant);
|
|
129
|
+
const newSource = descriptor.resolve(importedName, target.variant, target.headless);
|
|
90
130
|
const spec = importedName === exportedName ? importedName : `${importedName} as ${exportedName}`;
|
|
91
131
|
lines.push(`export { ${spec} } from '${newSource}';`);
|
|
92
132
|
}
|
|
@@ -94,6 +134,169 @@ function transformSource(source, options) {
|
|
|
94
134
|
continue;
|
|
95
135
|
src.overwrite(exp.start, exp.end, lines.join('\n'));
|
|
96
136
|
}
|
|
137
|
+
// Source-literal start offsets of dynamic imports that were atomized below.
|
|
138
|
+
// Used to suppress the "cannot be atomized" warning for imports we rewrote.
|
|
139
|
+
const rewrittenImportStarts = new Set();
|
|
140
|
+
if (allowDynamicImports) {
|
|
141
|
+
/**
|
|
142
|
+
* Resolves one destructured binding name to the atomic subpath it should be
|
|
143
|
+
* imported from, honoring the active `iconVariant` / `headless` / color rules
|
|
144
|
+
* (same policy as static imports). Returns `null` when the owning module can't
|
|
145
|
+
* be resolved — `targetFor` has already recorded an error diagnostic — which
|
|
146
|
+
* signals the caller to bail and leave the dynamic import untouched.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* // iconVariant: 'svg'
|
|
150
|
+
* resolveNameSource(reactIcons, 'AddFilled') // → '@fluentui/react-icons/svg/add'
|
|
151
|
+
* resolveNameSource(reactIcons, 'bundleIcon') // → '@fluentui/react-icons/utils'
|
|
152
|
+
* // iconVariant: 'fonts'
|
|
153
|
+
* resolveNameSource(reactIcons, 'AddFilled') // → '@fluentui/react-icons/fonts/add'
|
|
154
|
+
*/
|
|
155
|
+
const resolveNameSource = (descriptor, importedName) => {
|
|
156
|
+
const target = targetFor(descriptor, (0, modules_1.isColorIconName)(importedName));
|
|
157
|
+
if (!target)
|
|
158
|
+
return null;
|
|
159
|
+
return descriptor.resolve(importedName, target.variant, target.headless);
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Groups the properties of a destructuring object pattern by the atomic module
|
|
163
|
+
* each imported name resolves to, preserving first-seen order. Each group's
|
|
164
|
+
* `specs` are the emit-ready specifier strings (`'AddFilled'`, or
|
|
165
|
+
* `'ArrowLeftRegular: arrow'` for a rename).
|
|
166
|
+
*
|
|
167
|
+
* Returns `null` (bail — leave the dynamic import untouched) when any property
|
|
168
|
+
* isn't a plain, statically-known `name → binding` pair: rest elements,
|
|
169
|
+
* computed/string keys, default values, or nested patterns.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* // `{ AddFilled, AddRegular }` — both live in the `add` atom
|
|
173
|
+
* // → [{ source: '@fluentui/react-icons/svg/add', specs: ['AddFilled', 'AddRegular'] }]
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* // `{ AddFilled, ArrowLeftRegular: arrow }` — different atoms, one renamed
|
|
177
|
+
* // → [
|
|
178
|
+
* // { source: '@fluentui/react-icons/svg/add', specs: ['AddFilled'] },
|
|
179
|
+
* // { source: '@fluentui/react-icons/svg/arrow-left', specs: ['ArrowLeftRegular: arrow'] },
|
|
180
|
+
* // ]
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* // `{ AddFilled, ...rest }` → null (rest element → bail)
|
|
184
|
+
*/
|
|
185
|
+
const buildGroups = (objectPattern, descriptor) => {
|
|
186
|
+
const bySource = new Map();
|
|
187
|
+
const order = [];
|
|
188
|
+
for (const prop of objectPattern.properties) {
|
|
189
|
+
if (prop.type !== 'Property' || prop.computed || prop.kind !== 'init')
|
|
190
|
+
return null;
|
|
191
|
+
if (prop.key.type !== 'Identifier' || prop.value.type !== 'Identifier')
|
|
192
|
+
return null;
|
|
193
|
+
const importedName = prop.key.name;
|
|
194
|
+
const localName = prop.value.name;
|
|
195
|
+
const resolvedSource = resolveNameSource(descriptor, importedName);
|
|
196
|
+
if (resolvedSource === null)
|
|
197
|
+
return null;
|
|
198
|
+
const spec = importedName === localName ? importedName : `${importedName}: ${localName}`;
|
|
199
|
+
if (!bySource.has(resolvedSource)) {
|
|
200
|
+
bySource.set(resolvedSource, []);
|
|
201
|
+
order.push(resolvedSource);
|
|
202
|
+
}
|
|
203
|
+
bySource.get(resolvedSource).push(spec);
|
|
204
|
+
}
|
|
205
|
+
if (order.length === 0)
|
|
206
|
+
return null;
|
|
207
|
+
return order.map((groupSource) => ({ source: groupSource, specs: bySource.get(groupSource) }));
|
|
208
|
+
};
|
|
209
|
+
const importCallText = (groups) => groups.length === 1
|
|
210
|
+
? `import('${groups[0].source}')`
|
|
211
|
+
: `Promise.all([${groups.map((g) => `import('${g.source}')`).join(', ')}])`;
|
|
212
|
+
const patternText = (groups) => groups.length === 1
|
|
213
|
+
? `{ ${groups[0].specs.join(', ')} }`
|
|
214
|
+
: `[${groups.map((g) => `{ ${g.specs.join(', ')} }`).join(', ')}]`;
|
|
215
|
+
const visitor = new oxc_parser_1.Visitor({
|
|
216
|
+
// `const { A, B } = await import('barrel')`
|
|
217
|
+
VariableDeclarator(node) {
|
|
218
|
+
if (node.id.type !== 'ObjectPattern' ||
|
|
219
|
+
node.init?.type !== 'AwaitExpression' ||
|
|
220
|
+
node.init.argument.type !== 'ImportExpression') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const importExpr = node.init.argument;
|
|
224
|
+
if (importExpr.source.type !== 'Literal' || typeof importExpr.source.value !== 'string')
|
|
225
|
+
return;
|
|
226
|
+
const descriptor = (0, modules_1.getModuleDescriptor)(importExpr.source.value);
|
|
227
|
+
if (!descriptor)
|
|
228
|
+
return;
|
|
229
|
+
const groups = buildGroups(node.id, descriptor);
|
|
230
|
+
if (!groups)
|
|
231
|
+
return;
|
|
232
|
+
src.overwrite(node.start, node.end, `${patternText(groups)} = await ${importCallText(groups)}`);
|
|
233
|
+
rewrittenImportStarts.add(importExpr.source.start);
|
|
234
|
+
},
|
|
235
|
+
// `import('barrel').then(({ A, B }) => …)`
|
|
236
|
+
CallExpression(node) {
|
|
237
|
+
if (node.callee.type !== 'MemberExpression' ||
|
|
238
|
+
node.callee.computed ||
|
|
239
|
+
node.callee.property.type !== 'Identifier' ||
|
|
240
|
+
node.callee.property.name !== 'then' ||
|
|
241
|
+
node.callee.object.type !== 'ImportExpression') {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const importExpr = node.callee.object;
|
|
245
|
+
if (importExpr.source.type !== 'Literal' || typeof importExpr.source.value !== 'string')
|
|
246
|
+
return;
|
|
247
|
+
const descriptor = (0, modules_1.getModuleDescriptor)(importExpr.source.value);
|
|
248
|
+
if (!descriptor)
|
|
249
|
+
return;
|
|
250
|
+
const callback = node.arguments[0];
|
|
251
|
+
if (!callback || (callback.type !== 'ArrowFunctionExpression' && callback.type !== 'FunctionExpression')) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const param = callback.params[0];
|
|
255
|
+
if (param?.type !== 'ObjectPattern')
|
|
256
|
+
return;
|
|
257
|
+
const groups = buildGroups(param, descriptor);
|
|
258
|
+
if (!groups)
|
|
259
|
+
return;
|
|
260
|
+
src.overwrite(importExpr.start, importExpr.end, importCallText(groups));
|
|
261
|
+
// Multiple atoms resolve to an array, so the callback must destructure by
|
|
262
|
+
// position instead of by name.
|
|
263
|
+
if (groups.length > 1) {
|
|
264
|
+
src.overwrite(param.start, param.end, patternText(groups));
|
|
265
|
+
}
|
|
266
|
+
rewrittenImportStarts.add(importExpr.source.start);
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
visitor.visit(result.program);
|
|
270
|
+
}
|
|
271
|
+
// Dynamic imports of a barrel (`import('@fluentui/react-icons')`) cannot be
|
|
272
|
+
// atomized: the returned namespace object is a runtime value whose usage is
|
|
273
|
+
// not statically known, so the whole icon set ends up in the async chunk. We
|
|
274
|
+
// can't rewrite it safely, but we can warn and point at the atomic escape
|
|
275
|
+
// hatch (`import('@fluentui/react-icons/svg/add')`). Atomic and subpath
|
|
276
|
+
// requests don't match a barrel descriptor, so they never warn.
|
|
277
|
+
for (const dyn of dynamicImports) {
|
|
278
|
+
const request = dyn.moduleRequest;
|
|
279
|
+
if (!request)
|
|
280
|
+
continue;
|
|
281
|
+
// Skip imports we already atomized above (their usage was statically provable).
|
|
282
|
+
if (rewrittenImportStarts.has(request.start))
|
|
283
|
+
continue;
|
|
284
|
+
// Unlike static imports, oxc doesn't resolve a dynamic import's argument to a
|
|
285
|
+
// specifier value — it's an arbitrary expression. Match the raw span against
|
|
286
|
+
// each supported module's quoted spellings; this naturally ignores variables,
|
|
287
|
+
// interpolated templates, and subpath/atomic requests (module names never
|
|
288
|
+
// contain quotes).
|
|
289
|
+
const raw = source.slice(request.start, request.end);
|
|
290
|
+
const moduleName = modules_1.SUPPORTED_MODULE_NAMES.find((name) => raw === `'${name}'` || raw === `"${name}"` || raw === `\`${name}\``);
|
|
291
|
+
if (!moduleName)
|
|
292
|
+
continue;
|
|
293
|
+
pushDiagnostic({
|
|
294
|
+
level: 'warning',
|
|
295
|
+
message: `dynamic import of the "${moduleName}" barrel cannot be atomized, so the entire icon ` +
|
|
296
|
+
`set will be bundled into the async chunk. Import an atomic path directly instead, ` +
|
|
297
|
+
`e.g. import('${moduleName}/svg/add').`,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
97
300
|
return {
|
|
98
301
|
code: src.toString(),
|
|
99
302
|
map: src.generateMap({ hires: true }),
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluentui/react-icons-atomic-webpack-loader",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Webpack loader that transforms barrel imports and re-exports from @fluentui/react-icons into atomic deep paths",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build": "tsc -p .",
|
|
8
|
-
"
|
|
7
|
+
"build": "yarn run -T tsc -p .",
|
|
8
|
+
"lint": "yarn run -T eslint package.json",
|
|
9
|
+
"test": "yarn run -T vitest run && yarn run -T webpack -c test/webpack.config.js"
|
|
9
10
|
},
|
|
10
11
|
"engines": {
|
|
11
12
|
"node": ">=20.0.0"
|
|
@@ -24,11 +25,7 @@
|
|
|
24
25
|
"oxc-parser": "^0.125.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@fluentui/react-icons": "*"
|
|
28
|
-
"ts-loader": "^9.5.0",
|
|
29
|
-
"typescript": "5.0.4",
|
|
30
|
-
"webpack": "^5.72.0",
|
|
31
|
-
"@types/node": "22"
|
|
28
|
+
"@fluentui/react-icons": "*"
|
|
32
29
|
},
|
|
33
30
|
"peerDependencies": {
|
|
34
31
|
"webpack": ">=5.0.0"
|