foss-design 0.9.4 → 0.9.6
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 +456 -169
- package/dist/runtime/index.js +54 -0
- package/dist/viewer/assets/{index-CTraVnJV.js → index-Cg9mKtjz.js} +5 -5
- package/dist/viewer/assets/index-oN46u7re.css +2 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer-lib/index.js +37 -18
- package/dist/viewer-lib/styles.css +4 -0
- package/package.json +1 -1
- package/dist/viewer/assets/index-DCfxIE2h.css +0 -2
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. */
|
|
@@ -3273,51 +3275,84 @@ async function mapLimit(items, limit, fn) {
|
|
|
3273
3275
|
|
|
3274
3276
|
// src/capture/cover.ts
|
|
3275
3277
|
var SNAPSHOT_WIDTH = 900;
|
|
3276
|
-
|
|
3278
|
+
var TABS = 4;
|
|
3279
|
+
var THEMES2 = ["light", "dark"];
|
|
3280
|
+
function staleSnapshots(paths, canvasIds, filter = {}) {
|
|
3277
3281
|
const store = new SnapshotStore(paths.snapshots);
|
|
3278
3282
|
const project = new DesignProject(paths, DEV_URLS, store.lookup);
|
|
3279
3283
|
const stale = [];
|
|
3280
3284
|
for (const id2 of canvasIds) {
|
|
3281
3285
|
const canvas = project.canvas(id2);
|
|
3282
|
-
if (!canvas
|
|
3283
|
-
const
|
|
3284
|
-
const
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3286
|
+
if (!canvas) continue;
|
|
3287
|
+
const inLight = canvasFrames(canvas.doc, "", "light", { page: filter.page, includeUrls: false });
|
|
3288
|
+
const inDark = new Map(
|
|
3289
|
+
canvasFrames(canvas.doc, "", "dark", { page: filter.page, includeUrls: false }).map((job) => [job.id, job.theme])
|
|
3290
|
+
);
|
|
3291
|
+
const files = new Map(canvas.screens.map((screen) => [screen.id, screen.file]));
|
|
3292
|
+
for (const job of inLight) {
|
|
3293
|
+
if (filter.screens && !filter.screens.has(job.id)) continue;
|
|
3294
|
+
const file = files.get(job.id);
|
|
3295
|
+
if (!file || !fs19.existsSync(file)) continue;
|
|
3296
|
+
const changed = fs19.statSync(file).mtimeMs;
|
|
3297
|
+
const pinned = inDark.get(job.id) === job.theme;
|
|
3298
|
+
const themes = pinned ? [job.theme] : THEMES2;
|
|
3299
|
+
const screen = { id: job.id };
|
|
3300
|
+
for (const theme2 of themes) {
|
|
3301
|
+
const taken = store.version(id2, screen.id, theme2);
|
|
3302
|
+
if (!taken || taken < changed) stale.push({ canvas: id2, screen: screen.id, theme: theme2 });
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return stale;
|
|
3307
|
+
}
|
|
3308
|
+
async function takeSnapshots(paths, targets, base, progress2) {
|
|
3309
|
+
if (!targets.length) return [];
|
|
3310
|
+
if (!findChrome()) return ["No Chrome here, so screens keep the snapshots they have."];
|
|
3311
|
+
const store = new SnapshotStore(paths.snapshots);
|
|
3312
|
+
const project = new DesignProject(paths, DEV_URLS, store.lookup);
|
|
3291
3313
|
const url = await base();
|
|
3292
3314
|
const browser = await launchChrome();
|
|
3293
|
-
const
|
|
3315
|
+
const failed = [];
|
|
3316
|
+
const started = Date.now();
|
|
3317
|
+
let done = 0;
|
|
3318
|
+
const line = () => `Snapshots ${done}/${targets.length} (both themes, as the viewer and the cloud show them)`;
|
|
3319
|
+
progress2.update(line());
|
|
3294
3320
|
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(() => {
|
|
3321
|
+
await mapLimit([...targets], TABS, async ({ canvas, screen, theme: theme2 }) => {
|
|
3322
|
+
const job = canvasFrames(project.canvas(canvas).doc, url, theme2, { id: screen })[0];
|
|
3323
|
+
if (job) {
|
|
3324
|
+
const page = await browser.newPage({
|
|
3325
|
+
viewport: { width: job.width, height: job.height },
|
|
3326
|
+
deviceScaleFactor: Math.min(1, SNAPSHOT_WIDTH / job.width),
|
|
3327
|
+
colorScheme: theme2
|
|
3314
3328
|
});
|
|
3329
|
+
try {
|
|
3330
|
+
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
3331
|
+
if (report.ready) {
|
|
3332
|
+
const png = await page.screenshot({ fullPage: job.fullPage });
|
|
3333
|
+
const height = job.fullPage ? await page.evaluate(() => document.documentElement.scrollHeight) : void 0;
|
|
3334
|
+
store.save(canvas, screen, theme2, png, height);
|
|
3335
|
+
} else failed.push(`${canvas}/${screen} (${theme2})`);
|
|
3336
|
+
} catch {
|
|
3337
|
+
failed.push(`${canvas}/${screen} (${theme2})`);
|
|
3338
|
+
} finally {
|
|
3339
|
+
await page.close().catch(() => {
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3315
3342
|
}
|
|
3316
|
-
|
|
3343
|
+
done++;
|
|
3344
|
+
progress2.update(line());
|
|
3345
|
+
});
|
|
3317
3346
|
} finally {
|
|
3318
3347
|
await browser.close();
|
|
3319
3348
|
}
|
|
3320
|
-
|
|
3349
|
+
const seconds2 = Math.round((Date.now() - started) / 1e3);
|
|
3350
|
+
progress2.done(`Took ${targets.length - failed.length} snapshot${targets.length === 1 ? "" : "s"} in ${seconds2}s`);
|
|
3351
|
+
if (!failed.length) return [];
|
|
3352
|
+
const shown = failed.slice(0, 5).join(", ");
|
|
3353
|
+
return [
|
|
3354
|
+
`${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.`
|
|
3355
|
+
];
|
|
3321
3356
|
}
|
|
3322
3357
|
|
|
3323
3358
|
// src/cloud/pull.ts
|
|
@@ -3975,7 +4010,18 @@ async function push(ctx, options) {
|
|
|
3975
4010
|
bar.done(`Uploaded ${plural(missing.length, "file")} (${megabytes(bytes)}) in ${seconds(started)}`);
|
|
3976
4011
|
}
|
|
3977
4012
|
log(`Pushing ${plural(units.length, "unit")}\u2026`);
|
|
3978
|
-
|
|
4013
|
+
let revs2;
|
|
4014
|
+
try {
|
|
4015
|
+
revs2 = await client.push(link.project, units);
|
|
4016
|
+
} catch (error) {
|
|
4017
|
+
if (!(error instanceof CloudError) || error.status !== 409 || error.error !== "conflict") throw error;
|
|
4018
|
+
report.ok = false;
|
|
4019
|
+
report.error = {
|
|
4020
|
+
code: "pull_first",
|
|
4021
|
+
message: "Nothing was pushed: someone pushed first. Run `design pull`, then push again."
|
|
4022
|
+
};
|
|
4023
|
+
return report;
|
|
4024
|
+
}
|
|
3979
4025
|
for (const unit of units) {
|
|
3980
4026
|
const rev = revs2.get(unit.key) ?? unit.baseRev + 1;
|
|
3981
4027
|
link.units[unit.key] = { rev, files: hashes(unit.source) };
|
|
@@ -4174,6 +4220,7 @@ var EventHub = class {
|
|
|
4174
4220
|
|
|
4175
4221
|
// src/server/state.ts
|
|
4176
4222
|
import fs26 from "fs";
|
|
4223
|
+
import os3 from "os";
|
|
4177
4224
|
import path24 from "path";
|
|
4178
4225
|
var stateFile2 = (paths) => path24.join(paths.cache, "server.json");
|
|
4179
4226
|
var logFile = (paths) => path24.join(paths.cache, "server.log");
|
|
@@ -4184,16 +4231,71 @@ function readState(paths) {
|
|
|
4184
4231
|
return null;
|
|
4185
4232
|
}
|
|
4186
4233
|
}
|
|
4234
|
+
function registryDir() {
|
|
4235
|
+
const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path24.join(os3.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path24.join(os3.homedir(), ".cache");
|
|
4236
|
+
return path24.join(base, "foss-design", "previews");
|
|
4237
|
+
}
|
|
4238
|
+
var entryFile = (pid) => path24.join(registryDir(), `${pid}.json`);
|
|
4187
4239
|
function writeState(paths, state) {
|
|
4188
4240
|
fs26.mkdirSync(paths.cache, { recursive: true });
|
|
4189
4241
|
fs26.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
|
|
4190
4242
|
`);
|
|
4243
|
+
try {
|
|
4244
|
+
fs26.mkdirSync(registryDir(), { recursive: true });
|
|
4245
|
+
fs26.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
|
|
4246
|
+
`);
|
|
4247
|
+
} catch {
|
|
4248
|
+
}
|
|
4191
4249
|
}
|
|
4192
4250
|
function clearState(paths, pid) {
|
|
4193
4251
|
const state = readState(paths);
|
|
4252
|
+
fs26.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
|
|
4194
4253
|
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
4195
4254
|
fs26.rmSync(stateFile2(paths), { force: true });
|
|
4196
4255
|
}
|
|
4256
|
+
function listPreviews() {
|
|
4257
|
+
let files = [];
|
|
4258
|
+
try {
|
|
4259
|
+
files = fs26.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
|
|
4260
|
+
} catch {
|
|
4261
|
+
return [];
|
|
4262
|
+
}
|
|
4263
|
+
const found = [];
|
|
4264
|
+
for (const name of files) {
|
|
4265
|
+
const file = path24.join(registryDir(), name);
|
|
4266
|
+
let state = null;
|
|
4267
|
+
try {
|
|
4268
|
+
state = JSON.parse(fs26.readFileSync(file, "utf8"));
|
|
4269
|
+
} catch {
|
|
4270
|
+
}
|
|
4271
|
+
if (!state?.pid || !pidAlive(state.pid)) {
|
|
4272
|
+
fs26.rmSync(file, { force: true });
|
|
4273
|
+
continue;
|
|
4274
|
+
}
|
|
4275
|
+
let recorded = null;
|
|
4276
|
+
try {
|
|
4277
|
+
recorded = JSON.parse(fs26.readFileSync(path24.join(state.root, ".design", ".cache", "server.json"), "utf8"));
|
|
4278
|
+
} catch {
|
|
4279
|
+
}
|
|
4280
|
+
found.push({ ...state, tracked: recorded?.pid === state.pid });
|
|
4281
|
+
}
|
|
4282
|
+
return found.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
4283
|
+
}
|
|
4284
|
+
async function stopPreview(entry) {
|
|
4285
|
+
try {
|
|
4286
|
+
process.kill(entry.pid, "SIGTERM");
|
|
4287
|
+
} catch {
|
|
4288
|
+
}
|
|
4289
|
+
const until = Date.now() + 5e3;
|
|
4290
|
+
while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4291
|
+
fs26.rmSync(entryFile(entry.pid), { force: true });
|
|
4292
|
+
const stateAt = path24.join(entry.root, ".design", ".cache", "server.json");
|
|
4293
|
+
try {
|
|
4294
|
+
if (JSON.parse(fs26.readFileSync(stateAt, "utf8")).pid === entry.pid)
|
|
4295
|
+
fs26.rmSync(stateAt, { force: true });
|
|
4296
|
+
} catch {
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4197
4299
|
function pidAlive(pid) {
|
|
4198
4300
|
try {
|
|
4199
4301
|
process.kill(pid, 0);
|
|
@@ -4487,7 +4589,7 @@ async function startDevServer(options) {
|
|
|
4487
4589
|
// src/cli/commands/preview.ts
|
|
4488
4590
|
function openBrowser(url) {
|
|
4489
4591
|
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 });
|
|
4592
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
4491
4593
|
child.on("error", () => {
|
|
4492
4594
|
});
|
|
4493
4595
|
child.unref();
|
|
@@ -4535,11 +4637,19 @@ async function ensureServer(paths, options) {
|
|
|
4535
4637
|
if (stale)
|
|
4536
4638
|
print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
|
|
4537
4639
|
if (live2) await stopServer(paths);
|
|
4640
|
+
const recorded = readState(paths);
|
|
4641
|
+
if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
|
|
4642
|
+
for (const other of listPreviews()) if (other.root === paths.root) await stopPreview(other);
|
|
4538
4643
|
fs28.mkdirSync(paths.cache, { recursive: true });
|
|
4539
4644
|
const log = fs28.openSync(logFile(paths), "w");
|
|
4540
4645
|
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
4541
4646
|
if (options.port) args.push("--port", String(options.port));
|
|
4542
|
-
const child = spawn(process.execPath, args, {
|
|
4647
|
+
const child = spawn(process.execPath, args, {
|
|
4648
|
+
detached: true,
|
|
4649
|
+
windowsHide: true,
|
|
4650
|
+
stdio: ["ignore", log, log],
|
|
4651
|
+
cwd: paths.root
|
|
4652
|
+
});
|
|
4543
4653
|
child.unref();
|
|
4544
4654
|
const state = await waitForServer(paths, 4e4);
|
|
4545
4655
|
if (!state) {
|
|
@@ -4569,6 +4679,27 @@ async function runPreview(paths, options) {
|
|
|
4569
4679
|
async function runStop(paths) {
|
|
4570
4680
|
print(await stopServer(paths) ? "Stopped the preview server." : "No preview server is running for this project.");
|
|
4571
4681
|
}
|
|
4682
|
+
var since = (iso) => {
|
|
4683
|
+
const minutes = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 6e4));
|
|
4684
|
+
return minutes < 60 ? `${minutes}m` : `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
4685
|
+
};
|
|
4686
|
+
function runPreviews(options) {
|
|
4687
|
+
const previews = listPreviews();
|
|
4688
|
+
if (options.json) return print(JSON.stringify({ previews }, null, 2));
|
|
4689
|
+
if (!previews.length) return print("No preview servers are running.");
|
|
4690
|
+
for (const preview of previews) {
|
|
4691
|
+
const orphan = preview.tracked ? "" : ` ${bold("orphan")} ${dim("(its project names another server)")}`;
|
|
4692
|
+
print(`${preview.url} ${preview.root}${orphan}`);
|
|
4693
|
+
print(dim(` foss-design ${preview.version} \xB7 pid ${preview.pid} \xB7 up ${since(preview.startedAt)}`));
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
async function runStopAll() {
|
|
4697
|
+
const previews = listPreviews();
|
|
4698
|
+
for (const preview of previews) await stopPreview(preview);
|
|
4699
|
+
print(
|
|
4700
|
+
previews.length ? `Stopped ${previews.length} preview server${previews.length === 1 ? "" : "s"}.` : "No preview servers are running."
|
|
4701
|
+
);
|
|
4702
|
+
}
|
|
4572
4703
|
|
|
4573
4704
|
// src/cli/commands/sync.ts
|
|
4574
4705
|
var GOOD = /* @__PURE__ */ new Set(["pushed", "created", "updated", "deleted"]);
|
|
@@ -4649,11 +4780,19 @@ async function runSync(paths, command, json, run) {
|
|
|
4649
4780
|
if (!report.ok && report.error) throw new CliError(report.error.message, 2);
|
|
4650
4781
|
}
|
|
4651
4782
|
function runPush(paths, units, options) {
|
|
4652
|
-
const beforeBuild = (canvasIds) => refreshCovers(paths, canvasIds, async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`);
|
|
4653
4783
|
return runSync(paths, "push", options.json, async (ctx) => {
|
|
4784
|
+
const beforeBuild = (canvasIds) => takeSnapshots(
|
|
4785
|
+
paths,
|
|
4786
|
+
staleSnapshots(paths, canvasIds),
|
|
4787
|
+
async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`,
|
|
4788
|
+
ctx.progress()
|
|
4789
|
+
);
|
|
4654
4790
|
const first = await push(ctx, { units, resolved: options.resolved, beforeBuild });
|
|
4655
4791
|
if (first.ok || first.error?.code !== "pull_first") return first;
|
|
4656
|
-
|
|
4792
|
+
const raced = first.error.message.includes("someone pushed first");
|
|
4793
|
+
ctx.log(
|
|
4794
|
+
raced ? "Someone pushed while this push was uploading: merging their changes first\u2026" : "The cloud is ahead: merging its changes first\u2026"
|
|
4795
|
+
);
|
|
4657
4796
|
const pulled = await pull(ctx, { units: [], theirs: [] });
|
|
4658
4797
|
if (!pulled.ok) {
|
|
4659
4798
|
pulled.error = {
|
|
@@ -4665,7 +4804,7 @@ function runPush(paths, units, options) {
|
|
|
4665
4804
|
ctx.link = readLink(paths);
|
|
4666
4805
|
const second = await push(ctx, { units, resolved: [], beforeBuild });
|
|
4667
4806
|
second.hints.unshift(
|
|
4668
|
-
|
|
4807
|
+
`${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
4808
|
);
|
|
4670
4809
|
return second;
|
|
4671
4810
|
});
|
|
@@ -4896,6 +5035,10 @@ async function runMe(paths, options) {
|
|
|
4896
5035
|
}
|
|
4897
5036
|
|
|
4898
5037
|
// src/cli/commands/check.ts
|
|
5038
|
+
import fs29 from "fs";
|
|
5039
|
+
import http2 from "http";
|
|
5040
|
+
import path26 from "path";
|
|
5041
|
+
import sirv2 from "sirv";
|
|
4899
5042
|
var plural2 = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
|
|
4900
5043
|
function printIssues(issues) {
|
|
4901
5044
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -4935,29 +5078,103 @@ async function renderFrames(jobs) {
|
|
|
4935
5078
|
await browser.close();
|
|
4936
5079
|
}
|
|
4937
5080
|
}
|
|
4938
|
-
async function runCheck(paths,
|
|
5081
|
+
async function runCheck(paths, targets, options) {
|
|
4939
5082
|
const project = new DesignProject(paths, DEV_URLS);
|
|
4940
5083
|
const known = project.canvasIds();
|
|
4941
|
-
const
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
5084
|
+
const picked = /* @__PURE__ */ new Map();
|
|
5085
|
+
for (const target of targets) {
|
|
5086
|
+
const [canvasId = "", screenId] = target.split("/");
|
|
5087
|
+
if (!known.includes(canvasId))
|
|
5088
|
+
throw new CliError(`No canvas "${canvasId}". Canvases: ${known.join(", ") || "none yet"}`);
|
|
5089
|
+
const screens = picked.has(canvasId) ? picked.get(canvasId) : /* @__PURE__ */ new Set();
|
|
5090
|
+
if (!screenId || screens === null) picked.set(canvasId, null);
|
|
5091
|
+
else picked.set(canvasId, screens.add(screenId));
|
|
5092
|
+
}
|
|
5093
|
+
const canvases = [...picked.keys()];
|
|
5094
|
+
if (options.page) {
|
|
5095
|
+
if (canvases.length !== 1)
|
|
5096
|
+
throw new CliError("--page needs exactly one canvas: `design check <canvas> --page <id>`");
|
|
5097
|
+
const doc = project.canvas(canvases[0]).doc;
|
|
5098
|
+
if (!doc.pages.some((page) => page.id === options.page))
|
|
5099
|
+
throw new CliError(`No page "${options.page}" in "${doc.id}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
5100
|
+
}
|
|
5101
|
+
for (const [canvasId, screens] of picked) {
|
|
5102
|
+
if (!screens) continue;
|
|
5103
|
+
const doc = project.canvas(canvasId).doc;
|
|
5104
|
+
const ids = new Set(canvasFrames(doc, "", "light", { page: options.page, skipMissing: false }).map((job) => job.id));
|
|
5105
|
+
const missing = [...screens].filter((id2) => !ids.has(id2));
|
|
5106
|
+
if (missing.length)
|
|
5107
|
+
throw new CliError(
|
|
5108
|
+
`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" : ""}`
|
|
5109
|
+
);
|
|
4946
5110
|
}
|
|
4947
5111
|
const issues = canvases.length ? canvases.flatMap((id2) => project.canvas(id2)?.doc.issues ?? []) : project.issues();
|
|
4948
5112
|
let frames = [];
|
|
4949
5113
|
if (options.render) {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
5114
|
+
let base;
|
|
5115
|
+
let docOf = (id2) => project.canvas(id2).doc;
|
|
5116
|
+
let systemDoc = project.system()?.doc ?? null;
|
|
5117
|
+
let close = async () => {
|
|
5118
|
+
};
|
|
5119
|
+
if (options.built) {
|
|
5120
|
+
const out = path26.join(paths.cache, "check-build");
|
|
5121
|
+
fs29.rmSync(out, { recursive: true, force: true });
|
|
5122
|
+
if (!options.json) print(dim("Building the screens as design push does\u2026"));
|
|
5123
|
+
await buildSite(paths, out, {
|
|
5124
|
+
canvases: canvases.length ? canvases : void 0,
|
|
5125
|
+
includeSystem: !canvases.length,
|
|
5126
|
+
includeViewer: false
|
|
5127
|
+
});
|
|
5128
|
+
const read = (rel) => {
|
|
5129
|
+
try {
|
|
5130
|
+
return JSON.parse(fs29.readFileSync(path26.join(out, rel), "utf8"));
|
|
5131
|
+
} catch {
|
|
5132
|
+
return null;
|
|
5133
|
+
}
|
|
5134
|
+
};
|
|
5135
|
+
docOf = (id2) => read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
|
|
5136
|
+
systemDoc = canvases.length ? null : read("api/system.json");
|
|
5137
|
+
const files = sirv2(out, { dev: true });
|
|
5138
|
+
const server = http2.createServer((req, res) => {
|
|
5139
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
5140
|
+
const segments = url.pathname.split("/").map((segment) => {
|
|
5141
|
+
try {
|
|
5142
|
+
return decodeURIComponent(segment);
|
|
5143
|
+
} catch {
|
|
5144
|
+
return segment;
|
|
5145
|
+
}
|
|
5146
|
+
});
|
|
5147
|
+
req.url = segments.join("/") + url.search;
|
|
5148
|
+
files(req, res, () => {
|
|
5149
|
+
res.statusCode = 404;
|
|
5150
|
+
res.end("Not found");
|
|
5151
|
+
});
|
|
5152
|
+
});
|
|
5153
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
5154
|
+
base = `http://127.0.0.1:${server.address().port}`;
|
|
5155
|
+
close = () => new Promise((resolve) => server.close(() => resolve()));
|
|
5156
|
+
if (!options.json) print(dim(`Serving the build at ${base}`));
|
|
5157
|
+
} else {
|
|
5158
|
+
const wasRunning = await liveServer(paths);
|
|
5159
|
+
const server = await ensureServer(paths, {});
|
|
5160
|
+
if (!wasRunning && !options.json)
|
|
5161
|
+
print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
|
|
5162
|
+
base = `http://127.0.0.1:${server.port}`;
|
|
5163
|
+
}
|
|
5164
|
+
const jobs = (canvases.length ? canvases : known).flatMap((id2) => {
|
|
5165
|
+
const doc = docOf(id2);
|
|
5166
|
+
const theme2 = doc.theme ?? "light";
|
|
5167
|
+
const screens = picked.get(id2);
|
|
5168
|
+
const filter = { includeUrls: false, page: options.page };
|
|
5169
|
+
return screens ? [...screens].flatMap((screen) => canvasFrames(doc, base, theme2, { ...filter, id: screen })) : canvasFrames(doc, base, theme2, filter);
|
|
5170
|
+
});
|
|
5171
|
+
if (!canvases.length && systemDoc) jobs.unshift(...systemFrames(systemDoc, base, "light"));
|
|
5172
|
+
if (!options.json) print(dim(`Loading ${plural2(jobs.length, "frame")} in Chrome\u2026`));
|
|
5173
|
+
try {
|
|
5174
|
+
frames = await renderFrames(jobs);
|
|
5175
|
+
} finally {
|
|
5176
|
+
await close();
|
|
5177
|
+
}
|
|
4961
5178
|
}
|
|
4962
5179
|
const linkIssues = options.render ? unroutedLinks(frames, project, canvases.length ? canvases : known) : [];
|
|
4963
5180
|
const errors = issues.filter((issue) => issue.severity === "error").length;
|
|
@@ -4978,7 +5195,8 @@ async function runCheck(paths, canvases, options) {
|
|
|
4978
5195
|
}
|
|
4979
5196
|
}
|
|
4980
5197
|
if (linkIssues.length) printLinkIssues(linkIssues, project);
|
|
4981
|
-
const
|
|
5198
|
+
const screens = [...picked.values()].reduce((sum, set) => sum + (set?.size ?? 0), 0);
|
|
5199
|
+
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
5200
|
if (!errors && !warnings && !broken.length) {
|
|
4983
5201
|
print(
|
|
4984
5202
|
green(
|
|
@@ -5047,14 +5265,14 @@ function printLinkIssues(issues, project) {
|
|
|
5047
5265
|
}
|
|
5048
5266
|
|
|
5049
5267
|
// src/cli/commands/cloud.ts
|
|
5050
|
-
import
|
|
5051
|
-
import
|
|
5052
|
-
import
|
|
5268
|
+
import fs30 from "fs";
|
|
5269
|
+
import os4 from "os";
|
|
5270
|
+
import path27 from "path";
|
|
5053
5271
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5054
5272
|
async function runLogin() {
|
|
5055
5273
|
const host = cloudHost();
|
|
5056
5274
|
const client = new CloudClient(host, null);
|
|
5057
|
-
const code = await client.deviceCode(
|
|
5275
|
+
const code = await client.deviceCode(os4.hostname());
|
|
5058
5276
|
const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
|
|
5059
5277
|
const expiresIn = code.expires_in ?? 600;
|
|
5060
5278
|
print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);
|
|
@@ -5150,7 +5368,7 @@ async function runLink(paths, wanted, options) {
|
|
|
5150
5368
|
print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
|
|
5151
5369
|
} else {
|
|
5152
5370
|
writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
|
|
5153
|
-
|
|
5371
|
+
fs30.rmSync(path27.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
|
|
5154
5372
|
const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
|
|
5155
5373
|
print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
|
|
5156
5374
|
}
|
|
@@ -5177,8 +5395,8 @@ async function runLink(paths, wanted, options) {
|
|
|
5177
5395
|
}
|
|
5178
5396
|
|
|
5179
5397
|
// src/cli/commands/icon.ts
|
|
5180
|
-
import
|
|
5181
|
-
import
|
|
5398
|
+
import fs31 from "fs";
|
|
5399
|
+
import path28 from "path";
|
|
5182
5400
|
async function toCloud(paths, file) {
|
|
5183
5401
|
const link = readLink(paths);
|
|
5184
5402
|
if (!link) return;
|
|
@@ -5189,10 +5407,10 @@ async function toCloud(paths, file) {
|
|
|
5189
5407
|
}
|
|
5190
5408
|
const client = new CloudClient(link.host, credential.token);
|
|
5191
5409
|
try {
|
|
5192
|
-
const ext = file ?
|
|
5410
|
+
const ext = file ? path28.extname(file).slice(1) : null;
|
|
5193
5411
|
await client.setProjectIcon(
|
|
5194
5412
|
link.project,
|
|
5195
|
-
file && ext ? { data:
|
|
5413
|
+
file && ext ? { data: fs31.readFileSync(file), type: ICON_TYPES[ext] } : null
|
|
5196
5414
|
);
|
|
5197
5415
|
link.icon = file ? iconHash(file) : null;
|
|
5198
5416
|
writeLink(paths, link);
|
|
@@ -5205,35 +5423,35 @@ async function runIcon(paths, file, options) {
|
|
|
5205
5423
|
if (options.remove) {
|
|
5206
5424
|
const had = iconFile(paths);
|
|
5207
5425
|
writeIcon(paths, null);
|
|
5208
|
-
print(had ? `${green("Removed")} ${
|
|
5426
|
+
print(had ? `${green("Removed")} ${path28.relative(process.cwd(), had)}` : "The project has no icon.");
|
|
5209
5427
|
await toCloud(paths, null);
|
|
5210
5428
|
return;
|
|
5211
5429
|
}
|
|
5212
5430
|
if (!file) {
|
|
5213
5431
|
const current = iconFile(paths);
|
|
5214
5432
|
if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
|
|
5215
|
-
return print(`${
|
|
5433
|
+
return print(`${path28.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs31.statSync(current).size / 1024)} KB`)}`);
|
|
5216
5434
|
}
|
|
5217
5435
|
let data;
|
|
5218
5436
|
try {
|
|
5219
|
-
data =
|
|
5437
|
+
data = fs31.readFileSync(file);
|
|
5220
5438
|
} catch {
|
|
5221
5439
|
throw new CliError(`No file ${file}`);
|
|
5222
5440
|
}
|
|
5223
|
-
const problem = iconProblem(data,
|
|
5441
|
+
const problem = iconProblem(data, path28.extname(file).slice(1).toLowerCase() || void 0);
|
|
5224
5442
|
if (problem) throw new CliError(`${file}: ${problem}`);
|
|
5225
5443
|
const written = writeIcon(paths, data);
|
|
5226
|
-
print(`${green("Set")} the project icon: ${
|
|
5444
|
+
print(`${green("Set")} the project icon: ${path28.relative(process.cwd(), written)}`);
|
|
5227
5445
|
await toCloud(paths, written);
|
|
5228
5446
|
}
|
|
5229
5447
|
|
|
5230
5448
|
// src/cli/commands/import.ts
|
|
5231
|
-
import
|
|
5232
|
-
import
|
|
5449
|
+
import fs33 from "fs";
|
|
5450
|
+
import path30 from "path";
|
|
5233
5451
|
|
|
5234
5452
|
// src/import/claude-design.ts
|
|
5235
|
-
import
|
|
5236
|
-
import
|
|
5453
|
+
import fs32 from "fs";
|
|
5454
|
+
import path29 from "path";
|
|
5237
5455
|
|
|
5238
5456
|
// src/import/markup.ts
|
|
5239
5457
|
var VOID = /* @__PURE__ */ new Set([
|
|
@@ -5841,11 +6059,11 @@ function boardId(file, used) {
|
|
|
5841
6059
|
return id2;
|
|
5842
6060
|
}
|
|
5843
6061
|
function importClaudeDesign(dir, options = {}) {
|
|
5844
|
-
const canvasFile =
|
|
5845
|
-
if (!
|
|
6062
|
+
const canvasFile = path29.join(dir, "canvas.json");
|
|
6063
|
+
if (!fs32.existsSync(canvasFile))
|
|
5846
6064
|
throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
|
|
5847
|
-
const source = JSON.parse(
|
|
5848
|
-
const templates =
|
|
6065
|
+
const source = JSON.parse(fs32.readFileSync(canvasFile, "utf8"));
|
|
6066
|
+
const templates = fs32.readdirSync(dir).filter((file) => file.endsWith(EXT));
|
|
5849
6067
|
if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
|
|
5850
6068
|
const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
|
|
5851
6069
|
const warnings = /* @__PURE__ */ new Map();
|
|
@@ -5872,7 +6090,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5872
6090
|
try {
|
|
5873
6091
|
files.set(
|
|
5874
6092
|
`${name}.jsx`,
|
|
5875
|
-
convertFile(name,
|
|
6093
|
+
convertFile(name, fs32.readFileSync(path29.join(dir, file), "utf8"), { known, routes, warn: warn2 })
|
|
5876
6094
|
);
|
|
5877
6095
|
} catch (error) {
|
|
5878
6096
|
warn2(`could not be converted: ${error.message}`);
|
|
@@ -5920,7 +6138,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5920
6138
|
screens++;
|
|
5921
6139
|
}
|
|
5922
6140
|
const canvas = {
|
|
5923
|
-
title: options.title ?? source.title ??
|
|
6141
|
+
title: options.title ?? source.title ?? path29.basename(path29.resolve(dir)),
|
|
5924
6142
|
description: "Imported from Claude Design.",
|
|
5925
6143
|
// The templates bring their own styles; the design system's reset would restyle them.
|
|
5926
6144
|
system: false,
|
|
@@ -5932,21 +6150,21 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
5932
6150
|
// src/cli/commands/import.ts
|
|
5933
6151
|
var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
|
|
5934
6152
|
function runImport(paths, from, options) {
|
|
5935
|
-
const dir =
|
|
5936
|
-
if (!
|
|
6153
|
+
const dir = path30.resolve(from);
|
|
6154
|
+
if (!fs33.existsSync(dir) || !fs33.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
|
|
5937
6155
|
let result;
|
|
5938
6156
|
try {
|
|
5939
6157
|
result = importClaudeDesign(dir, { title: options.title });
|
|
5940
6158
|
} catch (error) {
|
|
5941
6159
|
throw new CliError(error.message);
|
|
5942
6160
|
}
|
|
5943
|
-
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(
|
|
6161
|
+
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path30.basename(dir)) || "imported");
|
|
5944
6162
|
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
|
-
|
|
6163
|
+
const out = path30.join(paths.canvases, id2);
|
|
6164
|
+
if (fs33.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
|
|
6165
|
+
fs33.mkdirSync(path30.join(out, "screens"), { recursive: true });
|
|
6166
|
+
for (const [name, content] of result.files) fs33.writeFileSync(path30.join(out, "screens", name), content);
|
|
6167
|
+
fs33.writeFileSync(path30.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
|
|
5950
6168
|
`);
|
|
5951
6169
|
print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
|
|
5952
6170
|
print(
|
|
@@ -5966,7 +6184,7 @@ function runImport(paths, from, options) {
|
|
|
5966
6184
|
|
|
5967
6185
|
// src/cli/commands/merge.ts
|
|
5968
6186
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
5969
|
-
import
|
|
6187
|
+
import path31 from "path";
|
|
5970
6188
|
import { createInterface } from "readline/promises";
|
|
5971
6189
|
var KIND = {
|
|
5972
6190
|
text: "both sides changed lines, the markers show them",
|
|
@@ -5982,7 +6200,7 @@ function matchTargets(paths, states, targets) {
|
|
|
5982
6200
|
for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
|
|
5983
6201
|
continue;
|
|
5984
6202
|
}
|
|
5985
|
-
const rel = [
|
|
6203
|
+
const rel = [path31.relative(paths.design, path31.resolve(target)), target.replace(/^\.design\//, "")];
|
|
5986
6204
|
const found = states.flatMap(
|
|
5987
6205
|
(state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
|
|
5988
6206
|
);
|
|
@@ -5998,7 +6216,7 @@ function printOpen(paths, states) {
|
|
|
5998
6216
|
print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
|
|
5999
6217
|
for (const conflict of open2) {
|
|
6000
6218
|
print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
|
|
6001
|
-
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${
|
|
6219
|
+
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path31.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
|
|
6002
6220
|
print(` ${dim(sides)}`);
|
|
6003
6221
|
}
|
|
6004
6222
|
}
|
|
@@ -6079,7 +6297,7 @@ async function runMerge(paths, targets, options) {
|
|
|
6079
6297
|
if (answer === "s") break;
|
|
6080
6298
|
if (answer === "e" && edit) {
|
|
6081
6299
|
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
6082
|
-
spawnSync2(editor, [
|
|
6300
|
+
spawnSync2(editor, [path31.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
|
|
6083
6301
|
if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
|
|
6084
6302
|
print(yellow(" Conflict markers are still there."));
|
|
6085
6303
|
}
|
|
@@ -6098,7 +6316,7 @@ async function runMerge(paths, targets, options) {
|
|
|
6098
6316
|
}
|
|
6099
6317
|
|
|
6100
6318
|
// src/cli/commands/rename.ts
|
|
6101
|
-
import
|
|
6319
|
+
import fs34 from "fs";
|
|
6102
6320
|
async function runRename(paths, name) {
|
|
6103
6321
|
const trimmed = name.trim();
|
|
6104
6322
|
if (!trimmed) throw new CliError("Usage: design rename <name>");
|
|
@@ -6113,17 +6331,17 @@ async function runRename(paths, name) {
|
|
|
6113
6331
|
}
|
|
6114
6332
|
let config = {};
|
|
6115
6333
|
try {
|
|
6116
|
-
config = JSON.parse(
|
|
6334
|
+
config = JSON.parse(fs34.readFileSync(paths.config, "utf8"));
|
|
6117
6335
|
} catch {
|
|
6118
6336
|
}
|
|
6119
|
-
|
|
6337
|
+
fs34.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
|
|
6120
6338
|
`);
|
|
6121
6339
|
print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
|
|
6122
6340
|
}
|
|
6123
6341
|
|
|
6124
6342
|
// src/cli/commands/scaffold.ts
|
|
6125
|
-
import
|
|
6126
|
-
import
|
|
6343
|
+
import fs35 from "fs";
|
|
6344
|
+
import path32 from "path";
|
|
6127
6345
|
|
|
6128
6346
|
// src/cli/templates.ts
|
|
6129
6347
|
var TOKENS_CSS = `/*
|
|
@@ -6334,40 +6552,40 @@ function mainScreen(canvasId) {
|
|
|
6334
6552
|
|
|
6335
6553
|
// src/cli/commands/scaffold.ts
|
|
6336
6554
|
function write(file, content, root) {
|
|
6337
|
-
if (
|
|
6338
|
-
print(` ${dim("exists ")} ${
|
|
6555
|
+
if (fs35.existsSync(file)) {
|
|
6556
|
+
print(` ${dim("exists ")} ${path32.relative(root, file)}`);
|
|
6339
6557
|
return false;
|
|
6340
6558
|
}
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
print(` ${green("created")} ${
|
|
6559
|
+
fs35.mkdirSync(path32.dirname(file), { recursive: true });
|
|
6560
|
+
fs35.writeFileSync(file, content);
|
|
6561
|
+
print(` ${green("created")} ${path32.relative(root, file)}`);
|
|
6344
6562
|
return true;
|
|
6345
6563
|
}
|
|
6346
6564
|
function ensureGitignored(root) {
|
|
6347
|
-
const file =
|
|
6348
|
-
const current =
|
|
6565
|
+
const file = path32.join(root, ".gitignore");
|
|
6566
|
+
const current = fs35.existsSync(file) ? fs35.readFileSync(file, "utf8") : "";
|
|
6349
6567
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
6350
6568
|
if (covered) return "present";
|
|
6351
6569
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
6352
|
-
|
|
6570
|
+
fs35.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
6353
6571
|
.design/
|
|
6354
6572
|
`);
|
|
6355
6573
|
return "added";
|
|
6356
6574
|
}
|
|
6357
6575
|
function projectName(root) {
|
|
6358
6576
|
try {
|
|
6359
|
-
const manifest = JSON.parse(
|
|
6577
|
+
const manifest = JSON.parse(fs35.readFileSync(path32.join(root, "package.json"), "utf8"));
|
|
6360
6578
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
6361
6579
|
} catch {
|
|
6362
6580
|
}
|
|
6363
|
-
return
|
|
6581
|
+
return path32.basename(root);
|
|
6364
6582
|
}
|
|
6365
6583
|
function runInit(root, options) {
|
|
6366
6584
|
const paths = designPaths(root);
|
|
6367
6585
|
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
6368
6586
|
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
6369
6587
|
`, root);
|
|
6370
|
-
|
|
6588
|
+
fs35.mkdirSync(paths.canvases, { recursive: true });
|
|
6371
6589
|
if (options.gitignore) {
|
|
6372
6590
|
const result = ensureGitignored(root);
|
|
6373
6591
|
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
@@ -6381,44 +6599,44 @@ function runSystemInit(root, options) {
|
|
|
6381
6599
|
const paths = designPaths(root);
|
|
6382
6600
|
let base = projectName(root);
|
|
6383
6601
|
try {
|
|
6384
|
-
base = JSON.parse(
|
|
6602
|
+
base = JSON.parse(fs35.readFileSync(paths.config, "utf8")).name || base;
|
|
6385
6603
|
} catch {
|
|
6386
6604
|
}
|
|
6387
6605
|
const name = options.name ?? `${base} design system`;
|
|
6388
|
-
print(`Scaffolding the design system in ${
|
|
6389
|
-
write(
|
|
6606
|
+
print(`Scaffolding the design system in ${path32.relative(root, paths.system)}`);
|
|
6607
|
+
write(path32.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
6390
6608
|
`, root);
|
|
6391
|
-
write(
|
|
6609
|
+
write(path32.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
6392
6610
|
if (!options.empty) {
|
|
6393
|
-
write(
|
|
6394
|
-
write(
|
|
6395
|
-
write(
|
|
6611
|
+
write(path32.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
6612
|
+
write(path32.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
6613
|
+
write(path32.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
6396
6614
|
}
|
|
6397
6615
|
}
|
|
6398
6616
|
function runNew(root, id2, options) {
|
|
6399
6617
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
6400
6618
|
const paths = designPaths(root);
|
|
6401
|
-
const dir =
|
|
6402
|
-
if (
|
|
6619
|
+
const dir = path32.join(paths.canvases, id2);
|
|
6620
|
+
if (fs35.existsSync(path32.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
6403
6621
|
print(`Creating canvas "${id2}"`);
|
|
6404
6622
|
const title = options.title ?? titleize(id2);
|
|
6405
6623
|
if (options.empty) {
|
|
6406
6624
|
write(
|
|
6407
|
-
|
|
6625
|
+
path32.join(dir, "canvas.json"),
|
|
6408
6626
|
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
6409
6627
|
`,
|
|
6410
6628
|
root
|
|
6411
6629
|
);
|
|
6412
|
-
|
|
6630
|
+
fs35.mkdirSync(path32.join(dir, "screens"), { recursive: true });
|
|
6413
6631
|
} else {
|
|
6414
|
-
write(
|
|
6415
|
-
write(
|
|
6632
|
+
write(path32.join(dir, "canvas.json"), canvasJson(title), root);
|
|
6633
|
+
write(path32.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
6416
6634
|
}
|
|
6417
6635
|
}
|
|
6418
6636
|
|
|
6419
6637
|
// src/cli/commands/shot.ts
|
|
6420
|
-
import
|
|
6421
|
-
import
|
|
6638
|
+
import fs36 from "fs";
|
|
6639
|
+
import path33 from "path";
|
|
6422
6640
|
var OVERVIEW_TIMEOUT = 9e4;
|
|
6423
6641
|
async function runShot(paths, target, options) {
|
|
6424
6642
|
const [canvasId = "", screenId] = target.split("/");
|
|
@@ -6434,15 +6652,15 @@ async function runShot(paths, target, options) {
|
|
|
6434
6652
|
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
6435
6653
|
}
|
|
6436
6654
|
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
6437
|
-
const outBase =
|
|
6438
|
-
const outDir =
|
|
6439
|
-
|
|
6655
|
+
const outBase = path33.resolve(options.out ?? paths.shots);
|
|
6656
|
+
const outDir = path33.join(outBase, canvasId);
|
|
6657
|
+
fs36.mkdirSync(outDir, { recursive: true });
|
|
6440
6658
|
const base = await serverBase(paths);
|
|
6441
6659
|
const browser = await launchChrome();
|
|
6442
6660
|
let failed = false;
|
|
6443
6661
|
try {
|
|
6444
6662
|
if (options.overview) {
|
|
6445
|
-
if (!
|
|
6663
|
+
if (!fs36.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
6446
6664
|
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
6665
|
for (const pageId of pages) {
|
|
6448
6666
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
@@ -6458,7 +6676,7 @@ async function runShot(paths, target, options) {
|
|
|
6458
6676
|
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
6459
6677
|
}
|
|
6460
6678
|
await page.waitForTimeout(400);
|
|
6461
|
-
const file =
|
|
6679
|
+
const file = path33.join(outDir, `${pageId}.overview.png`);
|
|
6462
6680
|
await page.screenshot({ path: file });
|
|
6463
6681
|
await page.close();
|
|
6464
6682
|
print(file);
|
|
@@ -6487,7 +6705,7 @@ async function shootFrames(browser, jobs, outDir) {
|
|
|
6487
6705
|
let failed = false;
|
|
6488
6706
|
const results = await mapLimit(jobs, 4, async (job) => {
|
|
6489
6707
|
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
6490
|
-
const file =
|
|
6708
|
+
const file = path33.join(outDir, `${job.id}.${job.theme}.png`);
|
|
6491
6709
|
try {
|
|
6492
6710
|
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
6493
6711
|
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
@@ -6519,8 +6737,8 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
6519
6737
|
if (id2 && !all.some((job) => job.id === id2)) {
|
|
6520
6738
|
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
6521
6739
|
}
|
|
6522
|
-
const outDir =
|
|
6523
|
-
|
|
6740
|
+
const outDir = path33.join(path33.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
6741
|
+
fs36.mkdirSync(outDir, { recursive: true });
|
|
6524
6742
|
const base = await serverBase(paths);
|
|
6525
6743
|
const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
|
|
6526
6744
|
const browser = await launchChrome();
|
|
@@ -6531,8 +6749,36 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
6531
6749
|
}
|
|
6532
6750
|
}
|
|
6533
6751
|
|
|
6752
|
+
// src/cli/commands/snapshots.ts
|
|
6753
|
+
async function runSnapshots(paths, targets, options) {
|
|
6754
|
+
const known = new DesignProject(paths, DEV_URLS).canvasIds();
|
|
6755
|
+
const picked = /* @__PURE__ */ new Map();
|
|
6756
|
+
for (const target of targets) {
|
|
6757
|
+
const [canvas = "", screen] = target.split("/");
|
|
6758
|
+
if (!known.includes(canvas))
|
|
6759
|
+
throw new CliError(`No canvas "${canvas}". Canvases: ${known.join(", ") || "none yet"}`);
|
|
6760
|
+
const screens = picked.has(canvas) ? picked.get(canvas) : /* @__PURE__ */ new Set();
|
|
6761
|
+
picked.set(canvas, !screen || screens === null ? null : screens.add(screen));
|
|
6762
|
+
}
|
|
6763
|
+
if (options.page && picked.size !== 1)
|
|
6764
|
+
throw new CliError("--page needs exactly one canvas: `design snapshots <canvas> --page <id>`");
|
|
6765
|
+
const canvases = picked.size ? [...picked.keys()] : known;
|
|
6766
|
+
const stale = canvases.flatMap(
|
|
6767
|
+
(canvas) => staleSnapshots(paths, [canvas], { page: options.page, screens: picked.get(canvas) ?? void 0 })
|
|
6768
|
+
);
|
|
6769
|
+
if (!stale.length) return print(green("\u2713 Every screen has current snapshots in both themes."));
|
|
6770
|
+
print(dim(`${stale.length} snapshot${stale.length === 1 ? "" : "s"} to take`));
|
|
6771
|
+
const lines = await takeSnapshots(
|
|
6772
|
+
paths,
|
|
6773
|
+
stale,
|
|
6774
|
+
async () => `http://127.0.0.1:${(await ensureServer(paths, {})).port}`,
|
|
6775
|
+
progress()
|
|
6776
|
+
);
|
|
6777
|
+
for (const line of lines) print(line);
|
|
6778
|
+
}
|
|
6779
|
+
|
|
6534
6780
|
// src/cli/commands/cloud-status.ts
|
|
6535
|
-
import
|
|
6781
|
+
import path34 from "path";
|
|
6536
6782
|
var STATE_TEXT = {
|
|
6537
6783
|
in_sync: green,
|
|
6538
6784
|
local_changes: yellow,
|
|
@@ -6555,7 +6801,7 @@ function detail(paths, status) {
|
|
|
6555
6801
|
if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
|
|
6556
6802
|
if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
|
|
6557
6803
|
parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
|
|
6558
|
-
if (status.conflictRev !== void 0) parts.push(`merge ${
|
|
6804
|
+
if (status.conflictRev !== void 0) parts.push(`merge ${path34.relative(paths.root, incomingDir(paths, status.key))}`);
|
|
6559
6805
|
return parts.join(" \xB7 ");
|
|
6560
6806
|
}
|
|
6561
6807
|
async function printCloudStatus(paths, label) {
|
|
@@ -6685,15 +6931,23 @@ ${bold("Preview")}
|
|
|
6685
6931
|
it; one from another version restarts) and print its URL;
|
|
6686
6932
|
--open also opens a browser
|
|
6687
6933
|
[--restart] [--foreground]
|
|
6688
|
-
stop
|
|
6934
|
+
stop [--all] Stop the viewer; --all stops every preview on this machine
|
|
6935
|
+
previews [--json] Every preview running on this machine: project, URL,
|
|
6936
|
+
version, uptime; orphans (a project's untracked server) marked
|
|
6689
6937
|
status Viewer state, a project summary and, with a cloud link, the
|
|
6690
6938
|
account, the linked project and each unit: in sync, local
|
|
6691
6939
|
changes, remote ahead, conflict or archived
|
|
6692
6940
|
|
|
6693
6941
|
${bold("Verify")}
|
|
6694
|
-
check [canvas\u2026] [--
|
|
6695
|
-
|
|
6696
|
-
errors and links that lead to no screen
|
|
6942
|
+
check [<canvas>[/<screen>]\u2026] [--page <id>] Validate canvas.json and the system (go() is an error);
|
|
6943
|
+
[--render [--built]] [--json] --render loads every screen in Chrome and reports runtime
|
|
6944
|
+
errors and links that lead to no screen; --built renders
|
|
6945
|
+
the production build design push uploads instead of the
|
|
6946
|
+
dev server. Narrow it to canvases, one page (--page) or
|
|
6947
|
+
single screens
|
|
6948
|
+
snapshots [<canvas>[/<screen>]\u2026] Take the snapshots screens lack, in both themes, for the
|
|
6949
|
+
[--page <id>] viewer to show while frames do not run (push does this
|
|
6950
|
+
for the canvases it pushes)
|
|
6697
6951
|
shot <canvas>[/<screen>] [--page <id>] Screenshot screens with Chrome (PNG paths are printed);
|
|
6698
6952
|
[--theme light|dark] [--out <dir>] @system or @system/<id> shoots the design system specimens
|
|
6699
6953
|
[--overview]
|
|
@@ -6712,7 +6966,7 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
|
|
|
6712
6966
|
push [canvas\u2026] [--json] Build and upload what changed here (or the named units); a
|
|
6713
6967
|
[--resolved <unit>] system change pushes every active canvas with it. When the
|
|
6714
6968
|
cloud is ahead it pulls and merges first, then pushes; it
|
|
6715
|
-
stops on conflicts.
|
|
6969
|
+
stops on conflicts. Takes missing snapshots (both themes).
|
|
6716
6970
|
Prints the web link of every pushed canvas; refuses screens
|
|
6717
6971
|
that still call go()
|
|
6718
6972
|
pull [canvas\u2026] [--json] Take cloud changes into .design; a unit changed on both sides
|
|
@@ -6742,7 +6996,7 @@ ${bold("Environment")}
|
|
|
6742
6996
|
FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
|
|
6743
6997
|
`;
|
|
6744
6998
|
function projectPaths(rootOption, requireDesign = true) {
|
|
6745
|
-
const found = rootOption ?
|
|
6999
|
+
const found = rootOption ? path35.resolve(rootOption) : findProjectRoot();
|
|
6746
7000
|
if (!found) {
|
|
6747
7001
|
if (!requireDesign) return designPaths(process.cwd());
|
|
6748
7002
|
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
@@ -6769,6 +7023,8 @@ async function main(argv) {
|
|
|
6769
7023
|
open: { type: "boolean" },
|
|
6770
7024
|
port: { type: "string" },
|
|
6771
7025
|
restart: { type: "boolean" },
|
|
7026
|
+
all: { type: "boolean" },
|
|
7027
|
+
built: { type: "boolean" },
|
|
6772
7028
|
foreground: { type: "boolean" },
|
|
6773
7029
|
render: { type: "boolean" },
|
|
6774
7030
|
json: { type: "boolean" },
|
|
@@ -6793,7 +7049,7 @@ async function main(argv) {
|
|
|
6793
7049
|
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
6794
7050
|
switch (command) {
|
|
6795
7051
|
case "init":
|
|
6796
|
-
return runInit(values.root ?
|
|
7052
|
+
return runInit(values.root ? path35.resolve(values.root) : process.cwd(), {
|
|
6797
7053
|
name: values.name,
|
|
6798
7054
|
gitignore: !values["no-gitignore"]
|
|
6799
7055
|
});
|
|
@@ -6831,11 +7087,20 @@ async function main(argv) {
|
|
|
6831
7087
|
});
|
|
6832
7088
|
}
|
|
6833
7089
|
case "stop":
|
|
6834
|
-
return runStop(projectPaths(values.root));
|
|
7090
|
+
return values.all ? runStopAll() : runStop(projectPaths(values.root));
|
|
7091
|
+
case "previews":
|
|
7092
|
+
return runPreviews({ json: !!values.json });
|
|
6835
7093
|
case "status":
|
|
6836
7094
|
return runStatus(projectPaths(values.root));
|
|
6837
7095
|
case "check":
|
|
6838
|
-
return runCheck(projectPaths(values.root), rest, {
|
|
7096
|
+
return runCheck(projectPaths(values.root), rest, {
|
|
7097
|
+
render: !!values.render,
|
|
7098
|
+
json: !!values.json,
|
|
7099
|
+
page: values.page,
|
|
7100
|
+
built: !!values.built
|
|
7101
|
+
});
|
|
7102
|
+
case "snapshots":
|
|
7103
|
+
return runSnapshots(projectPaths(values.root), rest, { page: values.page });
|
|
6839
7104
|
case "shot": {
|
|
6840
7105
|
if (!rest[0]) throw new CliError("Usage: design shot <canvas>[/<screen>] [--page <id>] [--theme light|dark]");
|
|
6841
7106
|
return runShot(projectPaths(values.root), rest[0], {
|
|
@@ -6884,20 +7149,42 @@ async function main(argv) {
|
|
|
6884
7149
|
throw new CliError(`Unknown command "${command}". Run \`design --help\`.`);
|
|
6885
7150
|
}
|
|
6886
7151
|
}
|
|
6887
|
-
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
7152
|
+
function relaunchFromRealPath() {
|
|
7153
|
+
if (process.platform !== "win32" || process.env.FOSS_DESIGN_RELAUNCHED) return false;
|
|
7154
|
+
const script = process.argv[1];
|
|
7155
|
+
if (!script) return false;
|
|
7156
|
+
let realScript;
|
|
7157
|
+
let realCwd;
|
|
7158
|
+
try {
|
|
7159
|
+
realScript = fs37.realpathSync.native(script);
|
|
7160
|
+
realCwd = fs37.realpathSync.native(process.cwd());
|
|
7161
|
+
} catch {
|
|
7162
|
+
return false;
|
|
6893
7163
|
}
|
|
6894
|
-
|
|
6895
|
-
|
|
7164
|
+
const same = (a, b) => path35.resolve(a).toLowerCase() === path35.resolve(b).toLowerCase();
|
|
7165
|
+
if (same(realScript, script) && same(realCwd, process.cwd())) return false;
|
|
7166
|
+
const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
|
|
7167
|
+
stdio: "inherit",
|
|
7168
|
+
cwd: realCwd,
|
|
7169
|
+
env: { ...process.env, FOSS_DESIGN_RELAUNCHED: "1" }
|
|
7170
|
+
});
|
|
7171
|
+
process.exit(result.status ?? 1);
|
|
7172
|
+
}
|
|
7173
|
+
if (!relaunchFromRealPath())
|
|
7174
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
7175
|
+
endLine();
|
|
7176
|
+
if (error instanceof CliError) {
|
|
7177
|
+
process.stderr.write(`${red("error")} ${error.message}
|
|
7178
|
+
`);
|
|
7179
|
+
process.exit(error.code);
|
|
7180
|
+
}
|
|
7181
|
+
if (error.code?.startsWith("ERR_PARSE_ARGS")) {
|
|
7182
|
+
process.stderr.write(`${red("error")} ${error.message}
|
|
6896
7183
|
Run \`design --help\`.
|
|
6897
7184
|
`);
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
7185
|
+
process.exit(2);
|
|
7186
|
+
}
|
|
7187
|
+
process.stderr.write(`${red("error")} ${error?.stack ?? String(error)}
|
|
6901
7188
|
`);
|
|
6902
|
-
|
|
6903
|
-
});
|
|
7189
|
+
process.exit(1);
|
|
7190
|
+
});
|