@terpjs/eslint-boundaries 0.8.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 +2 -2
- package/src/index.js +10 -5
- package/src/index.test.js +24 -0
- package/src/spec.js +21 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/eslint-boundaries",
|
|
3
|
-
"version": "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.
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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.
|
|
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
|