@multiplatform.one/config 7.11.0 → 7.16.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/README.md +35 -10
- package/lib/index.js +2 -2
- package/lib/{storybook-CGNSgSBS.js → storybook-DFdvDJ_s.js} +3 -1
- package/lib/storybook.js +1 -1
- package/lib/{vite-D6vMjPwv.js → vite-DXXr5In0.js} +167 -12
- package/lib/vite.js +3 -3
- package/lib/{workspacePublicPackages-B4FGgRYG.js → workspacePublicPackages-BuYRXxFs.js} +10 -3
- package/lint.d.ts +8 -0
- package/lint.mjs +350 -13
- package/oxlint.d.ts +2 -0
- package/oxlint.json +21 -0
- package/oxlint.mjs +168 -1
- package/package.json +8 -4
- package/spec-registry.mjs +183 -0
- package/src/lint.spec.ts +493 -1
- package/src/oxlint.spec.ts +172 -0
- package/src/oxlintPreset.spec.ts +65 -0
- package/src/publicConfig.spec.ts +272 -0
- package/src/publicConfig.ts +170 -0
- package/src/singletonDepAliases.spec.ts +52 -0
- package/src/singletonDepAliases.ts +10 -3
- package/src/spec-registry.spec.ts +107 -0
- package/src/storybook.spec.ts +2 -0
- package/src/storybook.ts +7 -0
- package/src/vite.spec.ts +58 -2
- package/src/vite.ts +53 -8
- package/types/publicConfig.d.ts +36 -0
- package/types/publicConfig.d.ts.map +1 -0
- package/types/singletonDepAliases.d.ts.map +1 -1
- package/types/storybook.d.ts.map +1 -1
- package/types/vite.d.ts +13 -4
- package/types/vite.d.ts.map +1 -1
package/oxlint.mjs
CHANGED
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* mpo-conventions/no-hex-literals — ban #rgb/#rrggbb(/aa) and rgb()/hsl()
|
|
10
10
|
* string literals. Palette modules (themes/base, themes/accent, and
|
|
11
11
|
* files whose name contains "palette") are exempt: a 12-step ramp is
|
|
12
|
-
* the deliverable.
|
|
12
|
+
* the deliverable. So is a file with a `// hex-escape: <reason>`
|
|
13
|
+
* comment, a surface no theme token reaches; lint.mjs fails the comment
|
|
14
|
+
* unless the spec's `## Colour-literal escapes` table has that file and
|
|
15
|
+
* reason (MPO-326).
|
|
13
16
|
* mpo-conventions/no-raw-typography — AN-8: app text never rides raw
|
|
14
17
|
* Tamagui*-prefixed typography escape hatches (TamaguiText, TamaguiHeading,
|
|
15
18
|
* TamaguiH1…, TamaguiParagraph, TamaguiSizableText, TamaguiAnchor) outside
|
|
@@ -23,9 +26,15 @@
|
|
|
23
26
|
* on those same imports. Silent behind sizeRecipeEscape() and inside a
|
|
24
27
|
* file listed as DRAWING. An undeclared `// mpo-drawing` pragma is a
|
|
25
28
|
* lint.mjs failure, not a skip here.
|
|
29
|
+
* mpo-conventions/no-child-margin — `00` L9a: the parent owns the gap. Ban a
|
|
30
|
+
* non-zero margin* prop on any element nested inside another element's
|
|
31
|
+
* JSX. The outermost element of a tree is left alone (a file cannot see
|
|
32
|
+
* whether its root is a screen's), and element names the spec's
|
|
33
|
+
* `## Slot margins` SLOT-MARGIN row lists are the declared slots (MPO-23).
|
|
26
34
|
*/
|
|
27
35
|
|
|
28
36
|
import { isDrawingFile, readDrawingRegistry } from "./drawing-registry.mjs";
|
|
37
|
+
import { readSlotMarginRegistry } from "./spec-registry.mjs";
|
|
29
38
|
|
|
30
39
|
/** Hex + functional CSS colour literals. AST-visited only (not a text scan). */
|
|
31
40
|
export const COLOR_LITERAL =
|
|
@@ -35,6 +44,41 @@ export const COLOR_LITERAL =
|
|
|
35
44
|
export const PALETTE_FILE =
|
|
36
45
|
/(?:^|\/)themes\/(base|accent)\.(tsx?|jsx?)$|(?:^|\/)[^/]*palette[^/]*\.(tsx?|jsx?)$/i;
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* A `hex-escape: <reason>` line or block comment. A JSDoc `*` line or a
|
|
49
|
+
* backticked mention is prose about the marker, not the marker.
|
|
50
|
+
*/
|
|
51
|
+
const HEX_ESCAPE_COMMENT = /(?<!`)(?:\/\/|\/\*)\s*hex-escape:\s*(.*?)\s*(?:\*\/.*)?$/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every `hex-escape:` comment in a source file, with the reason it writes.
|
|
55
|
+
* @param {string} src
|
|
56
|
+
* @returns {{ line: number, reason: string }[]}
|
|
57
|
+
*/
|
|
58
|
+
export function findHexEscapes(src) {
|
|
59
|
+
if (!src.includes("hex-escape:")) return [];
|
|
60
|
+
/** @type {{ line: number, reason: string }[]} */
|
|
61
|
+
const sites = [];
|
|
62
|
+
src.split("\n").forEach((text, index) => {
|
|
63
|
+
if (text.trim().startsWith("*")) return;
|
|
64
|
+
const match = text.match(HEX_ESCAPE_COMMENT);
|
|
65
|
+
if (match) sites.push({ line: index + 1, reason: match[1].trim() });
|
|
66
|
+
});
|
|
67
|
+
return sites;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {unknown} context
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function sourceText(context) {
|
|
75
|
+
const ctx =
|
|
76
|
+
/** @type {{ sourceCode?: { text?: string }, getSourceCode?: () => { text?: string } }} */ (
|
|
77
|
+
context
|
|
78
|
+
);
|
|
79
|
+
return ctx?.sourceCode?.text ?? ctx?.getSourceCode?.()?.text ?? "";
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
const RAW_TYPOGRAPHY_HATCH = /^Tamagui(?:Text|SizableText|Paragraph|Heading|H[1-6]|Anchor)$/;
|
|
39
83
|
const HOUSE_PACKAGE = /^@multiplatform\.one\//;
|
|
40
84
|
|
|
@@ -105,7 +149,40 @@ export const GEOMETRY_PROPS = Object.freeze([
|
|
|
105
149
|
"fontSize",
|
|
106
150
|
]);
|
|
107
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Every spelling of an outer margin: the long props @tamagui/helpers registers
|
|
154
|
+
* and the Tamagui shorthands for them. `00` L9a overrules the LAYOUT_PROPS
|
|
155
|
+
* allowance for sibling margins above: the place among siblings is the
|
|
156
|
+
* parent's `gap`.
|
|
157
|
+
*/
|
|
158
|
+
export const MARGIN_PROPS = Object.freeze([
|
|
159
|
+
"margin",
|
|
160
|
+
"marginTop",
|
|
161
|
+
"marginRight",
|
|
162
|
+
"marginBottom",
|
|
163
|
+
"marginLeft",
|
|
164
|
+
"marginHorizontal",
|
|
165
|
+
"marginVertical",
|
|
166
|
+
"marginStart",
|
|
167
|
+
"marginEnd",
|
|
168
|
+
"marginBlock",
|
|
169
|
+
"marginBlockStart",
|
|
170
|
+
"marginBlockEnd",
|
|
171
|
+
"marginInline",
|
|
172
|
+
"marginInlineStart",
|
|
173
|
+
"marginInlineEnd",
|
|
174
|
+
"m",
|
|
175
|
+
"mt",
|
|
176
|
+
"mr",
|
|
177
|
+
"mb",
|
|
178
|
+
"ml",
|
|
179
|
+
"mx",
|
|
180
|
+
"my",
|
|
181
|
+
]);
|
|
182
|
+
|
|
108
183
|
const CHROME_PROP_SET = new Set(CHROME_PROPS);
|
|
184
|
+
const MARGIN_PROP_SET = new Set(MARGIN_PROPS);
|
|
185
|
+
const ZERO_MARGIN = /^(?:-?0(?:\.0+)?(?:px)?|\$0)$/;
|
|
109
186
|
const GEOMETRY_PROP_SET = new Set(GEOMETRY_PROPS);
|
|
110
187
|
|
|
111
188
|
const RULE_OPTIONS_SCHEMA = [
|
|
@@ -119,6 +196,48 @@ const RULE_OPTIONS_SCHEMA = [
|
|
|
119
196
|
},
|
|
120
197
|
];
|
|
121
198
|
|
|
199
|
+
const SLOT_MARGIN_OPTIONS_SCHEMA = [
|
|
200
|
+
{
|
|
201
|
+
type: "object",
|
|
202
|
+
additionalProperties: false,
|
|
203
|
+
properties: {
|
|
204
|
+
slotMargins: { type: "array", items: { type: "string" } },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {unknown} context
|
|
211
|
+
* @returns {Set<string>}
|
|
212
|
+
*/
|
|
213
|
+
function resolveSlotMargins(context) {
|
|
214
|
+
const opt = context?.options?.[0] ?? {};
|
|
215
|
+
if (Array.isArray(opt.slotMargins)) return new Set(opt.slotMargins);
|
|
216
|
+
try {
|
|
217
|
+
return new Set(readSlotMarginRegistry().members);
|
|
218
|
+
} catch {
|
|
219
|
+
return new Set();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* `0` is `00` L8 E1, legal anywhere: a zero-reset is the absence of an outer
|
|
225
|
+
* margin, not one (Tamagui's own headings ship `margin: 0`).
|
|
226
|
+
* @param {unknown} value
|
|
227
|
+
*/
|
|
228
|
+
function isZeroMargin(value) {
|
|
229
|
+
const node = unwrapJsxValue(value);
|
|
230
|
+
if (!node || typeof node !== "object") return false;
|
|
231
|
+
const expr =
|
|
232
|
+
/** @type {{ type?: string, value?: unknown, operator?: string, argument?: unknown }} */ (node);
|
|
233
|
+
if (expr.type === "Literal") {
|
|
234
|
+
if (typeof expr.value === "number") return expr.value === 0;
|
|
235
|
+
if (typeof expr.value === "string") return ZERO_MARGIN.test(expr.value.trim());
|
|
236
|
+
}
|
|
237
|
+
if (expr.type === "UnaryExpression" && expr.operator === "-") return isZeroMargin(expr.argument);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
122
241
|
/**
|
|
123
242
|
* @param {unknown} context
|
|
124
243
|
* @returns {{ drawings: string[], chromeAllow: Set<string> }}
|
|
@@ -330,6 +449,9 @@ export const plugin = {
|
|
|
330
449
|
if (isPaletteModule(filename)) {
|
|
331
450
|
return {};
|
|
332
451
|
}
|
|
452
|
+
if (findHexEscapes(sourceText(context)).some((site) => site.reason)) {
|
|
453
|
+
return {};
|
|
454
|
+
}
|
|
333
455
|
|
|
334
456
|
function reportIfColor(node, value) {
|
|
335
457
|
const matched = matchColorLiteral(value);
|
|
@@ -445,6 +567,51 @@ export const plugin = {
|
|
|
445
567
|
return visitors;
|
|
446
568
|
},
|
|
447
569
|
},
|
|
570
|
+
"no-child-margin": {
|
|
571
|
+
meta: {
|
|
572
|
+
type: "problem",
|
|
573
|
+
docs: {
|
|
574
|
+
description:
|
|
575
|
+
"00 L9a: the parent owns the gap. Ban a non-zero margin* prop on any element nested inside another element's JSX, unless the spec declares the element a slot.",
|
|
576
|
+
},
|
|
577
|
+
schema: SLOT_MARGIN_OPTIONS_SCHEMA,
|
|
578
|
+
messages: {
|
|
579
|
+
childMargin:
|
|
580
|
+
"{{prop}} on {{name}} is an outer margin on a child (00 L9a). Set `gap` on the parent, express the slot as flex, or declare {{name}} in the spec's ## Slot margins table.",
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
create(context) {
|
|
584
|
+
const slotMargins = resolveSlotMargins(context);
|
|
585
|
+
let depth = 0;
|
|
586
|
+
const enter = () => {
|
|
587
|
+
depth += 1;
|
|
588
|
+
};
|
|
589
|
+
const exit = () => {
|
|
590
|
+
depth -= 1;
|
|
591
|
+
};
|
|
592
|
+
return {
|
|
593
|
+
JSXElement: enter,
|
|
594
|
+
"JSXElement:exit": exit,
|
|
595
|
+
JSXFragment: enter,
|
|
596
|
+
"JSXFragment:exit": exit,
|
|
597
|
+
JSXOpeningElement(node) {
|
|
598
|
+
if (depth < 2) return;
|
|
599
|
+
const name = jsxName(node.name);
|
|
600
|
+
if (elementAllowKeys(name).some((key) => slotMargins.has(key))) return;
|
|
601
|
+
for (const attr of node.attributes ?? []) {
|
|
602
|
+
const prop = jsxAttrName(attr);
|
|
603
|
+
if (!MARGIN_PROP_SET.has(prop)) continue;
|
|
604
|
+
if (isZeroMargin(attr.value)) continue;
|
|
605
|
+
context.report({
|
|
606
|
+
node: attr,
|
|
607
|
+
messageId: "childMargin",
|
|
608
|
+
data: { prop, name: name || "element" },
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
},
|
|
614
|
+
},
|
|
448
615
|
},
|
|
449
616
|
};
|
|
450
617
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multiplatform.one/config",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.16.0",
|
|
4
4
|
"description": "Shared build and test configuration presets for multiplatform.one",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"config",
|
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
],
|
|
16
16
|
"homepage": "https://multiplatform.one",
|
|
17
17
|
"bugs": {
|
|
18
|
-
"url": "https://
|
|
18
|
+
"url": "https://projects.corp.bitspur.com/tracker/MPO",
|
|
19
19
|
"email": "support@risserlabs.com"
|
|
20
20
|
},
|
|
21
21
|
"license": "Apache-2.0",
|
|
22
22
|
"author": "BitSpur <support@risserlabs.com> (https://risserlabs.com)",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
|
-
"url": "git+https://
|
|
25
|
+
"url": "git+https://git.corp.bitspur.com/multiplatform.one/mpo.git",
|
|
26
26
|
"directory": "public/config"
|
|
27
27
|
},
|
|
28
28
|
"source": "src/index.ts",
|
|
@@ -34,9 +34,11 @@
|
|
|
34
34
|
"README.md",
|
|
35
35
|
"oxlint.mjs",
|
|
36
36
|
"oxlint.d.ts",
|
|
37
|
+
"oxlint.json",
|
|
37
38
|
"lint.mjs",
|
|
38
39
|
"lint.d.ts",
|
|
39
40
|
"drawing-registry.mjs",
|
|
41
|
+
"spec-registry.mjs",
|
|
40
42
|
"!tsconfig/dist",
|
|
41
43
|
"!tsconfig/types",
|
|
42
44
|
"types",
|
|
@@ -84,7 +86,9 @@
|
|
|
84
86
|
"./tsconfig/app.json": "./tsconfig/app.json",
|
|
85
87
|
"./react-native-config": "./src/react-native.config.cjs",
|
|
86
88
|
"./oxlint": "./oxlint.mjs",
|
|
89
|
+
"./oxlint.json": "./oxlint.json",
|
|
87
90
|
"./drawing-registry": "./drawing-registry.mjs",
|
|
91
|
+
"./spec-registry": "./spec-registry.mjs",
|
|
88
92
|
"./lint": {
|
|
89
93
|
"types": "./lint.d.ts",
|
|
90
94
|
"default": "./lint.mjs",
|
|
@@ -95,7 +99,7 @@
|
|
|
95
99
|
"access": "public"
|
|
96
100
|
},
|
|
97
101
|
"dependencies": {
|
|
98
|
-
"@multiplatform.one/utils": "7.
|
|
102
|
+
"@multiplatform.one/utils": "7.16.0",
|
|
99
103
|
"@tamagui/vite-plugin": "2.7.6",
|
|
100
104
|
"@vitejs/plugin-react": "^6.0.1",
|
|
101
105
|
"dotenv": "^17.4.2",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slot-margin, size-recipe-escape and colour-literal-escape registries — parsed from the living
|
|
3
|
+
* rulebook, the same way drawing-registry.mjs and
|
|
4
|
+
* scripts/radius-identity-registry.mjs read theirs (MPO-23).
|
|
5
|
+
*
|
|
6
|
+
* `00` L8: an escape is declared by name in a committed registry that is
|
|
7
|
+
* generated from the spec, not from the code, so writing more violations can
|
|
8
|
+
* never widen the allowlist. Both tables live in
|
|
9
|
+
* `docs/theme-propagation-spec.md`:
|
|
10
|
+
*
|
|
11
|
+
* ## Slot margins `| Class | Normative meaning |`, one SLOT-MARGIN
|
|
12
|
+
* row whose **bolded** `Members:` are the element
|
|
13
|
+
* names that may carry a margin (L9a).
|
|
14
|
+
* ## Size-recipe escapes `| File | Reason |`, one row per escape site; the
|
|
15
|
+
* reason is the exact text the code writes (L8 E8).
|
|
16
|
+
* ## Colour-literal escapes `| File | Reason |`, one row per file whose
|
|
17
|
+
* `hex-escape:` comment exempts it from
|
|
18
|
+
* no-hex-literals; same verbatim reason (MPO-326).
|
|
19
|
+
*
|
|
20
|
+
* A consumer tree with no spec, or a spec without the section, yields
|
|
21
|
+
* `present: false` rather than throwing, so shc can pick the rules up before
|
|
22
|
+
* it has copied the tables.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* import { readSlotMarginRegistry, readSizeRecipeEscapeRegistry, readHexEscapeRegistry } from "./spec-registry.mjs";
|
|
26
|
+
* node spec-registry.mjs [--json]
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import { pathToFileURL } from "node:url";
|
|
30
|
+
import { defaultSpecPath } from "./drawing-registry.mjs";
|
|
31
|
+
|
|
32
|
+
export const SLOT_MARGIN_HEADING = "## Slot margins";
|
|
33
|
+
export const SLOT_MARGIN_CLASS = "SLOT-MARGIN";
|
|
34
|
+
export const SIZE_RECIPE_ESCAPE_HEADING = "## Size-recipe escapes";
|
|
35
|
+
export const HEX_ESCAPE_HEADING = "## Colour-literal escapes";
|
|
36
|
+
|
|
37
|
+
/** `` `a — b` `` → `a — b`. */
|
|
38
|
+
function unwrapCode(cell) {
|
|
39
|
+
const match = cell.match(/^`(.*)`$/);
|
|
40
|
+
return match ? match[1] : cell;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Body rows of the first markdown table under `heading`, header and separator
|
|
45
|
+
* dropped. `null` when the heading is absent.
|
|
46
|
+
* @param {string} markdown
|
|
47
|
+
* @param {string} heading e.g. "## Slot margins"
|
|
48
|
+
* @returns {string[][] | null}
|
|
49
|
+
*/
|
|
50
|
+
export function readSpecTable(markdown, heading) {
|
|
51
|
+
const lines = markdown.split("\n");
|
|
52
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
53
|
+
if (start === -1) return null;
|
|
54
|
+
/** @type {string[][]} */
|
|
55
|
+
const rows = [];
|
|
56
|
+
let seenTable = false;
|
|
57
|
+
let seenHeader = false;
|
|
58
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
59
|
+
const line = lines[i].trim();
|
|
60
|
+
if (/^#{1,2}\s/.test(line)) break;
|
|
61
|
+
if (!line.startsWith("|")) {
|
|
62
|
+
if (seenTable) break;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seenTable = true;
|
|
66
|
+
const cells = line
|
|
67
|
+
.split("|")
|
|
68
|
+
.slice(1, -1)
|
|
69
|
+
.map((cell) => cell.trim());
|
|
70
|
+
if (/^:?-+:?$/.test(cells[0] ?? "")) continue;
|
|
71
|
+
if (!seenHeader) {
|
|
72
|
+
seenHeader = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
rows.push(cells);
|
|
76
|
+
}
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `Members: **Card.Footer**, **Card.Header**.` → ["Card.Footer", "Card.Header"]. */
|
|
81
|
+
function parseMembers(text) {
|
|
82
|
+
const match = text.match(/Members:\s*(.*)$/i);
|
|
83
|
+
if (!match) return [];
|
|
84
|
+
return [...match[1].matchAll(/\*\*([^*]+)\*\*/g)].map((m) => m[1].trim()).filter(Boolean);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} specPath
|
|
89
|
+
* @param {string} heading
|
|
90
|
+
*/
|
|
91
|
+
function readTable(specPath, heading) {
|
|
92
|
+
if (!existsSync(specPath)) return null;
|
|
93
|
+
return readSpecTable(readFileSync(specPath, "utf8"), heading);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Element names that may carry a margin because the spec declares them an
|
|
98
|
+
* internal slot (`00` L9a: `Card.Footer` `marginTop: auto` is the precedent).
|
|
99
|
+
*
|
|
100
|
+
* @param {string} [specPath]
|
|
101
|
+
* @returns {{ specPath: string, present: boolean, members: string[] }}
|
|
102
|
+
*/
|
|
103
|
+
export function readSlotMarginRegistry(specPath = defaultSpecPath()) {
|
|
104
|
+
const rows = readTable(specPath, SLOT_MARGIN_HEADING);
|
|
105
|
+
if (!rows) return { specPath, present: false, members: [] };
|
|
106
|
+
const row = rows.find((cells) => cells[0] === SLOT_MARGIN_CLASS);
|
|
107
|
+
if (!row) {
|
|
108
|
+
throw new Error(`${specPath}: "${SLOT_MARGIN_HEADING}" has no ${SLOT_MARGIN_CLASS} row`);
|
|
109
|
+
}
|
|
110
|
+
return { specPath, present: true, members: parseMembers(row[1] ?? "") };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* `| File | Reason |` rows under `heading`: one `{ file, reason }` per row,
|
|
115
|
+
* placeholder `_(none)_` rows dropped.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} specPath
|
|
118
|
+
* @param {string} heading
|
|
119
|
+
* @returns {{ specPath: string, present: boolean, escapes: { file: string, reason: string }[] }}
|
|
120
|
+
*/
|
|
121
|
+
function readFileReasonRegistry(specPath, heading) {
|
|
122
|
+
const rows = readTable(specPath, heading);
|
|
123
|
+
if (!rows) return { specPath, present: false, escapes: [] };
|
|
124
|
+
/** @type {{ file: string, reason: string }[]} */
|
|
125
|
+
const escapes = [];
|
|
126
|
+
for (const cells of rows) {
|
|
127
|
+
const file = unwrapCode(cells[0] ?? "");
|
|
128
|
+
const reason = unwrapCode(cells[1] ?? "");
|
|
129
|
+
if (!file || /^[_*]?\(none/.test(file)) continue;
|
|
130
|
+
if (!reason) {
|
|
131
|
+
throw new Error(`${specPath}: "${heading}" row for ${file} has no reason`);
|
|
132
|
+
}
|
|
133
|
+
escapes.push({ file, reason });
|
|
134
|
+
}
|
|
135
|
+
return { specPath, present: true, escapes };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Declared size-recipe escape sites: one `{ file, reason }` per row. `file` is
|
|
140
|
+
* a path from the tree root; `reason` is compared verbatim with the reason the
|
|
141
|
+
* code writes, so rewording an escape is a spec edit too.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} [specPath]
|
|
144
|
+
* @returns {{ specPath: string, present: boolean, escapes: { file: string, reason: string }[] }}
|
|
145
|
+
*/
|
|
146
|
+
export function readSizeRecipeEscapeRegistry(specPath = defaultSpecPath()) {
|
|
147
|
+
return readFileReasonRegistry(specPath, SIZE_RECIPE_ESCAPE_HEADING);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Files exempt from `mpo-conventions/no-hex-literals`: one `{ file, reason }`
|
|
152
|
+
* per row, matched verbatim against the file's `hex-escape:` comment. These
|
|
153
|
+
* are surfaces no theme token reaches (GTK widget CSS outside the provider, a
|
|
154
|
+
* content script on third-party pages, the browser badge API, native build
|
|
155
|
+
* config).
|
|
156
|
+
*
|
|
157
|
+
* @param {string} [specPath]
|
|
158
|
+
* @returns {{ specPath: string, present: boolean, escapes: { file: string, reason: string }[] }}
|
|
159
|
+
*/
|
|
160
|
+
export function readHexEscapeRegistry(specPath = defaultSpecPath()) {
|
|
161
|
+
return readFileReasonRegistry(specPath, HEX_ESCAPE_HEADING);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const invokedDirectly =
|
|
165
|
+
typeof process.argv[1] === "string" && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
166
|
+
|
|
167
|
+
if (invokedDirectly) {
|
|
168
|
+
const slots = readSlotMarginRegistry();
|
|
169
|
+
const escapes = readSizeRecipeEscapeRegistry();
|
|
170
|
+
const hexEscapes = readHexEscapeRegistry();
|
|
171
|
+
if (process.argv.includes("--json")) {
|
|
172
|
+
console.log(JSON.stringify({ slots, escapes, hexEscapes }, null, 2));
|
|
173
|
+
} else {
|
|
174
|
+
console.log(`spec registries from ${slots.specPath}`);
|
|
175
|
+
console.log(
|
|
176
|
+
` SLOT-MARGIN (present=${slots.present}): ${slots.members.join(", ") || "(none)"}`,
|
|
177
|
+
);
|
|
178
|
+
console.log(` size-recipe escapes (present=${escapes.present}):`);
|
|
179
|
+
for (const { file, reason } of escapes.escapes) console.log(` ${file} — ${reason}`);
|
|
180
|
+
console.log(` colour-literal escapes (present=${hexEscapes.present}):`);
|
|
181
|
+
for (const { file, reason } of hexEscapes.escapes) console.log(` ${file} — ${reason}`);
|
|
182
|
+
}
|
|
183
|
+
}
|