@terpjs/eslint-boundaries 0.11.0 → 0.13.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 +2 -2
- package/src/budget.js +10 -8
- package/src/budget.test.js +14 -1
- package/src/corpus-harness.js +8 -3
- package/src/corpus.test.js +12 -4
- package/src/findings.js +25 -3
- package/src/findings.test.js +44 -4
- package/src/i18n.test.js +307 -0
- package/src/index.js +503 -10
- package/src/index.test.js +42 -12
- package/src/layouts.test.js +9 -9
- package/src/scorecard.js +13 -1
- package/src/spec.js +6 -3
- package/src/surface.test.js +9 -4
package/src/layouts.test.js
CHANGED
|
@@ -35,14 +35,14 @@ describe("terp/layout-contract", () => {
|
|
|
35
35
|
it("is inert without an opted-in contract (backwards compatible)", async () => {
|
|
36
36
|
const code =
|
|
37
37
|
'import { HubPage } from "@terpjs/react-core";\n' +
|
|
38
|
-
"export const W = () => <HubPage title=
|
|
38
|
+
"export const W = ({title}) => <HubPage title={title}><div /></HubPage>;";
|
|
39
39
|
expect((await lint(code)).map((m) => m.ruleId)).toEqual([]);
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it("refuses a non-conforming child in a HubPage body, with the directive message", async () => {
|
|
43
43
|
const code =
|
|
44
44
|
'import { HubPage, Stack } from "@terpjs/react-core";\n' +
|
|
45
|
-
"export const W = () => <HubPage title=
|
|
45
|
+
"export const W = ({title}) => <HubPage title={title}><Stack /></HubPage>;";
|
|
46
46
|
const messages = await lint(code, configWithContract("standard"));
|
|
47
47
|
expect(messages.map((m) => m.ruleId)).toContain("terp/layout-contract");
|
|
48
48
|
expect(messages[0].message).toBe(slotViolationMessage("standard", "HubPage", "<Stack>"));
|
|
@@ -51,9 +51,9 @@ describe("terp/layout-contract", () => {
|
|
|
51
51
|
it("passes a conforming hub / overview / detail composition", async () => {
|
|
52
52
|
const code = [
|
|
53
53
|
'import { Card, DataView, DetailList, HubCard, HubPage, OverviewPage, DetailPage, Stack } from "@terpjs/react-core";',
|
|
54
|
-
"export const H = () => <HubPage title=
|
|
55
|
-
"export const O = () => <OverviewPage title=
|
|
56
|
-
"export const D = () => <DetailPage title=
|
|
54
|
+
"export const H = ({title}) => <HubPage title={title}><HubCard to='/a' title={title} /></HubPage>;",
|
|
55
|
+
"export const O = ({title}) => <OverviewPage title={title}><Card title={title}><DataView /></Card></OverviewPage>;",
|
|
56
|
+
"export const D = ({title, body}) => <DetailPage title={title} parents={[]}><Card title={title}>{body}</Card><Stack><DetailList items={[]} /></Stack></DetailPage>;",
|
|
57
57
|
].join("\n");
|
|
58
58
|
expect((await lint(code, configWithContract("standard"))).map((m) => m.ruleId)).toEqual([]);
|
|
59
59
|
});
|
|
@@ -61,19 +61,19 @@ describe("terp/layout-contract", () => {
|
|
|
61
61
|
it("refuses raw text and recurses through fragments; dynamic children are left to the runtime half", async () => {
|
|
62
62
|
const text =
|
|
63
63
|
'import { OverviewPage } from "@terpjs/react-core";\n' +
|
|
64
|
-
"export const W = () => <OverviewPage title=
|
|
64
|
+
"export const W = ({title}) => <OverviewPage title={title}>loose text</OverviewPage>;";
|
|
65
65
|
expect((await lint(text, configWithContract("standard"))).map((m) => m.ruleId)).toContain(
|
|
66
66
|
"terp/layout-contract",
|
|
67
67
|
);
|
|
68
68
|
const fragment =
|
|
69
69
|
'import { HubPage } from "@terpjs/react-core";\n' +
|
|
70
|
-
"export const W = () => <HubPage title=
|
|
70
|
+
"export const W = ({title}) => <HubPage title={title}><><span /></></HubPage>;";
|
|
71
71
|
expect((await lint(fragment, configWithContract("standard"))).map((m) => m.ruleId)).toContain(
|
|
72
72
|
"terp/layout-contract",
|
|
73
73
|
);
|
|
74
74
|
const dynamic =
|
|
75
75
|
'import { HubPage } from "@terpjs/react-core";\n' +
|
|
76
|
-
"export const W = ({items}) => <HubPage title=
|
|
76
|
+
"export const W = ({title, items}) => <HubPage title={title}>{items}</HubPage>;";
|
|
77
77
|
expect((await lint(dynamic, configWithContract("standard"))).map((m) => m.ruleId)).toEqual([]);
|
|
78
78
|
});
|
|
79
79
|
|
|
@@ -86,7 +86,7 @@ describe("terp/layout-contract", () => {
|
|
|
86
86
|
it("honours a justified escape-hatch marker (and only a justified one)", async () => {
|
|
87
87
|
const code =
|
|
88
88
|
'import { HubPage } from "@terpjs/react-core";\n' +
|
|
89
|
-
"export const W = () => <HubPage title=
|
|
89
|
+
"export const W = ({title}) => <HubPage title={title}>\n" +
|
|
90
90
|
" {/* terp-allow-layout-contract: legacy widget pending HubCard port */}\n" +
|
|
91
91
|
" <div />\n" +
|
|
92
92
|
"</HubPage>;";
|
package/src/scorecard.js
CHANGED
|
@@ -54,13 +54,25 @@ export async function buildScorecard() {
|
|
|
54
54
|
.filter((name) => name.endsWith(".json"))
|
|
55
55
|
.map((name) => JSON.parse(fs.readFileSync(path.join(catalogDir, name), "utf8")))
|
|
56
56
|
.filter((entry) => entry.corpus);
|
|
57
|
+
const consumedRuleIds = new Set(
|
|
58
|
+
fs
|
|
59
|
+
.readdirSync(catalogDir)
|
|
60
|
+
.filter((name) => name.endsWith(".json"))
|
|
61
|
+
.map((name) => JSON.parse(fs.readFileSync(path.join(catalogDir, name), "utf8")).id),
|
|
62
|
+
);
|
|
57
63
|
for (const entry of entries) {
|
|
58
64
|
const ruleDir = path.join(corpusDir, entry.id.split("/")[1]);
|
|
59
65
|
let pass = true;
|
|
60
66
|
for (const caseName of fs.readdirSync(ruleDir).sort()) {
|
|
61
67
|
const caseDir = path.join(ruleDir, caseName);
|
|
62
68
|
if (!fs.statSync(caseDir).isDirectory()) continue;
|
|
63
|
-
|
|
69
|
+
// A framework implementation lands before its Standard candidate can certify.
|
|
70
|
+
// While running against the last published pin, ignore those forward rule ids;
|
|
71
|
+
// a substituted candidate includes them in consumedRuleIds and exercises them.
|
|
72
|
+
// Unattributed findings remain failures rather than disappearing in the filter.
|
|
73
|
+
const fired = (await lintCaseFindings(caseDir))
|
|
74
|
+
.filter((finding) => finding.rule === null || consumedRuleIds.has(finding.rule))
|
|
75
|
+
.map((finding) => finding.rule);
|
|
64
76
|
if (caseName.startsWith("violation-") && !fired.includes(entry.id)) pass = false;
|
|
65
77
|
if (caseName.startsWith("compliant-") && fired.length > 0) pass = false;
|
|
66
78
|
}
|
package/src/spec.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* ./index.js realises them for the React stack; a future stack (e.g. Svelte) can realise the same
|
|
4
4
|
* spec with its own adapter. The *rules* are shared; only the *enforcement adapter* is per-stack.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Structural/security boundaries apply to `src/modules/**`; localization applies to all
|
|
7
|
+
* app-authored `src/**`. Framework packages legitimately define the primitives these rules point
|
|
8
|
+
* back to and are outside an app's boundary config.
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
11
|
* The Terp Standard version this adapter is certified against — the `spec_version` a
|
|
@@ -15,9 +16,11 @@
|
|
|
15
16
|
* NOT by this package's own suite, which certification runs against candidate spec releases
|
|
16
17
|
* whose version is allowed to be newer).
|
|
17
18
|
*/
|
|
18
|
-
export const SPEC_VERSION = "0.
|
|
19
|
+
export const SPEC_VERSION = "0.29.1";
|
|
19
20
|
|
|
20
21
|
export const BOUNDARY_SPEC = {
|
|
22
|
+
/** Every app-authored TypeScript source file whose user-facing copy must be cataloged. */
|
|
23
|
+
appFiles: ["**/src/**/*.{ts,tsx}"],
|
|
21
24
|
/** App module files the boundary + frontend security defaults apply to. */
|
|
22
25
|
moduleFiles: ["**/modules/**/*.{ts,tsx}"],
|
|
23
26
|
/**
|
package/src/surface.test.js
CHANGED
|
@@ -160,18 +160,23 @@ const CATALOG_ENTRIES = fs
|
|
|
160
160
|
|
|
161
161
|
/** A minimal violating line per frontend catalog rule (the marker goes on the line above). */
|
|
162
162
|
const VIOLATION_SNIPPETS = {
|
|
163
|
-
"frontend/
|
|
163
|
+
"frontend/locale-catalogs-complete":
|
|
164
|
+
'export const title = { id: "widgets.title", message: "Widgets" };',
|
|
165
|
+
"frontend/token-styled-elements": "export const W = ({label}) => <button>{label}</button>;",
|
|
164
166
|
"frontend/no-inline-styling": 'export const W = () => <div className="x" />;',
|
|
165
|
-
"frontend/router-links": 'export const W = () => <a href="/notes">
|
|
167
|
+
"frontend/router-links": 'export const W = ({label}) => <a href="/notes">{label}</a>;',
|
|
166
168
|
"frontend/generated-client-only": 'export const ping = () => fetch("/healthz");',
|
|
167
169
|
"frontend/no-deep-imports": 'import { x } from "@terpjs/react-core/src/internal";',
|
|
168
170
|
"frontend/no-style-imports": 'import "./widget.css";',
|
|
169
171
|
"frontend/no-cross-module-imports": 'import { x } from "../other/thing";',
|
|
170
172
|
"frontend/no-dom-html-injection": "export const W = (el, html) => { el.innerHTML = html; };",
|
|
171
173
|
"frontend/no-eval": "export const run = (code) => eval(code);",
|
|
172
|
-
"frontend/no-unsafe-href":
|
|
174
|
+
"frontend/no-unsafe-href":
|
|
175
|
+
'export const W = ({label}) => <a href="javascript:alert(1)">{label}</a>;',
|
|
173
176
|
"frontend/no-unsafe-target-blank":
|
|
174
|
-
'export const W = () => <a href="https://example.com" target="_blank">
|
|
177
|
+
'export const W = ({label}) => <a href="https://example.com" target="_blank">{label}</a>;',
|
|
178
|
+
"frontend/no-untranslated-ui":
|
|
179
|
+
'export const W = () => <Text>Untranslated copy</Text>;',
|
|
175
180
|
};
|
|
176
181
|
|
|
177
182
|
describe(
|