@alexgorbatchev/typescript-ai-policy 1.0.1 → 1.0.2
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 +1 -1
- package/package.json +1 -1
- package/src/oxlint/createOxlintConfig.ts +1 -1
- package/src/oxlint/rules/component-file-naming-convention.ts +6 -2
- package/src/oxlint/rules/component-story-file-convention.ts +5 -3
- package/src/oxlint/rules/helpers.ts +10 -0
- package/src/oxlint/rules/hook-test-file-convention.ts +11 -4
- package/src/oxlint/rules/story-file-location-convention.ts +19 -8
package/README.md
CHANGED
|
@@ -197,7 +197,7 @@ For rule-by-rule rationale plus good/bad examples, see [`src/oxlint/README.md`](
|
|
|
197
197
|
| `@alexgorbatchev/no-react-create-element` | Regular application code must use JSX instead of `createElement`. |
|
|
198
198
|
| `@alexgorbatchev/require-component-root-testid` | Non-story exported React components must render a DOM root with `data-testid`/`testId` exactly equal to the component name, and child ids must use `ComponentName--thing`. |
|
|
199
199
|
| `@alexgorbatchev/component-file-contract` | Component ownership files may export exactly one main runtime component plus unrestricted type-only API. |
|
|
200
|
-
| `@alexgorbatchev/component-file-naming-convention` | Component filenames must match their exported PascalCase component name in either PascalCase or kebab-case form
|
|
200
|
+
| `@alexgorbatchev/component-file-naming-convention` | Component filenames must match their exported PascalCase component name in either PascalCase or kebab-case form; non-component `.tsx` modules should be renamed to `.ts`. |
|
|
201
201
|
| `@alexgorbatchev/component-story-file-convention` | Every component ownership file must have a matching `basename.stories.tsx` file somewhere under a sibling `stories/` directory. |
|
|
202
202
|
| `@alexgorbatchev/story-file-location-convention` | Storybook files must live under sibling `stories/` directories and must still match a sibling component basename. |
|
|
203
203
|
| `@alexgorbatchev/story-meta-type-annotation` | The default Storybook meta must use a typed `const meta: Meta<typeof ComponentName>` binding instead of object assertions. |
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ const DEFAULT_OXLINT_CONFIG = defineConfig({
|
|
|
26
26
|
"**/.vitepress/cache",
|
|
27
27
|
"**/.vitepress/dist",
|
|
28
28
|
],
|
|
29
|
-
plugins: ["unicorn", "typescript", "oxc", "react", "
|
|
29
|
+
plugins: ["unicorn", "typescript", "oxc", "react", "jest"],
|
|
30
30
|
jsPlugins: [
|
|
31
31
|
{
|
|
32
32
|
name: "@alexgorbatchev",
|
|
@@ -187,6 +187,10 @@ function readExpectedComponentNameFromFilename(filename: string): string | null
|
|
|
187
187
|
.join("");
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
function readInvalidFilenameReportNode(program: AstProgram): TSESTree.Node {
|
|
191
|
+
return program.body[0] ?? program;
|
|
192
|
+
}
|
|
193
|
+
|
|
190
194
|
const componentFileNamingConventionRule: RuleModule = {
|
|
191
195
|
meta: {
|
|
192
196
|
type: "problem" as const,
|
|
@@ -197,7 +201,7 @@ const componentFileNamingConventionRule: RuleModule = {
|
|
|
197
201
|
schema: [],
|
|
198
202
|
messages: {
|
|
199
203
|
invalidComponentFileName:
|
|
200
|
-
'Rename this file to
|
|
204
|
+
'Rename this file so its basename can map deterministically to the exported component name. Use "ComponentName.tsx" or "component-name.tsx"; if this is not a component ownership file and does not need JSX syntax, rename it to a ".ts" file instead.',
|
|
201
205
|
invalidComponentExportName:
|
|
202
206
|
"Rename the exported component to PascalCase. Component ownership files must export a PascalCase component name.",
|
|
203
207
|
mismatchedComponentFileName:
|
|
@@ -224,7 +228,7 @@ const componentFileNamingConventionRule: RuleModule = {
|
|
|
224
228
|
const expectedComponentName = readExpectedComponentNameFromFilename(context.filename);
|
|
225
229
|
if (!expectedComponentName) {
|
|
226
230
|
context.report({
|
|
227
|
-
node,
|
|
231
|
+
node: readInvalidFilenameReportNode(node),
|
|
228
232
|
messageId: "invalidComponentFileName",
|
|
229
233
|
});
|
|
230
234
|
return;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
isExemptSupportBasename,
|
|
8
8
|
isInStoriesDirectory,
|
|
9
9
|
isInTestsDirectory,
|
|
10
|
+
readAbbreviatedSiblingDirectoryPath,
|
|
10
11
|
} from "./helpers.ts";
|
|
11
12
|
|
|
12
13
|
function readRequiredStoriesDirectoryPath(filename: string): string {
|
|
@@ -67,12 +68,12 @@ const componentStoryFileConventionRule: RuleModule = {
|
|
|
67
68
|
type: "problem" as const,
|
|
68
69
|
docs: {
|
|
69
70
|
description:
|
|
70
|
-
'Require every component ownership file to have a matching "basename.stories.tsx" file
|
|
71
|
+
'Require every component ownership file to have a matching "basename.stories.tsx" file under a sibling "stories/" directory',
|
|
71
72
|
},
|
|
72
73
|
schema: [],
|
|
73
74
|
messages: {
|
|
74
75
|
missingComponentStoryFile:
|
|
75
|
-
'
|
|
76
|
+
'Create "{{ requiredStoryFileName }}" under "{{ requiredStoriesDirectoryPath }}". Component ownership files must keep their Storybook coverage under a sibling "stories/" directory.',
|
|
76
77
|
},
|
|
77
78
|
},
|
|
78
79
|
create(context) {
|
|
@@ -87,6 +88,7 @@ const componentStoryFileConventionRule: RuleModule = {
|
|
|
87
88
|
return {
|
|
88
89
|
Program(node: AstProgram) {
|
|
89
90
|
const requiredStoriesDirectoryPath = readRequiredStoriesDirectoryPath(context.filename);
|
|
91
|
+
const displayedStoriesDirectoryPath = readAbbreviatedSiblingDirectoryPath(context.filename, "stories");
|
|
90
92
|
const requiredStoryFileName = readRequiredStoryFileName(context.filename);
|
|
91
93
|
if (findDescendantFilePath(requiredStoriesDirectoryPath, requiredStoryFileName)) {
|
|
92
94
|
return;
|
|
@@ -96,7 +98,7 @@ const componentStoryFileConventionRule: RuleModule = {
|
|
|
96
98
|
node: readReportNode(node),
|
|
97
99
|
messageId: "missingComponentStoryFile",
|
|
98
100
|
data: {
|
|
99
|
-
requiredStoriesDirectoryPath,
|
|
101
|
+
requiredStoriesDirectoryPath: displayedStoriesDirectoryPath,
|
|
100
102
|
requiredStoryFileName,
|
|
101
103
|
},
|
|
102
104
|
});
|
|
@@ -74,6 +74,16 @@ export function readPathFromDirectory(filename: string, expectedDirectoryName: s
|
|
|
74
74
|
return pathSegments.slice(directoryIndex + 1).join("/");
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
export function readAbbreviatedPath(path: string, segmentCount = 3): string {
|
|
78
|
+
const displayedPathSegments = getPathSegments(path).slice(-segmentCount);
|
|
79
|
+
|
|
80
|
+
return `.../${displayedPathSegments.join("/")}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function readAbbreviatedSiblingDirectoryPath(filename: string, siblingDirectoryName: string): string {
|
|
84
|
+
return readAbbreviatedPath(`${dirname(filename)}/${siblingDirectoryName}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
export function readPathFromFirstMatchingDirectory(
|
|
78
88
|
filename: string,
|
|
79
89
|
expectedDirectoryNames: DirectoryNames,
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { AstProgram, RuleModule } from "./types.ts";
|
|
2
|
+
import type { TSESTree } from "@typescript-eslint/types";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import {
|
|
4
5
|
findDescendantFilePath,
|
|
5
6
|
getBaseName,
|
|
6
7
|
getFilenameWithoutExtension,
|
|
7
8
|
isExemptSupportBasename,
|
|
9
|
+
readAbbreviatedSiblingDirectoryPath,
|
|
8
10
|
} from "./helpers.ts";
|
|
9
11
|
|
|
10
12
|
function readRequiredTestsDirectoryPath(filename: string): string {
|
|
@@ -18,17 +20,21 @@ function readRequiredHookTestFileName(filename: string): string {
|
|
|
18
20
|
return `${sourceBaseName}${testExtension}`;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
function readReportNode(program: AstProgram): TSESTree.Node {
|
|
24
|
+
return program.body[0] ?? program;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const hookTestFileConventionRule: RuleModule = {
|
|
22
28
|
meta: {
|
|
23
29
|
type: "problem" as const,
|
|
24
30
|
docs: {
|
|
25
31
|
description:
|
|
26
|
-
'Require every hook ownership file to have a matching "basename.test.ts" or ".test.tsx" file
|
|
32
|
+
'Require every hook ownership file to have a matching "basename.test.ts" or ".test.tsx" file under a sibling "__tests__/" directory',
|
|
27
33
|
},
|
|
28
34
|
schema: [],
|
|
29
35
|
messages: {
|
|
30
36
|
missingHookTestFile:
|
|
31
|
-
'
|
|
37
|
+
'Create "{{ requiredTestFileName }}" under "{{ requiredTestsDirectoryPath }}". Hook ownership files must keep their tests under a sibling "__tests__/" directory.',
|
|
32
38
|
},
|
|
33
39
|
},
|
|
34
40
|
create(context) {
|
|
@@ -39,17 +45,18 @@ const hookTestFileConventionRule: RuleModule = {
|
|
|
39
45
|
return {
|
|
40
46
|
Program(node: AstProgram) {
|
|
41
47
|
const requiredTestsDirectoryPath = readRequiredTestsDirectoryPath(context.filename);
|
|
48
|
+
const displayedTestsDirectoryPath = readAbbreviatedSiblingDirectoryPath(context.filename, "__tests__");
|
|
42
49
|
const requiredTestFileName = readRequiredHookTestFileName(context.filename);
|
|
43
50
|
if (findDescendantFilePath(requiredTestsDirectoryPath, requiredTestFileName)) {
|
|
44
51
|
return;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
context.report({
|
|
48
|
-
node,
|
|
55
|
+
node: readReportNode(node),
|
|
49
56
|
messageId: "missingHookTestFile",
|
|
50
57
|
data: {
|
|
51
58
|
requiredTestFileName,
|
|
52
|
-
requiredTestsDirectoryPath,
|
|
59
|
+
requiredTestsDirectoryPath: displayedTestsDirectoryPath,
|
|
53
60
|
},
|
|
54
61
|
});
|
|
55
62
|
},
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TSESTree } from "@typescript-eslint/types";
|
|
2
|
+
import type { AstProgram, RuleModule } from "./types.ts";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
getStorySourceBaseName,
|
|
7
|
+
readAbbreviatedPath,
|
|
8
|
+
readPathFromStoriesDirectory,
|
|
9
|
+
readRootPathBeforeDirectory,
|
|
10
|
+
} from "./helpers.ts";
|
|
5
11
|
|
|
6
12
|
function readRequiredSiblingComponentFilePath(filename: string): string | null {
|
|
7
13
|
const storySourceBaseName = getStorySourceBaseName(filename);
|
|
@@ -17,28 +23,33 @@ function readRequiredSiblingComponentFilePath(filename: string): string | null {
|
|
|
17
23
|
return join(siblingDirectoryPath, `${storySourceBaseName}.tsx`);
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
function readReportNode(program: AstProgram): TSESTree.Node {
|
|
27
|
+
return program.body[0] ?? program;
|
|
28
|
+
}
|
|
29
|
+
|
|
20
30
|
const storyFileLocationConventionRule: RuleModule = {
|
|
21
31
|
meta: {
|
|
22
32
|
type: "problem" as const,
|
|
23
33
|
docs: {
|
|
24
34
|
description:
|
|
25
|
-
'Require Storybook files to live
|
|
35
|
+
'Require Storybook files to live under a sibling "stories/" directory and match a sibling component ownership file basename',
|
|
26
36
|
},
|
|
27
37
|
schema: [],
|
|
28
38
|
messages: {
|
|
29
39
|
invalidStoryFileLocation:
|
|
30
40
|
'Move this story file under a "stories/" directory. Storybook files must not live outside a sibling "stories/" tree.',
|
|
31
41
|
missingSiblingComponent:
|
|
32
|
-
'Rename or move this story so it matches an existing sibling component ownership file.
|
|
42
|
+
'Rename or move this story so it matches an existing sibling component ownership file. "{{ requiredComponentFilePath }}" must exist for this story file.',
|
|
33
43
|
},
|
|
34
44
|
},
|
|
35
45
|
create(context) {
|
|
36
46
|
return {
|
|
37
|
-
Program(node) {
|
|
47
|
+
Program(node: AstProgram) {
|
|
48
|
+
const reportNode = readReportNode(node);
|
|
38
49
|
const relativeStoryPath = readPathFromStoriesDirectory(context.filename);
|
|
39
50
|
if (relativeStoryPath === null) {
|
|
40
51
|
context.report({
|
|
41
|
-
node,
|
|
52
|
+
node: reportNode,
|
|
42
53
|
messageId: "invalidStoryFileLocation",
|
|
43
54
|
});
|
|
44
55
|
return;
|
|
@@ -50,10 +61,10 @@ const storyFileLocationConventionRule: RuleModule = {
|
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
context.report({
|
|
53
|
-
node,
|
|
64
|
+
node: reportNode,
|
|
54
65
|
messageId: "missingSiblingComponent",
|
|
55
66
|
data: {
|
|
56
|
-
requiredComponentFilePath,
|
|
67
|
+
requiredComponentFilePath: readAbbreviatedPath(requiredComponentFilePath),
|
|
57
68
|
},
|
|
58
69
|
});
|
|
59
70
|
},
|