@terpjs/eslint-boundaries 0.9.0 → 0.10.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/layouts.js +44 -3
- package/src/spec.js +23 -7
- package/src/surface.test.js +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terpjs/eslint-boundaries",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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.26.1",
|
|
29
29
|
"eslint": "^10.8.0",
|
|
30
30
|
"vitest": "^4.1.9"
|
|
31
31
|
},
|
package/src/layouts.js
CHANGED
|
@@ -35,9 +35,15 @@ export const LAYOUT_CONTRACTS = {
|
|
|
35
35
|
description:
|
|
36
36
|
"The standard three-level shape: hub bodies are card grids (HubCard only), " +
|
|
37
37
|
"overview bodies are data collections (DataView / ResourceList + framework " +
|
|
38
|
-
"states), detail bodies are record sections (DetailList / Stack / Tabs
|
|
39
|
-
"framework states); Card is allowed in overview and detail bodies as the " +
|
|
40
|
-
"sanctioned visual separation between sections
|
|
38
|
+
"states), detail bodies are record sections (DetailList / Stack / Grid / Tabs " +
|
|
39
|
+
"+ framework states); Card is allowed in overview and detail bodies as the " +
|
|
40
|
+
"sanctioned visual separation between sections, and Divider / Text as a rule " +
|
|
41
|
+
"between sections and a lead paragraph above them. Three specialised shapes sit " +
|
|
42
|
+
"beside those: a form body is a container (Stack) with optional Grid / Card " +
|
|
43
|
+
"sections and no Field at the top level, so the body is always a container rather " +
|
|
44
|
+
"than a loose run of controls; a settings body is Card sections and holds no " +
|
|
45
|
+
"collection; and a split " +
|
|
46
|
+
"body is two SplitPanes and nothing else. A bespoke screen composes " +
|
|
41
47
|
"the plain Page, which the contract deliberately leaves unconstrained.",
|
|
42
48
|
slots: {
|
|
43
49
|
HubPage: {
|
|
@@ -50,6 +56,8 @@ export const LAYOUT_CONTRACTS = {
|
|
|
50
56
|
ModuleNav: "module-nav",
|
|
51
57
|
Stack: "stack",
|
|
52
58
|
Card: "card",
|
|
59
|
+
Divider: "divider",
|
|
60
|
+
Text: "text",
|
|
53
61
|
EmptyState: "empty-state",
|
|
54
62
|
ErrorState: "error-state",
|
|
55
63
|
LoadingState: "loading-state",
|
|
@@ -57,6 +65,36 @@ export const LAYOUT_CONTRACTS = {
|
|
|
57
65
|
ConfirmDialog: "dialog",
|
|
58
66
|
},
|
|
59
67
|
},
|
|
68
|
+
FormPage: {
|
|
69
|
+
components: {
|
|
70
|
+
Stack: "stack",
|
|
71
|
+
Grid: "grid",
|
|
72
|
+
Card: "card",
|
|
73
|
+
Divider: "divider",
|
|
74
|
+
Text: "text",
|
|
75
|
+
EmptyState: "empty-state",
|
|
76
|
+
ErrorState: "error-state",
|
|
77
|
+
LoadingState: "loading-state",
|
|
78
|
+
Alert: "alert",
|
|
79
|
+
ConfirmDialog: "dialog",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
SettingsPage: {
|
|
83
|
+
components: {
|
|
84
|
+
Card: "card",
|
|
85
|
+
Stack: "stack",
|
|
86
|
+
Divider: "divider",
|
|
87
|
+
Text: "text",
|
|
88
|
+
EmptyState: "empty-state",
|
|
89
|
+
ErrorState: "error-state",
|
|
90
|
+
LoadingState: "loading-state",
|
|
91
|
+
Alert: "alert",
|
|
92
|
+
ConfirmDialog: "dialog",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
SplitPage: {
|
|
96
|
+
components: { SplitPane: "splitpane" },
|
|
97
|
+
},
|
|
60
98
|
DetailPage: {
|
|
61
99
|
components: {
|
|
62
100
|
DetailList: "detail-list",
|
|
@@ -65,6 +103,9 @@ export const LAYOUT_CONTRACTS = {
|
|
|
65
103
|
ModuleNav: "module-nav",
|
|
66
104
|
DataView: "dataview",
|
|
67
105
|
Card: "card",
|
|
106
|
+
Grid: "grid",
|
|
107
|
+
Divider: "divider",
|
|
108
|
+
Text: "text",
|
|
68
109
|
EmptyState: "empty-state",
|
|
69
110
|
ErrorState: "error-state",
|
|
70
111
|
LoadingState: "loading-state",
|
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.26.1";
|
|
19
19
|
|
|
20
20
|
export const BOUNDARY_SPEC = {
|
|
21
21
|
/** App module files the boundary + frontend security defaults apply to. */
|
|
@@ -42,12 +42,19 @@ export const BOUNDARY_SPEC = {
|
|
|
42
42
|
* Extra guidance for an element whose named replacement does not fit every case, so the
|
|
43
43
|
* refusal states what to do instead of implying something is missing.
|
|
44
44
|
*
|
|
45
|
-
*
|
|
46
|
-
* for
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
45
|
+
* The criterion: a restricted element earns a sentence here when its named replacement is
|
|
46
|
+
* right for some cases and actively misleading for others, so that an author who reads the
|
|
47
|
+
* bare rule concludes the framework ships nothing for their case. Two elements meet it.
|
|
48
|
+
*
|
|
49
|
+
* `dialog` was the first. The replacement is `ConfirmDialog`, which is right for a
|
|
50
|
+
* confirmation and wrong advice for anything else — an author who reads "use ConfirmDialog"
|
|
51
|
+
* for an edit form concludes the framework ships no dialog and the rule cannot be obeyed.
|
|
52
|
+
* The guidance below is what a reporting app arrived at on its own, and recorded as the
|
|
53
|
+
* better outcome: the editor moved into an expanded row, so the finding that sent the author
|
|
54
|
+
* there stayed on screen while they fixed it.
|
|
55
|
+
*
|
|
56
|
+
* `table` is the second (ADR 0099 §4). "Use DataView" reads as disproportionate for five
|
|
57
|
+
* static rows unless the sentence names the two-line recipe, so it does.
|
|
51
58
|
*/
|
|
52
59
|
restrictedElementGuidance: {
|
|
53
60
|
dialog:
|
|
@@ -57,6 +64,15 @@ export const BOUNDARY_SPEC = {
|
|
|
57
64
|
"screen. If you have a case neither covers, say so rather than reaching for a raw " +
|
|
58
65
|
"<dialog>: the modal contract (focus trap, focus restore, Escape, top layer) is " +
|
|
59
66
|
"what ConfirmDialog exists to provide and a hand-rolled one silently drops.",
|
|
67
|
+
table:
|
|
68
|
+
"Use DataView, and if it looks like too much for five static rows, the recipe is " +
|
|
69
|
+
"two lines: variant=\"embedded\" drops the view toggle, the page-size selector and " +
|
|
70
|
+
"the footer, and InMemoryDataViewRepository needs two functions: getRowId and " +
|
|
71
|
+
"getValue. That is worth " +
|
|
72
|
+
"saying because \"use DataView\" on its own reads as wrong advice here, and an " +
|
|
73
|
+
"author who believes it reaches for a raw <table> — which silently gives up " +
|
|
74
|
+
"sorting, column resizing and settings, the mobile card reflow and the row-level " +
|
|
75
|
+
"activation the rest of the app has.",
|
|
60
76
|
},
|
|
61
77
|
restrictedAttributes: ["style", "className"],
|
|
62
78
|
/**
|
package/src/surface.test.js
CHANGED
|
@@ -44,6 +44,22 @@ describe("structural parity: BOUNDARY_SPEC realises exactly the declared surface
|
|
|
44
44
|
);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
it("every guidance sentence belongs to an element that is actually restricted", () => {
|
|
48
|
+
// ADR 0099 §4 makes the WORDING a control: a restricted element whose named replacement is
|
|
49
|
+
// right for some cases and wrong advice for others gets a sentence naming the recipe. A
|
|
50
|
+
// sentence keyed to an element nobody restricts is never emitted, so it reads as shipped
|
|
51
|
+
// guidance while being unreachable — and nothing else here would notice, because the two
|
|
52
|
+
// objects are only ever read one key at a time at lint time.
|
|
53
|
+
const guided = Object.keys(BOUNDARY_SPEC.restrictedElementGuidance).sort();
|
|
54
|
+
const restricted = Object.keys(BOUNDARY_SPEC.restrictedElements);
|
|
55
|
+
expect(guided.filter((element) => !restricted.includes(element))).toEqual([]);
|
|
56
|
+
// And the sentences are non-empty: an empty string is falsy, so it would silently fall back
|
|
57
|
+
// to the bare refusal at the call site.
|
|
58
|
+
for (const element of guided) {
|
|
59
|
+
expect(BOUNDARY_SPEC.restrictedElementGuidance[element].length).toBeGreaterThan(0);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
47
63
|
it("restricted attributes match", () => {
|
|
48
64
|
expect([...BOUNDARY_SPEC.restrictedAttributes].sort()).toEqual(
|
|
49
65
|
[...SURFACE.restrictedAttributes].sort(),
|