@weatherboard/gyde-design 0.5.0 → 0.6.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 +38 -0
- package/floor.mjs +107 -0
- package/package.json +26 -19
- package/theme-entry.mjs +87 -13
- package/themehost.mjs +54 -10
package/README.md
CHANGED
|
@@ -171,6 +171,24 @@ into their repository.
|
|
|
171
171
|
product's; a scaffolder that clobbers has taken ownership of something it does
|
|
172
172
|
not own, silently.
|
|
173
173
|
|
|
174
|
+
## Importing a module directly
|
|
175
|
+
|
|
176
|
+
The barrel (`@weatherboard/gyde-design`) is the curated named surface. Every
|
|
177
|
+
module the package ships is also reachable by its own subpath:
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
import { themeStartup } from "@weatherboard/gyde-design/theme-entry.mjs";
|
|
181
|
+
import pkg from "@weatherboard/gyde-design/package.json" with { type: "json" };
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
That is a stated guarantee rather than an accident of file layout, and
|
|
185
|
+
`exports.test.mjs` fails if a shipped module is not reachable — so verifying
|
|
186
|
+
your own repository never means reaching into `node_modules` by filesystem
|
|
187
|
+
path. The two exceptions are named there with their reasons: the CLI, which is
|
|
188
|
+
the `bin` and executes on import, and the vendored parsers under `vendor/`,
|
|
189
|
+
which ship so the Action can run with nothing installed and are not ours to
|
|
190
|
+
promise.
|
|
191
|
+
|
|
174
192
|
## The rules, as data
|
|
175
193
|
|
|
176
194
|
A rule id reaches you three times — in a finding, in the `rules` stamp inside
|
|
@@ -368,6 +386,19 @@ whatever lands under that name next, and it is the only way the list could
|
|
|
368
386
|
shrink invisibly. The same discipline the allowance ledger has — it may only
|
|
369
387
|
ever get shorter, and never by accident.
|
|
370
388
|
|
|
389
|
+
**One `file` + tag may repeat, but not with different reasons.** An exception
|
|
390
|
+
identifies its element by file and tag name matched in source order, so nothing
|
|
391
|
+
records *which* `<div>` a reason was written about. Many elements sharing one
|
|
392
|
+
argument is normal and safe — an inline-styled block of markup is genuinely one
|
|
393
|
+
decision — and reordering them changes nothing a reader could act on. Two
|
|
394
|
+
entries for one file and tag carrying **different** arguments is a different
|
|
395
|
+
thing: a reorder reattaches each reason to an element nobody wrote it about,
|
|
396
|
+
with the count, the figure and the verdict all unchanged. That is an **error**,
|
|
397
|
+
and the fix is one reason true of the whole group, because that is the
|
|
398
|
+
granularity the exemption actually has. No occurrence index is offered: an
|
|
399
|
+
explicit ordinal makes the identity visible without making it stable, so the
|
|
400
|
+
same reorder still reattaches the reasons, now with a number agreeing.
|
|
401
|
+
|
|
371
402
|
**Every path fails closed.** A malformed entry, an unknown key, a scope that
|
|
372
403
|
does not resolve, a scope that matches no markup files, a scope whose files make
|
|
373
404
|
no visual decision, an unreadable component barrel, an exception naming
|
|
@@ -382,6 +413,13 @@ and a rule that matches nothing reads exactly like a rule that passes.
|
|
|
382
413
|
bespoke element and carries a reason somebody wrote. It cannot tell a true
|
|
383
414
|
reason from a plausible sentence. What it buys is that every one of them is
|
|
384
415
|
on the artefact, in the diff, with a name and a line number attached.
|
|
416
|
+
- **Element identity is still ordinal.** The reason-collision check above is a
|
|
417
|
+
check on the *configuration*, never on the source. A single entry for `<div>`
|
|
418
|
+
in a file that later grows a second bespoke `<div>` forms no group, so nothing
|
|
419
|
+
fires — and that entry now matches whichever comes first in source order,
|
|
420
|
+
which may not be the one it was written about. Within an identical-reason
|
|
421
|
+
group nothing is guarded at all, by design; if one member is later edited so
|
|
422
|
+
the shared reason stops being true of it, the group still validates.
|
|
385
423
|
- **It measures markup, not rendering.** An element is the system's if its tag
|
|
386
424
|
is one of the design system's exports. A component that imports the system
|
|
387
425
|
and then restyles it at the call site still counts — that is the styling
|
package/floor.mjs
CHANGED
|
@@ -149,6 +149,112 @@ const EXCEPTION_KEYS = new Set(["file", "element", "because"]);
|
|
|
149
149
|
*/
|
|
150
150
|
const MIN_REASON = 12;
|
|
151
151
|
|
|
152
|
+
/**
|
|
153
|
+
* G-135 — two exceptions for one `file` + tag, carrying DIFFERENT reasons.
|
|
154
|
+
*
|
|
155
|
+
* ===========================================================================
|
|
156
|
+
* THE DEFECT, AND WHY THE OBVIOUS GUARD IS THE WRONG ONE
|
|
157
|
+
* ===========================================================================
|
|
158
|
+
*
|
|
159
|
+
* `measureFloor` identifies an excepted element by `file` + tag name, matched
|
|
160
|
+
* in SOURCE ORDER: the first unclaimed `<div>` in that file, then the next.
|
|
161
|
+
* Nothing in the configuration says WHICH `div`, because nothing in the
|
|
162
|
+
* configuration can. So move a block, wrap something, reorder a return — and
|
|
163
|
+
* entry 1's reason attaches to entry 2's element. Every element is still real,
|
|
164
|
+
* the count is still right, the floor is still met: the gate stays green and
|
|
165
|
+
* the ledger now misdescribes which element was argued for and why. A silent
|
|
166
|
+
* wrong answer, which this repository holds to be worse than a red one.
|
|
167
|
+
*
|
|
168
|
+
* The obvious guard — refuse any repeated `file` + tag — is too blunt, and
|
|
169
|
+
* measurably so. Against the first consumer to adopt a floor, fourteen of
|
|
170
|
+
* nineteen exceptions sat in same-file-same-tag groups, and thirteen of those
|
|
171
|
+
* were one inline-styled preview surface: many elements, genuinely ONE
|
|
172
|
+
* argument. Reordering them is harmless, because the reasons are
|
|
173
|
+
* interchangeable in fact and not merely in the matcher. Forcing thirteen
|
|
174
|
+
* distinct sentences there would produce worse prose, not better identity.
|
|
175
|
+
*
|
|
176
|
+
* So the hazard is not repetition. It is **repetition carrying different
|
|
177
|
+
* reasons**, and that is exactly what is checked here.
|
|
178
|
+
*
|
|
179
|
+
* ===========================================================================
|
|
180
|
+
* WHY THIS REFUSES RATHER THAN WARNS
|
|
181
|
+
* ===========================================================================
|
|
182
|
+
*
|
|
183
|
+
* Every other malformed floor is an error — an unknown key, a duplicate scope,
|
|
184
|
+
* a zero-match scope, a stale exception. A warning would be the one advisory
|
|
185
|
+
* thing in a fail-closed file, and this repository's standing finding is that
|
|
186
|
+
* a gate nobody must act on is a gate that does not exist.
|
|
187
|
+
*
|
|
188
|
+
* The usual objection to refusing is that it removes a capability with nothing
|
|
189
|
+
* to replace it. It does not, and this is the crux: the per-element precision
|
|
190
|
+
* a differing-reason group appears to have **was never delivered**. Four divs
|
|
191
|
+
* with four reasons read as four arguments about four elements; the matcher
|
|
192
|
+
* can only offer "four of the divs in this file are excepted". The
|
|
193
|
+
* configuration is claiming a precision the mechanism does not have, and
|
|
194
|
+
* refusing it is Gyde declining to keep up the appearance.
|
|
195
|
+
*
|
|
196
|
+
* The way to express it is therefore already in the format, needs no new key,
|
|
197
|
+
* and is what the error says: **write one reason that covers the group.** A
|
|
198
|
+
* single argument true of all four members is a claim the matcher can actually
|
|
199
|
+
* honour, and a reader who disagrees with it disagrees with all four at once —
|
|
200
|
+
* which is the granularity the exemption really has.
|
|
201
|
+
*
|
|
202
|
+
* NO DISAMBIGUATOR IS ADDED, AND THAT IS A DECISION RATHER THAN AN OMISSION.
|
|
203
|
+
* An optional occurrence index (`nth`) is the tempting small fix and it buys
|
|
204
|
+
* nothing: it makes the ordinal identity EXPLICIT without making it STABLE, so
|
|
205
|
+
* the reorder that reattached the reasons silently still reattaches them —
|
|
206
|
+
* now with a number in the config agreeing. Line anchors and content hashes do
|
|
207
|
+
* fix identity, and were rejected on the issue for churning a reviewed ledger
|
|
208
|
+
* into a file people update mechanically without reading, which is the failure
|
|
209
|
+
* the reasons exist to prevent. A guard that needs no new vocabulary is worth
|
|
210
|
+
* more here than a key that needs one and still does not work.
|
|
211
|
+
*
|
|
212
|
+
* ===========================================================================
|
|
213
|
+
* WHAT THIS DOES NOT PROTECT AGAINST
|
|
214
|
+
* ===========================================================================
|
|
215
|
+
*
|
|
216
|
+
* - **A reorder inside an identical-reason group.** Deliberately unguarded:
|
|
217
|
+
* the reasons are interchangeable, so the reattachment changes nothing a
|
|
218
|
+
* reader could act on. The residue is that if one member is later edited so
|
|
219
|
+
* the shared reason stops being true of it, the group still validates and
|
|
220
|
+
* nothing notices.
|
|
221
|
+
* - **A collision introduced by the FILE rather than the config.** This is a
|
|
222
|
+
* schema check: it reads the configuration and never the source. One entry
|
|
223
|
+
* for `<div>` in a file that later grows a second bespoke `<div>` is not a
|
|
224
|
+
* group, so nothing here fires — and yet the single entry now matches
|
|
225
|
+
* whichever `div` comes first in source order, which may not be the one it
|
|
226
|
+
* was written about. That is the same defect arriving from the other side,
|
|
227
|
+
* and it is not covered.
|
|
228
|
+
* - **Whether a shared reason is true.** As everywhere else in this file,
|
|
229
|
+
* Gyde checks that a reason was written, never that it is honest.
|
|
230
|
+
*/
|
|
231
|
+
function reasonCollisions(at, exceptions) {
|
|
232
|
+
const groups = new Map();
|
|
233
|
+
exceptions.forEach((ex, j) => {
|
|
234
|
+
if (!isPlainObject(ex) || typeof ex.file !== "string" || typeof ex.element !== "string") return;
|
|
235
|
+
const key = JSON.stringify([norm(ex.file), ex.element]);
|
|
236
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
237
|
+
groups.get(key).push({ j, because: typeof ex.because === "string" ? ex.because.trim() : "" });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const [key, members] of groups) {
|
|
242
|
+
if (members.length < 2) continue;
|
|
243
|
+
const distinct = new Set(members.map((m) => m.because));
|
|
244
|
+
if (distinct.size < 2) continue;
|
|
245
|
+
const [file, element] = JSON.parse(key);
|
|
246
|
+
out.push(
|
|
247
|
+
`${at}.exceptions: ${members.length} exceptions name <${element}> in ${file} and ${distinct.size} of them ` +
|
|
248
|
+
`give DIFFERENT reasons (entries ${members.map((m) => m.j).join(", ")}). An exception identifies its element ` +
|
|
249
|
+
`by file and tag name in source order, so nothing records which <${element}> each reason was written about: ` +
|
|
250
|
+
`reorder the file and the reasons reattach to different elements, silently, with the count and the verdict ` +
|
|
251
|
+
`unchanged. Repeating a tag is fine — repeating it with different arguments is not. Give the group ONE reason ` +
|
|
252
|
+
`that is true of all ${members.length}, which is the granularity this exemption actually has.`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
152
258
|
/**
|
|
153
259
|
* Validate the declared floors.
|
|
154
260
|
*
|
|
@@ -243,6 +349,7 @@ export function validateFloors(raw) {
|
|
|
243
349
|
`(at least ${MIN_REASON} characters). An exception nobody had to justify is an exemption, not an exception.`);
|
|
244
350
|
}
|
|
245
351
|
});
|
|
352
|
+
for (const e of reasonCollisions(at, exceptions)) errors.push(e);
|
|
246
353
|
}
|
|
247
354
|
|
|
248
355
|
floors.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weatherboard/gyde-design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Scaffolds a design system into a product repository, then keeps auditing it.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,32 +15,39 @@
|
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./index.mjs",
|
|
18
|
-
"./
|
|
19
|
-
"./
|
|
20
|
-
"./
|
|
21
|
-
"./ruleindex.mjs": "./ruleindex.mjs",
|
|
22
|
-
"./scan.mjs": "./scan.mjs",
|
|
23
|
-
"./tokens.mjs": "./tokens.mjs",
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
"./adoption.mjs": "./adoption.mjs",
|
|
20
|
+
"./agentdocs.mjs": "./agentdocs.mjs",
|
|
24
21
|
"./boundaries.mjs": "./boundaries.mjs",
|
|
25
|
-
"./ratchet.mjs": "./ratchet.mjs",
|
|
26
|
-
"./emit.mjs": "./emit.mjs",
|
|
27
22
|
"./catalogue.mjs": "./catalogue.mjs",
|
|
28
|
-
"./agentdocs.mjs": "./agentdocs.mjs",
|
|
29
|
-
"./upgrade.mjs": "./upgrade.mjs",
|
|
30
|
-
"./usage.mjs": "./usage.mjs",
|
|
31
|
-
"./wiring.mjs": "./wiring.mjs",
|
|
32
23
|
"./clientboundary.mjs": "./clientboundary.mjs",
|
|
33
|
-
"./props.mjs": "./props.mjs",
|
|
34
|
-
"./docdrift.mjs": "./docdrift.mjs",
|
|
35
24
|
"./compound.mjs": "./compound.mjs",
|
|
25
|
+
"./docdrift.mjs": "./docdrift.mjs",
|
|
26
|
+
"./emit.mjs": "./emit.mjs",
|
|
27
|
+
"./enforcement.mjs": "./enforcement.mjs",
|
|
28
|
+
"./floor.mjs": "./floor.mjs",
|
|
36
29
|
"./htmlscan.mjs": "./htmlscan.mjs",
|
|
37
30
|
"./markup.mjs": "./markup.mjs",
|
|
38
|
-
"./floor.mjs": "./floor.mjs",
|
|
39
|
-
"./adoption.mjs": "./adoption.mjs",
|
|
40
|
-
"./tailwind.mjs": "./tailwind.mjs",
|
|
41
31
|
"./migration.mjs": "./migration.mjs",
|
|
32
|
+
"./normalise.mjs": "./normalise.mjs",
|
|
33
|
+
"./props.mjs": "./props.mjs",
|
|
34
|
+
"./ratchet.mjs": "./ratchet.mjs",
|
|
35
|
+
"./ruleindex.mjs": "./ruleindex.mjs",
|
|
36
|
+
"./rules.mjs": "./rules.mjs",
|
|
37
|
+
"./scan.mjs": "./scan.mjs",
|
|
38
|
+
"./scope.mjs": "./scope.mjs",
|
|
39
|
+
"./selfgate.mjs": "./selfgate.mjs",
|
|
42
40
|
"./stylex.mjs": "./stylex.mjs",
|
|
43
|
-
"./
|
|
41
|
+
"./tailwind.mjs": "./tailwind.mjs",
|
|
42
|
+
"./theme-choice.mjs": "./theme-choice.mjs",
|
|
43
|
+
"./theme-entry.mjs": "./theme-entry.mjs",
|
|
44
|
+
"./themehost.mjs": "./themehost.mjs",
|
|
45
|
+
"./tokens.mjs": "./tokens.mjs",
|
|
46
|
+
"./upgrade.mjs": "./upgrade.mjs",
|
|
47
|
+
"./usage.mjs": "./usage.mjs",
|
|
48
|
+
"./wiring.mjs": "./wiring.mjs",
|
|
49
|
+
"./workflow.mjs": "./workflow.mjs",
|
|
50
|
+
"./workspace.mjs": "./workspace.mjs"
|
|
44
51
|
},
|
|
45
52
|
"files": [
|
|
46
53
|
"*.mjs",
|
package/theme-entry.mjs
CHANGED
|
@@ -106,29 +106,103 @@ export function parseEntry(source) {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Why the answer names the QUESTION it answered, and not only "no" (G-141).
|
|
111
|
+
*
|
|
112
|
+
* `{ started: false, parseError: null }` used to be the answer to four
|
|
113
|
+
* different questions, and a caller could not tell them apart:
|
|
114
|
+
*
|
|
115
|
+
* - the entry has no import of `startThemeChoice` at all
|
|
116
|
+
* - it imports it as a TYPE, which is erased before the module runs
|
|
117
|
+
* - it imports it and the call is not the next top-level statement
|
|
118
|
+
* - `systemPackage` was never passed, so the walk searched for an import
|
|
119
|
+
* from the literal string `undefined/theme-choice` and matched nothing
|
|
120
|
+
*
|
|
121
|
+
* The last is a CALLER error and not a verdict about the entry, and it is the
|
|
122
|
+
* one that cost two wrong measurements: `themeStartup(source)` reads unary,
|
|
123
|
+
* returns the byte-identical shape of a genuinely unwired entry, and a
|
|
124
|
+
* correctly-wired file is then reported as failing the gate. So a missing or
|
|
125
|
+
* non-string `systemPackage` now THROWS rather than being searched for. A
|
|
126
|
+
* programming mistake that produces a plausible verdict is worse than one that
|
|
127
|
+
* produces an exception, which is G-134's lesson one layer along: a file the
|
|
128
|
+
* parser never read is not a wiring complaint, and a question nobody asked is
|
|
129
|
+
* not a wiring verdict either.
|
|
130
|
+
*
|
|
131
|
+
* The other three are real verdicts and stay verdicts — but each gets its own
|
|
132
|
+
* `reason`, because the caller's single message named only the third
|
|
133
|
+
* possibility and was therefore a false statement about the first two.
|
|
134
|
+
*/
|
|
135
|
+
export const STARTUP_REASON = Object.freeze({
|
|
136
|
+
STARTED: "started",
|
|
137
|
+
PARSE_ERROR: "parse-error",
|
|
138
|
+
/** Nothing in the file imports `startThemeChoice` as a value from the controller. */
|
|
139
|
+
NO_IMPORT: "no-import",
|
|
140
|
+
/** It is imported, but as a type, so the binding does not exist at runtime. */
|
|
141
|
+
TYPE_ONLY_IMPORT: "type-only-import",
|
|
142
|
+
/** It is imported as a value, and the call is not the next top-level statement. */
|
|
143
|
+
CALL_NOT_ADJACENT: "call-not-adjacent",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Worse news wins. A file with one irrelevant import and one nearly-correct one
|
|
148
|
+
* should be told about the nearly-correct one, so the reasons are ranked and
|
|
149
|
+
* the walk keeps the strongest it has seen rather than the last.
|
|
150
|
+
*/
|
|
151
|
+
const REASON_RANK = [
|
|
152
|
+
STARTUP_REASON.NO_IMPORT,
|
|
153
|
+
STARTUP_REASON.TYPE_ONLY_IMPORT,
|
|
154
|
+
STARTUP_REASON.CALL_NOT_ADJACENT,
|
|
155
|
+
];
|
|
156
|
+
|
|
109
157
|
/**
|
|
110
158
|
* Only an actual VALUE import followed by a direct top-level call counts.
|
|
111
159
|
*
|
|
112
|
-
* `{ started: true }`
|
|
113
|
-
* `{ started: false }`
|
|
114
|
-
* `{ started: false,
|
|
160
|
+
* `{ started: true, reason: "started" }` the contract is met
|
|
161
|
+
* `{ started: false, reason: "no-import" }` parsed fine, nothing imports it
|
|
162
|
+
* `{ started: false, reason: "type-only-import" }` imported, but erased at runtime
|
|
163
|
+
* `{ started: false, reason: "call-not-adjacent" }` imported, not called next
|
|
164
|
+
* `{ started: false, reason: "parse-error", parseError }` never read; say where
|
|
165
|
+
*
|
|
166
|
+
* @throws {TypeError} if `systemPackage` is missing or is not a non-empty
|
|
167
|
+
* string. See the note above: searching for `undefined/theme-choice` and
|
|
168
|
+
* reporting the miss as a wiring failure is a lie about the consumer's file.
|
|
115
169
|
*/
|
|
116
170
|
export function themeStartup(source, systemPackage) {
|
|
171
|
+
if (typeof systemPackage !== "string" || systemPackage === "") {
|
|
172
|
+
throw new TypeError(
|
|
173
|
+
"themeStartup(source, systemPackage): systemPackage must be a non-empty string naming the design-system " +
|
|
174
|
+
`package whose /theme-choice module the entry imports; received ${
|
|
175
|
+
systemPackage === undefined ? "undefined — it takes TWO arguments" : JSON.stringify(systemPackage)}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
117
178
|
const { program, error } = parseEntry(source);
|
|
118
|
-
if (error) return { started: false, parseError: error };
|
|
179
|
+
if (error) return { started: false, reason: STARTUP_REASON.PARSE_ERROR, parseError: error };
|
|
180
|
+
let reason = STARTUP_REASON.NO_IMPORT;
|
|
181
|
+
const worse = (next) => {
|
|
182
|
+
if (REASON_RANK.indexOf(next) > REASON_RANK.indexOf(reason)) reason = next;
|
|
183
|
+
};
|
|
119
184
|
for (const [index, node] of program.body.entries()) {
|
|
120
185
|
if (node.type !== "ImportDeclaration" ||
|
|
121
186
|
node.source.value !== `${systemPackage}/theme-choice`) continue;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (
|
|
187
|
+
const named = node.specifiers.find((part) => part.type === "ImportSpecifier" &&
|
|
188
|
+
part.imported.type === "Identifier" && part.imported.name === "startThemeChoice");
|
|
189
|
+
if (!named) continue;
|
|
190
|
+
// `import type { … }` and `import { type … }` are both erased before the
|
|
191
|
+
// module runs, so the call beside them throws rather than starting the theme.
|
|
192
|
+
if (node.importKind === "type" || named.importKind === "type") {
|
|
193
|
+
worse(STARTUP_REASON.TYPE_ONLY_IMPORT);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
worse(STARTUP_REASON.CALL_NOT_ADJACENT);
|
|
197
|
+
const binding = named.local.name;
|
|
128
198
|
const next = program.body[index + 1];
|
|
129
|
-
if (next?.type === "ExpressionStatement" && directCall(next.expression, binding))
|
|
199
|
+
if (next?.type === "ExpressionStatement" && directCall(next.expression, binding)) {
|
|
200
|
+
return { started: true, reason: STARTUP_REASON.STARTED, parseError: null };
|
|
201
|
+
}
|
|
130
202
|
if (next?.type === "VariableDeclaration" && next.kind === "const" &&
|
|
131
|
-
next.declarations.length === 1 && directCall(next.declarations[0].init, binding))
|
|
203
|
+
next.declarations.length === 1 && directCall(next.declarations[0].init, binding)) {
|
|
204
|
+
return { started: true, reason: STARTUP_REASON.STARTED, parseError: null };
|
|
205
|
+
}
|
|
132
206
|
}
|
|
133
|
-
return { started: false, parseError: null };
|
|
207
|
+
return { started: false, reason, parseError: null };
|
|
134
208
|
}
|
package/themehost.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { join, resolve, relative, sep } from "node:path";
|
|
4
4
|
import { htmlElements } from "./htmlscan.mjs";
|
|
5
|
-
import { themeStartup } from "./theme-entry.mjs";
|
|
5
|
+
import { themeStartup, STARTUP_REASON } from "./theme-entry.mjs";
|
|
6
6
|
|
|
7
7
|
function within(root, path) {
|
|
8
8
|
if (typeof path !== "string" || !path || path.startsWith("/")) return null;
|
|
@@ -24,14 +24,43 @@ function read(root, path) {
|
|
|
24
24
|
*/
|
|
25
25
|
export function checkThemeHosts(root, design = {}, { packages = null } = {}) {
|
|
26
26
|
const declared = design.themeHosts;
|
|
27
|
-
if (declared === undefined) return { checked: 0, errors: [] };
|
|
28
|
-
if (!Array.isArray(declared)) return { checked: 0, errors: ["design.themeHosts must be an array"] };
|
|
27
|
+
if (declared === undefined) return { declared: 0, checked: 0, errors: [] };
|
|
28
|
+
if (!Array.isArray(declared)) return { declared: 0, checked: 0, errors: ["design.themeHosts must be an array"] };
|
|
29
29
|
const errors = [];
|
|
30
|
+
/**
|
|
31
|
+
* `declared` and `checked` are two different numbers and used to be one
|
|
32
|
+
* (G-141 follow-up). The config-gap return below reported
|
|
33
|
+
* `checked: declared.length` from a path that inspects nothing — a count
|
|
34
|
+
* claiming work it did not do, which is the same defect class as the verdict
|
|
35
|
+
* that claimed an answer to a question nobody asked. CHARTER §5: "Gyde did
|
|
36
|
+
* not run" and "Gyde found nothing" must never render the same, and a single
|
|
37
|
+
* number cannot tell them apart.
|
|
38
|
+
*
|
|
39
|
+
* So this one is COUNTED as hosts are inspected rather than restated from
|
|
40
|
+
* the input. A count derived from `declared.length` is an assertion about
|
|
41
|
+
* the loop; a count the loop increments is a measurement of it, and only the
|
|
42
|
+
* second goes wrong when the loop stops running.
|
|
43
|
+
*/
|
|
44
|
+
let checked = 0;
|
|
30
45
|
const systemPath = design.systemPath || "packages/design-system";
|
|
31
|
-
|
|
46
|
+
// `${undefined}/design-system` is a STRING, so an undeclared scope used to
|
|
47
|
+
// produce the specifier "undefined/design-system", match no import, and be
|
|
48
|
+
// reported as a consumer's broken wiring (G-141, same shape as the arity bug
|
|
49
|
+
// below). Say which key is missing instead.
|
|
50
|
+
const named = (value) => (typeof value === "string" && value !== "" ? value : null);
|
|
51
|
+
const systemPackage = named(design.systemPackage) ??
|
|
52
|
+
(named(design.scope) ? `${design.scope}/design-system` : null);
|
|
53
|
+
if (systemPackage === null) {
|
|
54
|
+
return {
|
|
55
|
+
declared: declared.length,
|
|
56
|
+
checked: 0,
|
|
57
|
+
errors: ["design.themeHosts is declared, but neither design.systemPackage nor design.scope names the design-system package the entry must import"],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
32
60
|
const expected = read(root, `${systemPath}/src/theme-bootstrap.js`);
|
|
33
61
|
for (const [index, host] of declared.entries()) {
|
|
34
62
|
const label = `design.themeHosts[${index}]`;
|
|
63
|
+
checked += 1;
|
|
35
64
|
if (!host || typeof host !== "object" || host.adapter !== "html-inline") {
|
|
36
65
|
errors.push(`${label}: unsupported adapter; this gate currently supports html-inline`);
|
|
37
66
|
continue;
|
|
@@ -74,20 +103,35 @@ export function checkThemeHosts(root, design = {}, { packages = null } = {}) {
|
|
|
74
103
|
// A file the parser never finished reading has not failed the wiring
|
|
75
104
|
// contract; it has failed to be read. Saying otherwise sends the reader to
|
|
76
105
|
// rearrange imports the parser never reached (G-134).
|
|
106
|
+
//
|
|
107
|
+
// And one message for "not wired" was a false statement about two of the
|
|
108
|
+
// three ways an entry can be unwired (G-141): a file with no import at all
|
|
109
|
+
// was told its call was misplaced. Each reason gets the sentence that is
|
|
110
|
+
// true of it, or the reader is sent to fix something that is not wrong.
|
|
77
111
|
const startup = themeStartup(entry, systemPackage);
|
|
78
|
-
if (startup.
|
|
112
|
+
if (startup.reason === STARTUP_REASON.PARSE_ERROR) {
|
|
79
113
|
const { message, line, column } = startup.parseError;
|
|
80
114
|
const at = line === null ? "" : ` at line ${line}, column ${column}`;
|
|
81
115
|
errors.push(`${label}: declared module entry ${host.entry} could not be parsed${at} — ${message}`);
|
|
82
|
-
} else if (
|
|
83
|
-
errors.push(`${label}: linked entry
|
|
116
|
+
} else if (startup.reason === STARTUP_REASON.NO_IMPORT) {
|
|
117
|
+
errors.push(`${label}: linked entry ${host.entry} has no import of startThemeChoice from ${systemPackage}/theme-choice`);
|
|
118
|
+
} else if (startup.reason === STARTUP_REASON.TYPE_ONLY_IMPORT) {
|
|
119
|
+
errors.push(`${label}: linked entry ${host.entry} imports startThemeChoice as a TYPE, which is erased before the module runs — import it as a value and call it at module top level`);
|
|
120
|
+
} else if (startup.reason === STARTUP_REASON.CALL_NOT_ADJACENT) {
|
|
121
|
+
errors.push(`${label}: linked entry ${host.entry} imports startThemeChoice but must call it immediately, at module top level`);
|
|
84
122
|
}
|
|
85
123
|
}
|
|
86
|
-
return {
|
|
124
|
+
return { declared: declared.length, checked, errors };
|
|
87
125
|
}
|
|
88
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Both numbers are printed, always. The line used to render `checked` under
|
|
129
|
+
* the word "declared", so one number stood for two claims and the reader could
|
|
130
|
+
* not see a run that declared hosts and inspected none of them.
|
|
131
|
+
*/
|
|
89
132
|
export function formatThemeHosts(result) {
|
|
133
|
+
const counts = `${result.declared} declared, ${result.checked} checked`;
|
|
90
134
|
return result.errors.length
|
|
91
|
-
? `theme hosts ${
|
|
92
|
-
: `theme hosts ${
|
|
135
|
+
? `theme hosts ${counts}, ${result.errors.length} error(s):\n${result.errors.map((e) => ` ${e}`).join("\n")}`
|
|
136
|
+
: `theme hosts ${counts}`;
|
|
93
137
|
}
|