@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgorbatchev/typescript-ai-policy",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Shared TypeScript AI policy configs and Oxlint rules",
5
5
  "homepage": "https://github.com/alexgorbatchev/typescript-ai-policy#readme",
6
6
  "repository": {
@@ -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", "jsx-a11y", "jest"],
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 either "ComponentName.tsx" or "component-name.tsx" so its basename can map deterministically to the exported component name.',
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 somewhere under a sibling "stories/" directory',
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
- 'Add a story file named "{{ requiredStoryFileName }}" somewhere under "{{ requiredStoriesDirectoryPath }}". Component ownership files must keep their Storybook coverage under a sibling "stories/" directory.',
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 somewhere under a sibling "__tests__/" directory',
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
- 'Add a test file named "{{ requiredTestFileName }}" somewhere under "{{ requiredTestsDirectoryPath }}". Hook ownership files must keep their tests under a sibling "__tests__/" directory.',
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 { RuleModule } from "./types.ts";
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 { getStorySourceBaseName, readPathFromStoriesDirectory, readRootPathBeforeDirectory } from "./helpers.ts";
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 somewhere under a sibling "stories/" directory and match a sibling component ownership file basename',
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. Expected "{{ requiredComponentFilePath }}" to exist for this story 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
  },