@terpjs/eslint-boundaries 0.7.0 → 0.9.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@terpjs/eslint-boundaries",
3
- "version": "0.7.0",
3
+ "version": "0.9.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,7 +25,7 @@
25
25
  "typescript-eslint": "^8.20.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@terpjs/spec": "0.24.0",
28
+ "@terpjs/spec": "0.25.0",
29
29
  "eslint": "^10.8.0",
30
30
  "vitest": "^4.1.9"
31
31
  },
package/src/index.js CHANGED
@@ -412,11 +412,16 @@ const styleImportMessage =
412
412
  * {@link catalogRuleId}). {@link restrictedSyntax} strips the tag for the ESLint config.
413
413
  */
414
414
  function restrictedSyntaxWithCatalogIds() {
415
- const rawElements = Object.entries(BOUNDARY_SPEC.restrictedElements).map(([element, use]) => ({
416
- catalogId: "frontend/token-styled-elements",
417
- selector: `JSXOpeningElement[name.name='${element}']`,
418
- message: `Use ${use} from @terpjs/react-core, not a raw <${element}>.`,
419
- }));
415
+ const rawElements = Object.entries(BOUNDARY_SPEC.restrictedElements).map(([element, use]) => {
416
+ const guidance = BOUNDARY_SPEC.restrictedElementGuidance?.[element];
417
+ return {
418
+ catalogId: "frontend/token-styled-elements",
419
+ selector: `JSXOpeningElement[name.name='${element}']`,
420
+ message:
421
+ `Use ${use} from @terpjs/react-core, not a raw <${element}>.` +
422
+ (guidance === undefined ? "" : ` ${guidance}`),
423
+ };
424
+ });
420
425
  const rawAttributes = BOUNDARY_SPEC.restrictedAttributes.map((attribute) => ({
421
426
  catalogId: "frontend/no-inline-styling",
422
427
  selector: `JSXAttribute[name.name='${attribute}']`,
package/src/index.test.js CHANGED
@@ -18,6 +18,12 @@ async function lint(code, filePath = MODULE_FILE) {
18
18
  return result.messages.map((message) => message.ruleId);
19
19
  }
20
20
 
21
+ async function lintMessages(code, filePath = MODULE_FILE) {
22
+ const eslint = new ESLint({ overrideConfigFile: true, overrideConfig: terpBoundaries });
23
+ const [result] = await eslint.lintText(code, { filePath });
24
+ return result.messages.map((message) => message.message);
25
+ }
26
+
21
27
  describe("terpBoundaries", () => {
22
28
  it("passes clean module code (react-core components + generated client)", async () => {
23
29
  const code = [
@@ -315,4 +321,22 @@ describe("terpBoundaries", () => {
315
321
  const code = ["/* eslint-disable */", "export const W = () => <button>x</button>;"].join("\n");
316
322
  expect(await lint(code)).toContain("no-restricted-syntax");
317
323
  });
324
+
325
+ it("the dialog refusal says what to do instead of naming only ConfirmDialog", async () => {
326
+ // "Use ConfirmDialog" is right for a confirmation and wrong advice for an edit form,
327
+ // and an author who reads it for one concludes the rule cannot be obeyed. The reported
328
+ // evidence was an app that built the editor in an expanded row instead and recorded
329
+ // that as the better outcome — so the refusal carries that guidance, rather than the
330
+ // framework shipping a general modal whose absence produced the better UI.
331
+ const messages = await lintMessages("export const W = () => <dialog>x</dialog>;");
332
+ const refusal = messages.find((message) => message.includes("<dialog>"));
333
+ expect(refusal).toContain("ConfirmDialog");
334
+ expect(refusal).toContain("routed page");
335
+ expect(refusal).toContain("expanded row");
336
+ });
337
+
338
+ it("an element with no extra guidance keeps the plain one-line refusal", async () => {
339
+ const messages = await lintMessages("export const W = () => <button>x</button>;");
340
+ expect(messages).toContain("Use Button from @terpjs/react-core, not a raw <button>.");
341
+ });
318
342
  });
package/src/spec.js CHANGED
@@ -15,7 +15,7 @@
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.24.0";
18
+ export const SPEC_VERSION = "0.25.0";
19
19
 
20
20
  export const BOUNDARY_SPEC = {
21
21
  /** App module files the boundary + frontend security defaults apply to. */
@@ -38,6 +38,26 @@ export const BOUNDARY_SPEC = {
38
38
  * react-core components (`Stack` for layout), never ad-hoc per screen. `className` would be
39
39
  * a side channel into hand-authored CSS, so it is refused alongside `style`.
40
40
  */
41
+ /**
42
+ * Extra guidance for an element whose named replacement does not fit every case, so the
43
+ * refusal states what to do instead of implying something is missing.
44
+ *
45
+ * `dialog` is the one that needed it. The replacement is `ConfirmDialog`, which is right
46
+ * for a confirmation and wrong advice for anything else — and an author who reads "use
47
+ * ConfirmDialog" for an edit form concludes the framework ships no dialog and that the
48
+ * rule cannot be obeyed. The guidance below is what a reporting app arrived at on its
49
+ * own, and recorded as the better outcome: the editor moved into an expanded row, so the
50
+ * finding that sent the author there stayed on screen while they fixed it.
51
+ */
52
+ restrictedElementGuidance: {
53
+ dialog:
54
+ "A modal is for a confirmation (ConfirmDialog) or an explicit post-action moment. " +
55
+ "An edit form or a detail view belongs in a routed page, or in an expanded row " +
56
+ "beside the thing it edits — which keeps the context that sent the author there on " +
57
+ "screen. If you have a case neither covers, say so rather than reaching for a raw " +
58
+ "<dialog>: the modal contract (focus trap, focus restore, Escape, top layer) is " +
59
+ "what ConfirmDialog exists to provide and a hand-rolled one silently drops.",
60
+ },
41
61
  restrictedAttributes: ["style", "className"],
42
62
  /**
43
63
  * Raw in-app anchors (`<a href="/...">`) bypass the router (full reload, no role-aware