@design-twin/dtwin 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +959 -2
- package/figma-plugin/code.js +4207 -0
- package/figma-plugin/manifest.json +28 -0
- package/figma-plugin/ui.html +686 -0
- package/layer/cross-check.js +2746 -0
- package/layer/drift-lint.js +1526 -0
- package/layer/get-component.js +553 -0
- package/layer/map-bootstrap.js +1404 -0
- package/layer/map-validate.js +397 -0
- package/package.json +53 -4
- package/src/abort-reason.ts +12 -0
- package/src/asset-compare.ts +175 -0
- package/src/asset-owners.ts +280 -0
- package/src/atomic-write.ts +79 -0
- package/src/bridge-holder.ts +294 -0
- package/src/cli-commands.ts +427 -0
- package/src/client-presence.ts +146 -0
- package/src/clock.ts +45 -0
- package/src/commands.ts +561 -0
- package/src/daemon-auth.ts +174 -0
- package/src/daemon.ts +788 -0
- package/src/delivery.ts +85 -0
- package/src/design-system-layout.ts +232 -0
- package/src/doc-types.ts +1054 -0
- package/src/doctor.ts +602 -0
- package/src/errmsg.ts +14 -0
- package/src/export-reader.ts +409 -0
- package/src/export-verdict.ts +258 -0
- package/src/figma-mcp.ts +942 -0
- package/src/figma-pull.ts +1265 -0
- package/src/file-lock.ts +268 -0
- package/src/file-memo.ts +62 -0
- package/src/hash.ts +6 -0
- package/src/hex-color.ts +39 -0
- package/src/human-ms.ts +7 -0
- package/src/init.ts +424 -0
- package/src/instance-overrides.ts +7 -0
- package/src/json-file.ts +79 -0
- package/src/json-size.ts +108 -0
- package/src/json-util.ts +51 -0
- package/src/library-layout.ts +193 -0
- package/src/node-id.ts +69 -0
- package/src/pages-layout.ts +483 -0
- package/src/pending-requests.ts +187 -0
- package/src/plugin-frames.ts +32 -0
- package/src/png.ts +196 -0
- package/src/ports.ts +25 -0
- package/src/project-layout.ts +189 -0
- package/src/quick-keys.ts +68 -0
- package/src/read-json.ts +67 -0
- package/src/read-opts.ts +125 -0
- package/src/read-plan.ts +105 -0
- package/src/read-run.ts +119 -0
- package/src/route.ts +243 -0
- package/src/seed-components.ts +229 -0
- package/src/served-session.ts +347 -0
- package/src/server-core.ts +1093 -0
- package/src/settings.ts +156 -0
- package/src/source-stamp.ts +16 -0
- package/src/staleness.ts +63 -0
- package/src/stdio.ts +156 -0
- package/src/svg-normalize.ts +205 -0
- package/src/timeouts.ts +76 -0
- package/src/timings.ts +118 -0
- package/src/token-store.ts +261 -0
- package/src/variables-merge.ts +337 -0
- package/src/verbs.ts +140 -0
- package/src/write-out.ts +1255 -0
|
@@ -0,0 +1,1404 @@
|
|
|
1
|
+
// GENERATED from design-to-code/ by claude-plugin/build-scripts.js — edit the source, then rebuild.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// design-to-code/map-bootstrap.ts
|
|
5
|
+
import fs6 from "node:fs";
|
|
6
|
+
import { parseArgs as parseArgs2 } from "node:util";
|
|
7
|
+
|
|
8
|
+
// design-to-code/types.ts
|
|
9
|
+
var PLAN_VERIFICATION_RECORD_SCHEMA = "designtwin/plan-verification-record@2";
|
|
10
|
+
function isJsonObject(x) {
|
|
11
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// design-to-code/kinds.ts
|
|
15
|
+
var TYPE_TO_KIND = { VARIANT: "enum", BOOLEAN: "boolean", TEXT: "string", INSTANCE_SWAP: "instance", SLOT: "instance" };
|
|
16
|
+
|
|
17
|
+
// bridge/src/read-json.ts
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
|
|
20
|
+
// bridge/src/errmsg.ts
|
|
21
|
+
var errMsg = (e) => typeof e === "string" ? e : String(e && e.message || e);
|
|
22
|
+
var errCode = (e) => e && typeof e === "object" && "code" in e && typeof e.code === "string" ? e.code : void 0;
|
|
23
|
+
|
|
24
|
+
// bridge/src/read-json.ts
|
|
25
|
+
var anyJson = (_x) => true;
|
|
26
|
+
function readFailure(e) {
|
|
27
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
28
|
+
if (code === "ENOENT") return { error: "does not exist", missing: true };
|
|
29
|
+
return {
|
|
30
|
+
error: code === "EISDIR" ? "is a directory, not a file" : code === "EACCES" ? "is not readable (permission denied)" : `could not be read (${String(code || e)})`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function readJson(file, guard) {
|
|
34
|
+
let buf;
|
|
35
|
+
try {
|
|
36
|
+
buf = fs.readFileSync(file);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return readFailure(e);
|
|
39
|
+
}
|
|
40
|
+
if (buf.length >= 2 && (buf[0] === 255 && buf[1] === 254 || buf[0] === 254 && buf[1] === 255)) {
|
|
41
|
+
return { error: "is UTF-16, not UTF-8 \u2014 re-save it as UTF-8" };
|
|
42
|
+
}
|
|
43
|
+
let raw = buf.toString("utf8");
|
|
44
|
+
if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
|
|
45
|
+
let parsed;
|
|
46
|
+
try {
|
|
47
|
+
const value = JSON.parse(raw);
|
|
48
|
+
parsed = value;
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return { error: `is not valid JSON \u2014 ${errMsg(e)}` };
|
|
51
|
+
}
|
|
52
|
+
if (!guard(parsed)) return { error: `is not ${guard.expected || "the expected kind of document"}` };
|
|
53
|
+
return { doc: parsed };
|
|
54
|
+
}
|
|
55
|
+
function readJsonOrNull(file, guard) {
|
|
56
|
+
const r = readJson(file, guard);
|
|
57
|
+
return "doc" in r ? r.doc : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// bridge/src/stdio.ts
|
|
61
|
+
import { format, inspect } from "node:util";
|
|
62
|
+
function installQueuedConsole(o = {}) {
|
|
63
|
+
trackWrites(process.stdout);
|
|
64
|
+
trackWrites(process.stderr);
|
|
65
|
+
const out = o.logTo === "stderr" ? process.stderr : process.stdout;
|
|
66
|
+
const err = process.stderr;
|
|
67
|
+
let indent = "";
|
|
68
|
+
const write = (s, text) => {
|
|
69
|
+
s.write((indent ? text.replace(/^/gm, indent) : text) + "\n");
|
|
70
|
+
};
|
|
71
|
+
const line = (s) => (...a) => {
|
|
72
|
+
write(s, format(...a));
|
|
73
|
+
};
|
|
74
|
+
console.log = console.info = console.debug = line(out);
|
|
75
|
+
console.warn = console.error = line(err);
|
|
76
|
+
console.dir = (item, options) => {
|
|
77
|
+
write(out, inspect(item, { customInspect: false, ...options }));
|
|
78
|
+
};
|
|
79
|
+
console.dirxml = line(out);
|
|
80
|
+
console.table = (data) => {
|
|
81
|
+
write(out, inspect(data));
|
|
82
|
+
};
|
|
83
|
+
console.write = (...data) => {
|
|
84
|
+
let n = 0;
|
|
85
|
+
for (const d of data) {
|
|
86
|
+
const b = typeof d === "string" ? Buffer.from(d) : d instanceof ArrayBuffer ? Buffer.from(d) : Buffer.from(d.buffer, d.byteOffset, d.byteLength);
|
|
87
|
+
if (b.length) {
|
|
88
|
+
out.write(b);
|
|
89
|
+
n += b.length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return n;
|
|
93
|
+
};
|
|
94
|
+
console.clear = () => {
|
|
95
|
+
if (out.isTTY) out.write("\x1B[1;1H\x1B[0J");
|
|
96
|
+
};
|
|
97
|
+
console.trace = (...a) => {
|
|
98
|
+
const e = new Error(format(...a));
|
|
99
|
+
e.name = "Trace";
|
|
100
|
+
Error.captureStackTrace(e, console.trace);
|
|
101
|
+
write(err, e.stack ?? `Trace: ${e.message}`);
|
|
102
|
+
};
|
|
103
|
+
console.assert = (cond, ...a) => {
|
|
104
|
+
if (!cond) write(err, a.length ? "Assertion failed: " + format(...a) : "Assertion failed");
|
|
105
|
+
};
|
|
106
|
+
console.group = console.groupCollapsed = (...a) => {
|
|
107
|
+
if (a.length) write(out, format(...a));
|
|
108
|
+
indent += " ";
|
|
109
|
+
};
|
|
110
|
+
console.groupEnd = () => {
|
|
111
|
+
indent = indent.slice(0, -2);
|
|
112
|
+
};
|
|
113
|
+
const counts = /* @__PURE__ */ new Map();
|
|
114
|
+
console.count = (label = "default") => {
|
|
115
|
+
const n = (counts.get(label) ?? 0) + 1;
|
|
116
|
+
counts.set(label, n);
|
|
117
|
+
write(out, `${label}: ${n}`);
|
|
118
|
+
};
|
|
119
|
+
console.countReset = (label = "default") => {
|
|
120
|
+
counts.delete(label);
|
|
121
|
+
};
|
|
122
|
+
const timers = /* @__PURE__ */ new Map();
|
|
123
|
+
const elapsed = (label) => {
|
|
124
|
+
const t = timers.get(label);
|
|
125
|
+
return t === void 0 ? null : `${label}: ${(performance.now() - t).toFixed(3)}ms`;
|
|
126
|
+
};
|
|
127
|
+
console.time = (label = "default") => {
|
|
128
|
+
timers.set(label, performance.now());
|
|
129
|
+
};
|
|
130
|
+
console.timeLog = (label = "default", ...a) => {
|
|
131
|
+
const e = elapsed(label);
|
|
132
|
+
if (e === null) write(err, `Warning: No such label '${label}' for console.timeLog()`);
|
|
133
|
+
else write(out, [e, ...a.map((x) => format(x))].join(" "));
|
|
134
|
+
};
|
|
135
|
+
console.timeEnd = (label = "default") => {
|
|
136
|
+
const e = elapsed(label);
|
|
137
|
+
timers.delete(label);
|
|
138
|
+
write(e === null ? err : out, e ?? `Warning: No such label '${label}' for console.timeEnd()`);
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
142
|
+
function trackWrites(s) {
|
|
143
|
+
if (inFlight.has(s)) return;
|
|
144
|
+
const st = { n: 0, idle: [] };
|
|
145
|
+
inFlight.set(s, st);
|
|
146
|
+
const write = s.write.bind(s);
|
|
147
|
+
const settle = () => {
|
|
148
|
+
if (--st.n === 0) for (const r of st.idle.splice(0)) r();
|
|
149
|
+
};
|
|
150
|
+
s.write = (chunk, encoding, cb) => {
|
|
151
|
+
const done = typeof encoding === "function" ? encoding : cb;
|
|
152
|
+
const enc = typeof encoding === "function" ? void 0 : encoding;
|
|
153
|
+
if (chunk.length === 0) return write(chunk, enc, done);
|
|
154
|
+
st.n++;
|
|
155
|
+
let called = false;
|
|
156
|
+
return write(chunk, enc, (err) => {
|
|
157
|
+
if (!called) {
|
|
158
|
+
called = true;
|
|
159
|
+
settle();
|
|
160
|
+
}
|
|
161
|
+
done?.(err);
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
var CliExit = class extends Error {
|
|
166
|
+
code;
|
|
167
|
+
constructor(code) {
|
|
168
|
+
super(`exit ${code}`);
|
|
169
|
+
this.name = "CliExit";
|
|
170
|
+
this.code = code;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
var cliDepth = 0;
|
|
174
|
+
function exitCli(code) {
|
|
175
|
+
if (cliDepth > 0) throw new CliExit(code);
|
|
176
|
+
process.exit(code);
|
|
177
|
+
}
|
|
178
|
+
function runCli(main2, o, argv = process.argv.slice(2)) {
|
|
179
|
+
installQueuedConsole();
|
|
180
|
+
const done = o.done ?? ((code) => {
|
|
181
|
+
process.exitCode = code;
|
|
182
|
+
});
|
|
183
|
+
const fail = (e) => {
|
|
184
|
+
if (e instanceof CliExit) return done(e.code);
|
|
185
|
+
console.error(o.errorLine ? o.errorLine(e) : `${o.name}: ${e instanceof Error && e.stack || String(e)}`);
|
|
186
|
+
done(o.errorCode ?? 1);
|
|
187
|
+
};
|
|
188
|
+
cliDepth++;
|
|
189
|
+
let settled = false;
|
|
190
|
+
const leave = () => {
|
|
191
|
+
if (!settled) {
|
|
192
|
+
settled = true;
|
|
193
|
+
cliDepth--;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
try {
|
|
197
|
+
const r = main2(argv);
|
|
198
|
+
if (typeof r === "number") {
|
|
199
|
+
leave();
|
|
200
|
+
done(r);
|
|
201
|
+
} else r.then((c) => {
|
|
202
|
+
leave();
|
|
203
|
+
done(c);
|
|
204
|
+
}, (e) => {
|
|
205
|
+
leave();
|
|
206
|
+
fail(e);
|
|
207
|
+
});
|
|
208
|
+
} catch (e) {
|
|
209
|
+
leave();
|
|
210
|
+
fail(e);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// design-to-code/proc.ts
|
|
215
|
+
var pause = new Int32Array(new SharedArrayBuffer(4));
|
|
216
|
+
|
|
217
|
+
// design-to-code/catalog-input.ts
|
|
218
|
+
function isManifest(doc, payloadKey) {
|
|
219
|
+
return isJsonObject(doc) && isJsonObject(doc.files) && !Array.isArray(doc[payloadKey]);
|
|
220
|
+
}
|
|
221
|
+
function manifestProblem(doc, givenPath, payloadKey, wantFile) {
|
|
222
|
+
if (!isManifest(doc, payloadKey)) return null;
|
|
223
|
+
return `'${givenPath}' is the design-system MANIFEST (a pointer map), not the '${payloadKey}' catalog.
|
|
224
|
+
Since the design-system split it carries only a stamp, a \`files\` map and \`counts\`.
|
|
225
|
+
Pass the split file instead \u2014 e.g. ${doc.files[payloadKey === "variables" ? "tokens" : "componentsLocal"] || wantFile} (relative to the export dir that holds ${givenPath}).`;
|
|
226
|
+
}
|
|
227
|
+
function readJsonFile(file, what, hint) {
|
|
228
|
+
return readDocFile(file, what, anyJson, hint);
|
|
229
|
+
}
|
|
230
|
+
function readDocFile(file, what, guard, hint) {
|
|
231
|
+
return docOrExit(loadDocFile(file, what, guard, hint));
|
|
232
|
+
}
|
|
233
|
+
function loadDocFile(file, what, guard, hint, readAt = file) {
|
|
234
|
+
const r = readJson(readAt, guard);
|
|
235
|
+
if ("doc" in r) return r;
|
|
236
|
+
const ioFailure = r.missing || /^(is a directory|is not readable|could not be read)/.test(r.error);
|
|
237
|
+
return { error: `${what}: '${file}' ${r.error}${ioFailure ? "." : ""}` + (ioFailure && hint ? `
|
|
238
|
+
${hint}` : "") };
|
|
239
|
+
}
|
|
240
|
+
function docOrExit(r) {
|
|
241
|
+
if ("doc" in r) return r.doc;
|
|
242
|
+
console.error(`error ${r.error}`);
|
|
243
|
+
exitCli(2);
|
|
244
|
+
}
|
|
245
|
+
function readSplitFile(file, what, guard, payloadKey, wantFile, hint) {
|
|
246
|
+
return docOrExit(loadSplitFile(file, what, guard, payloadKey, wantFile, hint));
|
|
247
|
+
}
|
|
248
|
+
function loadSplitFile(file, what, guard, payloadKey, wantFile, hint) {
|
|
249
|
+
const r = loadDocFile(file, what, anyJson, hint);
|
|
250
|
+
if ("error" in r) return r;
|
|
251
|
+
const manifest = manifestProblem(r.doc, file, payloadKey, wantFile);
|
|
252
|
+
if (manifest !== null) return { error: manifest };
|
|
253
|
+
if (guard(r.doc)) return { doc: r.doc };
|
|
254
|
+
return { error: `${what}: '${file}' is not ${guard.expected || "the expected kind of document"}` };
|
|
255
|
+
}
|
|
256
|
+
var NO_DESIGN_SYSTEM_HINT = "A single-screen pull (`dtwin pull --node <id>`) exports only that screen \u2014 it does not\n write design/export/design-system/. Run `dtwin pull --design-system` to create it.";
|
|
257
|
+
|
|
258
|
+
// bridge/src/json-util.ts
|
|
259
|
+
function nullProto() {
|
|
260
|
+
return /* @__PURE__ */ Object.create(null);
|
|
261
|
+
}
|
|
262
|
+
function isRecord(x) {
|
|
263
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
264
|
+
}
|
|
265
|
+
function isStringArray(x) {
|
|
266
|
+
return Array.isArray(x) && x.every((v) => typeof v === "string");
|
|
267
|
+
}
|
|
268
|
+
function ifDefined(key, v) {
|
|
269
|
+
const o = {};
|
|
270
|
+
if (v !== void 0) o[key] = v;
|
|
271
|
+
return o;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// design-to-code/doc-guards.ts
|
|
275
|
+
function optArrayOf(x, each) {
|
|
276
|
+
return x === void 0 || Array.isArray(x) && x.every(each);
|
|
277
|
+
}
|
|
278
|
+
var optObj = (x) => x === void 0 || isJsonObject(x);
|
|
279
|
+
var optStr = (x) => x === void 0 || typeof x === "string";
|
|
280
|
+
var anyObject = (x) => isJsonObject(x);
|
|
281
|
+
function isVariable(x) {
|
|
282
|
+
return isJsonObject(x) && typeof x.name === "string" && typeof x.type === "string" && isJsonObject(x.values) && optStr(x.collection);
|
|
283
|
+
}
|
|
284
|
+
function isVariableCollection(x) {
|
|
285
|
+
return isJsonObject(x) && typeof x.name === "string" && isStringArray(x.modes);
|
|
286
|
+
}
|
|
287
|
+
function isTokensDoc(x) {
|
|
288
|
+
return isJsonObject(x) && optArrayOf(x.variables, isVariable) && optArrayOf(x.collections, isVariableCollection) && optArrayOf(x._slices, anyObject) && optArrayOf(x._conflicts, anyObject) && (x.hygiene === void 0 || isStringArray(x.hygiene));
|
|
289
|
+
}
|
|
290
|
+
isTokensDoc.expected = "a token catalog: an object whose `variables` (each {name, type, values}) and `collections` (each {name, modes[]}), when present, are arrays";
|
|
291
|
+
function isCatalogComponent(x) {
|
|
292
|
+
return isJsonObject(x) && typeof x.name === "string" && optStr(x.key) && optStr(x.id) && optObj(x.props) && optArrayOf(x.variants, anyObject);
|
|
293
|
+
}
|
|
294
|
+
function isComponentsCatalog(x) {
|
|
295
|
+
return isJsonObject(x) && Array.isArray(x.components) && x.components.every(isCatalogComponent);
|
|
296
|
+
}
|
|
297
|
+
isComponentsCatalog.expected = "a component catalog: an object with a `components` array of {name, type, key?, id?, props?}";
|
|
298
|
+
function isComponentDetailFile(x) {
|
|
299
|
+
return isJsonObject(x) && typeof x.name === "string" && optArrayOf(x.variants, anyObject) && optObj(x.node);
|
|
300
|
+
}
|
|
301
|
+
isComponentDetailFile.expected = "a component detail file: an object with a `name` and `variants[]` or `node`";
|
|
302
|
+
function isTextStylesDoc(x) {
|
|
303
|
+
return isJsonObject(x) && Array.isArray(x.styles) && x.styles.every((s) => isJsonObject(s) && typeof s.name === "string");
|
|
304
|
+
}
|
|
305
|
+
isTextStylesDoc.expected = "a text-style sheet: an object with a `styles` array of {name, \u2026}";
|
|
306
|
+
function isScreenAssetsDoc(x) {
|
|
307
|
+
return isJsonObject(x) && optArrayOf(x.heavy, (h) => isJsonObject(h) && typeof h.file === "string" && typeof h.bytes === "number" && (h.paths === void 0 || typeof h.paths === "number") && (h.embeddedRaster === void 0 || typeof h.embeddedRaster === "number")) && optArrayOf(x.files, (f) => isJsonObject(f) && typeof f.file === "string" && optStr(f.node));
|
|
308
|
+
}
|
|
309
|
+
isScreenAssetsDoc.expected = "a screen asset manifest: an object whose `heavy` ({file, bytes}) and `files` ({file, node?}), when present, are arrays";
|
|
310
|
+
function isLibrariesIndex(x) {
|
|
311
|
+
return isJsonObject(x) && Array.isArray(x.libraries) && x.libraries.every((r) => isJsonObject(r) && typeof r.dir === "string" && optStr(r.libraryName) && (r.collectionKeys === void 0 || isStringArray(r.collectionKeys)));
|
|
312
|
+
}
|
|
313
|
+
isLibrariesIndex.expected = "a library index: an object with a `libraries` array of {dir, libraryName?, collectionKeys?}";
|
|
314
|
+
var SEVERITIES = ["blocker", "warning", "info"];
|
|
315
|
+
function isAuditOverridesDoc(x) {
|
|
316
|
+
return isJsonObject(x) && Array.isArray(x.overrides) && x.overrides.every((o) => isJsonObject(o) && typeof o.code === "string" && typeof o.severity === "string" && SEVERITIES.includes(o.severity) && typeof o.reason === "string" && o.reason.trim() !== "" && ["nodeId", "token", "component", "collection", "mode", "category", "state", "screen", "decidedBy", "decidedAt"].every((k) => optStr(o[k])));
|
|
317
|
+
}
|
|
318
|
+
isAuditOverridesDoc.expected = "an audit overrides file: { overrides: [{ code, severity: blocker|warning|info, reason (non-empty), nodeId?, token?, component?, collection?, mode?, category?, state?, screen?, decidedBy?, decidedAt? }] }";
|
|
319
|
+
function isAuditReport(x) {
|
|
320
|
+
return isJsonObject(x) && isJsonObject(x.summary) && Array.isArray(x.findings) && x.findings.every((f) => isJsonObject(f) && typeof f.severity === "string" && typeof f.code === "string");
|
|
321
|
+
}
|
|
322
|
+
isAuditReport.expected = "an audit report (written by the audit script's --out): an object with `summary` and a `findings` array of {severity, code, message}";
|
|
323
|
+
function isProposal(x) {
|
|
324
|
+
return isJsonObject(x) && typeof x.name === "string";
|
|
325
|
+
}
|
|
326
|
+
function isProposalList(x) {
|
|
327
|
+
return Array.isArray(x) && x.every(isProposal);
|
|
328
|
+
}
|
|
329
|
+
isProposalList.expected = "a list of component proposals: an array of {name, catalog, confirmed, \u2026}";
|
|
330
|
+
function isVerifyExpectation(x) {
|
|
331
|
+
return isJsonObject(x) && isJsonObject(x.frame) && Array.isArray(x.nodes) && x.nodes.every((n) => isJsonObject(n) && typeof n.nodeId === "string") && optArrayOf(x.instances, anyObject) && optArrayOf(x.interactions, anyObject) && optArrayOf(x.notComparable, anyObject);
|
|
332
|
+
}
|
|
333
|
+
isVerifyExpectation.expected = "a verify expectation (the verify-screen script's --expect output): an object with `frame` and a `nodes` array of {nodeId, \u2026}";
|
|
334
|
+
var isNameVersion = (x) => isJsonObject(x) && typeof x.version === "string" && (typeof x.package === "string" || typeof x.name === "string");
|
|
335
|
+
function isProbeIdentity(x) {
|
|
336
|
+
return isJsonObject(x) && typeof x.name === "string" && (x.version === null || typeof x.version === "string") && typeof x.sha256 === "string" && isNameVersion(x.playwright) && isNameVersion(x.browser);
|
|
337
|
+
}
|
|
338
|
+
function isBuildIdentity(x) {
|
|
339
|
+
return isJsonObject(x) && typeof x.url === "string" && (x.mode === "vite-dev" || x.mode === "static" || x.mode === "unknown") && typeof x.assets === "number" && typeof x.assetsSha256 === "string" && (x.unhashed === void 0 || typeof x.unhashed === "number") && (x.gitHead === null || typeof x.gitHead === "string") && (x.gitDirty === null || typeof x.gitDirty === "boolean");
|
|
340
|
+
}
|
|
341
|
+
var isProbeFrame = (x) => isJsonObject(x) && typeof x.nodeId === "string" && typeof x.selector === "string" && typeof x.via === "string" && isJsonObject(x.rect);
|
|
342
|
+
var isCountMap = (x) => isJsonObject(x) && Object.values(x).every((v) => typeof v === "number");
|
|
343
|
+
var isNavigation = (x) => isJsonObject(x) && Array.isArray(x.events) && typeof x.afterInitialLoad === "number" && typeof x.reruns === "number";
|
|
344
|
+
var isTagsNotInExpectation = (x) => isJsonObject(x) && typeof x.count === "number" && Array.isArray(x.ids) && x.ids.every((r) => isJsonObject(r) && typeof r.id === "string" && typeof r.elements === "number");
|
|
345
|
+
var isReasonMap = (x) => isJsonObject(x) && Object.values(x).every((v) => typeof v === "string");
|
|
346
|
+
var isNum = (x) => typeof x === "number" && Number.isFinite(x);
|
|
347
|
+
function isProbeReach(x) {
|
|
348
|
+
return isJsonObject(x) && Array.isArray(x.steps) && x.steps.every(isJsonObject) && typeof x.sha256 === "string" && typeof x.source === "string" && typeof x.url === "string";
|
|
349
|
+
}
|
|
350
|
+
function isPageOverflow(x) {
|
|
351
|
+
return isJsonObject(x) && isJsonObject(x.viewport) && isNum(x.viewport.w) && isNum(x.viewport.h) && isNum(x.scrollWidth) && isNum(x.clientWidth) && typeof x.overflowX === "string" && typeof x.scrollable === "boolean" && Array.isArray(x.offenders) && x.offenders.every((o) => isJsonObject(o) && typeof o.path === "string" && (o.dt === null || typeof o.dt === "string") && isNum(o.right)) && optStr(x.compatMode);
|
|
352
|
+
}
|
|
353
|
+
var BEHAVIOUR_STATUSES = ["pass", "fail", "warn", "not-run", "unsupported"];
|
|
354
|
+
var isBehaviourStatus = (x) => typeof x === "string" && BEHAVIOUR_STATUSES.some((s) => s === x);
|
|
355
|
+
function isBehaviourCheck(x) {
|
|
356
|
+
return isJsonObject(x) && typeof x.id === "string" && isBehaviourStatus(x.status) && typeof x.detail === "string";
|
|
357
|
+
}
|
|
358
|
+
function isMeasuredBehaviour(x) {
|
|
359
|
+
return isJsonObject(x) && x.version === 1 && typeof x.ran === "boolean" && (x.ran ? Array.isArray(x.checks) && x.checks.every(isBehaviourCheck) : typeof x.why === "string");
|
|
360
|
+
}
|
|
361
|
+
function isMeasuredVisual(x) {
|
|
362
|
+
return isJsonObject(x) && x.version === 1 && typeof x.ran === "boolean" && (x.ran ? isNum(x.differingPct) && isNum(x.shiftTolerantPct) && Array.isArray(x.regions) : typeof x.why === "string");
|
|
363
|
+
}
|
|
364
|
+
var MEASURED_EXTRAS = [
|
|
365
|
+
["probe", isProbeIdentity, "the shipped probe's identity {name, version, sha256, playwright:{package, version}, browser:{name, version}} \u2014 read as probe: unknown"],
|
|
366
|
+
["frame", isProbeFrame, "a probe frame {nodeId, selector, via, rect}"],
|
|
367
|
+
["frames", (x) => Array.isArray(x) && x.every(isProbeFrame), "a list of probe frames {nodeId, selector, via, rect}"],
|
|
368
|
+
["navigation", isNavigation, "a navigation log {events[], afterInitialLoad, reruns}"],
|
|
369
|
+
["matchedByCensus", isCountMap, "a {rule: count} map"],
|
|
370
|
+
["notMeasured", Array.isArray, "a list \u2014 the probe's reasons for unmatched nodes are not used"],
|
|
371
|
+
// the run it belongs to and the build it was served
|
|
372
|
+
["runId", (x) => typeof x === "string" && x !== "", "a run id (string) \u2014 the measurement is tied to no verify run"],
|
|
373
|
+
["build", isBuildIdentity, "a build identity {url, mode: vite-dev|static|unknown, assets, assetsSha256, gitHead, gitDirty} \u2014 read as build: unknown"],
|
|
374
|
+
// the shipped probe's foreign tags
|
|
375
|
+
["tagsNotInExpectation", isTagsNotInExpectation, "a foreign-tag list {count, ids: [{id, elements}]}"],
|
|
376
|
+
// the steps replayed, the page's overflow, the behaviour/a11y block
|
|
377
|
+
["reach", isProbeReach, "the probe's steps {steps[], sha256, source, url}"],
|
|
378
|
+
["page", isPageOverflow, "a page overflow {viewport:{w,h}, scrollWidth, clientWidth, overflowX, scrollable, offenders[]} \u2014 page overflow not measured"],
|
|
379
|
+
["behaviour", isMeasuredBehaviour, "a behaviour block {version: 1, ran: true, checks: [{id, status: pass|fail|warn|not-run|unsupported, detail}], \u2026} or {version: 1, ran: false, why} \u2014 behaviour/a11y not reported"],
|
|
380
|
+
// the visual diff (informational)
|
|
381
|
+
["visual", isMeasuredVisual, "a visual block {version: 1, ran: true, differingPct, shiftTolerantPct, regions: [\u2026], \u2026} or {version: 1, ran: false, why} \u2014 the visual diff not reported"]
|
|
382
|
+
];
|
|
383
|
+
function isMeasuredCore(x) {
|
|
384
|
+
return isJsonObject(x) && optArrayOf(x.nodes, (n) => isJsonObject(n) && typeof n.nodeId === "string") && optArrayOf(x.components, anyObject) && optArrayOf(x.interactions, anyObject) && (x.artifacts === void 0 || Array.isArray(x.artifacts)) && optStr(x.mode) && optStr(x.expectationSha256);
|
|
385
|
+
}
|
|
386
|
+
function isVerifyMeasured(x) {
|
|
387
|
+
return isMeasuredCore(x) && MEASURED_EXTRAS.every(([k, ok]) => x[k] === void 0 || ok(x[k])) && (x.nodes === void 0 || Array.isArray(x.nodes) && x.nodes.every((n) => !isJsonObject(n) || n.unmeasured === void 0 || isReasonMap(n.unmeasured)));
|
|
388
|
+
}
|
|
389
|
+
isVerifyMeasured.expected = "probe measurements: an object whose `nodes` (each {nodeId, styles}), `components`, `interactions` and `artifacts`, when present, are arrays";
|
|
390
|
+
function isEvidence(x) {
|
|
391
|
+
return isJsonObject(x) && typeof x.nodeId === "string";
|
|
392
|
+
}
|
|
393
|
+
function isInteractionEvidenceList(x) {
|
|
394
|
+
return Array.isArray(x) && x.every(isEvidence);
|
|
395
|
+
}
|
|
396
|
+
isInteractionEvidenceList.expected = "interaction evidence: a JSON array of {nodeId, trigger, ok, selector, selectorCount, detail}";
|
|
397
|
+
function isMeasuredComponentList(x) {
|
|
398
|
+
return Array.isArray(x) && x.every((c) => isJsonObject(c) && optStr(c.setName) && optStr(c.name) && optStr(c.nodeId) && (c.present === void 0 || typeof c.present === "boolean"));
|
|
399
|
+
}
|
|
400
|
+
isMeasuredComponentList.expected = "component evidence: a JSON array of {setName|nodeId, present: true|false}";
|
|
401
|
+
function isVerifyReport(x) {
|
|
402
|
+
return isJsonObject(x) && optStr(x.schema) && optStr(x.verdict) && optStr(x.screen) && optStr(x.nodeId) && optStr(x.headline) && (x.why === void 0 || isStringArray(x.why)) && (x.integrity === void 0 || isStringArray(x.integrity)) && optArrayOf(x.deltas, anyObject) && optObj(x.inputs);
|
|
403
|
+
}
|
|
404
|
+
isVerifyReport.expected = "a verify report (the verify-screen script's --compare output): an object with `verdict`, `why[]`, `deltas[]`, `inputs`";
|
|
405
|
+
var PLAN_ARRAYS = ["files", "tokens", "components", "hidden", "anchorsSuggested", "deviations", "allowedLiterals", "waivers", "descopes"];
|
|
406
|
+
var PLAN_OBJECTS = ["anchors", "verification", "counts"];
|
|
407
|
+
var PLAN_STRINGS = ["schema", "screen", "screenName", "nodeId", "route", "file", "exportedAt", "status"];
|
|
408
|
+
function planProblem(x) {
|
|
409
|
+
if (!isJsonObject(x)) return "is not a plan (the file holds " + (Array.isArray(x) ? "an array" : x === null ? "null" : typeof x) + ", not an object)";
|
|
410
|
+
for (const k of PLAN_ARRAYS) if (x[k] !== void 0 && !Array.isArray(x[k])) return `is not a valid plan: \`${k}\` must be an array`;
|
|
411
|
+
for (const k of PLAN_OBJECTS) if (x[k] !== void 0 && !isJsonObject(x[k])) return `is not a valid plan: \`${k}\` must be an object`;
|
|
412
|
+
for (const k of PLAN_STRINGS) if (x[k] !== void 0 && x[k] !== null && typeof x[k] !== "string") return `is not a valid plan: \`${k}\` must be a string`;
|
|
413
|
+
if (x.files !== void 0 && !isStringArray(x.files)) return "is not a valid plan: `files` must be an array of paths (strings)";
|
|
414
|
+
for (const k of ["tokens", "components", "allowedLiterals", "deviations", "hidden", "anchorsSuggested", "waivers", "descopes"]) {
|
|
415
|
+
const list = x[k];
|
|
416
|
+
if (Array.isArray(list) && !list.every(isJsonObject)) return `is not a valid plan: every \`${k}\` entry must be an object`;
|
|
417
|
+
}
|
|
418
|
+
if (Array.isArray(x.tokens) && !x.tokens.every((t) => isJsonObject(t) && (t.figmaName === null || optStr(t.figmaName)))) return "is not a valid plan: a `tokens` row's `figmaName` must be a string";
|
|
419
|
+
if (Array.isArray(x.components) && !x.components.every((c) => isJsonObject(c) && typeof c.name === "string")) return "is not a valid plan: every `components` row needs its `name`";
|
|
420
|
+
if (isJsonObject(x.anchors) && !Object.values(x.anchors).every(isJsonObject)) return "is not a valid plan: every `anchors` entry must be an object";
|
|
421
|
+
if (x.auditGate !== void 0 && x.auditGate !== null && !isJsonObject(x.auditGate)) return "is not a valid plan: `auditGate` must be an object or null";
|
|
422
|
+
if (x.target !== void 0 && x.target !== null && typeof x.target !== "string" && !isJsonObject(x.target)) return "is not a valid plan: `target` must be a profile name, an object or null";
|
|
423
|
+
if (x.tagging !== void 0 && x.tagging !== null && !(isJsonObject(x.tagging) && (x.tagging.off === void 0 || typeof x.tagging.off === "boolean") && optStr(x.tagging.reason))) return 'is not a valid plan: `tagging` must be {"off": true, "reason": "\u2026"}';
|
|
424
|
+
if (Array.isArray(x.tokens) && !x.tokens.every((t) => isJsonObject(t) && optStr(t.acknowledged))) return "is not a valid plan: a `tokens` row's `acknowledged` must be a string (the reason)";
|
|
425
|
+
if (isJsonObject(x.verification) && x.verification.hook !== void 0 && !isJsonObject(x.verification.hook)) return "is not a valid plan: `verification.hook` must be an object";
|
|
426
|
+
if (isJsonObject(x.verification) && x.verification.recorded !== void 0 && !isJsonObject(x.verification.recorded)) return "is not a valid plan: `verification.recorded` must be an object";
|
|
427
|
+
for (const k of ["navigate", "interactions"]) if (x[k] !== void 0 && !Array.isArray(x[k])) return `is not a valid plan: \`${k}\` must be an array`;
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
function isPlan(x) {
|
|
431
|
+
return planProblem(x) === null;
|
|
432
|
+
}
|
|
433
|
+
isPlan.expected = "a plan (started by the plan-skeleton script): an object whose files/tokens/components/deviations are arrays of objects and whose anchors/verification are objects";
|
|
434
|
+
var recIsCount = (x) => typeof x === "number" && Number.isFinite(x);
|
|
435
|
+
var recIsStr = (v) => typeof v === "string";
|
|
436
|
+
var recNeed = (ok) => ({ need: true, ok });
|
|
437
|
+
var recOpt = (ok) => ({ need: false, ok });
|
|
438
|
+
var RECORD_COUNTS = Object.fromEntries(["high", "medium", "low", "accepted", "notMeasured", "inferred"].map((k) => [k, recNeed(recIsCount)]));
|
|
439
|
+
var RECORD_DELTA = { nodeId: recNeed(recIsStr), field: recNeed(recIsStr), severity: recNeed((v) => v === "high" || v === "medium"), expected: recOpt(() => true), actual: recOpt(() => true) };
|
|
440
|
+
var RECORD_A11Y = { tool: recNeed(recIsStr), violations: recNeed(recIsCount), warnings: recNeed(recIsCount), report: recNeed(recIsStr) };
|
|
441
|
+
var RECORD_FIELDS = {
|
|
442
|
+
schema: recNeed((v) => v === PLAN_VERIFICATION_RECORD_SCHEMA),
|
|
443
|
+
by: recNeed(recIsStr),
|
|
444
|
+
at: recNeed(recIsStr),
|
|
445
|
+
report: recNeed(recIsStr),
|
|
446
|
+
reportSha256: recNeed(recIsStr),
|
|
447
|
+
verdict: recNeed(recIsStr),
|
|
448
|
+
headline: recNeed(recIsStr),
|
|
449
|
+
behaviourHeadline: recOpt(recIsStr),
|
|
450
|
+
counts: recNeed(isJsonObject),
|
|
451
|
+
mode: recNeed((v) => v === "rendered" || v === "static-only"),
|
|
452
|
+
reason: recOpt(recIsStr),
|
|
453
|
+
renderer: recOpt(recIsStr),
|
|
454
|
+
artifacts: recNeed(isStringArray),
|
|
455
|
+
deltas: recNeed(Array.isArray),
|
|
456
|
+
a11y: recOpt(isJsonObject)
|
|
457
|
+
};
|
|
458
|
+
var RECORD_KEYS = Object.keys(RECORD_FIELDS);
|
|
459
|
+
var reqStr = (v) => typeof v === "string" && v.trim() !== "";
|
|
460
|
+
function isPlanWaiver(x) {
|
|
461
|
+
return isJsonObject(x) && reqStr(x.nodeId) && reqStr(x.field) && x.designed !== void 0 && x.built !== void 0 && reqStr(x.exportContentSha256) && reqStr(x.reason) && reqStr(x.decidedBy) && reqStr(x.decidedAt) && (x.tolerance === void 0 || typeof x.tolerance === "number" && x.tolerance >= 0) && optStr(x.cause);
|
|
462
|
+
}
|
|
463
|
+
isPlanWaiver.expected = "a plan waiver {nodeId, field, designed, built, exportContentSha256, reason, decidedBy, decidedAt, tolerance?, cause?}";
|
|
464
|
+
function isPlanDescope(x) {
|
|
465
|
+
return isJsonObject(x) && reqStr(x.nodeId) && reqStr(x.trigger) && optStr(x.destinationId) && reqStr(x.exportContentSha256) && reqStr(x.reason) && reqStr(x.decidedBy) && reqStr(x.decidedAt);
|
|
466
|
+
}
|
|
467
|
+
isPlanDescope.expected = "a plan descope {nodeId, trigger, destinationId?, exportContentSha256, reason, decidedBy, decidedAt}";
|
|
468
|
+
function isStringRecord(x) {
|
|
469
|
+
return isJsonObject(x) && Object.values(x).every((v) => typeof v === "string");
|
|
470
|
+
}
|
|
471
|
+
isStringRecord.expected = "an object of strings";
|
|
472
|
+
|
|
473
|
+
// design-to-code/export-shape.ts
|
|
474
|
+
function isIrNode(x) {
|
|
475
|
+
return isJsonObject(x) && typeof x.id === "string" && typeof x.type === "string" && (x.children === void 0 || Array.isArray(x.children));
|
|
476
|
+
}
|
|
477
|
+
function isScreenExport(x) {
|
|
478
|
+
return isJsonObject(x) && Array.isArray(x.nodes) && x.nodes.every(isIrNode);
|
|
479
|
+
}
|
|
480
|
+
function isLayerFile(x) {
|
|
481
|
+
return isJsonObject(x) && !Array.isArray(x.nodes) && isIrNode(x.tree);
|
|
482
|
+
}
|
|
483
|
+
function isScreenDoc(x) {
|
|
484
|
+
return isScreenExport(x) || isLayerFile(x) || isIrNode(x);
|
|
485
|
+
}
|
|
486
|
+
isScreenDoc.expected = "a screen export: {nodes:[\u2026]} whose every node has a string id and type, a layer file {tree: node}, or a bare node {id, type, \u2026}";
|
|
487
|
+
function screenRoots(doc) {
|
|
488
|
+
if (isScreenExport(doc)) return doc.nodes;
|
|
489
|
+
if (isLayerFile(doc)) return [doc.tree];
|
|
490
|
+
if (isIrNode(doc)) return [doc];
|
|
491
|
+
return [];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// design-to-code/cli-args.ts
|
|
495
|
+
import path from "node:path";
|
|
496
|
+
import { fileURLToPath } from "node:url";
|
|
497
|
+
import { parseArgs } from "node:util";
|
|
498
|
+
var SELF = fileURLToPath(import.meta.url);
|
|
499
|
+
var shellQuote = (p) => /["$`\\!]/.test(p) ? `'${p.replaceAll("'", `'\\''`)}'` : `"${p}"`;
|
|
500
|
+
var SCRIPT_RUNNER = "bun --no-install --no-env-file";
|
|
501
|
+
var scriptCmd = (name) => `${SCRIPT_RUNNER} ${shellQuote(path.join(path.dirname(SELF), name + path.extname(SELF)))}`;
|
|
502
|
+
function joinNegativeValues(argv, options) {
|
|
503
|
+
const out = [];
|
|
504
|
+
for (let i = 0; i < argv.length; i++) {
|
|
505
|
+
const tok = argv[i], next = argv[i + 1];
|
|
506
|
+
if (tok === void 0) continue;
|
|
507
|
+
if (tok === "--") {
|
|
508
|
+
out.push(...argv.slice(i));
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
const name = tok.startsWith("--") ? tok.slice(2) : void 0;
|
|
512
|
+
if (name !== void 0 && next !== void 0 && options[name]?.type === "string" && /^-\d/.test(next)) {
|
|
513
|
+
out.push(`${tok}=${next}`);
|
|
514
|
+
i++;
|
|
515
|
+
} else out.push(tok);
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
519
|
+
function badShortWords(args, options) {
|
|
520
|
+
const shorts = /* @__PURE__ */ new Map();
|
|
521
|
+
for (const o of Object.values(options)) if (o.short) shorts.set(o.short, o.type);
|
|
522
|
+
const bad = [];
|
|
523
|
+
let takesNext = false;
|
|
524
|
+
for (const a of args) {
|
|
525
|
+
if (a === "--") break;
|
|
526
|
+
if (takesNext) {
|
|
527
|
+
takesNext = false;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (a.startsWith("--")) {
|
|
531
|
+
takesNext = !a.includes("=") && options[a.slice(2)]?.type === "string";
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (!/^-[^-]/.test(a)) continue;
|
|
535
|
+
let ok = true;
|
|
536
|
+
for (const [i, c] of [...a.slice(1)].entries()) {
|
|
537
|
+
const type = shorts.get(c);
|
|
538
|
+
if (type === void 0) {
|
|
539
|
+
ok = false;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
if (type === "string") {
|
|
543
|
+
takesNext = i === a.length - 2;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!ok) bad.push(a);
|
|
548
|
+
}
|
|
549
|
+
return bad;
|
|
550
|
+
}
|
|
551
|
+
function cliParse(tool, argv, options, usage, exitCode, parse) {
|
|
552
|
+
const args = joinNegativeValues(argv, options);
|
|
553
|
+
const unknownFlags = () => {
|
|
554
|
+
const { tokens } = parseArgs({ args, options, strict: false, allowPositionals: true, tokens: true });
|
|
555
|
+
const long = tokens.flatMap((t) => t.kind === "option" && t.rawName.startsWith("--") && args[t.index]?.startsWith("--") && !(t.name in options) ? [{ name: t.rawName, at: t.index }] : []);
|
|
556
|
+
const short = badShortWords(args, options);
|
|
557
|
+
const hint = short.some((w) => w.length > 2) ? ' (a value starting with "-": put -- before it, or use --flag=value)' : "";
|
|
558
|
+
const named = [...long, ...short.map((w) => ({ name: w, at: args.indexOf(w) }))].sort((a, b) => a.at - b.at).map((n) => n.name);
|
|
559
|
+
return `${tool}: unknown flag ${[...new Set(named)].join(", ")}${hint}
|
|
560
|
+
${usage}`;
|
|
561
|
+
};
|
|
562
|
+
let failure;
|
|
563
|
+
try {
|
|
564
|
+
const parsed = parse(args);
|
|
565
|
+
if (!badShortWords(args, options).length) return parsed;
|
|
566
|
+
failure = unknownFlags();
|
|
567
|
+
} catch (e) {
|
|
568
|
+
const code = errCode(e);
|
|
569
|
+
if (code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
570
|
+
failure = unknownFlags();
|
|
571
|
+
} else if (code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE") {
|
|
572
|
+
const msg = e instanceof Error ? e.message : "";
|
|
573
|
+
const names = /Option '([^']*)'/.exec(msg)?.[1]?.match(/--?[\w-]+/g) ?? [];
|
|
574
|
+
const typed = names.find((n) => args.some((a) => a === n || a.startsWith(n + "="))) ?? names.at(-1);
|
|
575
|
+
failure = `${tool}: ${typed ?? "an option"} ${/does not take an argument/.test(msg) ? "takes no value" : "needs a value"}
|
|
576
|
+
${usage}`;
|
|
577
|
+
} else {
|
|
578
|
+
failure = `${tool}: ${errMsg(e)}
|
|
579
|
+
${usage}`;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
console.error(failure);
|
|
583
|
+
exitCli(exitCode);
|
|
584
|
+
}
|
|
585
|
+
function cliArity(tool, positionals, min, max, usage) {
|
|
586
|
+
if (positionals.length < min) {
|
|
587
|
+
console.error(usage);
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
if (positionals.length > max) {
|
|
591
|
+
console.error(`${tool}: unexpected argument ${positionals.slice(max).join(", ")}
|
|
592
|
+
${usage}`);
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// design-to-code/color.ts
|
|
599
|
+
var HEX = /^#?([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
600
|
+
function normHex(v) {
|
|
601
|
+
if (typeof v !== "string") return null;
|
|
602
|
+
const g = HEX.exec(v.trim())?.[1];
|
|
603
|
+
if (g === void 0) return null;
|
|
604
|
+
const h = g.toLowerCase();
|
|
605
|
+
return "#" + (h.length <= 4 ? h.split("").map((c) => c + c).join("") : h);
|
|
606
|
+
}
|
|
607
|
+
function colorKey(v) {
|
|
608
|
+
const h = normHex(v);
|
|
609
|
+
return h === null ? null : h.length === 7 ? h + "ff" : h;
|
|
610
|
+
}
|
|
611
|
+
function parseHex(v) {
|
|
612
|
+
const k = colorKey(v);
|
|
613
|
+
if (k === null) return null;
|
|
614
|
+
const n = (i) => parseInt(k.slice(i, i + 2), 16);
|
|
615
|
+
return { r: n(1), g: n(3), b: n(5), a: n(7) / 255 };
|
|
616
|
+
}
|
|
617
|
+
var NUM = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:e[+-]?\\d+)?";
|
|
618
|
+
var PCT = `${NUM}%`;
|
|
619
|
+
var CH = `${NUM}%?`;
|
|
620
|
+
var CH_OR_NONE = `(?:${CH}|none)`;
|
|
621
|
+
var legacy = (ch) => `\\(\\s*(${ch})\\s*,\\s*(${ch})\\s*,\\s*(${ch})\\s*(?:,\\s*(${CH})\\s*)?\\)`;
|
|
622
|
+
var RGB_LEGACY = new RegExp(`^rgba?(?:${legacy(PCT)}|${legacy(NUM)})$`);
|
|
623
|
+
var RGB_MODERN = new RegExp(`^rgba?\\(\\s*(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s*(?:/\\s*(${CH_OR_NONE})\\s*)?\\)$`);
|
|
624
|
+
var OK = new RegExp(`^(oklab|oklch)\\(\\s*(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s*(?:/\\s*(${CH_OR_NONE})\\s*)?\\)$`);
|
|
625
|
+
var SRGB = new RegExp(`^color\\(\\s*srgb\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s+(${CH_OR_NONE})\\s*(?:/\\s*(${CH_OR_NONE})\\s*)?\\)$`);
|
|
626
|
+
function compositeOver(fg, bg) {
|
|
627
|
+
return {
|
|
628
|
+
r: fg.r * fg.a + bg.r * (1 - fg.a),
|
|
629
|
+
g: fg.g * fg.a + bg.g * (1 - fg.a),
|
|
630
|
+
b: fg.b * fg.a + bg.b * (1 - fg.a),
|
|
631
|
+
a: 1
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// design-to-code/visible-tree.ts
|
|
636
|
+
var hiddenSelf = (node) => !!(node && typeof node === "object" && "hidden" in node && node.hidden);
|
|
637
|
+
var isHidden = (node, ancestorHidden) => !!ancestorHidden || hiddenSelf(node);
|
|
638
|
+
var WHITE = { r: 255, g: 255, b: 255, a: 1 };
|
|
639
|
+
var NOTHING = { color: null, complex: false, token: null };
|
|
640
|
+
var paintToken = (p) => {
|
|
641
|
+
const t = p.tokens && p.tokens.color;
|
|
642
|
+
const s = Array.isArray(t) ? t[0] : t;
|
|
643
|
+
return typeof s === "string" && s ? s : null;
|
|
644
|
+
};
|
|
645
|
+
function paintOver(base, n) {
|
|
646
|
+
if (n.type === "TEXT") return base;
|
|
647
|
+
const fills = (Array.isArray(n.fills) ? n.fills : []).filter(Boolean);
|
|
648
|
+
const nodeTok = n.tokens && typeof n.tokens.fills === "string" && n.tokens.fills ? n.tokens.fills : null;
|
|
649
|
+
if (!fills.length) return nodeTok ? { ...base, token: nodeTok } : base;
|
|
650
|
+
let { color, complex, token } = base;
|
|
651
|
+
for (const f of fills) {
|
|
652
|
+
if (f.opacity === 0 || f.visible === false) continue;
|
|
653
|
+
if (f.type !== "solid") {
|
|
654
|
+
complex = true;
|
|
655
|
+
token = null;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const c = parseHex(f.color);
|
|
659
|
+
const tok = paintToken(f) ?? (fills.length === 1 ? nodeTok : null);
|
|
660
|
+
if (!c) {
|
|
661
|
+
token = tok;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (c.a <= 0) continue;
|
|
665
|
+
color = color ? compositeOver(c, color) : c.a >= 1 ? c : compositeOver(c, WHITE);
|
|
666
|
+
if (c.a >= 1) {
|
|
667
|
+
complex = false;
|
|
668
|
+
token = tok;
|
|
669
|
+
} else token = null;
|
|
670
|
+
}
|
|
671
|
+
return { color, complex, token };
|
|
672
|
+
}
|
|
673
|
+
var INK_TYPES = /* @__PURE__ */ new Set(["VECTOR", "BOOLEAN_OPERATION", "LINE", "FRAME", "INSTANCE", "GROUP", "COMPONENT"]);
|
|
674
|
+
function isIconInk(n) {
|
|
675
|
+
if (!(n.asset || n.geometry || n.assetSkipped) || !INK_TYPES.has(String(n.type))) return false;
|
|
676
|
+
return !(Array.isArray(n.fills) && n.fills.some((f) => f.type === "image" || f.type === "gradient"));
|
|
677
|
+
}
|
|
678
|
+
function overlaps(a, b) {
|
|
679
|
+
if (!a || !b || typeof a.x !== "number" || typeof a.y !== "number" || typeof b.x !== "number" || typeof b.y !== "number") return true;
|
|
680
|
+
return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
681
|
+
}
|
|
682
|
+
var paintsSurface = (n) => !hiddenSelf(n) && n.type !== "TEXT" && !isIconInk(n) && Array.isArray(n.fills) && n.fills.length > 0;
|
|
683
|
+
function beneathOf(parent, kids, i) {
|
|
684
|
+
const child = kids[i];
|
|
685
|
+
if (!child) return [];
|
|
686
|
+
const stacks = !parent.layout || parent.layout.mode === "absolute";
|
|
687
|
+
if (!stacks && !child.absolute) return [];
|
|
688
|
+
return kids.slice(0, i).filter((s) => s && typeof s === "object" && paintsSurface(s) && overlaps(s.box, child.box));
|
|
689
|
+
}
|
|
690
|
+
function memo(fn) {
|
|
691
|
+
let cell = null;
|
|
692
|
+
return () => (cell ??= { v: fn() }).v;
|
|
693
|
+
}
|
|
694
|
+
function walk(root, fn, prune, opts) {
|
|
695
|
+
const pathOf = opts && opts.pathOf || ((n, i) => n.name || n.type || String(i));
|
|
696
|
+
(function go(node, parentHidden, path5, ancestors, under, beneath) {
|
|
697
|
+
if (!node || typeof node !== "object") return;
|
|
698
|
+
const hidden = isHidden(node, parentHidden);
|
|
699
|
+
if (hidden && prune) return;
|
|
700
|
+
const backdrop = memo(() => beneath().reduce(paintOver, under()));
|
|
701
|
+
const parentFrame = ancestors[ancestors.length - 1];
|
|
702
|
+
const frame = { node, hidden, get backdrop() {
|
|
703
|
+
return backdrop();
|
|
704
|
+
}, get beneath() {
|
|
705
|
+
return beneath();
|
|
706
|
+
} };
|
|
707
|
+
fn(node, {
|
|
708
|
+
hidden,
|
|
709
|
+
parentHidden: !!parentHidden,
|
|
710
|
+
path: path5,
|
|
711
|
+
parent: parentFrame ? parentFrame.node : null,
|
|
712
|
+
depth: ancestors.length,
|
|
713
|
+
ancestors,
|
|
714
|
+
get backdrop() {
|
|
715
|
+
return backdrop();
|
|
716
|
+
},
|
|
717
|
+
get beneath() {
|
|
718
|
+
return beneath();
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
const kids = Array.isArray(node.children) ? node.children : [];
|
|
722
|
+
if (!kids.length) return;
|
|
723
|
+
const inside = memo(() => paintOver(backdrop(), node));
|
|
724
|
+
const chain = [...ancestors, frame];
|
|
725
|
+
for (const [i, kid] of kids.entries()) go(kid, hidden, (path5 ? path5 + " > " : "") + pathOf(kid, i), chain, inside, memo(() => beneathOf(node, kids, i)));
|
|
726
|
+
})(root, false, root ? pathOf(root, 0) : "", [], () => NOTHING, () => []);
|
|
727
|
+
}
|
|
728
|
+
function walkVisible(root, fn, opts) {
|
|
729
|
+
walk(root, fn, true, opts);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// design-to-code/component-match.ts
|
|
733
|
+
function visibleInstances(doc, label) {
|
|
734
|
+
const out = [];
|
|
735
|
+
const visit = (n) => {
|
|
736
|
+
if (n.type === "INSTANCE" && n.mainComponent) {
|
|
737
|
+
const mc = n.mainComponent;
|
|
738
|
+
out.push({
|
|
739
|
+
screen: label,
|
|
740
|
+
nodeId: n.id,
|
|
741
|
+
layer: n.name,
|
|
742
|
+
name: mc.setName || mc.name || n.name,
|
|
743
|
+
...ifDefined("key", mc.key),
|
|
744
|
+
...ifDefined("setKey", mc.setKey),
|
|
745
|
+
remote: mc.remote === true,
|
|
746
|
+
variant: parseVariant(n.component) || parseVariant(mc.variant),
|
|
747
|
+
props: n.props && typeof n.props === "object" ? n.props : {}
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
for (const r of screenRoots(doc)) walkVisible(r, visit);
|
|
752
|
+
return out;
|
|
753
|
+
}
|
|
754
|
+
function parseVariant(s) {
|
|
755
|
+
if (isJsonObject(s)) {
|
|
756
|
+
const out2 = Object.fromEntries(Object.entries(s).filter((e) => typeof e[1] === "string"));
|
|
757
|
+
return Object.keys(out2).length ? out2 : null;
|
|
758
|
+
}
|
|
759
|
+
if (!s || typeof s !== "string" || !s.includes("=")) return null;
|
|
760
|
+
const out = {};
|
|
761
|
+
for (const part of s.split(/,\s*(?=[^,=]+=)/)) {
|
|
762
|
+
const i = part.indexOf("=");
|
|
763
|
+
if (i < 0) continue;
|
|
764
|
+
out[part.slice(0, i).trim()] = part.slice(i + 1).trim();
|
|
765
|
+
}
|
|
766
|
+
return Object.keys(out).length ? out : null;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// design-to-code/map-validate-core.ts
|
|
770
|
+
var STATUSES = ["active", "deprecated", "needs-review"];
|
|
771
|
+
var KEYS = {
|
|
772
|
+
root: ["version", "figmaFileKey", "components"],
|
|
773
|
+
entry: ["figma", "code", "props", "variantOverrides", "childrenByLayer", "status", "note"],
|
|
774
|
+
figma: ["key", "id", "name", "unstable"],
|
|
775
|
+
code: ["module", "export", "targets"],
|
|
776
|
+
target: ["module", "export"],
|
|
777
|
+
vo: ["when", "code"],
|
|
778
|
+
voCode: ["module", "export"],
|
|
779
|
+
children: ["layerNamePattern", "slot"],
|
|
780
|
+
prop: Object.assign(nullProto(), {
|
|
781
|
+
enum: ["kind", "codeProp", "values", "default", "omitDefault"],
|
|
782
|
+
boolean: ["kind", "codeProp", "default", "omitDefault"],
|
|
783
|
+
string: ["kind", "codeProp"],
|
|
784
|
+
instance: ["kind", "codeProp", "slot"]
|
|
785
|
+
})
|
|
786
|
+
};
|
|
787
|
+
var PROP_KINDS = Object.keys(KEYS.prop);
|
|
788
|
+
var isStr = (v) => typeof v === "string";
|
|
789
|
+
var isBool = (v) => typeof v === "boolean";
|
|
790
|
+
var noExtra = (obj, allowed, at, err) => {
|
|
791
|
+
if (!Array.isArray(allowed)) return;
|
|
792
|
+
for (const k of Object.keys(obj)) if (!allowed.includes(k)) err(`${at}.${k}`, "unknown property (additionalProperties:false)");
|
|
793
|
+
};
|
|
794
|
+
var optStrings = (obj, keys, at, err) => {
|
|
795
|
+
for (const k of keys) if (obj[k] !== void 0 && !isStr(obj[k])) err(`${at}.${k}`, "must be a string");
|
|
796
|
+
};
|
|
797
|
+
function validateMap(map) {
|
|
798
|
+
const errors = [];
|
|
799
|
+
const err = (path5, message) => errors.push({ path: path5, message });
|
|
800
|
+
if (!isRecord(map)) return { ok: false, errors: [{ path: "", message: "map must be an object" }] };
|
|
801
|
+
noExtra(map, KEYS.root, "", err);
|
|
802
|
+
if (map.version !== 1) err("version", "must be 1");
|
|
803
|
+
if (map.figmaFileKey !== void 0 && !isStr(map.figmaFileKey)) err("figmaFileKey", "must be a string");
|
|
804
|
+
if (!isRecord(map.components)) {
|
|
805
|
+
err("components", "must be an object");
|
|
806
|
+
return { ok: false, errors };
|
|
807
|
+
}
|
|
808
|
+
for (const key of Object.keys(map.components)) {
|
|
809
|
+
const e = map.components[key];
|
|
810
|
+
const at = `components.${key}`;
|
|
811
|
+
if (!isRecord(e)) {
|
|
812
|
+
err(at, "entry must be an object");
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
noExtra(e, KEYS.entry, at, err);
|
|
816
|
+
if (!isRecord(e.figma)) err(`${at}.figma`, "required object");
|
|
817
|
+
else {
|
|
818
|
+
noExtra(e.figma, KEYS.figma, `${at}.figma`, err);
|
|
819
|
+
if (!isStr(e.figma.name)) err(`${at}.figma.name`, "required string");
|
|
820
|
+
optStrings(e.figma, ["key", "id"], `${at}.figma`, err);
|
|
821
|
+
if (e.figma.unstable !== void 0 && !isBool(e.figma.unstable)) err(`${at}.figma.unstable`, "must be a boolean");
|
|
822
|
+
}
|
|
823
|
+
if (!isRecord(e.code)) err(`${at}.code`, "required object");
|
|
824
|
+
else {
|
|
825
|
+
noExtra(e.code, KEYS.code, `${at}.code`, err);
|
|
826
|
+
if (!isStr(e.code.module)) err(`${at}.code.module`, "required string");
|
|
827
|
+
if (!isStr(e.code.export)) err(`${at}.code.export`, "required string");
|
|
828
|
+
if (e.code.targets !== void 0) {
|
|
829
|
+
if (!isRecord(e.code.targets)) err(`${at}.code.targets`, "must be an object");
|
|
830
|
+
else for (const t of Object.keys(e.code.targets)) {
|
|
831
|
+
const tv = e.code.targets[t], ta = `${at}.code.targets.${t}`;
|
|
832
|
+
if (!isRecord(tv)) err(ta, "must be an object");
|
|
833
|
+
else {
|
|
834
|
+
noExtra(tv, KEYS.target, ta, err);
|
|
835
|
+
optStrings(tv, ["module", "export"], ta, err);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (e.status !== void 0 && (!isStr(e.status) || !STATUSES.includes(e.status))) err(`${at}.status`, `must be one of ${STATUSES.join("|")}`);
|
|
841
|
+
optStrings(e, ["note"], at, err);
|
|
842
|
+
if (e.props !== void 0) {
|
|
843
|
+
if (!isRecord(e.props)) err(`${at}.props`, "must be an object");
|
|
844
|
+
else for (const pn of Object.keys(e.props)) validateProp(e.props[pn], `${at}.props.${pn}`, err);
|
|
845
|
+
}
|
|
846
|
+
if (e.variantOverrides !== void 0) {
|
|
847
|
+
if (!Array.isArray(e.variantOverrides)) err(`${at}.variantOverrides`, "must be an array");
|
|
848
|
+
else e.variantOverrides.forEach((vo, i) => {
|
|
849
|
+
const va = `${at}.variantOverrides[${i}]`;
|
|
850
|
+
if (!isRecord(vo)) {
|
|
851
|
+
err(va, "must be an object");
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
noExtra(vo, KEYS.vo, va, err);
|
|
855
|
+
if (!isRecord(vo.when)) err(`${va}.when`, "required object");
|
|
856
|
+
else for (const wk of Object.keys(vo.when)) if (!isStr(vo.when[wk])) err(`${va}.when.${wk}`, "value must be a string");
|
|
857
|
+
if (!isRecord(vo.code)) err(`${va}.code`, "required object");
|
|
858
|
+
else {
|
|
859
|
+
noExtra(vo.code, KEYS.voCode, `${va}.code`, err);
|
|
860
|
+
if (!isStr(vo.code.module)) err(`${va}.code.module`, "required string");
|
|
861
|
+
if (!isStr(vo.code.export)) err(`${va}.code.export`, "required string");
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (e.childrenByLayer !== void 0) {
|
|
866
|
+
if (!isRecord(e.childrenByLayer)) err(`${at}.childrenByLayer`, "must be an object");
|
|
867
|
+
else {
|
|
868
|
+
noExtra(e.childrenByLayer, KEYS.children, `${at}.childrenByLayer`, err);
|
|
869
|
+
optStrings(e.childrenByLayer, ["layerNamePattern", "slot"], `${at}.childrenByLayer`, err);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
return { ok: errors.length === 0, errors };
|
|
874
|
+
}
|
|
875
|
+
function validateProp(p, at, err) {
|
|
876
|
+
if (!isRecord(p)) {
|
|
877
|
+
err(at, "prop must be an object");
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const allowed = typeof p.kind === "string" ? KEYS.prop[p.kind] : void 0;
|
|
881
|
+
if (!Array.isArray(allowed)) {
|
|
882
|
+
err(`${at}.kind`, `must be one of ${PROP_KINDS.join("|")}`);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
noExtra(p, allowed, at, err);
|
|
886
|
+
if (p.kind !== "instance" && !isStr(p.codeProp)) err(`${at}.codeProp`, "required string");
|
|
887
|
+
if (p.kind === "instance" && p.codeProp !== void 0 && !isStr(p.codeProp)) err(`${at}.codeProp`, "must be a string");
|
|
888
|
+
if (p.kind === "instance" && p.slot !== void 0 && !isStr(p.slot)) err(`${at}.slot`, "must be a string");
|
|
889
|
+
if (p.kind === "enum") {
|
|
890
|
+
if (p.values !== void 0 && !isRecord(p.values)) err(`${at}.values`, "must be an object (VARIANT option -> code value)");
|
|
891
|
+
else if (isRecord(p.values)) for (const k of Object.keys(p.values)) {
|
|
892
|
+
const t = typeof p.values[k];
|
|
893
|
+
if (p.values[k] !== null && !["string", "number", "boolean"].includes(t)) err(`${at}.values.${k}`, "value must be string|number|boolean|null");
|
|
894
|
+
}
|
|
895
|
+
if (p.default !== void 0 && !["string", "number", "boolean"].includes(typeof p.default)) err(`${at}.default`, "must be string|number|boolean");
|
|
896
|
+
}
|
|
897
|
+
if (p.kind === "boolean" && p.default !== void 0 && !isBool(p.default)) err(`${at}.default`, "must be a boolean");
|
|
898
|
+
if ((p.kind === "enum" || p.kind === "boolean") && p.omitDefault !== void 0 && !isBool(p.omitDefault)) err(`${at}.omitDefault`, "must be a boolean");
|
|
899
|
+
}
|
|
900
|
+
function isCodeConnectMap(x) {
|
|
901
|
+
return validateMap(x).ok;
|
|
902
|
+
}
|
|
903
|
+
isCodeConnectMap.expected = "a valid component map (the map-validate script lists what is wrong with it)";
|
|
904
|
+
|
|
905
|
+
// design-to-code/design-system-dir.ts
|
|
906
|
+
import fs2 from "node:fs";
|
|
907
|
+
import path3 from "node:path";
|
|
908
|
+
|
|
909
|
+
// bridge/src/project-layout.ts
|
|
910
|
+
import path2 from "node:path";
|
|
911
|
+
var DESIGN_DIR = "design";
|
|
912
|
+
var EXPORT_SUBDIR = "export";
|
|
913
|
+
var EXPORT_DIR = path2.join(DESIGN_DIR, EXPORT_SUBDIR);
|
|
914
|
+
var TARGET_FILE = path2.join(DESIGN_DIR, "target.json");
|
|
915
|
+
var MAP_FILE = path2.join(DESIGN_DIR, "codeconnect.local.json");
|
|
916
|
+
var PLAN_DIR = path2.join(DESIGN_DIR, "plan");
|
|
917
|
+
var AUDIT_DIR = path2.join(DESIGN_DIR, "audit");
|
|
918
|
+
var VERIFY_DIR = path2.join(DESIGN_DIR, "verify");
|
|
919
|
+
var SNAPSHOT_DIR = path2.join(DESIGN_DIR, ".sync");
|
|
920
|
+
var TAILWIND_SOURCE_NOT_NOTE = `Tailwind v4 scans every file git does not ignore, ${DESIGN_DIR}/ included, so class names quoted in ${DESIGN_DIR}/ notes, audits and plans end up in your CSS. Next to \`@import "tailwindcss";\` in your CSS entry, add \`@source not "<path from that CSS file to ${DESIGN_DIR}/>";\` (e.g. \`@source not "../${DESIGN_DIR}";\` for src/app.css) \u2014 Tailwind v4.1+`;
|
|
921
|
+
var VITE_WATCH_IGNORED_NOTE = `With Tailwind v4's automatic source detection, rewriting an existing text file under ${DESIGN_DIR}/ (a re-export, a verify report) makes Vite fully reload the open page. Either add \`server: { watch: { ignored: ['**/${DESIGN_DIR}/**'] } }\` in vite.config (merge it with any existing \`server.watch\` options), or the Tailwind \`@source not\` above \u2014 both stop it`;
|
|
922
|
+
var slashed = (p) => p.split(path2.sep).join("/");
|
|
923
|
+
var VERIFY_GITIGNORE_NOTE = `${slashed(VERIFY_DIR)}/ is regenerated on every verify run (measurements, screenshots, reports) \u2014 consider adding \`${slashed(VERIFY_DIR)}/\` to .gitignore; decisions live in ${slashed(PLAN_DIR)}/ and are not affected`;
|
|
924
|
+
|
|
925
|
+
// bridge/src/design-system-layout.ts
|
|
926
|
+
var COMPONENTS_DIR = "components";
|
|
927
|
+
var TOKENS = "tokens.json";
|
|
928
|
+
var COMPONENTS_LOCAL = "components.local.json";
|
|
929
|
+
var COMPONENTS_LIBRARY = "components.library.json";
|
|
930
|
+
var HYGIENE = "hygiene.json";
|
|
931
|
+
var MANIFEST = "design-system.json";
|
|
932
|
+
var STYLE_KIND_ROWS = {
|
|
933
|
+
paint: { kind: "paint", file: "styles.paint.json", key: "stylesPaint" },
|
|
934
|
+
text: { kind: "text", file: "styles.text.json", key: "stylesText" },
|
|
935
|
+
effect: { kind: "effect", file: "styles.effect.json", key: "stylesEffect" },
|
|
936
|
+
grid: { kind: "grid", file: "styles.grid.json", key: "stylesGrid" },
|
|
937
|
+
animation: { kind: "animation", file: "styles.animation.json", key: "stylesAnimation" }
|
|
938
|
+
// local custom animation styles
|
|
939
|
+
};
|
|
940
|
+
var STYLE_KINDS = Object.values(STYLE_KIND_ROWS);
|
|
941
|
+
var DESIGN_SYSTEM_FILES = { TOKENS, COMPONENTS_LOCAL, COMPONENTS_LIBRARY, COMPONENTS_DIR, HYGIENE, MANIFEST };
|
|
942
|
+
var DESIGN_SYSTEM_FILE_NAMES = [TOKENS, ...STYLE_KINDS.map((row) => row.file), COMPONENTS_LOCAL, COMPONENTS_LIBRARY, HYGIENE, MANIFEST];
|
|
943
|
+
|
|
944
|
+
// bridge/src/library-layout.ts
|
|
945
|
+
var ROOT = "libraries";
|
|
946
|
+
var INDEX = "index.json";
|
|
947
|
+
var { TOKENS: TOKENS2, HYGIENE: HYGIENE2 } = DESIGN_SYSTEM_FILES;
|
|
948
|
+
var COMPONENTS = "components.json";
|
|
949
|
+
var LIBRARY_FILE_NAMES = [TOKENS2, ...STYLE_KINDS.map((row) => row.file), COMPONENTS, HYGIENE2, INDEX];
|
|
950
|
+
|
|
951
|
+
// design-to-code/design-system-dir.ts
|
|
952
|
+
var { TOKENS: TOKENS3, COMPONENTS_LOCAL: COMPONENTS_LOCAL2, COMPONENTS_LIBRARY: COMPONENTS_LIBRARY2 } = DESIGN_SYSTEM_FILES;
|
|
953
|
+
var STYLES_TEXT = STYLE_KIND_ROWS.text.file;
|
|
954
|
+
function exportRootOf(screenFile, dsDir) {
|
|
955
|
+
const roots = [];
|
|
956
|
+
if (screenFile) {
|
|
957
|
+
const pages = path3.dirname(path3.dirname(screenFile));
|
|
958
|
+
if (path3.basename(pages) === "pages") roots.push(path3.dirname(pages));
|
|
959
|
+
}
|
|
960
|
+
if (dsDir) {
|
|
961
|
+
const parent = path3.dirname(path3.normalize(dsDir));
|
|
962
|
+
roots.push(path3.basename(parent) === ROOT ? path3.dirname(parent) : parent);
|
|
963
|
+
}
|
|
964
|
+
return roots.find((r) => fs2.existsSync(path3.join(r, ROOT, INDEX))) ?? null;
|
|
965
|
+
}
|
|
966
|
+
function findLibraryExports(screenFile, dsDir) {
|
|
967
|
+
const root = exportRootOf(screenFile, dsDir);
|
|
968
|
+
if (!root) return [];
|
|
969
|
+
const index = readJsonOrNull(path3.join(root, ROOT, INDEX), isLibrariesIndex);
|
|
970
|
+
if (!index) return [];
|
|
971
|
+
return index.libraries.filter((r) => r.dir && r.dir !== "." && r.dir !== ".." && !/[\\/]/.test(r.dir)).map((r) => {
|
|
972
|
+
const rel = path3.join(root, ROOT, r.dir).split(path3.sep).join("/");
|
|
973
|
+
return { rel, name: r.libraryName || r.dir, collectionKeys: r.collectionKeys || [], components: readJsonOrNull(path3.join(rel, COMPONENTS), isComponentsCatalog) };
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
function discoverCatalogs(namedFile, screenFile) {
|
|
977
|
+
const dir = path3.dirname(namedFile);
|
|
978
|
+
const out = [];
|
|
979
|
+
const seen = /* @__PURE__ */ new Set([path3.resolve(namedFile)]);
|
|
980
|
+
const add = (file, role, catalog) => {
|
|
981
|
+
if (seen.has(path3.resolve(file)) || !fs2.existsSync(file)) return;
|
|
982
|
+
seen.add(path3.resolve(file));
|
|
983
|
+
out.push({ file, role, catalog });
|
|
984
|
+
};
|
|
985
|
+
for (const lib of findLibraryExports(screenFile, dir)) add(path3.join(lib.rel, COMPONENTS), "library", lib.components);
|
|
986
|
+
const sample = path3.join(dir, COMPONENTS_LIBRARY2);
|
|
987
|
+
add(sample, "library-sample", readJsonOrNull(sample, isComponentsCatalog));
|
|
988
|
+
return out;
|
|
989
|
+
}
|
|
990
|
+
function loadCatalogSet(namedFile, named, extraFiles, screenFile) {
|
|
991
|
+
const sources = [{ file: namedFile, catalog: named, role: "named" }];
|
|
992
|
+
const skipped = [];
|
|
993
|
+
const seen = /* @__PURE__ */ new Set([path3.resolve(namedFile)]);
|
|
994
|
+
for (const f of extraFiles) {
|
|
995
|
+
if (seen.has(path3.resolve(f))) continue;
|
|
996
|
+
seen.add(path3.resolve(f));
|
|
997
|
+
const r = loadSplitFile(f, "component catalog (--catalog)", isComponentsCatalog, "components", "design-system/components.library.json");
|
|
998
|
+
if ("error" in r) return r;
|
|
999
|
+
sources.push({ file: f, role: "extra", catalog: r.doc });
|
|
1000
|
+
}
|
|
1001
|
+
for (const d of discoverCatalogs(namedFile, screenFile)) {
|
|
1002
|
+
if (seen.has(path3.resolve(d.file))) continue;
|
|
1003
|
+
seen.add(path3.resolve(d.file));
|
|
1004
|
+
if (d.catalog) sources.push({ file: d.file, role: d.role, catalog: d.catalog });
|
|
1005
|
+
else skipped.push(d.file);
|
|
1006
|
+
}
|
|
1007
|
+
return { sources, skipped };
|
|
1008
|
+
}
|
|
1009
|
+
function readCatalogSet(namedFile, named, extraFiles, screenFile, onSkip) {
|
|
1010
|
+
const r = loadCatalogSet(namedFile, named, extraFiles, screenFile);
|
|
1011
|
+
if ("error" in r) {
|
|
1012
|
+
console.error(`error ${r.error}`);
|
|
1013
|
+
exitCli(2);
|
|
1014
|
+
}
|
|
1015
|
+
if (onSkip) for (const f of r.skipped) onSkip(f);
|
|
1016
|
+
return r.sources;
|
|
1017
|
+
}
|
|
1018
|
+
function unionCatalog(sources) {
|
|
1019
|
+
const first = sources[0];
|
|
1020
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1021
|
+
const components = [];
|
|
1022
|
+
for (const s of sources) for (const c of s.catalog.components) {
|
|
1023
|
+
if (c.key) {
|
|
1024
|
+
if (keys.has(c.key)) continue;
|
|
1025
|
+
keys.add(c.key);
|
|
1026
|
+
}
|
|
1027
|
+
components.push(c);
|
|
1028
|
+
}
|
|
1029
|
+
return { ...first ? first.catalog : {}, components };
|
|
1030
|
+
}
|
|
1031
|
+
var catalogSetLine = (sources) => `catalogs read (${sources.length}): ` + sources.map((s) => `${s.file} [${s.role}, ${s.catalog.components.length} component(s)]`).join(", ");
|
|
1032
|
+
|
|
1033
|
+
// bridge/src/json-file.ts
|
|
1034
|
+
import fs5 from "node:fs";
|
|
1035
|
+
|
|
1036
|
+
// bridge/src/atomic-write.ts
|
|
1037
|
+
import crypto from "node:crypto";
|
|
1038
|
+
import fs3 from "node:fs";
|
|
1039
|
+
import path4 from "node:path";
|
|
1040
|
+
var tmpSuffix = () => `.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
1041
|
+
var RENAME_RETRY_MS = 2e3;
|
|
1042
|
+
var RENAME_TRANSIENT = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
1043
|
+
var sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
1044
|
+
function renameOver(from, to) {
|
|
1045
|
+
const t0 = Date.now();
|
|
1046
|
+
for (let delay = 5; ; delay = Math.min(delay * 2, 100)) {
|
|
1047
|
+
try {
|
|
1048
|
+
fs3.renameSync(from, to);
|
|
1049
|
+
return;
|
|
1050
|
+
} catch (e) {
|
|
1051
|
+
const code = errCode(e);
|
|
1052
|
+
if (process.platform !== "win32" || code === void 0 || !RENAME_TRANSIENT.has(code) || Date.now() - t0 >= RENAME_RETRY_MS) throw e;
|
|
1053
|
+
let dir = false;
|
|
1054
|
+
try {
|
|
1055
|
+
dir = fs3.statSync(to).isDirectory();
|
|
1056
|
+
} catch {
|
|
1057
|
+
}
|
|
1058
|
+
if (dir) throw e;
|
|
1059
|
+
Atomics.wait(sleeper, 0, 0, delay);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function syncDir(dir) {
|
|
1064
|
+
if (process.platform === "win32") return;
|
|
1065
|
+
try {
|
|
1066
|
+
const fd = fs3.openSync(dir, "r");
|
|
1067
|
+
try {
|
|
1068
|
+
fs3.fsyncSync(fd);
|
|
1069
|
+
} finally {
|
|
1070
|
+
fs3.closeSync(fd);
|
|
1071
|
+
}
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function writeFileAtomic(file, data, opts = {}) {
|
|
1076
|
+
const dir = path4.dirname(path4.resolve(file));
|
|
1077
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1078
|
+
const tmp = file + tmpSuffix();
|
|
1079
|
+
try {
|
|
1080
|
+
if (opts.sync) {
|
|
1081
|
+
const fd = fs3.openSync(tmp, "wx", opts.mode);
|
|
1082
|
+
try {
|
|
1083
|
+
fs3.writeFileSync(fd, data);
|
|
1084
|
+
fs3.fsyncSync(fd);
|
|
1085
|
+
} finally {
|
|
1086
|
+
fs3.closeSync(fd);
|
|
1087
|
+
}
|
|
1088
|
+
} else {
|
|
1089
|
+
fs3.writeFileSync(tmp, data, { flag: "wx", ...opts.mode === void 0 ? {} : { mode: opts.mode } });
|
|
1090
|
+
}
|
|
1091
|
+
if (opts.mode !== void 0) fs3.chmodSync(tmp, opts.mode);
|
|
1092
|
+
renameOver(tmp, file);
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
try {
|
|
1095
|
+
fs3.rmSync(tmp, { force: true });
|
|
1096
|
+
} catch {
|
|
1097
|
+
}
|
|
1098
|
+
throw e;
|
|
1099
|
+
}
|
|
1100
|
+
if (opts.sync) syncDir(dir);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// bridge/src/file-lock.ts
|
|
1104
|
+
import fs4 from "node:fs";
|
|
1105
|
+
var LIVE_STALE_MS = 2 * 6e4;
|
|
1106
|
+
var PID_NS = (() => {
|
|
1107
|
+
if (process.platform !== "linux") return "";
|
|
1108
|
+
try {
|
|
1109
|
+
return fs4.readlinkSync("/proc/self/ns/pid");
|
|
1110
|
+
} catch {
|
|
1111
|
+
return "";
|
|
1112
|
+
}
|
|
1113
|
+
})();
|
|
1114
|
+
var sleeper2 = new Int32Array(new SharedArrayBuffer(4));
|
|
1115
|
+
|
|
1116
|
+
// bridge/src/json-file.ts
|
|
1117
|
+
var indentOf = (text) => /\n([ \t]+)\S/.exec(text)?.[1] ?? " ";
|
|
1118
|
+
function formatJsonLike(value, raw) {
|
|
1119
|
+
if (raw === null) return JSON.stringify(value, null, 2) + "\n";
|
|
1120
|
+
const bom = raw.charCodeAt(0) === 65279 ? "\uFEFF" : "";
|
|
1121
|
+
const text = raw.slice(bom.length);
|
|
1122
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
1123
|
+
return bom + JSON.stringify(value, null, indentOf(text)).split("\n").join(eol) + (/\n$/.test(text) ? eol : "");
|
|
1124
|
+
}
|
|
1125
|
+
function writeJsonLike(file, value, raw) {
|
|
1126
|
+
let was = raw ?? null;
|
|
1127
|
+
if (raw === void 0) try {
|
|
1128
|
+
was = fs5.readFileSync(file, "utf8");
|
|
1129
|
+
} catch {
|
|
1130
|
+
was = null;
|
|
1131
|
+
}
|
|
1132
|
+
const out = formatJsonLike(value, was);
|
|
1133
|
+
if (out === was) return false;
|
|
1134
|
+
writeFileAtomic(file, out);
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// design-to-code/map-bootstrap.ts
|
|
1139
|
+
var clone = (o) => structuredClone(o);
|
|
1140
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1141
|
+
function safeAssign(target, source1, source2) {
|
|
1142
|
+
for (const src of [source1, source2]) {
|
|
1143
|
+
if (src === void 0 || src === null) continue;
|
|
1144
|
+
for (const k of Object.keys(src)) {
|
|
1145
|
+
if (UNSAFE_KEYS.has(k)) continue;
|
|
1146
|
+
target[k] = src[k];
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return target;
|
|
1150
|
+
}
|
|
1151
|
+
var words = (name) => String(name || "").replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
1152
|
+
function pascal(name) {
|
|
1153
|
+
const p = words(name).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
1154
|
+
return /^[A-Za-z]/.test(p) ? p : "Component";
|
|
1155
|
+
}
|
|
1156
|
+
function camel(name) {
|
|
1157
|
+
const base = String(name || "").split("/").pop() || "";
|
|
1158
|
+
const p = words(base).map((w, i) => (i === 0 ? w.charAt(0).toLowerCase() : w.charAt(0).toUpperCase()) + w.slice(1)).join("");
|
|
1159
|
+
return /^[A-Za-z]/.test(p) ? p : "prop";
|
|
1160
|
+
}
|
|
1161
|
+
function propEntry(name, def) {
|
|
1162
|
+
const kind = TYPE_TO_KIND[def.type];
|
|
1163
|
+
if (kind === "enum") {
|
|
1164
|
+
const values = {};
|
|
1165
|
+
for (const opt of def.options || []) values[opt] = opt;
|
|
1166
|
+
const p = { kind, codeProp: camel(name), values };
|
|
1167
|
+
if (typeof def.default === "string" || typeof def.default === "number" || typeof def.default === "boolean") {
|
|
1168
|
+
p.default = def.default;
|
|
1169
|
+
p.omitDefault = true;
|
|
1170
|
+
}
|
|
1171
|
+
return p;
|
|
1172
|
+
}
|
|
1173
|
+
if (kind === "boolean") {
|
|
1174
|
+
const p = { kind, codeProp: camel(name) };
|
|
1175
|
+
if (def.default !== void 0) {
|
|
1176
|
+
p.default = !!def.default;
|
|
1177
|
+
p.omitDefault = true;
|
|
1178
|
+
}
|
|
1179
|
+
return p;
|
|
1180
|
+
}
|
|
1181
|
+
if (kind === "string") return { kind, codeProp: /label|text|title/i.test(name) ? "children" : camel(name) };
|
|
1182
|
+
if (kind === "instance") return { kind, slot: camel(name) };
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
function freshEntry(c) {
|
|
1186
|
+
const figma = { name: c.name || c.key || c.id || "Unnamed" };
|
|
1187
|
+
if (c.key) figma.key = c.key;
|
|
1188
|
+
else figma.unstable = true;
|
|
1189
|
+
if (c.id) figma.id = c.id;
|
|
1190
|
+
const entry = { figma, code: { module: "TODO: import path", export: pascal(c.name || "Component") }, status: "needs-review" };
|
|
1191
|
+
const props = nullProto();
|
|
1192
|
+
const defs = c.props || {};
|
|
1193
|
+
for (const [pn, def] of Object.entries(defs)) {
|
|
1194
|
+
const pe = propEntry(pn, def);
|
|
1195
|
+
if (pe) props[pn] = pe;
|
|
1196
|
+
}
|
|
1197
|
+
if (Object.keys(props).length) entry.props = props;
|
|
1198
|
+
return entry;
|
|
1199
|
+
}
|
|
1200
|
+
function bootstrap(catalog, existing) {
|
|
1201
|
+
const out = { version: 1, components: nullProto() };
|
|
1202
|
+
if (existing && existing.figmaFileKey) out.figmaFileKey = existing.figmaFileKey;
|
|
1203
|
+
const prev = existing && existing.components || {};
|
|
1204
|
+
const prevByIdent = /* @__PURE__ */ new Map();
|
|
1205
|
+
for (const [pk, pe] of Object.entries(prev)) {
|
|
1206
|
+
if (!prevByIdent.has(pk)) prevByIdent.set(pk, pk);
|
|
1207
|
+
const pf = pe.figma || {};
|
|
1208
|
+
if (pf.key && !prevByIdent.has(pf.key)) prevByIdent.set(pf.key, pk);
|
|
1209
|
+
if (pf.id && !prevByIdent.has(pf.id)) prevByIdent.set(pf.id, pk);
|
|
1210
|
+
}
|
|
1211
|
+
const usedPrev = /* @__PURE__ */ new Set();
|
|
1212
|
+
for (const c of catalog && catalog.components || []) {
|
|
1213
|
+
if (c.type !== "COMPONENT" && c.type !== "COMPONENT_SET") continue;
|
|
1214
|
+
const id = c.key || c.id;
|
|
1215
|
+
if (!id) continue;
|
|
1216
|
+
const keyClash = (x) => {
|
|
1217
|
+
if (!c.key || x === c.key) return false;
|
|
1218
|
+
const pk = prevByIdent.get(x);
|
|
1219
|
+
const pkKey = pk ? (prev[pk]?.figma || {}).key : void 0;
|
|
1220
|
+
return !!pkKey && pkKey !== c.key;
|
|
1221
|
+
};
|
|
1222
|
+
const matchKey = [c.key, c.id, id].find((x) => x && prevByIdent.has(x) && !keyClash(x));
|
|
1223
|
+
const prevKey = matchKey ? prevByIdent.get(matchKey) : null;
|
|
1224
|
+
const prevEntry = prevKey ? prev[prevKey] : null;
|
|
1225
|
+
if (prevKey && prevEntry) {
|
|
1226
|
+
usedPrev.add(prevKey);
|
|
1227
|
+
const entry = clone(prevEntry);
|
|
1228
|
+
entry.figma = safeAssign({}, entry.figma, { name: c.name || entry.figma && entry.figma.name || "Unnamed" });
|
|
1229
|
+
if (c.id) entry.figma.id = c.id;
|
|
1230
|
+
if (c.key) entry.figma.key = c.key;
|
|
1231
|
+
else if (!entry.figma.key) entry.figma.unstable = true;
|
|
1232
|
+
const props = nullProto();
|
|
1233
|
+
if (entry.props) Object.assign(props, entry.props);
|
|
1234
|
+
entry.props = props;
|
|
1235
|
+
const defs = c.props || {};
|
|
1236
|
+
for (const [pn, cdef] of Object.entries(defs)) {
|
|
1237
|
+
const kind = TYPE_TO_KIND[cdef.type], ex = props[pn];
|
|
1238
|
+
if (!ex || kind && ex.kind && ex.kind !== kind) {
|
|
1239
|
+
const pe = propEntry(pn, cdef);
|
|
1240
|
+
if (pe) props[pn] = pe;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
if (!Object.keys(props).length) delete entry.props;
|
|
1244
|
+
out.components[prevKey !== c.key && prevKey !== c.id ? prevKey : id] = entry;
|
|
1245
|
+
} else {
|
|
1246
|
+
out.components[id] = freshEntry(c);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
for (const [pk, pe] of Object.entries(prev)) if (!usedPrev.has(pk) && !(pk in out.components)) out.components[pk] = clone(pe);
|
|
1250
|
+
return out;
|
|
1251
|
+
}
|
|
1252
|
+
function bootstrapFromProposals(proposals, catalog, existing, others) {
|
|
1253
|
+
const out = { version: 1, components: nullProto() };
|
|
1254
|
+
if (existing && existing.figmaFileKey) out.figmaFileKey = existing.figmaFileKey;
|
|
1255
|
+
const prev = existing && existing.components || {};
|
|
1256
|
+
for (const [pk, pe] of Object.entries(prev)) out.components[pk] = clone(pe);
|
|
1257
|
+
const comps = catalog && catalog.components || [];
|
|
1258
|
+
const report = { confirmed: 0, added: 0, kept: 0, skipped: [] };
|
|
1259
|
+
for (const p of proposals || []) {
|
|
1260
|
+
if (!p || p.confirmed !== true) continue;
|
|
1261
|
+
report.confirmed++;
|
|
1262
|
+
const want = p.catalog || {};
|
|
1263
|
+
const c = want.key ? comps.find((x) => x.key === want.key) || (others && others.components || []).find((x) => x.key === want.key) : want.id ? comps.find((x) => x.id === want.id) : void 0;
|
|
1264
|
+
const mapKey = (p.instanceKeys || [])[0];
|
|
1265
|
+
if (!c) {
|
|
1266
|
+
report.skipped.push(`'${p.name}': catalog component ${want.id || want.key || "?"} is not in this catalog`);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (!mapKey) {
|
|
1270
|
+
report.skipped.push(`'${p.name}': the proposal carries no instance key to file it under`);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (mapKey in out.components) {
|
|
1274
|
+
report.kept++;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
const entry = freshEntry(c);
|
|
1278
|
+
out.components[mapKey] = entry;
|
|
1279
|
+
report.added++;
|
|
1280
|
+
for (const extra of (p.instanceKeys || []).slice(1)) report.skipped.push(`'${p.name}': also used under instance key ${extra} \u2014 filed once, under ${mapKey}`);
|
|
1281
|
+
}
|
|
1282
|
+
return { map: out, report };
|
|
1283
|
+
}
|
|
1284
|
+
function proposalsIn(doc) {
|
|
1285
|
+
if (isProposalList(doc)) return doc;
|
|
1286
|
+
if (isJsonObject(doc) && isProposalList(doc.componentProposals)) return doc.componentProposals;
|
|
1287
|
+
if (isJsonObject(doc) && isJsonObject(doc.crossFile) && isProposalList(doc.crossFile.componentProposals)) return doc.crossFile.componentProposals;
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
function main(argv) {
|
|
1291
|
+
const usage = `usage: ${scriptCmd("map-bootstrap")} <design-system/components.local.json> [existing-map.json] [--out <file>] [--from-proposals <cross-check report.json>] [--screen <screen.json>] [--catalog <components.json>]...`;
|
|
1292
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1293
|
+
console.log(usage);
|
|
1294
|
+
return 0;
|
|
1295
|
+
}
|
|
1296
|
+
const OPTIONS = {
|
|
1297
|
+
out: { type: "string" },
|
|
1298
|
+
"from-proposals": { type: "string" },
|
|
1299
|
+
screen: { type: "string" },
|
|
1300
|
+
catalog: { type: "string", multiple: true },
|
|
1301
|
+
help: { type: "boolean", short: "h" }
|
|
1302
|
+
};
|
|
1303
|
+
const { values: flags, positionals } = cliParse("map-bootstrap", argv, OPTIONS, usage, 1, (args) => parseArgs2({ args, options: OPTIONS, allowPositionals: true }));
|
|
1304
|
+
if (flags.help) {
|
|
1305
|
+
console.log(usage);
|
|
1306
|
+
return 0;
|
|
1307
|
+
}
|
|
1308
|
+
if (!cliArity("map-bootstrap", positionals, 1, 2, usage)) return 1;
|
|
1309
|
+
const emptyFlag = ["out", "from-proposals", "screen"].find((n) => flags[n] === "") ?? (flags.catalog?.includes("") ? "catalog" : void 0);
|
|
1310
|
+
if (emptyFlag) {
|
|
1311
|
+
console.error(`map-bootstrap: --${emptyFlag} needs a value
|
|
1312
|
+
${usage}`);
|
|
1313
|
+
return 1;
|
|
1314
|
+
}
|
|
1315
|
+
const outFile = flags.out ?? null, proposalsFile = flags["from-proposals"] ?? null, screenFile = flags.screen ?? null;
|
|
1316
|
+
const extraCatalogFiles = flags.catalog ?? [];
|
|
1317
|
+
const [catalogFile, existingArg] = positionals;
|
|
1318
|
+
if (!catalogFile) {
|
|
1319
|
+
console.error(usage);
|
|
1320
|
+
return 1;
|
|
1321
|
+
}
|
|
1322
|
+
const catalog = readSplitFile(
|
|
1323
|
+
catalogFile,
|
|
1324
|
+
"component catalog",
|
|
1325
|
+
isComponentsCatalog,
|
|
1326
|
+
"components",
|
|
1327
|
+
"design-system/components.local.json",
|
|
1328
|
+
NO_DESIGN_SYSTEM_HINT + "\n Or build without a component map: every instance then counts as new (build-screen, step 1)."
|
|
1329
|
+
);
|
|
1330
|
+
const sources = screenFile || proposalsFile ? readCatalogSet(
|
|
1331
|
+
catalogFile,
|
|
1332
|
+
catalog,
|
|
1333
|
+
extraCatalogFiles,
|
|
1334
|
+
screenFile ?? void 0,
|
|
1335
|
+
(f) => console.error(`map-bootstrap: warn ${f} is not a readable component catalog \u2014 skipped`)
|
|
1336
|
+
) : [{ file: catalogFile, catalog, role: "named" }];
|
|
1337
|
+
const union = unionCatalog(sources);
|
|
1338
|
+
const others = { components: unionCatalog(sources.slice(1)).components.filter((c) => !!c.key && !catalog.components.some((n) => n.key === c.key)).map(({ id: _foreignId, ...c }) => c) };
|
|
1339
|
+
if (screenFile || proposalsFile) console.error(`map-bootstrap: ${catalogSetLine(sources)}`);
|
|
1340
|
+
const existingFile = existingArg || outFile;
|
|
1341
|
+
const existingRaw = existingFile && fs6.existsSync(existingFile) ? readJsonFile(existingFile, "existing map") : null;
|
|
1342
|
+
let existing = null;
|
|
1343
|
+
if (existingRaw !== null) {
|
|
1344
|
+
const valid = validateMap(existingRaw);
|
|
1345
|
+
if (!valid.ok || !isCodeConnectMap(existingRaw)) {
|
|
1346
|
+
valid.errors.forEach((e) => console.error(`map-bootstrap: ${existingFile}: ${e.path || "(root)"}: ${e.message}`));
|
|
1347
|
+
console.error(`map-bootstrap: ${existingFile} is not a valid component map (${valid.errors.length} error(s)) \u2014 refusing to rewrite it. Fix it (\`${scriptCmd("map-validate")} ${existingFile}\`) or move it aside. Nothing was written.`);
|
|
1348
|
+
return 1;
|
|
1349
|
+
}
|
|
1350
|
+
existing = existingRaw;
|
|
1351
|
+
}
|
|
1352
|
+
if (proposalsFile) {
|
|
1353
|
+
const doc = readJsonFile(proposalsFile, "proposals report");
|
|
1354
|
+
const proposals = proposalsIn(doc);
|
|
1355
|
+
if (!proposals) {
|
|
1356
|
+
console.error(`map-bootstrap: ${proposalsFile} has no componentProposals \u2014 run \`${scriptCmd("cross-check")} <screen.json> --out <base>\` (or --json) and pass the JSON it wrote`);
|
|
1357
|
+
return 1;
|
|
1358
|
+
}
|
|
1359
|
+
const { map, report } = bootstrapFromProposals(proposals, catalog, existing, others);
|
|
1360
|
+
if (!report.confirmed) {
|
|
1361
|
+
console.error(`map-bootstrap: none of the ${proposals.length} proposal(s) in ${proposalsFile} is confirmed. Show the user the list, set "confirmed": true on each entry they accept, and re-run. Nothing was written \u2014 proposals are never accepted automatically.`);
|
|
1362
|
+
return 1;
|
|
1363
|
+
}
|
|
1364
|
+
const out = JSON.stringify(map, null, 2) + "\n";
|
|
1365
|
+
if (outFile) fs6.writeFileSync(outFile, out);
|
|
1366
|
+
else process.stdout.write(out);
|
|
1367
|
+
for (const sk of report.skipped) console.error(`warn ${sk}`);
|
|
1368
|
+
console.error(`map-bootstrap: ${report.confirmed} confirmed proposal(s) \u2192 ${report.added} new stub(s), ${report.kept} already mapped${outFile ? ` \u2014 wrote ${outFile}` : ""}`);
|
|
1369
|
+
return 0;
|
|
1370
|
+
}
|
|
1371
|
+
let scopedCatalog = catalog;
|
|
1372
|
+
if (screenFile) {
|
|
1373
|
+
const screenDoc = readDocFile(screenFile, "screen export", isScreenDoc);
|
|
1374
|
+
const used = /* @__PURE__ */ new Set();
|
|
1375
|
+
for (const i of visibleInstances(screenDoc, "screen")) {
|
|
1376
|
+
if (i.key) used.add(i.key);
|
|
1377
|
+
if (i.setKey) used.add(i.setKey);
|
|
1378
|
+
}
|
|
1379
|
+
const all = union.components;
|
|
1380
|
+
const fromNamed = catalog.components.filter((c) => c.key && used.has(c.key) || c.id && used.has(c.id));
|
|
1381
|
+
const fromLibs = others.components.filter((c) => c.key && used.has(c.key));
|
|
1382
|
+
const scoped = [...fromNamed, ...fromLibs];
|
|
1383
|
+
scopedCatalog = safeAssign({}, catalog, { components: scoped });
|
|
1384
|
+
const fromOthers = fromLibs.length;
|
|
1385
|
+
console.error(`map-bootstrap: --screen scoped the catalog${sources.length > 1 ? `s` : ""} from ${all.length} to ${scoped.length} component(s) this screen actually uses` + (fromOthers ? ` (${fromOthers} of them from a library catalog).` : "."));
|
|
1386
|
+
}
|
|
1387
|
+
const written = bootstrap(scopedCatalog, existing);
|
|
1388
|
+
if (!outFile) {
|
|
1389
|
+
process.stdout.write(JSON.stringify(written, null, 2) + "\n");
|
|
1390
|
+
} else {
|
|
1391
|
+
writeJsonLike(outFile, written);
|
|
1392
|
+
const entries = Object.values(written.components);
|
|
1393
|
+
const review = entries.filter((e) => e.status === "needs-review").length;
|
|
1394
|
+
console.error(`map-bootstrap: wrote ${outFile} \u2014 ${entries.length} component(s), ${review} needing review${existing ? " (merged into the existing map)" : ""}`);
|
|
1395
|
+
}
|
|
1396
|
+
return 0;
|
|
1397
|
+
}
|
|
1398
|
+
if (import.meta.main) runCli(main, { name: "map-bootstrap" });
|
|
1399
|
+
export {
|
|
1400
|
+
bootstrap,
|
|
1401
|
+
bootstrapFromProposals,
|
|
1402
|
+
main,
|
|
1403
|
+
proposalsIn
|
|
1404
|
+
};
|