desen-cli 1.0.0-draft.1 ā 1.0.0-draft.11
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/.turbo/turbo-build.log +1 -1
- package/dist/index.js +161 -97
- package/package.json +25 -25
- package/src/index.ts +162 -98
package/.turbo/turbo-build.log
CHANGED
package/dist/index.js
CHANGED
|
@@ -172,8 +172,9 @@ program
|
|
|
172
172
|
<div class="card">
|
|
173
173
|
<h1>š DESEN Daemon Active</h1>
|
|
174
174
|
<p>The Synchronization (Sync) server is waiting for incoming <strong>UI AST</strong> data from Figma. It can safely continue running in the background.</p>
|
|
175
|
-
<p>
|
|
176
|
-
<div class="code">
|
|
175
|
+
<p>When you press <strong>Sync to Localhost</strong> in Figma, the daemon will <strong>automatically</strong>:</p>
|
|
176
|
+
<div class="code">1. Validate & save JSON files<br/>2. Install dependencies (if needed)<br/>3. Start dev server on port 3001<br/>4. Open your browser āØ</div>
|
|
177
|
+
<p style="margin-top:16px;color:#888;">Zero-touch. Just press Sync and watch the magic.</p>
|
|
177
178
|
</div>
|
|
178
179
|
</body>
|
|
179
180
|
</html>
|
|
@@ -187,19 +188,43 @@ program
|
|
|
187
188
|
if (!payloads || payloads.length === 0 || !payloads[0].path) {
|
|
188
189
|
return res.status(400).json({ error: "Missing payloads or path in request body" });
|
|
189
190
|
}
|
|
190
|
-
//
|
|
191
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
192
|
+
// VALIDATION (Warn-and-Proceed for Development)
|
|
193
|
+
// Daemon is a dev tool ā strict fail-closed enforcement
|
|
194
|
+
// belongs in `desen validate` (CI/CD pipeline).
|
|
195
|
+
// Here we log warnings but NEVER block the sync.
|
|
196
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
197
|
+
let validationWarnings = 0;
|
|
191
198
|
for (const item of payloads) {
|
|
192
199
|
if (!item.path || !item.ast)
|
|
193
200
|
continue;
|
|
194
|
-
|
|
201
|
+
let result;
|
|
202
|
+
if (item.path.includes('surfaces/')) {
|
|
203
|
+
result = desen_core_1.surfaceSpec.safeParse(item.ast);
|
|
204
|
+
}
|
|
205
|
+
else if (item.path.includes('elements/') || item.path.includes('compositions/')) {
|
|
206
|
+
result = desen_core_1.nodeSpec.safeParse(item.ast);
|
|
207
|
+
}
|
|
208
|
+
else if (item.path.includes('themes/')) {
|
|
209
|
+
result = { success: true };
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
result = desen_core_1.nodeSpec.safeParse(item.ast);
|
|
213
|
+
}
|
|
195
214
|
if (!result.success) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
validationWarnings++;
|
|
216
|
+
console.warn(chalk_1.default.yellow(`ā ļø [Validation Warning] ${item.path}`));
|
|
217
|
+
const formatted = result.error.format();
|
|
218
|
+
// Log a concise summary instead of the full error tree
|
|
219
|
+
const issues = result.error.errors.map((e) => ` ā [${e.path.join('.')}] ${e.message}`).join('\n');
|
|
220
|
+
console.warn(chalk_1.default.yellow(issues));
|
|
202
221
|
}
|
|
222
|
+
else {
|
|
223
|
+
console.log(chalk_1.default.green(` ā ${item.path}`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (validationWarnings > 0) {
|
|
227
|
+
console.warn(chalk_1.default.yellow(`\nā ļø ${validationWarnings} file(s) have validation warnings. Run 'desen validate' for strict checks.`));
|
|
203
228
|
}
|
|
204
229
|
const projectRoot = process.cwd();
|
|
205
230
|
const workspaceDir = path_1.default.join(projectRoot, "desen-workspace");
|
|
@@ -235,8 +260,8 @@ program
|
|
|
235
260
|
"start": "next start"
|
|
236
261
|
},
|
|
237
262
|
dependencies: {
|
|
238
|
-
"desen-core": "1.0.0-draft.
|
|
239
|
-
"desen": "1.0.0-draft.
|
|
263
|
+
"desen-core": "1.0.0-draft.8",
|
|
264
|
+
"desen": "1.0.0-draft.8",
|
|
240
265
|
"next": "14.2.3",
|
|
241
266
|
"react": "18.3.1",
|
|
242
267
|
"react-dom": "18.3.1"
|
|
@@ -274,7 +299,7 @@ program
|
|
|
274
299
|
const appDir = path_1.default.join(workspaceDir, "src", "app");
|
|
275
300
|
fs_1.default.mkdirSync(appDir, { recursive: true });
|
|
276
301
|
// Find first surface directly
|
|
277
|
-
const firstSurface = payloads.find((p) => p.path.
|
|
302
|
+
const firstSurface = payloads.find((p) => p.path.includes('surfaces/'))?.path || 'desen/surfaces/frame-1.desen.json';
|
|
278
303
|
// API Route to load local .desen.json dynamically
|
|
279
304
|
const apiDir = path_1.default.join(appDir, "api", "surface", "route");
|
|
280
305
|
fs_1.default.mkdirSync(apiDir, { recursive: true });
|
|
@@ -312,13 +337,24 @@ async function resolveRefs(obj: any, baseDir: string): Promise<any> {
|
|
|
312
337
|
export async function GET() {
|
|
313
338
|
try {
|
|
314
339
|
const baseDir = process.cwd();
|
|
340
|
+
|
|
341
|
+
// Load surface
|
|
315
342
|
const filePath = path.join(baseDir, '${firstSurface}');
|
|
316
343
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
317
|
-
let
|
|
344
|
+
let parsedSurface = JSON.parse(content);
|
|
345
|
+
parsedSurface = await resolveRefs(parsedSurface, baseDir);
|
|
318
346
|
|
|
319
|
-
|
|
347
|
+
// Load tokens gracefully
|
|
348
|
+
let parsedTokens = {};
|
|
349
|
+
try {
|
|
350
|
+
const tokensPath = path.join(baseDir, 'desen/themes/tokens.desen.json');
|
|
351
|
+
const tokensContent = await fs.readFile(tokensPath, 'utf-8');
|
|
352
|
+
parsedTokens = await resolveRefs(JSON.parse(tokensContent), baseDir);
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.log("No custom tokens found, using defaults.");
|
|
355
|
+
}
|
|
320
356
|
|
|
321
|
-
return NextResponse.json(
|
|
357
|
+
return NextResponse.json({ surface: parsedSurface, tokens: parsedTokens });
|
|
322
358
|
} catch (e) {
|
|
323
359
|
return NextResponse.json({ error: 'System is waiting for your sync!' }, { status: 404 });
|
|
324
360
|
}
|
|
@@ -363,13 +399,15 @@ import { DesenProvider, DesenRenderer } from "desen";
|
|
|
363
399
|
import { registry } from "../lib/desenRegistry";
|
|
364
400
|
|
|
365
401
|
export default function Page() {
|
|
366
|
-
const [
|
|
402
|
+
const [uiData, setUiData] = useState<any>(null);
|
|
367
403
|
|
|
368
404
|
useEffect(() => {
|
|
369
405
|
const fetchUI = () => {
|
|
370
406
|
fetch("/api/surface/route")
|
|
371
407
|
.then(res => res.json())
|
|
372
|
-
.then(json => {
|
|
408
|
+
.then(json => {
|
|
409
|
+
if(!json.error) setUiData(json);
|
|
410
|
+
})
|
|
373
411
|
.catch(e => console.error(e));
|
|
374
412
|
};
|
|
375
413
|
fetchUI();
|
|
@@ -377,18 +415,19 @@ export default function Page() {
|
|
|
377
415
|
return () => clearInterval(intv);
|
|
378
416
|
}, []);
|
|
379
417
|
|
|
380
|
-
if (!
|
|
418
|
+
if (!uiData || !uiData.surface) return <div style={{padding: 40, fontFamily: 'monospace'}}>Waiting for Figma Sync...</div>;
|
|
381
419
|
|
|
382
420
|
// SPEC.md: Surface = full page. Pass full surface object, not data.root.
|
|
383
421
|
return (
|
|
384
422
|
<DesenProvider
|
|
385
423
|
registry={registry}
|
|
424
|
+
tokens={uiData.tokens?.tokens || uiData.tokens || {}}
|
|
386
425
|
session_id="workspace_session"
|
|
387
426
|
revision_id="rev_1"
|
|
388
427
|
onTelemetry={e => console.log("[Telemetry]", e)}
|
|
389
428
|
onAction={a => console.log("[Action]", a)}
|
|
390
429
|
>
|
|
391
|
-
<DesenRenderer node={
|
|
430
|
+
<DesenRenderer node={uiData.surface} />
|
|
392
431
|
</DesenProvider>
|
|
393
432
|
);
|
|
394
433
|
}
|
|
@@ -399,67 +438,6 @@ export default function Page() {
|
|
|
399
438
|
fs_1.default.writeFileSync(path_1.default.join(libDir, "desenRegistry.tsx"), `
|
|
400
439
|
import React from 'react';
|
|
401
440
|
|
|
402
|
-
function resolveToken(tokenStr: string): string | undefined {
|
|
403
|
-
if (!tokenStr) return undefined;
|
|
404
|
-
const match = tokenStr.match(/t_([a-fA-F0-9]{3,8})/);
|
|
405
|
-
return match ? \`#\${match[1]}\` : undefined;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function resolveColor(tokenStr?: string, hexStr?: string): string | undefined {
|
|
409
|
-
if (tokenStr) return resolveToken(tokenStr);
|
|
410
|
-
if (hexStr) return hexStr;
|
|
411
|
-
return undefined;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function buildCssFromNode(layout: any, style: any, opts?: { isText?: boolean; isSurfaceRoot?: boolean }): React.CSSProperties {
|
|
415
|
-
const css: any = { boxSizing: 'border-box' };
|
|
416
|
-
|
|
417
|
-
if (layout) {
|
|
418
|
-
css.display = 'flex';
|
|
419
|
-
css.flexDirection = layout.direction === 'row' ? 'row' : 'column';
|
|
420
|
-
if (layout.gap !== undefined && layout.gap !== 0) css.gap = layout.gap;
|
|
421
|
-
|
|
422
|
-
const am: Record<string, string> = { start:'flex-start', end:'flex-end', center:'center', 'space-between':'space-between', 'space-around':'space-around', 'space-evenly':'space-evenly' };
|
|
423
|
-
css.alignItems = am[layout.align_items] || 'stretch';
|
|
424
|
-
css.justifyContent = am[layout.align_content] || 'flex-start';
|
|
425
|
-
|
|
426
|
-
if (layout.padding !== undefined) {
|
|
427
|
-
css.padding = Array.isArray(layout.padding) ? layout.padding.map((p:number)=>p+'px').join(' ') : layout.padding !== 0 ? layout.padding+'px' : undefined;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (opts?.isSurfaceRoot) {
|
|
431
|
-
css.width = '100%'; css.minHeight = '100vh';
|
|
432
|
-
} else {
|
|
433
|
-
if (layout.sizing_h === 'FILL') css.width = '100%';
|
|
434
|
-
else if (layout.sizing_h === 'FIXED' && style?.width !== undefined) css.width = style.width;
|
|
435
|
-
if (layout.sizing_v === 'FILL') css.height = '100%';
|
|
436
|
-
else if (layout.sizing_v === 'FIXED' && style?.height !== undefined) css.height = style.height;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (style) {
|
|
441
|
-
const bgColor = resolveColor(style.bg_color_token, style.bg_color);
|
|
442
|
-
if (bgColor) { if (opts?.isText) css.color = bgColor; else css.backgroundColor = bgColor; }
|
|
443
|
-
const bc = resolveColor(style.border_color_token, style.border_color);
|
|
444
|
-
if (bc) css.borderColor = bc;
|
|
445
|
-
if (style.border_width !== undefined) { css.borderWidth = style.border_width; css.borderStyle = 'solid'; }
|
|
446
|
-
if (style.border_radius !== undefined) {
|
|
447
|
-
css.borderRadius = Array.isArray(style.border_radius) ? style.border_radius.map((r:number)=>r+'px').join(' ') : style.border_radius;
|
|
448
|
-
}
|
|
449
|
-
if (!layout && !opts?.isText) { if (style.width !== undefined) css.width = style.width; if (style.height !== undefined) css.height = style.height; }
|
|
450
|
-
if (style.opacity !== undefined) css.opacity = style.opacity;
|
|
451
|
-
if (style.font_size) css.fontSize = style.font_size;
|
|
452
|
-
if (style.font_weight) css.fontWeight = style.font_weight;
|
|
453
|
-
if (style.font_family) css.fontFamily = style.font_family;
|
|
454
|
-
if (style.letter_spacing) css.letterSpacing = style.letter_spacing;
|
|
455
|
-
if (style.line_height) css.lineHeight = style.line_height + 'px';
|
|
456
|
-
if (style.text_align) css.textAlign = style.text_align;
|
|
457
|
-
if (style.text_decoration) css.textDecoration = style.text_decoration;
|
|
458
|
-
if (style.text_transform) css.textTransform = style.text_transform;
|
|
459
|
-
}
|
|
460
|
-
return css;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
441
|
const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
|
|
464
442
|
if (!fonts || fonts.length === 0) return null;
|
|
465
443
|
|
|
@@ -483,33 +461,44 @@ const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
|
|
|
483
461
|
};
|
|
484
462
|
|
|
485
463
|
const SurfaceComponent = (props: any) => (
|
|
486
|
-
<div style={
|
|
464
|
+
<div style={props.parsedStyle}>
|
|
487
465
|
<DynamicFontLoader fonts={props.metadata?.fonts} />
|
|
488
466
|
{props.children}
|
|
489
467
|
</div>
|
|
490
468
|
);
|
|
491
469
|
|
|
492
470
|
const GenericContainer = (props: any) => (
|
|
493
|
-
<div style={
|
|
471
|
+
<div style={props.parsedStyle}>{props.children}</div>
|
|
494
472
|
);
|
|
495
473
|
|
|
496
474
|
const TextComponent = (props: any) => (
|
|
497
|
-
<span style={
|
|
475
|
+
<span style={props.parsedStyle}>{props.content || props.text || props.name}</span>
|
|
498
476
|
);
|
|
499
477
|
|
|
500
478
|
const ButtonComponent = (props: any) => {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (props.
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if (props.text_style.font_weight) css.fontWeight = props.text_style.font_weight;
|
|
509
|
-
if (props.text_style.font_family) css.fontFamily = props.text_style.font_family;
|
|
510
|
-
if (props.text_style.text_align) css.textAlign = props.text_style.text_align;
|
|
479
|
+
const css: any = { ...props.parsedStyle, cursor: 'pointer', border: 'none' };
|
|
480
|
+
|
|
481
|
+
// If no layout, center text by default (most buttons center their text)
|
|
482
|
+
if (!props.layout) {
|
|
483
|
+
css.display = 'flex';
|
|
484
|
+
css.alignItems = 'center';
|
|
485
|
+
css.justifyContent = 'center';
|
|
511
486
|
}
|
|
512
|
-
|
|
487
|
+
|
|
488
|
+
const textStyle = props.style?.text_style || {};
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<button style={css}>
|
|
492
|
+
<span style={{
|
|
493
|
+
fontFamily: textStyle.font_family,
|
|
494
|
+
fontSize: textStyle.font_size,
|
|
495
|
+
fontWeight: textStyle.font_weight,
|
|
496
|
+
color: textStyle.text_color || textStyle.bg_color // Figma plugin often maps text fill to bg_color
|
|
497
|
+
}}>
|
|
498
|
+
{props.children || props.content || props.text || props.name}
|
|
499
|
+
</span>
|
|
500
|
+
</button>
|
|
501
|
+
);
|
|
513
502
|
};
|
|
514
503
|
|
|
515
504
|
export const registry: Record<string, React.FC<any>> = {
|
|
@@ -519,11 +508,18 @@ export const registry: Record<string, React.FC<any>> = {
|
|
|
519
508
|
element: GenericContainer,
|
|
520
509
|
group: GenericContainer,
|
|
521
510
|
stack: GenericContainer,
|
|
511
|
+
Stack: GenericContainer,
|
|
512
|
+
Card: GenericContainer,
|
|
513
|
+
Grid: GenericContainer,
|
|
514
|
+
Header: GenericContainer,
|
|
515
|
+
Footer: GenericContainer,
|
|
522
516
|
text: TextComponent,
|
|
523
517
|
Text: TextComponent,
|
|
524
518
|
button: ButtonComponent,
|
|
525
|
-
|
|
526
|
-
|
|
519
|
+
Button: ButtonComponent,
|
|
520
|
+
input: (props: any) => <input style={props.parsedStyle} placeholder={props.content || props.name} />,
|
|
521
|
+
icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āļø</div>,
|
|
522
|
+
Icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āļø</div>,
|
|
527
523
|
};
|
|
528
524
|
`.trim());
|
|
529
525
|
console.log(chalk_1.default.green(`ā
Done! Standalone App Scaffolded.`));
|
|
@@ -538,6 +534,74 @@ export const registry: Record<string, React.FC<any>> = {
|
|
|
538
534
|
}
|
|
539
535
|
}
|
|
540
536
|
} // End if(!package.json)
|
|
537
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
538
|
+
// AUTO-PILOT: npm install + npm run dev + browser open
|
|
539
|
+
// Zero-touch DX ā developer never leaves Figma
|
|
540
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
541
|
+
const nodeModulesPath = path_1.default.join(workspaceDir, "node_modules");
|
|
542
|
+
const devServerPort = 3001;
|
|
543
|
+
const isPortInUse = (port) => {
|
|
544
|
+
return new Promise((resolve) => {
|
|
545
|
+
const net = require("net");
|
|
546
|
+
const tester = net.createServer()
|
|
547
|
+
.once("error", (err) => {
|
|
548
|
+
if (err.code === "EADDRINUSE")
|
|
549
|
+
resolve(true);
|
|
550
|
+
else
|
|
551
|
+
resolve(false);
|
|
552
|
+
})
|
|
553
|
+
.once("listening", () => {
|
|
554
|
+
tester.close(() => resolve(false));
|
|
555
|
+
})
|
|
556
|
+
.listen(port);
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
const runNpmInstallIfNeeded = async () => {
|
|
560
|
+
if (fs_1.default.existsSync(nodeModulesPath))
|
|
561
|
+
return;
|
|
562
|
+
console.log(chalk_1.default.blue(`\nš¦ Installing dependencies...`));
|
|
563
|
+
const { execSync } = require("child_process");
|
|
564
|
+
try {
|
|
565
|
+
execSync("npm install", { cwd: workspaceDir, stdio: "inherit" });
|
|
566
|
+
console.log(chalk_1.default.green(`ā
Dependencies installed.`));
|
|
567
|
+
}
|
|
568
|
+
catch (e) {
|
|
569
|
+
console.error(chalk_1.default.red(`ā npm install failed: ${e.message}`));
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const startDevServerIfNeeded = async () => {
|
|
573
|
+
const portBusy = await isPortInUse(devServerPort);
|
|
574
|
+
if (portBusy) {
|
|
575
|
+
console.log(chalk_1.default.gray(` Dev server already running on port ${devServerPort}.`));
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
console.log(chalk_1.default.blue(`\nš Starting dev server on port ${devServerPort}...`));
|
|
579
|
+
const { spawn } = require("child_process");
|
|
580
|
+
const child = spawn("npm", ["run", "dev"], {
|
|
581
|
+
cwd: workspaceDir,
|
|
582
|
+
detached: true,
|
|
583
|
+
stdio: "ignore"
|
|
584
|
+
});
|
|
585
|
+
child.unref();
|
|
586
|
+
console.log(chalk_1.default.green(`ā
Dev server launched (PID: ${child.pid}).`));
|
|
587
|
+
// Wait a bit then open browser
|
|
588
|
+
setTimeout(() => {
|
|
589
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
590
|
+
const { exec } = require("child_process");
|
|
591
|
+
exec(`${openCmd} http://localhost:${devServerPort}`);
|
|
592
|
+
console.log(chalk_1.default.cyan(`š Browser opened: http://localhost:${devServerPort}`));
|
|
593
|
+
}, 3000);
|
|
594
|
+
};
|
|
595
|
+
// Fire and forget ā don't block the HTTP response
|
|
596
|
+
(async () => {
|
|
597
|
+
try {
|
|
598
|
+
await runNpmInstallIfNeeded();
|
|
599
|
+
await startDevServerIfNeeded();
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
console.error(chalk_1.default.yellow(`ā ļø Auto-pilot warning: ${e.message}`));
|
|
603
|
+
}
|
|
604
|
+
})();
|
|
541
605
|
res.status(200).json({ success: true, message: `Successfully saved ${savedCount} files.` });
|
|
542
606
|
}
|
|
543
607
|
catch (error) {
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
2
|
+
"name": "desen-cli",
|
|
3
|
+
"version": "1.0.0-draft.11",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"bin": {
|
|
6
|
+
"desen": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"dev": "tsc -w",
|
|
11
|
+
"clean": "rm -rf dist"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"desen-core": "workspace:*",
|
|
15
|
+
"chalk": "^5.6.2",
|
|
16
|
+
"commander": "^14.0.3",
|
|
17
|
+
"express": "^4.19.2",
|
|
18
|
+
"cors": "^2.8.5"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/cors": "^2.8.19",
|
|
22
|
+
"@types/express": "^5.0.6",
|
|
23
|
+
"@types/node": "^20.19.33",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { Command } from "commander";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
-
import { surfaceSpec } from "desen-core";
|
|
7
|
+
import { surfaceSpec, nodeSpec } from "desen-core";
|
|
8
8
|
import express from "express";
|
|
9
9
|
import cors from "cors";
|
|
10
10
|
|
|
@@ -190,8 +190,9 @@ program
|
|
|
190
190
|
<div class="card">
|
|
191
191
|
<h1>š DESEN Daemon Active</h1>
|
|
192
192
|
<p>The Synchronization (Sync) server is waiting for incoming <strong>UI AST</strong> data from Figma. It can safely continue running in the background.</p>
|
|
193
|
-
<p>
|
|
194
|
-
<div class="code">
|
|
193
|
+
<p>When you press <strong>Sync to Localhost</strong> in Figma, the daemon will <strong>automatically</strong>:</p>
|
|
194
|
+
<div class="code">1. Validate & save JSON files<br/>2. Install dependencies (if needed)<br/>3. Start dev server on port 3001<br/>4. Open your browser āØ</div>
|
|
195
|
+
<p style="margin-top:16px;color:#888;">Zero-touch. Just press Sync and watch the magic.</p>
|
|
195
196
|
</div>
|
|
196
197
|
</body>
|
|
197
198
|
</html>
|
|
@@ -208,20 +209,41 @@ program
|
|
|
208
209
|
return res.status(400).json({ error: "Missing payloads or path in request body" });
|
|
209
210
|
}
|
|
210
211
|
|
|
211
|
-
//
|
|
212
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
213
|
+
// VALIDATION (Warn-and-Proceed for Development)
|
|
214
|
+
// Daemon is a dev tool ā strict fail-closed enforcement
|
|
215
|
+
// belongs in `desen validate` (CI/CD pipeline).
|
|
216
|
+
// Here we log warnings but NEVER block the sync.
|
|
217
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
218
|
+
let validationWarnings = 0;
|
|
212
219
|
for (const item of payloads) {
|
|
213
220
|
if (!item.path || !item.ast) continue;
|
|
214
221
|
|
|
215
|
-
|
|
222
|
+
let result: any;
|
|
223
|
+
if (item.path.includes('surfaces/')) {
|
|
224
|
+
result = surfaceSpec.safeParse(item.ast);
|
|
225
|
+
} else if (item.path.includes('elements/') || item.path.includes('compositions/')) {
|
|
226
|
+
result = nodeSpec.safeParse(item.ast);
|
|
227
|
+
} else if (item.path.includes('themes/')) {
|
|
228
|
+
result = { success: true };
|
|
229
|
+
} else {
|
|
230
|
+
result = nodeSpec.safeParse(item.ast);
|
|
231
|
+
}
|
|
232
|
+
|
|
216
233
|
if (!result.success) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
validationWarnings++;
|
|
235
|
+
console.warn(chalk.yellow(`ā ļø [Validation Warning] ${item.path}`));
|
|
236
|
+
const formatted = result.error.format();
|
|
237
|
+
// Log a concise summary instead of the full error tree
|
|
238
|
+
const issues = result.error.errors.map((e: any) => ` ā [${e.path.join('.')}] ${e.message}`).join('\n');
|
|
239
|
+
console.warn(chalk.yellow(issues));
|
|
240
|
+
} else {
|
|
241
|
+
console.log(chalk.green(` ā ${item.path}`));
|
|
223
242
|
}
|
|
224
243
|
}
|
|
244
|
+
if (validationWarnings > 0) {
|
|
245
|
+
console.warn(chalk.yellow(`\nā ļø ${validationWarnings} file(s) have validation warnings. Run 'desen validate' for strict checks.`));
|
|
246
|
+
}
|
|
225
247
|
|
|
226
248
|
const projectRoot = process.cwd();
|
|
227
249
|
const workspaceDir = path.join(projectRoot, "desen-workspace");
|
|
@@ -263,8 +285,8 @@ program
|
|
|
263
285
|
"start": "next start"
|
|
264
286
|
},
|
|
265
287
|
dependencies: {
|
|
266
|
-
"desen-core": "1.0.0-draft.
|
|
267
|
-
"desen": "1.0.0-draft.
|
|
288
|
+
"desen-core": "1.0.0-draft.8",
|
|
289
|
+
"desen": "1.0.0-draft.8",
|
|
268
290
|
"next": "14.2.3",
|
|
269
291
|
"react": "18.3.1",
|
|
270
292
|
"react-dom": "18.3.1"
|
|
@@ -305,7 +327,7 @@ program
|
|
|
305
327
|
fs.mkdirSync(appDir, { recursive: true });
|
|
306
328
|
|
|
307
329
|
// Find first surface directly
|
|
308
|
-
const firstSurface = payloads.find((p: any) => p.path.
|
|
330
|
+
const firstSurface = payloads.find((p: any) => p.path.includes('surfaces/'))?.path || 'desen/surfaces/frame-1.desen.json';
|
|
309
331
|
|
|
310
332
|
// API Route to load local .desen.json dynamically
|
|
311
333
|
const apiDir = path.join(appDir, "api", "surface", "route");
|
|
@@ -344,13 +366,24 @@ async function resolveRefs(obj: any, baseDir: string): Promise<any> {
|
|
|
344
366
|
export async function GET() {
|
|
345
367
|
try {
|
|
346
368
|
const baseDir = process.cwd();
|
|
369
|
+
|
|
370
|
+
// Load surface
|
|
347
371
|
const filePath = path.join(baseDir, '${firstSurface}');
|
|
348
372
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
349
|
-
let
|
|
373
|
+
let parsedSurface = JSON.parse(content);
|
|
374
|
+
parsedSurface = await resolveRefs(parsedSurface, baseDir);
|
|
350
375
|
|
|
351
|
-
|
|
376
|
+
// Load tokens gracefully
|
|
377
|
+
let parsedTokens = {};
|
|
378
|
+
try {
|
|
379
|
+
const tokensPath = path.join(baseDir, 'desen/themes/tokens.desen.json');
|
|
380
|
+
const tokensContent = await fs.readFile(tokensPath, 'utf-8');
|
|
381
|
+
parsedTokens = await resolveRefs(JSON.parse(tokensContent), baseDir);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.log("No custom tokens found, using defaults.");
|
|
384
|
+
}
|
|
352
385
|
|
|
353
|
-
return NextResponse.json(
|
|
386
|
+
return NextResponse.json({ surface: parsedSurface, tokens: parsedTokens });
|
|
354
387
|
} catch (e) {
|
|
355
388
|
return NextResponse.json({ error: 'System is waiting for your sync!' }, { status: 404 });
|
|
356
389
|
}
|
|
@@ -398,13 +431,15 @@ import { DesenProvider, DesenRenderer } from "desen";
|
|
|
398
431
|
import { registry } from "../lib/desenRegistry";
|
|
399
432
|
|
|
400
433
|
export default function Page() {
|
|
401
|
-
const [
|
|
434
|
+
const [uiData, setUiData] = useState<any>(null);
|
|
402
435
|
|
|
403
436
|
useEffect(() => {
|
|
404
437
|
const fetchUI = () => {
|
|
405
438
|
fetch("/api/surface/route")
|
|
406
439
|
.then(res => res.json())
|
|
407
|
-
.then(json => {
|
|
440
|
+
.then(json => {
|
|
441
|
+
if(!json.error) setUiData(json);
|
|
442
|
+
})
|
|
408
443
|
.catch(e => console.error(e));
|
|
409
444
|
};
|
|
410
445
|
fetchUI();
|
|
@@ -412,18 +447,19 @@ export default function Page() {
|
|
|
412
447
|
return () => clearInterval(intv);
|
|
413
448
|
}, []);
|
|
414
449
|
|
|
415
|
-
if (!
|
|
450
|
+
if (!uiData || !uiData.surface) return <div style={{padding: 40, fontFamily: 'monospace'}}>Waiting for Figma Sync...</div>;
|
|
416
451
|
|
|
417
452
|
// SPEC.md: Surface = full page. Pass full surface object, not data.root.
|
|
418
453
|
return (
|
|
419
454
|
<DesenProvider
|
|
420
455
|
registry={registry}
|
|
456
|
+
tokens={uiData.tokens?.tokens || uiData.tokens || {}}
|
|
421
457
|
session_id="workspace_session"
|
|
422
458
|
revision_id="rev_1"
|
|
423
459
|
onTelemetry={e => console.log("[Telemetry]", e)}
|
|
424
460
|
onAction={a => console.log("[Action]", a)}
|
|
425
461
|
>
|
|
426
|
-
<DesenRenderer node={
|
|
462
|
+
<DesenRenderer node={uiData.surface} />
|
|
427
463
|
</DesenProvider>
|
|
428
464
|
);
|
|
429
465
|
}
|
|
@@ -436,67 +472,6 @@ export default function Page() {
|
|
|
436
472
|
fs.writeFileSync(path.join(libDir, "desenRegistry.tsx"), `
|
|
437
473
|
import React from 'react';
|
|
438
474
|
|
|
439
|
-
function resolveToken(tokenStr: string): string | undefined {
|
|
440
|
-
if (!tokenStr) return undefined;
|
|
441
|
-
const match = tokenStr.match(/t_([a-fA-F0-9]{3,8})/);
|
|
442
|
-
return match ? \`#\${match[1]}\` : undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function resolveColor(tokenStr?: string, hexStr?: string): string | undefined {
|
|
446
|
-
if (tokenStr) return resolveToken(tokenStr);
|
|
447
|
-
if (hexStr) return hexStr;
|
|
448
|
-
return undefined;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function buildCssFromNode(layout: any, style: any, opts?: { isText?: boolean; isSurfaceRoot?: boolean }): React.CSSProperties {
|
|
452
|
-
const css: any = { boxSizing: 'border-box' };
|
|
453
|
-
|
|
454
|
-
if (layout) {
|
|
455
|
-
css.display = 'flex';
|
|
456
|
-
css.flexDirection = layout.direction === 'row' ? 'row' : 'column';
|
|
457
|
-
if (layout.gap !== undefined && layout.gap !== 0) css.gap = layout.gap;
|
|
458
|
-
|
|
459
|
-
const am: Record<string, string> = { start:'flex-start', end:'flex-end', center:'center', 'space-between':'space-between', 'space-around':'space-around', 'space-evenly':'space-evenly' };
|
|
460
|
-
css.alignItems = am[layout.align_items] || 'stretch';
|
|
461
|
-
css.justifyContent = am[layout.align_content] || 'flex-start';
|
|
462
|
-
|
|
463
|
-
if (layout.padding !== undefined) {
|
|
464
|
-
css.padding = Array.isArray(layout.padding) ? layout.padding.map((p:number)=>p+'px').join(' ') : layout.padding !== 0 ? layout.padding+'px' : undefined;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
if (opts?.isSurfaceRoot) {
|
|
468
|
-
css.width = '100%'; css.minHeight = '100vh';
|
|
469
|
-
} else {
|
|
470
|
-
if (layout.sizing_h === 'FILL') css.width = '100%';
|
|
471
|
-
else if (layout.sizing_h === 'FIXED' && style?.width !== undefined) css.width = style.width;
|
|
472
|
-
if (layout.sizing_v === 'FILL') css.height = '100%';
|
|
473
|
-
else if (layout.sizing_v === 'FIXED' && style?.height !== undefined) css.height = style.height;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (style) {
|
|
478
|
-
const bgColor = resolveColor(style.bg_color_token, style.bg_color);
|
|
479
|
-
if (bgColor) { if (opts?.isText) css.color = bgColor; else css.backgroundColor = bgColor; }
|
|
480
|
-
const bc = resolveColor(style.border_color_token, style.border_color);
|
|
481
|
-
if (bc) css.borderColor = bc;
|
|
482
|
-
if (style.border_width !== undefined) { css.borderWidth = style.border_width; css.borderStyle = 'solid'; }
|
|
483
|
-
if (style.border_radius !== undefined) {
|
|
484
|
-
css.borderRadius = Array.isArray(style.border_radius) ? style.border_radius.map((r:number)=>r+'px').join(' ') : style.border_radius;
|
|
485
|
-
}
|
|
486
|
-
if (!layout && !opts?.isText) { if (style.width !== undefined) css.width = style.width; if (style.height !== undefined) css.height = style.height; }
|
|
487
|
-
if (style.opacity !== undefined) css.opacity = style.opacity;
|
|
488
|
-
if (style.font_size) css.fontSize = style.font_size;
|
|
489
|
-
if (style.font_weight) css.fontWeight = style.font_weight;
|
|
490
|
-
if (style.font_family) css.fontFamily = style.font_family;
|
|
491
|
-
if (style.letter_spacing) css.letterSpacing = style.letter_spacing;
|
|
492
|
-
if (style.line_height) css.lineHeight = style.line_height + 'px';
|
|
493
|
-
if (style.text_align) css.textAlign = style.text_align;
|
|
494
|
-
if (style.text_decoration) css.textDecoration = style.text_decoration;
|
|
495
|
-
if (style.text_transform) css.textTransform = style.text_transform;
|
|
496
|
-
}
|
|
497
|
-
return css;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
475
|
const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
|
|
501
476
|
if (!fonts || fonts.length === 0) return null;
|
|
502
477
|
|
|
@@ -520,33 +495,44 @@ const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
|
|
|
520
495
|
};
|
|
521
496
|
|
|
522
497
|
const SurfaceComponent = (props: any) => (
|
|
523
|
-
<div style={
|
|
498
|
+
<div style={props.parsedStyle}>
|
|
524
499
|
<DynamicFontLoader fonts={props.metadata?.fonts} />
|
|
525
500
|
{props.children}
|
|
526
501
|
</div>
|
|
527
502
|
);
|
|
528
503
|
|
|
529
504
|
const GenericContainer = (props: any) => (
|
|
530
|
-
<div style={
|
|
505
|
+
<div style={props.parsedStyle}>{props.children}</div>
|
|
531
506
|
);
|
|
532
507
|
|
|
533
508
|
const TextComponent = (props: any) => (
|
|
534
|
-
<span style={
|
|
509
|
+
<span style={props.parsedStyle}>{props.content || props.text || props.name}</span>
|
|
535
510
|
);
|
|
536
511
|
|
|
537
512
|
const ButtonComponent = (props: any) => {
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (props.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (props.text_style.font_weight) css.fontWeight = props.text_style.font_weight;
|
|
546
|
-
if (props.text_style.font_family) css.fontFamily = props.text_style.font_family;
|
|
547
|
-
if (props.text_style.text_align) css.textAlign = props.text_style.text_align;
|
|
513
|
+
const css: any = { ...props.parsedStyle, cursor: 'pointer', border: 'none' };
|
|
514
|
+
|
|
515
|
+
// If no layout, center text by default (most buttons center their text)
|
|
516
|
+
if (!props.layout) {
|
|
517
|
+
css.display = 'flex';
|
|
518
|
+
css.alignItems = 'center';
|
|
519
|
+
css.justifyContent = 'center';
|
|
548
520
|
}
|
|
549
|
-
|
|
521
|
+
|
|
522
|
+
const textStyle = props.style?.text_style || {};
|
|
523
|
+
|
|
524
|
+
return (
|
|
525
|
+
<button style={css}>
|
|
526
|
+
<span style={{
|
|
527
|
+
fontFamily: textStyle.font_family,
|
|
528
|
+
fontSize: textStyle.font_size,
|
|
529
|
+
fontWeight: textStyle.font_weight,
|
|
530
|
+
color: textStyle.text_color || textStyle.bg_color // Figma plugin often maps text fill to bg_color
|
|
531
|
+
}}>
|
|
532
|
+
{props.children || props.content || props.text || props.name}
|
|
533
|
+
</span>
|
|
534
|
+
</button>
|
|
535
|
+
);
|
|
550
536
|
};
|
|
551
537
|
|
|
552
538
|
export const registry: Record<string, React.FC<any>> = {
|
|
@@ -556,11 +542,18 @@ export const registry: Record<string, React.FC<any>> = {
|
|
|
556
542
|
element: GenericContainer,
|
|
557
543
|
group: GenericContainer,
|
|
558
544
|
stack: GenericContainer,
|
|
545
|
+
Stack: GenericContainer,
|
|
546
|
+
Card: GenericContainer,
|
|
547
|
+
Grid: GenericContainer,
|
|
548
|
+
Header: GenericContainer,
|
|
549
|
+
Footer: GenericContainer,
|
|
559
550
|
text: TextComponent,
|
|
560
551
|
Text: TextComponent,
|
|
561
552
|
button: ButtonComponent,
|
|
562
|
-
|
|
563
|
-
|
|
553
|
+
Button: ButtonComponent,
|
|
554
|
+
input: (props: any) => <input style={props.parsedStyle} placeholder={props.content || props.name} />,
|
|
555
|
+
icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āļø</div>,
|
|
556
|
+
Icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āļø</div>,
|
|
564
557
|
};
|
|
565
558
|
`.trim());
|
|
566
559
|
|
|
@@ -578,6 +571,77 @@ export const registry: Record<string, React.FC<any>> = {
|
|
|
578
571
|
}
|
|
579
572
|
} // End if(!package.json)
|
|
580
573
|
|
|
574
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
575
|
+
// AUTO-PILOT: npm install + npm run dev + browser open
|
|
576
|
+
// Zero-touch DX ā developer never leaves Figma
|
|
577
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
578
|
+
const nodeModulesPath = path.join(workspaceDir, "node_modules");
|
|
579
|
+
const devServerPort = 3001;
|
|
580
|
+
|
|
581
|
+
const isPortInUse = (port: number): Promise<boolean> => {
|
|
582
|
+
return new Promise((resolve) => {
|
|
583
|
+
const net = require("net");
|
|
584
|
+
const tester = net.createServer()
|
|
585
|
+
.once("error", (err: any) => {
|
|
586
|
+
if (err.code === "EADDRINUSE") resolve(true);
|
|
587
|
+
else resolve(false);
|
|
588
|
+
})
|
|
589
|
+
.once("listening", () => {
|
|
590
|
+
tester.close(() => resolve(false));
|
|
591
|
+
})
|
|
592
|
+
.listen(port);
|
|
593
|
+
});
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
const runNpmInstallIfNeeded = async () => {
|
|
597
|
+
if (fs.existsSync(nodeModulesPath)) return;
|
|
598
|
+
|
|
599
|
+
console.log(chalk.blue(`\nš¦ Installing dependencies...`));
|
|
600
|
+
const { execSync } = require("child_process");
|
|
601
|
+
try {
|
|
602
|
+
execSync("npm install", { cwd: workspaceDir, stdio: "inherit" });
|
|
603
|
+
console.log(chalk.green(`ā
Dependencies installed.`));
|
|
604
|
+
} catch (e: any) {
|
|
605
|
+
console.error(chalk.red(`ā npm install failed: ${e.message}`));
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const startDevServerIfNeeded = async () => {
|
|
610
|
+
const portBusy = await isPortInUse(devServerPort);
|
|
611
|
+
if (portBusy) {
|
|
612
|
+
console.log(chalk.gray(` Dev server already running on port ${devServerPort}.`));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.log(chalk.blue(`\nš Starting dev server on port ${devServerPort}...`));
|
|
617
|
+
const { spawn } = require("child_process");
|
|
618
|
+
const child = spawn("npm", ["run", "dev"], {
|
|
619
|
+
cwd: workspaceDir,
|
|
620
|
+
detached: true,
|
|
621
|
+
stdio: "ignore"
|
|
622
|
+
});
|
|
623
|
+
child.unref();
|
|
624
|
+
console.log(chalk.green(`ā
Dev server launched (PID: ${child.pid}).`));
|
|
625
|
+
|
|
626
|
+
// Wait a bit then open browser
|
|
627
|
+
setTimeout(() => {
|
|
628
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
629
|
+
const { exec } = require("child_process");
|
|
630
|
+
exec(`${openCmd} http://localhost:${devServerPort}`);
|
|
631
|
+
console.log(chalk.cyan(`š Browser opened: http://localhost:${devServerPort}`));
|
|
632
|
+
}, 3000);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// Fire and forget ā don't block the HTTP response
|
|
636
|
+
(async () => {
|
|
637
|
+
try {
|
|
638
|
+
await runNpmInstallIfNeeded();
|
|
639
|
+
await startDevServerIfNeeded();
|
|
640
|
+
} catch (e: any) {
|
|
641
|
+
console.error(chalk.yellow(`ā ļø Auto-pilot warning: ${e.message}`));
|
|
642
|
+
}
|
|
643
|
+
})();
|
|
644
|
+
|
|
581
645
|
res.status(200).json({ success: true, message: `Successfully saved ${savedCount} files.` });
|
|
582
646
|
} catch (error: any) {
|
|
583
647
|
console.error(chalk.red("ā Sync Error:"), error);
|