@agjs/tsforge 0.1.6 → 0.1.7
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/package.json +1 -1
- package/src/loop/rule-docs.generated.json +10 -0
- package/src/rule-packs/index.ts +2 -0
- package/src/rule-packs/module-boundaries/index.ts +23 -0
- package/src/rule-packs/module-boundaries/rules/no-import-build-output.ts +71 -0
- package/src/rule-packs/module-boundaries/rules/no-import-test-from-source.ts +74 -0
- package/src/rule-packs/module-boundaries/utils.ts +29 -0
package/package.json
CHANGED
|
@@ -289,6 +289,16 @@
|
|
|
289
289
|
"bad": "// Example that violates the rule",
|
|
290
290
|
"good": "// Corrected version"
|
|
291
291
|
},
|
|
292
|
+
"tsforge/no-import-build-output": {
|
|
293
|
+
"what": "Disallow importing from build/output directories within the project. Source must import source, not compiled artifacts, to avoid stale-code drift and broken module boundaries.",
|
|
294
|
+
"bad": "// Example that violates the rule",
|
|
295
|
+
"good": "// Corrected version"
|
|
296
|
+
},
|
|
297
|
+
"tsforge/no-import-test-from-source": {
|
|
298
|
+
"what": "Disallow production/source files from importing test files. Tests may depend on source, never the reverse — test code must not ship in the production graph.",
|
|
299
|
+
"bad": "// Example that violates the rule",
|
|
300
|
+
"good": "// Corrected version"
|
|
301
|
+
},
|
|
292
302
|
"tsforge/pkce-required-for-oidc": {
|
|
293
303
|
"what": "OIDC providers must use PKCE: `buildAuthorizationURL` must call `generateCodeVerifier()` and pass it to `createAuthorizationURL`.",
|
|
294
304
|
"bad": "// Example that violates the rule",
|
package/src/rule-packs/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { elysiaPack } from "./elysia";
|
|
|
8
8
|
import { envAccessPack } from "./env-access";
|
|
9
9
|
import { i18nKeysPack } from "./i18n-keys";
|
|
10
10
|
import { jwtCookiesPack } from "./jwt-cookies";
|
|
11
|
+
import { moduleBoundariesPack } from "./module-boundaries";
|
|
11
12
|
import { oauthSecurityPack } from "./oauth-security";
|
|
12
13
|
import { reactComponentArchitecturePack } from "./react-component-architecture";
|
|
13
14
|
import { structuredLoggingPack } from "./structured-logging";
|
|
@@ -25,6 +26,7 @@ export const RULE_PACKS = {
|
|
|
25
26
|
"env-access": envAccessPack,
|
|
26
27
|
"i18n-keys": i18nKeysPack,
|
|
27
28
|
"jwt-cookies": jwtCookiesPack,
|
|
29
|
+
"module-boundaries": moduleBoundariesPack,
|
|
28
30
|
"oauth-security": oauthSecurityPack,
|
|
29
31
|
"react-component-architecture": reactComponentArchitecturePack,
|
|
30
32
|
"structured-logging": structuredLoggingPack,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { noImportBuildOutputRule } from "./rules/no-import-build-output";
|
|
4
|
+
import { noImportTestFromSourceRule } from "./rules/no-import-test-from-source";
|
|
5
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
6
|
+
|
|
7
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
8
|
+
"no-import-build-output": noImportBuildOutputRule,
|
|
9
|
+
"no-import-test-from-source": noImportTestFromSourceRule,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const moduleBoundariesPack: IRulePack = {
|
|
13
|
+
id: "module-boundaries",
|
|
14
|
+
description:
|
|
15
|
+
"Module boundary hygiene: keep the test/production and source/build-output boundaries clean so the dependency graph stays sound.",
|
|
16
|
+
rules,
|
|
17
|
+
rulesConfig: {
|
|
18
|
+
"no-import-build-output": "error",
|
|
19
|
+
"no-import-test-from-source": "error",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default moduleBoundariesPack;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import { hasDirSegment, isRelativeImport } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const RULE_NAME = "no-import-build-output";
|
|
7
|
+
|
|
8
|
+
export interface NoImportBuildOutputOptions {
|
|
9
|
+
readonly outputDirs?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type RuleOptions = [NoImportBuildOutputOptions];
|
|
13
|
+
type MessageIds = "buildOutputImported";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OUTPUT_DIRS: readonly string[] = [
|
|
16
|
+
"dist",
|
|
17
|
+
"build",
|
|
18
|
+
"out",
|
|
19
|
+
".next",
|
|
20
|
+
"coverage",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const optionSchema: JSONSchema4 = {
|
|
24
|
+
type: "object",
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
outputDirs: {
|
|
28
|
+
type: "array",
|
|
29
|
+
uniqueItems: true,
|
|
30
|
+
items: { type: "string" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const noImportBuildOutputRule = createRule<RuleOptions, MessageIds>({
|
|
36
|
+
name: RULE_NAME,
|
|
37
|
+
meta: {
|
|
38
|
+
type: "problem",
|
|
39
|
+
docs: {
|
|
40
|
+
description:
|
|
41
|
+
"Disallow importing from build/output directories within the project. Source must import source, not compiled artifacts, to avoid stale-code drift and broken module boundaries.",
|
|
42
|
+
},
|
|
43
|
+
schema: [optionSchema],
|
|
44
|
+
messages: {
|
|
45
|
+
buildOutputImported:
|
|
46
|
+
"Do not import from build output ('{{source}}'). Import the source module directly so the build graph stays the single source of truth.",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultOptions: [{ outputDirs: [...DEFAULT_OUTPUT_DIRS] }],
|
|
50
|
+
create(context, [options]) {
|
|
51
|
+
const outputDirs = new Set(options.outputDirs ?? DEFAULT_OUTPUT_DIRS);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ImportDeclaration(node) {
|
|
55
|
+
const source = node.source.value;
|
|
56
|
+
|
|
57
|
+
if (typeof source !== "string" || !isRelativeImport(source)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (hasDirSegment(source, outputDirs)) {
|
|
62
|
+
context.report({
|
|
63
|
+
node: node.source,
|
|
64
|
+
messageId: "buildOutputImported",
|
|
65
|
+
data: { source },
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import { hasDirSegment, isRelativeImport, isTestFileName } from "../utils";
|
|
5
|
+
|
|
6
|
+
export const RULE_NAME = "no-import-test-from-source";
|
|
7
|
+
|
|
8
|
+
export interface NoImportTestFromSourceOptions {
|
|
9
|
+
readonly testDirNames?: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type RuleOptions = [NoImportTestFromSourceOptions];
|
|
13
|
+
type MessageIds = "testImportedFromSource";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TEST_DIR_NAMES: readonly string[] = ["__tests__", "__mocks__"];
|
|
16
|
+
|
|
17
|
+
const optionSchema: JSONSchema4 = {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
testDirNames: {
|
|
22
|
+
type: "array",
|
|
23
|
+
uniqueItems: true,
|
|
24
|
+
items: { type: "string" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const noImportTestFromSourceRule = createRule<RuleOptions, MessageIds>({
|
|
30
|
+
name: RULE_NAME,
|
|
31
|
+
meta: {
|
|
32
|
+
type: "problem",
|
|
33
|
+
docs: {
|
|
34
|
+
description:
|
|
35
|
+
"Disallow production/source files from importing test files. Tests may depend on source, never the reverse — test code must not ship in the production graph.",
|
|
36
|
+
},
|
|
37
|
+
schema: [optionSchema],
|
|
38
|
+
messages: {
|
|
39
|
+
testImportedFromSource:
|
|
40
|
+
"Source files must not import test files ('{{source}}'). Move shared helpers into a non-test module so production code never depends on tests.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
defaultOptions: [{ testDirNames: [...DEFAULT_TEST_DIR_NAMES] }],
|
|
44
|
+
create(context, [options]) {
|
|
45
|
+
const testDirs = new Set(options.testDirNames ?? DEFAULT_TEST_DIR_NAMES);
|
|
46
|
+
|
|
47
|
+
// A test file may freely import other test files; only enforce the
|
|
48
|
+
// boundary when the importing file is itself non-test.
|
|
49
|
+
if (
|
|
50
|
+
isTestFileName(context.filename) ||
|
|
51
|
+
hasDirSegment(context.filename, testDirs)
|
|
52
|
+
) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ImportDeclaration(node) {
|
|
58
|
+
const source = node.source.value;
|
|
59
|
+
|
|
60
|
+
if (typeof source !== "string" || !isRelativeImport(source)) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isTestFileName(source) || hasDirSegment(source, testDirs)) {
|
|
65
|
+
context.report({
|
|
66
|
+
node: node.source,
|
|
67
|
+
messageId: "testImportedFromSource",
|
|
68
|
+
data: { source },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** True for relative import specifiers (`./x`, `../x`) — i.e. paths inside the
|
|
2
|
+
* project, as opposed to bare package specifiers (`react`, `@scope/pkg`). */
|
|
3
|
+
export function isRelativeImport(source: string): boolean {
|
|
4
|
+
return source.startsWith("./") || source.startsWith("../");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Path segments of a "/"-separated specifier, dropping "", ".", "..". */
|
|
8
|
+
export function pathSegments(specifier: string): string[] {
|
|
9
|
+
return specifier
|
|
10
|
+
.split("/")
|
|
11
|
+
.filter((seg) => seg.length > 0 && seg !== "." && seg !== "..");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** True if any directory segment of the specifier is in `dirs`. */
|
|
15
|
+
export function hasDirSegment(
|
|
16
|
+
specifier: string,
|
|
17
|
+
dirs: ReadonlySet<string>
|
|
18
|
+
): boolean {
|
|
19
|
+
return pathSegments(specifier).some((seg) => dirs.has(seg));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** True if the specifier's final segment names a test/spec module
|
|
23
|
+
* (`foo.test`, `foo.test.ts`, `foo.spec.tsx`, …). */
|
|
24
|
+
export function isTestFileName(specifier: string): boolean {
|
|
25
|
+
const segments = specifier.split("/");
|
|
26
|
+
const base = segments[segments.length - 1] ?? "";
|
|
27
|
+
|
|
28
|
+
return /\.(?:test|spec)(?:\.[^.]+)?$/.test(base);
|
|
29
|
+
}
|