@terpjs/eslint-boundaries 0.2.0 → 0.4.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/package.json +4 -4
- package/src/corpus-harness.js +1 -1
- package/src/corpus.test.js +2 -3
- package/src/findings.js +2 -2
- package/src/findings.test.js +4 -4
- package/src/index.test.js +1 -1
- package/src/scorecard.js +6 -6
- package/src/scorecard.test.js +3 -3
- package/src/spec.js +3 -3
- package/src/surface.test.js +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/eslint-boundaries",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Terp frontend boundary rules (as data) + the ESLint adapter: no cross-module imports, no package internals, design-token-only styling (no style/className/module stylesheets), token-styled components for raw HTML tags, router-only in-app links, generated-client-only, and browser XSS/navigation sink bans. Strict-only (no modes); governed opt-outs via terp-allow markers + the escape-hatch budget ratchet (terp-boundaries-budget).",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"typescript-eslint": "^8.20.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@
|
|
29
|
-
"eslint": "^
|
|
30
|
-
"vitest": "^
|
|
28
|
+
"@terpjs/spec": "0.18.0",
|
|
29
|
+
"eslint": "^10.8.0",
|
|
30
|
+
"vitest": "^4.1.9"
|
|
31
31
|
},
|
|
32
32
|
"license": "Apache-2.0",
|
|
33
33
|
"repository": {
|
package/src/corpus-harness.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Shared by the certification test (corpus.test.js) and the scorecard emitter
|
|
6
6
|
* (scorecard.js), so the two can never apply different corpus semantics. The spec
|
|
7
|
-
* is resolved as a declared dependency (@
|
|
7
|
+
* is resolved as a declared dependency (@terpjs/spec, ADR 0082) by the CALLER —
|
|
8
8
|
* this module takes paths, never resolves the spec itself.
|
|
9
9
|
*/
|
|
10
10
|
|
package/src/corpus.test.js
CHANGED
|
@@ -22,10 +22,9 @@ import { describe, expect, it } from "vitest";
|
|
|
22
22
|
|
|
23
23
|
import { lintCaseFindings } from "./corpus-harness.js";
|
|
24
24
|
|
|
25
|
-
// The spec is a declared dependency (@
|
|
26
|
-
// inside the monorepo it resolves to the workspace member; after a repo split, to the pin.
|
|
25
|
+
// The spec is a declared dependency (@terpjs/spec, ADR 0082), never a repo-relative path.
|
|
27
26
|
const SPEC_ROOT = path.dirname(
|
|
28
|
-
createRequire(import.meta.url).resolve("@
|
|
27
|
+
createRequire(import.meta.url).resolve("@terpjs/spec/package.json"),
|
|
29
28
|
);
|
|
30
29
|
const CATALOG = path.join(SPEC_ROOT, "catalog", "frontend");
|
|
31
30
|
const CORPUS = path.join(SPEC_ROOT, "corpus", "frontend");
|
package/src/findings.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* check `terp-boundaries-budget` runs) in one command, and publishes one **findings
|
|
8
8
|
* envelope** on stdout:
|
|
9
9
|
*
|
|
10
|
-
* { "terp_findings": 1, "tool": "@
|
|
10
|
+
* { "terp_findings": 1, "tool": "@terpjs/eslint-boundaries",
|
|
11
11
|
* "rules": ["frontend/<rule>", …], // every catalog rule this run evaluated
|
|
12
12
|
* "not_applicable": ["frontend/<rule>", …], // opt-in rules this app has not enabled
|
|
13
13
|
* "findings": [{ rule, path, line, message }, …], // spec findings.schema.json shape
|
|
@@ -128,7 +128,7 @@ export function renderEnvelope(results, cwd = process.cwd(), options = {}) {
|
|
|
128
128
|
return {
|
|
129
129
|
envelope: {
|
|
130
130
|
terp_findings: 1,
|
|
131
|
-
tool: "@
|
|
131
|
+
tool: "@terpjs/eslint-boundaries",
|
|
132
132
|
rules: catalogRuleIds().filter((id) => !notApplicable.includes(id)),
|
|
133
133
|
not_applicable: notApplicable,
|
|
134
134
|
findings,
|
package/src/findings.test.js
CHANGED
|
@@ -16,7 +16,7 @@ import { SPEC_VERSION } from "./spec.js";
|
|
|
16
16
|
// stack-neutral catalog ids, humans keep stderr, and the exit code stays the verdict.
|
|
17
17
|
|
|
18
18
|
const SPEC_ROOT = path.dirname(
|
|
19
|
-
createRequire(import.meta.url).resolve("@
|
|
19
|
+
createRequire(import.meta.url).resolve("@terpjs/spec/package.json"),
|
|
20
20
|
);
|
|
21
21
|
const FINDINGS_BIN = fileURLToPath(new URL("./findings.js", import.meta.url));
|
|
22
22
|
|
|
@@ -71,7 +71,7 @@ describe("renderEnvelope", () => {
|
|
|
71
71
|
layoutContract: true,
|
|
72
72
|
});
|
|
73
73
|
expect(envelope.terp_findings).toBe(1);
|
|
74
|
-
expect(envelope.tool).toBe("@
|
|
74
|
+
expect(envelope.tool).toBe("@terpjs/eslint-boundaries");
|
|
75
75
|
expect(envelope.rules).toEqual(catalogRuleIds());
|
|
76
76
|
expect(envelope.not_applicable).toEqual([]);
|
|
77
77
|
const rules = envelope.findings.map((finding) => finding.rule);
|
|
@@ -252,11 +252,11 @@ describe("the check report (--format check-report, app-check-report.schema.json)
|
|
|
252
252
|
const report = asCheckReport(envelope);
|
|
253
253
|
expect(report.terp_check_report).toBe(1);
|
|
254
254
|
// The certified spec version rides every report. Shape only here — the
|
|
255
|
-
// equality lock against the pinned @
|
|
255
|
+
// equality lock against the pinned @terpjs/spec lives in the framework gate
|
|
256
256
|
// (test_check_json.py), because certification runs THIS suite against
|
|
257
257
|
// candidate spec releases whose version is allowed to be newer.
|
|
258
258
|
expect(report.spec_version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
259
|
-
expect(report.checker.tool).toBe("@
|
|
259
|
+
expect(report.checker.tool).toBe("@terpjs/eslint-boundaries");
|
|
260
260
|
expect(report.checker.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
261
261
|
expect(report.ok).toBe(false);
|
|
262
262
|
expect(report.rules).toEqual(catalogRuleIds());
|
package/src/index.test.js
CHANGED
|
@@ -187,7 +187,7 @@ describe("terpBoundaries", () => {
|
|
|
187
187
|
});
|
|
188
188
|
|
|
189
189
|
it("flags a deep import into a package's internals", async () => {
|
|
190
|
-
const code = 'import x from "@
|
|
190
|
+
const code = 'import x from "@terpjs/react-core/src/secret";\nexport const W = () => null;';
|
|
191
191
|
expect(await lint(code)).toContain("no-restricted-imports");
|
|
192
192
|
});
|
|
193
193
|
|
package/src/scorecard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Emit the Terp Standard conformance scorecard for `@
|
|
3
|
+
* Emit the Terp Standard conformance scorecard for `@terpjs/eslint-boundaries`
|
|
4
4
|
* (the frontend checker).
|
|
5
5
|
*
|
|
6
6
|
* The scorecard (`scorecard.schema.json` in the spec) turns "certified against
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* residuals the adapter relies on (held to a subset of the spec's recorded
|
|
10
10
|
* `corpus/RESIDUALS.json`). A consumer re-runs the corpus and reproduces it.
|
|
11
11
|
*
|
|
12
|
-
* A certification-context tool, not an app tool: it needs `@
|
|
12
|
+
* A certification-context tool, not an app tool: it needs `@terpjs/spec` (a dev
|
|
13
13
|
* dependency of the platform repo) and runs the SAME harness the corpus test
|
|
14
14
|
* uses (./corpus-harness.js), so the scorecard can never disagree with the
|
|
15
15
|
* suite. Self-validates and refuses to write an invalid or failing scorecard
|
|
@@ -31,12 +31,12 @@ const require = createRequire(import.meta.url);
|
|
|
31
31
|
const RULE_ID_RE = /^(backend\/[a-z0-9_]+|frontend\/[a-z0-9-]+)$/;
|
|
32
32
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
33
33
|
|
|
34
|
-
/** The @
|
|
34
|
+
/** The @terpjs/spec root — resolved lazily so importing this module never requires it. */
|
|
35
35
|
function specRoot() {
|
|
36
|
-
return path.dirname(require.resolve("@
|
|
36
|
+
return path.dirname(require.resolve("@terpjs/spec/package.json"));
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/** Build the @
|
|
39
|
+
/** Build the @terpjs/eslint-boundaries scorecard over the frontend corpus. */
|
|
40
40
|
export async function buildScorecard() {
|
|
41
41
|
const root = specRoot();
|
|
42
42
|
const catalogDir = path.join(root, "catalog", "frontend");
|
|
@@ -70,7 +70,7 @@ export async function buildScorecard() {
|
|
|
70
70
|
}
|
|
71
71
|
return {
|
|
72
72
|
spec_version: fs.readFileSync(path.join(root, "VERSION"), "utf8").trim(),
|
|
73
|
-
checker: { tool: "@
|
|
73
|
+
checker: { tool: "@terpjs/eslint-boundaries", version: packageVersion },
|
|
74
74
|
rules,
|
|
75
75
|
};
|
|
76
76
|
}
|
package/src/scorecard.test.js
CHANGED
|
@@ -15,10 +15,10 @@ import { describe, expect, it } from "vitest";
|
|
|
15
15
|
import { buildScorecard, validateScorecard } from "./scorecard.js";
|
|
16
16
|
|
|
17
17
|
const SPEC_ROOT = path.dirname(
|
|
18
|
-
createRequire(import.meta.url).resolve("@
|
|
18
|
+
createRequire(import.meta.url).resolve("@terpjs/spec/package.json"),
|
|
19
19
|
);
|
|
20
20
|
|
|
21
|
-
describe("the @
|
|
21
|
+
describe("the @terpjs/eslint-boundaries scorecard", () => {
|
|
22
22
|
it("claims the whole corpus-covered catalog, green, schema-shaped", async () => {
|
|
23
23
|
const scorecard = await buildScorecard();
|
|
24
24
|
expect(validateScorecard(scorecard)).toEqual([]);
|
|
@@ -34,7 +34,7 @@ describe("the @terp/eslint-boundaries scorecard", () => {
|
|
|
34
34
|
expect(scorecard.spec_version).toBe(
|
|
35
35
|
fs.readFileSync(path.join(SPEC_ROOT, "VERSION"), "utf8").trim(),
|
|
36
36
|
);
|
|
37
|
-
expect(scorecard.checker.tool).toBe("@
|
|
37
|
+
expect(scorecard.checker.tool).toBe("@terpjs/eslint-boundaries");
|
|
38
38
|
const itemProperties = Object.keys(schema.properties.rules.items.properties);
|
|
39
39
|
for (const claim of scorecard.rules) {
|
|
40
40
|
expect(claim.pass).toBe(true);
|
package/src/spec.js
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* The Terp Standard version this adapter is certified against — the `spec_version` a
|
|
11
11
|
* check report (`app-check-report.schema.json`) carries. A constant rather than a runtime
|
|
12
|
-
* `@
|
|
12
|
+
* `@terpjs/spec` read: the spec data package is a dev/certification dependency of the platform
|
|
13
13
|
* repo, not of a generated app, and the version is a property of the toolchain build. Held
|
|
14
14
|
* equal to the pinned spec release by the framework gate (test_check_json.py — deliberately
|
|
15
15
|
* NOT by this package's own suite, which certification runs against candidate spec releases
|
|
16
16
|
* whose version is allowed to be newer).
|
|
17
17
|
*/
|
|
18
|
-
export const SPEC_VERSION = "0.
|
|
18
|
+
export const SPEC_VERSION = "0.18.0";
|
|
19
19
|
|
|
20
20
|
export const BOUNDARY_SPEC = {
|
|
21
21
|
/** App module files the boundary + frontend security defaults apply to. */
|
|
@@ -45,7 +45,7 @@ export const BOUNDARY_SPEC = {
|
|
|
45
45
|
*/
|
|
46
46
|
restrictInAppAnchors: true,
|
|
47
47
|
/** Package internals an app module must not deep-import (import from the package root). */
|
|
48
|
-
internalImportPatterns: ["@
|
|
48
|
+
internalImportPatterns: ["@terpjs/*/src/*", "@terpjs/*/dist/*"],
|
|
49
49
|
/** Module-authored stylesheets are refused — theming flows from the app's token source. */
|
|
50
50
|
styleImportPatterns: [
|
|
51
51
|
"*.css",
|
package/src/surface.test.js
CHANGED
|
@@ -22,9 +22,9 @@ import { describe, expect, it } from "vitest";
|
|
|
22
22
|
|
|
23
23
|
import terpBoundaries, { BOUNDARY_SPEC, catalogRuleId } from "./index.js";
|
|
24
24
|
|
|
25
|
-
// The spec is a declared dependency (@
|
|
25
|
+
// The spec is a declared dependency (@terpjs/spec, ADR 0082), never a repo-relative path.
|
|
26
26
|
const SPEC_ROOT = path.dirname(
|
|
27
|
-
createRequire(import.meta.url).resolve("@
|
|
27
|
+
createRequire(import.meta.url).resolve("@terpjs/spec/package.json"),
|
|
28
28
|
);
|
|
29
29
|
const SURFACE = JSON.parse(
|
|
30
30
|
fs.readFileSync(path.join(SPEC_ROOT, "restricted-surface.json"), "utf8"),
|
|
@@ -70,7 +70,7 @@ describe("structural parity: BOUNDARY_SPEC realises exactly the declared surface
|
|
|
70
70
|
|
|
71
71
|
it("every declared deep-import segment is refused by the import patterns", () => {
|
|
72
72
|
for (const segment of SURFACE.deepImportPathSegments) {
|
|
73
|
-
expect(BOUNDARY_SPEC.internalImportPatterns).toContain(`@
|
|
73
|
+
expect(BOUNDARY_SPEC.internalImportPatterns).toContain(`@terpjs/*/${segment}/*`);
|
|
74
74
|
}
|
|
75
75
|
expect(BOUNDARY_SPEC.internalImportPatterns).toHaveLength(
|
|
76
76
|
SURFACE.deepImportPathSegments.length,
|
|
@@ -123,7 +123,7 @@ describe("behavioural parity: each declared primitive is refused with the right
|
|
|
123
123
|
for (const segment of SURFACE.deepImportPathSegments) {
|
|
124
124
|
it(`deep import via /${segment}/ -> frontend/no-deep-imports`, async () => {
|
|
125
125
|
expect(
|
|
126
|
-
await lintModuleSource(`import { x } from "@
|
|
126
|
+
await lintModuleSource(`import { x } from "@terpjs/react-core/${segment}/internal";\n`),
|
|
127
127
|
).toContain("frontend/no-deep-imports");
|
|
128
128
|
});
|
|
129
129
|
}
|
|
@@ -148,7 +148,7 @@ const VIOLATION_SNIPPETS = {
|
|
|
148
148
|
"frontend/no-inline-styling": 'export const W = () => <div className="x" />;',
|
|
149
149
|
"frontend/router-links": 'export const W = () => <a href="/notes">go</a>;',
|
|
150
150
|
"frontend/generated-client-only": 'export const ping = () => fetch("/healthz");',
|
|
151
|
-
"frontend/no-deep-imports": 'import { x } from "@
|
|
151
|
+
"frontend/no-deep-imports": 'import { x } from "@terpjs/react-core/src/internal";',
|
|
152
152
|
"frontend/no-style-imports": 'import "./widget.css";',
|
|
153
153
|
"frontend/no-cross-module-imports": 'import { x } from "../other/thing";',
|
|
154
154
|
"frontend/no-dom-html-injection": "export const W = (el, html) => { el.innerHTML = html; };",
|