@comity-dev/eslint-plugin 0.1.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/plugin.d.ts +28 -0
- package/dist/plugin.js +41 -0
- package/dist/recommended.d.ts +9 -0
- package/dist/recommended.js +10 -0
- package/dist/rules/no-crypto-in-core.d.ts +14 -0
- package/dist/rules/no-crypto-in-core.js +123 -0
- package/dist/rules/no-date-in-core.d.ts +19 -0
- package/dist/rules/no-date-in-core.js +163 -0
- package/dist/rules/no-forbidden-deep-import.d.ts +20 -0
- package/dist/rules/no-forbidden-deep-import.js +72 -0
- package/dist/rules/no-forbidden-subpath-import.d.ts +13 -0
- package/dist/rules/no-forbidden-subpath-import.js +79 -0
- package/dist/rules/no-layer-violation.d.ts +27 -0
- package/dist/rules/no-layer-violation.js +154 -0
- package/dist/rules/no-render-core-http-runtime.d.ts +18 -0
- package/dist/rules/no-render-core-http-runtime.js +100 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Filippo Bovo and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# @comity-dev/eslint-plugin
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Reusable ESLint rules for Comity-wide source-code policy. This package owns the source-code lint policy that every Comity repository MUST apply consistently.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
Six rules, each addressing a Comity-wide concern:
|
|
10
|
+
|
|
11
|
+
| Rule | Concern |
|
|
12
|
+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `no-forbidden-deep-import` | Disallow deep imports into another package's internals (`@comity/x/src/foo`, `@comity/x/internal/*`, `@comity/x/dist/*`, `@comity/x/lazy/*`). |
|
|
14
|
+
| `no-layer-violation` | Forbid imports that violate Comity's canonical layering model. |
|
|
15
|
+
| `no-date-in-core` | Forbid `Date` in Core Modules (domain must be deterministic). |
|
|
16
|
+
| `no-crypto-in-core` | Forbid `crypto` in Core Modules (domain must be deterministic). |
|
|
17
|
+
| `no-forbidden-subpath-import` | Disallow imports from forbidden package subpaths (`utils`, `helpers`, `shared`, `internal`, `lazy`). |
|
|
18
|
+
| `no-render-core-http-runtime` | Forbid render Core Modules from depending on HTTP runtime. |
|
|
19
|
+
|
|
20
|
+
## Ownership
|
|
21
|
+
|
|
22
|
+
Owned by `comity-development`. Consumed by Comity repositories (Community, Enterprise) via ESLint flat config.
|
|
23
|
+
|
|
24
|
+
## Public API
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// Default export: the ESLint plugin (loaded into flat-config)
|
|
28
|
+
import comityPlugin from "@comity-dev/eslint-plugin";
|
|
29
|
+
|
|
30
|
+
// Root exports: stable identifier list and the recommended rule set
|
|
31
|
+
import { recommended, RULE_IDS } from "@comity-dev/eslint-plugin";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Relationship to Comity Standards
|
|
35
|
+
|
|
36
|
+
- `architecture-validation.md` §4 — source-code lint policy.
|
|
37
|
+
- `public-api.md` §3.2 — forbidden subpath imports.
|
|
38
|
+
- `layering-policy.md` §8 — rendering core MUST NOT depend on HTTP runtime.
|
|
39
|
+
- `coding.md` — general style policy.
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm build
|
|
45
|
+
pnpm test
|
|
46
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
|
|
3
|
+
*
|
|
4
|
+
* This package owns the canonical ESLint rules for Comity. Repositories
|
|
5
|
+
* consume these rules via the shared orchestrator; they MUST NOT redefine
|
|
6
|
+
* source-code policy.
|
|
7
|
+
*
|
|
8
|
+
* Standard references:
|
|
9
|
+
* - architecture-validation.md §4
|
|
10
|
+
* - public-api.md §3.2 (forbidden subpath imports)
|
|
11
|
+
* - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
|
|
12
|
+
* - coding.md (general style policy)
|
|
13
|
+
*
|
|
14
|
+
* Public surface (intentional):
|
|
15
|
+
* - default export: the ESLint plugin (loaded into flat-config)
|
|
16
|
+
* - `recommended`: the canonical Comity rule set
|
|
17
|
+
* - `RULE_IDS`: stable list of rule identifiers
|
|
18
|
+
*/
|
|
19
|
+
export { plugin as default, RULE_IDS } from "./plugin.js";
|
|
20
|
+
export { recommended } from "./recommended.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
|
|
3
|
+
*
|
|
4
|
+
* This package owns the canonical ESLint rules for Comity. Repositories
|
|
5
|
+
* consume these rules via the shared orchestrator; they MUST NOT redefine
|
|
6
|
+
* source-code policy.
|
|
7
|
+
*
|
|
8
|
+
* Standard references:
|
|
9
|
+
* - architecture-validation.md §4
|
|
10
|
+
* - public-api.md §3.2 (forbidden subpath imports)
|
|
11
|
+
* - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
|
|
12
|
+
* - coding.md (general style policy)
|
|
13
|
+
*
|
|
14
|
+
* Public surface (intentional):
|
|
15
|
+
* - default export: the ESLint plugin (loaded into flat-config)
|
|
16
|
+
* - `recommended`: the canonical Comity rule set
|
|
17
|
+
* - `RULE_IDS`: stable list of rule identifiers
|
|
18
|
+
*/
|
|
19
|
+
export { plugin as default, RULE_IDS } from "./plugin.js";
|
|
20
|
+
export { recommended } from "./recommended.js";
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
|
|
3
|
+
*
|
|
4
|
+
* This package owns the canonical ESLint rules for Comity. Repositories
|
|
5
|
+
* consume these rules via the shared orchestrator; they MUST NOT redefine
|
|
6
|
+
* source-code policy.
|
|
7
|
+
*
|
|
8
|
+
* Standard references:
|
|
9
|
+
* - architecture-validation.md §4
|
|
10
|
+
* - public-api.md §3.2 (forbidden subpath imports)
|
|
11
|
+
* - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
|
|
12
|
+
* - coding.md (general style policy)
|
|
13
|
+
*
|
|
14
|
+
* Public surface (intentional):
|
|
15
|
+
* - default export: the ESLint plugin (loaded into flat-config)
|
|
16
|
+
* - `recommended`: the canonical Comity rule set
|
|
17
|
+
* - `RULE_IDS`: stable list of rule identifiers
|
|
18
|
+
*/
|
|
19
|
+
export interface BuiltinRule {
|
|
20
|
+
/** The stable identifier of the rule */
|
|
21
|
+
name: string;
|
|
22
|
+
/** The rule implementation */
|
|
23
|
+
rule: unknown;
|
|
24
|
+
}
|
|
25
|
+
export declare const BUILTIN_RULES: BuiltinRule[];
|
|
26
|
+
export declare const plugin: import("eslint").ESLint.Plugin;
|
|
27
|
+
/** Stable list of rule identifiers exported by this plugin. */
|
|
28
|
+
export declare const RULE_IDS: string[];
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/eslint-plugin — Reusable ESLint rules for Comity source-code policy.
|
|
3
|
+
*
|
|
4
|
+
* This package owns the canonical ESLint rules for Comity. Repositories
|
|
5
|
+
* consume these rules via the shared orchestrator; they MUST NOT redefine
|
|
6
|
+
* source-code policy.
|
|
7
|
+
*
|
|
8
|
+
* Standard references:
|
|
9
|
+
* - architecture-validation.md §4
|
|
10
|
+
* - public-api.md §3.2 (forbidden subpath imports)
|
|
11
|
+
* - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
|
|
12
|
+
* - coding.md (general style policy)
|
|
13
|
+
*
|
|
14
|
+
* Public surface (intentional):
|
|
15
|
+
* - default export: the ESLint plugin (loaded into flat-config)
|
|
16
|
+
* - `recommended`: the canonical Comity rule set
|
|
17
|
+
* - `RULE_IDS`: stable list of rule identifiers
|
|
18
|
+
*/
|
|
19
|
+
import noCryptoInCore from "./rules/no-crypto-in-core.js";
|
|
20
|
+
import noDateInCore from "./rules/no-date-in-core.js";
|
|
21
|
+
import noForbiddenDeepImport from "./rules/no-forbidden-deep-import.js";
|
|
22
|
+
import noForbiddenSubpathImport from "./rules/no-forbidden-subpath-import.js";
|
|
23
|
+
import noLayerViolation from "./rules/no-layer-violation.js";
|
|
24
|
+
import noRenderCoreHttpRuntime from "./rules/no-render-core-http-runtime.js";
|
|
25
|
+
export const BUILTIN_RULES = [
|
|
26
|
+
{ name: "no-forbidden-deep-import", rule: noForbiddenDeepImport },
|
|
27
|
+
{ name: "no-layer-violation", rule: noLayerViolation },
|
|
28
|
+
{ name: "no-date-in-core", rule: noDateInCore },
|
|
29
|
+
{ name: "no-crypto-in-core", rule: noCryptoInCore },
|
|
30
|
+
{ name: "no-forbidden-subpath-import", rule: noForbiddenSubpathImport },
|
|
31
|
+
{ name: "no-render-core-http-runtime", rule: noRenderCoreHttpRuntime },
|
|
32
|
+
];
|
|
33
|
+
export const plugin = {
|
|
34
|
+
meta: {
|
|
35
|
+
name: "@comity-dev/eslint-plugin",
|
|
36
|
+
version: "0.1.0",
|
|
37
|
+
},
|
|
38
|
+
rules: Object.fromEntries(BUILTIN_RULES.map((r) => [r.name, r.rule])),
|
|
39
|
+
};
|
|
40
|
+
/** Stable list of rule identifiers exported by this plugin. */
|
|
41
|
+
export const RULE_IDS = BUILTIN_RULES.map((r) => r.name);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommended Comity ESLint rule set.
|
|
3
|
+
*
|
|
4
|
+
* The "recommended" preset bundles the canonical Comity rules with their
|
|
5
|
+
* default severities. Repositories MAY override severities; they MUST NOT
|
|
6
|
+
* disable rules silently.
|
|
7
|
+
*/
|
|
8
|
+
import type { Linter } from "eslint";
|
|
9
|
+
export declare const recommended: Linter.Config;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const recommended = {
|
|
2
|
+
rules: {
|
|
3
|
+
"@comity-dev/no-forbidden-deep-import": "error",
|
|
4
|
+
"@comity-dev/no-layer-violation": "error",
|
|
5
|
+
"@comity-dev/no-date-in-core": "error",
|
|
6
|
+
"@comity-dev/no-crypto-in-core": "error",
|
|
7
|
+
"@comity-dev/no-forbidden-subpath-import": "error",
|
|
8
|
+
"@comity-dev/no-render-core-http-runtime": "error",
|
|
9
|
+
},
|
|
10
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-crypto-in-core
|
|
3
|
+
*
|
|
4
|
+
* Forbid Node's `crypto` and WebCrypto (`globalThis.crypto`) modules in Core
|
|
5
|
+
* packages. Core must be deterministic and side-effect free; cryptography
|
|
6
|
+
* belongs in Adapters that own runtime concerns.
|
|
7
|
+
*
|
|
8
|
+
* Standard references:
|
|
9
|
+
* - layering-policy.md §2.2 (Core boundaries)
|
|
10
|
+
* - adapters.md
|
|
11
|
+
*/
|
|
12
|
+
type Rule = unknown;
|
|
13
|
+
declare const rule: Rule;
|
|
14
|
+
export default rule;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-crypto-in-core
|
|
3
|
+
*
|
|
4
|
+
* Forbid Node's `crypto` and WebCrypto (`globalThis.crypto`) modules in Core
|
|
5
|
+
* packages. Core must be deterministic and side-effect free; cryptography
|
|
6
|
+
* belongs in Adapters that own runtime concerns.
|
|
7
|
+
*
|
|
8
|
+
* Standard references:
|
|
9
|
+
* - layering-policy.md §2.2 (Core boundaries)
|
|
10
|
+
* - adapters.md
|
|
11
|
+
*/
|
|
12
|
+
const FORBIDDEN_SPECIFIERS = new Set(["crypto", "node:crypto", "webcrypto"]);
|
|
13
|
+
const messages = {
|
|
14
|
+
forbiddenImport: "Importing '{{specifier}}' is forbidden in Core (layering-policy.md §2.2). Use a Technology Adapter instead.",
|
|
15
|
+
forbiddenGlobal: "Accessing the `crypto` global is forbidden in Core (layering-policy.md §2.2). Use a Technology Adapter instead.",
|
|
16
|
+
};
|
|
17
|
+
function isInCorePackage(filename) {
|
|
18
|
+
return (filename.includes("/packages/") &&
|
|
19
|
+
(filename.includes("/primitives/") ||
|
|
20
|
+
filename.includes("/kernel/") ||
|
|
21
|
+
filename.includes("/composition/") ||
|
|
22
|
+
filename.includes("/http/") ||
|
|
23
|
+
filename.includes("/router/") ||
|
|
24
|
+
filename.includes("/html/") ||
|
|
25
|
+
filename.includes("/hydration/") ||
|
|
26
|
+
filename.includes("/seo/") ||
|
|
27
|
+
filename.includes("/content/") ||
|
|
28
|
+
filename.includes("/media/") ||
|
|
29
|
+
filename.includes("/search/") ||
|
|
30
|
+
filename.includes("/cache/") ||
|
|
31
|
+
filename.includes("/auth/") ||
|
|
32
|
+
filename.includes("/catalog/") ||
|
|
33
|
+
filename.includes("/customer/") ||
|
|
34
|
+
filename.includes("/inventory/") ||
|
|
35
|
+
filename.includes("/order/") ||
|
|
36
|
+
filename.includes("/payment/") ||
|
|
37
|
+
filename.includes("/pricing/") ||
|
|
38
|
+
filename.includes("/storefront/") ||
|
|
39
|
+
filename.includes("/taxonomy/") ||
|
|
40
|
+
filename.includes("/validation/") ||
|
|
41
|
+
filename.includes("/address/") ||
|
|
42
|
+
filename.includes("/geography/") ||
|
|
43
|
+
filename.includes("/identity/") ||
|
|
44
|
+
filename.includes("/organization/")));
|
|
45
|
+
}
|
|
46
|
+
const rule = {
|
|
47
|
+
meta: {
|
|
48
|
+
type: "problem",
|
|
49
|
+
docs: {
|
|
50
|
+
description: "Forbid `crypto` imports in Core packages — cryptography belongs in Adapters.",
|
|
51
|
+
category: "Comity Architecture",
|
|
52
|
+
recommended: true,
|
|
53
|
+
},
|
|
54
|
+
schema: [
|
|
55
|
+
{
|
|
56
|
+
type: "object",
|
|
57
|
+
properties: {
|
|
58
|
+
sourceName: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Name of the source package (e.g., '@comity/auth'). Used to scope the rule.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
messages,
|
|
67
|
+
},
|
|
68
|
+
create(context) {
|
|
69
|
+
const opts = (context.options[0] ?? {});
|
|
70
|
+
const sourceName = opts.sourceName ?? null;
|
|
71
|
+
// If sourceName provided, use it for classification; otherwise fall back to filename
|
|
72
|
+
const inCore = sourceName
|
|
73
|
+
? sourceName.startsWith("@comity/") &&
|
|
74
|
+
!["@comity/cli", "@comity/graphql-builder"].includes(sourceName)
|
|
75
|
+
: isInCorePackage(context.filename ?? context.getFilename());
|
|
76
|
+
// Disable this rule outside Core packages
|
|
77
|
+
if (!inCore) {
|
|
78
|
+
return {};
|
|
79
|
+
}
|
|
80
|
+
function checkSpecifier(value) {
|
|
81
|
+
if (!value)
|
|
82
|
+
return false;
|
|
83
|
+
return FORBIDDEN_SPECIFIERS.has(value);
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
ImportDeclaration(node) {
|
|
87
|
+
if (checkSpecifier(node.source.value)) {
|
|
88
|
+
context.report({
|
|
89
|
+
node,
|
|
90
|
+
messageId: "forbiddenImport",
|
|
91
|
+
data: { specifier: node.source.value },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
ImportExpression(node) {
|
|
96
|
+
// dynamic import()
|
|
97
|
+
if (node.source.type === "Literal" &&
|
|
98
|
+
typeof node.source.value === "string" &&
|
|
99
|
+
checkSpecifier(node.source.value)) {
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
messageId: "forbiddenImport",
|
|
103
|
+
data: { specifier: node.source.value },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
Identifier(node) {
|
|
108
|
+
if (node.name !== "crypto")
|
|
109
|
+
return;
|
|
110
|
+
// Top-level access of the `crypto` global
|
|
111
|
+
const parent = node.parent;
|
|
112
|
+
if (!parent)
|
|
113
|
+
return;
|
|
114
|
+
if (parent.type === "ExpressionStatement" ||
|
|
115
|
+
parent.type === "CallExpression" ||
|
|
116
|
+
parent.type === "MemberExpression") {
|
|
117
|
+
context.report({ node, messageId: "forbiddenGlobal" });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
export default rule;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-date-in-core
|
|
3
|
+
*
|
|
4
|
+
* Forbid `Date`, `performance.now`, and `Math.random` access in Core and
|
|
5
|
+
* Kernel packages — they introduce non-deterministic behavior that breaks
|
|
6
|
+
* the Comity reproducibility contract.
|
|
7
|
+
*
|
|
8
|
+
* Legitimate exceptions:
|
|
9
|
+
* - Telemetry/timing measurements using `performance.now()` (observational, not domain logic)
|
|
10
|
+
* - Timestamp generation using `Date.now()` or `Date` constructor for logging/metadata
|
|
11
|
+
* - Time primitives in `@comity/primitives/time` (explicitly allowed)
|
|
12
|
+
*
|
|
13
|
+
* Standard references:
|
|
14
|
+
* - layering-policy.md §6 (determinism)
|
|
15
|
+
* - domain-modeling.md
|
|
16
|
+
*/
|
|
17
|
+
type Rule = unknown;
|
|
18
|
+
declare const rule: Rule;
|
|
19
|
+
export default rule;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-date-in-core
|
|
3
|
+
*
|
|
4
|
+
* Forbid `Date`, `performance.now`, and `Math.random` access in Core and
|
|
5
|
+
* Kernel packages — they introduce non-deterministic behavior that breaks
|
|
6
|
+
* the Comity reproducibility contract.
|
|
7
|
+
*
|
|
8
|
+
* Legitimate exceptions:
|
|
9
|
+
* - Telemetry/timing measurements using `performance.now()` (observational, not domain logic)
|
|
10
|
+
* - Timestamp generation using `Date.now()` or `Date` constructor for logging/metadata
|
|
11
|
+
* - Time primitives in `@comity/primitives/time` (explicitly allowed)
|
|
12
|
+
*
|
|
13
|
+
* Standard references:
|
|
14
|
+
* - layering-policy.md §6 (determinism)
|
|
15
|
+
* - domain-modeling.md
|
|
16
|
+
*/
|
|
17
|
+
const FORBIDDEN_GLOBALS = new Set(["Date", "performance"]);
|
|
18
|
+
const FORBIDDEN_PROPERTIES = {
|
|
19
|
+
Math: ["random"],
|
|
20
|
+
};
|
|
21
|
+
// Package names that are explicitly allowed to use Date/performance for telemetry
|
|
22
|
+
const TELEMETRY_ALLOWLIST = new Set([
|
|
23
|
+
"@comity/primitives", // time/instant primitives
|
|
24
|
+
"@comity/http", // facade timing
|
|
25
|
+
"@comity/html", // renderer pipeline timing
|
|
26
|
+
"@comity/hydration", // browser capability implementations (createBrowserHydrationCapabilities, createNoopHydrationCapabilities)
|
|
27
|
+
]);
|
|
28
|
+
// Allowlist for specific property access patterns that are legitimate telemetry
|
|
29
|
+
const TELEMETRY_PROPERTY_ALLOWLIST = new Set([
|
|
30
|
+
"performance.now",
|
|
31
|
+
"Date.now",
|
|
32
|
+
]);
|
|
33
|
+
const messages = {
|
|
34
|
+
forbiddenGlobal: "Non-deterministic global '{{name}}' is forbidden in Core / Kernel (layering-policy.md §6). Use telemetry allowlist or primitives/time for timing.",
|
|
35
|
+
forbiddenProperty: "Non-deterministic '{{object}}.{{property}}' is forbidden in Core / Kernel (layering-policy.md §6).",
|
|
36
|
+
};
|
|
37
|
+
function isInCoreOrKernelPackage(filename) {
|
|
38
|
+
return (filename.includes("/packages/") &&
|
|
39
|
+
(filename.includes("/primitives/") ||
|
|
40
|
+
filename.includes("/kernel/") ||
|
|
41
|
+
filename.includes("/composition/") ||
|
|
42
|
+
filename.includes("/http/") ||
|
|
43
|
+
filename.includes("/router/") ||
|
|
44
|
+
filename.includes("/html/") ||
|
|
45
|
+
filename.includes("/hydration/") ||
|
|
46
|
+
filename.includes("/seo/") ||
|
|
47
|
+
filename.includes("/content/") ||
|
|
48
|
+
filename.includes("/media/") ||
|
|
49
|
+
filename.includes("/search/")));
|
|
50
|
+
}
|
|
51
|
+
function isTelemetryAllowedPackage(filename) {
|
|
52
|
+
return Array.from(TELEMETRY_ALLOWLIST).some((pkg) => filename.includes(`/packages/${pkg.replace("@comity/", "")}/`) || filename.includes(`/community/packages/${pkg.replace("@comity/", "")}/`));
|
|
53
|
+
}
|
|
54
|
+
function isTelemetryPropertyAccess(objectName, propertyName) {
|
|
55
|
+
return TELEMETRY_PROPERTY_ALLOWLIST.has(`${objectName}.${propertyName}`);
|
|
56
|
+
}
|
|
57
|
+
const rule = {
|
|
58
|
+
meta: {
|
|
59
|
+
type: "problem",
|
|
60
|
+
docs: {
|
|
61
|
+
description: "Forbid Date / performance.now / Math.random in Core and Kernel packages.",
|
|
62
|
+
category: "Comity Architecture",
|
|
63
|
+
recommended: true,
|
|
64
|
+
},
|
|
65
|
+
schema: [
|
|
66
|
+
{
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
sourceName: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "Name of the source package (e.g., '@comity/http'). Used to scope the rule.",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: false,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
messages,
|
|
78
|
+
},
|
|
79
|
+
create(context) {
|
|
80
|
+
const opts = (context.options[0] ?? {});
|
|
81
|
+
const sourceName = opts.sourceName ?? null;
|
|
82
|
+
// If sourceName provided, use it for classification; otherwise fall back to filename
|
|
83
|
+
const inCoreOrKernel = sourceName
|
|
84
|
+
? sourceName.startsWith("@comity/") &&
|
|
85
|
+
!["@comity/cli", "@comity/graphql-builder"].includes(sourceName) // these are not core/kernel
|
|
86
|
+
: isInCoreOrKernelPackage(context.filename ?? context.getFilename());
|
|
87
|
+
// Disable this rule outside Core/Kernel packages
|
|
88
|
+
if (!inCoreOrKernel) {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
// Check if this package is allowed to use telemetry patterns
|
|
92
|
+
const allowTelemetry = sourceName
|
|
93
|
+
? TELEMETRY_ALLOWLIST.has(sourceName)
|
|
94
|
+
: isTelemetryAllowedPackage(context.filename ?? context.getFilename());
|
|
95
|
+
return {
|
|
96
|
+
Identifier(node) {
|
|
97
|
+
// Detect globals like `Date` and `performance`
|
|
98
|
+
if ((node.name === "Date" || node.name === "performance") &&
|
|
99
|
+
// Only top-level usage (not as property of another object)
|
|
100
|
+
node.parent &&
|
|
101
|
+
(node.parent.type === "ExpressionStatement" ||
|
|
102
|
+
node.parent.type === "VariableDeclarator" ||
|
|
103
|
+
node.parent.type === "CallExpression" ||
|
|
104
|
+
node.parent.type === "NewExpression" ||
|
|
105
|
+
node.parent.type === "MemberExpression")) {
|
|
106
|
+
// Allow telemetry patterns in allowlisted packages
|
|
107
|
+
if (allowTelemetry && isTelemetryPropertyAccess(node.parent.object?.name ?? "", node.name)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Allow Date.now() and performance.now() in telemetry allowlisted packages
|
|
111
|
+
if (allowTelemetry &&
|
|
112
|
+
((node.name === "Date" && node.parent.type === "MemberExpression" && node.parent.property?.name === "now") ||
|
|
113
|
+
(node.name === "performance" && node.parent.type === "MemberExpression" && node.parent.property?.name === "now"))) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Also handle optional chaining: window?.performance?.now()
|
|
117
|
+
// Walk up the ancestor chain to find a MemberExpression with property 'now'
|
|
118
|
+
if (allowTelemetry && (node.name === "Date" || node.name === "performance")) {
|
|
119
|
+
let current = node.parent;
|
|
120
|
+
while (current) {
|
|
121
|
+
if (current.type === "MemberExpression" && current.property?.name === "now") {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (current.type === "ChainExpression") {
|
|
125
|
+
current = current.expression;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
current = current.parent;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (FORBIDDEN_GLOBALS.has(node.name)) {
|
|
132
|
+
context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: "forbiddenGlobal",
|
|
135
|
+
data: { name: node.name },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Detect property access like Math.random
|
|
140
|
+
if (node.parent &&
|
|
141
|
+
node.parent.type === "MemberExpression" &&
|
|
142
|
+
node.parent.property === node) {
|
|
143
|
+
const obj = node.parent.object;
|
|
144
|
+
if (obj.type === "Identifier") {
|
|
145
|
+
const allowed = FORBIDDEN_PROPERTIES[obj.name];
|
|
146
|
+
if (allowed && allowed.includes(node.name)) {
|
|
147
|
+
// Allow Math.random in primitives/time (explicit time primitive)
|
|
148
|
+
if (allowTelemetry && sourceName === "@comity/primitives") {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
context.report({
|
|
152
|
+
node,
|
|
153
|
+
messageId: "forbiddenProperty",
|
|
154
|
+
data: { object: obj.name, property: node.name },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
export default rule;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-forbidden-deep-import
|
|
3
|
+
*
|
|
4
|
+
* Forbid deep imports into another `@comity/*` package's internals.
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* import { foo } from "@comity/pricing/src/foo" // ❌
|
|
8
|
+
* import { foo } from "@comity/pricing/dist/foo.js" // ❌
|
|
9
|
+
* import { foo } from "@comity/pricing/internal/x" // ❌
|
|
10
|
+
* import { foo } from "@comity/pricing/lazy/x" // ❌
|
|
11
|
+
* import { foo } from "@comity/pricing" // ✅
|
|
12
|
+
* import { foo } from "@comity/pricing/exports/foo" // ✅
|
|
13
|
+
*
|
|
14
|
+
* Standard references:
|
|
15
|
+
* - public-api.md §3.2
|
|
16
|
+
* - architecture-validation.md §4.1
|
|
17
|
+
*/
|
|
18
|
+
type Rule = unknown;
|
|
19
|
+
declare const rule: Rule;
|
|
20
|
+
export default rule;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-forbidden-deep-import
|
|
3
|
+
*
|
|
4
|
+
* Forbid deep imports into another `@comity/*` package's internals.
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* import { foo } from "@comity/pricing/src/foo" // ❌
|
|
8
|
+
* import { foo } from "@comity/pricing/dist/foo.js" // ❌
|
|
9
|
+
* import { foo } from "@comity/pricing/internal/x" // ❌
|
|
10
|
+
* import { foo } from "@comity/pricing/lazy/x" // ❌
|
|
11
|
+
* import { foo } from "@comity/pricing" // ✅
|
|
12
|
+
* import { foo } from "@comity/pricing/exports/foo" // ✅
|
|
13
|
+
*
|
|
14
|
+
* Standard references:
|
|
15
|
+
* - public-api.md §3.2
|
|
16
|
+
* - architecture-validation.md §4.1
|
|
17
|
+
*/
|
|
18
|
+
const FORBIDDEN_PATTERNS = [
|
|
19
|
+
/^@comity\/[^/]+\/src\//,
|
|
20
|
+
/^@comity\/[^/]+\/dist\//,
|
|
21
|
+
/^@comity\/[^/]+\/internal/,
|
|
22
|
+
/^@comity\/[^/]+\/lazy/,
|
|
23
|
+
];
|
|
24
|
+
function isForbidden(specifier) {
|
|
25
|
+
return FORBIDDEN_PATTERNS.some((re) => re.test(specifier));
|
|
26
|
+
}
|
|
27
|
+
const messages = {
|
|
28
|
+
forbidden: "Deep imports into another package's internals are forbidden (public-api.md §3.2). Import only from declared public subpaths.",
|
|
29
|
+
};
|
|
30
|
+
const rule = {
|
|
31
|
+
meta: {
|
|
32
|
+
type: "problem",
|
|
33
|
+
docs: {
|
|
34
|
+
description: "Forbid deep imports into other `@comity/*` packages' internals (src/, dist/, internal/, lazy/).",
|
|
35
|
+
category: "Comity Architecture",
|
|
36
|
+
recommended: true,
|
|
37
|
+
},
|
|
38
|
+
schema: [],
|
|
39
|
+
messages,
|
|
40
|
+
},
|
|
41
|
+
create(context) {
|
|
42
|
+
return {
|
|
43
|
+
ImportDeclaration(node) {
|
|
44
|
+
const specifier = node.source.value;
|
|
45
|
+
if (!specifier || !specifier.startsWith("@comity/"))
|
|
46
|
+
return;
|
|
47
|
+
if (isForbidden(specifier)) {
|
|
48
|
+
context.report({ node, messageId: "forbidden" });
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
ExportNamedDeclaration(node) {
|
|
52
|
+
if (!node.source || !node.source.value)
|
|
53
|
+
return;
|
|
54
|
+
const specifier = node.source.value;
|
|
55
|
+
if (!specifier.startsWith("@comity/"))
|
|
56
|
+
return;
|
|
57
|
+
if (isForbidden(specifier)) {
|
|
58
|
+
context.report({ node, messageId: "forbidden" });
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
ExportAllDeclaration(node) {
|
|
62
|
+
const specifier = node.source.value;
|
|
63
|
+
if (!specifier || !specifier.startsWith("@comity/"))
|
|
64
|
+
return;
|
|
65
|
+
if (isForbidden(specifier)) {
|
|
66
|
+
context.report({ node, messageId: "forbidden" });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export default rule;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-forbidden-subpath-import
|
|
3
|
+
*
|
|
4
|
+
* Forbid exports to forbidden subpaths (`utils`, `helpers`, `shared`,
|
|
5
|
+
* `internal`, `lazy`) per `public-api.md §3.2`.
|
|
6
|
+
*
|
|
7
|
+
* Standard references:
|
|
8
|
+
* - public-api.md §3.2
|
|
9
|
+
* - architecture-validation.md §4.2
|
|
10
|
+
*/
|
|
11
|
+
type Rule = unknown;
|
|
12
|
+
declare const rule: Rule;
|
|
13
|
+
export default rule;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-forbidden-subpath-import
|
|
3
|
+
*
|
|
4
|
+
* Forbid exports to forbidden subpaths (`utils`, `helpers`, `shared`,
|
|
5
|
+
* `internal`, `lazy`) per `public-api.md §3.2`.
|
|
6
|
+
*
|
|
7
|
+
* Standard references:
|
|
8
|
+
* - public-api.md §3.2
|
|
9
|
+
* - architecture-validation.md §4.2
|
|
10
|
+
*/
|
|
11
|
+
const FORBIDDEN_SUBPATHS = new Set([
|
|
12
|
+
"utils",
|
|
13
|
+
"helpers",
|
|
14
|
+
"shared",
|
|
15
|
+
"internal",
|
|
16
|
+
"lazy",
|
|
17
|
+
]);
|
|
18
|
+
const messages = {
|
|
19
|
+
forbiddenSubpath: "Exports subpath './{{segment}}' is forbidden (public-api.md §3.2).",
|
|
20
|
+
};
|
|
21
|
+
const rule = {
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
docs: {
|
|
25
|
+
description: "Forbid exports subpaths `utils`, `helpers`, `shared`, `internal`, `lazy`.",
|
|
26
|
+
category: "Comity Architecture",
|
|
27
|
+
recommended: true,
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
messages,
|
|
31
|
+
},
|
|
32
|
+
create(context) {
|
|
33
|
+
function isComityPackage(pkgName) {
|
|
34
|
+
return pkgName.startsWith("@comity/");
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
ExportNamedDeclaration(node) {
|
|
38
|
+
// No source = local export; not relevant
|
|
39
|
+
if (!node.source || !node.source.value)
|
|
40
|
+
return;
|
|
41
|
+
const specifier = node.source.value;
|
|
42
|
+
if (!specifier.startsWith("@comity/"))
|
|
43
|
+
return;
|
|
44
|
+
// Only check imports/exports OF other @comity packages
|
|
45
|
+
const pkgMatch = specifier.match(/^@comity\/[^/]+/);
|
|
46
|
+
if (!pkgMatch || !isComityPackage(pkgMatch[0]))
|
|
47
|
+
return;
|
|
48
|
+
// Subpath after the package name
|
|
49
|
+
const subpath = specifier.slice(pkgMatch[0].length).replace(/^\//, "");
|
|
50
|
+
const segment = subpath.split("/")[0];
|
|
51
|
+
if (segment && FORBIDDEN_SUBPATHS.has(segment)) {
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
messageId: "forbiddenSubpath",
|
|
55
|
+
data: { segment },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
ImportDeclaration(node) {
|
|
60
|
+
const specifier = node.source.value;
|
|
61
|
+
if (!specifier || !specifier.startsWith("@comity/"))
|
|
62
|
+
return;
|
|
63
|
+
const pkgMatch = specifier.match(/^@comity\/[^/]+/);
|
|
64
|
+
if (!pkgMatch || !isComityPackage(pkgMatch[0]))
|
|
65
|
+
return;
|
|
66
|
+
const subpath = specifier.slice(pkgMatch[0].length).replace(/^\//, "");
|
|
67
|
+
const segment = subpath.split("/")[0];
|
|
68
|
+
if (segment && FORBIDDEN_SUBPATHS.has(segment)) {
|
|
69
|
+
context.report({
|
|
70
|
+
node,
|
|
71
|
+
messageId: "forbiddenSubpath",
|
|
72
|
+
data: { segment },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export default rule;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-layer-violation
|
|
3
|
+
*
|
|
4
|
+
* Forbid imports that violate Comity's canonical layering policy.
|
|
5
|
+
*
|
|
6
|
+
* Disallowed edges (default):
|
|
7
|
+
* - primitives → any @comity/*
|
|
8
|
+
* - kernel → anything except primitives
|
|
9
|
+
* - composition → anything except primitives, kernel
|
|
10
|
+
* - core → adapters
|
|
11
|
+
* - adapters → adapters (except integration-adapter → technology-adapter)
|
|
12
|
+
*
|
|
13
|
+
* Technology adapters MAY import the core contract they implement
|
|
14
|
+
* (declared via `implements` metadata).
|
|
15
|
+
*
|
|
16
|
+
* Allowed Core-to-Core edges are gated by ADR-008; this rule does NOT
|
|
17
|
+
* evaluate that register (it is enforced separately by the dependency
|
|
18
|
+
* rules / orchestrator). Use this rule only to catch the obvious violations.
|
|
19
|
+
*
|
|
20
|
+
* Standard references:
|
|
21
|
+
* - layering-policy.md §2 (canonical layering model)
|
|
22
|
+
* - architecture-validation.md §7
|
|
23
|
+
* - ADR-007 (adapter categories)
|
|
24
|
+
*/
|
|
25
|
+
type Rule = unknown;
|
|
26
|
+
declare const rule: Rule;
|
|
27
|
+
export default rule;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-layer-violation
|
|
3
|
+
*
|
|
4
|
+
* Forbid imports that violate Comity's canonical layering policy.
|
|
5
|
+
*
|
|
6
|
+
* Disallowed edges (default):
|
|
7
|
+
* - primitives → any @comity/*
|
|
8
|
+
* - kernel → anything except primitives
|
|
9
|
+
* - composition → anything except primitives, kernel
|
|
10
|
+
* - core → adapters
|
|
11
|
+
* - adapters → adapters (except integration-adapter → technology-adapter)
|
|
12
|
+
*
|
|
13
|
+
* Technology adapters MAY import the core contract they implement
|
|
14
|
+
* (declared via `implements` metadata).
|
|
15
|
+
*
|
|
16
|
+
* Allowed Core-to-Core edges are gated by ADR-008; this rule does NOT
|
|
17
|
+
* evaluate that register (it is enforced separately by the dependency
|
|
18
|
+
* rules / orchestrator). Use this rule only to catch the obvious violations.
|
|
19
|
+
*
|
|
20
|
+
* Standard references:
|
|
21
|
+
* - layering-policy.md §2 (canonical layering model)
|
|
22
|
+
* - architecture-validation.md §7
|
|
23
|
+
* - ADR-007 (adapter categories)
|
|
24
|
+
*/
|
|
25
|
+
const messages = {
|
|
26
|
+
forbidden: "This import violates Comity's layering policy (layering-policy.md §2).",
|
|
27
|
+
};
|
|
28
|
+
const LAYER_PROFILES = {
|
|
29
|
+
primitives: { allowed: new Set() },
|
|
30
|
+
kernel: { allowed: new Set(["primitives"]) },
|
|
31
|
+
composition: { allowed: new Set(["primitives", "kernel"]) },
|
|
32
|
+
core: { allowed: new Set(["primitives", "kernel", "core"]) },
|
|
33
|
+
"technology-adapter": {
|
|
34
|
+
allowed: new Set(["primitives", "kernel"]),
|
|
35
|
+
},
|
|
36
|
+
"integration-adapter": {
|
|
37
|
+
allowed: new Set(["primitives", "kernel", "core", "technology-adapter"]),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
function classifyTarget(specifier) {
|
|
41
|
+
const m = specifier.match(/^(@comity\/[^/]+)/);
|
|
42
|
+
if (!m || !m[1])
|
|
43
|
+
return null;
|
|
44
|
+
return m[1];
|
|
45
|
+
}
|
|
46
|
+
function inferLayerFromFilename(filename) {
|
|
47
|
+
if (!filename)
|
|
48
|
+
return null;
|
|
49
|
+
if (filename.includes("/primitives/"))
|
|
50
|
+
return "primitives";
|
|
51
|
+
if (filename.includes("/kernel/"))
|
|
52
|
+
return "kernel";
|
|
53
|
+
if (filename.includes("/composition/"))
|
|
54
|
+
return "composition";
|
|
55
|
+
if (filename.includes("/packages/")) {
|
|
56
|
+
// Check for core packages
|
|
57
|
+
const corePackages = [
|
|
58
|
+
"http", "router", "html", "hydration", "seo", "content", "media", "search",
|
|
59
|
+
"cache", "auth", "catalog", "customer", "inventory", "order", "payment",
|
|
60
|
+
"pricing", "storefront", "taxonomy", "validation", "address", "geography",
|
|
61
|
+
"identity", "organization", "acl", "auth-tokens", "graphql-builder",
|
|
62
|
+
"graphql-client", "i18n", "sql", "storage"
|
|
63
|
+
];
|
|
64
|
+
for (const pkg of corePackages) {
|
|
65
|
+
if (filename.includes(`/packages/${pkg}/`))
|
|
66
|
+
return "core";
|
|
67
|
+
}
|
|
68
|
+
// Check for technology adapters
|
|
69
|
+
const adapterPackages = [
|
|
70
|
+
"html-preact", "html-react", "hydration-preact", "hydration-react",
|
|
71
|
+
"http-hono", "http-fetch", "router-path-to-regexp",
|
|
72
|
+
"acl-casl", "auth-jose", "cache-kv", "cache-redis",
|
|
73
|
+
"cli-commander", "graphql-client-fetch", "graphql-client-ws",
|
|
74
|
+
"i18n-typesafe", "sql-kysely", "validation-zod"
|
|
75
|
+
];
|
|
76
|
+
for (const pkg of adapterPackages) {
|
|
77
|
+
if (filename.includes(`/packages/${pkg}/`))
|
|
78
|
+
return "technology-adapter";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const rule = {
|
|
84
|
+
meta: {
|
|
85
|
+
type: "problem",
|
|
86
|
+
docs: {
|
|
87
|
+
description: "Forbid imports that violate Comity's canonical layering policy.",
|
|
88
|
+
category: "Comity Architecture",
|
|
89
|
+
recommended: true,
|
|
90
|
+
},
|
|
91
|
+
schema: [
|
|
92
|
+
{
|
|
93
|
+
type: "object",
|
|
94
|
+
properties: {
|
|
95
|
+
sourceName: {
|
|
96
|
+
type: "string",
|
|
97
|
+
description: "Name of the source package (e.g., '@comity/pricing'). Required.",
|
|
98
|
+
},
|
|
99
|
+
classification: {
|
|
100
|
+
type: "object",
|
|
101
|
+
description: "Map of package name → layer. The plugin receives this via configuration; it MUST NOT hardcode layers.",
|
|
102
|
+
},
|
|
103
|
+
implements: {
|
|
104
|
+
type: "object",
|
|
105
|
+
description: "Map of package name → implemented core package (for technology-adapter packages).",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["sourceName", "classification"],
|
|
109
|
+
additionalProperties: false,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
messages,
|
|
113
|
+
},
|
|
114
|
+
create(context) {
|
|
115
|
+
const opts = (context.options[0] ?? {});
|
|
116
|
+
const classification = opts.classification ?? {};
|
|
117
|
+
const implementsMap = opts.implements ?? {};
|
|
118
|
+
const sourceName = opts.sourceName ?? null;
|
|
119
|
+
// Fallback: infer source layer from filename if sourceName not provided
|
|
120
|
+
let sourceLayer = sourceName ? classification[sourceName] : inferLayerFromFilename(context.filename ?? context.getFilename());
|
|
121
|
+
if (!sourceLayer)
|
|
122
|
+
return {};
|
|
123
|
+
const profile = LAYER_PROFILES[sourceLayer];
|
|
124
|
+
if (!profile)
|
|
125
|
+
return {};
|
|
126
|
+
// If sourceName not provided, we can't check implementsMap, so skip that allowance
|
|
127
|
+
const effectiveSourceName = sourceName;
|
|
128
|
+
return {
|
|
129
|
+
ImportDeclaration(node) {
|
|
130
|
+
const specifier = node.source.value;
|
|
131
|
+
if (!specifier)
|
|
132
|
+
return;
|
|
133
|
+
const target = classifyTarget(specifier);
|
|
134
|
+
if (!target)
|
|
135
|
+
return;
|
|
136
|
+
// Look up the target package's layer
|
|
137
|
+
const targetLayer = classification[target];
|
|
138
|
+
if (!targetLayer)
|
|
139
|
+
return; // unknown package — outside this rule's scope
|
|
140
|
+
// Allow technology-adapter to import its implemented core contract
|
|
141
|
+
if (effectiveSourceName &&
|
|
142
|
+
sourceLayer === "technology-adapter" &&
|
|
143
|
+
targetLayer === "core" &&
|
|
144
|
+
implementsMap[effectiveSourceName] === target) {
|
|
145
|
+
return; // Allowed: adapter implements this core contract
|
|
146
|
+
}
|
|
147
|
+
if (!profile.allowed.has(targetLayer)) {
|
|
148
|
+
context.report({ node, messageId: "forbidden" });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
export default rule;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-render-core-http-runtime
|
|
3
|
+
*
|
|
4
|
+
* Forbid Rendering Core Modules (html, hydration, search, seo, content)
|
|
5
|
+
* from importing HTTP runtime implementations (http-hono, http-fetch,
|
|
6
|
+
* html-preact, html-react, hydration-preact, hydration-react).
|
|
7
|
+
*
|
|
8
|
+
* The rule is scoped to only apply to files within the declared Rendering
|
|
9
|
+
* Core packages. Technology adapters and client entry points are permitted
|
|
10
|
+
* to import their respective runtimes.
|
|
11
|
+
*
|
|
12
|
+
* Standard references:
|
|
13
|
+
* - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
|
|
14
|
+
* - architecture-validation.md §4.3
|
|
15
|
+
*/
|
|
16
|
+
type Rule = unknown;
|
|
17
|
+
declare const rule: Rule;
|
|
18
|
+
export default rule;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @comity-dev/no-render-core-http-runtime
|
|
3
|
+
*
|
|
4
|
+
* Forbid Rendering Core Modules (html, hydration, search, seo, content)
|
|
5
|
+
* from importing HTTP runtime implementations (http-hono, http-fetch,
|
|
6
|
+
* html-preact, html-react, hydration-preact, hydration-react).
|
|
7
|
+
*
|
|
8
|
+
* The rule is scoped to only apply to files within the declared Rendering
|
|
9
|
+
* Core packages. Technology adapters and client entry points are permitted
|
|
10
|
+
* to import their respective runtimes.
|
|
11
|
+
*
|
|
12
|
+
* Standard references:
|
|
13
|
+
* - layering-policy.md §8 (rendering core MUST NOT depend on HTTP runtime)
|
|
14
|
+
* - architecture-validation.md §4.3
|
|
15
|
+
*/
|
|
16
|
+
const RENDERING_CORE_PACKAGES = new Set([
|
|
17
|
+
"@comity/html",
|
|
18
|
+
"@comity/hydration",
|
|
19
|
+
"@comity/search",
|
|
20
|
+
"@comity/seo",
|
|
21
|
+
"@comity/content",
|
|
22
|
+
]);
|
|
23
|
+
const FORBIDDEN_RUNTIME_PACKAGES = new Set([
|
|
24
|
+
"@comity/http-hono",
|
|
25
|
+
"@comity/http-fetch",
|
|
26
|
+
"@comity/html-preact",
|
|
27
|
+
"@comity/html-react",
|
|
28
|
+
"@comity/hydration-preact",
|
|
29
|
+
"@comity/hydration-react",
|
|
30
|
+
]);
|
|
31
|
+
const messages = {
|
|
32
|
+
forbiddenRuntime: "Rendering Core Module MUST NOT import HTTP/Rendering runtime '{{target}}' (layering-policy.md §8).",
|
|
33
|
+
};
|
|
34
|
+
function isInRenderingCorePackage(filename) {
|
|
35
|
+
return Array.from(RENDERING_CORE_PACKAGES).some((pkg) => filename.includes(`/packages/${pkg.replace("@comity/", "")}/`));
|
|
36
|
+
}
|
|
37
|
+
const rule = {
|
|
38
|
+
meta: {
|
|
39
|
+
type: "problem",
|
|
40
|
+
docs: {
|
|
41
|
+
description: "Forbid Rendering Core Modules from importing HTTP / Rendering runtimes.",
|
|
42
|
+
category: "Comity Architecture",
|
|
43
|
+
recommended: true,
|
|
44
|
+
},
|
|
45
|
+
schema: [
|
|
46
|
+
{
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {
|
|
49
|
+
sourceName: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Name of the source package (e.g., '@comity/html'). Used to scope the rule.",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
messages,
|
|
58
|
+
},
|
|
59
|
+
create(context) {
|
|
60
|
+
const opts = (context.options[0] ?? {});
|
|
61
|
+
const sourceName = opts.sourceName ?? null;
|
|
62
|
+
// If no sourceName provided, fall back to filename heuristic
|
|
63
|
+
const inRenderingCore = sourceName
|
|
64
|
+
? RENDERING_CORE_PACKAGES.has(sourceName)
|
|
65
|
+
: isInRenderingCorePackage(context.filename ?? context.getFilename());
|
|
66
|
+
// Disable this rule outside rendering core packages
|
|
67
|
+
if (!inRenderingCore) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
function checkImport(specifier, node) {
|
|
71
|
+
if (!specifier.startsWith("@comity/"))
|
|
72
|
+
return;
|
|
73
|
+
if (FORBIDDEN_RUNTIME_PACKAGES.has(specifier)) {
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
context.report({
|
|
76
|
+
node: node,
|
|
77
|
+
messageId: "forbiddenRuntime",
|
|
78
|
+
data: { target: specifier },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ImportDeclaration(node) {
|
|
84
|
+
checkImport(node.source.value, node);
|
|
85
|
+
},
|
|
86
|
+
ImportExpression(node) {
|
|
87
|
+
if (node.source.type === "Literal" &&
|
|
88
|
+
typeof node.source.value === "string") {
|
|
89
|
+
checkImport(node.source.value, node);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
ExportNamedDeclaration(node) {
|
|
93
|
+
if (node.source && node.source.value) {
|
|
94
|
+
checkImport(node.source.value, node);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
export default rule;
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@comity-dev/eslint-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable ESLint rules for Comity source-code policy. Development tooling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"comity": {
|
|
9
|
+
"layer": "dev-tooling"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=24.0.0"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./recommended": {
|
|
22
|
+
"types": "./dist/recommended.d.ts",
|
|
23
|
+
"default": "./dist/recommended.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"./dist"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"eslint": "^9.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"eslint": "^9.39.5",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc -p tsconfig.json",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|