@design-twin/dtwin 0.1.0-alpha.0 → 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/README.md +13 -3
- 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 +5 -1
- package/src/figma-mcp.ts +16 -14
- package/src/init.ts +6 -6
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// GENERATED from design-to-code/ by claude-plugin/build-scripts.js — edit the source, then rebuild.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// design-to-code/cli-args.ts
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// bridge/src/errmsg.ts
|
|
9
|
+
var errMsg = (e) => typeof e === "string" ? e : String(e && e.message || e);
|
|
10
|
+
|
|
11
|
+
// bridge/src/stdio.ts
|
|
12
|
+
import { format, inspect } from "node:util";
|
|
13
|
+
function installQueuedConsole(o = {}) {
|
|
14
|
+
trackWrites(process.stdout);
|
|
15
|
+
trackWrites(process.stderr);
|
|
16
|
+
const out = o.logTo === "stderr" ? process.stderr : process.stdout;
|
|
17
|
+
const err = process.stderr;
|
|
18
|
+
let indent = "";
|
|
19
|
+
const write = (s, text) => {
|
|
20
|
+
s.write((indent ? text.replace(/^/gm, indent) : text) + "\n");
|
|
21
|
+
};
|
|
22
|
+
const line = (s) => (...a) => {
|
|
23
|
+
write(s, format(...a));
|
|
24
|
+
};
|
|
25
|
+
console.log = console.info = console.debug = line(out);
|
|
26
|
+
console.warn = console.error = line(err);
|
|
27
|
+
console.dir = (item, options) => {
|
|
28
|
+
write(out, inspect(item, { customInspect: false, ...options }));
|
|
29
|
+
};
|
|
30
|
+
console.dirxml = line(out);
|
|
31
|
+
console.table = (data) => {
|
|
32
|
+
write(out, inspect(data));
|
|
33
|
+
};
|
|
34
|
+
console.write = (...data) => {
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const d of data) {
|
|
37
|
+
const b = typeof d === "string" ? Buffer.from(d) : d instanceof ArrayBuffer ? Buffer.from(d) : Buffer.from(d.buffer, d.byteOffset, d.byteLength);
|
|
38
|
+
if (b.length) {
|
|
39
|
+
out.write(b);
|
|
40
|
+
n += b.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
};
|
|
45
|
+
console.clear = () => {
|
|
46
|
+
if (out.isTTY) out.write("\x1B[1;1H\x1B[0J");
|
|
47
|
+
};
|
|
48
|
+
console.trace = (...a) => {
|
|
49
|
+
const e = new Error(format(...a));
|
|
50
|
+
e.name = "Trace";
|
|
51
|
+
Error.captureStackTrace(e, console.trace);
|
|
52
|
+
write(err, e.stack ?? `Trace: ${e.message}`);
|
|
53
|
+
};
|
|
54
|
+
console.assert = (cond, ...a) => {
|
|
55
|
+
if (!cond) write(err, a.length ? "Assertion failed: " + format(...a) : "Assertion failed");
|
|
56
|
+
};
|
|
57
|
+
console.group = console.groupCollapsed = (...a) => {
|
|
58
|
+
if (a.length) write(out, format(...a));
|
|
59
|
+
indent += " ";
|
|
60
|
+
};
|
|
61
|
+
console.groupEnd = () => {
|
|
62
|
+
indent = indent.slice(0, -2);
|
|
63
|
+
};
|
|
64
|
+
const counts = /* @__PURE__ */ new Map();
|
|
65
|
+
console.count = (label = "default") => {
|
|
66
|
+
const n = (counts.get(label) ?? 0) + 1;
|
|
67
|
+
counts.set(label, n);
|
|
68
|
+
write(out, `${label}: ${n}`);
|
|
69
|
+
};
|
|
70
|
+
console.countReset = (label = "default") => {
|
|
71
|
+
counts.delete(label);
|
|
72
|
+
};
|
|
73
|
+
const timers = /* @__PURE__ */ new Map();
|
|
74
|
+
const elapsed = (label) => {
|
|
75
|
+
const t = timers.get(label);
|
|
76
|
+
return t === void 0 ? null : `${label}: ${(performance.now() - t).toFixed(3)}ms`;
|
|
77
|
+
};
|
|
78
|
+
console.time = (label = "default") => {
|
|
79
|
+
timers.set(label, performance.now());
|
|
80
|
+
};
|
|
81
|
+
console.timeLog = (label = "default", ...a) => {
|
|
82
|
+
const e = elapsed(label);
|
|
83
|
+
if (e === null) write(err, `Warning: No such label '${label}' for console.timeLog()`);
|
|
84
|
+
else write(out, [e, ...a.map((x) => format(x))].join(" "));
|
|
85
|
+
};
|
|
86
|
+
console.timeEnd = (label = "default") => {
|
|
87
|
+
const e = elapsed(label);
|
|
88
|
+
timers.delete(label);
|
|
89
|
+
write(e === null ? err : out, e ?? `Warning: No such label '${label}' for console.timeEnd()`);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
93
|
+
function trackWrites(s) {
|
|
94
|
+
if (inFlight.has(s)) return;
|
|
95
|
+
const st = { n: 0, idle: [] };
|
|
96
|
+
inFlight.set(s, st);
|
|
97
|
+
const write = s.write.bind(s);
|
|
98
|
+
const settle = () => {
|
|
99
|
+
if (--st.n === 0) for (const r of st.idle.splice(0)) r();
|
|
100
|
+
};
|
|
101
|
+
s.write = (chunk, encoding, cb) => {
|
|
102
|
+
const done = typeof encoding === "function" ? encoding : cb;
|
|
103
|
+
const enc = typeof encoding === "function" ? void 0 : encoding;
|
|
104
|
+
if (chunk.length === 0) return write(chunk, enc, done);
|
|
105
|
+
st.n++;
|
|
106
|
+
let called = false;
|
|
107
|
+
return write(chunk, enc, (err) => {
|
|
108
|
+
if (!called) {
|
|
109
|
+
called = true;
|
|
110
|
+
settle();
|
|
111
|
+
}
|
|
112
|
+
done?.(err);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
var CliExit = class extends Error {
|
|
117
|
+
code;
|
|
118
|
+
constructor(code) {
|
|
119
|
+
super(`exit ${code}`);
|
|
120
|
+
this.name = "CliExit";
|
|
121
|
+
this.code = code;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
var cliDepth = 0;
|
|
125
|
+
function exitCli(code) {
|
|
126
|
+
if (cliDepth > 0) throw new CliExit(code);
|
|
127
|
+
process.exit(code);
|
|
128
|
+
}
|
|
129
|
+
function runCli(main2, o, argv = process.argv.slice(2)) {
|
|
130
|
+
installQueuedConsole();
|
|
131
|
+
const done = o.done ?? ((code) => {
|
|
132
|
+
process.exitCode = code;
|
|
133
|
+
});
|
|
134
|
+
const fail = (e) => {
|
|
135
|
+
if (e instanceof CliExit) return done(e.code);
|
|
136
|
+
console.error(o.errorLine ? o.errorLine(e) : `${o.name}: ${e instanceof Error && e.stack || String(e)}`);
|
|
137
|
+
done(o.errorCode ?? 1);
|
|
138
|
+
};
|
|
139
|
+
cliDepth++;
|
|
140
|
+
let settled = false;
|
|
141
|
+
const leave = () => {
|
|
142
|
+
if (!settled) {
|
|
143
|
+
settled = true;
|
|
144
|
+
cliDepth--;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
const r = main2(argv);
|
|
149
|
+
if (typeof r === "number") {
|
|
150
|
+
leave();
|
|
151
|
+
done(r);
|
|
152
|
+
} else r.then((c) => {
|
|
153
|
+
leave();
|
|
154
|
+
done(c);
|
|
155
|
+
}, (e) => {
|
|
156
|
+
leave();
|
|
157
|
+
fail(e);
|
|
158
|
+
});
|
|
159
|
+
} catch (e) {
|
|
160
|
+
leave();
|
|
161
|
+
fail(e);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// design-to-code/proc.ts
|
|
166
|
+
var pause = new Int32Array(new SharedArrayBuffer(4));
|
|
167
|
+
|
|
168
|
+
// design-to-code/cli-args.ts
|
|
169
|
+
var SELF = fileURLToPath(import.meta.url);
|
|
170
|
+
var shellQuote = (p) => /["$`\\!]/.test(p) ? `'${p.replaceAll("'", `'\\''`)}'` : `"${p}"`;
|
|
171
|
+
var SCRIPT_RUNNER = "bun --no-install --no-env-file";
|
|
172
|
+
var scriptCmd = (name) => `${SCRIPT_RUNNER} ${shellQuote(path.join(path.dirname(SELF), name + path.extname(SELF)))}`;
|
|
173
|
+
|
|
174
|
+
// bridge/src/read-json.ts
|
|
175
|
+
import fs from "node:fs";
|
|
176
|
+
var anyJson = (_x) => true;
|
|
177
|
+
function readFailure(e) {
|
|
178
|
+
const code = e && typeof e === "object" && "code" in e ? e.code : void 0;
|
|
179
|
+
if (code === "ENOENT") return { error: "does not exist", missing: true };
|
|
180
|
+
return {
|
|
181
|
+
error: code === "EISDIR" ? "is a directory, not a file" : code === "EACCES" ? "is not readable (permission denied)" : `could not be read (${String(code || e)})`
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function readJson(file, guard) {
|
|
185
|
+
let buf;
|
|
186
|
+
try {
|
|
187
|
+
buf = fs.readFileSync(file);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
return readFailure(e);
|
|
190
|
+
}
|
|
191
|
+
if (buf.length >= 2 && (buf[0] === 255 && buf[1] === 254 || buf[0] === 254 && buf[1] === 255)) {
|
|
192
|
+
return { error: "is UTF-16, not UTF-8 \u2014 re-save it as UTF-8" };
|
|
193
|
+
}
|
|
194
|
+
let raw = buf.toString("utf8");
|
|
195
|
+
if (raw.charCodeAt(0) === 65279) raw = raw.slice(1);
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
const value = JSON.parse(raw);
|
|
199
|
+
parsed = value;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return { error: `is not valid JSON \u2014 ${errMsg(e)}` };
|
|
202
|
+
}
|
|
203
|
+
if (!guard(parsed)) return { error: `is not ${guard.expected || "the expected kind of document"}` };
|
|
204
|
+
return { doc: parsed };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// design-to-code/catalog-input.ts
|
|
208
|
+
function readJsonFile(file, what, hint) {
|
|
209
|
+
return readDocFile(file, what, anyJson, hint);
|
|
210
|
+
}
|
|
211
|
+
function readDocFile(file, what, guard, hint) {
|
|
212
|
+
return docOrExit(loadDocFile(file, what, guard, hint));
|
|
213
|
+
}
|
|
214
|
+
function loadDocFile(file, what, guard, hint, readAt = file) {
|
|
215
|
+
const r = readJson(readAt, guard);
|
|
216
|
+
if ("doc" in r) return r;
|
|
217
|
+
const ioFailure = r.missing || /^(is a directory|is not readable|could not be read)/.test(r.error);
|
|
218
|
+
return { error: `${what}: '${file}' ${r.error}${ioFailure ? "." : ""}` + (ioFailure && hint ? `
|
|
219
|
+
${hint}` : "") };
|
|
220
|
+
}
|
|
221
|
+
function docOrExit(r) {
|
|
222
|
+
if ("doc" in r) return r.doc;
|
|
223
|
+
console.error(`error ${r.error}`);
|
|
224
|
+
exitCli(2);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// bridge/src/json-util.ts
|
|
228
|
+
function nullProto() {
|
|
229
|
+
return /* @__PURE__ */ Object.create(null);
|
|
230
|
+
}
|
|
231
|
+
function isRecord(x) {
|
|
232
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// design-to-code/map-validate-core.ts
|
|
236
|
+
var STATUSES = ["active", "deprecated", "needs-review"];
|
|
237
|
+
var KEYS = {
|
|
238
|
+
root: ["version", "figmaFileKey", "components"],
|
|
239
|
+
entry: ["figma", "code", "props", "variantOverrides", "childrenByLayer", "status", "note"],
|
|
240
|
+
figma: ["key", "id", "name", "unstable"],
|
|
241
|
+
code: ["module", "export", "targets"],
|
|
242
|
+
target: ["module", "export"],
|
|
243
|
+
vo: ["when", "code"],
|
|
244
|
+
voCode: ["module", "export"],
|
|
245
|
+
children: ["layerNamePattern", "slot"],
|
|
246
|
+
prop: Object.assign(nullProto(), {
|
|
247
|
+
enum: ["kind", "codeProp", "values", "default", "omitDefault"],
|
|
248
|
+
boolean: ["kind", "codeProp", "default", "omitDefault"],
|
|
249
|
+
string: ["kind", "codeProp"],
|
|
250
|
+
instance: ["kind", "codeProp", "slot"]
|
|
251
|
+
})
|
|
252
|
+
};
|
|
253
|
+
var PROP_KINDS = Object.keys(KEYS.prop);
|
|
254
|
+
var isStr = (v) => typeof v === "string";
|
|
255
|
+
var isBool = (v) => typeof v === "boolean";
|
|
256
|
+
var noExtra = (obj, allowed, at, err) => {
|
|
257
|
+
if (!Array.isArray(allowed)) return;
|
|
258
|
+
for (const k of Object.keys(obj)) if (!allowed.includes(k)) err(`${at}.${k}`, "unknown property (additionalProperties:false)");
|
|
259
|
+
};
|
|
260
|
+
var optStrings = (obj, keys, at, err) => {
|
|
261
|
+
for (const k of keys) if (obj[k] !== void 0 && !isStr(obj[k])) err(`${at}.${k}`, "must be a string");
|
|
262
|
+
};
|
|
263
|
+
function validateMap(map) {
|
|
264
|
+
const errors = [];
|
|
265
|
+
const err = (path2, message) => errors.push({ path: path2, message });
|
|
266
|
+
if (!isRecord(map)) return { ok: false, errors: [{ path: "", message: "map must be an object" }] };
|
|
267
|
+
noExtra(map, KEYS.root, "", err);
|
|
268
|
+
if (map.version !== 1) err("version", "must be 1");
|
|
269
|
+
if (map.figmaFileKey !== void 0 && !isStr(map.figmaFileKey)) err("figmaFileKey", "must be a string");
|
|
270
|
+
if (!isRecord(map.components)) {
|
|
271
|
+
err("components", "must be an object");
|
|
272
|
+
return { ok: false, errors };
|
|
273
|
+
}
|
|
274
|
+
for (const key of Object.keys(map.components)) {
|
|
275
|
+
const e = map.components[key];
|
|
276
|
+
const at = `components.${key}`;
|
|
277
|
+
if (!isRecord(e)) {
|
|
278
|
+
err(at, "entry must be an object");
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
noExtra(e, KEYS.entry, at, err);
|
|
282
|
+
if (!isRecord(e.figma)) err(`${at}.figma`, "required object");
|
|
283
|
+
else {
|
|
284
|
+
noExtra(e.figma, KEYS.figma, `${at}.figma`, err);
|
|
285
|
+
if (!isStr(e.figma.name)) err(`${at}.figma.name`, "required string");
|
|
286
|
+
optStrings(e.figma, ["key", "id"], `${at}.figma`, err);
|
|
287
|
+
if (e.figma.unstable !== void 0 && !isBool(e.figma.unstable)) err(`${at}.figma.unstable`, "must be a boolean");
|
|
288
|
+
}
|
|
289
|
+
if (!isRecord(e.code)) err(`${at}.code`, "required object");
|
|
290
|
+
else {
|
|
291
|
+
noExtra(e.code, KEYS.code, `${at}.code`, err);
|
|
292
|
+
if (!isStr(e.code.module)) err(`${at}.code.module`, "required string");
|
|
293
|
+
if (!isStr(e.code.export)) err(`${at}.code.export`, "required string");
|
|
294
|
+
if (e.code.targets !== void 0) {
|
|
295
|
+
if (!isRecord(e.code.targets)) err(`${at}.code.targets`, "must be an object");
|
|
296
|
+
else for (const t of Object.keys(e.code.targets)) {
|
|
297
|
+
const tv = e.code.targets[t], ta = `${at}.code.targets.${t}`;
|
|
298
|
+
if (!isRecord(tv)) err(ta, "must be an object");
|
|
299
|
+
else {
|
|
300
|
+
noExtra(tv, KEYS.target, ta, err);
|
|
301
|
+
optStrings(tv, ["module", "export"], ta, err);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (e.status !== void 0 && (!isStr(e.status) || !STATUSES.includes(e.status))) err(`${at}.status`, `must be one of ${STATUSES.join("|")}`);
|
|
307
|
+
optStrings(e, ["note"], at, err);
|
|
308
|
+
if (e.props !== void 0) {
|
|
309
|
+
if (!isRecord(e.props)) err(`${at}.props`, "must be an object");
|
|
310
|
+
else for (const pn of Object.keys(e.props)) validateProp(e.props[pn], `${at}.props.${pn}`, err);
|
|
311
|
+
}
|
|
312
|
+
if (e.variantOverrides !== void 0) {
|
|
313
|
+
if (!Array.isArray(e.variantOverrides)) err(`${at}.variantOverrides`, "must be an array");
|
|
314
|
+
else e.variantOverrides.forEach((vo, i) => {
|
|
315
|
+
const va = `${at}.variantOverrides[${i}]`;
|
|
316
|
+
if (!isRecord(vo)) {
|
|
317
|
+
err(va, "must be an object");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
noExtra(vo, KEYS.vo, va, err);
|
|
321
|
+
if (!isRecord(vo.when)) err(`${va}.when`, "required object");
|
|
322
|
+
else for (const wk of Object.keys(vo.when)) if (!isStr(vo.when[wk])) err(`${va}.when.${wk}`, "value must be a string");
|
|
323
|
+
if (!isRecord(vo.code)) err(`${va}.code`, "required object");
|
|
324
|
+
else {
|
|
325
|
+
noExtra(vo.code, KEYS.voCode, `${va}.code`, err);
|
|
326
|
+
if (!isStr(vo.code.module)) err(`${va}.code.module`, "required string");
|
|
327
|
+
if (!isStr(vo.code.export)) err(`${va}.code.export`, "required string");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
if (e.childrenByLayer !== void 0) {
|
|
332
|
+
if (!isRecord(e.childrenByLayer)) err(`${at}.childrenByLayer`, "must be an object");
|
|
333
|
+
else {
|
|
334
|
+
noExtra(e.childrenByLayer, KEYS.children, `${at}.childrenByLayer`, err);
|
|
335
|
+
optStrings(e.childrenByLayer, ["layerNamePattern", "slot"], `${at}.childrenByLayer`, err);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { ok: errors.length === 0, errors };
|
|
340
|
+
}
|
|
341
|
+
function validateProp(p, at, err) {
|
|
342
|
+
if (!isRecord(p)) {
|
|
343
|
+
err(at, "prop must be an object");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const allowed = typeof p.kind === "string" ? KEYS.prop[p.kind] : void 0;
|
|
347
|
+
if (!Array.isArray(allowed)) {
|
|
348
|
+
err(`${at}.kind`, `must be one of ${PROP_KINDS.join("|")}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
noExtra(p, allowed, at, err);
|
|
352
|
+
if (p.kind !== "instance" && !isStr(p.codeProp)) err(`${at}.codeProp`, "required string");
|
|
353
|
+
if (p.kind === "instance" && p.codeProp !== void 0 && !isStr(p.codeProp)) err(`${at}.codeProp`, "must be a string");
|
|
354
|
+
if (p.kind === "instance" && p.slot !== void 0 && !isStr(p.slot)) err(`${at}.slot`, "must be a string");
|
|
355
|
+
if (p.kind === "enum") {
|
|
356
|
+
if (p.values !== void 0 && !isRecord(p.values)) err(`${at}.values`, "must be an object (VARIANT option -> code value)");
|
|
357
|
+
else if (isRecord(p.values)) for (const k of Object.keys(p.values)) {
|
|
358
|
+
const t = typeof p.values[k];
|
|
359
|
+
if (p.values[k] !== null && !["string", "number", "boolean"].includes(t)) err(`${at}.values.${k}`, "value must be string|number|boolean|null");
|
|
360
|
+
}
|
|
361
|
+
if (p.default !== void 0 && !["string", "number", "boolean"].includes(typeof p.default)) err(`${at}.default`, "must be string|number|boolean");
|
|
362
|
+
}
|
|
363
|
+
if (p.kind === "boolean" && p.default !== void 0 && !isBool(p.default)) err(`${at}.default`, "must be a boolean");
|
|
364
|
+
if ((p.kind === "enum" || p.kind === "boolean") && p.omitDefault !== void 0 && !isBool(p.omitDefault)) err(`${at}.omitDefault`, "must be a boolean");
|
|
365
|
+
}
|
|
366
|
+
function isCodeConnectMap(x) {
|
|
367
|
+
return validateMap(x).ok;
|
|
368
|
+
}
|
|
369
|
+
isCodeConnectMap.expected = "a valid component map (the map-validate script lists what is wrong with it)";
|
|
370
|
+
|
|
371
|
+
// design-to-code/map-validate.ts
|
|
372
|
+
function main(argv) {
|
|
373
|
+
const file = argv[0];
|
|
374
|
+
const USAGE = `usage: ${scriptCmd("map-validate")} <map.json>`;
|
|
375
|
+
if (file === "--help" || file === "-h") {
|
|
376
|
+
console.log(USAGE);
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
379
|
+
if (!file || file.startsWith("-")) {
|
|
380
|
+
console.error((file ? `map-validate: unknown flag ${file}
|
|
381
|
+
` : "") + USAGE);
|
|
382
|
+
return 1;
|
|
383
|
+
}
|
|
384
|
+
const res = validateMap(readJsonFile(file, "component map"));
|
|
385
|
+
if (res.ok) {
|
|
386
|
+
console.log("map valid");
|
|
387
|
+
return 0;
|
|
388
|
+
}
|
|
389
|
+
res.errors.forEach((e) => console.error(` ${e.path || "(root)"}: ${e.message}`));
|
|
390
|
+
console.error(`
|
|
391
|
+
${res.errors.length} error(s)`);
|
|
392
|
+
return 1;
|
|
393
|
+
}
|
|
394
|
+
if (import.meta.main) runCli(main, { name: "map-validate" });
|
|
395
|
+
export {
|
|
396
|
+
main
|
|
397
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@design-twin/dtwin",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Design Twin bridge: local CLI + MCP server between Claude Code and the Design Twin Figma plugin. Reads a design on the free Figma plan, fully offline.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mohamed Magdy Omar",
|
|
@@ -31,11 +31,15 @@
|
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"src",
|
|
34
|
+
"layer",
|
|
35
|
+
"figma-plugin",
|
|
34
36
|
"README.md",
|
|
35
37
|
"LICENSE"
|
|
36
38
|
],
|
|
37
39
|
"scripts": {
|
|
38
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
+
"prepack": "bun --no-install --no-env-file pack.ts",
|
|
42
|
+
"postpack": "bun --no-install --no-env-file pack.ts --clean",
|
|
39
43
|
"pull": "bun src/figma-pull.ts",
|
|
40
44
|
"pull:selection": "bun src/figma-pull.ts design --selection",
|
|
41
45
|
"mcp": "bun src/figma-mcp.ts"
|
package/src/figma-mcp.ts
CHANGED
|
@@ -752,14 +752,16 @@ server.registerTool(
|
|
|
752
752
|
// — without these the agent has the figma_* tools and no way in, and the map and the codegen skill drift
|
|
753
753
|
// into two different formats. Registering them here is what makes that layer exist for its actual
|
|
754
754
|
// consumer.
|
|
755
|
-
// Loaded LAZILY, on call, and never at module scope.
|
|
756
|
-
// root (bridge/)
|
|
757
|
-
//
|
|
758
|
-
//
|
|
759
|
-
//
|
|
755
|
+
// Loaded LAZILY, on call, and never at module scope, from one of two places. In a repo checkout it is
|
|
756
|
+
// ../../design-to-code/<mod>.ts, a SIBLING of this package root (bridge/) — which npm's `files` cannot reach. So
|
|
757
|
+
// the npm package carries the layer as bundles instead: `prepack` (bridge/pack.ts) copies the committed,
|
|
758
|
+
// self-contained claude-plugin/scripts/<mod>.js into ../layer/. Neither there (a checkout without design-to-code/,
|
|
759
|
+
// a package packed without its prepack): importing eagerly would take the whole server down with a
|
|
760
|
+
// module-not-found, killing the other tools over two that merely cannot work — so the server always starts and only
|
|
761
|
+
// these two report, precisely, what is missing.
|
|
760
762
|
// The slice of each layer module this server calls, declared HERE (over doc-types.ts shapes) rather
|
|
761
763
|
// than as `typeof import("../../design-to-code/x.ts")`: even a type-only reference pulls that file and
|
|
762
|
-
// its imports into the bridge program, and the published package (src/
|
|
764
|
+
// its imports into the bridge program, and the published package (src/ is TypeScript, the layer only bundles) would name files it
|
|
763
765
|
// does not ship (see doc-types.ts). So bridge/src has no dependency of any kind on the layer — only the
|
|
764
766
|
// dynamic import below, which fails soft. Mirrors design-to-code/get-component.ts GetComponentResult and
|
|
765
767
|
// design-to-code/drift-lint.ts runDriftLint(); keep them in step (test/mcp-smoke.test.ts checks the real modules still satisfy these at compile time).
|
|
@@ -775,27 +777,27 @@ export interface DriftLintModule {
|
|
|
775
777
|
| { status: "linted"; errors: unknown[]; warnings: unknown[]; freshness: unknown; summary: unknown; catalogs: ReadonlyArray<{ file: string }>; coverage: { coverage: object; warnings: unknown[] } | null };
|
|
776
778
|
}
|
|
777
779
|
async function loadLayer<T>(mod: string, tool: string): Promise<T> {
|
|
778
|
-
// The
|
|
780
|
+
// The source in a checkout first (what the repo's tests run), else the package's bundle. Whether either is there
|
|
779
781
|
// is asked of the file system, before the import: the import's own failure cannot say it — a missing
|
|
780
782
|
// dependency OF the layer fails the same way, and the error's code and url fields differ between
|
|
781
783
|
// runtimes (Bun reports neither the way Node did). Any error from an import of a file that IS there
|
|
782
784
|
// is passed on as it is.
|
|
783
|
-
const url = new URL(
|
|
784
|
-
if (!
|
|
785
|
+
const url = [new URL(`../../design-to-code/${mod}.ts`, import.meta.url), new URL(`../layer/${mod}.js`, import.meta.url)].find((u) => fs.existsSync(u));
|
|
786
|
+
if (!url) {
|
|
785
787
|
throw new Error(
|
|
786
|
-
`${tool} needs the design-to-code layer, which is not present in this install
|
|
787
|
-
`
|
|
788
|
-
`checkout (bun bridge/src/figma-mcp.ts)
|
|
788
|
+
`${tool} needs the design-to-code layer, which is not present in this install: neither a repo checkout's ` +
|
|
789
|
+
`design-to-code/${mod}.ts nor the npm package's layer/${mod}.js. Reinstall @design-twin/dtwin, or run this MCP ` +
|
|
790
|
+
`server from a repo checkout (bun bridge/src/figma-mcp.ts). Everything else on this server works either way.`
|
|
789
791
|
);
|
|
790
792
|
}
|
|
791
793
|
return (await import(url.href)) as T;
|
|
792
794
|
}
|
|
793
795
|
const getComponent = async (catalogFile: string, handle: string) =>
|
|
794
|
-
(await loadLayer<GetComponentModule>("get-component
|
|
796
|
+
(await loadLayer<GetComponentModule>("get-component", "design_get_component")).getComponent(
|
|
795
797
|
catalogFile,
|
|
796
798
|
handle
|
|
797
799
|
);
|
|
798
|
-
const driftLintLayer = () => loadLayer<DriftLintModule>("drift-lint
|
|
800
|
+
const driftLintLayer = () => loadLayer<DriftLintModule>("drift-lint", "design_drift_lint");
|
|
799
801
|
|
|
800
802
|
// The export's component catalog is export-reader.ts readComponentCatalog's: the `files.componentsLocal` pointer
|
|
801
803
|
// (inside the export only), else an older export's inline components[] in design-system.json — the catalog `dtwin
|
package/src/init.ts
CHANGED
|
@@ -119,7 +119,7 @@ function bunxCacheOf(dir: string): string | null {
|
|
|
119
119
|
}
|
|
120
120
|
const bunxRefusal = (cache: string): string =>
|
|
121
121
|
`this dtwin runs from a bunx cache (${cache}) that Bun replaces or deletes on its own — a .mcp.json entry pointing into it would stop starting one day. ` +
|
|
122
|
-
"Install it for good (in a clone: `bun install`, then `cd bridge && bun link`), and run `dtwin init --mcp` from that install";
|
|
122
|
+
"Install it for good (`bun add -g @design-twin/dtwin`, or in a clone: `bun install`, then `cd bridge && bun link`), and run `dtwin init --mcp` from that install";
|
|
123
123
|
|
|
124
124
|
/** How to look at the machine (seams for the suites): does a path exist, and what does a bare command resolve to. */
|
|
125
125
|
export interface EntryProbe {
|
|
@@ -371,14 +371,14 @@ function main(argv: string[]): void {
|
|
|
371
371
|
// Run through `bunx @design-twin/dtwin init`: this very file is in bunx's cache, so no entry may point at it.
|
|
372
372
|
const cache = bunxCacheOf(import.meta.dirname);
|
|
373
373
|
const refuseMcp = cache ? bunxRefusal(cache) : undefined;
|
|
374
|
-
if (cache && !argv.includes("--mcp")) log(`note this dtwin runs from a bunx cache (${cache}) — install it for good (in a clone: \`bun install\`, then \`cd bridge && bun link\`) to have \`dtwin\` on PATH and to register the MCP server`);
|
|
374
|
+
if (cache && !argv.includes("--mcp")) log(`note this dtwin runs from a bunx cache (${cache}) — install it for good (\`bun add -g @design-twin/dtwin\`, or in a clone: \`bun install\`, then \`cd bridge && bun link\`) to have \`dtwin\` on PATH and to register the MCP server`);
|
|
375
375
|
const actions = plan(cwd, { mcp: argv.includes("--mcp"), mcpEntry, ...ifDefined("refuseMcp", refuseMcp), token });
|
|
376
376
|
if (dry) for (const a of actions) log(`${a.kind === "write" ? "would write" : a.kind === "mkdir" ? "would create" : a.kind} ${"path" in a ? a.path + " — " : ""}${a.note}`);
|
|
377
377
|
else apply(cwd, actions, log);
|
|
378
378
|
|
|
379
|
-
// The plugin manifest
|
|
380
|
-
//
|
|
381
|
-
const manifest = path.join(import.meta.dirname, "..", "..", "figma-plugin", "manifest.json");
|
|
379
|
+
// The Figma plugin's manifest: a repo checkout's (bridge/src/ -> ../../figma-plugin/), else the copy the npm
|
|
380
|
+
// package carries (../figma-plugin/, put there by bridge/pack.ts). Neither: the step names the repo instead.
|
|
381
|
+
const manifest = [path.join(import.meta.dirname, "..", "..", "figma-plugin", "manifest.json"), path.join(import.meta.dirname, "..", "figma-plugin", "manifest.json")].find((m) => fs.existsSync(m));
|
|
382
382
|
// Step 3 is already done for anyone who reached init THROUGH the installed plugin's help skill —
|
|
383
383
|
// which is the documented path. Claude Code records installed plugins under ~/.claude; when the
|
|
384
384
|
// marker is there, saying "install the plugin" is noise at best and confusing at worst.
|
|
@@ -400,7 +400,7 @@ function main(argv: string[]): void {
|
|
|
400
400
|
})();
|
|
401
401
|
|
|
402
402
|
const steps = [
|
|
403
|
-
"In Figma DESKTOP: Plugins → Development → Import plugin from manifest… → " + (
|
|
403
|
+
"In Figma DESKTOP: Plugins → Development → Import plugin from manifest… → " + (manifest ?? "figma-plugin/manifest.json from a clone of the Design Twin repo (this install does not include it)"),
|
|
404
404
|
"Run the plugin (Plugins → Development → Design Twin), paste the bridge token into its \"Bridge token\" field, Save: dtwin --show-token | pbcopy",
|
|
405
405
|
];
|
|
406
406
|
if (!pluginInstalled) {
|