@comity-dev/eslint-plugin 0.2.0 → 0.3.1
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 +2 -1
- package/dist/plugin.js +2 -0
- package/dist/recommended.js +1 -0
- package/dist/rules/no-layer-violation.d.ts +29 -12
- package/dist/rules/no-layer-violation.js +31 -14
- package/dist/rules/require-internal-marker.d.ts +43 -0
- package/dist/rules/require-internal-marker.js +154 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Reusable ESLint rules for Comity-wide source-code policy. This package owns the
|
|
|
6
6
|
|
|
7
7
|
## Scope
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Seven rules, each addressing a Comity-wide concern:
|
|
10
10
|
|
|
11
11
|
| Rule | Concern |
|
|
12
12
|
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -16,6 +16,7 @@ Six rules, each addressing a Comity-wide concern:
|
|
|
16
16
|
| `no-crypto-in-core` | Forbid `crypto` in Core Modules (domain must be deterministic). |
|
|
17
17
|
| `no-forbidden-subpath-import` | Disallow imports from forbidden package subpaths (`utils`, `helpers`, `shared`, `internal`, `lazy`). |
|
|
18
18
|
| `no-render-core-http-runtime` | Forbid render Core Modules from depending on HTTP runtime. |
|
|
19
|
+
| `require-internal-marker` | Require an `@internal` JSDoc tag on production sources under `src/internal/` and `src/lazy/` (owner-approved Contract B). |
|
|
19
20
|
|
|
20
21
|
## Ownership
|
|
21
22
|
|
package/dist/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ import noForbiddenDeepImport from "./rules/no-forbidden-deep-import.js";
|
|
|
4
4
|
import noForbiddenSubpathImport from "./rules/no-forbidden-subpath-import.js";
|
|
5
5
|
import noLayerViolation from "./rules/no-layer-violation.js";
|
|
6
6
|
import noRenderCoreHttpRuntime from "./rules/no-render-core-http-runtime.js";
|
|
7
|
+
import requireInternalMarker from "./rules/require-internal-marker.js";
|
|
7
8
|
export const BUILTIN_RULES = [
|
|
8
9
|
{ name: "no-forbidden-deep-import", rule: noForbiddenDeepImport },
|
|
9
10
|
{ name: "no-layer-violation", rule: noLayerViolation },
|
|
@@ -11,6 +12,7 @@ export const BUILTIN_RULES = [
|
|
|
11
12
|
{ name: "no-crypto-in-core", rule: noCryptoInCore },
|
|
12
13
|
{ name: "no-forbidden-subpath-import", rule: noForbiddenSubpathImport },
|
|
13
14
|
{ name: "no-render-core-http-runtime", rule: noRenderCoreHttpRuntime },
|
|
15
|
+
{ name: "require-internal-marker", rule: requireInternalMarker },
|
|
14
16
|
];
|
|
15
17
|
export const plugin = {
|
|
16
18
|
meta: {
|
package/dist/recommended.js
CHANGED
|
@@ -3,23 +3,40 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Forbid imports that violate Comity's canonical layering policy.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - core → adapters
|
|
11
|
-
* - adapters → adapters (except integration-adapter → technology-adapter)
|
|
6
|
+
* The permitted edges per source layer are the table `LAYER_PROFILES` below. They follow
|
|
7
|
+
* ADR-025 §5 (formal package dependencies: Application → Technology Adapters → Core
|
|
8
|
+
* Modules → composition → kernel → primitives; every edge points downward), together with
|
|
9
|
+
* `layering-policy.md` §2 and `architecture-validation.md` §4.1:
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - primitives → no internal package
|
|
12
|
+
* - kernel → primitives
|
|
13
|
+
* (kernel → composition is forbidden, ADR-025 §5 "Forbidden")
|
|
14
|
+
* - composition → primitives, kernel
|
|
15
|
+
* (composition → any Core Module is forbidden, ADR-025 hard
|
|
16
|
+
* invariant)
|
|
17
|
+
* - core → primitives, kernel, composition, core
|
|
18
|
+
* (Core → Core is further gated by ADR-008, not by this rule)
|
|
19
|
+
* - technology-adapter → primitives, kernel, composition, and the Core Module it
|
|
20
|
+
* implements
|
|
21
|
+
* - integration-adapter → primitives, kernel, core, technology-adapter
|
|
15
22
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Technology Adapters MAY import the core contract they implement (declared via
|
|
24
|
+
* `implements` metadata).
|
|
25
|
+
*
|
|
26
|
+
* A source package whose layer has no profile (for example `dev-tooling`), and a target
|
|
27
|
+
* that is not in the supplied classification, are outside this rule's scope. The
|
|
28
|
+
* classification is data supplied by the consumer (ADR-026); this rule never exempts a
|
|
29
|
+
* layer: there is no option to remove one.
|
|
30
|
+
*
|
|
31
|
+
* Allowed Core-to-Core edges are gated by ADR-008; this rule does NOT evaluate that
|
|
32
|
+
* register (it is enforced separately by the dependency rules / orchestrator). The
|
|
33
|
+
* sibling generator `@comity-dev/dependency-rules` expresses the same layer policy for
|
|
34
|
+
* dependency-cruiser; see `architecture-validation.md` §4.1 for how the two relate.
|
|
19
35
|
*
|
|
20
36
|
* Standard references:
|
|
37
|
+
* - ADR-025 §5 (formal package dependencies)
|
|
21
38
|
* - layering-policy.md §2 (canonical layering model)
|
|
22
|
-
* - architecture-validation.md §7
|
|
39
|
+
* - architecture-validation.md §4.1, §7
|
|
23
40
|
* - ADR-007 (adapter categories)
|
|
24
41
|
*/
|
|
25
42
|
type Rule = unknown;
|
|
@@ -3,23 +3,40 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Forbid imports that violate Comity's canonical layering policy.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - core → adapters
|
|
11
|
-
* - adapters → adapters (except integration-adapter → technology-adapter)
|
|
6
|
+
* The permitted edges per source layer are the table `LAYER_PROFILES` below. They follow
|
|
7
|
+
* ADR-025 §5 (formal package dependencies: Application → Technology Adapters → Core
|
|
8
|
+
* Modules → composition → kernel → primitives; every edge points downward), together with
|
|
9
|
+
* `layering-policy.md` §2 and `architecture-validation.md` §4.1:
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - primitives → no internal package
|
|
12
|
+
* - kernel → primitives
|
|
13
|
+
* (kernel → composition is forbidden, ADR-025 §5 "Forbidden")
|
|
14
|
+
* - composition → primitives, kernel
|
|
15
|
+
* (composition → any Core Module is forbidden, ADR-025 hard
|
|
16
|
+
* invariant)
|
|
17
|
+
* - core → primitives, kernel, composition, core
|
|
18
|
+
* (Core → Core is further gated by ADR-008, not by this rule)
|
|
19
|
+
* - technology-adapter → primitives, kernel, composition, and the Core Module it
|
|
20
|
+
* implements
|
|
21
|
+
* - integration-adapter → primitives, kernel, core, technology-adapter
|
|
15
22
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* Technology Adapters MAY import the core contract they implement (declared via
|
|
24
|
+
* `implements` metadata).
|
|
25
|
+
*
|
|
26
|
+
* A source package whose layer has no profile (for example `dev-tooling`), and a target
|
|
27
|
+
* that is not in the supplied classification, are outside this rule's scope. The
|
|
28
|
+
* classification is data supplied by the consumer (ADR-026); this rule never exempts a
|
|
29
|
+
* layer: there is no option to remove one.
|
|
30
|
+
*
|
|
31
|
+
* Allowed Core-to-Core edges are gated by ADR-008; this rule does NOT evaluate that
|
|
32
|
+
* register (it is enforced separately by the dependency rules / orchestrator). The
|
|
33
|
+
* sibling generator `@comity-dev/dependency-rules` expresses the same layer policy for
|
|
34
|
+
* dependency-cruiser; see `architecture-validation.md` §4.1 for how the two relate.
|
|
19
35
|
*
|
|
20
36
|
* Standard references:
|
|
37
|
+
* - ADR-025 §5 (formal package dependencies)
|
|
21
38
|
* - layering-policy.md §2 (canonical layering model)
|
|
22
|
-
* - architecture-validation.md §7
|
|
39
|
+
* - architecture-validation.md §4.1, §7
|
|
23
40
|
* - ADR-007 (adapter categories)
|
|
24
41
|
*/
|
|
25
42
|
const messages = {
|
|
@@ -29,9 +46,9 @@ const LAYER_PROFILES = {
|
|
|
29
46
|
primitives: { allowed: new Set() },
|
|
30
47
|
kernel: { allowed: new Set(["primitives"]) },
|
|
31
48
|
composition: { allowed: new Set(["primitives", "kernel"]) },
|
|
32
|
-
core: { allowed: new Set(["primitives", "kernel", "core"]) },
|
|
49
|
+
core: { allowed: new Set(["primitives", "kernel", "composition", "core"]) },
|
|
33
50
|
"technology-adapter": {
|
|
34
|
-
allowed: new Set(["primitives", "kernel"]),
|
|
51
|
+
allowed: new Set(["primitives", "kernel", "composition"]),
|
|
35
52
|
},
|
|
36
53
|
"integration-adapter": {
|
|
37
54
|
allowed: new Set(["primitives", "kernel", "core", "technology-adapter"]),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/require-internal-marker
|
|
3
|
+
*
|
|
4
|
+
* Contract B: production TypeScript sources under `packages/<name>/src/internal/`
|
|
5
|
+
* or `packages/<name>/src/lazy/` MUST declare an `@internal` JSDoc tag.
|
|
6
|
+
*
|
|
7
|
+
* Both accepted marker forms are equivalent: the inline form (the tag on the
|
|
8
|
+
* same line as the JSDoc opener) and the conventional multiline form (the tag
|
|
9
|
+
* on its own line inside the block).
|
|
10
|
+
*
|
|
11
|
+
* The marker must be an actual JSDoc tag, so it must occupy a JSDoc tag
|
|
12
|
+
* position — immediately after the block opener, or at the start of a line
|
|
13
|
+
* inside the block after the optional leading asterisk. Consequences:
|
|
14
|
+
*
|
|
15
|
+
* - A prose mention of `@internal` inside a JSDoc description is not a tag.
|
|
16
|
+
* - `@internalOnly` and `@internal-extra` are different tag names.
|
|
17
|
+
* - Line comments, ordinary block comments, and marker-like text inside
|
|
18
|
+
* strings, template literals, tagged templates, or regular-expression
|
|
19
|
+
* literals never satisfy the rule, because the parser only produces
|
|
20
|
+
* comment tokens for real comments.
|
|
21
|
+
*
|
|
22
|
+
* The requirement is file-level: a file satisfies it when it contains a valid
|
|
23
|
+
* `@internal` JSDoc block followed by code, since the marker annotates the
|
|
24
|
+
* file's declarations. A marker attached to any declaration — top-level,
|
|
25
|
+
* class member, or nested — satisfies it, and a trailing marker with no
|
|
26
|
+
* following code does not. The repository's internal sources attach the
|
|
27
|
+
* docblock to the declaration it documents, which is the narrowest reading
|
|
28
|
+
* consistent with the approved contract.
|
|
29
|
+
*
|
|
30
|
+
* Scope: production TypeScript sources under the two internal directories
|
|
31
|
+
* only. Test files, mocks, fixtures, declaration/generated output, build and
|
|
32
|
+
* coverage output, and dependency directories are excluded, matching the
|
|
33
|
+
* repository's established ESLint conventions.
|
|
34
|
+
*
|
|
35
|
+
* Traceability: the marker policy is an owner-approved contract decision. It
|
|
36
|
+
* is NOT defined by public-api.md §3.2, which forbids public `/internal` and
|
|
37
|
+
* `/lazy` sub-entrypoints. This rule replaces the retired Semgrep rule
|
|
38
|
+
* `comity-module-must-have-internal-marker`, which could not inspect real
|
|
39
|
+
* JSDoc syntax (Semgrep strips comments from structural patterns).
|
|
40
|
+
*/
|
|
41
|
+
type Rule = unknown;
|
|
42
|
+
declare const rule: Rule;
|
|
43
|
+
export default rule;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/require-internal-marker
|
|
3
|
+
*
|
|
4
|
+
* Contract B: production TypeScript sources under `packages/<name>/src/internal/`
|
|
5
|
+
* or `packages/<name>/src/lazy/` MUST declare an `@internal` JSDoc tag.
|
|
6
|
+
*
|
|
7
|
+
* Both accepted marker forms are equivalent: the inline form (the tag on the
|
|
8
|
+
* same line as the JSDoc opener) and the conventional multiline form (the tag
|
|
9
|
+
* on its own line inside the block).
|
|
10
|
+
*
|
|
11
|
+
* The marker must be an actual JSDoc tag, so it must occupy a JSDoc tag
|
|
12
|
+
* position — immediately after the block opener, or at the start of a line
|
|
13
|
+
* inside the block after the optional leading asterisk. Consequences:
|
|
14
|
+
*
|
|
15
|
+
* - A prose mention of `@internal` inside a JSDoc description is not a tag.
|
|
16
|
+
* - `@internalOnly` and `@internal-extra` are different tag names.
|
|
17
|
+
* - Line comments, ordinary block comments, and marker-like text inside
|
|
18
|
+
* strings, template literals, tagged templates, or regular-expression
|
|
19
|
+
* literals never satisfy the rule, because the parser only produces
|
|
20
|
+
* comment tokens for real comments.
|
|
21
|
+
*
|
|
22
|
+
* The requirement is file-level: a file satisfies it when it contains a valid
|
|
23
|
+
* `@internal` JSDoc block followed by code, since the marker annotates the
|
|
24
|
+
* file's declarations. A marker attached to any declaration — top-level,
|
|
25
|
+
* class member, or nested — satisfies it, and a trailing marker with no
|
|
26
|
+
* following code does not. The repository's internal sources attach the
|
|
27
|
+
* docblock to the declaration it documents, which is the narrowest reading
|
|
28
|
+
* consistent with the approved contract.
|
|
29
|
+
*
|
|
30
|
+
* Scope: production TypeScript sources under the two internal directories
|
|
31
|
+
* only. Test files, mocks, fixtures, declaration/generated output, build and
|
|
32
|
+
* coverage output, and dependency directories are excluded, matching the
|
|
33
|
+
* repository's established ESLint conventions.
|
|
34
|
+
*
|
|
35
|
+
* Traceability: the marker policy is an owner-approved contract decision. It
|
|
36
|
+
* is NOT defined by public-api.md §3.2, which forbids public `/internal` and
|
|
37
|
+
* `/lazy` sub-entrypoints. This rule replaces the retired Semgrep rule
|
|
38
|
+
* `comity-module-must-have-internal-marker`, which could not inspect real
|
|
39
|
+
* JSDoc syntax (Semgrep strips comments from structural patterns).
|
|
40
|
+
*/
|
|
41
|
+
const messages = {
|
|
42
|
+
missing: "Production sources under `src/internal/` or `src/lazy/` MUST declare an `@internal` JSDoc tag (inline or multiline).",
|
|
43
|
+
};
|
|
44
|
+
/** File extensions treated as production TypeScript sources. */
|
|
45
|
+
const TYPESCRIPT_FILE = /\.(?:ts|tsx|mts|cts)$/;
|
|
46
|
+
/** Test files: excluded by the validator ESLint engine and the consumer config. */
|
|
47
|
+
const TEST_FILE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
48
|
+
/** Declaration output is generated by `tsc`; the consumer config ignores it. */
|
|
49
|
+
const DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
50
|
+
/** Generated sources are excluded by the consumer's ESLint ignore list. */
|
|
51
|
+
const GENERATED_FILE = /\.generated\./;
|
|
52
|
+
/**
|
|
53
|
+
* Path segments excluded by repository convention: dependency directories,
|
|
54
|
+
* build/coverage output, and test/mock/fixture trees (the consumer keeps real
|
|
55
|
+
* `src/__mocks__` directories, and the retired Semgrep rule excluded
|
|
56
|
+
* `__fixtures__`).
|
|
57
|
+
*/
|
|
58
|
+
const EXCLUDED_SEGMENTS = new Set([
|
|
59
|
+
"node_modules",
|
|
60
|
+
"dist",
|
|
61
|
+
"build",
|
|
62
|
+
"out",
|
|
63
|
+
"coverage",
|
|
64
|
+
"__tests__",
|
|
65
|
+
"__fixtures__",
|
|
66
|
+
"__mocks__",
|
|
67
|
+
]);
|
|
68
|
+
/** `packages/<name>/src/internal/` or `packages/<name>/src/lazy/` */
|
|
69
|
+
const SCOPED_PATH = /(?:^|\/)packages\/[^/]+\/src\/(?:internal|lazy)\//;
|
|
70
|
+
function normalizeFilename(filename) {
|
|
71
|
+
return filename.replace(/\\/g, "/");
|
|
72
|
+
}
|
|
73
|
+
function isScopedProductionFile(filename) {
|
|
74
|
+
if (!filename)
|
|
75
|
+
return false;
|
|
76
|
+
const normalized = normalizeFilename(filename);
|
|
77
|
+
if (!TYPESCRIPT_FILE.test(normalized))
|
|
78
|
+
return false;
|
|
79
|
+
if (TEST_FILE.test(normalized))
|
|
80
|
+
return false;
|
|
81
|
+
if (DECLARATION_FILE.test(normalized))
|
|
82
|
+
return false;
|
|
83
|
+
if (GENERATED_FILE.test(normalized))
|
|
84
|
+
return false;
|
|
85
|
+
if (normalized.split("/").some((segment) => EXCLUDED_SEGMENTS.has(segment))) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return SCOPED_PATH.test(normalized);
|
|
89
|
+
}
|
|
90
|
+
/** A JSDoc tag must not be followed by more tag-name characters. */
|
|
91
|
+
const INTERNAL_TAG = /^[ \t]*@internal(?![A-Za-z0-9_-])/;
|
|
92
|
+
/**
|
|
93
|
+
* Whether a JSDoc block comment body declares the `@internal` tag.
|
|
94
|
+
*
|
|
95
|
+
* @param value - The comment value. A JSDoc block starts with `*`.
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* The tag must begin a line inside the block (after the optional leading
|
|
99
|
+
* asterisk) or immediately follow the block opener, which is where JSDoc
|
|
100
|
+
* allows tags. Prose that merely mentions `@internal` is therefore not a tag.
|
|
101
|
+
*/
|
|
102
|
+
function declaresInternalTag(value) {
|
|
103
|
+
const lines = value.split(/\r?\n/);
|
|
104
|
+
for (let index = 0; index < lines.length; index++) {
|
|
105
|
+
const raw = lines[index] ?? "";
|
|
106
|
+
const content = index === 0 ? raw.slice(1) : raw.replace(/^[ \t]*/, "");
|
|
107
|
+
if (index > 0 && content.startsWith("*")) {
|
|
108
|
+
if (INTERNAL_TAG.test(content.slice(1)))
|
|
109
|
+
return true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (INTERNAL_TAG.test(content))
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const rule = {
|
|
118
|
+
meta: {
|
|
119
|
+
type: "problem",
|
|
120
|
+
docs: {
|
|
121
|
+
description: "Require an `@internal` JSDoc marker on production sources under `src/internal/` and `src/lazy/`.",
|
|
122
|
+
category: "Comity Architecture",
|
|
123
|
+
recommended: true,
|
|
124
|
+
},
|
|
125
|
+
schema: [],
|
|
126
|
+
messages,
|
|
127
|
+
},
|
|
128
|
+
create(context) {
|
|
129
|
+
const filename = context.filename ?? context.getFilename();
|
|
130
|
+
if (!isScopedProductionFile(String(filename ?? "")))
|
|
131
|
+
return {};
|
|
132
|
+
return {
|
|
133
|
+
Program(node) {
|
|
134
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
135
|
+
const lastToken = sourceCode.getLastToken(node);
|
|
136
|
+
for (const comment of sourceCode.getAllComments()) {
|
|
137
|
+
if (comment.type !== "Block")
|
|
138
|
+
continue;
|
|
139
|
+
const value = String(comment.value ?? "");
|
|
140
|
+
// JSDoc block comments start with `*`; `/* ... */` does not.
|
|
141
|
+
if (!value.startsWith("*"))
|
|
142
|
+
continue;
|
|
143
|
+
if (!declaresInternalTag(value))
|
|
144
|
+
continue;
|
|
145
|
+
// The marker must annotate following code, not trail the file.
|
|
146
|
+
if (lastToken && lastToken.range[0] > comment.range[1])
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
context.report({ node, messageId: "missing" });
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
export default rule;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@comity-dev/eslint-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Reusable ESLint rules for Comity source-code policy. Development tooling.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"typescript": "^5.9.3"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
43
43
|
"test": "vitest run"
|
|
44
44
|
}
|
|
45
45
|
}
|