@fluentui/eslint-plugin-react-icons 0.0.1

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 ADDED
@@ -0,0 +1,83 @@
1
+ # @fluentui/eslint-plugin-react-icons
2
+
3
+ ESLint plugin for [`@fluentui/react-icons`](https://github.com/microsoft/fluentui-system-icons/tree/main/packages/react-icons).
4
+
5
+ ## Overview
6
+
7
+ This ESLint plugin enforces best practices and coding standards when consuming
8
+ Fluent UI System and Brand Icons. It helps developers:
9
+
10
+ - Maintain consistency across `@fluentui/react-icons` usage
11
+ - Catch common mistakes and anti-patterns early
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # npm
17
+ npm install --save-dev @fluentui/eslint-plugin-react-icons
18
+
19
+ # yarn
20
+ yarn add --dev @fluentui/eslint-plugin-react-icons
21
+
22
+ # pnpm
23
+ pnpm add --save-dev @fluentui/eslint-plugin-react-icons
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### Flat Config (ESLint 9+, recommended)
29
+
30
+ Add the plugin to your `eslint.config.js`:
31
+
32
+ ```js
33
+ const fluentuiReactIcons = require('@fluentui/eslint-plugin-react-icons');
34
+
35
+ module.exports = [
36
+ fluentuiReactIcons.configs.recommended,
37
+ // Your other configs...
38
+ ];
39
+ ```
40
+
41
+ Or configure individual rules manually:
42
+
43
+ ```js
44
+ const fluentuiReactIcons = require('@fluentui/eslint-plugin-react-icons');
45
+
46
+ module.exports = [
47
+ {
48
+ plugins: {
49
+ '@fluentui/react-icons': fluentuiReactIcons,
50
+ },
51
+ rules: {
52
+ '@fluentui/react-icons/prefer-resizable': 'warn',
53
+ },
54
+ },
55
+ ];
56
+ ```
57
+
58
+ ### Legacy Config (ESLint 8 and below)
59
+
60
+ Add the plugin to your `.eslintrc.js`:
61
+
62
+ ```js
63
+ module.exports = {
64
+ plugins: ['@fluentui/react-icons'],
65
+ extends: ['plugin:@fluentui/react-icons/recommended-legacy'],
66
+ };
67
+ ```
68
+
69
+ ## Available Rules
70
+
71
+ 💼 Configurations enabled in.\
72
+ 🔧 Whether the rule has an auto-fixer and/or has suggestions.
73
+
74
+ | Name | Description | 💼 | 🔧 |
75
+ | ---------------------------------------------------- | ----------------------------------------------------------------- | --- | --- |
76
+ | [prefer-resizable](./docs/rules/prefer-resizable.md) | Prefer resizable Fluent icons over fixed-size ("sized") variants. | ✅ | 💡 |
77
+
78
+ 💼 ✅ = enabled in the `recommended` config (as `warn`).\
79
+ 🔧 = fixable , 💡 = has suggestions.
80
+
81
+ ## License
82
+
83
+ This project is licensed under the MIT License.
@@ -0,0 +1,80 @@
1
+ # prefer-resizable
2
+
3
+ Prefer resizable Fluent icons over fixed-size ("sized") variants.
4
+
5
+ ## Rule Details
6
+
7
+ `@fluentui/react-icons` (and `@fluentui/react-brand-icons`) ship each icon in two
8
+ shapes:
9
+
10
+ - **Resizable** — e.g. `SendRegular`, `AccessTimeFilled`. Rendered at `1em`, so
11
+ they scale to any size via `fontSize` / CSS and let a bundler ship a single glyph
12
+ that can be reused at multiple sizes.
13
+ - **Sized** — e.g. `Send24Regular`, `AccessTime24Filled`. Locked to a design pixel
14
+ size; each size is a separate export and its hardcoded geometry ignores
15
+ `fontSize`.
16
+
17
+ This rule flags imports (and namespace usages) of sized icons and offers a
18
+ suggestion to switch to the resizable equivalent.
19
+
20
+ ## How sized icons are detected
21
+
22
+ Detection is data-driven, generated from `@fluentui/react-icons` `metadata.json`:
23
+
24
+ - A name is "sized" when it ends with a real two-digit Fluent size
25
+ (`10, 12, 16, 20, 24, 28, 32, 48`) immediately before its variant suffix
26
+ (`Filled` / `Regular` / `Color` / `Light`).
27
+ - Names whose trailing digits are part of a product name rather than a size
28
+ (e.g. `Battery10Regular`, `Fps120Regular`, `Timer10Filled`) are excluded via a
29
+ generated collision denylist, so they are never mis-flagged.
30
+
31
+ The generated data is verified to classify every icon exactly as `metadata.json` does;
32
+ regenerate it with `yarn generate:data` when the icon set changes.
33
+
34
+ ## Examples
35
+
36
+ ### ❌ Incorrect
37
+
38
+ ```tsx
39
+ import { Send24Regular } from '@fluentui/react-icons';
40
+ import { Send24Filled } from '@fluentui/react-brand-icons';
41
+ import * as Icons from '@fluentui/react-icons';
42
+
43
+ const a = <Send24Regular />;
44
+ const b = <Icons.Send24Regular />;
45
+ ```
46
+
47
+ ### ✅ Correct
48
+
49
+ ```tsx
50
+ import { SendRegular } from '@fluentui/react-icons';
51
+ import * as Icons from '@fluentui/react-icons';
52
+
53
+ const a = <SendRegular fontSize={24} />;
54
+ const b = <Icons.SendRegular />;
55
+
56
+ // Product names that merely end in a size-shaped number are fine:
57
+ import { Battery10Regular, Fps120Regular } from '@fluentui/react-icons';
58
+ ```
59
+
60
+ ## Options
61
+
62
+ ```jsonc
63
+ {
64
+ "@fluentui/react-icons/prefer-resizable": [
65
+ "warn",
66
+ {
67
+ // Module sources treated as Fluent icon packages (matched exactly or as a
68
+ // subpath prefix, e.g. "@fluentui/react-icons/fonts/send").
69
+ "sources": ["@fluentui/react-icons", "@fluentui/react-brand-icons"],
70
+ },
71
+ ],
72
+ }
73
+ ```
74
+
75
+ ## When Not To Use It
76
+
77
+ If you intentionally rely on a specific pixel size's optical tuning and do not want
78
+ to migrate those usages, disable the rule (or the specific lines). Note that
79
+ resizable icons are the 20px artwork made scalable, so swapping a sized icon may
80
+ change its optical rendering.
package/lib/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const meta: {
3
+ name: string;
4
+ version: string;
5
+ };
6
+ export declare const rules: {
7
+ "prefer-resizable": TSESLint.RuleModule<import("./rules/prefer-resizable").MessageIds, import("./rules/prefer-resizable").Options, TSESLint.RuleListener>;
8
+ };
9
+ export declare const configs: Record<string, TSESLint.FlatConfig.Config | TSESLint.ClassicConfig.Config>;
package/lib/index.js ADDED
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configs = exports.rules = exports.meta = void 0;
4
+ const node_module_1 = require("node:module");
5
+ const prefer_resizable_1 = require("./rules/prefer-resizable");
6
+ // Read name/version from package.json. A runtime `require` (relative to the built
7
+ // `lib/` output) is used instead of a static `import ... from '../package.json'`
8
+ // because the latter is outside tsc's `rootDir` and would break the flat `lib/`
9
+ // output layout.
10
+ const { name, version } = (0, node_module_1.createRequire)(__filename)('../package.json');
11
+ exports.meta = {
12
+ name,
13
+ version,
14
+ };
15
+ exports.rules = {
16
+ [prefer_resizable_1.RULE_NAME]: prefer_resizable_1.rule,
17
+ };
18
+ const plugin = { meta: exports.meta, rules: exports.rules };
19
+ exports.configs = {
20
+ // Flat config (ESLint 9+, recommended).
21
+ recommended: {
22
+ name: '@fluentui/react-icons/recommended',
23
+ plugins: {
24
+ '@fluentui/react-icons': plugin,
25
+ },
26
+ rules: {
27
+ [`@fluentui/react-icons/${prefer_resizable_1.RULE_NAME}`]: 'warn',
28
+ },
29
+ },
30
+ // Legacy config (ESLint 8 and below).
31
+ 'recommended-legacy': {
32
+ plugins: ['@fluentui/react-icons'],
33
+ rules: {
34
+ [`@fluentui/react-icons/${prefer_resizable_1.RULE_NAME}`]: 'warn',
35
+ },
36
+ },
37
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Pixel sizes used by "sized" Fluent icon variants (e.g. `AccessTime24Filled`).
3
+ * All Fluent icon sizes are two digits; a sized export name embeds one of these
4
+ * immediately before its variant suffix.
5
+ */
6
+ export declare const SIZED_ICON_SIZES: readonly number[];
7
+ /**
8
+ * "resizable" icon names that syntactically look "sized" because a product name
9
+ * ends in a two-digit number matching a real size (e.g. `Battery10Regular`, where
10
+ * "10" is a battery level, not a size). These must NOT be flagged as sized.
11
+ */
12
+ export declare const RESIZABLE_COLLISIONS: readonly string[];
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ // AUTO-GENERATED by scripts/generate-icon-sizes.mjs — do not edit by hand.
3
+ // Regenerate with: npm run generate:data (inside packages/eslint-plugin-react-icons).
4
+ //
5
+ // Source of truth: @fluentui/react-icons metadata.json (icon "type" field).
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.RESIZABLE_COLLISIONS = exports.SIZED_ICON_SIZES = void 0;
8
+ /**
9
+ * Pixel sizes used by "sized" Fluent icon variants (e.g. `AccessTime24Filled`).
10
+ * All Fluent icon sizes are two digits; a sized export name embeds one of these
11
+ * immediately before its variant suffix.
12
+ */
13
+ exports.SIZED_ICON_SIZES = [10, 12, 16, 20, 24, 28, 32, 48];
14
+ /**
15
+ * "resizable" icon names that syntactically look "sized" because a product name
16
+ * ends in a two-digit number matching a real size (e.g. `Battery10Regular`, where
17
+ * "10" is a battery level, not a size). These must NOT be flagged as sized.
18
+ */
19
+ exports.RESIZABLE_COLLISIONS = [
20
+ 'Battery10Filled',
21
+ 'Battery10Regular',
22
+ 'BatteryCharge10Filled',
23
+ 'BatteryCharge10Regular',
24
+ 'Fps120Filled',
25
+ 'Fps120Regular',
26
+ 'Fps124Filled',
27
+ 'Fps124Regular',
28
+ 'SkipBack10Filled',
29
+ 'SkipBack10Regular',
30
+ 'SkipForward10Filled',
31
+ 'SkipForward10Regular',
32
+ 'Timer10Filled',
33
+ 'Timer10Regular',
34
+ ];
@@ -0,0 +1,7 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ export declare const RULE_NAME = "prefer-resizable";
3
+ export type MessageIds = 'preferResizable' | 'useResizable';
4
+ export type Options = [{
5
+ sources?: string[];
6
+ }];
7
+ export declare const rule: ESLintUtils.RuleModule<MessageIds, Options, ESLintUtils.RuleListener>;
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT license.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.rule = exports.RULE_NAME = void 0;
6
+ const utils_1 = require("@typescript-eslint/utils");
7
+ const sized_icons_1 = require("./sized-icons");
8
+ exports.RULE_NAME = 'prefer-resizable';
9
+ const createRule = utils_1.ESLintUtils.RuleCreator((name) => `https://github.com/microsoft/fluentui-system-icons/blob/main/packages/eslint-plugin-react-icons/docs/rules/${name}.md`);
10
+ const DEFAULT_SOURCES = ['@fluentui/react-icons', '@fluentui/react-brand-icons'];
11
+ exports.rule = createRule({
12
+ name: exports.RULE_NAME,
13
+ meta: {
14
+ type: 'suggestion',
15
+ docs: {
16
+ description: 'Prefer resizable Fluent icons over fixed-size ("sized") variants. Resizable icons scale to any size and reduce bundle size.',
17
+ },
18
+ hasSuggestions: true,
19
+ messages: {
20
+ preferResizable: 'Prefer the resizable "{{resizable}}" icon over the fixed-size "{{sized}}" icon. Resizable icons scale to any size (via the `fontSize`/CSS) and reduce bundle size.',
21
+ useResizable: 'Replace "{{sized}}" with "{{resizable}}".',
22
+ },
23
+ schema: [
24
+ {
25
+ type: 'object',
26
+ properties: {
27
+ sources: {
28
+ type: 'array',
29
+ items: { type: 'string' },
30
+ description: 'Module sources treated as Fluent icon packages.',
31
+ },
32
+ },
33
+ additionalProperties: false,
34
+ },
35
+ ],
36
+ },
37
+ defaultOptions: [{ sources: DEFAULT_SOURCES }],
38
+ create(context, [options]) {
39
+ const sources = options.sources ?? DEFAULT_SOURCES;
40
+ const { sourceCode } = context;
41
+ /** Local identifiers bound to `import * as X` from an icon source. */
42
+ const namespaceLocals = new Set();
43
+ function isIconSource(value) {
44
+ return sources.some((source) => value === source || value.startsWith(`${source}/`));
45
+ }
46
+ function reportNamed(specifier) {
47
+ const importedName = specifier.imported.name;
48
+ const resizable = (0, sized_icons_1.getResizableIconName)(importedName);
49
+ if (resizable === null) {
50
+ return;
51
+ }
52
+ const isAliased = specifier.local.range[0] !== specifier.imported.range[0];
53
+ const fix = (fixer) => {
54
+ if (isAliased) {
55
+ // `import { Send24Regular as Foo }` -> only rename the imported name.
56
+ return [fixer.replaceText(specifier.imported, resizable)];
57
+ }
58
+ // `import { Send24Regular }` -> rename the binding and every usage.
59
+ const [variable] = sourceCode.getDeclaredVariables(specifier);
60
+ const fixes = [fixer.replaceText(specifier.local, resizable)];
61
+ for (const reference of variable?.references ?? []) {
62
+ fixes.push(fixer.replaceText(reference.identifier, resizable));
63
+ }
64
+ return fixes;
65
+ };
66
+ context.report({
67
+ node: specifier.imported,
68
+ messageId: 'preferResizable',
69
+ data: { sized: importedName, resizable },
70
+ suggest: [
71
+ {
72
+ messageId: 'useResizable',
73
+ data: { sized: importedName, resizable },
74
+ fix,
75
+ },
76
+ ],
77
+ });
78
+ }
79
+ function reportNamespaceMember(property) {
80
+ const resizable = (0, sized_icons_1.getResizableIconName)(property.name);
81
+ if (resizable === null) {
82
+ return;
83
+ }
84
+ const fix = (fixer) => fixer.replaceText(property, resizable);
85
+ context.report({
86
+ node: property,
87
+ messageId: 'preferResizable',
88
+ data: { sized: property.name, resizable },
89
+ suggest: [
90
+ {
91
+ messageId: 'useResizable',
92
+ data: { sized: property.name, resizable },
93
+ fix,
94
+ },
95
+ ],
96
+ });
97
+ }
98
+ return {
99
+ ImportDeclaration(node) {
100
+ if (!isIconSource(node.source.value)) {
101
+ return;
102
+ }
103
+ for (const specifier of node.specifiers) {
104
+ if (specifier.type === 'ImportSpecifier') {
105
+ reportNamed(specifier);
106
+ }
107
+ else if (specifier.type === 'ImportNamespaceSpecifier') {
108
+ namespaceLocals.add(specifier.local.name);
109
+ }
110
+ }
111
+ },
112
+ 'JSXMemberExpression[object.type="JSXIdentifier"]'(node) {
113
+ const object = node.object;
114
+ if (namespaceLocals.has(object.name)) {
115
+ reportNamespaceMember(node.property);
116
+ }
117
+ },
118
+ 'MemberExpression[object.type="Identifier"][property.type="Identifier"][computed=false]'(node) {
119
+ const object = node.object;
120
+ if (namespaceLocals.has(object.name)) {
121
+ reportNamespaceMember(node.property);
122
+ }
123
+ },
124
+ };
125
+ },
126
+ });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Matches a "sized" Fluent icon export name: a two-digit pixel size immediately
3
+ * before the variant suffix, e.g. `AccessTime24Filled`. Sizes are sorted
4
+ * descending so any multi-digit size (should one ever be added) is matched first.
5
+ */
6
+ export declare const SIZED_ICON_RE: RegExp;
7
+ /**
8
+ * Whether an icon export name refers to a fixed-size ("sized") icon variant
9
+ * (e.g. `Send24Regular`) rather than a resizable one (e.g. `SendRegular`).
10
+ *
11
+ * Names that look sized but are actually resizable (a product name ending in a
12
+ * size-shaped number, e.g. `Battery10Regular`) are excluded via the generated
13
+ * collision denylist.
14
+ */
15
+ export declare function isSizedIconName(name: string): boolean;
16
+ /**
17
+ * The resizable equivalent of a sized icon name (the size token stripped), or
18
+ * `null` when the name is not a sized icon. E.g. `Send24Regular` -> `SendRegular`,
19
+ * `PresenceDnd10Filled` -> `PresenceDndFilled`.
20
+ */
21
+ export declare function getResizableIconName(name: string): string | null;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ // Copyright (c) Microsoft Corporation.
3
+ // Licensed under the MIT license.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.getResizableIconName = exports.isSizedIconName = exports.SIZED_ICON_RE = void 0;
6
+ const icon_sizes_generated_1 = require("./icon-sizes.generated");
7
+ const VARIANT_SUFFIX = 'Filled|Regular|Color|Light';
8
+ /**
9
+ * Matches a "sized" Fluent icon export name: a two-digit pixel size immediately
10
+ * before the variant suffix, e.g. `AccessTime24Filled`. Sizes are sorted
11
+ * descending so any multi-digit size (should one ever be added) is matched first.
12
+ */
13
+ exports.SIZED_ICON_RE = new RegExp(`(${[...icon_sizes_generated_1.SIZED_ICON_SIZES].sort((a, b) => b - a).join('|')})(${VARIANT_SUFFIX})$`);
14
+ const collisions = new Set(icon_sizes_generated_1.RESIZABLE_COLLISIONS);
15
+ /**
16
+ * Whether an icon export name refers to a fixed-size ("sized") icon variant
17
+ * (e.g. `Send24Regular`) rather than a resizable one (e.g. `SendRegular`).
18
+ *
19
+ * Names that look sized but are actually resizable (a product name ending in a
20
+ * size-shaped number, e.g. `Battery10Regular`) are excluded via the generated
21
+ * collision denylist.
22
+ */
23
+ function isSizedIconName(name) {
24
+ return exports.SIZED_ICON_RE.test(name) && !collisions.has(name);
25
+ }
26
+ exports.isSizedIconName = isSizedIconName;
27
+ /**
28
+ * The resizable equivalent of a sized icon name (the size token stripped), or
29
+ * `null` when the name is not a sized icon. E.g. `Send24Regular` -> `SendRegular`,
30
+ * `PresenceDnd10Filled` -> `PresenceDndFilled`.
31
+ */
32
+ function getResizableIconName(name) {
33
+ if (!isSizedIconName(name)) {
34
+ return null;
35
+ }
36
+ return name.replace(exports.SIZED_ICON_RE, '$2');
37
+ }
38
+ exports.getResizableIconName = getResizableIconName;
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@fluentui/eslint-plugin-react-icons",
3
+ "version": "0.0.1",
4
+ "description": "ESLint plugin for @fluentui/react-icons and @fluentui/react-brand-icons",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "type": "commonjs",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "require": "./lib/index.js",
12
+ "default": "./lib/index.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "scripts": {
17
+ "clean": "git clean -fXd lib/",
18
+ "generate:data": "node scripts/generate-icon-sizes.mjs --metadata ../react-icons/metadata.json --out src/rules/prefer-resizable/icon-sizes.generated.ts",
19
+ "build": "yarn clean && yarn generate:data && yarn run -T tsc -p tsconfig.lib.json",
20
+ "lint": "yarn run -T eslint src package.json",
21
+ "test": "yarn run -T vitest run",
22
+ "type-check": "yarn run -T tsc -p tsconfig.json",
23
+ "type-check:infra": "yarn run -T tsc -p tsconfig.utils.json"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.0.0"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/microsoft/fluentui-system-icons.git",
31
+ "directory": "packages/eslint-plugin-react-icons"
32
+ },
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/microsoft/fluentui-system-icons/issues"
36
+ },
37
+ "dependencies": {
38
+ "@typescript-eslint/utils": "7.18.0"
39
+ },
40
+ "peerDependencies": {
41
+ "eslint": ">=8.0.0"
42
+ },
43
+ "files": [
44
+ "lib/*",
45
+ "docs"
46
+ ]
47
+ }