@trackunit/eslint-plugin-trackunit 0.6.153 → 0.6.155-alpha-965e4725884.0
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 +7 -0
- package/README.md +34 -3
- package/package.json +2 -2
- package/src/index.d.ts +2 -1
- package/src/index.js +5 -1
- package/src/lib/config/fragments/react-rules.d.ts +0 -1
- package/src/lib/config/fragments/react-rules.js +1 -9
- package/src/lib/config/fragments/story-relaxations.d.ts +23 -0
- package/src/lib/config/fragments/story-relaxations.js +36 -0
- package/src/lib/config/index.d.ts +11 -26
- package/src/lib/config/plugins.d.ts +19 -0
- package/src/lib/config/plugins.js +18 -1
- package/src/lib/config/presets/base.js +4 -0
- package/src/lib/config/presets/react.d.ts +11 -26
- package/src/lib/config/presets/react.js +11 -13
- package/src/lib/rules/require-csf-factory/require-csf-factory.d.ts +55 -0
- package/src/lib/rules/require-csf-factory/require-csf-factory.js +185 -0
- package/src/lib/rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page.d.ts +28 -4
- package/src/lib/rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page.js +137 -73
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -86,9 +86,40 @@ All custom rules are enabled automatically through the presets under the `@track
|
|
|
86
86
|
|
|
87
87
|
### Storybook
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
This package owns the `@trackunit/storybook/*` rule implementations and registers the
|
|
90
|
+
`@trackunit/storybook` plugin from the published `react` preset, but the published `react` preset
|
|
91
|
+
does **not enable any rule under that namespace itself**. Inside the `manager` monorepo, a
|
|
92
|
+
separate internal preset (`@trackunit/eslint-plugin-trackunit-internal`'s `storybook` preset,
|
|
93
|
+
consumed via `internalConfigs.storybook`) is what turns these rules on for every story-bearing
|
|
94
|
+
project.
|
|
95
|
+
|
|
96
|
+
| Rule | Severity in `manager` | Auto-fix | Enforces |
|
|
97
|
+
| ----------------------------------------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
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
|
+
| `@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
|
+
| `@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
|
+
| `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
|
+
**External consumers opt in themselves.** The published `react` preset registers the
|
|
104
|
+
`@trackunit/storybook` plugin (so its rule ids resolve without extra config) but enables zero
|
|
105
|
+
rules under that namespace. If you consume `@trackunit/eslint-plugin-trackunit` outside the
|
|
106
|
+
`manager` monorepo and want this coverage, turn the rules on yourself:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
module.exports = [
|
|
110
|
+
...configs.base,
|
|
111
|
+
...configs.react,
|
|
112
|
+
{
|
|
113
|
+
rules: {
|
|
114
|
+
"@trackunit/storybook/require-folder-landing-page": "error",
|
|
115
|
+
"@trackunit/storybook/require-csf-factory": "warn",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
```
|
|
120
|
+
|
|
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.
|
|
92
123
|
|
|
93
124
|
### Localization
|
|
94
125
|
|
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.155-alpha-965e4725884.0",
|
|
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.56-alpha-965e4725884.0",
|
|
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",
|
package/src/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export { findMonorepoRoot } from "./lib/config/utils";
|
|
|
6
6
|
export type { MatchCriteria, ProjectMetadata } from "./lib/utils/nx-utils";
|
|
7
7
|
export { strictJsdocRules } from "./lib/config/fragments/jsdoc-rules";
|
|
8
8
|
export { restrictedImportRules } from "./lib/config/fragments/restricted-imports";
|
|
9
|
-
export {
|
|
9
|
+
export { storyFileGlobs, storyRelaxations } from "./lib/config/fragments/story-relaxations";
|
|
10
|
+
export { globals, jsdoc, localRulesPlugin, nx, reactHooks, storybookPlugin, tsParser } from "./lib/config/plugins";
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.tsParser = exports.reactHooks = exports.nx = exports.localRulesPlugin = exports.jsdoc = exports.globals = exports.restrictedImportRules = exports.strictJsdocRules = exports.findMonorepoRoot = exports.createSkipWhen = exports.configs = void 0;
|
|
3
|
+
exports.tsParser = exports.storybookPlugin = exports.reactHooks = exports.nx = exports.localRulesPlugin = exports.jsdoc = exports.globals = exports.storyRelaxations = exports.storyFileGlobs = exports.restrictedImportRules = exports.strictJsdocRules = exports.findMonorepoRoot = exports.createSkipWhen = exports.configs = void 0;
|
|
4
4
|
const rules_map_1 = require("./lib/rules-map");
|
|
5
5
|
exports.default = rules_map_1.rulesMap;
|
|
6
6
|
var config_1 = require("./lib/config");
|
|
@@ -13,11 +13,15 @@ var jsdoc_rules_1 = require("./lib/config/fragments/jsdoc-rules");
|
|
|
13
13
|
Object.defineProperty(exports, "strictJsdocRules", { enumerable: true, get: function () { return jsdoc_rules_1.strictJsdocRules; } });
|
|
14
14
|
var restricted_imports_1 = require("./lib/config/fragments/restricted-imports");
|
|
15
15
|
Object.defineProperty(exports, "restrictedImportRules", { enumerable: true, get: function () { return restricted_imports_1.restrictedImportRules; } });
|
|
16
|
+
var story_relaxations_1 = require("./lib/config/fragments/story-relaxations");
|
|
17
|
+
Object.defineProperty(exports, "storyFileGlobs", { enumerable: true, get: function () { return story_relaxations_1.storyFileGlobs; } });
|
|
18
|
+
Object.defineProperty(exports, "storyRelaxations", { enumerable: true, get: function () { return story_relaxations_1.storyRelaxations; } });
|
|
16
19
|
var plugins_1 = require("./lib/config/plugins");
|
|
17
20
|
Object.defineProperty(exports, "globals", { enumerable: true, get: function () { return plugins_1.globals; } });
|
|
18
21
|
Object.defineProperty(exports, "jsdoc", { enumerable: true, get: function () { return plugins_1.jsdoc; } });
|
|
19
22
|
Object.defineProperty(exports, "localRulesPlugin", { enumerable: true, get: function () { return plugins_1.localRulesPlugin; } });
|
|
20
23
|
Object.defineProperty(exports, "nx", { enumerable: true, get: function () { return plugins_1.nx; } });
|
|
21
24
|
Object.defineProperty(exports, "reactHooks", { enumerable: true, get: function () { return plugins_1.reactHooks; } });
|
|
25
|
+
Object.defineProperty(exports, "storybookPlugin", { enumerable: true, get: function () { return plugins_1.storybookPlugin; } });
|
|
22
26
|
Object.defineProperty(exports, "tsParser", { enumerable: true, get: function () { return plugins_1.tsParser; } });
|
|
23
27
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,4 +2,3 @@ import type { Linter } from "eslint";
|
|
|
2
2
|
export declare const reactRules: Linter.RulesRecord;
|
|
3
3
|
export declare const reactCustomRules: Linter.RulesRecord;
|
|
4
4
|
export declare const reactTestingLibraryOverrides: Linter.Config;
|
|
5
|
-
export declare const reactStoryOverrides: Linter.Config;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.reactTestingLibraryOverrides = exports.reactCustomRules = exports.reactRules = void 0;
|
|
4
4
|
exports.reactRules = {
|
|
5
5
|
"react-hooks/rules-of-hooks": "error",
|
|
6
6
|
"react-hooks/exhaustive-deps": "error",
|
|
@@ -141,12 +141,4 @@ exports.reactTestingLibraryOverrides = {
|
|
|
141
141
|
"jest/expect-expect": "error",
|
|
142
142
|
},
|
|
143
143
|
};
|
|
144
|
-
exports.reactStoryOverrides = {
|
|
145
|
-
files: ["**/*.stories.ts", "**/*.stories.tsx"],
|
|
146
|
-
rules: {
|
|
147
|
-
"jsdoc/require-jsdoc": "off",
|
|
148
|
-
"no-console": "off",
|
|
149
|
-
"@typescript-eslint/no-empty-function": "off",
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
144
|
//# sourceMappingURL=react-rules.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Linter } from "eslint";
|
|
2
|
+
/**
|
|
3
|
+
* Story-file glob patterns shared by every preset that needs to scope config to Storybook CSF
|
|
4
|
+
* files. Kept as its own export (rather than only living inside `storyRelaxations.files`) so
|
|
5
|
+
* consumers that need the globs for something other than the rule relaxations below (e.g. an
|
|
6
|
+
* `@nx/dependency-checks` `ignoredFiles` entry) can reuse the same list without re-typing it.
|
|
7
|
+
*/
|
|
8
|
+
export declare const storyFileGlobs: ReadonlyArray<string>;
|
|
9
|
+
/**
|
|
10
|
+
* The single source of truth for the rule relaxations every story-bearing preset needs. Story
|
|
11
|
+
* files (CSF `meta`/story objects) legitimately:
|
|
12
|
+
* - skip JSDoc (`jsdoc/require-jsdoc`) — they're fixtures, not public API surface.
|
|
13
|
+
* - use `console.*` in play/interaction functions (`no-console`).
|
|
14
|
+
* - declare empty handlers as story args (`@typescript-eslint/no-empty-function`).
|
|
15
|
+
* - export multiple story objects per file and name them after the story, not the file
|
|
16
|
+
* (`@trackunit/one-component-per-file`, `@trackunit/component-name-matches-filename`).
|
|
17
|
+
* - render components without satisfying the prop-contract shape the rule expects from real
|
|
18
|
+
* components (`@trackunit/require-component-prop-contracts`).
|
|
19
|
+
*
|
|
20
|
+
* Intentionally does NOT touch `@trackunit/storybook/*` or `storybook/*` rules — those are
|
|
21
|
+
* registered and enabled by separate presets, not by this rule-relaxation fragment.
|
|
22
|
+
*/
|
|
23
|
+
export declare const storyRelaxations: Linter.Config;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.storyRelaxations = exports.storyFileGlobs = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Story-file glob patterns shared by every preset that needs to scope config to Storybook CSF
|
|
6
|
+
* files. Kept as its own export (rather than only living inside `storyRelaxations.files`) so
|
|
7
|
+
* consumers that need the globs for something other than the rule relaxations below (e.g. an
|
|
8
|
+
* `@nx/dependency-checks` `ignoredFiles` entry) can reuse the same list without re-typing it.
|
|
9
|
+
*/
|
|
10
|
+
exports.storyFileGlobs = ["**/*.stories.ts", "**/*.stories.tsx"];
|
|
11
|
+
/**
|
|
12
|
+
* The single source of truth for the rule relaxations every story-bearing preset needs. Story
|
|
13
|
+
* files (CSF `meta`/story objects) legitimately:
|
|
14
|
+
* - skip JSDoc (`jsdoc/require-jsdoc`) — they're fixtures, not public API surface.
|
|
15
|
+
* - use `console.*` in play/interaction functions (`no-console`).
|
|
16
|
+
* - declare empty handlers as story args (`@typescript-eslint/no-empty-function`).
|
|
17
|
+
* - export multiple story objects per file and name them after the story, not the file
|
|
18
|
+
* (`@trackunit/one-component-per-file`, `@trackunit/component-name-matches-filename`).
|
|
19
|
+
* - render components without satisfying the prop-contract shape the rule expects from real
|
|
20
|
+
* components (`@trackunit/require-component-prop-contracts`).
|
|
21
|
+
*
|
|
22
|
+
* Intentionally does NOT touch `@trackunit/storybook/*` or `storybook/*` rules — those are
|
|
23
|
+
* registered and enabled by separate presets, not by this rule-relaxation fragment.
|
|
24
|
+
*/
|
|
25
|
+
exports.storyRelaxations = {
|
|
26
|
+
files: [...exports.storyFileGlobs],
|
|
27
|
+
rules: {
|
|
28
|
+
"jsdoc/require-jsdoc": "off",
|
|
29
|
+
"no-console": "off",
|
|
30
|
+
"@typescript-eslint/no-empty-function": "off",
|
|
31
|
+
"@trackunit/one-component-per-file": "off",
|
|
32
|
+
"@trackunit/component-name-matches-filename": "off",
|
|
33
|
+
"@trackunit/require-component-prop-contracts": "off",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=story-relaxations.js.map
|
|
@@ -603,6 +603,16 @@ export declare const configs: {
|
|
|
603
603
|
};
|
|
604
604
|
};
|
|
605
605
|
};
|
|
606
|
+
"@trackunit/storybook": {
|
|
607
|
+
rules: {
|
|
608
|
+
"require-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
609
|
+
name: string;
|
|
610
|
+
};
|
|
611
|
+
"require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
612
|
+
name: string;
|
|
613
|
+
};
|
|
614
|
+
};
|
|
615
|
+
};
|
|
606
616
|
"@typescript-eslint": {
|
|
607
617
|
configs: Record<string, import("@typescript-eslint/utils/ts-eslint").ClassicConfig.Config>;
|
|
608
618
|
meta: import("typescript-eslint").FlatConfig.PluginMeta;
|
|
@@ -1038,26 +1048,8 @@ export declare const configs: {
|
|
|
1038
1048
|
})[];
|
|
1039
1049
|
"no-unreachable": string;
|
|
1040
1050
|
"prefer-spread": string;
|
|
1041
|
-
"jsdoc/require-jsdoc"?: undefined;
|
|
1042
|
-
"no-console"?: undefined;
|
|
1043
|
-
"@typescript-eslint/no-empty-function"?: undefined;
|
|
1044
1051
|
"@trackunit/one-component-per-file"?: undefined;
|
|
1045
1052
|
"@trackunit/component-name-matches-filename"?: undefined;
|
|
1046
|
-
"@trackunit/require-storybook-folder-landing-page"?: undefined;
|
|
1047
|
-
};
|
|
1048
|
-
plugins?: undefined;
|
|
1049
|
-
settings?: undefined;
|
|
1050
|
-
languageOptions?: undefined;
|
|
1051
|
-
} | {
|
|
1052
|
-
files: string[];
|
|
1053
|
-
rules: {
|
|
1054
|
-
"jsdoc/require-jsdoc": string;
|
|
1055
|
-
"no-console": string;
|
|
1056
|
-
"@typescript-eslint/no-empty-function": string;
|
|
1057
|
-
"@trackunit/one-component-per-file": string;
|
|
1058
|
-
"@trackunit/component-name-matches-filename": string;
|
|
1059
|
-
"@trackunit/require-storybook-folder-landing-page": string;
|
|
1060
|
-
"no-restricted-properties"?: undefined;
|
|
1061
1053
|
};
|
|
1062
1054
|
plugins?: undefined;
|
|
1063
1055
|
settings?: undefined;
|
|
@@ -1068,10 +1060,6 @@ export declare const configs: {
|
|
|
1068
1060
|
"@trackunit/one-component-per-file": string;
|
|
1069
1061
|
"@trackunit/component-name-matches-filename": string;
|
|
1070
1062
|
"no-restricted-properties": string;
|
|
1071
|
-
"jsdoc/require-jsdoc"?: undefined;
|
|
1072
|
-
"no-console"?: undefined;
|
|
1073
|
-
"@typescript-eslint/no-empty-function"?: undefined;
|
|
1074
|
-
"@trackunit/require-storybook-folder-landing-page"?: undefined;
|
|
1075
1063
|
};
|
|
1076
1064
|
plugins?: undefined;
|
|
1077
1065
|
settings?: undefined;
|
|
@@ -1125,6 +1113,7 @@ export declare const configs: {
|
|
|
1125
1113
|
};
|
|
1126
1114
|
};
|
|
1127
1115
|
"@trackunit"?: undefined;
|
|
1116
|
+
"@trackunit/storybook"?: undefined;
|
|
1128
1117
|
"@typescript-eslint"?: undefined;
|
|
1129
1118
|
jsdoc?: undefined;
|
|
1130
1119
|
react?: undefined;
|
|
@@ -1144,12 +1133,8 @@ export declare const configs: {
|
|
|
1144
1133
|
rules: {
|
|
1145
1134
|
"jest/no-commented-out-tests": string;
|
|
1146
1135
|
"jest/expect-expect": string;
|
|
1147
|
-
"jsdoc/require-jsdoc"?: undefined;
|
|
1148
|
-
"no-console"?: undefined;
|
|
1149
|
-
"@typescript-eslint/no-empty-function"?: undefined;
|
|
1150
1136
|
"@trackunit/one-component-per-file"?: undefined;
|
|
1151
1137
|
"@trackunit/component-name-matches-filename"?: undefined;
|
|
1152
|
-
"@trackunit/require-storybook-folder-landing-page"?: undefined;
|
|
1153
1138
|
"no-restricted-properties"?: undefined;
|
|
1154
1139
|
};
|
|
1155
1140
|
settings?: undefined;
|
|
@@ -121,4 +121,23 @@ export declare const localRulesPlugin: {
|
|
|
121
121
|
};
|
|
122
122
|
};
|
|
123
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Shared plugin object for the `@trackunit/storybook` namespace. Rule code stays in this
|
|
126
|
+
* package; this object is what presets (and external consumers who opt in directly) register
|
|
127
|
+
* under the `@trackunit/storybook` plugin key so ESLint resolves e.g.
|
|
128
|
+
* `@trackunit/storybook/require-folder-landing-page` to plugin `@trackunit/storybook` + rule
|
|
129
|
+
* `require-folder-landing-page`. Must stay a single object reference — flat config compares
|
|
130
|
+
* plugin references by identity — so import this export everywhere the namespace is registered,
|
|
131
|
+
* never construct a new `{ rules: {...} }` object elsewhere.
|
|
132
|
+
*/
|
|
133
|
+
export declare const storybookPlugin: {
|
|
134
|
+
rules: {
|
|
135
|
+
"require-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
136
|
+
name: string;
|
|
137
|
+
};
|
|
138
|
+
"require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
139
|
+
name: string;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
};
|
|
124
143
|
export { eslintConfigPrettier, globals, importPlugin, jest, jsdoc, jsoncParser, noNull, nx, react, reactHooks, testingLibrary, tsParser, typescriptEslint, };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.typescriptEslint = exports.tsParser = exports.testingLibrary = exports.reactHooks = exports.react = exports.nx = exports.noNull = exports.jsoncParser = exports.jsdoc = exports.jest = exports.importPlugin = exports.globals = exports.eslintConfigPrettier = exports.localRulesPlugin = void 0;
|
|
3
|
+
exports.typescriptEslint = exports.tsParser = exports.testingLibrary = exports.reactHooks = exports.react = exports.nx = exports.noNull = exports.jsoncParser = exports.jsdoc = exports.jest = exports.importPlugin = exports.globals = exports.eslintConfigPrettier = exports.storybookPlugin = exports.localRulesPlugin = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
/**
|
|
6
6
|
* Shared plugin instances for ESLint flat config.
|
|
@@ -37,6 +37,23 @@ 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 require_csf_factory_1 = require("../rules/require-csf-factory/require-csf-factory");
|
|
41
|
+
const require_storybook_folder_landing_page_1 = require("../rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page");
|
|
40
42
|
const rules_map_1 = require("../rules-map");
|
|
41
43
|
exports.localRulesPlugin = { rules: rules_map_1.rulesMap };
|
|
44
|
+
/**
|
|
45
|
+
* Shared plugin object for the `@trackunit/storybook` namespace. Rule code stays in this
|
|
46
|
+
* package; this object is what presets (and external consumers who opt in directly) register
|
|
47
|
+
* under the `@trackunit/storybook` plugin key so ESLint resolves e.g.
|
|
48
|
+
* `@trackunit/storybook/require-folder-landing-page` to plugin `@trackunit/storybook` + rule
|
|
49
|
+
* `require-folder-landing-page`. Must stay a single object reference — flat config compares
|
|
50
|
+
* plugin references by identity — so import this export everywhere the namespace is registered,
|
|
51
|
+
* never construct a new `{ rules: {...} }` object elsewhere.
|
|
52
|
+
*/
|
|
53
|
+
exports.storybookPlugin = {
|
|
54
|
+
rules: {
|
|
55
|
+
"require-folder-landing-page": require_storybook_folder_landing_page_1.requireFolderLandingPage,
|
|
56
|
+
"require-csf-factory": require_csf_factory_1.requireCsfFactory,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
42
59
|
//# sourceMappingURL=plugins.js.map
|
|
@@ -9,6 +9,7 @@ const jsdoc_rules_1 = require("../fragments/jsdoc-rules");
|
|
|
9
9
|
const module_boundaries_1 = require("../fragments/module-boundaries");
|
|
10
10
|
const no_direct_storage_access_1 = require("../fragments/no-direct-storage-access");
|
|
11
11
|
const restricted_imports_1 = require("../fragments/restricted-imports");
|
|
12
|
+
const story_relaxations_1 = require("../fragments/story-relaxations");
|
|
12
13
|
const typescript_rules_1 = require("../fragments/typescript-rules");
|
|
13
14
|
const plugins_1 = require("../plugins");
|
|
14
15
|
const utils_1 = require("../utils");
|
|
@@ -187,6 +188,9 @@ exports.base = [
|
|
|
187
188
|
},
|
|
188
189
|
},
|
|
189
190
|
plugins_1.eslintConfigPrettier,
|
|
191
|
+
// Shared with the `react` preset and the internal `publicApi` preset. Consumers that only
|
|
192
|
+
// spread `base` (without `react`) still get the six rule relaxations for story files.
|
|
193
|
+
story_relaxations_1.storyRelaxations,
|
|
190
194
|
{
|
|
191
195
|
files: ["**/*.stories.{ts,tsx,js,jsx}", "**/*.demo.{ts,tsx,js,jsx}"],
|
|
192
196
|
rules: {
|
|
@@ -102,6 +102,16 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
|
|
|
102
102
|
};
|
|
103
103
|
};
|
|
104
104
|
};
|
|
105
|
+
"@trackunit/storybook": {
|
|
106
|
+
rules: {
|
|
107
|
+
"require-folder-landing-page": import("@typescript-eslint/utils/ts-eslint").RuleModule<"missingLandingPage", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
108
|
+
name: string;
|
|
109
|
+
};
|
|
110
|
+
"require-csf-factory": import("@typescript-eslint/utils/ts-eslint").RuleModule<"oldFormat" | "mixedFormat" | "missingMeta", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
111
|
+
name: string;
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
};
|
|
105
115
|
"@typescript-eslint": {
|
|
106
116
|
configs: Record<string, import("@typescript-eslint/utils/ts-eslint").ClassicConfig.Config>;
|
|
107
117
|
meta: import("typescript-eslint").FlatConfig.PluginMeta;
|
|
@@ -537,26 +547,8 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
|
|
|
537
547
|
})[];
|
|
538
548
|
"no-unreachable": string;
|
|
539
549
|
"prefer-spread": string;
|
|
540
|
-
"jsdoc/require-jsdoc"?: undefined;
|
|
541
|
-
"no-console"?: undefined;
|
|
542
|
-
"@typescript-eslint/no-empty-function"?: undefined;
|
|
543
550
|
"@trackunit/one-component-per-file"?: undefined;
|
|
544
551
|
"@trackunit/component-name-matches-filename"?: undefined;
|
|
545
|
-
"@trackunit/require-storybook-folder-landing-page"?: undefined;
|
|
546
|
-
};
|
|
547
|
-
plugins?: undefined;
|
|
548
|
-
settings?: undefined;
|
|
549
|
-
languageOptions?: undefined;
|
|
550
|
-
} | {
|
|
551
|
-
files: string[];
|
|
552
|
-
rules: {
|
|
553
|
-
"jsdoc/require-jsdoc": string;
|
|
554
|
-
"no-console": string;
|
|
555
|
-
"@typescript-eslint/no-empty-function": string;
|
|
556
|
-
"@trackunit/one-component-per-file": string;
|
|
557
|
-
"@trackunit/component-name-matches-filename": string;
|
|
558
|
-
"@trackunit/require-storybook-folder-landing-page": string;
|
|
559
|
-
"no-restricted-properties"?: undefined;
|
|
560
552
|
};
|
|
561
553
|
plugins?: undefined;
|
|
562
554
|
settings?: undefined;
|
|
@@ -567,10 +559,6 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
|
|
|
567
559
|
"@trackunit/one-component-per-file": string;
|
|
568
560
|
"@trackunit/component-name-matches-filename": string;
|
|
569
561
|
"no-restricted-properties": string;
|
|
570
|
-
"jsdoc/require-jsdoc"?: undefined;
|
|
571
|
-
"no-console"?: undefined;
|
|
572
|
-
"@typescript-eslint/no-empty-function"?: undefined;
|
|
573
|
-
"@trackunit/require-storybook-folder-landing-page"?: undefined;
|
|
574
562
|
};
|
|
575
563
|
plugins?: undefined;
|
|
576
564
|
settings?: undefined;
|
|
@@ -624,6 +612,7 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
|
|
|
624
612
|
};
|
|
625
613
|
};
|
|
626
614
|
"@trackunit"?: undefined;
|
|
615
|
+
"@trackunit/storybook"?: undefined;
|
|
627
616
|
"@typescript-eslint"?: undefined;
|
|
628
617
|
jsdoc?: undefined;
|
|
629
618
|
react?: undefined;
|
|
@@ -643,12 +632,8 @@ export declare const reactPreset: (import("eslint").Linter.Config<import("eslint
|
|
|
643
632
|
rules: {
|
|
644
633
|
"jest/no-commented-out-tests": string;
|
|
645
634
|
"jest/expect-expect": string;
|
|
646
|
-
"jsdoc/require-jsdoc"?: undefined;
|
|
647
|
-
"no-console"?: undefined;
|
|
648
|
-
"@typescript-eslint/no-empty-function"?: undefined;
|
|
649
635
|
"@trackunit/one-component-per-file"?: undefined;
|
|
650
636
|
"@trackunit/component-name-matches-filename"?: undefined;
|
|
651
|
-
"@trackunit/require-storybook-folder-landing-page"?: undefined;
|
|
652
637
|
"no-restricted-properties"?: undefined;
|
|
653
638
|
};
|
|
654
639
|
settings?: undefined;
|
|
@@ -3,12 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.reactPreset = void 0;
|
|
4
4
|
const no_direct_storage_access_1 = require("../fragments/no-direct-storage-access");
|
|
5
5
|
const react_rules_1 = require("../fragments/react-rules");
|
|
6
|
+
const story_relaxations_1 = require("../fragments/story-relaxations");
|
|
6
7
|
const testing_library_1 = require("../fragments/testing-library");
|
|
7
8
|
const plugins_1 = require("../plugins");
|
|
8
9
|
exports.reactPreset = [
|
|
9
10
|
{
|
|
10
11
|
plugins: {
|
|
11
12
|
"@trackunit": plugins_1.localRulesPlugin,
|
|
13
|
+
// Registers the `@trackunit/storybook` namespace so opt-in consumers can reference
|
|
14
|
+
// `@trackunit/storybook/require-folder-landing-page` without extra config. This preset
|
|
15
|
+
// intentionally does not enable any rule under that key itself.
|
|
16
|
+
"@trackunit/storybook": plugins_1.storybookPlugin,
|
|
12
17
|
"@typescript-eslint": plugins_1.typescriptEslint,
|
|
13
18
|
jsdoc: plugins_1.jsdoc,
|
|
14
19
|
react: plugins_1.react,
|
|
@@ -63,19 +68,12 @@ exports.reactPreset = [
|
|
|
63
68
|
},
|
|
64
69
|
},
|
|
65
70
|
no_direct_storage_access_1.noDirectStoragePropertyExceptionOverrides,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"@trackunit/one-component-per-file": "off",
|
|
73
|
-
"@trackunit/component-name-matches-filename": "off",
|
|
74
|
-
// Every Storybook folder implied by a story title must have a landing page
|
|
75
|
-
// (*.docs.mdx with a matching <Meta title>). See docs/STORYBOOK-LANDING-PAGES.md.
|
|
76
|
-
"@trackunit/require-storybook-folder-landing-page": "error",
|
|
77
|
-
},
|
|
78
|
-
},
|
|
71
|
+
// The published preset registers `@trackunit/storybook` (above) for opt-in use but does NOT
|
|
72
|
+
// enable `@trackunit/storybook/require-folder-landing-page` (nor its deprecated
|
|
73
|
+
// `@trackunit/require-storybook-folder-landing-page` alias) here. External consumers who want
|
|
74
|
+
// the landing-page check opt in explicitly. The rule relaxations below are unrelated to that
|
|
75
|
+
// namespace and shared with `base` and the internal `publicApi` preset.
|
|
76
|
+
story_relaxations_1.storyRelaxations,
|
|
79
77
|
{
|
|
80
78
|
files: ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx"],
|
|
81
79
|
rules: {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Warn when a `*.stories.*` file is written in the legacy CSF (Component Story Format)
|
|
3
|
+
* default-export shape instead of Storybook's CSF **factory** format.
|
|
4
|
+
*
|
|
5
|
+
* Architecture: per-story-file rule, syntax-only (no TypeScript type information — see
|
|
6
|
+
* `isFactoryMetaCall`'s doc comment for why type-checking `component:` is unnecessary).
|
|
7
|
+
*
|
|
8
|
+
* Recognized shapes, per file:
|
|
9
|
+
* - **Old format** — any `export default ...` in the file (the legacy `export default { title,
|
|
10
|
+
* component, ... }` meta object, whether inline or via a `const meta = {...}; export default
|
|
11
|
+
* meta;` variable), and no factory `.meta({...})` call anywhere in the file. Reported with
|
|
12
|
+
* `oldFormat`, pointing at the `export default` node.
|
|
13
|
+
* - **Factory format** (silent) — a call `<importedBinding>.meta({...})` (e.g.
|
|
14
|
+
* `const meta = preview.meta({ component, title });`), where `<importedBinding>` resolves to
|
|
15
|
+
* an imported identifier, and NEITHER an `export default` NOR an old-style named
|
|
16
|
+
* object-literal story export (`export const Foo = { args: {...} };`, including a `satisfies` or
|
|
17
|
+
* `as` wrapper around that object) is present. Individual
|
|
18
|
+
* stories are expected to be exported as `<meta>.story({...})`, but this rule does not require
|
|
19
|
+
* at least one such export to exist — it only needs the absence of old-format signals.
|
|
20
|
+
* A `component:` property that is a generic/instantiation expression (e.g.
|
|
21
|
+
* `MultiSelectField<Option>`) still matches this shape: the rule only inspects the *call*
|
|
22
|
+
* shape, never the properties inside the `.meta({...})` object, so it never needs to
|
|
23
|
+
* type-check `component`.
|
|
24
|
+
* - **Mixed format** — a factory `.meta({...})` call together with an
|
|
25
|
+
* `export default` OR an old-style object-literal named story export. Reported with
|
|
26
|
+
* `mixedFormat`, pointing at the `.meta({...})` call.
|
|
27
|
+
* - **Missing meta** — neither an `export default` nor a factory
|
|
28
|
+
* `.meta({...})` call anywhere in the file. Reported with `missingMeta`, pointing at the
|
|
29
|
+
* file's `Program` node (there is no more specific node to point at — the file declares no
|
|
30
|
+
* meta object at all).
|
|
31
|
+
*
|
|
32
|
+
* Never visits `.storybook/` config files, `*.demo.*` files, or `*.mdx` files, even if their
|
|
33
|
+
* name would otherwise match the story-file pattern.
|
|
34
|
+
*
|
|
35
|
+
* This is a **detection-only** rule: no autofix (`meta.fixable` unset) and no suggestions
|
|
36
|
+
* (`meta.hasSuggestions` unset) — migrating a story file to the factory format requires
|
|
37
|
+
* rewriting the whole file's shape (imports, meta declaration, every story export), which is far
|
|
38
|
+
* beyond what a syntax-preserving ESLint fixer can safely produce. Storybook ships an official
|
|
39
|
+
* codemod for this (`npx storybook@latest automigrate csf-factories`) — see
|
|
40
|
+
* `REQUIRE_CSF_FACTORY_DOCS_URL`.
|
|
41
|
+
*
|
|
42
|
+
* See README.md for incorrect/correct examples.
|
|
43
|
+
*/
|
|
44
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
45
|
+
type MessageIds = "oldFormat" | "mixedFormat" | "missingMeta";
|
|
46
|
+
/**
|
|
47
|
+
* Canonical permalink to the how-to-migrate doc. Single source of truth, embedded in every
|
|
48
|
+
* message so developers tripping the rule get a direct link. Mirrors the
|
|
49
|
+
* `STORYBOOK_LANDING_PAGE_DOCS_URL` idiom in `../require-storybook-folder-landing-page/require-storybook-folder-landing-page.ts`.
|
|
50
|
+
*/
|
|
51
|
+
export declare const REQUIRE_CSF_FACTORY_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-CSF-FACTORY.md";
|
|
52
|
+
export declare const requireCsfFactory: ESLintUtils.RuleModule<MessageIds, [], unknown, ESLintUtils.RuleListener> & {
|
|
53
|
+
name: string;
|
|
54
|
+
};
|
|
55
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Warn when a `*.stories.*` file is written in the legacy CSF (Component Story Format)
|
|
4
|
+
* default-export shape instead of Storybook's CSF **factory** format.
|
|
5
|
+
*
|
|
6
|
+
* Architecture: per-story-file rule, syntax-only (no TypeScript type information — see
|
|
7
|
+
* `isFactoryMetaCall`'s doc comment for why type-checking `component:` is unnecessary).
|
|
8
|
+
*
|
|
9
|
+
* Recognized shapes, per file:
|
|
10
|
+
* - **Old format** — any `export default ...` in the file (the legacy `export default { title,
|
|
11
|
+
* component, ... }` meta object, whether inline or via a `const meta = {...}; export default
|
|
12
|
+
* meta;` variable), and no factory `.meta({...})` call anywhere in the file. Reported with
|
|
13
|
+
* `oldFormat`, pointing at the `export default` node.
|
|
14
|
+
* - **Factory format** (silent) — a call `<importedBinding>.meta({...})` (e.g.
|
|
15
|
+
* `const meta = preview.meta({ component, title });`), where `<importedBinding>` resolves to
|
|
16
|
+
* an imported identifier, and NEITHER an `export default` NOR an old-style named
|
|
17
|
+
* object-literal story export (`export const Foo = { args: {...} };`, including a `satisfies` or
|
|
18
|
+
* `as` wrapper around that object) is present. Individual
|
|
19
|
+
* stories are expected to be exported as `<meta>.story({...})`, but this rule does not require
|
|
20
|
+
* at least one such export to exist — it only needs the absence of old-format signals.
|
|
21
|
+
* A `component:` property that is a generic/instantiation expression (e.g.
|
|
22
|
+
* `MultiSelectField<Option>`) still matches this shape: the rule only inspects the *call*
|
|
23
|
+
* shape, never the properties inside the `.meta({...})` object, so it never needs to
|
|
24
|
+
* type-check `component`.
|
|
25
|
+
* - **Mixed format** — a factory `.meta({...})` call together with an
|
|
26
|
+
* `export default` OR an old-style object-literal named story export. Reported with
|
|
27
|
+
* `mixedFormat`, pointing at the `.meta({...})` call.
|
|
28
|
+
* - **Missing meta** — neither an `export default` nor a factory
|
|
29
|
+
* `.meta({...})` call anywhere in the file. Reported with `missingMeta`, pointing at the
|
|
30
|
+
* file's `Program` node (there is no more specific node to point at — the file declares no
|
|
31
|
+
* meta object at all).
|
|
32
|
+
*
|
|
33
|
+
* Never visits `.storybook/` config files, `*.demo.*` files, or `*.mdx` files, even if their
|
|
34
|
+
* name would otherwise match the story-file pattern.
|
|
35
|
+
*
|
|
36
|
+
* This is a **detection-only** rule: no autofix (`meta.fixable` unset) and no suggestions
|
|
37
|
+
* (`meta.hasSuggestions` unset) — migrating a story file to the factory format requires
|
|
38
|
+
* rewriting the whole file's shape (imports, meta declaration, every story export), which is far
|
|
39
|
+
* beyond what a syntax-preserving ESLint fixer can safely produce. Storybook ships an official
|
|
40
|
+
* codemod for this (`npx storybook@latest automigrate csf-factories`) — see
|
|
41
|
+
* `REQUIRE_CSF_FACTORY_DOCS_URL`.
|
|
42
|
+
*
|
|
43
|
+
* See README.md for incorrect/correct examples.
|
|
44
|
+
*/
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.requireCsfFactory = exports.REQUIRE_CSF_FACTORY_DOCS_URL = void 0;
|
|
47
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
48
|
+
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
|
|
49
|
+
/**
|
|
50
|
+
* Canonical permalink to the how-to-migrate doc. Single source of truth, embedded in every
|
|
51
|
+
* message so developers tripping the rule get a direct link. Mirrors the
|
|
52
|
+
* `STORYBOOK_LANDING_PAGE_DOCS_URL` idiom in `../require-storybook-folder-landing-page/require-storybook-folder-landing-page.ts`.
|
|
53
|
+
*/
|
|
54
|
+
exports.REQUIRE_CSF_FACTORY_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-CSF-FACTORY.md";
|
|
55
|
+
// Only analyze story files — mirrors `STORY_FILE_REGEX` in the landing-page rule.
|
|
56
|
+
const STORY_FILE_REGEX = /\.stories\.(js|jsx|ts|tsx)$/;
|
|
57
|
+
// Excluded even when the name would otherwise match a story file.
|
|
58
|
+
const STORYBOOK_CONFIG_DIR_REGEX = /(^|\/)\.storybook\//;
|
|
59
|
+
// Not anchored to the end: a real demo file is named `Foo.demo.tsx` (never also `.stories.`,
|
|
60
|
+
// so it's already excluded by `STORY_FILE_REGEX` below), but this stays non-anchored so it also
|
|
61
|
+
// excludes a hypothetical combined name like `Foo.demo.stories.tsx` rather than relying on that
|
|
62
|
+
// combination never existing.
|
|
63
|
+
const DEMO_FILE_REGEX = /\.demo\./;
|
|
64
|
+
const MDX_REGEX = /\.mdx$/;
|
|
65
|
+
const isAnalyzableStoryFile = (filename) => {
|
|
66
|
+
if (MDX_REGEX.test(filename)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (STORYBOOK_CONFIG_DIR_REGEX.test(filename)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (DEMO_FILE_REGEX.test(filename)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return STORY_FILE_REGEX.test(filename);
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* True for a call `<identifier>.meta({...})` where `<identifier>` resolves to an imported
|
|
79
|
+
* binding. Only the call *shape* is inspected — the object argument's properties (e.g. a
|
|
80
|
+
* generic/instantiation `component:` value) are never type-checked or destructured, so this
|
|
81
|
+
* rule stays syntax-only.
|
|
82
|
+
*/
|
|
83
|
+
const isFactoryMetaCall = (node, context) => {
|
|
84
|
+
const { callee } = node;
|
|
85
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
if (callee.property.type !== utils_1.AST_NODE_TYPES.Identifier || callee.property.name !== "meta") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
if (callee.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const [firstArgument] = node.arguments;
|
|
95
|
+
if (firstArgument === undefined || firstArgument.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const scope = context.sourceCode.getScope(callee.object);
|
|
99
|
+
const variable = scope.references.find(ref => ref.identifier === callee.object)?.resolved;
|
|
100
|
+
if (!variable) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
return variable.defs.some(def => def.type === "ImportBinding");
|
|
104
|
+
};
|
|
105
|
+
/** Unwrap `satisfies` / `as` wrappers, same as the landing-page rule's `unwrapExpression`. */
|
|
106
|
+
const unwrapExpression = (node) => {
|
|
107
|
+
let current = node;
|
|
108
|
+
while (current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression || current.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
|
|
109
|
+
current = current.expression;
|
|
110
|
+
}
|
|
111
|
+
return current;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* An old-style named story export: `export const Foo = { ... };` (object literal, not a call).
|
|
115
|
+
* `satisfies` and `as` wrappers still count (`export const Legacy = { args: {} } satisfies StoryObj`),
|
|
116
|
+
* matching how current stories write `export const Default: Story = { ... }` with a bare object.
|
|
117
|
+
*/
|
|
118
|
+
const isObjectLiteralStoryExport = (node) => {
|
|
119
|
+
const { declaration } = node;
|
|
120
|
+
if (declaration === null || declaration.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return declaration.declarations.some(declarator => {
|
|
124
|
+
if (declarator.init === null) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return unwrapExpression(declarator.init).type === utils_1.AST_NODE_TYPES.ObjectExpression;
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
const create = context => {
|
|
131
|
+
if (!isAnalyzableStoryFile(context.filename)) {
|
|
132
|
+
return {};
|
|
133
|
+
}
|
|
134
|
+
let defaultExportNode = null;
|
|
135
|
+
let metaCallNode = null;
|
|
136
|
+
let hasObjectLiteralStoryExport = false;
|
|
137
|
+
return {
|
|
138
|
+
ExportDefaultDeclaration(node) {
|
|
139
|
+
defaultExportNode = node;
|
|
140
|
+
},
|
|
141
|
+
ExportNamedDeclaration(node) {
|
|
142
|
+
if (isObjectLiteralStoryExport(node)) {
|
|
143
|
+
hasObjectLiteralStoryExport = true;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
CallExpression(node) {
|
|
147
|
+
if (metaCallNode === null && isFactoryMetaCall(node, context)) {
|
|
148
|
+
metaCallNode = node;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
"Program:exit"(node) {
|
|
152
|
+
const hasDefaultExport = defaultExportNode !== null;
|
|
153
|
+
if (!hasDefaultExport && metaCallNode === null) {
|
|
154
|
+
context.report({ node, messageId: "missingMeta" });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (metaCallNode !== null && (hasDefaultExport || hasObjectLiteralStoryExport)) {
|
|
158
|
+
context.report({ node: metaCallNode, messageId: "mixedFormat" });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (defaultExportNode !== null) {
|
|
162
|
+
context.report({ node: defaultExportNode, messageId: "oldFormat" });
|
|
163
|
+
}
|
|
164
|
+
// Otherwise: factory format (a `.meta({...})` call with no old-format signals) — silent.
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
exports.requireCsfFactory = createRule({
|
|
169
|
+
name: "require-csf-factory",
|
|
170
|
+
meta: {
|
|
171
|
+
type: "problem",
|
|
172
|
+
docs: {
|
|
173
|
+
description: "Warn on Storybook story files that are not written in CSF factory format",
|
|
174
|
+
},
|
|
175
|
+
schema: [],
|
|
176
|
+
messages: {
|
|
177
|
+
oldFormat: `[require-csf-factory] This story file uses the legacy \`export default { ... }\` CSF format instead of Storybook's CSF factory format (\`preview.meta({...})\` + \`meta.story({...})\`). See ${exports.REQUIRE_CSF_FACTORY_DOCS_URL} for how to migrate.`,
|
|
178
|
+
mixedFormat: `[require-csf-factory] This story file mixes a factory \`.meta({...})\` call with legacy \`export default\`/object-literal story exports. Migrate the whole file to the CSF factory format. See ${exports.REQUIRE_CSF_FACTORY_DOCS_URL} for how to migrate.`,
|
|
179
|
+
missingMeta: `[require-csf-factory] This story file declares no meta object (neither \`export default\` nor a factory \`.meta({...})\` call). See ${exports.REQUIRE_CSF_FACTORY_DOCS_URL} for how to migrate.`,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
defaultOptions: [],
|
|
183
|
+
create,
|
|
184
|
+
});
|
|
185
|
+
//# sourceMappingURL=require-csf-factory.js.map
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* FOLDER implied by a story's title.
|
|
4
4
|
*
|
|
5
5
|
* Architecture: per-story-file rule. Each `*.stories.@(js|jsx|ts|tsx)` file declares a Storybook
|
|
6
|
-
* `title` (e.g. "Hooks/react-core-hooks/useDebounce")
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* `title` (e.g. "Hooks/react-core-hooks/useDebounce"), read from either the legacy default-export
|
|
7
|
+
* meta object (`export default { title, ... }`, including via an intermediate variable) or a
|
|
8
|
+
* Storybook CSF factory `.meta({...})` call (`const meta = preview.meta({ title, ... });`).
|
|
9
|
+
* The folders implied by that title are the accumulated path prefixes
|
|
10
|
+
* EXCLUDING the story leaf (here: "Hooks" and "Hooks/react-core-hooks"). Every such folder must
|
|
11
|
+
* have a landing page: a `*.docs.mdx` somewhere in the workspace whose `<Meta title="FOLDER" />`
|
|
12
|
+
* EXACTLY equals the folder path, with non-empty body and no TODO marker.
|
|
10
13
|
*
|
|
11
14
|
* The rule is REPORT-ONLY: it does not scaffold files. Creating a separate `*.docs.mdx` cannot be
|
|
12
15
|
* expressed as a pure ESLint fix, and ESLint evaluates fix/suggestion callbacks on every lint pass
|
|
@@ -18,6 +21,13 @@
|
|
|
18
21
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
19
22
|
/** Canonical TODO marker. A landing page still containing this marker does NOT satisfy the rule. */
|
|
20
23
|
export declare const STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-folder-landing-page */}";
|
|
24
|
+
/**
|
|
25
|
+
* The namespaced rule id (plugin `@trackunit/storybook`, rule `require-folder-landing-page`).
|
|
26
|
+
* Registered as the `require-folder-landing-page` entry of the shared `storybookPlugin` object
|
|
27
|
+
* (see `../../config/plugins.ts`). This is the id the deprecated alias below points at via
|
|
28
|
+
* `meta.deprecated.replacedBy`.
|
|
29
|
+
*/
|
|
30
|
+
export declare const STORYBOOK_NAMESPACED_RULE_ID = "@trackunit/storybook/require-folder-landing-page";
|
|
21
31
|
/**
|
|
22
32
|
* Canonical permalink to the how-to-author doc. Single source of truth, embedded in the rule's
|
|
23
33
|
* error message so developers tripping the rule get a direct link. The `master` permalink stays
|
|
@@ -26,6 +36,20 @@ export declare const STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-f
|
|
|
26
36
|
export declare const STORYBOOK_LANDING_PAGE_DOCS_URL = "https://github.com/Trackunit/manager/blob/master/docs/STORYBOOK-LANDING-PAGES.md";
|
|
27
37
|
/** Clear the module-level scan cache (primarily for tests). */
|
|
28
38
|
export declare const clearLandingPageCache: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* The namespaced rule: `@trackunit/storybook/require-folder-landing-page` (registered under that
|
|
41
|
+
* key in the shared `storybookPlugin` object exported from `../../config/plugins.ts`). This is
|
|
42
|
+
* the current, non-deprecated rule id.
|
|
43
|
+
*/
|
|
44
|
+
export declare const requireFolderLandingPage: ESLintUtils.RuleModule<"missingLandingPage", [], unknown, ESLintUtils.RuleListener> & {
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Deprecated alias of {@link requireFolderLandingPage}, kept registered under the original
|
|
49
|
+
* `require-storybook-folder-landing-page` key in `rulesMap` (id `@trackunit/require-storybook-folder-landing-page`)
|
|
50
|
+
* so existing configs keep working. Delegates to the identical `create` implementation — only the
|
|
51
|
+
* `meta` differs (marks itself deprecated and points at the namespaced replacement).
|
|
52
|
+
*/
|
|
29
53
|
export declare const requireStorybookFolderLandingPage: ESLintUtils.RuleModule<"missingLandingPage", [], unknown, ESLintUtils.RuleListener> & {
|
|
30
54
|
name: string;
|
|
31
55
|
};
|
package/src/lib/rules/require-storybook-folder-landing-page/require-storybook-folder-landing-page.js
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* FOLDER implied by a story's title.
|
|
5
5
|
*
|
|
6
6
|
* Architecture: per-story-file rule. Each `*.stories.@(js|jsx|ts|tsx)` file declares a Storybook
|
|
7
|
-
* `title` (e.g. "Hooks/react-core-hooks/useDebounce")
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `title` (e.g. "Hooks/react-core-hooks/useDebounce"), read from either the legacy default-export
|
|
8
|
+
* meta object (`export default { title, ... }`, including via an intermediate variable) or a
|
|
9
|
+
* Storybook CSF factory `.meta({...})` call (`const meta = preview.meta({ title, ... });`).
|
|
10
|
+
* The folders implied by that title are the accumulated path prefixes
|
|
11
|
+
* EXCLUDING the story leaf (here: "Hooks" and "Hooks/react-core-hooks"). Every such folder must
|
|
12
|
+
* have a landing page: a `*.docs.mdx` somewhere in the workspace whose `<Meta title="FOLDER" />`
|
|
13
|
+
* EXACTLY equals the folder path, with non-empty body and no TODO marker.
|
|
11
14
|
*
|
|
12
15
|
* The rule is REPORT-ONLY: it does not scaffold files. Creating a separate `*.docs.mdx` cannot be
|
|
13
16
|
* expressed as a pure ESLint fix, and ESLint evaluates fix/suggestion callbacks on every lint pass
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
* See README.md for incorrect/correct examples.
|
|
18
21
|
*/
|
|
19
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.requireStorybookFolderLandingPage = exports.clearLandingPageCache = exports.STORYBOOK_LANDING_PAGE_DOCS_URL = exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = void 0;
|
|
23
|
+
exports.requireStorybookFolderLandingPage = exports.requireFolderLandingPage = exports.clearLandingPageCache = exports.STORYBOOK_LANDING_PAGE_DOCS_URL = exports.STORYBOOK_NAMESPACED_RULE_ID = exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = void 0;
|
|
21
24
|
const tslib_1 = require("tslib");
|
|
22
25
|
const utils_1 = require("@typescript-eslint/utils");
|
|
23
26
|
const crypto = tslib_1.__importStar(require("crypto"));
|
|
@@ -27,6 +30,13 @@ const file_utils_1 = require("../../utils/file-utils");
|
|
|
27
30
|
const createRule = utils_1.ESLintUtils.RuleCreator(name => `https://github.com/trackunit/manager/blob/master/libs/eslint/plugin-trackunit/src/lib/rules/${name}/${name}.ts`);
|
|
28
31
|
/** Canonical TODO marker. A landing page still containing this marker does NOT satisfy the rule. */
|
|
29
32
|
exports.STORYBOOK_LANDING_PAGE_TODO_MARKER = "{/* TODO: storybook-folder-landing-page */}";
|
|
33
|
+
/**
|
|
34
|
+
* The namespaced rule id (plugin `@trackunit/storybook`, rule `require-folder-landing-page`).
|
|
35
|
+
* Registered as the `require-folder-landing-page` entry of the shared `storybookPlugin` object
|
|
36
|
+
* (see `../../config/plugins.ts`). This is the id the deprecated alias below points at via
|
|
37
|
+
* `meta.deprecated.replacedBy`.
|
|
38
|
+
*/
|
|
39
|
+
exports.STORYBOOK_NAMESPACED_RULE_ID = "@trackunit/storybook/require-folder-landing-page";
|
|
30
40
|
/**
|
|
31
41
|
* Canonical permalink to the how-to-author doc. Single source of truth, embedded in the rule's
|
|
32
42
|
* error message so developers tripping the rule get a direct link. The `master` permalink stays
|
|
@@ -157,82 +167,136 @@ const getTitleFromObject = (object) => {
|
|
|
157
167
|
}
|
|
158
168
|
return null;
|
|
159
169
|
};
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
/**
|
|
171
|
+
* The rule's `create` implementation, shared by every rule object below (the namespaced rule and
|
|
172
|
+
* the deprecated alias). Extracted to a standalone function so both `createRule(...)` calls
|
|
173
|
+
* delegate to the identical implementation instead of duplicating the detection/reporting logic.
|
|
174
|
+
*/
|
|
175
|
+
const create = context => {
|
|
176
|
+
const currentFile = context.filename;
|
|
177
|
+
// Only analyze story files (never .docs.* or .mdx).
|
|
178
|
+
if (!STORY_FILE_REGEX.test(currentFile)) {
|
|
179
|
+
return {};
|
|
180
|
+
}
|
|
181
|
+
const nxJsonPath = (0, file_utils_1.findNearestFile)(currentFile, "nx.json");
|
|
182
|
+
if (nxJsonPath === null) {
|
|
183
|
+
return {};
|
|
184
|
+
}
|
|
185
|
+
const workspaceRoot = path.dirname(nxJsonPath);
|
|
186
|
+
const reportForObject = (titleNode, reportNode) => {
|
|
187
|
+
const title = titleNode.value;
|
|
188
|
+
if (typeof title !== "string") {
|
|
189
|
+
return;
|
|
179
190
|
}
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
return
|
|
191
|
+
const folders = deriveFolders(title);
|
|
192
|
+
if (folders.length === 0) {
|
|
193
|
+
return;
|
|
183
194
|
}
|
|
184
|
-
const
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
const folders = deriveFolders(title);
|
|
191
|
-
if (folders.length === 0) {
|
|
192
|
-
return;
|
|
195
|
+
const landingPages = getLandingPages(workspaceRoot);
|
|
196
|
+
for (const folder of folders) {
|
|
197
|
+
const entry = landingPages.get(folder);
|
|
198
|
+
if (isSatisfied(entry)) {
|
|
199
|
+
continue;
|
|
193
200
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
context.report({
|
|
202
|
+
node: reportNode,
|
|
203
|
+
messageId: "missingLandingPage",
|
|
204
|
+
data: { folder },
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
ExportDefaultDeclaration(node) {
|
|
210
|
+
const declaration = node.declaration;
|
|
211
|
+
// Shape 1: export default { title: "..." }
|
|
212
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
213
|
+
const titleNode = getTitleFromObject(declaration);
|
|
214
|
+
if (titleNode) {
|
|
215
|
+
reportForObject(titleNode, node);
|
|
199
216
|
}
|
|
200
|
-
|
|
201
|
-
node: reportNode,
|
|
202
|
-
messageId: "missingLandingPage",
|
|
203
|
-
data: { folder },
|
|
204
|
-
});
|
|
217
|
+
return;
|
|
205
218
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// Shape 2: const meta = { title: "..." } satisfies Meta; export default meta;
|
|
219
|
-
if (declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
220
|
-
const variable = context.sourceCode
|
|
221
|
-
.getScope(node)
|
|
222
|
-
.references.find(ref => ref.identifier === declaration)?.resolved;
|
|
223
|
-
const definition = variable?.defs[0];
|
|
224
|
-
if (definition && definition.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && definition.node.init) {
|
|
225
|
-
const initializer = unwrapExpression(definition.node.init);
|
|
226
|
-
if (initializer.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
227
|
-
const titleNode = getTitleFromObject(initializer);
|
|
228
|
-
if (titleNode) {
|
|
229
|
-
reportForObject(titleNode, node);
|
|
230
|
-
}
|
|
219
|
+
// Shape 2: const meta = { title: "..." } satisfies Meta; export default meta;
|
|
220
|
+
if (declaration.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
221
|
+
const variable = context.sourceCode
|
|
222
|
+
.getScope(node)
|
|
223
|
+
.references.find(ref => ref.identifier === declaration)?.resolved;
|
|
224
|
+
const definition = variable?.defs[0];
|
|
225
|
+
if (definition && definition.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && definition.node.init) {
|
|
226
|
+
const initializer = unwrapExpression(definition.node.init);
|
|
227
|
+
if (initializer.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
228
|
+
const titleNode = getTitleFromObject(initializer);
|
|
229
|
+
if (titleNode) {
|
|
230
|
+
reportForObject(titleNode, node);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
// Shape 3 (CSF factory format): `<binding>.meta({ title: "..." })`, e.g.
|
|
237
|
+
// `const meta = preview.meta({ title: "Foo/Widget", component: Widget });`. Only the call
|
|
238
|
+
// shape is checked (a `.meta(...)` member call with an object-literal first argument) — this
|
|
239
|
+
// is purely additive: it does not change how the default-export shapes above are detected.
|
|
240
|
+
CallExpression(node) {
|
|
241
|
+
const { callee } = node;
|
|
242
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (callee.property.type !== utils_1.AST_NODE_TYPES.Identifier || callee.property.name !== "meta") {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const [firstArgument] = node.arguments;
|
|
249
|
+
if (firstArgument === undefined || firstArgument.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const titleNode = getTitleFromObject(firstArgument);
|
|
253
|
+
if (titleNode) {
|
|
254
|
+
reportForObject(titleNode, node);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Builds a fresh `meta` object for a landing-page rule object. Called once per rule object (never
|
|
261
|
+
* shared by reference) so the namespaced rule and the deprecated alias can carry different
|
|
262
|
+
* `deprecated` values without one mutating the other.
|
|
263
|
+
*/
|
|
264
|
+
const buildMeta = (deprecated) => ({
|
|
265
|
+
type: "problem",
|
|
266
|
+
docs: {
|
|
267
|
+
description: "Require a Storybook landing page (*.docs.mdx) for every ancestor folder implied by a story's title",
|
|
268
|
+
},
|
|
269
|
+
schema: [],
|
|
270
|
+
messages: {
|
|
271
|
+
// URL embedded from the single-source-of-truth const (see STORYBOOK_LANDING_PAGE_DOCS_URL).
|
|
272
|
+
missingLandingPage: `[require-storybook-folder-landing-page] Missing Storybook landing page for folder "{{folder}}". The <Meta title> must equal "{{folder}}" exactly (no extra leaf). Add a *.docs.mdx with <Meta title="{{folder}}" /> and real content. See ${exports.STORYBOOK_LANDING_PAGE_DOCS_URL} for how to author one.`,
|
|
236
273
|
},
|
|
274
|
+
...(deprecated === undefined ? {} : { deprecated }),
|
|
275
|
+
});
|
|
276
|
+
/**
|
|
277
|
+
* The namespaced rule: `@trackunit/storybook/require-folder-landing-page` (registered under that
|
|
278
|
+
* key in the shared `storybookPlugin` object exported from `../../config/plugins.ts`). This is
|
|
279
|
+
* the current, non-deprecated rule id.
|
|
280
|
+
*/
|
|
281
|
+
exports.requireFolderLandingPage = createRule({
|
|
282
|
+
name: "require-storybook-folder-landing-page",
|
|
283
|
+
meta: buildMeta(),
|
|
284
|
+
defaultOptions: [],
|
|
285
|
+
create,
|
|
286
|
+
});
|
|
287
|
+
/**
|
|
288
|
+
* Deprecated alias of {@link requireFolderLandingPage}, kept registered under the original
|
|
289
|
+
* `require-storybook-folder-landing-page` key in `rulesMap` (id `@trackunit/require-storybook-folder-landing-page`)
|
|
290
|
+
* so existing configs keep working. Delegates to the identical `create` implementation — only the
|
|
291
|
+
* `meta` differs (marks itself deprecated and points at the namespaced replacement).
|
|
292
|
+
*/
|
|
293
|
+
exports.requireStorybookFolderLandingPage = createRule({
|
|
294
|
+
name: "require-storybook-folder-landing-page",
|
|
295
|
+
meta: buildMeta({
|
|
296
|
+
message: `Use \`${exports.STORYBOOK_NAMESPACED_RULE_ID}\` instead.`,
|
|
297
|
+
replacedBy: [{ rule: { name: exports.STORYBOOK_NAMESPACED_RULE_ID } }],
|
|
298
|
+
}),
|
|
299
|
+
defaultOptions: [],
|
|
300
|
+
create,
|
|
237
301
|
});
|
|
238
302
|
//# sourceMappingURL=require-storybook-folder-landing-page.js.map
|