@weatherboard/gyde-design 0.4.0 → 0.4.2
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 +69 -1
- package/agentdocs.mjs +38 -1
- package/catalogue.mjs +33 -2
- package/cli.mjs +188 -19
- package/emit.mjs +89 -27
- package/index.mjs +2 -1
- package/markup.mjs +2 -2
- package/normalise.mjs +115 -0
- package/package.json +3 -2
- package/props.mjs +8 -5
- package/ratchet.mjs +191 -31
- package/ruleindex.mjs +138 -0
- package/rules.mjs +17 -0
- package/scope.mjs +195 -0
- package/selfgate.mjs +162 -0
- package/theme-choice.mjs +111 -0
- package/tokens.mjs +2 -0
- package/wiring.mjs +59 -4
- package/workflow.mjs +1 -1
package/selfgate.mjs
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G-127 — Gyde runs its own gate over the scaffold Gyde emits.
|
|
3
|
+
*
|
|
4
|
+
* ===========================================================================
|
|
5
|
+
* WHY THIS FILE EXISTS
|
|
6
|
+
* ===========================================================================
|
|
7
|
+
*
|
|
8
|
+
* `emit.mjs` has carried this sentence since G-101:
|
|
9
|
+
*
|
|
10
|
+
* "So this stays below the threshold until the templates earn it. The rule,
|
|
11
|
+
* and `emit.test.mjs` enforces it: a fresh scaffold must pass the gate Gyde
|
|
12
|
+
* would run against it."
|
|
13
|
+
*
|
|
14
|
+
* `emit.test.mjs` did not enforce it. It checked that no component declares a
|
|
15
|
+
* style-shaped prop and that none spreads `{...rest}` — both worth checking,
|
|
16
|
+
* neither of them the gate. Nothing anywhere ran `gyde-design gate` against the
|
|
17
|
+
* emitted tree, so the claim was true of nobody's code and read as true of all
|
|
18
|
+
* of it.
|
|
19
|
+
*
|
|
20
|
+
* What that cost, measured: the scaffold emitted on 2026-09-15 shipped **ten**
|
|
21
|
+
* `optional-prop` findings across five of its five components — Button 3,
|
|
22
|
+
* Checkbox 3, Select 2, Card 1, Text 1. The tool that fails a customer for an
|
|
23
|
+
* optional prop wrote ten of them into every repository it scaffolded, and the
|
|
24
|
+
* customer found out, not us.
|
|
25
|
+
*
|
|
26
|
+
* ===========================================================================
|
|
27
|
+
* IT MEASURES THROUGH THE FRONT DOOR
|
|
28
|
+
* ===========================================================================
|
|
29
|
+
*
|
|
30
|
+
* `init` and `gate` are run as SUBPROCESSES, through `cli.mjs`, exactly as a
|
|
31
|
+
* customer runs them. Importing `scan()` and calling it here would be shorter
|
|
32
|
+
* and would measure a different thing: the CLI is where `design.scope` is read,
|
|
33
|
+
* where `exportedComponents` narrows `componentRoots`, and where the ledger is
|
|
34
|
+
* consulted. A number taken around it is a real number about a repository that
|
|
35
|
+
* does not exist.
|
|
36
|
+
*
|
|
37
|
+
* ===========================================================================
|
|
38
|
+
* THE ASSERTION IS ON THE BASELINE, NOT ON THE EXIT CODE
|
|
39
|
+
* ===========================================================================
|
|
40
|
+
*
|
|
41
|
+
* `gate` on a tree with no ledger RECORDS one and exits 1 — deliberately, since
|
|
42
|
+
* recording debt is not a pass. So a green exit code cannot be the check here:
|
|
43
|
+
* the clean run's exit code is 1 by design, and the second run's exit code is 0
|
|
44
|
+
* whatever the first run recorded, because the first run wrote every finding
|
|
45
|
+
* into the allowance.
|
|
46
|
+
*
|
|
47
|
+
* Gate a scaffold with ten optional props and the second run passes. That is
|
|
48
|
+
* correct behaviour for a customer adopting Gyde and useless as a check on
|
|
49
|
+
* ourselves, which is why this reads `total` out of the recorded baseline.
|
|
50
|
+
* **A scaffold Gyde emits starts at zero, or it is not a scaffold Gyde may
|
|
51
|
+
* emit.** The exit code of the second run is checked as well, because a
|
|
52
|
+
* baseline of zero followed by a failing gate would mean something else is
|
|
53
|
+
* wrong.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { execFileSync } from "node:child_process";
|
|
57
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
|
|
58
|
+
import { tmpdir } from "node:os";
|
|
59
|
+
import { join, dirname } from "node:path";
|
|
60
|
+
import { fileURLToPath } from "node:url";
|
|
61
|
+
|
|
62
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
const CLI = join(HERE, "cli.mjs");
|
|
64
|
+
|
|
65
|
+
/** The scope the probe repository declares. Arbitrary, but it must agree with itself (G-123). */
|
|
66
|
+
const SCOPE = "@selfgate";
|
|
67
|
+
|
|
68
|
+
function cli(args) {
|
|
69
|
+
try {
|
|
70
|
+
return { code: 0, out: execFileSync(process.execPath, [CLI, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) };
|
|
71
|
+
} catch (error) {
|
|
72
|
+
/**
|
|
73
|
+
* A non-zero exit is an ORDINARY outcome here — `gate` exits 1 when it
|
|
74
|
+
* records a baseline — so it is returned rather than thrown. Throwing would
|
|
75
|
+
* make the clean path the exceptional one.
|
|
76
|
+
*/
|
|
77
|
+
return { code: error.status ?? 1, out: `${error.stdout ?? ""}${error.stderr ?? ""}` };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Emit a scaffold into a fresh directory and gate it.
|
|
83
|
+
*
|
|
84
|
+
* `corrupt` receives the directory after `init` and before `gate`, so a caller
|
|
85
|
+
* can put a defect into the emitted source and prove the gate sees it. That is
|
|
86
|
+
* the negative fixture, and it is the only reason this parameter exists: a
|
|
87
|
+
* check that has never been watched to fail is a check nobody has evidence of.
|
|
88
|
+
*/
|
|
89
|
+
export function gateAScaffold({ corrupt = null, dir = null } = {}) {
|
|
90
|
+
const root = dir ?? mkdtempSync(join(tmpdir(), "gyde-selfgate-"));
|
|
91
|
+
mkdirSync(root, { recursive: true });
|
|
92
|
+
|
|
93
|
+
writeFileSync(join(root, "package.json"),
|
|
94
|
+
JSON.stringify({ name: "gyde-selfgate-probe", private: true, workspaces: ["packages/*", "apps/*"] }, null, 2) + "\n");
|
|
95
|
+
writeFileSync(join(root, "gyde.config.json"),
|
|
96
|
+
JSON.stringify({ version: 1, design: { scope: SCOPE } }, null, 2) + "\n");
|
|
97
|
+
|
|
98
|
+
const init = cli(["init", root]);
|
|
99
|
+
if (init.code !== 0) return { root, phase: "init", ok: false, total: null, init, baseline: null, judged: null };
|
|
100
|
+
|
|
101
|
+
if (corrupt) corrupt(root);
|
|
102
|
+
|
|
103
|
+
const baseline = cli(["gate", root]);
|
|
104
|
+
let total = null;
|
|
105
|
+
try { total = JSON.parse(readFileSync(join(root, "gyde-allowance.json"), "utf8")).total ?? null; } catch { /* reported as null */ }
|
|
106
|
+
|
|
107
|
+
const judged = cli(["gate", root]);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
root,
|
|
111
|
+
phase: "gate",
|
|
112
|
+
total,
|
|
113
|
+
baseline,
|
|
114
|
+
judged,
|
|
115
|
+
ok: total === 0 && judged.code === 0,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The findings the baseline recorded, as `file rule count` lines.
|
|
121
|
+
*
|
|
122
|
+
* Read back out of the written ledger rather than parsed from the gate's
|
|
123
|
+
* stdout: the prose changes, the file is the record.
|
|
124
|
+
*/
|
|
125
|
+
export function baselineFindings(root) {
|
|
126
|
+
let led;
|
|
127
|
+
try { led = JSON.parse(readFileSync(join(root, "gyde-allowance.json"), "utf8")); } catch { return []; }
|
|
128
|
+
const out = [];
|
|
129
|
+
for (const [file, rules] of Object.entries(led.allowed ?? {})) {
|
|
130
|
+
for (const [rule, count] of Object.entries(rules)) out.push({ file, rule, count });
|
|
131
|
+
}
|
|
132
|
+
return out.sort((a, b) => a.file.localeCompare(b.file) || a.rule.localeCompare(b.rule));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function formatSelfGate(result) {
|
|
136
|
+
const L = [];
|
|
137
|
+
if (result.phase === "init") {
|
|
138
|
+
L.push("the scaffold could not be emitted at all:");
|
|
139
|
+
L.push(result.init.out.trim());
|
|
140
|
+
return L.join("\n");
|
|
141
|
+
}
|
|
142
|
+
const findings = baselineFindings(result.root);
|
|
143
|
+
if (result.ok) return `a fresh scaffold records a baseline of 0 finding(s) and passes \`gyde-design gate\``;
|
|
144
|
+
|
|
145
|
+
L.push(`Gyde emitted a scaffold its own gate rejects: ${result.total} finding(s).`);
|
|
146
|
+
L.push("");
|
|
147
|
+
for (const f of findings) L.push(` ${f.file} ${f.rule} x${f.count}`);
|
|
148
|
+
L.push("");
|
|
149
|
+
if (result.judged.code !== 0) L.push(` the judged run also exited ${result.judged.code}`);
|
|
150
|
+
L.push(" Fix the TEMPLATES in packages/design/emit.mjs. Do not record a baseline for");
|
|
151
|
+
L.push(" this — a customer's ledger may carry pre-existing debt, but the scaffold we");
|
|
152
|
+
L.push(" hand them starts at zero or it is not one we may hand them.");
|
|
153
|
+
return L.join("\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Run directly (`node packages/design/selfgate.mjs`) — this is what `verify` calls. */
|
|
157
|
+
if (process.argv[1] && process.argv[1].endsWith("selfgate.mjs")) {
|
|
158
|
+
const result = gateAScaffold();
|
|
159
|
+
console.log(formatSelfGate(result));
|
|
160
|
+
try { rmSync(result.root, { recursive: true, force: true }); } catch { /* a leftover temp dir is not a failure */ }
|
|
161
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
162
|
+
}
|
package/theme-choice.mjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Browser theme lifecycle emitted into each product's design system. */
|
|
2
|
+
export function emitThemeChoice({ path = "packages/design-system", storageKey }) {
|
|
3
|
+
const key = JSON.stringify(storageKey);
|
|
4
|
+
const bootstrap = `/* GENERATED BY GYDE — then yours. Inline this script in <head> before styles. */
|
|
5
|
+
(() => {
|
|
6
|
+
const key = ${key};
|
|
7
|
+
let choice = "system";
|
|
8
|
+
try {
|
|
9
|
+
const stored = localStorage.getItem(key);
|
|
10
|
+
if (stored === "light" || stored === "dark") choice = stored;
|
|
11
|
+
} catch { /* Storage can be unavailable; the system preference remains usable. */ }
|
|
12
|
+
if (choice === "system") document.documentElement.removeAttribute("data-theme");
|
|
13
|
+
else document.documentElement.setAttribute("data-theme", choice);
|
|
14
|
+
})();
|
|
15
|
+
`;
|
|
16
|
+
const controller = `/* GENERATED BY GYDE — then yours.
|
|
17
|
+
* The app owns its root and calls startThemeChoice() once after hydration.
|
|
18
|
+
* This module adds no provider or layout element.
|
|
19
|
+
*/
|
|
20
|
+
export const THEME_STORAGE_KEY = ${key};
|
|
21
|
+
const valid = (value) => value === "light" || value === "dark" || value === "system";
|
|
22
|
+
|
|
23
|
+
export function startThemeChoice({ doc = document, win = window } = {}) {
|
|
24
|
+
let choice = "system";
|
|
25
|
+
let root;
|
|
26
|
+
let rootObserver;
|
|
27
|
+
const listeners = new Set();
|
|
28
|
+
const media = win.matchMedia("(prefers-color-scheme: dark)");
|
|
29
|
+
const read = () => {
|
|
30
|
+
try {
|
|
31
|
+
const stored = win.localStorage.getItem(THEME_STORAGE_KEY);
|
|
32
|
+
choice = valid(stored) ? stored : "system";
|
|
33
|
+
} catch { /* Keep the in-memory choice when storage is unavailable. */ }
|
|
34
|
+
};
|
|
35
|
+
const resolvedTheme = () => choice === "system" ? (media.matches ? "dark" : "light") : choice;
|
|
36
|
+
const notify = () => { for (const listener of listeners) listener(choice, resolvedTheme()); };
|
|
37
|
+
const reconcile = () => {
|
|
38
|
+
if (root !== doc.documentElement) {
|
|
39
|
+
rootObserver?.disconnect();
|
|
40
|
+
root = doc.documentElement;
|
|
41
|
+
if (root) rootObserver?.observe(root, { attributes: true, attributeFilter: ["data-theme"] });
|
|
42
|
+
}
|
|
43
|
+
if (!root) return;
|
|
44
|
+
if (choice === "system") {
|
|
45
|
+
if (root.hasAttribute("data-theme")) root.removeAttribute("data-theme");
|
|
46
|
+
} else if (root.getAttribute("data-theme") !== choice) root.setAttribute("data-theme", choice);
|
|
47
|
+
};
|
|
48
|
+
rootObserver = new win.MutationObserver(reconcile);
|
|
49
|
+
const documentObserver = new win.MutationObserver(reconcile);
|
|
50
|
+
documentObserver.observe(doc, { childList: true });
|
|
51
|
+
read();
|
|
52
|
+
reconcile();
|
|
53
|
+
const onStorage = (event) => {
|
|
54
|
+
if (event.key !== THEME_STORAGE_KEY && event.key !== null) return;
|
|
55
|
+
try { if (event.storageArea && event.storageArea !== win.localStorage) return; }
|
|
56
|
+
catch { /* Storage access is blocked; read() retains the in-memory choice. */ }
|
|
57
|
+
read(); reconcile(); notify();
|
|
58
|
+
};
|
|
59
|
+
const onPageShow = () => { read(); reconcile(); notify(); };
|
|
60
|
+
const onNavigate = () => { reconcile(); notify(); };
|
|
61
|
+
const onMedia = () => { if (choice === "system") notify(); };
|
|
62
|
+
win.addEventListener("storage", onStorage);
|
|
63
|
+
win.addEventListener("pageshow", onPageShow);
|
|
64
|
+
win.addEventListener("popstate", onNavigate);
|
|
65
|
+
win.addEventListener("hashchange", onNavigate);
|
|
66
|
+
media.addEventListener("change", onMedia);
|
|
67
|
+
return {
|
|
68
|
+
getChoice: () => choice,
|
|
69
|
+
getResolvedTheme: resolvedTheme,
|
|
70
|
+
setChoice(next) {
|
|
71
|
+
if (!valid(next)) throw new TypeError("Theme choice must be light, dark, or system");
|
|
72
|
+
choice = next;
|
|
73
|
+
try { win.localStorage.setItem(THEME_STORAGE_KEY, next); } catch { /* Current tab still works. */ }
|
|
74
|
+
reconcile(); notify();
|
|
75
|
+
},
|
|
76
|
+
reconcile,
|
|
77
|
+
subscribe(listener) { listeners.add(listener); listener(choice, resolvedTheme()); return () => listeners.delete(listener); },
|
|
78
|
+
stop() {
|
|
79
|
+
rootObserver.disconnect();
|
|
80
|
+
documentObserver.disconnect();
|
|
81
|
+
win.removeEventListener("storage", onStorage);
|
|
82
|
+
win.removeEventListener("pageshow", onPageShow);
|
|
83
|
+
win.removeEventListener("popstate", onNavigate);
|
|
84
|
+
win.removeEventListener("hashchange", onNavigate);
|
|
85
|
+
media.removeEventListener("change", onMedia);
|
|
86
|
+
listeners.clear();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
return {
|
|
92
|
+
[`${path}/src/theme-bootstrap.js`]: bootstrap,
|
|
93
|
+
[`${path}/src/theme-choice.js`]: controller,
|
|
94
|
+
[`${path}/src/theme-choice.d.ts`]: `/* GENERATED BY GYDE — then yours.
|
|
95
|
+
* Gyde wrote this file once and will never overwrite it.
|
|
96
|
+
*/
|
|
97
|
+
export type ThemeChoice = "light" | "dark" | "system";
|
|
98
|
+
export type ResolvedTheme = "light" | "dark";
|
|
99
|
+
export declare const THEME_STORAGE_KEY: string;
|
|
100
|
+
export interface ThemeChoiceController {
|
|
101
|
+
getChoice(): ThemeChoice;
|
|
102
|
+
getResolvedTheme(): ResolvedTheme;
|
|
103
|
+
setChoice(next: ThemeChoice): void;
|
|
104
|
+
reconcile(): void;
|
|
105
|
+
subscribe(listener: (choice: ThemeChoice, resolved: ResolvedTheme) => void): () => void;
|
|
106
|
+
stop(): void;
|
|
107
|
+
}
|
|
108
|
+
export declare function startThemeChoice(): ThemeChoiceController;
|
|
109
|
+
`,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/tokens.mjs
CHANGED
|
@@ -252,10 +252,12 @@ export function generateCss(dict = SEED) {
|
|
|
252
252
|
"/* An explicit choice, on any element — which is what makes both themes",
|
|
253
253
|
" renderable on one page, and what a portalled popup looks up. */",
|
|
254
254
|
'[data-theme="dark"] {',
|
|
255
|
+
' color-scheme: dark;',
|
|
255
256
|
decls("dark"),
|
|
256
257
|
"}",
|
|
257
258
|
"",
|
|
258
259
|
'[data-theme="light"] {',
|
|
260
|
+
' color-scheme: light;',
|
|
259
261
|
decls("light"),
|
|
260
262
|
"}",
|
|
261
263
|
"",
|
package/wiring.mjs
CHANGED
|
@@ -28,7 +28,24 @@ import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
|
28
28
|
import { join, extname, relative, sep } from "node:path";
|
|
29
29
|
|
|
30
30
|
const SKIP = new Set(["node_modules", "dist", "build", ".next", ".turbo", ".git", "coverage"]);
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* G-126. `.astro`, `.vue` and `.svelte` are import sites too.
|
|
33
|
+
*
|
|
34
|
+
* Measured 2026-09-15 on a consumer repository: `apps/web` was
|
|
35
|
+
* reported as "renders design-system components but never imports the token
|
|
36
|
+
* stylesheet" while `src/layouts/Base.astro` imported the stylesheet on line 2
|
|
37
|
+
* and the `:root` block was inlined into all four built pages. The walk simply
|
|
38
|
+
* never opened the file, so the import could not be seen.
|
|
39
|
+
*
|
|
40
|
+
* It is the worst direction to be wrong in for the second time in this file: a
|
|
41
|
+
* check that fails a correctly-wired repository gets switched off, and then it
|
|
42
|
+
* is not protecting the ones that ARE broken. The single-file-component formats
|
|
43
|
+
* put their imports in a frontmatter or `<script>` block rather than at the top
|
|
44
|
+
* of a `.js`, and everything below reads file TEXT rather than a parse tree, so
|
|
45
|
+
* recognising the extension is the whole fix — the `import "...css"` line looks
|
|
46
|
+
* identical once the file is read.
|
|
47
|
+
*/
|
|
48
|
+
const SOURCE = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".css", ".scss", ".astro", ".vue", ".svelte"]);
|
|
32
49
|
|
|
33
50
|
export const WIRING = {
|
|
34
51
|
OK: "wired",
|
|
@@ -125,10 +142,48 @@ export function exportedComponents(root, systemPath) {
|
|
|
125
142
|
* Depth is bounded and cycles are tracked, because a stylesheet importing
|
|
126
143
|
* itself should be a finding somewhere else, not a hang here.
|
|
127
144
|
*/
|
|
128
|
-
function reachesTokens(root, startFile, { packageDirs, tokensPackage, seen = new Set(), depth = 0 }) {
|
|
145
|
+
function reachesTokens(root, startFile, { packageDirs, tokensPackage, seen = new Set(), depth = 0, viaPackageSpecifier = false }) {
|
|
129
146
|
if (depth > 8 || seen.has(startFile)) return false;
|
|
130
147
|
seen.add(startFile);
|
|
131
148
|
|
|
149
|
+
/**
|
|
150
|
+
* G-126. The file we have arrived at IS the token stylesheet.
|
|
151
|
+
*
|
|
152
|
+
* Without this, a consumer that imports the tokens DIRECTLY — the simplest
|
|
153
|
+
* correct arrangement — reports unwired whenever `tokensPackage` is unset,
|
|
154
|
+
* because every branch below looks for an import OF the tokens rather than
|
|
155
|
+
* for the tokens themselves. A repository with no `gyde.config.json` has
|
|
156
|
+
* `tokensPackage` null, which is most of them.
|
|
157
|
+
*
|
|
158
|
+
* `viaPackageSpecifier` is the whole safety of it, and the first version of
|
|
159
|
+
* this change did not have it. A bare basename test fires on any path that
|
|
160
|
+
* ENDS in `tokens.css`, and two of the three ways into this function arrive
|
|
161
|
+
* with a path nobody resolved: `checkPackage` passes each walked file itself,
|
|
162
|
+
* and it passes a relative `./tokens.css` target with no `existsSync` behind
|
|
163
|
+
* it. The test then returned before the `readFileSync` that had been doing
|
|
164
|
+
* the existence check by accident. Four arrangements reported `wired` that
|
|
165
|
+
* are not: a stray unimported `apps/web/src/tokens.css`; a file importing an
|
|
166
|
+
* unrelated local `./tokens.css`; a file importing a `./tokens.css` that does
|
|
167
|
+
* not exist at all; and the stray-file case with `tokensPackage` configured,
|
|
168
|
+
* which had nothing to do with frontmatter and would have been a plain
|
|
169
|
+
* regression.
|
|
170
|
+
*
|
|
171
|
+
* So the shortcut fires only when we got here by resolving a workspace
|
|
172
|
+
* package specifier to a file that exists — the two `packageDirs` sites,
|
|
173
|
+
* which are the only callers that check. `depth` cannot stand in for this:
|
|
174
|
+
* `checkPackage`'s package-specifier loop calls at depth 0.
|
|
175
|
+
*
|
|
176
|
+
* And it declines entirely when `tokensPackage` IS configured, because then
|
|
177
|
+
* it can only ever be wrong. The two branches above already answer the
|
|
178
|
+
* question exactly for a configured repository — they compare against the
|
|
179
|
+
* package the config names — so the shortcut adds no correct verdict and one
|
|
180
|
+
* incorrect one: `import "@x/charts/src/tokens.css"`, an unrelated workspace
|
|
181
|
+
* package that happens to ship a file of that name, resolved to a real path
|
|
182
|
+
* and reported as wiring. Narrowing by HOW we arrived was not enough; the
|
|
183
|
+
* basename is only evidence at all when nothing better is available.
|
|
184
|
+
*/
|
|
185
|
+
if (viaPackageSpecifier && !tokensPackage && /(^|\/)tokens\.css$/.test(startFile)) return true;
|
|
186
|
+
|
|
132
187
|
let text; try { text = readFileSync(join(root, startFile), "utf8"); } catch { return false; }
|
|
133
188
|
if (tokensPackage && text.includes(`${tokensPackage}/`) && /tokens\.css/.test(text)) return true;
|
|
134
189
|
|
|
@@ -142,7 +197,7 @@ function reachesTokens(root, startFile, { packageDirs, tokensPackage, seen = new
|
|
|
142
197
|
const sub = spec.slice(name.length + 1);
|
|
143
198
|
for (const guess of [join(dir, sub), join(dir, "src", sub)]) {
|
|
144
199
|
if (existsSync(join(root, guess))) {
|
|
145
|
-
if (reachesTokens(root, guess, { packageDirs, tokensPackage, seen, depth: depth + 1 })) return true;
|
|
200
|
+
if (reachesTokens(root, guess, { packageDirs, tokensPackage, seen, depth: depth + 1, viaPackageSpecifier: true })) return true;
|
|
146
201
|
}
|
|
147
202
|
}
|
|
148
203
|
}
|
|
@@ -185,7 +240,7 @@ export function checkPackage(root, pkg, { systemPackage, tokensPackage, componen
|
|
|
185
240
|
if (!spec.startsWith(name + "/")) continue;
|
|
186
241
|
const sub = spec.slice(name.length + 1);
|
|
187
242
|
for (const guess of [join(dir, sub), join(dir, "src", sub)]) {
|
|
188
|
-
if (existsSync(join(root, guess)) && reachesTokens(root, guess, { packageDirs, tokensPackage })) importsTokens = true;
|
|
243
|
+
if (existsSync(join(root, guess)) && reachesTokens(root, guess, { packageDirs, tokensPackage, viaPackageSpecifier: true })) importsTokens = true;
|
|
189
244
|
}
|
|
190
245
|
}
|
|
191
246
|
}
|
package/workflow.mjs
CHANGED
|
@@ -127,7 +127,7 @@ export function emitWorkflow({
|
|
|
127
127
|
// rewrote this to "System A/gyde@v1" and two tests caught it — an emitted
|
|
128
128
|
// workflow pointing at an organisation that does not exist. The org and one
|
|
129
129
|
// of the products genuinely share a name; only one of the two uses is data.
|
|
130
|
-
action = "
|
|
130
|
+
action = "Weatherboard-Studio/gyde@v1",
|
|
131
131
|
/**
|
|
132
132
|
* Blocking by default.
|
|
133
133
|
*
|