@weatherboard/gyde-design 0.3.0 → 0.4.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/cli.mjs +31 -5
- package/emit.mjs +1 -1
- package/package.json +1 -1
- package/stylex.mjs +108 -0
package/cli.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* matched nothing are reported as suspicious rather than omitted.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
24
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
25
25
|
import { execFileSync } from "node:child_process";
|
|
26
26
|
import { join, resolve, dirname } from "node:path";
|
|
27
27
|
|
|
@@ -43,7 +43,7 @@ import { checkDocDrift, formatDocDrift } from "./docdrift.mjs";
|
|
|
43
43
|
import { reconcile } from "./adoption.mjs";
|
|
44
44
|
import { detectTailwind, formatTailwind } from "./tailwind.mjs";
|
|
45
45
|
import { migrationProgress, formatMigration } from "./migration.mjs";
|
|
46
|
-
import { checkStyleX, formatStyleX } from "./stylex.mjs";
|
|
46
|
+
import { checkStyleX, formatStyleX, checkClosedStyling, formatClosedStyling } from "./stylex.mjs";
|
|
47
47
|
import { blocksAt, formatSchedule, SCHEDULE } from "./enforcement.mjs";
|
|
48
48
|
import { buildUsage, guidance, formatUsage } from "./usage.mjs";
|
|
49
49
|
import { record, gate, formatGate, adopt, LEDGER_NOTE } from "./ratchet.mjs";
|
|
@@ -64,6 +64,24 @@ import { loadRules } from "./rules.mjs";
|
|
|
64
64
|
* silently never fires. Returns null when it cannot tell, so the caller falls
|
|
65
65
|
* back explicitly instead of this function inventing an answer.
|
|
66
66
|
*/
|
|
67
|
+
/**
|
|
68
|
+
* The design system's own component sources, read from disk.
|
|
69
|
+
*
|
|
70
|
+
* Read rather than imported (G-100): a prop removed from a type but still
|
|
71
|
+
* spread onto the element is a hole an import-based check cannot see, and the
|
|
72
|
+
* emitted set makes the same choice for the same reason.
|
|
73
|
+
*/
|
|
74
|
+
function systemComponents(root, design) {
|
|
75
|
+
const dir = join(root, design.systemPath || "packages/design-system", "src");
|
|
76
|
+
let names; try { names = readdirSync(dir); } catch { return []; }
|
|
77
|
+
return names
|
|
78
|
+
.filter((n) => /^[A-Z]\w*\.tsx$/.test(n))
|
|
79
|
+
.map((n) => {
|
|
80
|
+
try { return { file: n, text: readFileSync(join(dir, n), "utf8") }; } catch { return null; }
|
|
81
|
+
})
|
|
82
|
+
.filter(Boolean);
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
function detectDefaultBranch(root) {
|
|
68
86
|
try {
|
|
69
87
|
const head = execFileSync("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"],
|
|
@@ -276,7 +294,11 @@ function cmdScan(root, config) {
|
|
|
276
294
|
// not be removed while classes still resolve through it.
|
|
277
295
|
// G-100. The positive half of G-98: one styling layer, and it is wired.
|
|
278
296
|
console.log("");
|
|
279
|
-
console.log(
|
|
297
|
+
console.log(formatClosedStyling(checkClosedStyling(root, {
|
|
298
|
+
packages: ws.packages,
|
|
299
|
+
renders: sum.carryUI,
|
|
300
|
+
components: systemComponents(root, config.design || {}),
|
|
301
|
+
})));
|
|
280
302
|
|
|
281
303
|
console.log("");
|
|
282
304
|
console.log(formatTailwind(detectTailwind(root, {
|
|
@@ -653,13 +675,17 @@ function cmdGate(root, config) {
|
|
|
653
675
|
utilityClasses: result.tailwindClasses,
|
|
654
676
|
cssDirectives: result.tailwindCssDirectives,
|
|
655
677
|
});
|
|
656
|
-
const sx =
|
|
678
|
+
const sx = checkClosedStyling(root, {
|
|
679
|
+
packages: ws.packages,
|
|
680
|
+
renders: summarise(ws).carryUI,
|
|
681
|
+
components: systemComponents(root, design),
|
|
682
|
+
});
|
|
657
683
|
|
|
658
684
|
const breaches = [
|
|
659
685
|
{ rule: "client-boundary", n: cb.crossings.length, what: `${cb.crossings.length} value(s) crossing the "use client" boundary` },
|
|
660
686
|
{ rule: "tailwind-present", n: tw.present ? tw.dependencies.length + tw.configs.length : 0, what: "Tailwind is installed" },
|
|
661
687
|
{ rule: "tailwind-in-css", n: result.tailwindCssDirectives, what: `${result.tailwindCssDirectives} Tailwind directive(s) in stylesheets` },
|
|
662
|
-
{ rule: "styling-layer", n: sx.unknown ? 0 : sx.
|
|
688
|
+
{ rule: "styling-layer", n: sx.unknown || sx.ok ? 0 : sx.hatches.length + sx.competing.length, what: "the styling layer is open — a call site can restyle the component set" },
|
|
663
689
|
].filter((b) => b.n > 0);
|
|
664
690
|
|
|
665
691
|
const blocking = breaches.filter((b) => blocksAt(b.rule, templateVersion));
|
package/emit.mjs
CHANGED
|
@@ -60,7 +60,7 @@ import { generateCss, SEED, varName, CSS_HEADER } from "./tokens.mjs";
|
|
|
60
60
|
* `emit.test.mjs` enforces it: a fresh scaffold must pass the gate Gyde would
|
|
61
61
|
* run against it.
|
|
62
62
|
*/
|
|
63
|
-
export const SCAFFOLD_VERSION = "0.
|
|
63
|
+
export const SCAFFOLD_VERSION = "0.3.0";
|
|
64
64
|
|
|
65
65
|
const GENERATED = (what) => `/* GENERATED BY GYDE — then yours.
|
|
66
66
|
*
|
package/package.json
CHANGED
package/stylex.mjs
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
48
|
+
import { propMembers } from "./props.mjs";
|
|
48
49
|
import { join } from "node:path";
|
|
49
50
|
|
|
50
51
|
export const RULE = "styling-layer";
|
|
@@ -176,3 +177,110 @@ export function formatStyleX(result) {
|
|
|
176
177
|
}
|
|
177
178
|
return L.join("\n");
|
|
178
179
|
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* G-100, narrowed — the styling layer must be CLOSED, not specifically StyleX.
|
|
183
|
+
*
|
|
184
|
+
* The original mandate named an implementation, and its own scaffold failed it.
|
|
185
|
+
* Gyde emits components that style through data attributes against one
|
|
186
|
+
* stylesheet of custom properties, exposing no `className` and no `style` prop
|
|
187
|
+
* at all — and `emit.mjs` ships a test that fails the build if one appears.
|
|
188
|
+
*
|
|
189
|
+
* That set is STRICTER than StyleX, not looser. `stylex.props()` returns a
|
|
190
|
+
* className, which is the escape hatch this rule exists to close. Converting
|
|
191
|
+
* the templates to satisfy the letter of G-100 would have weakened the wall and
|
|
192
|
+
* added a compiler to every scaffold — the cost G-58 Decision 3 measured and
|
|
193
|
+
* declined.
|
|
194
|
+
*
|
|
195
|
+
* So the mandate is restated as the property it was always about:
|
|
196
|
+
*
|
|
197
|
+
* A design system's components must not accept styling from the call site.
|
|
198
|
+
*
|
|
199
|
+
* StyleX satisfies it when wired. So does a closed component set over custom
|
|
200
|
+
* properties. Tailwind does not, which was the actual point (G-98), and neither
|
|
201
|
+
* does a component that takes `className`.
|
|
202
|
+
*
|
|
203
|
+
* WHY THE COMPONENT API IS THE REAL CHECK.
|
|
204
|
+
*
|
|
205
|
+
* Package detection asks what a repository INSTALLED. This asks what its
|
|
206
|
+
* components ACCEPT, which is the thing that decides whether every other rule
|
|
207
|
+
* is enforceable or advisory: one `className` prop on one shared component
|
|
208
|
+
* makes the token layer optional everywhere it is used, whatever the manifest
|
|
209
|
+
* says. G-58 Decision 2 called the wrapper the wall; this is the test that the
|
|
210
|
+
* wall has no door in it.
|
|
211
|
+
*/
|
|
212
|
+
export const CLOSED_RULE = "styling-layer";
|
|
213
|
+
|
|
214
|
+
/** Props that hand styling control back to the call site. */
|
|
215
|
+
const ESCAPE_HATCH = /^(className|class|style|css|sx|tw|unstyled|classNames)$/;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Does the design system expose a way to style it from outside?
|
|
219
|
+
*
|
|
220
|
+
* `components` is the source of the system's own components, as
|
|
221
|
+
* `[{ file, text }]`. Read rather than imported: a prop removed from a type but
|
|
222
|
+
* still spread onto the element is a hole an import-based check cannot see.
|
|
223
|
+
*/
|
|
224
|
+
export function escapeHatches(components) {
|
|
225
|
+
const found = [];
|
|
226
|
+
for (const { file, text } of components) {
|
|
227
|
+
for (const m of propMembers(text, { filename: file })) {
|
|
228
|
+
if (!ESCAPE_HATCH.test(m.prop)) continue;
|
|
229
|
+
found.push({
|
|
230
|
+
file, rule: CLOSED_RULE, line: m.line, raw: m.prop,
|
|
231
|
+
prop: m.prop, type: m.type, kind: "escape-hatch",
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return found;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Is the styling layer closed?
|
|
240
|
+
*
|
|
241
|
+
* Two ways to satisfy it, and the check reports WHICH, because "closed by
|
|
242
|
+
* construction" and "closed by StyleX" fail differently later and a consumer
|
|
243
|
+
* should know which one they are relying on.
|
|
244
|
+
*/
|
|
245
|
+
export function checkClosedStyling(root, { packages = [], renders = null, components = [] } = {}) {
|
|
246
|
+
const hatches = escapeHatches(components);
|
|
247
|
+
const sx = checkStyleX(root, { packages, renders });
|
|
248
|
+
|
|
249
|
+
// A competing runtime CSS-in-JS is still a second way to express styles, and
|
|
250
|
+
// that stays a finding whichever implementation is chosen.
|
|
251
|
+
const competing = sx.unknown ? [] : sx.competing;
|
|
252
|
+
|
|
253
|
+
const hasStyleX = !sx.unknown && sx.missing.length === 0 && sx.unwired.length === 0;
|
|
254
|
+
const closedByConstruction = components.length > 0 && hatches.length === 0;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
unknown: components.length === 0 && sx.unknown,
|
|
258
|
+
hatches, competing,
|
|
259
|
+
implementation: hasStyleX ? "stylex" : closedByConstruction ? "closed-set" : null,
|
|
260
|
+
stylex: sx,
|
|
261
|
+
ok: hatches.length === 0 && competing.length === 0 && (hasStyleX || closedByConstruction),
|
|
262
|
+
componentsChecked: components.length,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function formatClosedStyling(r) {
|
|
267
|
+
if (r.unknown) return "styling layer could not tell — no components read and no package manifest to judge";
|
|
268
|
+
if (r.ok) {
|
|
269
|
+
return r.implementation === "stylex"
|
|
270
|
+
? `styling layer closed, via StyleX (${r.componentsChecked} component(s) expose no styling prop)`
|
|
271
|
+
: `styling layer closed by construction — ${r.componentsChecked} component(s) accept no className, style or css prop`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const L = [`styling layer ${r.hatches.length + r.competing.length} problem(s)`];
|
|
275
|
+
for (const h of r.hatches) L.push(` ${h.file}:${h.line} ${h.type}.${h.prop} — the call site can restyle this component`);
|
|
276
|
+
for (const c of r.competing) L.push(` ${c.package} ${c.name}@${c.version} — ${c.why}`);
|
|
277
|
+
if (r.hatches.length) {
|
|
278
|
+
L.push("");
|
|
279
|
+
L.push(" One styling prop on one shared component makes the token layer optional");
|
|
280
|
+
L.push(" everywhere it is used, whatever the manifest says. The wrapper is the wall");
|
|
281
|
+
L.push(" (G-58 §2); this is the test that the wall has no door in it.");
|
|
282
|
+
} else if (!r.implementation) {
|
|
283
|
+
L.push(` no closed styling layer detected — neither StyleX wired, nor a component set without escape hatches`);
|
|
284
|
+
}
|
|
285
|
+
return L.join("\n");
|
|
286
|
+
}
|