@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.
@@ -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='x'><div /></HubPage>;";
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='x'><Stack /></HubPage>;";
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='x'><HubCard to='/a' title='a' /></HubPage>;",
55
- "export const O = () => <OverviewPage title='x'><Card title='open'><DataView /></Card></OverviewPage>;",
56
- "export const D = () => <DetailPage title='x' parents={[]}><Card title='a section'>body</Card><Stack><DetailList items={[]} /></Stack></DetailPage>;",
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='x'>loose text</OverviewPage>;";
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='x'><><span /></></HubPage>;";
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='x'>{items}</HubPage>;";
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='x'>\n" +
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
- const fired = (await lintCaseFindings(caseDir)).map((finding) => finding.rule);
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
- * They apply to the **app-authored surface** (`src/modules/**`) the code agents and users write —
7
- * not the framework packages, which legitimately define the very primitives the rules point back to.
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.27.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
  /**
@@ -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/token-styled-elements": "export const W = () => <button>x</button>;",
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">go</a>;',
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": 'export const W = () => <a href="javascript:alert(1)">x</a>;',
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">x</a>;',
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(