@trackunit/eslint-plugin-trackunit 0.6.157 → 0.6.159
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/CHANGELOG.md +14 -0
- package/README.md +2 -3
- package/package.json +2 -2
- package/src/lib/config/index.d.ts +3 -0
- package/src/lib/config/plugins.d.ts +3 -0
- package/src/lib/config/plugins.js +3 -1
- package/src/lib/config/presets/react.d.ts +3 -0
- package/src/lib/rules/no-local-meta-component/no-local-meta-component.d.ts +40 -0
- package/src/lib/rules/no-local-meta-component/no-local-meta-component.js +352 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## 0.6.159 (2026-09-30)
|
|
2
|
+
|
|
3
|
+
### 🧱 Updated Dependencies
|
|
4
|
+
|
|
5
|
+
- Updated css-classname-utils to 0.0.60
|
|
6
|
+
- Updated shared-utils to 1.17.4
|
|
7
|
+
|
|
8
|
+
## 0.6.158 (2026-09-30)
|
|
9
|
+
|
|
10
|
+
### 🧱 Updated Dependencies
|
|
11
|
+
|
|
12
|
+
- Updated css-classname-utils to 0.0.59
|
|
13
|
+
- Updated shared-utils to 1.17.3
|
|
14
|
+
|
|
1
15
|
## 0.6.157 (2026-09-29)
|
|
2
16
|
|
|
3
17
|
### 🧱 Updated Dependencies
|
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ project.
|
|
|
98
98
|
| `@trackunit/storybook/require-folder-landing-page` | error | no | Requires a `*.docs.mdx` landing page (`<Meta title="...">`) for every ancestor folder implied by a story's `title` — including titles read from CSF factory `<binding>.meta({...})` calls. Without a landing page, Storybook renders an empty grouping node in the sidebar. |
|
|
99
99
|
| `@trackunit/require-storybook-folder-landing-page` | not enabled by default | no | Deprecated alias of `@trackunit/storybook/require-folder-landing-page` — same rule implementation, kept only so configs still referencing the pre-namespace id keep working. Neither the published `react` preset nor the internal `storybook` preset enables it; a consumer must re-enable it explicitly. |
|
|
100
100
|
| `@trackunit/storybook/require-csf-factory` | warn | no | Detection-only: flags story files in the legacy `export default {...}` shape (`oldFormat`), files mixing that shape with a factory `.meta({...})` call (`mixedFormat`), and files with neither signal (`missingMeta`). Stays silent on files already in the CSF **factory** format (`<binding>.meta({...})` + `<meta>.story({...})`). |
|
|
101
|
+
| `@trackunit/storybook/no-local-meta-component` | error | no | Detection-only: flags a meta `component` (in `export default {...}`, `const meta = {...}; export default meta`, or any `<binding>.meta({...})`) that resolves to a function/class declared in the story file, an inline component, a wrapper call around a local (`memo(Local)`), or an import from another `*.stories` module. Docgen skips `*.stories.*`, so Controls loses JSDoc prop descriptions and "Show code" names a story-only identifier. Fix by pointing at the real component (`component: MultiSelectField<Option>` for generics) or moving the demo into a sibling `*.demo.tsx`. |
|
|
101
102
|
| `storybook/*` (upstream `eslint-plugin-storybook@^10.3.5` `flat/recommended`) | per-rule (upstream defaults), with `storybook/default-exports` forced `off` | per-rule | Upstream Storybook lint rules, spread only by the internal `storybook` preset — never by the published `react` preset. `storybook/default-exports` is turned back off immediately after the spread because it contradicts the CSF factory format's implicit default export via the `meta` object; `require-csf-factory` above already reports a story file with no meta/default export at all, so no detection guarantee is lost by disabling it. |
|
|
102
103
|
|
|
103
104
|
**External consumers opt in themselves.** The published `react` preset registers the
|
|
@@ -113,14 +114,12 @@ module.exports = [
|
|
|
113
114
|
rules: {
|
|
114
115
|
"@trackunit/storybook/require-folder-landing-page": "error",
|
|
115
116
|
"@trackunit/storybook/require-csf-factory": "warn",
|
|
117
|
+
"@trackunit/storybook/no-local-meta-component": "error",
|
|
116
118
|
},
|
|
117
119
|
},
|
|
118
120
|
];
|
|
119
121
|
```
|
|
120
122
|
|
|
121
|
-
> **Planned:** A `no-local-meta-component` rule is expected to join the
|
|
122
|
-
> `@trackunit/storybook/*` namespace once it's written; it does not exist yet.
|
|
123
|
-
|
|
124
123
|
### Localization
|
|
125
124
|
|
|
126
125
|
| Rule | Severity | Auto-fix | Description |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/eslint-plugin-trackunit",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.159",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
5
5
|
"repository": "https://github.com/Trackunit/manager",
|
|
6
6
|
"engines": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@nx/eslint-plugin": "23.1.0",
|
|
11
|
-
"@trackunit/css-classname-utils": "0.0.
|
|
11
|
+
"@trackunit/css-classname-utils": "0.0.60",
|
|
12
12
|
"@typescript-eslint/eslint-plugin": "8.58.1",
|
|
13
13
|
"@typescript-eslint/utils": "8.58.1",
|
|
14
14
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -611,6 +611,9 @@ export declare const configs: {
|
|
|
611
611
|
"require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
612
612
|
name: string;
|
|
613
613
|
};
|
|
614
|
+
"no-local-meta-component": import("@typescript-eslint/utils/ts-eslint").RuleModule<"localMetaComponent", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
615
|
+
name: string;
|
|
616
|
+
};
|
|
614
617
|
};
|
|
615
618
|
};
|
|
616
619
|
"@typescript-eslint": {
|
|
@@ -138,6 +138,9 @@ export declare const storybookPlugin: {
|
|
|
138
138
|
"require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
139
139
|
name: string;
|
|
140
140
|
};
|
|
141
|
+
"no-local-meta-component": import("@typescript-eslint/utils/ts-eslint").RuleModule<"localMetaComponent", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
142
|
+
name: string;
|
|
143
|
+
};
|
|
141
144
|
};
|
|
142
145
|
};
|
|
143
146
|
export { eslintConfigPrettier, globals, importPlugin, jest, jsdoc, jsoncParser, noNull, nx, react, reactHooks, testingLibrary, tsParser, typescriptEslint, };
|
|
@@ -37,9 +37,10 @@ const globals = tslib_1.__importStar(require("globals"));
|
|
|
37
37
|
exports.globals = globals;
|
|
38
38
|
const jsoncParser = tslib_1.__importStar(require("jsonc-eslint-parser"));
|
|
39
39
|
exports.jsoncParser = jsoncParser;
|
|
40
|
+
const rules_map_1 = require("../rules-map");
|
|
41
|
+
const no_local_meta_component_1 = require("../rules/no-local-meta-component/no-local-meta-component");
|
|
40
42
|
const require_csf_factory_1 = require("../rules/require-csf-factory/require-csf-factory");
|
|
41
43
|
const require_storybook_folder_landing_page_1 = require("../rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page");
|
|
42
|
-
const rules_map_1 = require("../rules-map");
|
|
43
44
|
exports.localRulesPlugin = { rules: rules_map_1.rulesMap };
|
|
44
45
|
/**
|
|
45
46
|
* Shared plugin object for the `@trackunit/storybook` namespace. Rule code stays in this
|
|
@@ -54,6 +55,7 @@ exports.storybookPlugin = {
|
|
|
54
55
|
rules: {
|
|
55
56
|
"require-folder-landing-page": require_storybook_folder_landing_page_1.requireFolderLandingPage,
|
|
56
57
|
"require-csf-factory": require_csf_factory_1.requireCsfFactory,
|
|
58
|
+
"no-local-meta-component": no_local_meta_component_1.noLocalMetaComponent,
|
|
57
59
|
},
|
|
58
60
|
};
|
|
59
61
|
//# sourceMappingURL=plugins.js.map
|
|
@@ -110,6 +110,9 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
|
|
|
110
110
|
"require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
111
111
|
name: string;
|
|
112
112
|
};
|
|
113
|
+
"no-local-meta-component": import("@typescript-eslint/utils/ts-eslint").RuleModule<"localMetaComponent", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
114
|
+
name: string;
|
|
115
|
+
};
|
|
113
116
|
};
|
|
114
117
|
};
|
|
115
118
|
"@typescript-eslint": {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error when a Storybook meta's `component` is declared inside a `*.stories.*` file.
|
|
3
|
+
*
|
|
4
|
+
* Architecture: per-story-file rule, syntax-only AST + ESLint scope analysis (no TypeScript type
|
|
5
|
+
* information). The meta object is read from any of:
|
|
6
|
+
* - `export default { component, ... }` (including a `satisfies` / `as` wrapper),
|
|
7
|
+
* - `const meta = { component, ... } satisfies Meta<...>; export default meta;` (or `as`, or a
|
|
8
|
+
* plain type annotation: `const meta: Meta<...> = { component, ... };`),
|
|
9
|
+
* - any `<x>.meta({ component, ... })` member call (covers `preview.meta({...})` and
|
|
10
|
+
* `preview.type<...>().meta({...})`).
|
|
11
|
+
*
|
|
12
|
+
* The `component` value is unwrapped (`satisfies`, `as`, instantiation expressions such as
|
|
13
|
+
* `MultiSelectField<Option>`) and resolved through scope. It is reported when it resolves to:
|
|
14
|
+
* - a function/arrow/class expression written inline or declared in this file,
|
|
15
|
+
* - a call (`memo(Local)`, `forwardRef(Local)`, any call) whose wrapped component resolves locally.
|
|
16
|
+
* The wrapped component is the first traceable argument, skipping literal options and inline
|
|
17
|
+
* functions after the first position — `memo(Widget, areEqual)` wraps `Widget`,
|
|
18
|
+
* `memo(Local, isEqual)` wraps `Local`,
|
|
19
|
+
* - member access on, destructuring from, or a `const` alias of, something that resolves locally
|
|
20
|
+
* (for an object literal, only the accessed or destructured property is checked, following
|
|
21
|
+
* spreads into their source; a computed member or a nested/unresolvable destructuring pattern
|
|
22
|
+
* is untraced rather than checked against every sibling),
|
|
23
|
+
* - an import from another `*.stories` module.
|
|
24
|
+
* Imports from non-story modules (and aliases / member access / instantiations of them) pass.
|
|
25
|
+
*
|
|
26
|
+
* Metas without `component`, non-story files, and locally declared components that are not the
|
|
27
|
+
* meta component (e.g. example-only demos used in `render`) are never checked.
|
|
28
|
+
*
|
|
29
|
+
* This is a **detection-only** rule: no autofix and no suggestions — the fix is either importing
|
|
30
|
+
* the real component or moving the demo into a sibling `*.demo.tsx` file, both of which need
|
|
31
|
+
* developer intent and cross-file edits.
|
|
32
|
+
*
|
|
33
|
+
* See README.md for incorrect/correct examples.
|
|
34
|
+
*/
|
|
35
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
36
|
+
/** Canonical permalink to this rule's README, embedded in the message. */
|
|
37
|
+
export declare const NO_LOCAL_META_COMPONENT_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/no-local-meta-component/README.md";
|
|
38
|
+
export declare const noLocalMetaComponent: ESLintUtils.RuleModule<"localMetaComponent", [], unknown, ESLintUtils.RuleListener> & {
|
|
39
|
+
name: string;
|
|
40
|
+
};
|
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Error when a Storybook meta's `component` is declared inside a `*.stories.*` file.
|
|
4
|
+
*
|
|
5
|
+
* Architecture: per-story-file rule, syntax-only AST + ESLint scope analysis (no TypeScript type
|
|
6
|
+
* information). The meta object is read from any of:
|
|
7
|
+
* - `export default { component, ... }` (including a `satisfies` / `as` wrapper),
|
|
8
|
+
* - `const meta = { component, ... } satisfies Meta<...>; export default meta;` (or `as`, or a
|
|
9
|
+
* plain type annotation: `const meta: Meta<...> = { component, ... };`),
|
|
10
|
+
* - any `<x>.meta({ component, ... })` member call (covers `preview.meta({...})` and
|
|
11
|
+
* `preview.type<...>().meta({...})`).
|
|
12
|
+
*
|
|
13
|
+
* The `component` value is unwrapped (`satisfies`, `as`, instantiation expressions such as
|
|
14
|
+
* `MultiSelectField<Option>`) and resolved through scope. It is reported when it resolves to:
|
|
15
|
+
* - a function/arrow/class expression written inline or declared in this file,
|
|
16
|
+
* - a call (`memo(Local)`, `forwardRef(Local)`, any call) whose wrapped component resolves locally.
|
|
17
|
+
* The wrapped component is the first traceable argument, skipping literal options and inline
|
|
18
|
+
* functions after the first position — `memo(Widget, areEqual)` wraps `Widget`,
|
|
19
|
+
* `memo(Local, isEqual)` wraps `Local`,
|
|
20
|
+
* - member access on, destructuring from, or a `const` alias of, something that resolves locally
|
|
21
|
+
* (for an object literal, only the accessed or destructured property is checked, following
|
|
22
|
+
* spreads into their source; a computed member or a nested/unresolvable destructuring pattern
|
|
23
|
+
* is untraced rather than checked against every sibling),
|
|
24
|
+
* - an import from another `*.stories` module.
|
|
25
|
+
* Imports from non-story modules (and aliases / member access / instantiations of them) pass.
|
|
26
|
+
*
|
|
27
|
+
* Metas without `component`, non-story files, and locally declared components that are not the
|
|
28
|
+
* meta component (e.g. example-only demos used in `render`) are never checked.
|
|
29
|
+
*
|
|
30
|
+
* This is a **detection-only** rule: no autofix and no suggestions — the fix is either importing
|
|
31
|
+
* the real component or moving the demo into a sibling `*.demo.tsx` file, both of which need
|
|
32
|
+
* developer intent and cross-file edits.
|
|
33
|
+
*
|
|
34
|
+
* See README.md for incorrect/correct examples.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.noLocalMetaComponent = exports.NO_LOCAL_META_COMPONENT_DOCS_URL = void 0;
|
|
38
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
39
|
+
const { DefinitionType } = utils_1.TSESLint.Scope;
|
|
40
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
|
|
41
|
+
/** Canonical permalink to this rule's README, embedded in the message. */
|
|
42
|
+
exports.NO_LOCAL_META_COMPONENT_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/no-local-meta-component/README.md";
|
|
43
|
+
const STORY_FILE_REGEX = /\.stories\.(js|jsx|ts|tsx)$/;
|
|
44
|
+
// Import sources are usually extensionless (`./Other.stories`), so the extension is optional.
|
|
45
|
+
const STORY_IMPORT_SOURCE_REGEX = /\.stories(\.(js|jsx|ts|tsx))?$/;
|
|
46
|
+
/** Unwrap `satisfies` / `as` wrappers and instantiation expressions (`MultiSelectField<Option>`). */
|
|
47
|
+
const unwrapExpression = (node) => {
|
|
48
|
+
let current = node;
|
|
49
|
+
while (current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
|
|
50
|
+
current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
|
|
51
|
+
current.type === utils_1.AST_NODE_TYPES.TSInstantiationExpression) {
|
|
52
|
+
current = current.expression;
|
|
53
|
+
}
|
|
54
|
+
return current;
|
|
55
|
+
};
|
|
56
|
+
const getComponentValue = (object) => {
|
|
57
|
+
for (const property of object.properties) {
|
|
58
|
+
if (property.type === utils_1.AST_NODE_TYPES.Property &&
|
|
59
|
+
!property.computed &&
|
|
60
|
+
property.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
61
|
+
property.key.name === "component") {
|
|
62
|
+
return property.value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
const isInlineComponent = (node) => node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
68
|
+
node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
69
|
+
node.type === utils_1.AST_NODE_TYPES.ClassExpression;
|
|
70
|
+
const create = context => {
|
|
71
|
+
if (!STORY_FILE_REGEX.test(context.filename)) {
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `name`'s key and default in `const { key: name = default } = ...`. `null` when `name` isn't a
|
|
76
|
+
* direct property of `pattern` — a nested pattern (`{ a: { b: name } }`) or a computed key
|
|
77
|
+
* (`{ [k]: name }`) — since which property that is can't be read syntactically.
|
|
78
|
+
*/
|
|
79
|
+
const getDestructuredKey = (pattern, name) => {
|
|
80
|
+
for (const property of pattern.properties) {
|
|
81
|
+
if (property.type !== utils_1.AST_NODE_TYPES.Property ||
|
|
82
|
+
property.computed ||
|
|
83
|
+
property.key.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (property.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
|
|
87
|
+
if (property.value.left === name) {
|
|
88
|
+
return { key: property.key.name, default: property.value.right };
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (property.value === name) {
|
|
93
|
+
return { key: property.key.name, default: null };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The object literal `node` evaluates to, following `const` aliases (`alias = widgets`), member
|
|
100
|
+
* access (`ns.widgets`) and destructuring (`const { inner } = ...`); `null` when it can't be traced.
|
|
101
|
+
*
|
|
102
|
+
* `path` holds the nodes currently being resolved, so only a real cycle (`const A = B; const B = A;`)
|
|
103
|
+
* is cut; a node reached again by another route is resolved normally.
|
|
104
|
+
*/
|
|
105
|
+
const resolveObjectLiteral = (node, path) => {
|
|
106
|
+
const expression = unwrapExpression(node);
|
|
107
|
+
if (expression.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
108
|
+
return expression;
|
|
109
|
+
}
|
|
110
|
+
if (path.has(expression)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
path.add(expression);
|
|
114
|
+
try {
|
|
115
|
+
switch (expression.type) {
|
|
116
|
+
case utils_1.AST_NODE_TYPES.MemberExpression: {
|
|
117
|
+
if (expression.computed || expression.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const value = findPropertyValue(expression.object, expression.property.name, path);
|
|
121
|
+
return value ? resolveObjectLiteral(value, path) : null;
|
|
122
|
+
}
|
|
123
|
+
case utils_1.AST_NODE_TYPES.Identifier: {
|
|
124
|
+
const definition = utils_1.ASTUtils.findVariable(context.sourceCode.getScope(expression), expression)?.defs[0];
|
|
125
|
+
if (definition?.type !== DefinitionType.Variable || definition.node.init === null) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
if (definition.node.id.type !== utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
129
|
+
return resolveObjectLiteral(definition.node.init, path);
|
|
130
|
+
}
|
|
131
|
+
const destructured = getDestructuredKey(definition.node.id, definition.name);
|
|
132
|
+
if (destructured === null) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const value = findPropertyValue(definition.node.init, destructured.key, path);
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const resolved = value ?? destructured.default;
|
|
140
|
+
return resolved ? resolveObjectLiteral(resolved, path) : null;
|
|
141
|
+
}
|
|
142
|
+
default:
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
path.delete(expression);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
/**
|
|
151
|
+
* The node that provides `object[key]` when `object` traces to an object literal: the property
|
|
152
|
+
* value, or the source of an untraceable spread that may provide it. `null` when the literal has
|
|
153
|
+
* no such key; `undefined` when `object` isn't a traceable object literal.
|
|
154
|
+
*
|
|
155
|
+
* Properties are read last to first, so a later key overrides an earlier spread; a spread whose
|
|
156
|
+
* source is a literal without the key falls through to earlier properties. A literal already on
|
|
157
|
+
* `path` is a spread cycle (`const a = { ...b }; const b = { ...a };`) and provides nothing.
|
|
158
|
+
*/
|
|
159
|
+
const findPropertyValue = (object, key, path) => {
|
|
160
|
+
const literal = resolveObjectLiteral(object, path);
|
|
161
|
+
if (literal === null) {
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
if (path.has(literal)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
path.add(literal);
|
|
168
|
+
try {
|
|
169
|
+
for (const property of [...literal.properties].reverse()) {
|
|
170
|
+
if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
171
|
+
const spreadValue = findPropertyValue(property.argument, key, path);
|
|
172
|
+
if (spreadValue === undefined) {
|
|
173
|
+
return property.argument;
|
|
174
|
+
}
|
|
175
|
+
if (spreadValue !== null) {
|
|
176
|
+
return spreadValue;
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (!property.computed && property.key.type === utils_1.AST_NODE_TYPES.Identifier && property.key.name === key) {
|
|
181
|
+
return property.value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
path.delete(literal);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
/**
|
|
191
|
+
* Classifies `object[key]`; checks only the accessed property when `object` traces to an object
|
|
192
|
+
* literal, falling back to `defaultNode` (a destructuring default) when the literal lacks `key`.
|
|
193
|
+
*
|
|
194
|
+
* `key === null` means the accessed property can't be read syntactically — a computed member
|
|
195
|
+
* (`bag[k]`) or an unresolvable destructuring pattern. When `object` traces to an object literal,
|
|
196
|
+
* this is untraced (`null`) rather than classifying every sibling, which would flag e.g. an
|
|
197
|
+
* imported `widgets.Widget` merely because `widgets` also has a local `Demo`. When `object` isn't
|
|
198
|
+
* a traceable literal (e.g. a namespace import, `Ns.Widget`), `object` itself is classified.
|
|
199
|
+
*/
|
|
200
|
+
const classifyProperty = (object, key, defaultNode, visited) => {
|
|
201
|
+
if (key === null) {
|
|
202
|
+
return resolveObjectLiteral(object, new Set()) === null ? classify(object, visited) : null;
|
|
203
|
+
}
|
|
204
|
+
const value = findPropertyValue(object, key, new Set());
|
|
205
|
+
if (value === undefined) {
|
|
206
|
+
return classify(object, visited);
|
|
207
|
+
}
|
|
208
|
+
if (value === null) {
|
|
209
|
+
return defaultNode ? classify(defaultNode, visited) : null;
|
|
210
|
+
}
|
|
211
|
+
return classify(value, visited);
|
|
212
|
+
};
|
|
213
|
+
/**
|
|
214
|
+
* Arguments that can't be the wrapped component: literal options and, after the first position,
|
|
215
|
+
* inline callbacks (`memo`'s comparator).
|
|
216
|
+
*/
|
|
217
|
+
const isNonComponentArgument = (argument, index) => {
|
|
218
|
+
const expression = unwrapExpression(argument);
|
|
219
|
+
switch (expression.type) {
|
|
220
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
221
|
+
case utils_1.AST_NODE_TYPES.ArrayExpression:
|
|
222
|
+
case utils_1.AST_NODE_TYPES.Literal:
|
|
223
|
+
case utils_1.AST_NODE_TYPES.TemplateLiteral:
|
|
224
|
+
return true;
|
|
225
|
+
default:
|
|
226
|
+
return index > 0 && isInlineComponent(expression);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const classifyDefinition = (def, visited) => {
|
|
230
|
+
switch (def.type) {
|
|
231
|
+
case DefinitionType.FunctionName:
|
|
232
|
+
case DefinitionType.ClassName:
|
|
233
|
+
return "local";
|
|
234
|
+
case DefinitionType.Variable:
|
|
235
|
+
if (def.node.init === null) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
if (def.node.id.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
239
|
+
const destructured = getDestructuredKey(def.node.id, def.name);
|
|
240
|
+
return classifyProperty(def.node.init, destructured?.key ?? null, destructured?.default ?? null, visited);
|
|
241
|
+
}
|
|
242
|
+
return classify(def.node.init, visited);
|
|
243
|
+
case DefinitionType.ImportBinding:
|
|
244
|
+
if (def.parent.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return STORY_IMPORT_SOURCE_REGEX.test(def.parent.source.value) ? "local" : "external";
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
/**
|
|
253
|
+
* `"local"` when `node` resolves to something declared in a story file, `"external"` when it
|
|
254
|
+
* resolves to an import from a non-story module, `null` when it can't be traced.
|
|
255
|
+
*/
|
|
256
|
+
const classify = (node, visited) => {
|
|
257
|
+
const expression = unwrapExpression(node);
|
|
258
|
+
// Guards alias cycles such as `const A = B; const B = A;`.
|
|
259
|
+
if (visited.has(expression)) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
visited.add(expression);
|
|
263
|
+
if (isInlineComponent(expression)) {
|
|
264
|
+
return "local";
|
|
265
|
+
}
|
|
266
|
+
switch (expression.type) {
|
|
267
|
+
case utils_1.AST_NODE_TYPES.CallExpression: {
|
|
268
|
+
// The wrapped component is the first traceable argument that could be one; later arguments
|
|
269
|
+
// are its callbacks and options, so they can't outvote it (`memo(Local, isEqual)`,
|
|
270
|
+
// `memo(Widget, areEqual)`, `withTheme({ strict: true }, Local)`).
|
|
271
|
+
for (const [index, argument] of expression.arguments.entries()) {
|
|
272
|
+
if (isNonComponentArgument(argument, index)) {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
const origin = classify(argument, visited);
|
|
276
|
+
if (origin !== null) {
|
|
277
|
+
return origin;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
283
|
+
return classifyProperty(expression.object, !expression.computed && expression.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
284
|
+
? expression.property.name
|
|
285
|
+
: null, null, visited);
|
|
286
|
+
case utils_1.AST_NODE_TYPES.ObjectExpression:
|
|
287
|
+
return expression.properties.some(property => property.type === utils_1.AST_NODE_TYPES.Property && classify(property.value, visited) === "local")
|
|
288
|
+
? "local"
|
|
289
|
+
: null;
|
|
290
|
+
case utils_1.AST_NODE_TYPES.Identifier: {
|
|
291
|
+
const variable = utils_1.ASTUtils.findVariable(context.sourceCode.getScope(expression), expression);
|
|
292
|
+
const origins = variable?.defs.map(def => classifyDefinition(def, visited)) ?? [];
|
|
293
|
+
return origins.includes("local") ? "local" : (origins.find(origin => origin !== null) ?? null);
|
|
294
|
+
}
|
|
295
|
+
default:
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const checkMetaObject = (object) => {
|
|
300
|
+
const component = getComponentValue(object);
|
|
301
|
+
if (component && classify(component, new Set()) === "local") {
|
|
302
|
+
context.report({ node: component, messageId: "localMetaComponent" });
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
ExportDefaultDeclaration(node) {
|
|
307
|
+
const declaration = unwrapExpression(node.declaration);
|
|
308
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
309
|
+
checkMetaObject(declaration);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
313
|
+
const variable = utils_1.ASTUtils.findVariable(context.sourceCode.getScope(node), declaration);
|
|
314
|
+
const definition = variable?.defs[0];
|
|
315
|
+
if (definition?.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && definition.node.init) {
|
|
316
|
+
const initializer = unwrapExpression(definition.node.init);
|
|
317
|
+
if (initializer.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
318
|
+
checkMetaObject(initializer);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
CallExpression(node) {
|
|
324
|
+
const { callee } = node;
|
|
325
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
|
|
326
|
+
callee.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
|
|
327
|
+
callee.property.name !== "meta") {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const [firstArgument] = node.arguments;
|
|
331
|
+
if (firstArgument?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
332
|
+
checkMetaObject(firstArgument);
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
exports.noLocalMetaComponent = createRule({
|
|
338
|
+
name: "no-local-meta-component",
|
|
339
|
+
meta: {
|
|
340
|
+
type: "problem",
|
|
341
|
+
docs: {
|
|
342
|
+
description: "Disallow a Storybook meta `component` that is declared inside a story file",
|
|
343
|
+
},
|
|
344
|
+
schema: [],
|
|
345
|
+
messages: {
|
|
346
|
+
localMetaComponent: `[no-local-meta-component] The meta \`component\` is declared in a story file. Storybook's docgen enricher skips \`*.stories.*\` files, so Controls loses the component's JSDoc prop descriptions, and "Show code" names an identifier that only exists inside the story. Either point \`component\` at the real component (specialise a generic with an instantiation expression, e.g. \`component: MultiSelectField<Option>\`), or move the documented demo into a sibling \`*.demo.tsx\` file and import it from there. See ${exports.NO_LOCAL_META_COMPONENT_DOCS_URL}.`,
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
defaultOptions: [],
|
|
350
|
+
create,
|
|
351
|
+
});
|
|
352
|
+
//# sourceMappingURL=no-local-meta-component.js.map
|