@lerret/cli 0.1.10 → 0.1.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/dist-studio/.bundle-stamp +1 -1
- package/package.json +1 -1
- package/src/export.js +174 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lerret/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "The `lerret` design canvas CLI — a folder of plain React component files renders as a visual canvas. Includes the Vite dev server, headless export, and the bundled studio.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/export.js
CHANGED
|
@@ -1125,6 +1125,53 @@ function formatLocation(segments) {
|
|
|
1125
1125
|
return segments.length === 0 ? '(top)' : segments.join('/');
|
|
1126
1126
|
}
|
|
1127
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* Group expanded artboard entries by their `pagePath`, preserving first-seen
|
|
1130
|
+
* order. Each entry is paired with its pre-computed base filename so the
|
|
1131
|
+
* downstream capture loop doesn't have to re-derive names.
|
|
1132
|
+
*
|
|
1133
|
+
* The studio renders one project-page at a time, so the CLI navigates the
|
|
1134
|
+
* URL hash to each page once and captures every artboard on that page before
|
|
1135
|
+
* moving on. The returned order is whatever order pages first appear in
|
|
1136
|
+
* `expanded` — which mirrors `collectArtboards`'s model-walk order (already
|
|
1137
|
+
* alphabetical, so a project's page-visit order is deterministic).
|
|
1138
|
+
*
|
|
1139
|
+
* @param {Array<{ artboard: object, variantName: string | undefined, domId: string }>} expanded
|
|
1140
|
+
* @param {string[]} baseFilenames Same length as `expanded`; the precomputed
|
|
1141
|
+
* filename for each entry.
|
|
1142
|
+
* @returns {Array<{
|
|
1143
|
+
* pagePath: string | null,
|
|
1144
|
+
* entries: Array<{ artboard: object, variantName: string | undefined, domId: string, filename: string }>
|
|
1145
|
+
* }>}
|
|
1146
|
+
* One group per distinct `pagePath`. `pagePath` is `null` only for hand-
|
|
1147
|
+
* crafted artboards in tests that omit the field — the orchestrator skips
|
|
1148
|
+
* navigation for that bucket and uses whatever the studio is showing.
|
|
1149
|
+
*/
|
|
1150
|
+
export function groupEntriesByPage(expanded, baseFilenames) {
|
|
1151
|
+
/** @type {Map<string, Array<{ artboard: object, variantName: string | undefined, domId: string, filename: string }>>} */
|
|
1152
|
+
const byPath = new Map();
|
|
1153
|
+
const NULL_KEY = '__lerret_no_page__';
|
|
1154
|
+
for (let i = 0; i < expanded.length; i++) {
|
|
1155
|
+
const entry = expanded[i];
|
|
1156
|
+
const pagePath =
|
|
1157
|
+
entry.artboard && typeof entry.artboard.pagePath === 'string'
|
|
1158
|
+
? entry.artboard.pagePath
|
|
1159
|
+
: null;
|
|
1160
|
+
const key = pagePath === null ? NULL_KEY : pagePath;
|
|
1161
|
+
const paired = { ...entry, filename: baseFilenames[i] };
|
|
1162
|
+
const bucket = byPath.get(key);
|
|
1163
|
+
if (bucket) {
|
|
1164
|
+
bucket.push(paired);
|
|
1165
|
+
} else {
|
|
1166
|
+
byPath.set(key, [paired]);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return [...byPath.entries()].map(([key, entries]) => ({
|
|
1170
|
+
pagePath: key === NULL_KEY ? null : key,
|
|
1171
|
+
entries,
|
|
1172
|
+
}));
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1128
1175
|
/**
|
|
1129
1176
|
* Run `@lerret/cli export`. Resolves the scope, boots Vite + Chromium, captures
|
|
1130
1177
|
* each artboard, writes the result to disk, and returns an exit code.
|
|
@@ -1288,29 +1335,23 @@ export async function runExport(argv, deps = {}) {
|
|
|
1288
1335
|
|
|
1289
1336
|
// Open a single page and navigate to the studio. We use one page for the
|
|
1290
1337
|
// whole run — captureArtboard is independent per artboard and the studio
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1338
|
+
// pages share the same Vite/HMR session. A fresh page per artboard would
|
|
1339
|
+
// re-bundle / re-fetch fonts each time.
|
|
1340
|
+
//
|
|
1341
|
+
// The studio renders ONE project-page at a time (the dock's page picker
|
|
1342
|
+
// drives `ProjectStudio`'s hash route — see `packages/studio/src/project-
|
|
1343
|
+
// studio.jsx`). To capture artboards across every project page we group
|
|
1344
|
+
// by `pagePath`, navigate the URL hash to each page, wait for its first
|
|
1345
|
+
// slot to attach, then capture all of that page's artboards before
|
|
1346
|
+
// moving on. The first hash-set fires `hashchange` even when it matches
|
|
1347
|
+
// the default page, which keeps the navigation predictable.
|
|
1293
1348
|
const context = await browser.newContext();
|
|
1294
1349
|
const page = await context.newPage();
|
|
1295
1350
|
await page.goto(url, { waitUntil: 'load', timeout: 60000 });
|
|
1296
1351
|
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
// surface that as a fatal error rather than silently producing zero
|
|
1301
|
-
// captures.
|
|
1302
|
-
const firstSelector = ARTBOARD_SELECTORS.slotByDataAttr(expanded[0].domId);
|
|
1303
|
-
try {
|
|
1304
|
-
await page.waitForSelector(firstSelector, { state: 'attached', timeout: 30000 });
|
|
1305
|
-
} catch (err) {
|
|
1306
|
-
process.stderr.write(
|
|
1307
|
-
`@lerret/cli export: studio did not render any artboards within 30s ` +
|
|
1308
|
-
`(${err && err.message ? err.message : String(err)}). The project may be empty or failed to load.\n`,
|
|
1309
|
-
);
|
|
1310
|
-
return 1;
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// 5. Build name counts for flat-mode disambiguation.
|
|
1352
|
+
// 5. Build name counts for flat-mode disambiguation. These are computed
|
|
1353
|
+
// across the WHOLE run so collisions are detected even when colliding
|
|
1354
|
+
// artboards live on different pages.
|
|
1314
1355
|
/** @type {Map<string, number>} */
|
|
1315
1356
|
const nameCounts = new Map();
|
|
1316
1357
|
const baseFilenames = expanded.map((entry) =>
|
|
@@ -1320,73 +1361,132 @@ export async function runExport(argv, deps = {}) {
|
|
|
1320
1361
|
nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
|
|
1321
1362
|
}
|
|
1322
1363
|
|
|
1323
|
-
// 6.
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1364
|
+
// 6. Group entries by `pagePath` so we can navigate the studio to each
|
|
1365
|
+
// page once. Insertion order matters — `collectArtboards` walks pages
|
|
1366
|
+
// in model order (alphabetical by the loader), so the resulting page
|
|
1367
|
+
// visit order is deterministic.
|
|
1368
|
+
const pageGroups = groupEntriesByPage(expanded, baseFilenames);
|
|
1369
|
+
|
|
1370
|
+
// 7. Capture each artboard, one page-batch at a time. Failures are
|
|
1371
|
+
// isolated — a single bad capture is logged and the run continues.
|
|
1372
|
+
// Unembedded fonts are aggregated across the run.
|
|
1326
1373
|
const allUnembeddedFonts = new Set();
|
|
1327
1374
|
/** @type {Array<{ artboard: object, reason: string }>} */
|
|
1328
1375
|
const failures = [];
|
|
1329
1376
|
let writtenCount = 0;
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
failures.push({ artboard: entry.artboard, reason });
|
|
1361
|
-
continue;
|
|
1377
|
+
let runIndex = 0;
|
|
1378
|
+
|
|
1379
|
+
for (const group of pageGroups) {
|
|
1380
|
+
// Navigate the studio to this page via the hash. `ProjectStudio`'s
|
|
1381
|
+
// `useHashRoute` listens on `hashchange`, so setting `location.hash`
|
|
1382
|
+
// re-renders `ProjectCanvas` for the matching page. When `pagePath` is
|
|
1383
|
+
// null (test artboards without a page hint) we skip navigation —
|
|
1384
|
+
// whatever the studio is currently showing serves the capture.
|
|
1385
|
+
if (group.pagePath !== null) {
|
|
1386
|
+
try {
|
|
1387
|
+
await page.evaluate((p) => {
|
|
1388
|
+
// eslint-disable-next-line no-undef
|
|
1389
|
+
window.location.hash = '#' + p;
|
|
1390
|
+
}, group.pagePath);
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
// A navigation failure for this page is fatal for the page batch
|
|
1393
|
+
// but not for the run — log every entry in this group as failed
|
|
1394
|
+
// and move on. We don't return 1 because other pages may succeed.
|
|
1395
|
+
const reason =
|
|
1396
|
+
`could not navigate to page ${group.pagePath}: ` +
|
|
1397
|
+
`${err && err.message ? err.message : String(err)}`;
|
|
1398
|
+
for (const entry of group.entries) {
|
|
1399
|
+
runIndex++;
|
|
1400
|
+
const human = `${runIndex}/${expanded.length}`;
|
|
1401
|
+
const label = `${formatLocation(entry.artboard.locationSegments)}/${entry.artboard.asset.name}${entry.variantName ? `#${entry.variantName}` : ''}`;
|
|
1402
|
+
process.stderr.write(`[${human}] FAILED ${label}: ${reason}\n`);
|
|
1403
|
+
failures.push({ artboard: entry.artboard, reason });
|
|
1404
|
+
}
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1362
1407
|
}
|
|
1363
1408
|
|
|
1364
|
-
//
|
|
1365
|
-
|
|
1409
|
+
// Wait for the first slot on this page to attach. If the studio fails
|
|
1410
|
+
// to render this page's artboards within the timeout, log every entry
|
|
1411
|
+
// in the batch as failed (with a clear reason) and move to the next
|
|
1412
|
+
// page rather than aborting — partial output is more useful than zero.
|
|
1413
|
+
const firstSelector = ARTBOARD_SELECTORS.slotByDataAttr(group.entries[0].domId);
|
|
1366
1414
|
try {
|
|
1367
|
-
|
|
1368
|
-
bytes = new Uint8Array(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
1415
|
+
await page.waitForSelector(firstSelector, { state: 'attached', timeout: 30000 });
|
|
1369
1416
|
} catch (err) {
|
|
1370
|
-
const reason =
|
|
1371
|
-
|
|
1372
|
-
|
|
1417
|
+
const reason =
|
|
1418
|
+
`studio did not render page ${group.pagePath || '(default)'} within 30s ` +
|
|
1419
|
+
`(${err && err.message ? err.message : String(err)})`;
|
|
1420
|
+
for (const entry of group.entries) {
|
|
1421
|
+
runIndex++;
|
|
1422
|
+
const human = `${runIndex}/${expanded.length}`;
|
|
1423
|
+
const label = `${formatLocation(entry.artboard.locationSegments)}/${entry.artboard.asset.name}${entry.variantName ? `#${entry.variantName}` : ''}`;
|
|
1424
|
+
process.stderr.write(`[${human}] FAILED ${label}: ${reason}\n`);
|
|
1425
|
+
failures.push({ artboard: entry.artboard, reason });
|
|
1426
|
+
}
|
|
1373
1427
|
continue;
|
|
1374
1428
|
}
|
|
1375
1429
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1430
|
+
for (const entry of group.entries) {
|
|
1431
|
+
runIndex++;
|
|
1432
|
+
const filename = entry.filename;
|
|
1433
|
+
const nameCount = nameCounts.get(filename) ?? 1;
|
|
1434
|
+
const outputPath = buildOutputPath({
|
|
1435
|
+
outDir: outDirAbs,
|
|
1436
|
+
artboard: entry.artboard,
|
|
1437
|
+
filename,
|
|
1438
|
+
flat: flags.flat,
|
|
1439
|
+
nameCount,
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
const human = `${runIndex}/${expanded.length}`;
|
|
1443
|
+
const label = `${formatLocation(entry.artboard.locationSegments)}/${entry.artboard.asset.name}${entry.variantName ? `#${entry.variantName}` : ''}`;
|
|
1444
|
+
process.stdout.write(`[${human}] capturing ${label}\n`);
|
|
1445
|
+
|
|
1446
|
+
let result;
|
|
1447
|
+
try {
|
|
1448
|
+
result = await captureInPage(page, entry.domId, flags.format);
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
result = {
|
|
1451
|
+
ok: false,
|
|
1452
|
+
error: err && err.message ? err.message : String(err),
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
if (!result || !result.ok) {
|
|
1457
|
+
const reason = (result && result.error) || 'unknown capture failure';
|
|
1458
|
+
process.stderr.write(`[${human}] FAILED ${label}: ${reason}\n`);
|
|
1459
|
+
failures.push({ artboard: entry.artboard, reason });
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1387
1462
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1463
|
+
// Decode base64 → Uint8Array and write the bytes to disk.
|
|
1464
|
+
let bytes;
|
|
1465
|
+
try {
|
|
1466
|
+
const binary = Buffer.from(result.bytesB64, 'base64');
|
|
1467
|
+
bytes = new Uint8Array(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
const reason = `failed to decode capture bytes: ${err && err.message ? err.message : String(err)}`;
|
|
1470
|
+
process.stderr.write(`[${human}] FAILED ${label}: ${reason}\n`);
|
|
1471
|
+
failures.push({ artboard: entry.artboard, reason });
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
try {
|
|
1476
|
+
await ensureDirFn(toLerretPath(dirname(outputPath)));
|
|
1477
|
+
await writeBinary(outputPath, bytes);
|
|
1478
|
+
writtenCount++;
|
|
1479
|
+
process.stdout.write(`[${human}] wrote ${outputPath}\n`);
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
const reason = `write failed: ${err && err.message ? err.message : String(err)}`;
|
|
1482
|
+
process.stderr.write(`[${human}] FAILED ${label}: ${reason}\n`);
|
|
1483
|
+
failures.push({ artboard: entry.artboard, reason });
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
for (const font of result.unembeddedFonts || []) {
|
|
1488
|
+
allUnembeddedFonts.add(font);
|
|
1489
|
+
}
|
|
1390
1490
|
}
|
|
1391
1491
|
}
|
|
1392
1492
|
|