foss-design 0.9.5 → 0.9.7
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/cli.js +629 -281
- package/dist/runtime/index.js +54 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import
|
|
4
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
5
|
+
import fs37 from "fs";
|
|
6
|
+
import path35 from "path";
|
|
5
7
|
import { parseArgs } from "util";
|
|
6
8
|
|
|
7
9
|
// src/core/paths.ts
|
|
@@ -346,8 +348,8 @@ function routeError(pattern) {
|
|
|
346
348
|
}
|
|
347
349
|
return null;
|
|
348
350
|
}
|
|
349
|
-
function split(
|
|
350
|
-
return
|
|
351
|
+
function split(path36) {
|
|
352
|
+
return path36.split("/").filter(Boolean);
|
|
351
353
|
}
|
|
352
354
|
function compileRoute(pattern) {
|
|
353
355
|
return {
|
|
@@ -366,8 +368,8 @@ function decode2(part) {
|
|
|
366
368
|
return part;
|
|
367
369
|
}
|
|
368
370
|
}
|
|
369
|
-
function scoreRoute(route2,
|
|
370
|
-
const parts = split(
|
|
371
|
+
function scoreRoute(route2, path36) {
|
|
372
|
+
const parts = split(path36.split(/[?#]/)[0] ?? "").map(decode2);
|
|
371
373
|
const walk2 = (s, p) => {
|
|
372
374
|
if (s === route2.segments.length) return p === parts.length ? 0 : null;
|
|
373
375
|
const segment = route2.segments[s];
|
|
@@ -385,11 +387,11 @@ function scoreRoute(route2, path35) {
|
|
|
385
387
|
};
|
|
386
388
|
return walk2(0, 0);
|
|
387
389
|
}
|
|
388
|
-
function matchRoute(entries,
|
|
390
|
+
function matchRoute(entries, path36) {
|
|
389
391
|
let best = null;
|
|
390
392
|
for (const entry of entries) {
|
|
391
393
|
for (const route2 of entry.routes) {
|
|
392
|
-
const score = scoreRoute(route2,
|
|
394
|
+
const score = scoreRoute(route2, path36);
|
|
393
395
|
if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
|
|
394
396
|
}
|
|
395
397
|
}
|
|
@@ -516,9 +518,9 @@ var CanvasSchema = z.strictObject({
|
|
|
516
518
|
pages: z.array(z.unknown()).min(1, "add at least one page")
|
|
517
519
|
});
|
|
518
520
|
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
519
|
-
function formatPath(base,
|
|
521
|
+
function formatPath(base, path36) {
|
|
520
522
|
let out = base;
|
|
521
|
-
for (const part of
|
|
523
|
+
for (const part of path36) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
|
|
522
524
|
return out;
|
|
523
525
|
}
|
|
524
526
|
function describeZodError(error, base = "") {
|
|
@@ -2632,7 +2634,7 @@ var CloudClient = class {
|
|
|
2632
2634
|
}
|
|
2633
2635
|
host;
|
|
2634
2636
|
token;
|
|
2635
|
-
async send(method,
|
|
2637
|
+
async send(method, path36, init) {
|
|
2636
2638
|
const headers = {
|
|
2637
2639
|
"User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
|
|
2638
2640
|
Accept: "application/json"
|
|
@@ -2640,7 +2642,7 @@ var CloudClient = class {
|
|
|
2640
2642
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
2641
2643
|
if (init.type) headers["Content-Type"] = init.type;
|
|
2642
2644
|
try {
|
|
2643
|
-
return await fetch(`${this.host}/api${
|
|
2645
|
+
return await fetch(`${this.host}/api${path36}`, {
|
|
2644
2646
|
method,
|
|
2645
2647
|
headers,
|
|
2646
2648
|
body: init.body,
|
|
@@ -2661,8 +2663,8 @@ var CloudClient = class {
|
|
|
2661
2663
|
throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
|
|
2662
2664
|
}
|
|
2663
2665
|
/** JSON in, JSON out; any non-2xx answer throws a CloudError. */
|
|
2664
|
-
async request(method,
|
|
2665
|
-
const response = await this.send(method,
|
|
2666
|
+
async request(method, path36, body) {
|
|
2667
|
+
const response = await this.send(method, path36, {
|
|
2666
2668
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
2667
2669
|
type: body === void 0 ? void 0 : "application/json",
|
|
2668
2670
|
timeout: JSON_TIMEOUT
|
|
@@ -2674,7 +2676,7 @@ var CloudClient = class {
|
|
|
2674
2676
|
try {
|
|
2675
2677
|
return JSON.parse(text2);
|
|
2676
2678
|
} catch {
|
|
2677
|
-
throw new CloudError(`${this.host} answered ${method} ${
|
|
2679
|
+
throw new CloudError(`${this.host} answered ${method} ${path36} with something other than JSON`, 0, "bad_response");
|
|
2678
2680
|
}
|
|
2679
2681
|
}
|
|
2680
2682
|
/** POST /device/code. */
|
|
@@ -3027,7 +3029,8 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
|
|
|
3027
3029
|
var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
|
|
3028
3030
|
|
|
3029
3031
|
// src/capture/cover.ts
|
|
3030
|
-
import
|
|
3032
|
+
import fs20 from "fs";
|
|
3033
|
+
import os3 from "os";
|
|
3031
3034
|
|
|
3032
3035
|
// src/server/snapshots.ts
|
|
3033
3036
|
import fs17 from "fs";
|
|
@@ -3085,11 +3088,15 @@ var SnapshotStore = class {
|
|
|
3085
3088
|
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
|
|
3086
3089
|
const dir = path17.join(this.dir, canvas);
|
|
3087
3090
|
fs17.mkdirSync(dir, { recursive: true });
|
|
3088
|
-
|
|
3091
|
+
const file = path17.join(dir, `${id2}.${theme2}.png`);
|
|
3092
|
+
fs17.writeFileSync(`${file}.${process.pid}.tmp`, png);
|
|
3093
|
+
fs17.renameSync(`${file}.${process.pid}.tmp`, file);
|
|
3089
3094
|
const data = this.load(canvas);
|
|
3090
3095
|
const version = Date.now();
|
|
3091
3096
|
data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
|
|
3092
|
-
|
|
3097
|
+
const temp = `${this.metaFile(canvas)}.${process.pid}.tmp`;
|
|
3098
|
+
fs17.writeFileSync(temp, JSON.stringify(data));
|
|
3099
|
+
fs17.renameSync(temp, this.metaFile(canvas));
|
|
3093
3100
|
return { url: this.url(canvas, id2, theme2, version) };
|
|
3094
3101
|
}
|
|
3095
3102
|
/** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
|
|
@@ -3102,8 +3109,54 @@ var SnapshotStore = class {
|
|
|
3102
3109
|
}
|
|
3103
3110
|
};
|
|
3104
3111
|
|
|
3105
|
-
// src/capture/
|
|
3112
|
+
// src/capture/build-server.ts
|
|
3106
3113
|
import fs18 from "fs";
|
|
3114
|
+
import http from "http";
|
|
3115
|
+
import path18 from "path";
|
|
3116
|
+
import sirv from "sirv";
|
|
3117
|
+
async function serveBuild(paths, name, options) {
|
|
3118
|
+
const out = path18.join(paths.cache, name);
|
|
3119
|
+
fs18.rmSync(out, { recursive: true, force: true });
|
|
3120
|
+
await buildSite(paths, out, {
|
|
3121
|
+
canvases: options.canvases,
|
|
3122
|
+
includeSystem: options.includeSystem,
|
|
3123
|
+
includeViewer: false
|
|
3124
|
+
});
|
|
3125
|
+
const files = sirv(out, { dev: true });
|
|
3126
|
+
const server = http.createServer((req, res) => {
|
|
3127
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3128
|
+
const segments = url.pathname.split("/").map((segment) => {
|
|
3129
|
+
try {
|
|
3130
|
+
return decodeURIComponent(segment);
|
|
3131
|
+
} catch {
|
|
3132
|
+
return segment;
|
|
3133
|
+
}
|
|
3134
|
+
});
|
|
3135
|
+
req.url = segments.join("/") + url.search;
|
|
3136
|
+
files(req, res, () => {
|
|
3137
|
+
res.statusCode = 404;
|
|
3138
|
+
res.end("Not found");
|
|
3139
|
+
});
|
|
3140
|
+
});
|
|
3141
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
3142
|
+
return {
|
|
3143
|
+
base: `http://127.0.0.1:${server.address().port}`,
|
|
3144
|
+
read(rel) {
|
|
3145
|
+
try {
|
|
3146
|
+
return JSON.parse(fs18.readFileSync(path18.join(out, rel), "utf8"));
|
|
3147
|
+
} catch {
|
|
3148
|
+
return null;
|
|
3149
|
+
}
|
|
3150
|
+
},
|
|
3151
|
+
close: () => new Promise((resolve) => {
|
|
3152
|
+
server.closeAllConnections();
|
|
3153
|
+
server.close(() => resolve());
|
|
3154
|
+
})
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
// src/capture/chrome.ts
|
|
3159
|
+
import fs19 from "fs";
|
|
3107
3160
|
var CANDIDATES = {
|
|
3108
3161
|
darwin: [
|
|
3109
3162
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -3127,8 +3180,8 @@ var CANDIDATES = {
|
|
|
3127
3180
|
};
|
|
3128
3181
|
function findChrome() {
|
|
3129
3182
|
const configured = process.env.DESIGN_CHROME;
|
|
3130
|
-
if (configured &&
|
|
3131
|
-
for (const candidate of CANDIDATES[process.platform] ?? []) if (
|
|
3183
|
+
if (configured && fs19.existsSync(configured)) return configured;
|
|
3184
|
+
for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
|
|
3132
3185
|
return null;
|
|
3133
3186
|
}
|
|
3134
3187
|
async function launchChrome() {
|
|
@@ -3159,6 +3212,8 @@ async function openFrame(page, url, options = {}) {
|
|
|
3159
3212
|
record2(text2);
|
|
3160
3213
|
});
|
|
3161
3214
|
let ready = false;
|
|
3215
|
+
let loadedAt = 0;
|
|
3216
|
+
let readyMs;
|
|
3162
3217
|
let reloads = 0;
|
|
3163
3218
|
const onNavigate = () => {
|
|
3164
3219
|
reloads++;
|
|
@@ -3171,6 +3226,7 @@ async function openFrame(page, url, options = {}) {
|
|
|
3171
3226
|
outdated = () => resolve("outdated");
|
|
3172
3227
|
});
|
|
3173
3228
|
await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
|
|
3229
|
+
loadedAt = Date.now();
|
|
3174
3230
|
page.on("load", onNavigate);
|
|
3175
3231
|
if (!ready) {
|
|
3176
3232
|
const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
|
|
@@ -3179,6 +3235,7 @@ async function openFrame(page, url, options = {}) {
|
|
|
3179
3235
|
signal.catch(() => {
|
|
3180
3236
|
});
|
|
3181
3237
|
ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
|
|
3238
|
+
if (ready) readyMs = Date.now() - loadedAt;
|
|
3182
3239
|
}
|
|
3183
3240
|
if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
|
|
3184
3241
|
page.off("load", onNavigate);
|
|
@@ -3196,7 +3253,12 @@ async function openFrame(page, url, options = {}) {
|
|
|
3196
3253
|
const unique2 = errors.filter(
|
|
3197
3254
|
(error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
|
|
3198
3255
|
);
|
|
3199
|
-
return {
|
|
3256
|
+
return {
|
|
3257
|
+
errors: [...new Set(unique2)],
|
|
3258
|
+
ready,
|
|
3259
|
+
...held ? { held } : {},
|
|
3260
|
+
...readyMs !== void 0 ? { readyMs } : {}
|
|
3261
|
+
};
|
|
3200
3262
|
}
|
|
3201
3263
|
|
|
3202
3264
|
// src/capture/frames.ts
|
|
@@ -3273,121 +3335,168 @@ async function mapLimit(items, limit, fn) {
|
|
|
3273
3335
|
|
|
3274
3336
|
// src/capture/cover.ts
|
|
3275
3337
|
var SNAPSHOT_WIDTH = 900;
|
|
3276
|
-
|
|
3338
|
+
var TABS = Math.min(8, Math.max(2, os3.availableParallelism() - 1));
|
|
3339
|
+
var THEMES2 = ["light", "dark"];
|
|
3340
|
+
function staleSnapshots(paths, canvasIds, filter = {}) {
|
|
3277
3341
|
const store = new SnapshotStore(paths.snapshots);
|
|
3278
3342
|
const project = new DesignProject(paths, DEV_URLS, store.lookup);
|
|
3279
3343
|
const stale = [];
|
|
3280
3344
|
for (const id2 of canvasIds) {
|
|
3281
3345
|
const canvas = project.canvas(id2);
|
|
3282
|
-
if (!canvas
|
|
3283
|
-
const
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3346
|
+
if (!canvas) continue;
|
|
3347
|
+
const inLight = canvasFrames(canvas.doc, "", "light", { page: filter.page, includeUrls: false });
|
|
3348
|
+
const inDark = new Map(
|
|
3349
|
+
canvasFrames(canvas.doc, "", "dark", { page: filter.page, includeUrls: false }).map((job) => [job.id, job.theme])
|
|
3350
|
+
);
|
|
3351
|
+
const files = new Map(canvas.screens.map((screen) => [screen.id, screen.file]));
|
|
3352
|
+
for (const job of inLight) {
|
|
3353
|
+
if (filter.screens && !filter.screens.has(job.id)) continue;
|
|
3354
|
+
const file = files.get(job.id);
|
|
3355
|
+
if (!file || !fs20.existsSync(file)) continue;
|
|
3356
|
+
const changed = fs20.statSync(file).mtimeMs;
|
|
3357
|
+
const pinned = inDark.get(job.id) === job.theme;
|
|
3358
|
+
const themes = pinned ? [job.theme] : THEMES2;
|
|
3359
|
+
const screen = { id: job.id };
|
|
3360
|
+
for (const theme2 of themes) {
|
|
3361
|
+
const taken = store.version(id2, screen.id, theme2);
|
|
3362
|
+
if (!taken || taken < changed) stale.push({ canvas: id2, screen: screen.id, theme: theme2 });
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return stale;
|
|
3367
|
+
}
|
|
3368
|
+
async function takeSnapshots(paths, targets, preview, progress2) {
|
|
3369
|
+
if (!targets.length) return [];
|
|
3370
|
+
if (!findChrome()) return ["No Chrome here, so screens keep the snapshots they have."];
|
|
3371
|
+
const store = new SnapshotStore(paths.snapshots);
|
|
3372
|
+
const project = new DesignProject(paths, DEV_URLS, store.lookup);
|
|
3373
|
+
const canvases = [...new Set(targets.map((target) => target.canvas))];
|
|
3374
|
+
progress2.update(`Building ${canvases.join(", ")} to take snapshots from\u2026`);
|
|
3375
|
+
let url;
|
|
3376
|
+
let docOf = (id2) => project.canvas(id2).doc;
|
|
3377
|
+
let stop = async () => {
|
|
3378
|
+
};
|
|
3379
|
+
try {
|
|
3380
|
+
const build2 = await serveBuild(paths, "snapshot-build", { canvases, includeSystem: false });
|
|
3381
|
+
url = build2.base;
|
|
3382
|
+
docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
|
|
3383
|
+
stop = build2.close;
|
|
3384
|
+
} catch {
|
|
3385
|
+
url = await preview();
|
|
3386
|
+
}
|
|
3292
3387
|
const browser = await launchChrome();
|
|
3293
|
-
const
|
|
3388
|
+
const failed = [];
|
|
3389
|
+
const started = Date.now();
|
|
3390
|
+
let done = 0;
|
|
3391
|
+
const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
|
|
3392
|
+
progress2.update(line());
|
|
3294
3393
|
try {
|
|
3295
|
-
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
});
|
|
3303
|
-
try {
|
|
3304
|
-
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
3305
|
-
if (!report.ready) {
|
|
3306
|
-
lines.push(`${canvas}: ${screen} did not finish rendering, so its cover keeps the last snapshot`);
|
|
3307
|
-
continue;
|
|
3308
|
-
}
|
|
3309
|
-
const png = await page.screenshot({ fullPage: job.fullPage });
|
|
3310
|
-
const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
|
|
3311
|
-
store.save(canvas, screen, "light", png, height);
|
|
3312
|
-
} finally {
|
|
3313
|
-
await page.close().catch(() => {
|
|
3394
|
+
await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
|
|
3395
|
+
const job = canvasFrames(docOf(canvas), url, theme2, { id: screen })[0];
|
|
3396
|
+
if (job) {
|
|
3397
|
+
const page = await browser.newPage({
|
|
3398
|
+
viewport: { width: job.width, height: job.height },
|
|
3399
|
+
deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width),
|
|
3400
|
+
colorScheme: theme2
|
|
3314
3401
|
});
|
|
3402
|
+
try {
|
|
3403
|
+
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
3404
|
+
if (report.ready) {
|
|
3405
|
+
const png = await page.screenshot({ fullPage: job.fullPage });
|
|
3406
|
+
const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
|
|
3407
|
+
store.save(canvas, screen, theme2, png, height);
|
|
3408
|
+
} else failed.push(`${canvas}/${screen} (${theme2})`);
|
|
3409
|
+
} catch {
|
|
3410
|
+
failed.push(`${canvas}/${screen} (${theme2})`);
|
|
3411
|
+
} finally {
|
|
3412
|
+
await page.close().catch(() => {
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3315
3415
|
}
|
|
3316
|
-
|
|
3416
|
+
done++;
|
|
3417
|
+
progress2.update(line());
|
|
3418
|
+
});
|
|
3317
3419
|
} finally {
|
|
3318
3420
|
await browser.close();
|
|
3319
|
-
|
|
3320
|
-
|
|
3421
|
+
await stop();
|
|
3422
|
+
}
|
|
3423
|
+
const seconds2 = Math.round((Date.now() - started) / 1e3);
|
|
3424
|
+
progress2.done(`Took ${targets.length - failed.length} snapshot${targets.length === 1 ? "" : "s"} in ${seconds2}s`);
|
|
3425
|
+
if (!failed.length) return [];
|
|
3426
|
+
const shown = failed.slice(0, 5).join(", ");
|
|
3427
|
+
return [
|
|
3428
|
+
`${failed.length} screen${failed.length === 1 ? "" : "s"} did not finish rendering, so they keep their last snapshot: ${shown}${failed.length > 5 ? ", \u2026" : ""}. \`design check --render\` says why.`
|
|
3429
|
+
];
|
|
3321
3430
|
}
|
|
3322
3431
|
|
|
3323
3432
|
// src/cloud/pull.ts
|
|
3324
|
-
import
|
|
3325
|
-
import
|
|
3433
|
+
import fs24 from "fs";
|
|
3434
|
+
import path22 from "path";
|
|
3326
3435
|
|
|
3327
3436
|
// src/cloud/merge.ts
|
|
3328
|
-
import
|
|
3329
|
-
import
|
|
3437
|
+
import fs22 from "fs";
|
|
3438
|
+
import path20 from "path";
|
|
3330
3439
|
import { merge as diff3 } from "node-diff3";
|
|
3331
3440
|
|
|
3332
3441
|
// src/cloud/write.ts
|
|
3333
|
-
import
|
|
3334
|
-
import
|
|
3442
|
+
import fs21 from "fs";
|
|
3443
|
+
import path19 from "path";
|
|
3335
3444
|
function writeInside(root, rel, data) {
|
|
3336
|
-
const file =
|
|
3445
|
+
const file = path19.join(root, rel);
|
|
3337
3446
|
if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
|
|
3338
|
-
|
|
3339
|
-
if (!isInside(
|
|
3447
|
+
fs21.mkdirSync(path19.dirname(file), { recursive: true });
|
|
3448
|
+
if (!isInside(fs21.realpathSync(root), fs21.realpathSync(path19.dirname(file))))
|
|
3340
3449
|
throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
|
|
3341
|
-
if (
|
|
3342
|
-
|
|
3450
|
+
if (fs21.existsSync(file) && fs21.lstatSync(file).isSymbolicLink()) fs21.unlinkSync(file);
|
|
3451
|
+
fs21.writeFileSync(file, data);
|
|
3343
3452
|
}
|
|
3344
3453
|
function pruneEmpty(dir, stop) {
|
|
3345
3454
|
let current = dir;
|
|
3346
3455
|
while (isInside(stop, current) && current !== stop) {
|
|
3347
3456
|
try {
|
|
3348
|
-
if (
|
|
3349
|
-
|
|
3457
|
+
if (fs21.readdirSync(current).length) return;
|
|
3458
|
+
fs21.rmdirSync(current);
|
|
3350
3459
|
} catch {
|
|
3351
3460
|
return;
|
|
3352
3461
|
}
|
|
3353
|
-
current =
|
|
3462
|
+
current = path19.dirname(current);
|
|
3354
3463
|
}
|
|
3355
3464
|
}
|
|
3356
3465
|
|
|
3357
3466
|
// src/cloud/merge.ts
|
|
3358
3467
|
var TEXT = /\.(?:[cm]?[jt]sx?|json|css|scss|html?|mdx?|svg|txt|ya?ml|csv|xml|toml)$/i;
|
|
3359
3468
|
var isText = (rel, ...data) => TEXT.test(rel) && data.every((buffer) => !buffer || !buffer.subarray(0, 8192).includes(0));
|
|
3360
|
-
var mergeDir = (paths, key) =>
|
|
3361
|
-
var stateFile = (paths, key) =>
|
|
3362
|
-
var sideFile = (paths, key, rel, side) =>
|
|
3469
|
+
var mergeDir = (paths, key) => path20.join(cloudCache(paths), "merge", ...key.split("/"));
|
|
3470
|
+
var stateFile = (paths, key) => path20.join(mergeDir(paths, key), "state.json");
|
|
3471
|
+
var sideFile = (paths, key, rel, side) => path20.join(mergeDir(paths, key), side, rel);
|
|
3363
3472
|
function readMergeState(paths, key) {
|
|
3364
3473
|
try {
|
|
3365
|
-
return JSON.parse(
|
|
3474
|
+
return JSON.parse(fs22.readFileSync(stateFile(paths, key), "utf8"));
|
|
3366
3475
|
} catch {
|
|
3367
3476
|
return null;
|
|
3368
3477
|
}
|
|
3369
3478
|
}
|
|
3370
3479
|
function saveState(paths, state) {
|
|
3371
|
-
|
|
3372
|
-
|
|
3480
|
+
fs22.mkdirSync(mergeDir(paths, state.unit), { recursive: true });
|
|
3481
|
+
fs22.writeFileSync(stateFile(paths, state.unit), `${JSON.stringify(state, null, 2)}
|
|
3373
3482
|
`);
|
|
3374
3483
|
}
|
|
3375
3484
|
function mergingUnits(paths) {
|
|
3376
|
-
const root =
|
|
3485
|
+
const root = path20.join(cloudCache(paths), "merge");
|
|
3377
3486
|
const found = [];
|
|
3378
3487
|
const visit = (dir, prefix) => {
|
|
3379
|
-
if (
|
|
3488
|
+
if (fs22.existsSync(path20.join(dir, "state.json"))) {
|
|
3380
3489
|
const state = readMergeState(paths, prefix);
|
|
3381
3490
|
if (state) found.push(state);
|
|
3382
3491
|
return;
|
|
3383
3492
|
}
|
|
3384
3493
|
let entries = [];
|
|
3385
3494
|
try {
|
|
3386
|
-
entries =
|
|
3495
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
3387
3496
|
} catch {
|
|
3388
3497
|
}
|
|
3389
3498
|
for (const entry of entries)
|
|
3390
|
-
if (entry.isDirectory()) visit(
|
|
3499
|
+
if (entry.isDirectory()) visit(path20.join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name);
|
|
3391
3500
|
};
|
|
3392
3501
|
visit(root, "");
|
|
3393
3502
|
return found;
|
|
@@ -3397,7 +3506,7 @@ function openConflicts(paths, state) {
|
|
|
3397
3506
|
return state.conflicts.filter((conflict) => {
|
|
3398
3507
|
if (conflict.kind !== "text") return true;
|
|
3399
3508
|
try {
|
|
3400
|
-
return MARKER.test(
|
|
3509
|
+
return MARKER.test(fs22.readFileSync(path20.join(paths.design, conflict.path), "utf8"));
|
|
3401
3510
|
} catch {
|
|
3402
3511
|
return false;
|
|
3403
3512
|
}
|
|
@@ -3406,19 +3515,19 @@ function openConflicts(paths, state) {
|
|
|
3406
3515
|
function keep(paths, key, rel, side, data) {
|
|
3407
3516
|
if (!data) return;
|
|
3408
3517
|
const file = sideFile(paths, key, rel, side);
|
|
3409
|
-
|
|
3410
|
-
|
|
3518
|
+
fs22.mkdirSync(path20.dirname(file), { recursive: true });
|
|
3519
|
+
fs22.writeFileSync(file, data);
|
|
3411
3520
|
}
|
|
3412
3521
|
function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
|
|
3413
3522
|
const files = { added: 0, changed: 0, removed: 0, merged: 0 };
|
|
3414
3523
|
const conflicts = [];
|
|
3415
|
-
const stop = key === SYSTEM_UNIT ? paths.design :
|
|
3416
|
-
|
|
3417
|
-
const read = (rel) =>
|
|
3524
|
+
const stop = key === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, key));
|
|
3525
|
+
fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
|
|
3526
|
+
const read = (rel) => fs22.readFileSync(path20.join(paths.design, rel));
|
|
3418
3527
|
const remove = (rel) => {
|
|
3419
|
-
const file =
|
|
3420
|
-
|
|
3421
|
-
pruneEmpty(
|
|
3528
|
+
const file = path20.join(paths.design, rel);
|
|
3529
|
+
fs22.rmSync(file, { force: true });
|
|
3530
|
+
pruneEmpty(path20.dirname(file), stop);
|
|
3422
3531
|
};
|
|
3423
3532
|
for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
|
|
3424
3533
|
const b = base[rel];
|
|
@@ -3474,11 +3583,11 @@ function resolveConflict(paths, state, rel, side) {
|
|
|
3474
3583
|
const conflict = state.conflicts.find((item) => item.path === rel);
|
|
3475
3584
|
if (!conflict) return;
|
|
3476
3585
|
const kept = sideFile(paths, state.unit, rel, side);
|
|
3477
|
-
const stop = state.unit === SYSTEM_UNIT ? paths.design :
|
|
3478
|
-
if (
|
|
3586
|
+
const stop = state.unit === SYSTEM_UNIT ? paths.design : path20.dirname(unitRoot(paths, state.unit));
|
|
3587
|
+
if (fs22.existsSync(kept)) writeInside(paths.design, rel, fs22.readFileSync(kept));
|
|
3479
3588
|
else {
|
|
3480
|
-
|
|
3481
|
-
pruneEmpty(
|
|
3589
|
+
fs22.rmSync(path20.join(paths.design, rel), { force: true });
|
|
3590
|
+
pruneEmpty(path20.dirname(path20.join(paths.design, rel)), stop);
|
|
3482
3591
|
}
|
|
3483
3592
|
state.conflicts = state.conflicts.filter((item) => item.path !== rel);
|
|
3484
3593
|
saveState(paths, state);
|
|
@@ -3488,15 +3597,15 @@ function finishMerge(paths, link, key, accept = false) {
|
|
|
3488
3597
|
if (state) {
|
|
3489
3598
|
const open2 = openConflicts(paths, state).filter((conflict) => !(accept && conflict.kind !== "text"));
|
|
3490
3599
|
if (open2.length) return open2;
|
|
3491
|
-
|
|
3600
|
+
fs22.rmSync(mergeDir(paths, key), { recursive: true, force: true });
|
|
3492
3601
|
}
|
|
3493
3602
|
delete link.conflicts[key];
|
|
3494
3603
|
return [];
|
|
3495
3604
|
}
|
|
3496
3605
|
|
|
3497
3606
|
// src/cloud/meta.ts
|
|
3498
|
-
import
|
|
3499
|
-
import
|
|
3607
|
+
import fs23 from "fs";
|
|
3608
|
+
import path21 from "path";
|
|
3500
3609
|
async function syncProjectMeta(paths, client, link) {
|
|
3501
3610
|
const remote = await client.project(link.project);
|
|
3502
3611
|
if (!remote) return [];
|
|
@@ -3515,8 +3624,8 @@ async function syncProjectMeta(paths, client, link) {
|
|
|
3515
3624
|
if (localChanged && !cloudChanged) {
|
|
3516
3625
|
try {
|
|
3517
3626
|
if (file) {
|
|
3518
|
-
const ext =
|
|
3519
|
-
await client.setProjectIcon(link.project, { data:
|
|
3627
|
+
const ext = path21.extname(file).slice(1);
|
|
3628
|
+
await client.setProjectIcon(link.project, { data: fs23.readFileSync(file), type: ICON_TYPES[ext] });
|
|
3520
3629
|
lines.push("The project icon went up to the cloud.");
|
|
3521
3630
|
} else {
|
|
3522
3631
|
await client.setProjectIcon(link.project, null);
|
|
@@ -3551,12 +3660,12 @@ function checkManifest(key, manifest) {
|
|
|
3551
3660
|
}
|
|
3552
3661
|
function applyUnit(paths, key, local, target, blobs) {
|
|
3553
3662
|
const counts = { added: 0, changed: 0, removed: 0 };
|
|
3554
|
-
const stop = key === SYSTEM_UNIT ? paths.design :
|
|
3663
|
+
const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
|
|
3555
3664
|
for (const rel of Object.keys(local)) {
|
|
3556
3665
|
if (rel in target) continue;
|
|
3557
|
-
const file =
|
|
3558
|
-
|
|
3559
|
-
pruneEmpty(
|
|
3666
|
+
const file = path22.join(paths.design, rel);
|
|
3667
|
+
fs24.rmSync(file, { force: true });
|
|
3668
|
+
pruneEmpty(path22.dirname(file), stop);
|
|
3560
3669
|
counts.removed++;
|
|
3561
3670
|
}
|
|
3562
3671
|
for (const [rel, entry] of Object.entries(target)) {
|
|
@@ -3626,7 +3735,7 @@ async function pull(ctx, options) {
|
|
|
3626
3735
|
}
|
|
3627
3736
|
if (s.conflictRev !== void 0) {
|
|
3628
3737
|
finishMerge(paths, link, key, true);
|
|
3629
|
-
|
|
3738
|
+
fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3630
3739
|
}
|
|
3631
3740
|
report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
|
|
3632
3741
|
} else if (!s.changed.length) {
|
|
@@ -3655,7 +3764,7 @@ async function pull(ctx, options) {
|
|
|
3655
3764
|
}
|
|
3656
3765
|
const byHash = /* @__PURE__ */ new Map();
|
|
3657
3766
|
for (const unit of scan.units.values())
|
|
3658
|
-
for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash,
|
|
3767
|
+
for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path22.join(paths.design, rel));
|
|
3659
3768
|
const needed = /* @__PURE__ */ new Set();
|
|
3660
3769
|
const bases = /* @__PURE__ */ new Set();
|
|
3661
3770
|
for (const plan of plans) {
|
|
@@ -3676,7 +3785,7 @@ async function pull(ctx, options) {
|
|
|
3676
3785
|
const download = [];
|
|
3677
3786
|
for (const hash of needed) {
|
|
3678
3787
|
const file = byHash.get(hash);
|
|
3679
|
-
if (file) blobs.set(hash,
|
|
3788
|
+
if (file) blobs.set(hash, fs24.readFileSync(file));
|
|
3680
3789
|
else download.push(hash);
|
|
3681
3790
|
}
|
|
3682
3791
|
if (download.length) {
|
|
@@ -3712,7 +3821,7 @@ async function pull(ctx, options) {
|
|
|
3712
3821
|
if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
|
|
3713
3822
|
else delete link.units[key];
|
|
3714
3823
|
delete link.conflicts[key];
|
|
3715
|
-
|
|
3824
|
+
fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3716
3825
|
} else {
|
|
3717
3826
|
const result = mergeUnit(
|
|
3718
3827
|
paths,
|
|
@@ -3723,7 +3832,7 @@ async function pull(ctx, options) {
|
|
|
3723
3832
|
plan.remote.manifest,
|
|
3724
3833
|
(hash) => blobs.get(hash)
|
|
3725
3834
|
);
|
|
3726
|
-
|
|
3835
|
+
fs24.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3727
3836
|
link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
|
|
3728
3837
|
entry.rev = plan.remote.headRev;
|
|
3729
3838
|
entry.files = result.files;
|
|
@@ -3753,8 +3862,8 @@ async function pull(ctx, options) {
|
|
|
3753
3862
|
}
|
|
3754
3863
|
|
|
3755
3864
|
// src/cloud/push.ts
|
|
3756
|
-
import
|
|
3757
|
-
import
|
|
3865
|
+
import fs25 from "fs";
|
|
3866
|
+
import path23 from "path";
|
|
3758
3867
|
function blockedReport(status, message) {
|
|
3759
3868
|
return {
|
|
3760
3869
|
unit: status.key,
|
|
@@ -3870,7 +3979,7 @@ async function push(ctx, options) {
|
|
|
3870
3979
|
for (const key of toPush) {
|
|
3871
3980
|
const s = status(key);
|
|
3872
3981
|
if (s.conflictRev !== void 0) {
|
|
3873
|
-
const dir =
|
|
3982
|
+
const dir = path23.relative(paths.root, incomingDir(paths, key));
|
|
3874
3983
|
blocked.push(
|
|
3875
3984
|
blockedReport(
|
|
3876
3985
|
s,
|
|
@@ -3919,7 +4028,7 @@ async function push(ctx, options) {
|
|
|
3919
4028
|
const files = /* @__PURE__ */ new Map();
|
|
3920
4029
|
const addFiles = (manifest, dir) => {
|
|
3921
4030
|
for (const [rel, entry] of Object.entries(manifest))
|
|
3922
|
-
if (!files.has(entry.hash)) files.set(entry.hash,
|
|
4031
|
+
if (!files.has(entry.hash)) files.set(entry.hash, path23.join(dir, rel));
|
|
3923
4032
|
};
|
|
3924
4033
|
for (const key of pushing) {
|
|
3925
4034
|
const id2 = unitCanvasId(key);
|
|
@@ -3951,7 +4060,7 @@ async function push(ctx, options) {
|
|
|
3951
4060
|
if (missing.length) {
|
|
3952
4061
|
const unknown = missing.find((hash) => !files.has(hash));
|
|
3953
4062
|
if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
|
|
3954
|
-
const bytes = missing.reduce((sum, hash) => sum +
|
|
4063
|
+
const bytes = missing.reduce((sum, hash) => sum + fs25.statSync(files.get(hash)).size, 0);
|
|
3955
4064
|
const bar = ctx.progress();
|
|
3956
4065
|
const started = Date.now();
|
|
3957
4066
|
let sentFiles = 0;
|
|
@@ -3960,12 +4069,12 @@ async function push(ctx, options) {
|
|
|
3960
4069
|
bar.update(line());
|
|
3961
4070
|
await mapLimit(missing, TRANSFERS, async (hash) => {
|
|
3962
4071
|
const file = files.get(hash);
|
|
3963
|
-
const data =
|
|
4072
|
+
const data = fs25.readFileSync(file);
|
|
3964
4073
|
try {
|
|
3965
4074
|
await client.putBlob(link.project, hash, data);
|
|
3966
4075
|
} catch (error) {
|
|
3967
4076
|
if (error instanceof CloudError && error.status === 413)
|
|
3968
|
-
throw new CliError(`${
|
|
4077
|
+
throw new CliError(`${path23.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
|
|
3969
4078
|
throw error;
|
|
3970
4079
|
}
|
|
3971
4080
|
sentFiles++;
|
|
@@ -3975,7 +4084,18 @@ async function push(ctx, options) {
|
|
|
3975
4084
|
bar.done(`Uploaded ${plural(missing.length, "file")} (${megabytes(bytes)}) in ${seconds(started)}`);
|
|
3976
4085
|
}
|
|
3977
4086
|
log(`Pushing ${plural(units.length, "unit")}\u2026`);
|
|
3978
|
-
|
|
4087
|
+
let revs2;
|
|
4088
|
+
try {
|
|
4089
|
+
revs2 = await client.push(link.project, units);
|
|
4090
|
+
} catch (error) {
|
|
4091
|
+
if (!(error instanceof CloudError) || error.status !== 409 || error.error !== "conflict") throw error;
|
|
4092
|
+
report.ok = false;
|
|
4093
|
+
report.error = {
|
|
4094
|
+
code: "pull_first",
|
|
4095
|
+
message: "Nothing was pushed: someone pushed first. Run `design pull`, then push again."
|
|
4096
|
+
};
|
|
4097
|
+
return report;
|
|
4098
|
+
}
|
|
3979
4099
|
for (const unit of units) {
|
|
3980
4100
|
const rev = revs2.get(unit.key) ?? unit.baseRev + 1;
|
|
3981
4101
|
link.units[unit.key] = { rev, files: hashes(unit.source) };
|
|
@@ -3987,7 +4107,7 @@ async function push(ctx, options) {
|
|
|
3987
4107
|
url: webUrl(link, unit.key)
|
|
3988
4108
|
});
|
|
3989
4109
|
}
|
|
3990
|
-
for (const key of resolved)
|
|
4110
|
+
for (const key of resolved) fs25.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3991
4111
|
writeLink(paths, link);
|
|
3992
4112
|
report.units.sort((a, b) => byUnit(a.unit, b.unit));
|
|
3993
4113
|
return report;
|
|
@@ -3998,7 +4118,7 @@ function refuseLegacyGo(paths, keys) {
|
|
|
3998
4118
|
for (const key of keys) {
|
|
3999
4119
|
if (!key.startsWith("canvas/")) continue;
|
|
4000
4120
|
const id2 = key.slice("canvas/".length);
|
|
4001
|
-
for (const file of canvasCodeFiles(
|
|
4121
|
+
for (const file of canvasCodeFiles(path23.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
|
|
4002
4122
|
for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
|
|
4003
4123
|
}
|
|
4004
4124
|
}
|
|
@@ -4014,18 +4134,18 @@ Still using go():
|
|
|
4014
4134
|
|
|
4015
4135
|
// src/cli/commands/preview.ts
|
|
4016
4136
|
import { spawn } from "child_process";
|
|
4017
|
-
import
|
|
4137
|
+
import fs29 from "fs";
|
|
4018
4138
|
|
|
4019
4139
|
// src/server/index.ts
|
|
4020
|
-
import
|
|
4021
|
-
import
|
|
4022
|
-
import
|
|
4023
|
-
import
|
|
4140
|
+
import fs28 from "fs";
|
|
4141
|
+
import http2 from "http";
|
|
4142
|
+
import path26 from "path";
|
|
4143
|
+
import sirv2 from "sirv";
|
|
4024
4144
|
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
4025
4145
|
|
|
4026
4146
|
// src/server/api.ts
|
|
4027
|
-
import
|
|
4028
|
-
import
|
|
4147
|
+
import fs26 from "fs";
|
|
4148
|
+
import path24 from "path";
|
|
4029
4149
|
function sendJson(res, status, body) {
|
|
4030
4150
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
4031
4151
|
res.end(JSON.stringify(body));
|
|
@@ -4076,17 +4196,17 @@ async function handleApi(ctx, req, res, url) {
|
|
|
4076
4196
|
}
|
|
4077
4197
|
if (route2 === "/source") {
|
|
4078
4198
|
const rel = url.searchParams.get("path") ?? "";
|
|
4079
|
-
const file =
|
|
4080
|
-
if (!rel || !isInside(project.paths.root, file) || !
|
|
4199
|
+
const file = path24.resolve(project.paths.root, rel);
|
|
4200
|
+
if (!rel || !isInside(project.paths.root, file) || !fs26.existsSync(file) || fs26.statSync(file).isDirectory()) {
|
|
4081
4201
|
sendJson(res, 404, { error: "not found" });
|
|
4082
4202
|
return true;
|
|
4083
4203
|
}
|
|
4084
|
-
if (
|
|
4204
|
+
if (fs26.statSync(file).size > 2 * 1024 * 1024) {
|
|
4085
4205
|
sendJson(res, 413, { error: "file too large to show" });
|
|
4086
4206
|
return true;
|
|
4087
4207
|
}
|
|
4088
4208
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
4089
|
-
res.end(
|
|
4209
|
+
res.end(fs26.readFileSync(file, "utf8"));
|
|
4090
4210
|
return true;
|
|
4091
4211
|
}
|
|
4092
4212
|
if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
|
|
@@ -4133,7 +4253,7 @@ async function handleApi(ctx, req, res, url) {
|
|
|
4133
4253
|
return true;
|
|
4134
4254
|
}
|
|
4135
4255
|
res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
|
|
4136
|
-
|
|
4256
|
+
fs26.createReadStream(file).pipe(res);
|
|
4137
4257
|
return true;
|
|
4138
4258
|
}
|
|
4139
4259
|
return false;
|
|
@@ -4173,26 +4293,82 @@ var EventHub = class {
|
|
|
4173
4293
|
};
|
|
4174
4294
|
|
|
4175
4295
|
// src/server/state.ts
|
|
4176
|
-
import
|
|
4177
|
-
import
|
|
4178
|
-
|
|
4179
|
-
var
|
|
4296
|
+
import fs27 from "fs";
|
|
4297
|
+
import os4 from "os";
|
|
4298
|
+
import path25 from "path";
|
|
4299
|
+
var stateFile2 = (paths) => path25.join(paths.cache, "server.json");
|
|
4300
|
+
var logFile = (paths) => path25.join(paths.cache, "server.log");
|
|
4180
4301
|
function readState(paths) {
|
|
4181
4302
|
try {
|
|
4182
|
-
return JSON.parse(
|
|
4303
|
+
return JSON.parse(fs27.readFileSync(stateFile2(paths), "utf8"));
|
|
4183
4304
|
} catch {
|
|
4184
4305
|
return null;
|
|
4185
4306
|
}
|
|
4186
4307
|
}
|
|
4308
|
+
function registryDir() {
|
|
4309
|
+
const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path25.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path25.join(os4.homedir(), ".cache");
|
|
4310
|
+
return path25.join(base, "foss-design", "previews");
|
|
4311
|
+
}
|
|
4312
|
+
var entryFile = (pid) => path25.join(registryDir(), `${pid}.json`);
|
|
4187
4313
|
function writeState(paths, state) {
|
|
4188
|
-
|
|
4189
|
-
|
|
4314
|
+
fs27.mkdirSync(paths.cache, { recursive: true });
|
|
4315
|
+
fs27.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
|
|
4316
|
+
`);
|
|
4317
|
+
try {
|
|
4318
|
+
fs27.mkdirSync(registryDir(), { recursive: true });
|
|
4319
|
+
fs27.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
|
|
4190
4320
|
`);
|
|
4321
|
+
} catch {
|
|
4322
|
+
}
|
|
4191
4323
|
}
|
|
4192
4324
|
function clearState(paths, pid) {
|
|
4193
4325
|
const state = readState(paths);
|
|
4326
|
+
fs27.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
|
|
4194
4327
|
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
4195
|
-
|
|
4328
|
+
fs27.rmSync(stateFile2(paths), { force: true });
|
|
4329
|
+
}
|
|
4330
|
+
function listPreviews() {
|
|
4331
|
+
let files = [];
|
|
4332
|
+
try {
|
|
4333
|
+
files = fs27.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
|
|
4334
|
+
} catch {
|
|
4335
|
+
return [];
|
|
4336
|
+
}
|
|
4337
|
+
const found = [];
|
|
4338
|
+
for (const name of files) {
|
|
4339
|
+
const file = path25.join(registryDir(), name);
|
|
4340
|
+
let state = null;
|
|
4341
|
+
try {
|
|
4342
|
+
state = JSON.parse(fs27.readFileSync(file, "utf8"));
|
|
4343
|
+
} catch {
|
|
4344
|
+
}
|
|
4345
|
+
if (!state?.pid || !pidAlive(state.pid)) {
|
|
4346
|
+
fs27.rmSync(file, { force: true });
|
|
4347
|
+
continue;
|
|
4348
|
+
}
|
|
4349
|
+
let recorded = null;
|
|
4350
|
+
try {
|
|
4351
|
+
recorded = JSON.parse(fs27.readFileSync(path25.join(state.root, ".design", ".cache", "server.json"), "utf8"));
|
|
4352
|
+
} catch {
|
|
4353
|
+
}
|
|
4354
|
+
found.push({ ...state, tracked: recorded?.pid === state.pid });
|
|
4355
|
+
}
|
|
4356
|
+
return found.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
4357
|
+
}
|
|
4358
|
+
async function stopPreview(entry) {
|
|
4359
|
+
try {
|
|
4360
|
+
process.kill(entry.pid, "SIGTERM");
|
|
4361
|
+
} catch {
|
|
4362
|
+
}
|
|
4363
|
+
const until = Date.now() + 5e3;
|
|
4364
|
+
while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4365
|
+
fs27.rmSync(entryFile(entry.pid), { force: true });
|
|
4366
|
+
const stateAt = path25.join(entry.root, ".design", ".cache", "server.json");
|
|
4367
|
+
try {
|
|
4368
|
+
if (JSON.parse(fs27.readFileSync(stateAt, "utf8")).pid === entry.pid)
|
|
4369
|
+
fs27.rmSync(stateAt, { force: true });
|
|
4370
|
+
} catch {
|
|
4371
|
+
}
|
|
4196
4372
|
}
|
|
4197
4373
|
function pidAlive(pid) {
|
|
4198
4374
|
try {
|
|
@@ -4249,15 +4425,15 @@ function notFoundPage(res, title, text2) {
|
|
|
4249
4425
|
}
|
|
4250
4426
|
async function startDevServer(options) {
|
|
4251
4427
|
const paths = designPaths(options.root);
|
|
4252
|
-
if (!
|
|
4253
|
-
|
|
4428
|
+
if (!fs28.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
|
|
4429
|
+
fs28.mkdirSync(paths.cache, { recursive: true });
|
|
4254
4430
|
const snapshots = new SnapshotStore(paths.snapshots);
|
|
4255
4431
|
const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
|
|
4256
4432
|
const entries = new Entries(project);
|
|
4257
4433
|
entries.sync();
|
|
4258
4434
|
const links = linkShippedPackages(paths, project.appDir());
|
|
4259
4435
|
const events = new EventHub();
|
|
4260
|
-
const httpServer =
|
|
4436
|
+
const httpServer = http2.createServer();
|
|
4261
4437
|
dropStaleDepCache(paths, project.appDir());
|
|
4262
4438
|
const base = baseConfig(project);
|
|
4263
4439
|
const vite = await createServer({
|
|
@@ -4276,8 +4452,8 @@ async function startDevServer(options) {
|
|
|
4276
4452
|
// `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
|
|
4277
4453
|
// generated entries are modules the frames load, so those stay watched.
|
|
4278
4454
|
ignored: (file) => {
|
|
4279
|
-
const rel = toPosix(
|
|
4280
|
-
const inCache = rel !== "" && !rel.startsWith("..") && !
|
|
4455
|
+
const rel = toPosix(path26.relative(paths.cache, file));
|
|
4456
|
+
const inCache = rel !== "" && !rel.startsWith("..") && !path26.isAbsolute(rel);
|
|
4281
4457
|
return inCache && rel !== "entries" && !rel.startsWith("entries/");
|
|
4282
4458
|
}
|
|
4283
4459
|
},
|
|
@@ -4286,11 +4462,11 @@ async function startDevServer(options) {
|
|
|
4286
4462
|
forwardConsole: false
|
|
4287
4463
|
},
|
|
4288
4464
|
optimizeDeps: {
|
|
4289
|
-
entries: entries.files().map((file) => toPosix(
|
|
4465
|
+
entries: entries.files().map((file) => toPosix(path26.relative(paths.design, file)))
|
|
4290
4466
|
}
|
|
4291
4467
|
});
|
|
4292
4468
|
const publicDir = project.publicDir();
|
|
4293
|
-
const servePublic = publicDir ?
|
|
4469
|
+
const servePublic = publicDir ? sirv2(publicDir, { dev: true }) : null;
|
|
4294
4470
|
const publicRel = (encoded) => {
|
|
4295
4471
|
try {
|
|
4296
4472
|
return decodeURIComponent(encoded);
|
|
@@ -4298,16 +4474,16 @@ async function startDevServer(options) {
|
|
|
4298
4474
|
return null;
|
|
4299
4475
|
}
|
|
4300
4476
|
};
|
|
4301
|
-
const viewer =
|
|
4302
|
-
const viewerIndex =
|
|
4477
|
+
const viewer = fs28.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
|
|
4478
|
+
const viewerIndex = path26.join(PKG.viewer, "index.html");
|
|
4303
4479
|
const serveViewerIndex = (res) => {
|
|
4304
|
-
const html =
|
|
4480
|
+
const html = fs28.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
|
|
4305
4481
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
4306
4482
|
res.end(html);
|
|
4307
4483
|
};
|
|
4308
4484
|
const ctx = { project, events, snapshots };
|
|
4309
4485
|
const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
|
|
4310
|
-
const bootUrl = `/${relToDesign(paths,
|
|
4486
|
+
const bootUrl = `/${relToDesign(paths, path26.join(entries.dir, HTML_BOOT))}`;
|
|
4311
4487
|
const frameHead = (source, extra = {}) => {
|
|
4312
4488
|
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
4313
4489
|
return {
|
|
@@ -4328,7 +4504,7 @@ async function startDevServer(options) {
|
|
|
4328
4504
|
notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
|
|
4329
4505
|
return;
|
|
4330
4506
|
}
|
|
4331
|
-
if (!
|
|
4507
|
+
if (!fs28.existsSync(entries.entryFile(source))) entries.sync();
|
|
4332
4508
|
const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
|
|
4333
4509
|
const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
|
|
4334
4510
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -4337,8 +4513,8 @@ async function startDevServer(options) {
|
|
|
4337
4513
|
};
|
|
4338
4514
|
const serveHtmlScreen = async (res, url2) => {
|
|
4339
4515
|
const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
|
|
4340
|
-
const file =
|
|
4341
|
-
if (!isInside(paths.design, file) || !
|
|
4516
|
+
const file = path26.resolve(paths.design, rel);
|
|
4517
|
+
if (!isInside(paths.design, file) || !fs28.existsSync(file)) {
|
|
4342
4518
|
notFoundPage(res, "Screen not found", `No file .design/${rel}`);
|
|
4343
4519
|
return;
|
|
4344
4520
|
}
|
|
@@ -4348,13 +4524,13 @@ async function startDevServer(options) {
|
|
|
4348
4524
|
key: `${canvas}/${id2}`,
|
|
4349
4525
|
canvas,
|
|
4350
4526
|
id: id2,
|
|
4351
|
-
title:
|
|
4527
|
+
title: path26.basename(file),
|
|
4352
4528
|
file,
|
|
4353
4529
|
format: "html",
|
|
4354
4530
|
props: {},
|
|
4355
4531
|
system: true
|
|
4356
4532
|
};
|
|
4357
|
-
const raw =
|
|
4533
|
+
const raw = fs28.readFileSync(file, "utf8");
|
|
4358
4534
|
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
4359
4535
|
const html = injectIntoHtml(
|
|
4360
4536
|
raw,
|
|
@@ -4387,7 +4563,7 @@ async function startDevServer(options) {
|
|
|
4387
4563
|
}
|
|
4388
4564
|
if (url2.pathname.startsWith("/_fs/")) {
|
|
4389
4565
|
const rel = publicRel(url2.pathname.slice("/_fs/".length));
|
|
4390
|
-
if (publicDir && rel !== null && !project.isPublicFile(rel) &&
|
|
4566
|
+
if (publicDir && rel !== null && !project.isPublicFile(rel) && fs28.existsSync(path26.join(publicDir, rel))) {
|
|
4391
4567
|
notFoundPage(res, "Not found", url2.pathname);
|
|
4392
4568
|
return;
|
|
4393
4569
|
}
|
|
@@ -4399,7 +4575,7 @@ async function startDevServer(options) {
|
|
|
4399
4575
|
const rel = publicRel(url2.pathname.slice(1));
|
|
4400
4576
|
if (servePublic && rel !== null && project.isPublicFile(rel))
|
|
4401
4577
|
servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
|
|
4402
|
-
else if (
|
|
4578
|
+
else if (path26.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
|
|
4403
4579
|
else serveViewerIndex(res);
|
|
4404
4580
|
};
|
|
4405
4581
|
if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
|
|
@@ -4427,7 +4603,7 @@ async function startDevServer(options) {
|
|
|
4427
4603
|
};
|
|
4428
4604
|
vite.watcher.on("all", (event, file) => {
|
|
4429
4605
|
if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
|
|
4430
|
-
const rel = toPosix(
|
|
4606
|
+
const rel = toPosix(path26.relative(paths.design, file));
|
|
4431
4607
|
if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
|
|
4432
4608
|
if (rel === "design.json") {
|
|
4433
4609
|
queue("config", () => {
|
|
@@ -4487,7 +4663,7 @@ async function startDevServer(options) {
|
|
|
4487
4663
|
// src/cli/commands/preview.ts
|
|
4488
4664
|
function openBrowser(url) {
|
|
4489
4665
|
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
4490
|
-
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
4666
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
4491
4667
|
child.on("error", () => {
|
|
4492
4668
|
});
|
|
4493
4669
|
child.unref();
|
|
@@ -4535,15 +4711,23 @@ async function ensureServer(paths, options) {
|
|
|
4535
4711
|
if (stale)
|
|
4536
4712
|
print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
|
|
4537
4713
|
if (live2) await stopServer(paths);
|
|
4538
|
-
|
|
4539
|
-
|
|
4714
|
+
const recorded = readState(paths);
|
|
4715
|
+
if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
|
|
4716
|
+
for (const other of listPreviews()) if (other.root === paths.root) await stopPreview(other);
|
|
4717
|
+
fs29.mkdirSync(paths.cache, { recursive: true });
|
|
4718
|
+
const log = fs29.openSync(logFile(paths), "w");
|
|
4540
4719
|
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
4541
4720
|
if (options.port) args.push("--port", String(options.port));
|
|
4542
|
-
const child = spawn(process.execPath, args, {
|
|
4721
|
+
const child = spawn(process.execPath, args, {
|
|
4722
|
+
detached: true,
|
|
4723
|
+
windowsHide: true,
|
|
4724
|
+
stdio: ["ignore", log, log],
|
|
4725
|
+
cwd: paths.root
|
|
4726
|
+
});
|
|
4543
4727
|
child.unref();
|
|
4544
4728
|
const state = await waitForServer(paths, 4e4);
|
|
4545
4729
|
if (!state) {
|
|
4546
|
-
const tail =
|
|
4730
|
+
const tail = fs29.existsSync(logFile(paths)) ? fs29.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
|
|
4547
4731
|
throw new CliError(`The preview server did not start.
|
|
4548
4732
|
${tail.join("\n")}`);
|
|
4549
4733
|
}
|
|
@@ -4569,6 +4753,27 @@ async function runPreview(paths, options) {
|
|
|
4569
4753
|
async function runStop(paths) {
|
|
4570
4754
|
print(await stopServer(paths) ? "Stopped the preview server." : "No preview server is running for this project.");
|
|
4571
4755
|
}
|
|
4756
|
+
var since = (iso) => {
|
|
4757
|
+
const minutes = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 6e4));
|
|
4758
|
+
return minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
4759
|
+
};
|
|
4760
|
+
function runPreviews(options) {
|
|
4761
|
+
const previews = listPreviews();
|
|
4762
|
+
if (options.json) return print(JSON.stringify({ previews }, null, 2));
|
|
4763
|
+
if (!previews.length) return print("No preview servers are running.");
|
|
4764
|
+
for (const preview of previews) {
|
|
4765
|
+
const orphan = preview.tracked ? "" : ` ${bold("orphan")} ${dim("(its project names another server)")}`;
|
|
4766
|
+
print(`${preview.url} ${preview.root}${orphan}`);
|
|
4767
|
+
print(dim(` foss-design ${preview.version} \xB7 pid ${preview.pid} \xB7 up ${since(preview.startedAt)}`));
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
async function runStopAll() {
|
|
4771
|
+
const previews = listPreviews();
|
|
4772
|
+
for (const preview of previews) await stopPreview(preview);
|
|
4773
|
+
print(
|
|
4774
|
+
previews.length ? `Stopped ${previews.length} preview server${previews.length === 1 ? "" : "s"}.` : "No preview servers are running."
|
|
4775
|
+
);
|
|
4776
|
+
}
|
|
4572
4777
|
|
|
4573
4778
|
// src/cli/commands/sync.ts
|
|
4574
4779
|
var GOOD = /* @__PURE__ */ new Set(["pushed", "created", "updated", "deleted"]);
|
|
@@ -4649,11 +4854,19 @@ async function runSync(paths, command, json, run) {
|
|
|
4649
4854
|
if (!report.ok && report.error) throw new CliError(report.error.message, 2);
|
|
4650
4855
|
}
|
|
4651
4856
|
function runPush(paths, units, options) {
|
|
4652
|
-
const beforeBuild = (canvasIds) => refreshCovers(paths, canvasIds, async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`);
|
|
4653
4857
|
return runSync(paths, "push", options.json, async (ctx) => {
|
|
4858
|
+
const beforeBuild = (canvasIds) => takeSnapshots(
|
|
4859
|
+
paths,
|
|
4860
|
+
staleSnapshots(paths, canvasIds),
|
|
4861
|
+
async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`,
|
|
4862
|
+
ctx.progress()
|
|
4863
|
+
);
|
|
4654
4864
|
const first = await push(ctx, { units, resolved: options.resolved, beforeBuild });
|
|
4655
4865
|
if (first.ok || first.error?.code !== "pull_first") return first;
|
|
4656
|
-
|
|
4866
|
+
const raced = first.error.message.includes("someone pushed first");
|
|
4867
|
+
ctx.log(
|
|
4868
|
+
raced ? "Someone pushed while this push was uploading: merging their changes first\u2026" : "The cloud is ahead: merging its changes first\u2026"
|
|
4869
|
+
);
|
|
4657
4870
|
const pulled = await pull(ctx, { units: [], theirs: [] });
|
|
4658
4871
|
if (!pulled.ok) {
|
|
4659
4872
|
pulled.error = {
|
|
@@ -4665,7 +4878,7 @@ function runPush(paths, units, options) {
|
|
|
4665
4878
|
ctx.link = readLink(paths);
|
|
4666
4879
|
const second = await push(ctx, { units, resolved: [], beforeBuild });
|
|
4667
4880
|
second.hints.unshift(
|
|
4668
|
-
|
|
4881
|
+
`${raced ? "Someone pushed while this push was uploading; merged" : "Merged"} the cloud's changes first: ${pulled.units.filter((entry) => entry.action !== "up_to_date").map((entry) => `${entry.unit} (${entry.action.replaceAll("_", " ")})`).join(", ")}`
|
|
4669
4882
|
);
|
|
4670
4883
|
return second;
|
|
4671
4884
|
});
|
|
@@ -4897,6 +5110,7 @@ async function runMe(paths, options) {
|
|
|
4897
5110
|
|
|
4898
5111
|
// src/cli/commands/check.ts
|
|
4899
5112
|
var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
|
|
5113
|
+
var SLOW_MS = 3e3;
|
|
4900
5114
|
function printIssues(issues) {
|
|
4901
5115
|
const byFile = /* @__PURE__ */ new Map();
|
|
4902
5116
|
for (const issue of issues) byFile.set(issue.file, [...byFile.get(issue.file) ?? [], issue]);
|
|
@@ -4929,35 +5143,90 @@ async function renderFrames(jobs) {
|
|
|
4929
5143
|
await page.close().catch(() => {
|
|
4930
5144
|
});
|
|
4931
5145
|
}
|
|
4932
|
-
return {
|
|
5146
|
+
return {
|
|
5147
|
+
key: job.key,
|
|
5148
|
+
url: job.url,
|
|
5149
|
+
ready: report.ready,
|
|
5150
|
+
held: report.held,
|
|
5151
|
+
readyMs: report.readyMs,
|
|
5152
|
+
errors: report.errors,
|
|
5153
|
+
links
|
|
5154
|
+
};
|
|
4933
5155
|
});
|
|
4934
5156
|
} finally {
|
|
4935
5157
|
await browser.close();
|
|
4936
5158
|
}
|
|
4937
5159
|
}
|
|
4938
|
-
async function runCheck(paths,
|
|
5160
|
+
async function runCheck(paths, targets, options) {
|
|
4939
5161
|
const project = new DesignProject(paths, DEV_URLS);
|
|
4940
5162
|
const known = project.canvasIds();
|
|
4941
|
-
const
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
5163
|
+
const picked = /* @__PURE__ */ new Map();
|
|
5164
|
+
for (const target of targets) {
|
|
5165
|
+
const [canvasId = "", screenId] = target.split("/");
|
|
5166
|
+
if (!known.includes(canvasId))
|
|
5167
|
+
throw new CliError(`No canvas "${canvasId}". Canvases: ${known.join(", ") || "none yet"}`);
|
|
5168
|
+
const screens = picked.has(canvasId) ? picked.get(canvasId) : /* @__PURE__ */ new Set();
|
|
5169
|
+
if (!screenId || screens === null) picked.set(canvasId, null);
|
|
5170
|
+
else picked.set(canvasId, screens.add(screenId));
|
|
5171
|
+
}
|
|
5172
|
+
const canvases = [...picked.keys()];
|
|
5173
|
+
if (options.page) {
|
|
5174
|
+
if (canvases.length !== 1)
|
|
5175
|
+
throw new CliError("--page needs exactly one canvas: `design check <canvas> --page <id>`");
|
|
5176
|
+
const doc = project.canvas(canvases[0]).doc;
|
|
5177
|
+
if (!doc.pages.some((page) => page.id === options.page))
|
|
5178
|
+
throw new CliError(`No page "${options.page}" in "${doc.id}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
5179
|
+
}
|
|
5180
|
+
for (const [canvasId, screens] of picked) {
|
|
5181
|
+
if (!screens) continue;
|
|
5182
|
+
const doc = project.canvas(canvasId).doc;
|
|
5183
|
+
const ids = new Set(canvasFrames(doc, "", "light", { page: options.page, skipMissing: false }).map((job) => job.id));
|
|
5184
|
+
const missing = [...screens].filter((id2) => !ids.has(id2));
|
|
5185
|
+
if (missing.length)
|
|
5186
|
+
throw new CliError(
|
|
5187
|
+
`No screen ${missing.map((id2) => `"${id2}"`).join(", ")} in "${canvasId}"${options.page ? ` on page "${options.page}"` : ""}. Screens: ${[...ids].slice(0, 30).join(", ")}${ids.size > 30 ? ", \u2026" : ""}`
|
|
5188
|
+
);
|
|
4946
5189
|
}
|
|
4947
5190
|
const issues = canvases.length ? canvases.flatMap((id2) => project.canvas(id2)?.doc.issues ?? []) : project.issues();
|
|
4948
5191
|
let frames = [];
|
|
4949
5192
|
if (options.render) {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
5193
|
+
let base;
|
|
5194
|
+
let docOf = (id2) => project.canvas(id2).doc;
|
|
5195
|
+
let systemDoc = project.system()?.doc ?? null;
|
|
5196
|
+
let close = async () => {
|
|
5197
|
+
};
|
|
5198
|
+
if (options.built) {
|
|
5199
|
+
if (!options.json) print(dim("Building the screens as design push does\u2026"));
|
|
5200
|
+
const build2 = await serveBuild(paths, "check-build", {
|
|
5201
|
+
canvases: canvases.length ? canvases : void 0,
|
|
5202
|
+
includeSystem: !canvases.length
|
|
5203
|
+
});
|
|
5204
|
+
docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
|
|
5205
|
+
systemDoc = canvases.length ? null : build2.read("api/system.json");
|
|
5206
|
+
base = build2.base;
|
|
5207
|
+
close = build2.close;
|
|
5208
|
+
if (!options.json) print(dim(`Serving the build at ${base}`));
|
|
5209
|
+
} else {
|
|
5210
|
+
const wasRunning = await liveServer(paths);
|
|
5211
|
+
const server = await ensureServer(paths, {});
|
|
5212
|
+
if (!wasRunning && !options.json)
|
|
5213
|
+
print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
|
|
5214
|
+
base = `http://127.0.0.1:${server.port}`;
|
|
5215
|
+
}
|
|
5216
|
+
const jobs = (canvases.length ? canvases : known).flatMap((id2) => {
|
|
5217
|
+
const doc = docOf(id2);
|
|
5218
|
+
const theme2 = doc.theme ?? "light";
|
|
5219
|
+
const screens = picked.get(id2);
|
|
5220
|
+
const filter = { includeUrls: false, page: options.page };
|
|
5221
|
+
return screens ? [...screens].flatMap((screen) => canvasFrames(doc, base, theme2, { ...filter, id: screen })) : canvasFrames(doc, base, theme2, filter);
|
|
5222
|
+
});
|
|
5223
|
+
if (!canvases.length && systemDoc) jobs.unshift(...systemFrames(systemDoc, base, "light"));
|
|
5224
|
+
if (!options.json) print(dim(`Loading ${plural2(jobs.length, "frame")} in Chrome\u2026`));
|
|
5225
|
+
try {
|
|
5226
|
+
frames = await renderFrames(jobs);
|
|
5227
|
+
} finally {
|
|
5228
|
+
await close();
|
|
5229
|
+
}
|
|
4961
5230
|
}
|
|
4962
5231
|
const linkIssues = options.render ? unroutedLinks(frames, project, canvases.length ? canvases : known) : [];
|
|
4963
5232
|
const errors = issues.filter((issue) => issue.severity === "error").length;
|
|
@@ -4975,10 +5244,20 @@ async function runCheck(paths, canvases, options) {
|
|
|
4975
5244
|
if (frame.held) print(` ${red(`holdReady() was not released within 20s (${frame.held} still held)`)}`);
|
|
4976
5245
|
else if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
|
|
4977
5246
|
for (const error of frame.errors) print(` ${red(error.split("\n")[0])}`);
|
|
5247
|
+
if (frame.readyMs !== void 0 && frame.readyMs > SLOW_MS)
|
|
5248
|
+
print(` ${yellow(`slow: ${(frame.readyMs / 1e3).toFixed(1)}s to become ready`)}`);
|
|
4978
5249
|
}
|
|
5250
|
+
const slow = frames.filter((frame) => (frame.readyMs ?? 0) > SLOW_MS);
|
|
5251
|
+
if (slow.length)
|
|
5252
|
+
print(
|
|
5253
|
+
dim(
|
|
5254
|
+
`${plural2(slow.length, "screen")} took over ${SLOW_MS / 1e3}s to become ready; every frame of them costs that in the viewer, check and push snapshots. See what they wait for (retries, timers, polling, entrance gates).`
|
|
5255
|
+
)
|
|
5256
|
+
);
|
|
4979
5257
|
}
|
|
4980
5258
|
if (linkIssues.length) printLinkIssues(linkIssues, project);
|
|
4981
|
-
const
|
|
5259
|
+
const screens = [...picked.values()].reduce((sum, set) => sum + (set?.size ?? 0), 0);
|
|
5260
|
+
const scope = !canvases.length ? "the project" : screens && [...picked.values()].every(Boolean) ? plural2(screens, "screen") : options.page ? `page "${options.page}" of ${canvases[0]}` : plural2(canvases.length, "canvas", "canvases");
|
|
4982
5261
|
if (!errors && !warnings && !broken.length) {
|
|
4983
5262
|
print(
|
|
4984
5263
|
green(
|
|
@@ -5047,14 +5326,14 @@ function printLinkIssues(issues, project) {
|
|
|
5047
5326
|
}
|
|
5048
5327
|
|
|
5049
5328
|
// src/cli/commands/cloud.ts
|
|
5050
|
-
import
|
|
5051
|
-
import
|
|
5052
|
-
import
|
|
5329
|
+
import fs30 from "fs";
|
|
5330
|
+
import os5 from "os";
|
|
5331
|
+
import path27 from "path";
|
|
5053
5332
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5054
5333
|
async function runLogin() {
|
|
5055
5334
|
const host = cloudHost();
|
|
5056
5335
|
const client = new CloudClient(host, null);
|
|
5057
|
-
const code = await client.deviceCode(
|
|
5336
|
+
const code = await client.deviceCode(os5.hostname());
|
|
5058
5337
|
const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
|
|
5059
5338
|
const expiresIn = code.expires_in ?? 600;
|
|
5060
5339
|
print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);
|
|
@@ -5150,7 +5429,7 @@ async function runLink(paths, wanted, options) {
|
|
|
5150
5429
|
print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
|
|
5151
5430
|
} else {
|
|
5152
5431
|
writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
|
|
5153
|
-
|
|
5432
|
+
fs30.rmSync(path27.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
|
|
5154
5433
|
const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
|
|
5155
5434
|
print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
|
|
5156
5435
|
}
|
|
@@ -5177,8 +5456,8 @@ async function runLink(paths, wanted, options) {
|
|
|
5177
5456
|
}
|
|
5178
5457
|
|
|
5179
5458
|
// src/cli/commands/icon.ts
|
|
5180
|
-
import
|
|
5181
|
-
import
|
|
5459
|
+
import fs31 from "fs";
|
|
5460
|
+
import path28 from "path";
|
|
5182
5461
|
async function toCloud(paths, file) {
|
|
5183
5462
|
const link = readLink(paths);
|
|
5184
5463
|
if (!link) return;
|
|
@@ -5189,10 +5468,10 @@ async function toCloud(paths, file) {
|
|
|
5189
5468
|
}
|
|
5190
5469
|
const client = new CloudClient(link.host, credential.token);
|
|
5191
5470
|
try {
|
|
5192
|
-
const ext = file ?
|
|
5471
|
+
const ext = file ? path28.extname(file).slice(1) : null;
|
|
5193
5472
|
await client.setProjectIcon(
|
|
5194
5473
|
link.project,
|
|
5195
|
-
file && ext ? { data:
|
|
5474
|
+
file && ext ? { data: fs31.readFileSync(file), type: ICON_TYPES[ext] } : null
|
|
5196
5475
|
);
|
|
5197
5476
|
link.icon = file ? iconHash(file) : null;
|
|
5198
5477
|
writeLink(paths, link);
|
|
@@ -5205,35 +5484,35 @@ async function runIcon(paths, file, options) {
|
|
|
5205
5484
|
if (options.remove) {
|
|
5206
5485
|
const had = iconFile(paths);
|
|
5207
5486
|
writeIcon(paths, null);
|
|
5208
|
-
print(had ? `${green("Removed")} ${
|
|
5487
|
+
print(had ? `${green("Removed")} ${path28.relative(process.cwd(), had)}` : "The project has no icon.");
|
|
5209
5488
|
await toCloud(paths, null);
|
|
5210
5489
|
return;
|
|
5211
5490
|
}
|
|
5212
5491
|
if (!file) {
|
|
5213
5492
|
const current = iconFile(paths);
|
|
5214
5493
|
if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
|
|
5215
|
-
return print(`${
|
|
5494
|
+
return print(`${path28.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
|
|
5216
5495
|
}
|
|
5217
5496
|
let data;
|
|
5218
5497
|
try {
|
|
5219
|
-
data =
|
|
5498
|
+
data = fs31.readFileSync(file);
|
|
5220
5499
|
} catch {
|
|
5221
5500
|
throw new CliError(`No file ${file}`);
|
|
5222
5501
|
}
|
|
5223
|
-
const problem = iconProblem(data,
|
|
5502
|
+
const problem = iconProblem(data, path28.extname(file).slice(1).toLowerCase() || void 0);
|
|
5224
5503
|
if (problem) throw new CliError(`${file}: ${problem}`);
|
|
5225
5504
|
const written = writeIcon(paths, data);
|
|
5226
|
-
print(`${green("Set")} the project icon: ${
|
|
5505
|
+
print(`${green("Set")} the project icon: ${path28.relative(process.cwd(), written)}`);
|
|
5227
5506
|
await toCloud(paths, written);
|
|
5228
5507
|
}
|
|
5229
5508
|
|
|
5230
5509
|
// src/cli/commands/import.ts
|
|
5231
|
-
import
|
|
5232
|
-
import
|
|
5510
|
+
import fs33 from "fs";
|
|
5511
|
+
import path30 from "path";
|
|
5233
5512
|
|
|
5234
5513
|
// src/import/claude-design.ts
|
|
5235
|
-
import
|
|
5236
|
-
import
|
|
5514
|
+
import fs32 from "fs";
|
|
5515
|
+
import path29 from "path";
|
|
5237
5516
|
|
|
5238
5517
|
// src/import/markup.ts
|
|
5239
5518
|
var VOID = /* @__PURE__ */ new Set([
|
|
@@ -5841,11 +6120,11 @@ function boardId(file, used) {
|
|
|
5841
6120
|
return id2;
|
|
5842
6121
|
}
|
|
5843
6122
|
function importClaudeDesign(dir, options = {}) {
|
|
5844
|
-
const canvasFile =
|
|
5845
|
-
if (!
|
|
6123
|
+
const canvasFile = path29.join(dir, "canvas.json");
|
|
6124
|
+
if (!fs32.existsSync(canvasFile))
|
|
5846
6125
|
throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
|
|
5847
|
-
const source = JSON.parse(
|
|
5848
|
-
const templates =
|
|
6126
|
+
const source = JSON.parse(fs32.readFileSync(canvasFile, "utf8"));
|
|
6127
|
+
const templates = fs32.readdirSync(dir).filter((file) => file.endsWith(EXT));
|
|
5849
6128
|
if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
|
|
5850
6129
|
const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
|
|
5851
6130
|
const warnings = /* @__PURE__ */ new Map();
|
|
@@ -5872,7 +6151,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5872
6151
|
try {
|
|
5873
6152
|
files.set(
|
|
5874
6153
|
`${name}.jsx`,
|
|
5875
|
-
convertFile(name,
|
|
6154
|
+
convertFile(name, fs32.readFileSync(path29.join(dir, file), "utf8"), { known, routes, warn: warn2 })
|
|
5876
6155
|
);
|
|
5877
6156
|
} catch (error) {
|
|
5878
6157
|
warn2(`could not be converted: ${error.message}`);
|
|
@@ -5920,7 +6199,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5920
6199
|
screens++;
|
|
5921
6200
|
}
|
|
5922
6201
|
const canvas = {
|
|
5923
|
-
title: options.title ?? source.title ??
|
|
6202
|
+
title: options.title ?? source.title ?? path29.basename(path29.resolve(dir)),
|
|
5924
6203
|
description: "Imported from Claude Design.",
|
|
5925
6204
|
// The templates bring their own styles; the design system's reset would restyle them.
|
|
5926
6205
|
system: false,
|
|
@@ -5932,21 +6211,21 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5932
6211
|
// src/cli/commands/import.ts
|
|
5933
6212
|
var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
|
|
5934
6213
|
function runImport(paths, from, options) {
|
|
5935
|
-
const dir =
|
|
5936
|
-
if (!
|
|
6214
|
+
const dir = path30.resolve(from);
|
|
6215
|
+
if (!fs33.existsSync(dir) || !fs33.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
|
|
5937
6216
|
let result;
|
|
5938
6217
|
try {
|
|
5939
6218
|
result = importClaudeDesign(dir, { title: options.title });
|
|
5940
6219
|
} catch (error) {
|
|
5941
6220
|
throw new CliError(error.message);
|
|
5942
6221
|
}
|
|
5943
|
-
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(
|
|
6222
|
+
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path30.basename(dir)) || "imported");
|
|
5944
6223
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
|
|
5945
|
-
const out =
|
|
5946
|
-
if (
|
|
5947
|
-
|
|
5948
|
-
for (const [name, content] of result.files)
|
|
5949
|
-
|
|
6224
|
+
const out = path30.join(paths.canvases, id2);
|
|
6225
|
+
if (fs33.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
|
|
6226
|
+
fs33.mkdirSync(path30.join(out, "screens"), { recursive: true });
|
|
6227
|
+
for (const [name, content] of result.files) fs33.writeFileSync(path30.join(out, "screens", name), content);
|
|
6228
|
+
fs33.writeFileSync(path30.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
|
|
5950
6229
|
`);
|
|
5951
6230
|
print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
|
|
5952
6231
|
print(
|
|
@@ -5966,7 +6245,7 @@ function runImport(paths, from, options) {
|
|
|
5966
6245
|
|
|
5967
6246
|
// src/cli/commands/merge.ts
|
|
5968
6247
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5969
|
-
import
|
|
6248
|
+
import path31 from "path";
|
|
5970
6249
|
import { createInterface } from "readline/promises";
|
|
5971
6250
|
var KIND = {
|
|
5972
6251
|
text: "both sides changed lines, the markers show them",
|
|
@@ -5982,7 +6261,7 @@ function matchTargets(paths, states, targets) {
|
|
|
5982
6261
|
for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
|
|
5983
6262
|
continue;
|
|
5984
6263
|
}
|
|
5985
|
-
const rel = [
|
|
6264
|
+
const rel = [path31.relative(paths.design, path31.resolve(target)), target.replace(/^\.design\//, "")];
|
|
5986
6265
|
const found = states.flatMap(
|
|
5987
6266
|
(state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
|
|
5988
6267
|
);
|
|
@@ -5998,7 +6277,7 @@ function printOpen(paths, states) {
|
|
|
5998
6277
|
print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
|
|
5999
6278
|
for (const conflict of open2) {
|
|
6000
6279
|
print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
|
|
6001
|
-
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${
|
|
6280
|
+
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path31.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
|
|
6002
6281
|
print(` ${dim(sides)}`);
|
|
6003
6282
|
}
|
|
6004
6283
|
}
|
|
@@ -6079,7 +6358,7 @@ async function runMerge(paths, targets, options) {
|
|
|
6079
6358
|
if (answer === "s") break;
|
|
6080
6359
|
if (answer === "e" && edit) {
|
|
6081
6360
|
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
6082
|
-
spawnSync2(editor, [
|
|
6361
|
+
spawnSync2(editor, [path31.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
|
|
6083
6362
|
if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
|
|
6084
6363
|
print(yellow(" Conflict markers are still there."));
|
|
6085
6364
|
}
|
|
@@ -6098,7 +6377,7 @@ async function runMerge(paths, targets, options) {
|
|
|
6098
6377
|
}
|
|
6099
6378
|
|
|
6100
6379
|
// src/cli/commands/rename.ts
|
|
6101
|
-
import
|
|
6380
|
+
import fs34 from "fs";
|
|
6102
6381
|
async function runRename(paths, name) {
|
|
6103
6382
|
const trimmed = name.trim();
|
|
6104
6383
|
if (!trimmed) throw new CliError("Usage: design rename <name>");
|
|
@@ -6113,17 +6392,17 @@ async function runRename(paths, name) {
|
|
|
6113
6392
|
}
|
|
6114
6393
|
let config = {};
|
|
6115
6394
|
try {
|
|
6116
|
-
config = JSON.parse(
|
|
6395
|
+
config = JSON.parse(fs34.readFileSync(paths.config, "utf8"));
|
|
6117
6396
|
} catch {
|
|
6118
6397
|
}
|
|
6119
|
-
|
|
6398
|
+
fs34.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
|
|
6120
6399
|
`);
|
|
6121
6400
|
print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
|
|
6122
6401
|
}
|
|
6123
6402
|
|
|
6124
6403
|
// src/cli/commands/scaffold.ts
|
|
6125
|
-
import
|
|
6126
|
-
import
|
|
6404
|
+
import fs35 from "fs";
|
|
6405
|
+
import path32 from "path";
|
|
6127
6406
|
|
|
6128
6407
|
// src/cli/templates.ts
|
|
6129
6408
|
var TOKENS_CSS = `/*
|
|
@@ -6334,40 +6613,40 @@ function mainScreen(canvasId) {
|
|
|
6334
6613
|
|
|
6335
6614
|
// src/cli/commands/scaffold.ts
|
|
6336
6615
|
function write(file, content, root) {
|
|
6337
|
-
if (
|
|
6338
|
-
print(` ${dim("exists ")} ${
|
|
6616
|
+
if (fs35.existsSync(file)) {
|
|
6617
|
+
print(` ${dim("exists ")} ${path32.relative(root, file)}`);
|
|
6339
6618
|
return false;
|
|
6340
6619
|
}
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
print(` ${green("created")} ${
|
|
6620
|
+
fs35.mkdirSync(path32.dirname(file), { recursive: true });
|
|
6621
|
+
fs35.writeFileSync(file, content);
|
|
6622
|
+
print(` ${green("created")} ${path32.relative(root, file)}`);
|
|
6344
6623
|
return true;
|
|
6345
6624
|
}
|
|
6346
6625
|
function ensureGitignored(root) {
|
|
6347
|
-
const file =
|
|
6348
|
-
const current =
|
|
6626
|
+
const file = path32.join(root, ".gitignore");
|
|
6627
|
+
const current = fs35.existsSync(file) ? fs35.readFileSync(file, "utf8") : "";
|
|
6349
6628
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
6350
6629
|
if (covered) return "present";
|
|
6351
6630
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
6352
|
-
|
|
6631
|
+
fs35.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
6353
6632
|
.design/
|
|
6354
6633
|
`);
|
|
6355
6634
|
return "added";
|
|
6356
6635
|
}
|
|
6357
6636
|
function projectName(root) {
|
|
6358
6637
|
try {
|
|
6359
|
-
const manifest = JSON.parse(
|
|
6638
|
+
const manifest = JSON.parse(fs35.readFileSync(path32.join(root, "package.json"), "utf8"));
|
|
6360
6639
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
6361
6640
|
} catch {
|
|
6362
6641
|
}
|
|
6363
|
-
return
|
|
6642
|
+
return path32.basename(root);
|
|
6364
6643
|
}
|
|
6365
6644
|
function runInit(root, options) {
|
|
6366
6645
|
const paths = designPaths(root);
|
|
6367
6646
|
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
6368
6647
|
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
6369
6648
|
`, root);
|
|
6370
|
-
|
|
6649
|
+
fs35.mkdirSync(paths.canvases, { recursive: true });
|
|
6371
6650
|
if (options.gitignore) {
|
|
6372
6651
|
const result = ensureGitignored(root);
|
|
6373
6652
|
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
@@ -6381,44 +6660,44 @@ function runSystemInit(root, options) {
|
|
|
6381
6660
|
const paths = designPaths(root);
|
|
6382
6661
|
let base = projectName(root);
|
|
6383
6662
|
try {
|
|
6384
|
-
base = JSON.parse(
|
|
6663
|
+
base = JSON.parse(fs35.readFileSync(paths.config, "utf8")).name || base;
|
|
6385
6664
|
} catch {
|
|
6386
6665
|
}
|
|
6387
6666
|
const name = options.name ?? `${base} design system`;
|
|
6388
|
-
print(`Scaffolding the design system in ${
|
|
6389
|
-
write(
|
|
6667
|
+
print(`Scaffolding the design system in ${path32.relative(root, paths.system)}`);
|
|
6668
|
+
write(path32.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
6390
6669
|
`, root);
|
|
6391
|
-
write(
|
|
6670
|
+
write(path32.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
6392
6671
|
if (!options.empty) {
|
|
6393
|
-
write(
|
|
6394
|
-
write(
|
|
6395
|
-
write(
|
|
6672
|
+
write(path32.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
6673
|
+
write(path32.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
6674
|
+
write(path32.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
6396
6675
|
}
|
|
6397
6676
|
}
|
|
6398
6677
|
function runNew(root, id2, options) {
|
|
6399
6678
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
6400
6679
|
const paths = designPaths(root);
|
|
6401
|
-
const dir =
|
|
6402
|
-
if (
|
|
6680
|
+
const dir = path32.join(paths.canvases, id2);
|
|
6681
|
+
if (fs35.existsSync(path32.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
6403
6682
|
print(`Creating canvas "${id2}"`);
|
|
6404
6683
|
const title = options.title ?? titleize(id2);
|
|
6405
6684
|
if (options.empty) {
|
|
6406
6685
|
write(
|
|
6407
|
-
|
|
6686
|
+
path32.join(dir, "canvas.json"),
|
|
6408
6687
|
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
6409
6688
|
`,
|
|
6410
6689
|
root
|
|
6411
6690
|
);
|
|
6412
|
-
|
|
6691
|
+
fs35.mkdirSync(path32.join(dir, "screens"), { recursive: true });
|
|
6413
6692
|
} else {
|
|
6414
|
-
write(
|
|
6415
|
-
write(
|
|
6693
|
+
write(path32.join(dir, "canvas.json"), canvasJson(title), root);
|
|
6694
|
+
write(path32.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
6416
6695
|
}
|
|
6417
6696
|
}
|
|
6418
6697
|
|
|
6419
6698
|
// src/cli/commands/shot.ts
|
|
6420
|
-
import
|
|
6421
|
-
import
|
|
6699
|
+
import fs36 from "fs";
|
|
6700
|
+
import path33 from "path";
|
|
6422
6701
|
var OVERVIEW_TIMEOUT = 9e4;
|
|
6423
6702
|
async function runShot(paths, target, options) {
|
|
6424
6703
|
const [canvasId = "", screenId] = target.split("/");
|
|
@@ -6434,15 +6713,15 @@ async function runShot(paths, target, options) {
|
|
|
6434
6713
|
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
6435
6714
|
}
|
|
6436
6715
|
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
6437
|
-
const outBase =
|
|
6438
|
-
const outDir =
|
|
6439
|
-
|
|
6716
|
+
const outBase = path33.resolve(options.out ?? paths.shots);
|
|
6717
|
+
const outDir = path33.join(outBase, canvasId);
|
|
6718
|
+
fs36.mkdirSync(outDir, { recursive: true });
|
|
6440
6719
|
const base = await serverBase(paths);
|
|
6441
6720
|
const browser = await launchChrome();
|
|
6442
6721
|
let failed = false;
|
|
6443
6722
|
try {
|
|
6444
6723
|
if (options.overview) {
|
|
6445
|
-
if (!
|
|
6724
|
+
if (!fs36.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
6446
6725
|
const pages = options.page ? [options.page] : screenId ? doc.pages.filter((page) => page.sections.some((section) => section.items.some((item) => item.id === screenId))).map((page) => page.id) : doc.pages.map((page) => page.id);
|
|
6447
6726
|
for (const pageId of pages) {
|
|
6448
6727
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
@@ -6458,7 +6737,7 @@ async function runShot(paths, target, options) {
|
|
|
6458
6737
|
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
6459
6738
|
}
|
|
6460
6739
|
await page.waitForTimeout(400);
|
|
6461
|
-
const file =
|
|
6740
|
+
const file = path33.join(outDir, `${pageId}.overview.png`);
|
|
6462
6741
|
await page.screenshot({ path: file });
|
|
6463
6742
|
await page.close();
|
|
6464
6743
|
print(file);
|
|
@@ -6487,7 +6766,7 @@ async function shootFrames(browser, jobs, outDir) {
|
|
|
6487
6766
|
let failed = false;
|
|
6488
6767
|
const results = await mapLimit(jobs, 4, async (job) => {
|
|
6489
6768
|
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
6490
|
-
const file =
|
|
6769
|
+
const file = path33.join(outDir, `${job.id}.${job.theme}.png`);
|
|
6491
6770
|
try {
|
|
6492
6771
|
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
6493
6772
|
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
@@ -6519,8 +6798,8 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
6519
6798
|
if (id2 && !all.some((job) => job.id === id2)) {
|
|
6520
6799
|
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
6521
6800
|
}
|
|
6522
|
-
const outDir =
|
|
6523
|
-
|
|
6801
|
+
const outDir = path33.join(path33.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
6802
|
+
fs36.mkdirSync(outDir, { recursive: true });
|
|
6524
6803
|
const base = await serverBase(paths);
|
|
6525
6804
|
const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
|
|
6526
6805
|
const browser = await launchChrome();
|
|
@@ -6531,8 +6810,36 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
6531
6810
|
}
|
|
6532
6811
|
}
|
|
6533
6812
|
|
|
6813
|
+
// src/cli/commands/snapshots.ts
|
|
6814
|
+
async function runSnapshots(paths, targets, options) {
|
|
6815
|
+
const known = new DesignProject(paths, DEV_URLS).canvasIds();
|
|
6816
|
+
const picked = /* @__PURE__ */ new Map();
|
|
6817
|
+
for (const target of targets) {
|
|
6818
|
+
const [canvas = "", screen] = target.split("/");
|
|
6819
|
+
if (!known.includes(canvas))
|
|
6820
|
+
throw new CliError(`No canvas "${canvas}". Canvases: ${known.join(", ") || "none yet"}`);
|
|
6821
|
+
const screens = picked.has(canvas) ? picked.get(canvas) : /* @__PURE__ */ new Set();
|
|
6822
|
+
picked.set(canvas, !screen || screens === null ? null : screens.add(screen));
|
|
6823
|
+
}
|
|
6824
|
+
if (options.page && picked.size !== 1)
|
|
6825
|
+
throw new CliError("--page needs exactly one canvas: `design snapshots <canvas> --page <id>`");
|
|
6826
|
+
const canvases = picked.size ? [...picked.keys()] : known;
|
|
6827
|
+
const stale = canvases.flatMap(
|
|
6828
|
+
(canvas) => staleSnapshots(paths, [canvas], { page: options.page, screens: picked.get(canvas) ?? void 0 })
|
|
6829
|
+
);
|
|
6830
|
+
if (!stale.length) return print(green("\u2713 Every screen has current snapshots in both themes."));
|
|
6831
|
+
print(dim(`${stale.length} snapshot${stale.length === 1 ? "" : "s"} to take`));
|
|
6832
|
+
const lines = await takeSnapshots(
|
|
6833
|
+
paths,
|
|
6834
|
+
stale,
|
|
6835
|
+
async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`,
|
|
6836
|
+
progress()
|
|
6837
|
+
);
|
|
6838
|
+
for (const line of lines) print(line);
|
|
6839
|
+
}
|
|
6840
|
+
|
|
6534
6841
|
// src/cli/commands/cloud-status.ts
|
|
6535
|
-
import
|
|
6842
|
+
import path34 from "path";
|
|
6536
6843
|
var STATE_TEXT = {
|
|
6537
6844
|
in_sync: green,
|
|
6538
6845
|
local_changes: yellow,
|
|
@@ -6555,7 +6862,7 @@ function detail(paths, status) {
|
|
|
6555
6862
|
if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
|
|
6556
6863
|
if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
|
|
6557
6864
|
parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
|
|
6558
|
-
if (status.conflictRev !== void 0) parts.push(`merge ${
|
|
6865
|
+
if (status.conflictRev !== void 0) parts.push(`merge ${path34.relative(paths.root, incomingDir(paths, status.key))}`);
|
|
6559
6866
|
return parts.join(" \xB7 ");
|
|
6560
6867
|
}
|
|
6561
6868
|
async function printCloudStatus(paths, label) {
|
|
@@ -6685,15 +6992,23 @@ ${bold("Preview")}
|
|
|
6685
6992
|
it; one from another version restarts) and print its URL;
|
|
6686
6993
|
--open also opens a browser
|
|
6687
6994
|
[--restart] [--foreground]
|
|
6688
|
-
stop
|
|
6995
|
+
stop [--all] Stop the viewer; --all stops every preview on this machine
|
|
6996
|
+
previews [--json] Every preview running on this machine: project, URL,
|
|
6997
|
+
version, uptime; orphans (a project's untracked server) marked
|
|
6689
6998
|
status Viewer state, a project summary and, with a cloud link, the
|
|
6690
6999
|
account, the linked project and each unit: in sync, local
|
|
6691
7000
|
changes, remote ahead, conflict or archived
|
|
6692
7001
|
|
|
6693
7002
|
${bold("Verify")}
|
|
6694
|
-
check [canvas\u2026] [--
|
|
6695
|
-
|
|
6696
|
-
errors and links that lead to no screen
|
|
7003
|
+
check [<canvas>[/<screen>]\u2026] [--page <id>] Validate canvas.json and the system (go() is an error);
|
|
7004
|
+
[--render [--built]] [--json] --render loads every screen in Chrome and reports runtime
|
|
7005
|
+
errors and links that lead to no screen; --built renders
|
|
7006
|
+
the production build design push uploads instead of the
|
|
7007
|
+
dev server. Narrow it to canvases, one page (--page) or
|
|
7008
|
+
single screens
|
|
7009
|
+
snapshots [<canvas>[/<screen>]\u2026] Take the snapshots screens lack, in both themes, for the
|
|
7010
|
+
[--page <id>] viewer to show while frames do not run (push does this
|
|
7011
|
+
for the canvases it pushes)
|
|
6697
7012
|
shot <canvas>[/<screen>] [--page <id>] Screenshot screens with Chrome (PNG paths are printed);
|
|
6698
7013
|
[--theme light|dark] [--out <dir>] @system or @system/<id> shoots the design system specimens
|
|
6699
7014
|
[--overview]
|
|
@@ -6712,7 +7027,7 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
|
|
|
6712
7027
|
push [canvas\u2026] [--json] Build and upload what changed here (or the named units); a
|
|
6713
7028
|
[--resolved <unit>] system change pushes every active canvas with it. When the
|
|
6714
7029
|
cloud is ahead it pulls and merges first, then pushes; it
|
|
6715
|
-
stops on conflicts.
|
|
7030
|
+
stops on conflicts. Takes missing snapshots (both themes).
|
|
6716
7031
|
Prints the web link of every pushed canvas; refuses screens
|
|
6717
7032
|
that still call go()
|
|
6718
7033
|
pull [canvas\u2026] [--json] Take cloud changes into .design; a unit changed on both sides
|
|
@@ -6742,7 +7057,7 @@ ${bold("Environment")}
|
|
|
6742
7057
|
FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
|
|
6743
7058
|
`;
|
|
6744
7059
|
function projectPaths(rootOption, requireDesign = true) {
|
|
6745
|
-
const found = rootOption ?
|
|
7060
|
+
const found = rootOption ? path35.resolve(rootOption) : findProjectRoot();
|
|
6746
7061
|
if (!found) {
|
|
6747
7062
|
if (!requireDesign) return designPaths(process.cwd());
|
|
6748
7063
|
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
@@ -6769,6 +7084,8 @@ async function main(argv) {
|
|
|
6769
7084
|
open: { type: "boolean" },
|
|
6770
7085
|
port: { type: "string" },
|
|
6771
7086
|
restart: { type: "boolean" },
|
|
7087
|
+
all: { type: "boolean" },
|
|
7088
|
+
built: { type: "boolean" },
|
|
6772
7089
|
foreground: { type: "boolean" },
|
|
6773
7090
|
render: { type: "boolean" },
|
|
6774
7091
|
json: { type: "boolean" },
|
|
@@ -6793,7 +7110,7 @@ async function main(argv) {
|
|
|
6793
7110
|
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
6794
7111
|
switch (command) {
|
|
6795
7112
|
case "init":
|
|
6796
|
-
return runInit(values.root ?
|
|
7113
|
+
return runInit(values.root ? path35.resolve(values.root) : process.cwd(), {
|
|
6797
7114
|
name: values.name,
|
|
6798
7115
|
gitignore: !values["no-gitignore"]
|
|
6799
7116
|
});
|
|
@@ -6831,11 +7148,20 @@ async function main(argv) {
|
|
|
6831
7148
|
});
|
|
6832
7149
|
}
|
|
6833
7150
|
case "stop":
|
|
6834
|
-
return runStop(projectPaths(values.root));
|
|
7151
|
+
return values.all ? runStopAll() : runStop(projectPaths(values.root));
|
|
7152
|
+
case "previews":
|
|
7153
|
+
return runPreviews({ json: !!values.json });
|
|
6835
7154
|
case "status":
|
|
6836
7155
|
return runStatus(projectPaths(values.root));
|
|
6837
7156
|
case "check":
|
|
6838
|
-
return runCheck(projectPaths(values.root), rest, {
|
|
7157
|
+
return runCheck(projectPaths(values.root), rest, {
|
|
7158
|
+
render: !!values.render,
|
|
7159
|
+
json: !!values.json,
|
|
7160
|
+
page: values.page,
|
|
7161
|
+
built: !!values.built
|
|
7162
|
+
});
|
|
7163
|
+
case "snapshots":
|
|
7164
|
+
return runSnapshots(projectPaths(values.root), rest, { page: values.page });
|
|
6839
7165
|
case "shot": {
|
|
6840
7166
|
if (!rest[0]) throw new CliError("Usage: design shot <canvas>[/<screen>] [--page <id>] [--theme light|dark]");
|
|
6841
7167
|
return runShot(projectPaths(values.root), rest[0], {
|
|
@@ -6884,20 +7210,42 @@ async function main(argv) {
|
|
|
6884
7210
|
throw new CliError(`Unknown command "${command}". Run \`design --help\`.`);
|
|
6885
7211
|
}
|
|
6886
7212
|
}
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
7213
|
+
function relaunchFromRealPath() {
|
|
7214
|
+
if (process.platform !== "win32" || process.env.FOSS_DESIGN_RELAUNCHED) return false;
|
|
7215
|
+
const script = process.argv[1];
|
|
7216
|
+
if (!script) return false;
|
|
7217
|
+
let realScript;
|
|
7218
|
+
let realCwd;
|
|
7219
|
+
try {
|
|
7220
|
+
realScript = fs37.realpathSync.native(script);
|
|
7221
|
+
realCwd = fs37.realpathSync.native(process.cwd());
|
|
7222
|
+
} catch {
|
|
7223
|
+
return false;
|
|
6893
7224
|
}
|
|
6894
|
-
|
|
6895
|
-
|
|
7225
|
+
const same = (a, b) => path35.resolve(a).toLowerCase() === path35.resolve(b).toLowerCase();
|
|
7226
|
+
if (same(realScript, script) && same(realCwd, process.cwd())) return false;
|
|
7227
|
+
const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
|
|
7228
|
+
stdio: "inherit",
|
|
7229
|
+
cwd: realCwd,
|
|
7230
|
+
env: { ...process.env, FOSS_DESIGN_RELAUNCHED: "1" }
|
|
7231
|
+
});
|
|
7232
|
+
process.exit(result.status ?? 1);
|
|
7233
|
+
}
|
|
7234
|
+
if (!relaunchFromRealPath())
|
|
7235
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
7236
|
+
endLine();
|
|
7237
|
+
if (error instanceof CliError) {
|
|
7238
|
+
process.stderr.write(`${red("error")} ${error.message}
|
|
7239
|
+
`);
|
|
7240
|
+
process.exit(error.code);
|
|
7241
|
+
}
|
|
7242
|
+
if (error.code?.startsWith("ERR_PARSE_ARGS")) {
|
|
7243
|
+
process.stderr.write(`${red("error")} ${error.message}
|
|
6896
7244
|
Run \`design --help\`.
|
|
6897
7245
|
`);
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
7246
|
+
process.exit(2);
|
|
7247
|
+
}
|
|
7248
|
+
process.stderr.write(`${red("error")} ${error?.stack ?? String(error)}
|
|
6901
7249
|
`);
|
|
6902
|
-
|
|
6903
|
-
});
|
|
7250
|
+
process.exit(1);
|
|
7251
|
+
});
|