foss-design 0.9.12 → 0.9.13
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 +1064 -176
- package/dist/viewer/assets/index-BOLNePag.css +2 -0
- package/dist/viewer/assets/index-BrtsfBgT.js +163 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer-lib/index.js +3588 -1356
- package/dist/viewer-lib/styles.css +76 -4
- package/dist/viewer-lib/types/src/shared/drawings.d.ts +153 -0
- package/dist/viewer-lib/types/viewer/src/lib/drawings.d.ts +61 -0
- package/dist/viewer-lib/types/viewer/src/lib/source.d.ts +6 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/board.d.ts +16 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/camera.d.ts +7 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/canvas-bar.d.ts +12 -1
- package/dist/viewer-lib/types/viewer/src/pages/canvas/drawing/editor.d.ts +68 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/drawing/strokes.d.ts +15 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/drawing-capture.d.ts +18 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/markup.d.ts +26 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/view-state.d.ts +2 -0
- package/dist/viewer-lib/types/viewer/src/pages/canvas/viewport.d.ts +4 -2
- package/dist/viewer-lib/types/viewer/src/ui/icon-paths.d.ts +7 -0
- package/package.json +4 -1
- package/dist/viewer/assets/index-BUDcnDgZ.css +0 -2
- package/dist/viewer/assets/index-Cm4CEv8V.js +0 -159
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
5
|
+
import fs39 from "fs";
|
|
6
|
+
import path38 from "path";
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
8
|
|
|
9
9
|
// src/core/paths.ts
|
|
@@ -348,8 +348,8 @@ function routeError(pattern) {
|
|
|
348
348
|
}
|
|
349
349
|
return null;
|
|
350
350
|
}
|
|
351
|
-
function split(
|
|
352
|
-
return
|
|
351
|
+
function split(path39) {
|
|
352
|
+
return path39.split("/").filter(Boolean);
|
|
353
353
|
}
|
|
354
354
|
function compileRoute(pattern) {
|
|
355
355
|
return {
|
|
@@ -368,8 +368,8 @@ function decode2(part) {
|
|
|
368
368
|
return part;
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
|
-
function scoreRoute(route2,
|
|
372
|
-
const parts = split(
|
|
371
|
+
function scoreRoute(route2, path39) {
|
|
372
|
+
const parts = split(path39.split(/[?#]/)[0] ?? "").map(decode2);
|
|
373
373
|
const walk2 = (s, p) => {
|
|
374
374
|
if (s === route2.segments.length) return p === parts.length ? 0 : null;
|
|
375
375
|
const segment = route2.segments[s];
|
|
@@ -387,11 +387,11 @@ function scoreRoute(route2, path38) {
|
|
|
387
387
|
};
|
|
388
388
|
return walk2(0, 0);
|
|
389
389
|
}
|
|
390
|
-
function matchRoute(entries,
|
|
390
|
+
function matchRoute(entries, path39) {
|
|
391
391
|
let best = null;
|
|
392
392
|
for (const entry of entries) {
|
|
393
393
|
for (const route2 of entry.routes) {
|
|
394
|
-
const score = scoreRoute(route2,
|
|
394
|
+
const score = scoreRoute(route2, path39);
|
|
395
395
|
if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
|
|
396
396
|
}
|
|
397
397
|
}
|
|
@@ -518,9 +518,9 @@ var CanvasSchema = z.strictObject({
|
|
|
518
518
|
pages: z.array(z.unknown()).min(1, "add at least one page")
|
|
519
519
|
});
|
|
520
520
|
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
521
|
-
function formatPath(base,
|
|
521
|
+
function formatPath(base, path39) {
|
|
522
522
|
let out = base;
|
|
523
|
-
for (const part of
|
|
523
|
+
for (const part of path39) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
|
|
524
524
|
return out;
|
|
525
525
|
}
|
|
526
526
|
function describeZodError(error, base = "") {
|
|
@@ -1943,10 +1943,10 @@ function linkShippedPackages(paths, appDir = null) {
|
|
|
1943
1943
|
allow.add(path10.dirname(target2));
|
|
1944
1944
|
fromApp.push(name);
|
|
1945
1945
|
}
|
|
1946
|
-
const
|
|
1946
|
+
const record3 = path10.join(modules, APP_LINKS);
|
|
1947
1947
|
let before = [];
|
|
1948
1948
|
try {
|
|
1949
|
-
before = JSON.parse(fs10.readFileSync(
|
|
1949
|
+
before = JSON.parse(fs10.readFileSync(record3, "utf8"));
|
|
1950
1950
|
} catch {
|
|
1951
1951
|
}
|
|
1952
1952
|
for (const name of before) {
|
|
@@ -1962,7 +1962,7 @@ function linkShippedPackages(paths, appDir = null) {
|
|
|
1962
1962
|
}
|
|
1963
1963
|
if (fromApp.length || before.length) {
|
|
1964
1964
|
fs10.mkdirSync(modules, { recursive: true });
|
|
1965
|
-
fs10.writeFileSync(
|
|
1965
|
+
fs10.writeFileSync(record3, JSON.stringify(fromApp.sort()));
|
|
1966
1966
|
}
|
|
1967
1967
|
const target = fs10.realpathSync(PKG_ROOT);
|
|
1968
1968
|
linkTo(path10.join(modules, "foss-design-cli"), target);
|
|
@@ -2394,14 +2394,14 @@ async function buildSite(paths, out, options = {}) {
|
|
|
2394
2394
|
for (const canvas of canvases) writeJson(path13.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
|
|
2395
2395
|
if (system) {
|
|
2396
2396
|
writeJson(path13.join(out, "api", "system.json"), system.doc);
|
|
2397
|
-
const
|
|
2397
|
+
const texts2 = {};
|
|
2398
2398
|
for (const component of system.doc.components) {
|
|
2399
2399
|
for (const rel of [component.specimen, ...component.sources]) {
|
|
2400
2400
|
const file = path13.join(paths.root, rel);
|
|
2401
|
-
if (!(rel in
|
|
2401
|
+
if (!(rel in texts2) && fs13.existsSync(file)) texts2[rel] = fs13.readFileSync(file, "utf8");
|
|
2402
2402
|
}
|
|
2403
2403
|
}
|
|
2404
|
-
writeJson(path13.join(out, "api", "sources.json"),
|
|
2404
|
+
writeJson(path13.join(out, "api", "sources.json"), texts2);
|
|
2405
2405
|
}
|
|
2406
2406
|
fs13.rmSync(staging, { recursive: true, force: true });
|
|
2407
2407
|
const errors = [
|
|
@@ -2821,7 +2821,7 @@ var CloudClient = class {
|
|
|
2821
2821
|
}
|
|
2822
2822
|
host;
|
|
2823
2823
|
token;
|
|
2824
|
-
async send(method,
|
|
2824
|
+
async send(method, path39, init) {
|
|
2825
2825
|
const headers = {
|
|
2826
2826
|
"User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
|
|
2827
2827
|
Accept: "application/json"
|
|
@@ -2829,7 +2829,7 @@ var CloudClient = class {
|
|
|
2829
2829
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
2830
2830
|
if (init.type) headers["Content-Type"] = init.type;
|
|
2831
2831
|
try {
|
|
2832
|
-
return await fetch(`${this.host}/api${
|
|
2832
|
+
return await fetch(`${this.host}/api${path39}`, {
|
|
2833
2833
|
method,
|
|
2834
2834
|
headers,
|
|
2835
2835
|
body: init.body,
|
|
@@ -2850,8 +2850,8 @@ var CloudClient = class {
|
|
|
2850
2850
|
throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
|
|
2851
2851
|
}
|
|
2852
2852
|
/** JSON in, JSON out; any non-2xx answer throws a CloudError. */
|
|
2853
|
-
async request(method,
|
|
2854
|
-
const response = await this.send(method,
|
|
2853
|
+
async request(method, path39, body) {
|
|
2854
|
+
const response = await this.send(method, path39, {
|
|
2855
2855
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
2856
2856
|
type: body === void 0 ? void 0 : "application/json",
|
|
2857
2857
|
timeout: JSON_TIMEOUT
|
|
@@ -2863,7 +2863,7 @@ var CloudClient = class {
|
|
|
2863
2863
|
try {
|
|
2864
2864
|
return JSON.parse(text2);
|
|
2865
2865
|
} catch {
|
|
2866
|
-
throw new CloudError(`${this.host} answered ${method} ${
|
|
2866
|
+
throw new CloudError(`${this.host} answered ${method} ${path39} with something other than JSON`, 0, "bad_response");
|
|
2867
2867
|
}
|
|
2868
2868
|
}
|
|
2869
2869
|
/** POST /device/code. */
|
|
@@ -3135,7 +3135,7 @@ function requireLink(paths) {
|
|
|
3135
3135
|
return link;
|
|
3136
3136
|
}
|
|
3137
3137
|
function writeLink(paths, link) {
|
|
3138
|
-
const sorted = (
|
|
3138
|
+
const sorted = (record3) => Object.fromEntries(Object.entries(record3).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
3139
3139
|
const file = linkFile(paths);
|
|
3140
3140
|
const temp = `${file}.${process.pid}.tmp`;
|
|
3141
3141
|
const data = {
|
|
@@ -3446,17 +3446,17 @@ async function openFrame(page, url, options = {}) {
|
|
|
3446
3446
|
let errors = [];
|
|
3447
3447
|
let outdated = () => {
|
|
3448
3448
|
};
|
|
3449
|
-
const
|
|
3449
|
+
const record3 = (text2) => {
|
|
3450
3450
|
errors.push(text2);
|
|
3451
3451
|
if (OUTDATED_DEP.test(text2)) outdated();
|
|
3452
3452
|
};
|
|
3453
|
-
page.on("pageerror", (error) =>
|
|
3453
|
+
page.on("pageerror", (error) => record3(error.message));
|
|
3454
3454
|
page.on("console", (message) => {
|
|
3455
3455
|
if (message.type() !== "error") return;
|
|
3456
3456
|
let text2 = message.text().replace(/^(?:\s*%[osdifcO])+\s*/, "");
|
|
3457
3457
|
const where = message.location().url;
|
|
3458
3458
|
if (/^Failed to load resource/.test(text2) && where && !text2.includes(where)) text2 += ` (${where})`;
|
|
3459
|
-
|
|
3459
|
+
record3(text2);
|
|
3460
3460
|
});
|
|
3461
3461
|
let ready = false;
|
|
3462
3462
|
let loadedAt = 0;
|
|
@@ -4404,18 +4404,252 @@ Still using go():
|
|
|
4404
4404
|
|
|
4405
4405
|
// src/cli/commands/preview.ts
|
|
4406
4406
|
import { spawn } from "child_process";
|
|
4407
|
-
import
|
|
4407
|
+
import fs31 from "fs";
|
|
4408
4408
|
|
|
4409
4409
|
// src/server/index.ts
|
|
4410
|
-
import
|
|
4410
|
+
import fs30 from "fs";
|
|
4411
4411
|
import http2 from "http";
|
|
4412
|
-
import
|
|
4412
|
+
import path29 from "path";
|
|
4413
4413
|
import sirv2 from "sirv";
|
|
4414
4414
|
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
4415
4415
|
|
|
4416
4416
|
// src/server/api.ts
|
|
4417
4417
|
import fs27 from "fs";
|
|
4418
4418
|
import path26 from "path";
|
|
4419
|
+
|
|
4420
|
+
// src/shared/drawings.ts
|
|
4421
|
+
var INKS = ["ink", "red", "blue", "green"];
|
|
4422
|
+
var STROKE_KINDS = ["pen", "arrow", "rect", "text"];
|
|
4423
|
+
var MAX_STROKES = 4e3;
|
|
4424
|
+
var MAX_ERASED = 2e4;
|
|
4425
|
+
var MAX_NUMBERS = 12e3;
|
|
4426
|
+
var MAX_TEXT = 2e3;
|
|
4427
|
+
var MAX_COORD = 1e5;
|
|
4428
|
+
var MAX_MESSAGE = 256 * 1024;
|
|
4429
|
+
var KEY = /^(board|markup):[a-z0-9][a-z0-9_-]{0,127}$/i;
|
|
4430
|
+
var STROKE_ID = /^[a-z0-9_-]{1,40}$/i;
|
|
4431
|
+
var markupKey = (item) => `markup:${item}`;
|
|
4432
|
+
var isDrawingKey = (value) => typeof value === "string" && KEY.test(value);
|
|
4433
|
+
var emptyDrawing = () => ({ strokes: [], erased: [] });
|
|
4434
|
+
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
4435
|
+
function parseStroke(value) {
|
|
4436
|
+
const data = record2(value);
|
|
4437
|
+
if (!data) return null;
|
|
4438
|
+
const { id: id2, kind, ink, size: size2, points, text: text2 } = data;
|
|
4439
|
+
if (typeof id2 !== "string" || !STROKE_ID.test(id2)) return null;
|
|
4440
|
+
if (!STROKE_KINDS.includes(kind) || !INKS.includes(ink)) return null;
|
|
4441
|
+
if (typeof size2 !== "number" || !(size2 >= 0.5 && size2 <= 400)) return null;
|
|
4442
|
+
if (!Array.isArray(points) || points.length > MAX_NUMBERS) return null;
|
|
4443
|
+
if (!points.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && Math.abs(n2) <= MAX_COORD)) return null;
|
|
4444
|
+
const need = kind === "pen" ? points.length >= 3 && points.length % 3 === 0 : points.length === (kind === "text" ? 2 : 4);
|
|
4445
|
+
if (!need) return null;
|
|
4446
|
+
if (kind === "text" && (typeof text2 !== "string" || !text2.trim() || text2.length > MAX_TEXT)) return null;
|
|
4447
|
+
const stroke = { id: id2, kind, ink, size: size2, points };
|
|
4448
|
+
if (kind === "text") stroke.text = text2;
|
|
4449
|
+
return stroke;
|
|
4450
|
+
}
|
|
4451
|
+
function parseIds(value, max) {
|
|
4452
|
+
if (!Array.isArray(value) || value.length > max) return null;
|
|
4453
|
+
return value.every((id2) => typeof id2 === "string" && STROKE_ID.test(id2)) ? value : null;
|
|
4454
|
+
}
|
|
4455
|
+
function parseOps(value) {
|
|
4456
|
+
const data = record2(value);
|
|
4457
|
+
if (!data) return null;
|
|
4458
|
+
const ops = {};
|
|
4459
|
+
if (data.add !== void 0) {
|
|
4460
|
+
if (!Array.isArray(data.add) || data.add.length > MAX_STROKES) return null;
|
|
4461
|
+
const add = data.add.map(parseStroke);
|
|
4462
|
+
if (add.some((stroke) => !stroke)) return null;
|
|
4463
|
+
ops.add = add;
|
|
4464
|
+
}
|
|
4465
|
+
if (data.erase !== void 0) {
|
|
4466
|
+
const erase = parseIds(data.erase, MAX_ERASED);
|
|
4467
|
+
if (!erase) return null;
|
|
4468
|
+
ops.erase = erase;
|
|
4469
|
+
}
|
|
4470
|
+
return ops;
|
|
4471
|
+
}
|
|
4472
|
+
function parseDrawing(value) {
|
|
4473
|
+
const data = record2(value);
|
|
4474
|
+
if (!data) return null;
|
|
4475
|
+
const erased = parseIds(data.erased ?? [], MAX_ERASED);
|
|
4476
|
+
if (!erased || !Array.isArray(data.strokes) || data.strokes.length > MAX_STROKES) return null;
|
|
4477
|
+
const strokes = data.strokes.map(parseStroke);
|
|
4478
|
+
if (strokes.some((stroke) => !stroke)) return null;
|
|
4479
|
+
return { strokes, erased };
|
|
4480
|
+
}
|
|
4481
|
+
function parseDrawings(value) {
|
|
4482
|
+
const out = {};
|
|
4483
|
+
for (const [key, drawing] of Object.entries(record2(value) ?? {})) {
|
|
4484
|
+
const parsed = isDrawingKey(key) ? parseDrawing(drawing) : null;
|
|
4485
|
+
if (parsed) out[key] = parsed;
|
|
4486
|
+
}
|
|
4487
|
+
return out;
|
|
4488
|
+
}
|
|
4489
|
+
function applyOps(drawing, ops) {
|
|
4490
|
+
const erased = new Set(drawing.erased);
|
|
4491
|
+
let changed = false;
|
|
4492
|
+
const newlyErased = [];
|
|
4493
|
+
for (const id2 of ops.erase ?? []) {
|
|
4494
|
+
if (erased.has(id2)) continue;
|
|
4495
|
+
erased.add(id2);
|
|
4496
|
+
newlyErased.push(id2);
|
|
4497
|
+
changed = true;
|
|
4498
|
+
}
|
|
4499
|
+
const strokes = newlyErased.length ? drawing.strokes.filter((stroke) => !erased.has(stroke.id)) : [...drawing.strokes];
|
|
4500
|
+
const ids = new Set(strokes.map((stroke) => stroke.id));
|
|
4501
|
+
for (const stroke of ops.add ?? []) {
|
|
4502
|
+
if (erased.has(stroke.id) || ids.has(stroke.id) || strokes.length >= MAX_STROKES) continue;
|
|
4503
|
+
strokes.push(stroke);
|
|
4504
|
+
ids.add(stroke.id);
|
|
4505
|
+
changed = true;
|
|
4506
|
+
}
|
|
4507
|
+
if (!changed) return null;
|
|
4508
|
+
const erasedList = [...drawing.erased, ...newlyErased];
|
|
4509
|
+
return { strokes, erased: erasedList.length > MAX_ERASED ? erasedList.slice(-MAX_ERASED) : erasedList };
|
|
4510
|
+
}
|
|
4511
|
+
function diffDrawings(from, to) {
|
|
4512
|
+
const toErased = new Set(to.erased);
|
|
4513
|
+
const toIds = new Set(to.strokes.map((stroke) => stroke.id));
|
|
4514
|
+
const add = from.strokes.filter((stroke) => !toIds.has(stroke.id) && !toErased.has(stroke.id));
|
|
4515
|
+
const erase = from.erased.filter((id2) => !toErased.has(id2));
|
|
4516
|
+
if (!add.length && !erase.length) return null;
|
|
4517
|
+
return { ...add.length ? { add } : {}, ...erase.length ? { erase } : {} };
|
|
4518
|
+
}
|
|
4519
|
+
var TEXT_LINE_HEIGHT = 1.25;
|
|
4520
|
+
function textBox(stroke) {
|
|
4521
|
+
const lines = (stroke.text ?? "").split("\n");
|
|
4522
|
+
const longest = Math.max(...lines.map((line) => line.length), 1);
|
|
4523
|
+
return {
|
|
4524
|
+
x: stroke.points[0],
|
|
4525
|
+
y: stroke.points[1],
|
|
4526
|
+
w: longest * stroke.size * 0.56,
|
|
4527
|
+
h: lines.length * stroke.size * TEXT_LINE_HEIGHT
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4530
|
+
function strokeBounds(stroke) {
|
|
4531
|
+
if (stroke.kind === "text") return textBox(stroke);
|
|
4532
|
+
const step = stroke.kind === "pen" ? 3 : 2;
|
|
4533
|
+
const pad = stroke.kind === "arrow" ? Math.max(10, stroke.size * 4.5) : stroke.size;
|
|
4534
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
4535
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
4536
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
4537
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
4538
|
+
for (let i = 0; i + 1 < stroke.points.length; i += step) {
|
|
4539
|
+
minX = Math.min(minX, stroke.points[i]);
|
|
4540
|
+
minY = Math.min(minY, stroke.points[i + 1]);
|
|
4541
|
+
maxX = Math.max(maxX, stroke.points[i]);
|
|
4542
|
+
maxY = Math.max(maxY, stroke.points[i + 1]);
|
|
4543
|
+
}
|
|
4544
|
+
return { x: minX - pad, y: minY - pad, w: maxX - minX + pad * 2, h: maxY - minY + pad * 2 };
|
|
4545
|
+
}
|
|
4546
|
+
var union = (a, b) => {
|
|
4547
|
+
const x = Math.min(a.x, b.x);
|
|
4548
|
+
const y = Math.min(a.y, b.y);
|
|
4549
|
+
return { x, y, w: Math.max(a.x + a.w, b.x + b.w) - x, h: Math.max(a.y + a.h, b.y + b.h) - y };
|
|
4550
|
+
};
|
|
4551
|
+
var GROUP_GAP = 240;
|
|
4552
|
+
function drawingGroups(strokes) {
|
|
4553
|
+
let groups = strokes.map((stroke) => ({ box: strokeBounds(stroke), strokes: [stroke] }));
|
|
4554
|
+
const near = (a, b) => a.x - GROUP_GAP < b.x + b.w && b.x - GROUP_GAP < a.x + a.w && a.y - GROUP_GAP < b.y + b.h && b.y - GROUP_GAP < a.y + a.h;
|
|
4555
|
+
for (let merged = true; merged; ) {
|
|
4556
|
+
merged = false;
|
|
4557
|
+
const next = [];
|
|
4558
|
+
for (const group of groups) {
|
|
4559
|
+
const into = next.find((other) => near(other.box, group.box));
|
|
4560
|
+
if (into) {
|
|
4561
|
+
into.box = union(into.box, group.box);
|
|
4562
|
+
into.strokes.push(...group.strokes);
|
|
4563
|
+
merged = true;
|
|
4564
|
+
} else next.push({ box: group.box, strokes: [...group.strokes] });
|
|
4565
|
+
}
|
|
4566
|
+
groups = next;
|
|
4567
|
+
}
|
|
4568
|
+
const order = new Map(strokes.map((stroke, i) => [stroke.id, i]));
|
|
4569
|
+
for (const group of groups) group.strokes.sort((a, b) => order.get(a.id) - order.get(b.id));
|
|
4570
|
+
groups.sort((a, b) => a.box.y - b.box.y);
|
|
4571
|
+
const rows = [];
|
|
4572
|
+
for (const group of groups) {
|
|
4573
|
+
const row = rows.at(-1);
|
|
4574
|
+
if (row && group.box.y < row[0].box.y + row[0].box.h / 2) row.push(group);
|
|
4575
|
+
else rows.push([group]);
|
|
4576
|
+
}
|
|
4577
|
+
return rows.flatMap((row) => row.sort((a, b) => a.box.x - b.box.x));
|
|
4578
|
+
}
|
|
4579
|
+
function parseLive(value) {
|
|
4580
|
+
const data = record2(value);
|
|
4581
|
+
if (!data) return null;
|
|
4582
|
+
const { id: id2, kind, ink, size: size2, text: text2, at, points } = data;
|
|
4583
|
+
if (typeof id2 !== "string" || !STROKE_ID.test(id2)) return null;
|
|
4584
|
+
if (!STROKE_KINDS.includes(kind) || !INKS.includes(ink)) return null;
|
|
4585
|
+
if (typeof size2 !== "number" || !(size2 >= 0.5 && size2 <= 400)) return null;
|
|
4586
|
+
if (typeof at !== "number" || !Number.isInteger(at) || at < 0 || at > MAX_NUMBERS) return null;
|
|
4587
|
+
if (!Array.isArray(points) || points.length > MAX_NUMBERS) return null;
|
|
4588
|
+
if (!points.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && Math.abs(n2) <= MAX_COORD)) return null;
|
|
4589
|
+
if (text2 !== void 0 && (typeof text2 !== "string" || text2.length > MAX_TEXT)) return null;
|
|
4590
|
+
const stroke = { id: id2, kind, ink, size: size2, at, points };
|
|
4591
|
+
if (typeof text2 === "string") stroke.text = text2;
|
|
4592
|
+
return stroke;
|
|
4593
|
+
}
|
|
4594
|
+
function parseJson(raw) {
|
|
4595
|
+
if (raw.length > MAX_MESSAGE) return null;
|
|
4596
|
+
try {
|
|
4597
|
+
return record2(JSON.parse(raw));
|
|
4598
|
+
} catch {
|
|
4599
|
+
return null;
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
function parseClientMessage(raw) {
|
|
4603
|
+
const data = parseJson(raw);
|
|
4604
|
+
if (!data || !isDrawingKey(data.key)) return null;
|
|
4605
|
+
const key = data.key;
|
|
4606
|
+
if (data.type === "ops") {
|
|
4607
|
+
if (typeof data.id !== "number" || !Number.isInteger(data.id)) return null;
|
|
4608
|
+
const ops = parseOps(data);
|
|
4609
|
+
return ops ? { type: "ops", id: data.id, key, ...ops } : null;
|
|
4610
|
+
}
|
|
4611
|
+
if (data.type === "live") {
|
|
4612
|
+
const stroke = parseLive(data.stroke);
|
|
4613
|
+
return stroke ? { type: "live", key, stroke } : null;
|
|
4614
|
+
}
|
|
4615
|
+
if (data.type === "live-end")
|
|
4616
|
+
return typeof data.id === "string" && STROKE_ID.test(data.id) ? { type: "live-end", key, id: data.id } : null;
|
|
4617
|
+
return null;
|
|
4618
|
+
}
|
|
4619
|
+
function parseServerMessage(raw) {
|
|
4620
|
+
const data = parseJson(raw);
|
|
4621
|
+
if (!data) return null;
|
|
4622
|
+
const peer = typeof data.peer === "string" ? data.peer : "";
|
|
4623
|
+
switch (data.type) {
|
|
4624
|
+
case "hello":
|
|
4625
|
+
return { type: "hello", peer, canDraw: data.canDraw === true, drawings: parseDrawings(data.drawings) };
|
|
4626
|
+
case "ops": {
|
|
4627
|
+
const ops = isDrawingKey(data.key) ? parseOps(data) : null;
|
|
4628
|
+
return ops ? { type: "ops", key: data.key, ...ops, peer } : null;
|
|
4629
|
+
}
|
|
4630
|
+
case "ack":
|
|
4631
|
+
return typeof data.id === "number" ? { type: "ack", id: data.id } : null;
|
|
4632
|
+
case "doc": {
|
|
4633
|
+
const drawing = isDrawingKey(data.key) ? parseDrawing(data.drawing) : null;
|
|
4634
|
+
return drawing ? { type: "doc", key: data.key, drawing } : null;
|
|
4635
|
+
}
|
|
4636
|
+
case "live": {
|
|
4637
|
+
const stroke = isDrawingKey(data.key) ? parseLive(data.stroke) : null;
|
|
4638
|
+
if (!stroke) return null;
|
|
4639
|
+
const name = typeof data.name === "string" ? data.name.slice(0, 80) : void 0;
|
|
4640
|
+
return { type: "live", key: data.key, stroke, peer, ...name ? { name } : {} };
|
|
4641
|
+
}
|
|
4642
|
+
case "live-end":
|
|
4643
|
+
return isDrawingKey(data.key) && typeof data.id === "string" ? { type: "live-end", key: data.key, id: data.id, peer } : null;
|
|
4644
|
+
case "gone":
|
|
4645
|
+
return { type: "gone", peer };
|
|
4646
|
+
case "error":
|
|
4647
|
+
return { type: "error", message: typeof data.message === "string" ? data.message.slice(0, 500) : "error" };
|
|
4648
|
+
}
|
|
4649
|
+
return null;
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
// src/server/api.ts
|
|
4419
4653
|
function sendJson(res, status, body) {
|
|
4420
4654
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
4421
4655
|
res.end(JSON.stringify(body));
|
|
@@ -4436,7 +4670,7 @@ function readBody(req, limit = 25 * 1024 * 1024) {
|
|
|
4436
4670
|
});
|
|
4437
4671
|
}
|
|
4438
4672
|
async function handleApi(ctx, req, res, url) {
|
|
4439
|
-
const { project, events, snapshots } = ctx;
|
|
4673
|
+
const { project, events, snapshots, drawings } = ctx;
|
|
4440
4674
|
const route2 = url.pathname.slice("/api".length);
|
|
4441
4675
|
const method = req.method ?? "GET";
|
|
4442
4676
|
if (route2 === "/health") {
|
|
@@ -4497,6 +4731,25 @@ async function handleApi(ctx, req, res, url) {
|
|
|
4497
4731
|
}
|
|
4498
4732
|
return true;
|
|
4499
4733
|
}
|
|
4734
|
+
const drawingsMatch = /^\/drawings\/([^/]+)(?:\/([^/]+))?$/.exec(route2);
|
|
4735
|
+
if (drawingsMatch) {
|
|
4736
|
+
const canvas = decodeURIComponent(drawingsMatch[1]);
|
|
4737
|
+
const key = drawingsMatch[2] ? decodeURIComponent(drawingsMatch[2]) : null;
|
|
4738
|
+
if (method === "GET" && !key) {
|
|
4739
|
+
const found = await drawings.read(canvas);
|
|
4740
|
+
if (found) sendJson(res, 200, found);
|
|
4741
|
+
else sendJson(res, 404, { error: `No canvas "${canvas}"` });
|
|
4742
|
+
return true;
|
|
4743
|
+
}
|
|
4744
|
+
if (method === "DELETE" && key && isDrawingKey(key)) {
|
|
4745
|
+
const erased = await drawings.clear(canvas, key);
|
|
4746
|
+
if (erased === null) sendJson(res, 404, { error: `No canvas "${canvas}"` });
|
|
4747
|
+
else sendJson(res, 200, { erased });
|
|
4748
|
+
return true;
|
|
4749
|
+
}
|
|
4750
|
+
sendJson(res, 405, { error: "GET /api/drawings/<canvas> or DELETE /api/drawings/<canvas>/<key>" });
|
|
4751
|
+
return true;
|
|
4752
|
+
}
|
|
4500
4753
|
const snapshotPost = /^\/snapshots\/([^/]+)\/([^/]+)$/.exec(route2);
|
|
4501
4754
|
if (snapshotPost && method === "POST") {
|
|
4502
4755
|
const canvas = decodeURIComponent(snapshotPost[1]);
|
|
@@ -4533,6 +4786,433 @@ async function handleApi(ctx, req, res, url) {
|
|
|
4533
4786
|
return false;
|
|
4534
4787
|
}
|
|
4535
4788
|
|
|
4789
|
+
// src/server/drawings.ts
|
|
4790
|
+
import fs28 from "fs";
|
|
4791
|
+
import path27 from "path";
|
|
4792
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
4793
|
+
|
|
4794
|
+
// src/server/hosts.ts
|
|
4795
|
+
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
4796
|
+
var FRAME_HOST = "127.0.0.1";
|
|
4797
|
+
var VIEWER_HOST = "localhost";
|
|
4798
|
+
function parseHost(header) {
|
|
4799
|
+
const match = /^(\[[0-9a-f:.]+\]|[a-z0-9.-]+)(?::(\d{1,5}))?$/i.exec(header ?? "");
|
|
4800
|
+
if (!match) return null;
|
|
4801
|
+
return { name: match[1].toLowerCase(), port: match[2] ? Number(match[2]) : 80 };
|
|
4802
|
+
}
|
|
4803
|
+
function isAllowedHost(header, port, extra) {
|
|
4804
|
+
const host = parseHost(header);
|
|
4805
|
+
if (!host || host.port !== port) return false;
|
|
4806
|
+
return LOOPBACK.has(host.name) || host.name.endsWith(".localhost") || !!extra && host.name === extra.toLowerCase();
|
|
4807
|
+
}
|
|
4808
|
+
function isFrameApi(pathname, method) {
|
|
4809
|
+
if (pathname === "/api/health") return method === "GET";
|
|
4810
|
+
return /^\/api\/snapshots\/[^/]+\/[^/]+$/.test(pathname) && (method === "GET" || method === "POST");
|
|
4811
|
+
}
|
|
4812
|
+
|
|
4813
|
+
// src/server/drawings.ts
|
|
4814
|
+
var FILE = ".drawings.json";
|
|
4815
|
+
var IDLE_MS = 3e4;
|
|
4816
|
+
var SYNC_WAIT_MS = 6e3;
|
|
4817
|
+
var drawingsFile = (paths, canvas) => path27.join(paths.canvases, canvas, FILE);
|
|
4818
|
+
var send = (socket, message) => {
|
|
4819
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(message));
|
|
4820
|
+
};
|
|
4821
|
+
function readFile(file) {
|
|
4822
|
+
let raw;
|
|
4823
|
+
try {
|
|
4824
|
+
raw = fs28.readFileSync(file, "utf8");
|
|
4825
|
+
} catch {
|
|
4826
|
+
return {};
|
|
4827
|
+
}
|
|
4828
|
+
try {
|
|
4829
|
+
return parseDrawings(JSON.parse(raw).drawings);
|
|
4830
|
+
} catch {
|
|
4831
|
+
console.warn(`[design] ${file} is not valid JSON; its drawings are not shown`);
|
|
4832
|
+
return {};
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
var Room = class {
|
|
4836
|
+
constructor(hub, canvas) {
|
|
4837
|
+
this.hub = hub;
|
|
4838
|
+
this.canvas = canvas;
|
|
4839
|
+
this.drawings = readFile(this.file);
|
|
4840
|
+
}
|
|
4841
|
+
hub;
|
|
4842
|
+
canvas;
|
|
4843
|
+
drawings;
|
|
4844
|
+
clients = /* @__PURE__ */ new Map();
|
|
4845
|
+
cloud = null;
|
|
4846
|
+
written = null;
|
|
4847
|
+
saveTimer;
|
|
4848
|
+
idleTimer;
|
|
4849
|
+
get file() {
|
|
4850
|
+
return drawingsFile(this.hub.paths, this.canvas);
|
|
4851
|
+
}
|
|
4852
|
+
canDraw() {
|
|
4853
|
+
return this.cloud?.canDraw ?? true;
|
|
4854
|
+
}
|
|
4855
|
+
cloudState() {
|
|
4856
|
+
return this.cloud ? this.cloud.state : "unlinked";
|
|
4857
|
+
}
|
|
4858
|
+
/** The cloud's answer, or what is here once `timeout` passes. */
|
|
4859
|
+
async synced(timeout) {
|
|
4860
|
+
this.touch();
|
|
4861
|
+
if (this.cloud) await Promise.race([this.cloud.ready, new Promise((resolve) => setTimeout(resolve, timeout))]);
|
|
4862
|
+
}
|
|
4863
|
+
/** Keep (or open) the cloud socket for a while even with nobody here: the CLI reads through it. */
|
|
4864
|
+
touch() {
|
|
4865
|
+
this.connectCloud();
|
|
4866
|
+
this.release();
|
|
4867
|
+
}
|
|
4868
|
+
connectCloud() {
|
|
4869
|
+
clearTimeout(this.idleTimer);
|
|
4870
|
+
if (this.cloud) return;
|
|
4871
|
+
const link = readLink(this.hub.paths);
|
|
4872
|
+
const credential = link ? getCredential(link.host) : null;
|
|
4873
|
+
if (!link || !credential) return;
|
|
4874
|
+
this.cloud = new CloudRelay(this, link.host, link.project, credential.token);
|
|
4875
|
+
}
|
|
4876
|
+
/** With nobody left, let the cloud socket go after a while. */
|
|
4877
|
+
release() {
|
|
4878
|
+
clearTimeout(this.idleTimer);
|
|
4879
|
+
if (this.clients.size) return;
|
|
4880
|
+
this.idleTimer = setTimeout(() => {
|
|
4881
|
+
if (this.clients.size) return;
|
|
4882
|
+
this.cloud?.close();
|
|
4883
|
+
this.cloud = null;
|
|
4884
|
+
}, IDLE_MS);
|
|
4885
|
+
this.idleTimer.unref();
|
|
4886
|
+
}
|
|
4887
|
+
join(socket) {
|
|
4888
|
+
const client = { peer: this.hub.nextPeer(), live: /* @__PURE__ */ new Map() };
|
|
4889
|
+
this.clients.set(socket, client);
|
|
4890
|
+
this.connectCloud();
|
|
4891
|
+
send(socket, { type: "hello", peer: client.peer, canDraw: this.canDraw(), drawings: this.drawings });
|
|
4892
|
+
socket.on("message", (data, isBinary) => {
|
|
4893
|
+
if (isBinary) return;
|
|
4894
|
+
const message = parseClientMessage(data.toString());
|
|
4895
|
+
if (message) this.fromClient(socket, client, message);
|
|
4896
|
+
else send(socket, { type: "error", message: "Not a drawings message." });
|
|
4897
|
+
});
|
|
4898
|
+
socket.on("close", () => {
|
|
4899
|
+
this.clients.delete(socket);
|
|
4900
|
+
for (const [id2, key] of client.live) this.cloud?.send({ type: "live-end", key, id: id2 });
|
|
4901
|
+
this.broadcast({ type: "gone", peer: client.peer });
|
|
4902
|
+
this.release();
|
|
4903
|
+
});
|
|
4904
|
+
socket.on("error", () => socket.terminate());
|
|
4905
|
+
}
|
|
4906
|
+
fromClient(socket, client, message) {
|
|
4907
|
+
if (!this.canDraw()) {
|
|
4908
|
+
send(socket, { type: "error", message: "You can look at these drawings but not draw on them." });
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
if (message.type === "ops") {
|
|
4912
|
+
this.change(message.key, { add: message.add, erase: message.erase }, client.peer);
|
|
4913
|
+
send(socket, { type: "ack", id: message.id });
|
|
4914
|
+
} else if (message.type === "live") {
|
|
4915
|
+
client.live.set(message.stroke.id, message.key);
|
|
4916
|
+
this.broadcast({ ...message, peer: client.peer }, socket);
|
|
4917
|
+
this.cloud?.send(message);
|
|
4918
|
+
} else {
|
|
4919
|
+
client.live.delete(message.id);
|
|
4920
|
+
this.broadcast({ ...message, peer: client.peer }, socket);
|
|
4921
|
+
this.cloud?.send(message);
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
/** A change made here (a viewer, the CLI): applied, and passed on to the cloud. */
|
|
4925
|
+
change(key, ops, peer) {
|
|
4926
|
+
this.apply(key, ops, peer);
|
|
4927
|
+
this.cloud?.forward(key, ops);
|
|
4928
|
+
}
|
|
4929
|
+
/** Apply ops from anywhere; tell everyone here and save when they changed something. */
|
|
4930
|
+
apply(key, ops, peer) {
|
|
4931
|
+
const next = applyOps(this.drawings[key] ?? emptyDrawing(), ops);
|
|
4932
|
+
if (!next) return false;
|
|
4933
|
+
this.drawings = { ...this.drawings, [key]: next };
|
|
4934
|
+
this.broadcast({ type: "ops", key, ...ops, peer });
|
|
4935
|
+
this.save();
|
|
4936
|
+
return true;
|
|
4937
|
+
}
|
|
4938
|
+
/** A whole drawing from elsewhere, merged with this one; returns what this side has that it lacked. */
|
|
4939
|
+
merge(key, drawing) {
|
|
4940
|
+
const here = this.drawings[key] ?? emptyDrawing();
|
|
4941
|
+
const missing = diffDrawings(drawing, here);
|
|
4942
|
+
if (missing) {
|
|
4943
|
+
const next = applyOps(here, missing);
|
|
4944
|
+
if (next) {
|
|
4945
|
+
this.drawings = { ...this.drawings, [key]: next };
|
|
4946
|
+
this.broadcast({ type: "doc", key, drawing: next });
|
|
4947
|
+
this.save();
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
return diffDrawings(this.drawings[key] ?? emptyDrawing(), drawing);
|
|
4951
|
+
}
|
|
4952
|
+
/** The cloud's answer came in, or its verdict on drawing changed: everyone here starts over from it. */
|
|
4953
|
+
rehello() {
|
|
4954
|
+
for (const [socket, client] of this.clients)
|
|
4955
|
+
send(socket, { type: "hello", peer: client.peer, canDraw: this.canDraw(), drawings: this.drawings });
|
|
4956
|
+
}
|
|
4957
|
+
broadcast(message, except) {
|
|
4958
|
+
const data = JSON.stringify(message);
|
|
4959
|
+
for (const socket of this.clients.keys())
|
|
4960
|
+
if (socket !== except && socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
4961
|
+
}
|
|
4962
|
+
save() {
|
|
4963
|
+
clearTimeout(this.saveTimer);
|
|
4964
|
+
this.saveTimer = setTimeout(() => this.flush(), 150);
|
|
4965
|
+
}
|
|
4966
|
+
flush() {
|
|
4967
|
+
clearTimeout(this.saveTimer);
|
|
4968
|
+
const dir = path27.dirname(this.file);
|
|
4969
|
+
if (!fs28.existsSync(dir)) return;
|
|
4970
|
+
const drawings = Object.fromEntries(
|
|
4971
|
+
Object.entries(this.drawings).filter(([, d]) => d.strokes.length || d.erased.length)
|
|
4972
|
+
);
|
|
4973
|
+
const text2 = `${JSON.stringify({ drawings })}
|
|
4974
|
+
`;
|
|
4975
|
+
if (text2 === this.written) return;
|
|
4976
|
+
const temp = `${this.file}.${process.pid}.tmp`;
|
|
4977
|
+
fs28.writeFileSync(temp, text2);
|
|
4978
|
+
fs28.renameSync(temp, this.file);
|
|
4979
|
+
this.written = text2;
|
|
4980
|
+
}
|
|
4981
|
+
/** The file changed on disk: unless it was this server's own write, it is the drawings now. */
|
|
4982
|
+
reload() {
|
|
4983
|
+
let raw = null;
|
|
4984
|
+
try {
|
|
4985
|
+
raw = fs28.readFileSync(this.file, "utf8");
|
|
4986
|
+
} catch {
|
|
4987
|
+
}
|
|
4988
|
+
if (raw !== null && raw === this.written) return;
|
|
4989
|
+
const next = readFile(this.file);
|
|
4990
|
+
for (const key of /* @__PURE__ */ new Set([...Object.keys(this.drawings), ...Object.keys(next)])) {
|
|
4991
|
+
const drawing = next[key] ?? emptyDrawing();
|
|
4992
|
+
if (JSON.stringify(drawing) === JSON.stringify(this.drawings[key] ?? emptyDrawing())) continue;
|
|
4993
|
+
this.broadcast({ type: "doc", key, drawing });
|
|
4994
|
+
const ops = diffDrawings(drawing, this.drawings[key] ?? emptyDrawing());
|
|
4995
|
+
if (ops) this.cloud?.forward(key, ops);
|
|
4996
|
+
}
|
|
4997
|
+
this.drawings = next;
|
|
4998
|
+
this.written = raw;
|
|
4999
|
+
}
|
|
5000
|
+
close() {
|
|
5001
|
+
this.flush();
|
|
5002
|
+
this.cloud?.close();
|
|
5003
|
+
this.cloud = null;
|
|
5004
|
+
clearTimeout(this.idleTimer);
|
|
5005
|
+
for (const socket of this.clients.keys()) socket.close(1001);
|
|
5006
|
+
}
|
|
5007
|
+
};
|
|
5008
|
+
var CloudRelay = class {
|
|
5009
|
+
constructor(room, host, project, token) {
|
|
5010
|
+
this.room = room;
|
|
5011
|
+
this.host = host;
|
|
5012
|
+
this.project = project;
|
|
5013
|
+
this.token = token;
|
|
5014
|
+
this.ready = new Promise((resolve) => {
|
|
5015
|
+
this.settle = resolve;
|
|
5016
|
+
});
|
|
5017
|
+
this.connect();
|
|
5018
|
+
}
|
|
5019
|
+
room;
|
|
5020
|
+
host;
|
|
5021
|
+
project;
|
|
5022
|
+
token;
|
|
5023
|
+
canDraw = true;
|
|
5024
|
+
state = "connecting";
|
|
5025
|
+
/** Settles on the cloud's first answer, or once it is clear there will be none soon. */
|
|
5026
|
+
ready;
|
|
5027
|
+
settle = () => {
|
|
5028
|
+
};
|
|
5029
|
+
socket = null;
|
|
5030
|
+
peer = "";
|
|
5031
|
+
nextId = 1;
|
|
5032
|
+
delay = 1e3;
|
|
5033
|
+
timer;
|
|
5034
|
+
closed = false;
|
|
5035
|
+
/** Cloud peers with strokes on the way here, cleared when the socket drops. */
|
|
5036
|
+
peers = /* @__PURE__ */ new Set();
|
|
5037
|
+
url() {
|
|
5038
|
+
const base = new URL(this.host);
|
|
5039
|
+
base.protocol = base.protocol === "https:" ? "wss:" : "ws:";
|
|
5040
|
+
base.pathname = `/api/projects/${encodeURIComponent(this.project)}/canvases/${encodeURIComponent(this.room.canvas)}/drawings`;
|
|
5041
|
+
return base.href;
|
|
5042
|
+
}
|
|
5043
|
+
connect() {
|
|
5044
|
+
if (this.closed) return;
|
|
5045
|
+
const socket = new WebSocket(this.url(), {
|
|
5046
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
5047
|
+
maxPayload: MAX_MESSAGE * 4,
|
|
5048
|
+
handshakeTimeout: 1e4
|
|
5049
|
+
});
|
|
5050
|
+
this.socket = socket;
|
|
5051
|
+
let refusal = 0;
|
|
5052
|
+
socket.on("unexpected-response", (_req, res) => {
|
|
5053
|
+
refusal = res.statusCode ?? 0;
|
|
5054
|
+
res.resume();
|
|
5055
|
+
socket.terminate();
|
|
5056
|
+
});
|
|
5057
|
+
socket.on("message", (data, isBinary) => {
|
|
5058
|
+
if (isBinary) return;
|
|
5059
|
+
const message = parseServerMessage(data.toString());
|
|
5060
|
+
if (message) this.receive(message);
|
|
5061
|
+
});
|
|
5062
|
+
socket.on("error", () => {
|
|
5063
|
+
});
|
|
5064
|
+
socket.on("close", () => {
|
|
5065
|
+
if (this.socket !== socket) return;
|
|
5066
|
+
this.socket = null;
|
|
5067
|
+
for (const peer of this.peers) this.room.broadcast({ type: "gone", peer });
|
|
5068
|
+
this.peers.clear();
|
|
5069
|
+
if (this.closed) return;
|
|
5070
|
+
const wasSynced = this.state === "synced";
|
|
5071
|
+
this.state = "offline";
|
|
5072
|
+
this.settle();
|
|
5073
|
+
const wait = refusal === 401 ? 30 * 6e4 : refusal >= 400 ? 5 * 6e4 : wasSynced ? 1e3 : this.delay;
|
|
5074
|
+
this.delay = Math.min(this.delay * 2, 3e4);
|
|
5075
|
+
if (!this.canDraw) {
|
|
5076
|
+
this.canDraw = true;
|
|
5077
|
+
this.room.rehello();
|
|
5078
|
+
}
|
|
5079
|
+
this.timer = setTimeout(() => this.connect(), wait);
|
|
5080
|
+
this.timer.unref();
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
5083
|
+
receive(message) {
|
|
5084
|
+
const cloudPeer = (peer) => `cloud:${peer}`;
|
|
5085
|
+
switch (message.type) {
|
|
5086
|
+
case "hello": {
|
|
5087
|
+
this.peer = message.peer;
|
|
5088
|
+
this.canDraw = message.canDraw;
|
|
5089
|
+
this.state = "synced";
|
|
5090
|
+
this.delay = 1e3;
|
|
5091
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(message.drawings), ...Object.keys(this.room.drawings)]);
|
|
5092
|
+
for (const key of keys) {
|
|
5093
|
+
const up = this.room.merge(key, message.drawings[key] ?? emptyDrawing());
|
|
5094
|
+
if (up && this.canDraw) this.forward(key, up);
|
|
5095
|
+
}
|
|
5096
|
+
this.room.rehello();
|
|
5097
|
+
this.settle();
|
|
5098
|
+
break;
|
|
5099
|
+
}
|
|
5100
|
+
case "ops":
|
|
5101
|
+
if (message.peer !== this.peer)
|
|
5102
|
+
this.room.apply(message.key, { add: message.add, erase: message.erase }, cloudPeer(message.peer));
|
|
5103
|
+
break;
|
|
5104
|
+
case "doc": {
|
|
5105
|
+
const up = this.room.merge(message.key, message.drawing);
|
|
5106
|
+
if (up && this.canDraw) this.forward(message.key, up);
|
|
5107
|
+
break;
|
|
5108
|
+
}
|
|
5109
|
+
case "live":
|
|
5110
|
+
case "live-end":
|
|
5111
|
+
this.peers.add(cloudPeer(message.peer));
|
|
5112
|
+
this.room.broadcast({ ...message, peer: cloudPeer(message.peer) });
|
|
5113
|
+
break;
|
|
5114
|
+
case "gone":
|
|
5115
|
+
this.peers.delete(cloudPeer(message.peer));
|
|
5116
|
+
this.room.broadcast({ type: "gone", peer: cloudPeer(message.peer) });
|
|
5117
|
+
break;
|
|
5118
|
+
case "error":
|
|
5119
|
+
console.warn(`[design] cloud drawings (${this.room.canvas}): ${message.message}`);
|
|
5120
|
+
break;
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
forward(key, ops) {
|
|
5124
|
+
this.send({ type: "ops", id: this.nextId++, key, ...ops });
|
|
5125
|
+
}
|
|
5126
|
+
send(message) {
|
|
5127
|
+
if (this.state !== "synced" || !this.canDraw || !this.socket) return;
|
|
5128
|
+
send(this.socket, message);
|
|
5129
|
+
}
|
|
5130
|
+
close() {
|
|
5131
|
+
this.closed = true;
|
|
5132
|
+
clearTimeout(this.timer);
|
|
5133
|
+
this.socket?.close(1e3);
|
|
5134
|
+
this.socket = null;
|
|
5135
|
+
this.settle();
|
|
5136
|
+
}
|
|
5137
|
+
};
|
|
5138
|
+
var DrawingHub = class {
|
|
5139
|
+
constructor(paths, hasCanvas) {
|
|
5140
|
+
this.paths = paths;
|
|
5141
|
+
this.hasCanvas = hasCanvas;
|
|
5142
|
+
}
|
|
5143
|
+
paths;
|
|
5144
|
+
hasCanvas;
|
|
5145
|
+
rooms = /* @__PURE__ */ new Map();
|
|
5146
|
+
server = new WebSocketServer({ noServer: true, maxPayload: MAX_MESSAGE });
|
|
5147
|
+
peers = 0;
|
|
5148
|
+
nextPeer() {
|
|
5149
|
+
this.peers += 1;
|
|
5150
|
+
return `p${this.peers}`;
|
|
5151
|
+
}
|
|
5152
|
+
room(canvas) {
|
|
5153
|
+
if (!ID_PATTERN.test(canvas) || !this.hasCanvas(canvas)) return null;
|
|
5154
|
+
let room = this.rooms.get(canvas);
|
|
5155
|
+
if (!room) {
|
|
5156
|
+
room = new Room(this, canvas);
|
|
5157
|
+
this.rooms.set(canvas, room);
|
|
5158
|
+
}
|
|
5159
|
+
return room;
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* `GET /api/drawings/<canvas>` as a socket. Only the viewer's own pages may open one: the Origin
|
|
5163
|
+
* is this server on the viewer's host (a site elsewhere, or a frame on 127.0.0.1, is refused).
|
|
5164
|
+
*/
|
|
5165
|
+
upgrade(req, socket, head, port, extraHost) {
|
|
5166
|
+
const refuse = (status, text2) => {
|
|
5167
|
+
socket.end(`HTTP/1.1 ${status} ${text2}\r
|
|
5168
|
+
Connection: close\r
|
|
5169
|
+
Content-Length: 0\r
|
|
5170
|
+
\r
|
|
5171
|
+
`);
|
|
5172
|
+
};
|
|
5173
|
+
const host = req.headers.host;
|
|
5174
|
+
const origin = req.headers.origin;
|
|
5175
|
+
if (!isAllowedHost(host, port, extraHost) || parseHost(host)?.name === FRAME_HOST || origin !== `http://${host}`)
|
|
5176
|
+
return refuse(403, "Forbidden");
|
|
5177
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
5178
|
+
let canvas = "";
|
|
5179
|
+
try {
|
|
5180
|
+
canvas = decodeURIComponent(url.pathname.slice("/api/drawings/".length));
|
|
5181
|
+
} catch {
|
|
5182
|
+
}
|
|
5183
|
+
const room = this.room(canvas);
|
|
5184
|
+
if (!room) return refuse(404, "Not Found");
|
|
5185
|
+
this.server.handleUpgrade(req, socket, head, (ws) => room.join(ws));
|
|
5186
|
+
}
|
|
5187
|
+
/** Every drawing of a canvas, after the cloud had a moment to answer when `.design` is linked. */
|
|
5188
|
+
async read(canvas) {
|
|
5189
|
+
const room = this.room(canvas);
|
|
5190
|
+
if (!room) return null;
|
|
5191
|
+
await room.synced(SYNC_WAIT_MS);
|
|
5192
|
+
return { drawings: room.drawings, cloud: room.cloudState() };
|
|
5193
|
+
}
|
|
5194
|
+
/** Erase everything in one drawing; the number of strokes erased, or null without such a canvas. */
|
|
5195
|
+
async clear(canvas, key) {
|
|
5196
|
+
const room = this.room(canvas);
|
|
5197
|
+
if (!room) return null;
|
|
5198
|
+
await room.synced(SYNC_WAIT_MS);
|
|
5199
|
+
const erase = (room.drawings[key]?.strokes ?? []).map((stroke) => stroke.id);
|
|
5200
|
+
if (!erase.length) return 0;
|
|
5201
|
+
room.change(key, { erase }, "cli");
|
|
5202
|
+
room.flush();
|
|
5203
|
+
return erase.length;
|
|
5204
|
+
}
|
|
5205
|
+
/** A `.drawings.json` changed on disk. */
|
|
5206
|
+
fileChanged(canvas) {
|
|
5207
|
+
this.rooms.get(canvas)?.reload();
|
|
5208
|
+
}
|
|
5209
|
+
close() {
|
|
5210
|
+
for (const room of this.rooms.values()) room.close();
|
|
5211
|
+
this.rooms.clear();
|
|
5212
|
+
this.server.close();
|
|
5213
|
+
}
|
|
5214
|
+
};
|
|
5215
|
+
|
|
4536
5216
|
// src/server/events.ts
|
|
4537
5217
|
var EventHub = class {
|
|
4538
5218
|
clients = /* @__PURE__ */ new Set();
|
|
@@ -4566,82 +5246,63 @@ var EventHub = class {
|
|
|
4566
5246
|
}
|
|
4567
5247
|
};
|
|
4568
5248
|
|
|
4569
|
-
// src/server/hosts.ts
|
|
4570
|
-
var LOOPBACK = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
4571
|
-
var FRAME_HOST = "127.0.0.1";
|
|
4572
|
-
var VIEWER_HOST = "localhost";
|
|
4573
|
-
function parseHost(header) {
|
|
4574
|
-
const match = /^(\[[0-9a-f:.]+\]|[a-z0-9.-]+)(?::(\d{1,5}))?$/i.exec(header ?? "");
|
|
4575
|
-
if (!match) return null;
|
|
4576
|
-
return { name: match[1].toLowerCase(), port: match[2] ? Number(match[2]) : 80 };
|
|
4577
|
-
}
|
|
4578
|
-
function isAllowedHost(header, port, extra) {
|
|
4579
|
-
const host = parseHost(header);
|
|
4580
|
-
if (!host || host.port !== port) return false;
|
|
4581
|
-
return LOOPBACK.has(host.name) || host.name.endsWith(".localhost") || !!extra && host.name === extra.toLowerCase();
|
|
4582
|
-
}
|
|
4583
|
-
function isFrameApi(pathname, method) {
|
|
4584
|
-
if (pathname === "/api/health") return method === "GET";
|
|
4585
|
-
return /^\/api\/snapshots\/[^/]+\/[^/]+$/.test(pathname) && (method === "GET" || method === "POST");
|
|
4586
|
-
}
|
|
4587
|
-
|
|
4588
5249
|
// src/server/state.ts
|
|
4589
|
-
import
|
|
5250
|
+
import fs29 from "fs";
|
|
4590
5251
|
import os4 from "os";
|
|
4591
|
-
import
|
|
4592
|
-
var stateFile2 = (paths) =>
|
|
4593
|
-
var logFile = (paths) =>
|
|
5252
|
+
import path28 from "path";
|
|
5253
|
+
var stateFile2 = (paths) => path28.join(paths.cache, "server.json");
|
|
5254
|
+
var logFile = (paths) => path28.join(paths.cache, "server.log");
|
|
4594
5255
|
function readState(paths) {
|
|
4595
5256
|
try {
|
|
4596
|
-
return JSON.parse(
|
|
5257
|
+
return JSON.parse(fs29.readFileSync(stateFile2(paths), "utf8"));
|
|
4597
5258
|
} catch {
|
|
4598
5259
|
return null;
|
|
4599
5260
|
}
|
|
4600
5261
|
}
|
|
4601
5262
|
function registryDir() {
|
|
4602
|
-
const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA ||
|
|
4603
|
-
return
|
|
5263
|
+
const base = process.platform === "win32" ? process.env.LOCALAPPDATA || process.env.APPDATA || path28.join(os4.homedir(), "AppData", "Local") : process.env.XDG_CACHE_HOME || path28.join(os4.homedir(), ".cache");
|
|
5264
|
+
return path28.join(base, "foss-design", "previews");
|
|
4604
5265
|
}
|
|
4605
|
-
var entryFile = (pid) =>
|
|
5266
|
+
var entryFile = (pid) => path28.join(registryDir(), `${pid}.json`);
|
|
4606
5267
|
function writeState(paths, state) {
|
|
4607
|
-
|
|
4608
|
-
|
|
5268
|
+
fs29.mkdirSync(paths.cache, { recursive: true });
|
|
5269
|
+
fs29.writeFileSync(stateFile2(paths), `${JSON.stringify(state, null, 2)}
|
|
4609
5270
|
`);
|
|
4610
5271
|
try {
|
|
4611
|
-
|
|
4612
|
-
|
|
5272
|
+
fs29.mkdirSync(registryDir(), { recursive: true });
|
|
5273
|
+
fs29.writeFileSync(entryFile(state.pid), `${JSON.stringify(state, null, 2)}
|
|
4613
5274
|
`);
|
|
4614
5275
|
} catch {
|
|
4615
5276
|
}
|
|
4616
5277
|
}
|
|
4617
5278
|
function clearState(paths, pid) {
|
|
4618
5279
|
const state = readState(paths);
|
|
4619
|
-
|
|
5280
|
+
fs29.rmSync(entryFile(pid ?? state?.pid ?? -1), { force: true });
|
|
4620
5281
|
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
4621
|
-
|
|
5282
|
+
fs29.rmSync(stateFile2(paths), { force: true });
|
|
4622
5283
|
}
|
|
4623
5284
|
function listPreviews() {
|
|
4624
5285
|
let files = [];
|
|
4625
5286
|
try {
|
|
4626
|
-
files =
|
|
5287
|
+
files = fs29.readdirSync(registryDir()).filter((name) => name.endsWith(".json"));
|
|
4627
5288
|
} catch {
|
|
4628
5289
|
return [];
|
|
4629
5290
|
}
|
|
4630
5291
|
const found = [];
|
|
4631
5292
|
for (const name of files) {
|
|
4632
|
-
const file =
|
|
5293
|
+
const file = path28.join(registryDir(), name);
|
|
4633
5294
|
let state = null;
|
|
4634
5295
|
try {
|
|
4635
|
-
state = JSON.parse(
|
|
5296
|
+
state = JSON.parse(fs29.readFileSync(file, "utf8"));
|
|
4636
5297
|
} catch {
|
|
4637
5298
|
}
|
|
4638
5299
|
if (!state?.pid || !pidAlive(state.pid)) {
|
|
4639
|
-
|
|
5300
|
+
fs29.rmSync(file, { force: true });
|
|
4640
5301
|
continue;
|
|
4641
5302
|
}
|
|
4642
5303
|
let recorded = null;
|
|
4643
5304
|
try {
|
|
4644
|
-
recorded = JSON.parse(
|
|
5305
|
+
recorded = JSON.parse(fs29.readFileSync(path28.join(state.root, ".design", ".cache", "server.json"), "utf8"));
|
|
4645
5306
|
} catch {
|
|
4646
5307
|
}
|
|
4647
5308
|
found.push({ ...state, tracked: recorded?.pid === state.pid });
|
|
@@ -4655,11 +5316,11 @@ async function stopPreview(entry) {
|
|
|
4655
5316
|
}
|
|
4656
5317
|
const until = Date.now() + 5e3;
|
|
4657
5318
|
while (Date.now() < until && pidAlive(entry.pid)) await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4658
|
-
|
|
4659
|
-
const stateAt =
|
|
5319
|
+
fs29.rmSync(entryFile(entry.pid), { force: true });
|
|
5320
|
+
const stateAt = path28.join(entry.root, ".design", ".cache", "server.json");
|
|
4660
5321
|
try {
|
|
4661
|
-
if (JSON.parse(
|
|
4662
|
-
|
|
5322
|
+
if (JSON.parse(fs29.readFileSync(stateAt, "utf8")).pid === entry.pid)
|
|
5323
|
+
fs29.rmSync(stateAt, { force: true });
|
|
4663
5324
|
} catch {
|
|
4664
5325
|
}
|
|
4665
5326
|
}
|
|
@@ -4718,14 +5379,15 @@ function notFoundPage(res, title, text2) {
|
|
|
4718
5379
|
}
|
|
4719
5380
|
async function startDevServer(options) {
|
|
4720
5381
|
const paths = designPaths(options.root);
|
|
4721
|
-
if (!
|
|
4722
|
-
|
|
5382
|
+
if (!fs30.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
|
|
5383
|
+
fs30.mkdirSync(paths.cache, { recursive: true });
|
|
4723
5384
|
const snapshots = new SnapshotStore(paths.snapshots);
|
|
4724
5385
|
const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
|
|
4725
5386
|
const entries = new Entries(project);
|
|
4726
5387
|
entries.sync();
|
|
4727
5388
|
const links = linkShippedPackages(paths, project.appDir());
|
|
4728
5389
|
const events = new EventHub();
|
|
5390
|
+
const drawings = new DrawingHub(paths, (id2) => project.canvasIds().includes(id2));
|
|
4729
5391
|
const httpServer = http2.createServer();
|
|
4730
5392
|
const listening = () => httpServer.address()?.port ?? 0;
|
|
4731
5393
|
dropStaleDepCache(paths, project.appDir());
|
|
@@ -4746,8 +5408,8 @@ async function startDevServer(options) {
|
|
|
4746
5408
|
// `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
|
|
4747
5409
|
// generated entries are modules the frames load, so those stay watched.
|
|
4748
5410
|
ignored: (file) => {
|
|
4749
|
-
const rel = toPosix(
|
|
4750
|
-
const inCache = rel !== "" && !rel.startsWith("..") && !
|
|
5411
|
+
const rel = toPosix(path29.relative(paths.cache, file));
|
|
5412
|
+
const inCache = rel !== "" && !rel.startsWith("..") && !path29.isAbsolute(rel);
|
|
4751
5413
|
return inCache && rel !== "entries" && !rel.startsWith("entries/");
|
|
4752
5414
|
}
|
|
4753
5415
|
},
|
|
@@ -4756,7 +5418,7 @@ async function startDevServer(options) {
|
|
|
4756
5418
|
forwardConsole: false
|
|
4757
5419
|
},
|
|
4758
5420
|
optimizeDeps: {
|
|
4759
|
-
entries: entries.files().map((file) => toPosix(
|
|
5421
|
+
entries: entries.files().map((file) => toPosix(path29.relative(paths.design, file)))
|
|
4760
5422
|
}
|
|
4761
5423
|
});
|
|
4762
5424
|
const publicDir = project.publicDir();
|
|
@@ -4768,21 +5430,21 @@ async function startDevServer(options) {
|
|
|
4768
5430
|
return null;
|
|
4769
5431
|
}
|
|
4770
5432
|
};
|
|
4771
|
-
const viewer =
|
|
4772
|
-
const viewerIndex =
|
|
5433
|
+
const viewer = fs30.existsSync(PKG.viewer) ? sirv2(PKG.viewer, { dev: true, etag: true }) : null;
|
|
5434
|
+
const viewerIndex = path29.join(PKG.viewer, "index.html");
|
|
4773
5435
|
const serveViewerIndex = (req, res, url2) => {
|
|
4774
5436
|
if (parseHost(req.headers.host)?.name === FRAME_HOST) {
|
|
4775
5437
|
res.writeHead(302, { location: `http://${VIEWER_HOST}:${listening()}${url2.pathname}${url2.search}` });
|
|
4776
5438
|
res.end();
|
|
4777
5439
|
return;
|
|
4778
5440
|
}
|
|
4779
|
-
const html =
|
|
5441
|
+
const html = fs30.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
|
|
4780
5442
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
4781
5443
|
res.end(html);
|
|
4782
5444
|
};
|
|
4783
|
-
const ctx = { project, events, snapshots };
|
|
5445
|
+
const ctx = { project, events, snapshots, drawings };
|
|
4784
5446
|
const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
|
|
4785
|
-
const bootUrl = `/${relToDesign(paths,
|
|
5447
|
+
const bootUrl = `/${relToDesign(paths, path29.join(entries.dir, HTML_BOOT))}`;
|
|
4786
5448
|
const frameHead = (source, extra = {}) => {
|
|
4787
5449
|
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
4788
5450
|
return {
|
|
@@ -4803,7 +5465,7 @@ async function startDevServer(options) {
|
|
|
4803
5465
|
notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
|
|
4804
5466
|
return;
|
|
4805
5467
|
}
|
|
4806
|
-
if (!
|
|
5468
|
+
if (!fs30.existsSync(entries.entryFile(source))) entries.sync();
|
|
4807
5469
|
const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
|
|
4808
5470
|
const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
|
|
4809
5471
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -4812,8 +5474,8 @@ async function startDevServer(options) {
|
|
|
4812
5474
|
};
|
|
4813
5475
|
const serveHtmlScreen = async (res, url2) => {
|
|
4814
5476
|
const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
|
|
4815
|
-
const file =
|
|
4816
|
-
if (!isInside(paths.design, file) || !
|
|
5477
|
+
const file = path29.resolve(paths.design, rel);
|
|
5478
|
+
if (!isInside(paths.design, file) || !fs30.existsSync(file)) {
|
|
4817
5479
|
notFoundPage(res, "Screen not found", `No file .design/${rel}`);
|
|
4818
5480
|
return;
|
|
4819
5481
|
}
|
|
@@ -4823,13 +5485,13 @@ async function startDevServer(options) {
|
|
|
4823
5485
|
key: `${canvas}/${id2}`,
|
|
4824
5486
|
canvas,
|
|
4825
5487
|
id: id2,
|
|
4826
|
-
title:
|
|
5488
|
+
title: path29.basename(file),
|
|
4827
5489
|
file,
|
|
4828
5490
|
format: "html",
|
|
4829
5491
|
props: {},
|
|
4830
5492
|
system: true
|
|
4831
5493
|
};
|
|
4832
|
-
const raw =
|
|
5494
|
+
const raw = fs30.readFileSync(file, "utf8");
|
|
4833
5495
|
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
4834
5496
|
const html = injectIntoHtml(
|
|
4835
5497
|
raw,
|
|
@@ -4870,7 +5532,7 @@ async function startDevServer(options) {
|
|
|
4870
5532
|
}
|
|
4871
5533
|
if (url2.pathname.startsWith("/_fs/")) {
|
|
4872
5534
|
const rel = publicRel(url2.pathname.slice("/_fs/".length));
|
|
4873
|
-
if (publicDir && rel !== null && !project.isPublicFile(rel) &&
|
|
5535
|
+
if (publicDir && rel !== null && !project.isPublicFile(rel) && fs30.existsSync(path29.join(publicDir, rel))) {
|
|
4874
5536
|
notFoundPage(res, "Not found", url2.pathname);
|
|
4875
5537
|
return;
|
|
4876
5538
|
}
|
|
@@ -4882,7 +5544,7 @@ async function startDevServer(options) {
|
|
|
4882
5544
|
const rel = publicRel(url2.pathname.slice(1));
|
|
4883
5545
|
if (servePublic && rel !== null && project.isPublicFile(rel))
|
|
4884
5546
|
servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
|
|
4885
|
-
else if (
|
|
5547
|
+
else if (path29.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
|
|
4886
5548
|
else serveViewerIndex(req, res, url2);
|
|
4887
5549
|
};
|
|
4888
5550
|
if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(req, res, url2);
|
|
@@ -4892,6 +5554,11 @@ async function startDevServer(options) {
|
|
|
4892
5554
|
fail(error);
|
|
4893
5555
|
}
|
|
4894
5556
|
});
|
|
5557
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
5558
|
+
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
5559
|
+
if (pathname.startsWith("/api/drawings/")) drawings.upgrade(req, socket, head, listening(), options.host);
|
|
5560
|
+
else if (pathname.startsWith("/api/")) socket.destroy();
|
|
5561
|
+
});
|
|
4895
5562
|
const pending = /* @__PURE__ */ new Map();
|
|
4896
5563
|
let flushTimer;
|
|
4897
5564
|
const queue = (key, run) => {
|
|
@@ -4910,7 +5577,7 @@ async function startDevServer(options) {
|
|
|
4910
5577
|
};
|
|
4911
5578
|
vite.watcher.on("all", (event, file) => {
|
|
4912
5579
|
if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
|
|
4913
|
-
const rel = toPosix(
|
|
5580
|
+
const rel = toPosix(path29.relative(paths.design, file));
|
|
4914
5581
|
if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
|
|
4915
5582
|
if (rel === "design.json") {
|
|
4916
5583
|
queue("config", () => {
|
|
@@ -4926,6 +5593,10 @@ async function startDevServer(options) {
|
|
|
4926
5593
|
} else if (rel.startsWith("canvas/")) {
|
|
4927
5594
|
const [, id2, ...rest] = rel.split("/");
|
|
4928
5595
|
if (!id2) return;
|
|
5596
|
+
if (rest.join("/") === ".drawings.json") {
|
|
5597
|
+
drawings.fileChanged(id2);
|
|
5598
|
+
return;
|
|
5599
|
+
}
|
|
4929
5600
|
const structural = event !== "change" || rest.join("/") === "canvas.json";
|
|
4930
5601
|
if (!structural) return;
|
|
4931
5602
|
queue(`canvas:${id2}`, () => {
|
|
@@ -4957,6 +5628,7 @@ async function startDevServer(options) {
|
|
|
4957
5628
|
if (closed) return;
|
|
4958
5629
|
closed = true;
|
|
4959
5630
|
events.close();
|
|
5631
|
+
drawings.close();
|
|
4960
5632
|
await vite.close();
|
|
4961
5633
|
await new Promise((resolve) => {
|
|
4962
5634
|
httpServer.close(() => resolve());
|
|
@@ -5021,8 +5693,8 @@ async function ensureServer(paths, options) {
|
|
|
5021
5693
|
const recorded = readState(paths);
|
|
5022
5694
|
if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
|
|
5023
5695
|
for (const other of listPreviews()) if (other.root === paths.root) await stopPreview(other);
|
|
5024
|
-
|
|
5025
|
-
const log =
|
|
5696
|
+
fs31.mkdirSync(paths.cache, { recursive: true });
|
|
5697
|
+
const log = fs31.openSync(logFile(paths), "w");
|
|
5026
5698
|
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
5027
5699
|
if (options.port) args.push("--port", String(options.port));
|
|
5028
5700
|
const child = spawn(process.execPath, args, {
|
|
@@ -5034,7 +5706,7 @@ async function ensureServer(paths, options) {
|
|
|
5034
5706
|
child.unref();
|
|
5035
5707
|
const state = await waitForServer(paths, 4e4);
|
|
5036
5708
|
if (!state) {
|
|
5037
|
-
const tail =
|
|
5709
|
+
const tail = fs31.existsSync(logFile(paths)) ? fs31.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
|
|
5038
5710
|
throw new CliError(`The preview server did not start.
|
|
5039
5711
|
${tail.join("\n")}`);
|
|
5040
5712
|
}
|
|
@@ -5653,9 +6325,9 @@ function printLinkIssues(issues, project) {
|
|
|
5653
6325
|
}
|
|
5654
6326
|
|
|
5655
6327
|
// src/cli/commands/cloud.ts
|
|
5656
|
-
import
|
|
6328
|
+
import fs32 from "fs";
|
|
5657
6329
|
import os5 from "os";
|
|
5658
|
-
import
|
|
6330
|
+
import path30 from "path";
|
|
5659
6331
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5660
6332
|
async function runLogin() {
|
|
5661
6333
|
const host = cloudHost();
|
|
@@ -5756,7 +6428,7 @@ async function runLink(paths, wanted, options) {
|
|
|
5756
6428
|
print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
|
|
5757
6429
|
} else {
|
|
5758
6430
|
writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
|
|
5759
|
-
|
|
6431
|
+
fs32.rmSync(path30.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
|
|
5760
6432
|
const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
|
|
5761
6433
|
print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
|
|
5762
6434
|
}
|
|
@@ -5782,9 +6454,120 @@ async function runLink(paths, wanted, options) {
|
|
|
5782
6454
|
else if (remote) print(dim("Everything is in sync."));
|
|
5783
6455
|
}
|
|
5784
6456
|
|
|
6457
|
+
// src/cli/commands/drawings.ts
|
|
6458
|
+
import http3 from "http";
|
|
6459
|
+
function call(port, method, path39) {
|
|
6460
|
+
return new Promise((resolve, reject) => {
|
|
6461
|
+
const req = http3.request(
|
|
6462
|
+
{ host: "127.0.0.1", port, method, path: path39, headers: { host: `localhost:${port}` }, timeout: 3e4 },
|
|
6463
|
+
(res) => {
|
|
6464
|
+
const chunks = [];
|
|
6465
|
+
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
6466
|
+
res.on("end", () => {
|
|
6467
|
+
let body = null;
|
|
6468
|
+
try {
|
|
6469
|
+
body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
6470
|
+
} catch {
|
|
6471
|
+
}
|
|
6472
|
+
resolve({ status: res.statusCode ?? 0, body });
|
|
6473
|
+
});
|
|
6474
|
+
}
|
|
6475
|
+
);
|
|
6476
|
+
req.on("timeout", () => req.destroy(new Error("the preview server did not answer")));
|
|
6477
|
+
req.on("error", reject);
|
|
6478
|
+
req.end();
|
|
6479
|
+
});
|
|
6480
|
+
}
|
|
6481
|
+
async function previewPort(paths, quiet = false) {
|
|
6482
|
+
const wasRunning = await liveServer(paths);
|
|
6483
|
+
const server = await ensureServer(paths, {});
|
|
6484
|
+
if (!wasRunning && !quiet) print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
|
|
6485
|
+
return server.port;
|
|
6486
|
+
}
|
|
6487
|
+
async function readDrawings(port, canvas) {
|
|
6488
|
+
const { status, body } = await call(port, "GET", `/api/drawings/${encodeURIComponent(canvas)}`);
|
|
6489
|
+
if (status !== 200) throw new CliError(body?.error ?? `No canvas "${canvas}"`);
|
|
6490
|
+
const data = body;
|
|
6491
|
+
return { drawings: parseDrawings(data.drawings), cloud: data.cloud ?? "unlinked" };
|
|
6492
|
+
}
|
|
6493
|
+
function drawn(drawings, prefix) {
|
|
6494
|
+
const found = /* @__PURE__ */ new Map();
|
|
6495
|
+
for (const [key, drawing] of Object.entries(drawings))
|
|
6496
|
+
if (key.startsWith(`${prefix}:`) && drawing.strokes.length) found.set(key.slice(prefix.length + 1), drawing);
|
|
6497
|
+
return found;
|
|
6498
|
+
}
|
|
6499
|
+
var texts = (drawing) => drawing.strokes.flatMap((stroke) => stroke.kind === "text" && stroke.text ? [stroke.text] : []);
|
|
6500
|
+
async function runDrawings(paths, target, options) {
|
|
6501
|
+
const project = new DesignProject(paths, DEV_URLS);
|
|
6502
|
+
const [canvasId, screenId] = (target ?? "").split("/");
|
|
6503
|
+
const known = project.canvasIds();
|
|
6504
|
+
if (canvasId && !known.includes(canvasId))
|
|
6505
|
+
throw new CliError(`No canvas "${canvasId}". Canvases: ${known.join(", ") || "none yet"}`);
|
|
6506
|
+
if (options.clear && !screenId) throw new CliError("Usage: design drawings <canvas>/<screen> --clear");
|
|
6507
|
+
const port = await previewPort(paths, options.json);
|
|
6508
|
+
if (options.clear) {
|
|
6509
|
+
const { status, body } = await call(
|
|
6510
|
+
port,
|
|
6511
|
+
"DELETE",
|
|
6512
|
+
`/api/drawings/${encodeURIComponent(canvasId)}/${encodeURIComponent(markupKey(screenId))}`
|
|
6513
|
+
);
|
|
6514
|
+
if (status !== 200) throw new CliError(body?.error ?? `Could not erase (${status})`);
|
|
6515
|
+
const erased = body.erased ?? 0;
|
|
6516
|
+
if (options.json) print(JSON.stringify({ canvas: canvasId, screen: screenId, erased }));
|
|
6517
|
+
else
|
|
6518
|
+
print(
|
|
6519
|
+
erased ? `Erased the markup on ${canvasId}/${screenId} (${erased} strokes).` : `No markup on ${canvasId}/${screenId}.`
|
|
6520
|
+
);
|
|
6521
|
+
return;
|
|
6522
|
+
}
|
|
6523
|
+
const report = [];
|
|
6524
|
+
for (const id2 of canvasId ? [canvasId] : known) {
|
|
6525
|
+
const { drawings, cloud: cloud2 } = await readDrawings(port, id2);
|
|
6526
|
+
const doc = project.canvas(id2)?.doc;
|
|
6527
|
+
const pageTitle = (page) => doc?.pages.find((p) => p.id === page)?.title;
|
|
6528
|
+
const itemTitle = (item) => doc?.pages.flatMap((p) => p.sections.flatMap((s) => s.items)).find((i) => i.id === item)?.title;
|
|
6529
|
+
const boards = [...drawn(drawings, "board")].map(([page, drawing]) => ({
|
|
6530
|
+
page,
|
|
6531
|
+
title: pageTitle(page),
|
|
6532
|
+
strokes: drawing.strokes.length,
|
|
6533
|
+
groups: drawingGroups(drawing.strokes).length,
|
|
6534
|
+
texts: texts(drawing)
|
|
6535
|
+
}));
|
|
6536
|
+
const markup = [...drawn(drawings, "markup")].filter(([screen]) => !screenId || screen === screenId).map(([screen, drawing]) => ({
|
|
6537
|
+
screen,
|
|
6538
|
+
title: itemTitle(screen),
|
|
6539
|
+
strokes: drawing.strokes.length,
|
|
6540
|
+
texts: texts(drawing)
|
|
6541
|
+
}));
|
|
6542
|
+
report.push({ canvas: id2, cloud: cloud2, boards: screenId ? [] : boards, markup });
|
|
6543
|
+
}
|
|
6544
|
+
if (options.json) {
|
|
6545
|
+
print(JSON.stringify({ canvases: report }, null, 2));
|
|
6546
|
+
return;
|
|
6547
|
+
}
|
|
6548
|
+
const quote = (text2) => `\u201C${text2.replace(/\s*\n\s*/g, " / ").slice(0, 120)}\u201D`;
|
|
6549
|
+
let any = false;
|
|
6550
|
+
for (const entry of report) {
|
|
6551
|
+
if (!entry.boards.length && !entry.markup.length) continue;
|
|
6552
|
+
any = true;
|
|
6553
|
+
const cloud2 = entry.cloud === "synced" ? dim(" \xB7 in step with the cloud") : entry.cloud === "unlinked" ? "" : dim(" \xB7 the cloud did not answer: drawings on this machine only");
|
|
6554
|
+
print(`${bold(entry.canvas)}${cloud2}`);
|
|
6555
|
+
for (const board of entry.boards)
|
|
6556
|
+
print(
|
|
6557
|
+
` board ${board.page}${board.title ? dim(` (${board.title})`) : ""} \xB7 ${board.strokes} strokes${board.groups > 1 ? ` in ${board.groups} groups` : ""}${board.texts.length ? ` \xB7 ${board.texts.map(quote).join(", ")}` : ""}`
|
|
6558
|
+
);
|
|
6559
|
+
for (const mark of entry.markup)
|
|
6560
|
+
print(
|
|
6561
|
+
` markup ${mark.screen}${mark.title ? dim(` (${mark.title})`) : ""} \xB7 ${mark.strokes} strokes${mark.texts.length ? ` \xB7 ${mark.texts.map(quote).join(", ")}` : ""}`
|
|
6562
|
+
);
|
|
6563
|
+
}
|
|
6564
|
+
if (!any) print(canvasId ? `No drawings on ${target}.` : "No drawings yet.");
|
|
6565
|
+
else print(dim("Pictures: design shot <canvas> --board | design shot <canvas>[/<screen>] --markup"));
|
|
6566
|
+
}
|
|
6567
|
+
|
|
5785
6568
|
// src/cli/commands/icon.ts
|
|
5786
|
-
import
|
|
5787
|
-
import
|
|
6569
|
+
import fs33 from "fs";
|
|
6570
|
+
import path31 from "path";
|
|
5788
6571
|
async function toCloud(paths, file) {
|
|
5789
6572
|
const link = readLink(paths);
|
|
5790
6573
|
if (!link) return;
|
|
@@ -5795,10 +6578,10 @@ async function toCloud(paths, file) {
|
|
|
5795
6578
|
}
|
|
5796
6579
|
const client = new CloudClient(link.host, credential.token);
|
|
5797
6580
|
try {
|
|
5798
|
-
const ext = file ?
|
|
6581
|
+
const ext = file ? path31.extname(file).slice(1) : null;
|
|
5799
6582
|
await client.setProjectIcon(
|
|
5800
6583
|
link.project,
|
|
5801
|
-
file && ext ? { data:
|
|
6584
|
+
file && ext ? { data: fs33.readFileSync(file), type: ICON_TYPES[ext] } : null
|
|
5802
6585
|
);
|
|
5803
6586
|
link.icon = file ? iconHash(file) : null;
|
|
5804
6587
|
writeLink(paths, link);
|
|
@@ -5811,35 +6594,35 @@ async function runIcon(paths, file, options) {
|
|
|
5811
6594
|
if (options.remove) {
|
|
5812
6595
|
const had = iconFile(paths);
|
|
5813
6596
|
writeIcon(paths, null);
|
|
5814
|
-
print(had ? `${green("Removed")} ${
|
|
6597
|
+
print(had ? `${green("Removed")} ${path31.relative(process.cwd(), had)}` : "The project has no icon.");
|
|
5815
6598
|
await toCloud(paths, null);
|
|
5816
6599
|
return;
|
|
5817
6600
|
}
|
|
5818
6601
|
if (!file) {
|
|
5819
6602
|
const current = iconFile(paths);
|
|
5820
6603
|
if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
|
|
5821
|
-
return print(`${
|
|
6604
|
+
return print(`${path31.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs33.statSync(current).size / 1024)} KB`)}`);
|
|
5822
6605
|
}
|
|
5823
6606
|
let data;
|
|
5824
6607
|
try {
|
|
5825
|
-
data =
|
|
6608
|
+
data = fs33.readFileSync(file);
|
|
5826
6609
|
} catch {
|
|
5827
6610
|
throw new CliError(`No file ${file}`);
|
|
5828
6611
|
}
|
|
5829
|
-
const problem = iconProblem(data,
|
|
6612
|
+
const problem = iconProblem(data, path31.extname(file).slice(1).toLowerCase() || void 0);
|
|
5830
6613
|
if (problem) throw new CliError(`${file}: ${problem}`);
|
|
5831
6614
|
const written = writeIcon(paths, data);
|
|
5832
|
-
print(`${green("Set")} the project icon: ${
|
|
6615
|
+
print(`${green("Set")} the project icon: ${path31.relative(process.cwd(), written)}`);
|
|
5833
6616
|
await toCloud(paths, written);
|
|
5834
6617
|
}
|
|
5835
6618
|
|
|
5836
6619
|
// src/cli/commands/import.ts
|
|
5837
|
-
import
|
|
5838
|
-
import
|
|
6620
|
+
import fs35 from "fs";
|
|
6621
|
+
import path33 from "path";
|
|
5839
6622
|
|
|
5840
6623
|
// src/import/claude-design.ts
|
|
5841
|
-
import
|
|
5842
|
-
import
|
|
6624
|
+
import fs34 from "fs";
|
|
6625
|
+
import path32 from "path";
|
|
5843
6626
|
|
|
5844
6627
|
// src/import/markup.ts
|
|
5845
6628
|
var VOID = /* @__PURE__ */ new Set([
|
|
@@ -6447,11 +7230,11 @@ function boardId(file, used) {
|
|
|
6447
7230
|
return id2;
|
|
6448
7231
|
}
|
|
6449
7232
|
function importClaudeDesign(dir, options = {}) {
|
|
6450
|
-
const canvasFile =
|
|
6451
|
-
if (!
|
|
7233
|
+
const canvasFile = path32.join(dir, "canvas.json");
|
|
7234
|
+
if (!fs34.existsSync(canvasFile))
|
|
6452
7235
|
throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
|
|
6453
|
-
const source = JSON.parse(
|
|
6454
|
-
const templates =
|
|
7236
|
+
const source = JSON.parse(fs34.readFileSync(canvasFile, "utf8"));
|
|
7237
|
+
const templates = fs34.readdirSync(dir).filter((file) => file.endsWith(EXT));
|
|
6455
7238
|
if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
|
|
6456
7239
|
const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
|
|
6457
7240
|
const warnings = /* @__PURE__ */ new Map();
|
|
@@ -6478,7 +7261,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
6478
7261
|
try {
|
|
6479
7262
|
files.set(
|
|
6480
7263
|
`${name}.jsx`,
|
|
6481
|
-
convertFile(name,
|
|
7264
|
+
convertFile(name, fs34.readFileSync(path32.join(dir, file), "utf8"), { known, routes, warn: warn2 })
|
|
6482
7265
|
);
|
|
6483
7266
|
} catch (error) {
|
|
6484
7267
|
warn2(`could not be converted: ${error.message}`);
|
|
@@ -6526,7 +7309,7 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
6526
7309
|
screens++;
|
|
6527
7310
|
}
|
|
6528
7311
|
const canvas = {
|
|
6529
|
-
title: options.title ?? source.title ??
|
|
7312
|
+
title: options.title ?? source.title ?? path32.basename(path32.resolve(dir)),
|
|
6530
7313
|
description: "Imported from Claude Design.",
|
|
6531
7314
|
// The templates bring their own styles; the design system's reset would restyle them.
|
|
6532
7315
|
system: false,
|
|
@@ -6538,21 +7321,21 @@ function importClaudeDesign(dir, options = {}) {
|
|
|
6538
7321
|
// src/cli/commands/import.ts
|
|
6539
7322
|
var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
|
|
6540
7323
|
function runImport(paths, from, options) {
|
|
6541
|
-
const dir =
|
|
6542
|
-
if (!
|
|
7324
|
+
const dir = path33.resolve(from);
|
|
7325
|
+
if (!fs35.existsSync(dir) || !fs35.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
|
|
6543
7326
|
let result;
|
|
6544
7327
|
try {
|
|
6545
7328
|
result = importClaudeDesign(dir, { title: options.title });
|
|
6546
7329
|
} catch (error) {
|
|
6547
7330
|
throw new CliError(error.message);
|
|
6548
7331
|
}
|
|
6549
|
-
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(
|
|
7332
|
+
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path33.basename(dir)) || "imported");
|
|
6550
7333
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
|
|
6551
|
-
const out =
|
|
6552
|
-
if (
|
|
6553
|
-
|
|
6554
|
-
for (const [name, content] of result.files)
|
|
6555
|
-
|
|
7334
|
+
const out = path33.join(paths.canvases, id2);
|
|
7335
|
+
if (fs35.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
|
|
7336
|
+
fs35.mkdirSync(path33.join(out, "screens"), { recursive: true });
|
|
7337
|
+
for (const [name, content] of result.files) fs35.writeFileSync(path33.join(out, "screens", name), content);
|
|
7338
|
+
fs35.writeFileSync(path33.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
|
|
6556
7339
|
`);
|
|
6557
7340
|
print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
|
|
6558
7341
|
print(
|
|
@@ -6572,7 +7355,7 @@ function runImport(paths, from, options) {
|
|
|
6572
7355
|
|
|
6573
7356
|
// src/cli/commands/merge.ts
|
|
6574
7357
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
6575
|
-
import
|
|
7358
|
+
import path34 from "path";
|
|
6576
7359
|
import { createInterface } from "readline/promises";
|
|
6577
7360
|
var KIND = {
|
|
6578
7361
|
text: "both sides changed lines, the markers show them",
|
|
@@ -6588,7 +7371,7 @@ function matchTargets(paths, states, targets2) {
|
|
|
6588
7371
|
for (const conflict of openConflicts(paths, unit)) picked.push({ state: unit, conflict });
|
|
6589
7372
|
continue;
|
|
6590
7373
|
}
|
|
6591
|
-
const rel = [
|
|
7374
|
+
const rel = [path34.relative(paths.design, path34.resolve(target)), target.replace(/^\.design\//, "")];
|
|
6592
7375
|
const found = states.flatMap(
|
|
6593
7376
|
(state) => state.conflicts.filter((item) => rel.includes(item.path)).map((conflict) => ({ state, conflict }))
|
|
6594
7377
|
);
|
|
@@ -6604,7 +7387,7 @@ function printOpen(paths, states) {
|
|
|
6604
7387
|
print(`${bold(state.unit)} ${dim(`merged with cloud r${state.headRev}`)}`);
|
|
6605
7388
|
for (const conflict of open2) {
|
|
6606
7389
|
print(` ${red("\u2717")} ${conflict.path} ${dim(KIND[conflict.kind])}`);
|
|
6607
|
-
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${
|
|
7390
|
+
const sides = ["here", "cloud", "base"].map((side) => `${side}: ${path34.relative(process.cwd(), sideFile(paths, state.unit, conflict.path, side))}`).join(" \xB7 ");
|
|
6608
7391
|
print(` ${dim(sides)}`);
|
|
6609
7392
|
}
|
|
6610
7393
|
}
|
|
@@ -6685,7 +7468,7 @@ async function runMerge(paths, targets2, options) {
|
|
|
6685
7468
|
if (answer === "s") break;
|
|
6686
7469
|
if (answer === "e" && edit) {
|
|
6687
7470
|
const editor = process.env.VISUAL || process.env.EDITOR || "vi";
|
|
6688
|
-
spawnSync2(editor, [
|
|
7471
|
+
spawnSync2(editor, [path34.join(paths.design, conflict.path)], { stdio: "inherit", shell: true });
|
|
6689
7472
|
if (!openConflicts(paths, state).some((item) => item.path === conflict.path)) break;
|
|
6690
7473
|
print(yellow(" Conflict markers are still there."));
|
|
6691
7474
|
}
|
|
@@ -6704,7 +7487,7 @@ async function runMerge(paths, targets2, options) {
|
|
|
6704
7487
|
}
|
|
6705
7488
|
|
|
6706
7489
|
// src/cli/commands/rename.ts
|
|
6707
|
-
import
|
|
7490
|
+
import fs36 from "fs";
|
|
6708
7491
|
async function runRename(paths, name) {
|
|
6709
7492
|
const trimmed = name.trim();
|
|
6710
7493
|
if (!trimmed) throw new CliError("Usage: design rename <name>");
|
|
@@ -6719,17 +7502,17 @@ async function runRename(paths, name) {
|
|
|
6719
7502
|
}
|
|
6720
7503
|
let config = {};
|
|
6721
7504
|
try {
|
|
6722
|
-
config = JSON.parse(
|
|
7505
|
+
config = JSON.parse(fs36.readFileSync(paths.config, "utf8"));
|
|
6723
7506
|
} catch {
|
|
6724
7507
|
}
|
|
6725
|
-
|
|
7508
|
+
fs36.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
|
|
6726
7509
|
`);
|
|
6727
7510
|
print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
|
|
6728
7511
|
}
|
|
6729
7512
|
|
|
6730
7513
|
// src/cli/commands/scaffold.ts
|
|
6731
|
-
import
|
|
6732
|
-
import
|
|
7514
|
+
import fs37 from "fs";
|
|
7515
|
+
import path35 from "path";
|
|
6733
7516
|
|
|
6734
7517
|
// src/cli/templates.ts
|
|
6735
7518
|
var TOKENS_CSS = `/*
|
|
@@ -6940,40 +7723,40 @@ function mainScreen(canvasId) {
|
|
|
6940
7723
|
|
|
6941
7724
|
// src/cli/commands/scaffold.ts
|
|
6942
7725
|
function write(file, content, root) {
|
|
6943
|
-
if (
|
|
6944
|
-
print(` ${dim("exists ")} ${
|
|
7726
|
+
if (fs37.existsSync(file)) {
|
|
7727
|
+
print(` ${dim("exists ")} ${path35.relative(root, file)}`);
|
|
6945
7728
|
return false;
|
|
6946
7729
|
}
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
print(` ${green("created")} ${
|
|
7730
|
+
fs37.mkdirSync(path35.dirname(file), { recursive: true });
|
|
7731
|
+
fs37.writeFileSync(file, content);
|
|
7732
|
+
print(` ${green("created")} ${path35.relative(root, file)}`);
|
|
6950
7733
|
return true;
|
|
6951
7734
|
}
|
|
6952
7735
|
function ensureGitignored(root) {
|
|
6953
|
-
const file =
|
|
6954
|
-
const current =
|
|
7736
|
+
const file = path35.join(root, ".gitignore");
|
|
7737
|
+
const current = fs37.existsSync(file) ? fs37.readFileSync(file, "utf8") : "";
|
|
6955
7738
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
6956
7739
|
if (covered) return "present";
|
|
6957
7740
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
6958
|
-
|
|
7741
|
+
fs37.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
6959
7742
|
.design/
|
|
6960
7743
|
`);
|
|
6961
7744
|
return "added";
|
|
6962
7745
|
}
|
|
6963
7746
|
function projectName(root) {
|
|
6964
7747
|
try {
|
|
6965
|
-
const manifest = JSON.parse(
|
|
7748
|
+
const manifest = JSON.parse(fs37.readFileSync(path35.join(root, "package.json"), "utf8"));
|
|
6966
7749
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
6967
7750
|
} catch {
|
|
6968
7751
|
}
|
|
6969
|
-
return
|
|
7752
|
+
return path35.basename(root);
|
|
6970
7753
|
}
|
|
6971
7754
|
function runInit(root, options) {
|
|
6972
7755
|
const paths = designPaths(root);
|
|
6973
7756
|
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
6974
7757
|
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
6975
7758
|
`, root);
|
|
6976
|
-
|
|
7759
|
+
fs37.mkdirSync(paths.canvases, { recursive: true });
|
|
6977
7760
|
if (options.gitignore) {
|
|
6978
7761
|
const result = ensureGitignored(root);
|
|
6979
7762
|
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
@@ -6987,49 +7770,59 @@ function runSystemInit(root, options) {
|
|
|
6987
7770
|
const paths = designPaths(root);
|
|
6988
7771
|
let base = projectName(root);
|
|
6989
7772
|
try {
|
|
6990
|
-
base = JSON.parse(
|
|
7773
|
+
base = JSON.parse(fs37.readFileSync(paths.config, "utf8")).name || base;
|
|
6991
7774
|
} catch {
|
|
6992
7775
|
}
|
|
6993
7776
|
const name = options.name ?? `${base} design system`;
|
|
6994
|
-
print(`Scaffolding the design system in ${
|
|
6995
|
-
write(
|
|
7777
|
+
print(`Scaffolding the design system in ${path35.relative(root, paths.system)}`);
|
|
7778
|
+
write(path35.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
6996
7779
|
`, root);
|
|
6997
|
-
write(
|
|
7780
|
+
write(path35.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
6998
7781
|
if (!options.empty) {
|
|
6999
|
-
write(
|
|
7000
|
-
write(
|
|
7001
|
-
write(
|
|
7782
|
+
write(path35.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
7783
|
+
write(path35.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
7784
|
+
write(path35.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
7002
7785
|
}
|
|
7003
7786
|
}
|
|
7004
7787
|
function runNew(root, id2, options) {
|
|
7005
7788
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
7006
7789
|
const paths = designPaths(root);
|
|
7007
|
-
const dir =
|
|
7008
|
-
if (
|
|
7790
|
+
const dir = path35.join(paths.canvases, id2);
|
|
7791
|
+
if (fs37.existsSync(path35.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
7009
7792
|
print(`Creating canvas "${id2}"`);
|
|
7010
7793
|
const title = options.title ?? titleize(id2);
|
|
7011
7794
|
if (options.empty) {
|
|
7012
7795
|
write(
|
|
7013
|
-
|
|
7796
|
+
path35.join(dir, "canvas.json"),
|
|
7014
7797
|
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
7015
7798
|
`,
|
|
7016
7799
|
root
|
|
7017
7800
|
);
|
|
7018
|
-
|
|
7801
|
+
fs37.mkdirSync(path35.join(dir, "screens"), { recursive: true });
|
|
7019
7802
|
} else {
|
|
7020
|
-
write(
|
|
7021
|
-
write(
|
|
7803
|
+
write(path35.join(dir, "canvas.json"), canvasJson(title), root);
|
|
7804
|
+
write(path35.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
7022
7805
|
}
|
|
7023
7806
|
}
|
|
7024
7807
|
|
|
7025
7808
|
// src/cli/commands/shot.ts
|
|
7026
|
-
import
|
|
7027
|
-
import
|
|
7809
|
+
import fs38 from "fs";
|
|
7810
|
+
import path36 from "path";
|
|
7028
7811
|
var OVERVIEW_TIMEOUT = 9e4;
|
|
7812
|
+
var DRAWING_TIMEOUT = 45e3;
|
|
7813
|
+
var DRAWING_MAX_SIDE = 1280;
|
|
7029
7814
|
async function runShot(paths, target, options) {
|
|
7030
7815
|
const [canvasId = "", screenId] = target.split("/");
|
|
7031
7816
|
const project = new DesignProject(paths, DEV_URLS);
|
|
7032
|
-
if (canvasId === SYSTEM_CANVAS)
|
|
7817
|
+
if (canvasId === SYSTEM_CANVAS) {
|
|
7818
|
+
if (options.board || options.markup) throw new CliError("--board and --markup apply to canvases, not @system");
|
|
7819
|
+
return shootSystem(paths, project, screenId, options);
|
|
7820
|
+
}
|
|
7821
|
+
if (options.board && options.markup) throw new CliError("Take --board and --markup one at a time");
|
|
7822
|
+
if (options.max !== void 0 && !options.board && !options.markup)
|
|
7823
|
+
throw new CliError("--max and --full size the pictures of --board and --markup");
|
|
7824
|
+
if (options.board && screenId)
|
|
7825
|
+
throw new CliError("Boards belong to pages: design shot <canvas> --board [--page <id>]");
|
|
7033
7826
|
const canvas = project.canvas(canvasId);
|
|
7034
7827
|
if (!canvas) {
|
|
7035
7828
|
const known = project.canvasIds();
|
|
@@ -7040,15 +7833,26 @@ async function runShot(paths, target, options) {
|
|
|
7040
7833
|
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
7041
7834
|
}
|
|
7042
7835
|
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
7043
|
-
const outBase =
|
|
7044
|
-
const outDir =
|
|
7045
|
-
|
|
7836
|
+
const outBase = path36.resolve(options.out ?? paths.shots);
|
|
7837
|
+
const outDir = path36.join(outBase, canvasId);
|
|
7838
|
+
fs38.mkdirSync(outDir, { recursive: true });
|
|
7046
7839
|
const base = await serverBase(paths);
|
|
7047
7840
|
const browser = await launchChrome();
|
|
7048
7841
|
let failed = false;
|
|
7049
7842
|
try {
|
|
7843
|
+
if (options.board || options.markup) {
|
|
7844
|
+
if (!fs38.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there are no drawings to shoot.");
|
|
7845
|
+
const drawingsFailed = await shootDrawings(browser, base, canvasId, outDir, theme2, {
|
|
7846
|
+
board: !!options.board,
|
|
7847
|
+
page: options.page,
|
|
7848
|
+
screen: screenId,
|
|
7849
|
+
max: options.max ?? DRAWING_MAX_SIDE
|
|
7850
|
+
});
|
|
7851
|
+
if (drawingsFailed) process.exitCode = 1;
|
|
7852
|
+
return;
|
|
7853
|
+
}
|
|
7050
7854
|
if (options.overview) {
|
|
7051
|
-
if (!
|
|
7855
|
+
if (!fs38.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
7052
7856
|
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);
|
|
7053
7857
|
for (const pageId of pages) {
|
|
7054
7858
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
@@ -7064,7 +7868,7 @@ async function runShot(paths, target, options) {
|
|
|
7064
7868
|
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
7065
7869
|
}
|
|
7066
7870
|
await page.waitForTimeout(400);
|
|
7067
|
-
const file =
|
|
7871
|
+
const file = path36.join(outDir, `${pageId}.overview.png`);
|
|
7068
7872
|
await page.screenshot({ path: file });
|
|
7069
7873
|
await page.close();
|
|
7070
7874
|
print(file);
|
|
@@ -7083,6 +7887,62 @@ async function runShot(paths, target, options) {
|
|
|
7083
7887
|
}
|
|
7084
7888
|
if (failed) process.exitCode = 1;
|
|
7085
7889
|
}
|
|
7890
|
+
async function shootDrawings(browser, base, canvasId, outDir, theme2, want) {
|
|
7891
|
+
const port = Number(new URL(base).port);
|
|
7892
|
+
const { drawings, cloud: cloud2 } = await readDrawings(port, canvasId);
|
|
7893
|
+
if (cloud2 === "offline" || cloud2 === "connecting")
|
|
7894
|
+
warn("the cloud did not answer in time: these are the drawings on this machine");
|
|
7895
|
+
const found = drawn(drawings, want.board ? "board" : "markup");
|
|
7896
|
+
const named = want.board ? want.page : want.screen;
|
|
7897
|
+
const ids = named ? [named] : [...found.keys()];
|
|
7898
|
+
if (!ids.length) {
|
|
7899
|
+
print(want.board ? `No idea board drawn on in "${canvasId}".` : `No screen marked up in "${canvasId}".`);
|
|
7900
|
+
return false;
|
|
7901
|
+
}
|
|
7902
|
+
if (named && !found.has(named))
|
|
7903
|
+
print(dim(want.board ? `Nothing on the board of "${named}" yet.` : `No markup on "${named}" yet.`));
|
|
7904
|
+
let failed = false;
|
|
7905
|
+
for (const id2 of ids) {
|
|
7906
|
+
const kind = want.board ? "board" : "markup";
|
|
7907
|
+
const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, colorScheme: theme2 });
|
|
7908
|
+
try {
|
|
7909
|
+
const url = `${base}/c/${encodeURIComponent(canvasId)}/${kind}/${encodeURIComponent(id2)}?capture=1&theme=${theme2}&max=${want.max}`;
|
|
7910
|
+
await page.goto(url, { waitUntil: "load" });
|
|
7911
|
+
await page.waitForFunction(
|
|
7912
|
+
() => window.__DESIGN_DRAWING_READY__ === true,
|
|
7913
|
+
null,
|
|
7914
|
+
{ timeout: DRAWING_TIMEOUT }
|
|
7915
|
+
);
|
|
7916
|
+
const size2 = await page.evaluate(
|
|
7917
|
+
() => window.__DESIGN_DRAWING_SIZE__
|
|
7918
|
+
);
|
|
7919
|
+
if (size2?.width && size2.height)
|
|
7920
|
+
await page.setViewportSize({ width: Math.round(size2.width), height: Math.min(Math.round(size2.height), 16e3) });
|
|
7921
|
+
await page.waitForTimeout(200);
|
|
7922
|
+
if (!want.board) {
|
|
7923
|
+
const file = path36.join(outDir, `${id2}.markup.${theme2}.png`);
|
|
7924
|
+
await page.screenshot({ path: file });
|
|
7925
|
+
print(file);
|
|
7926
|
+
continue;
|
|
7927
|
+
}
|
|
7928
|
+
const parts = await page.evaluate(
|
|
7929
|
+
() => window.__DESIGN_DRAWING_PARTS__ ?? 0
|
|
7930
|
+
);
|
|
7931
|
+
for (let part = 0; part < parts; part++) {
|
|
7932
|
+
const file = path36.join(outDir, parts > 1 ? `${id2}.board.${part + 1}.${theme2}.png` : `${id2}.board.${theme2}.png`);
|
|
7933
|
+
await page.locator(`[data-part="${part}"]`).screenshot({ path: file });
|
|
7934
|
+
print(file);
|
|
7935
|
+
}
|
|
7936
|
+
} catch (error) {
|
|
7937
|
+
failed = true;
|
|
7938
|
+
print(`${red("\u2717")} ${canvasId}/${id2}: ${error.message.split("\n")[0]}`);
|
|
7939
|
+
} finally {
|
|
7940
|
+
await page.close().catch(() => {
|
|
7941
|
+
});
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
return failed;
|
|
7945
|
+
}
|
|
7086
7946
|
async function serverBase(paths) {
|
|
7087
7947
|
const wasRunning = await liveServer(paths);
|
|
7088
7948
|
const server = await ensureServer(paths, {});
|
|
@@ -7093,7 +7953,7 @@ async function shootFrames(browser, jobs, outDir) {
|
|
|
7093
7953
|
let failed = false;
|
|
7094
7954
|
const results = await mapLimit(jobs, 4, async (job) => {
|
|
7095
7955
|
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
7096
|
-
const file =
|
|
7956
|
+
const file = path36.join(outDir, `${job.id}.${job.theme}.png`);
|
|
7097
7957
|
try {
|
|
7098
7958
|
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
7099
7959
|
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
@@ -7125,8 +7985,8 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
7125
7985
|
if (id2 && !all.some((job) => job.id === id2)) {
|
|
7126
7986
|
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
7127
7987
|
}
|
|
7128
|
-
const outDir =
|
|
7129
|
-
|
|
7988
|
+
const outDir = path36.join(path36.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
7989
|
+
fs38.mkdirSync(outDir, { recursive: true });
|
|
7130
7990
|
const base = await serverBase(paths);
|
|
7131
7991
|
const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
|
|
7132
7992
|
const browser = await launchChrome();
|
|
@@ -7176,7 +8036,7 @@ async function runSnapshots(paths, targets2, options) {
|
|
|
7176
8036
|
}
|
|
7177
8037
|
|
|
7178
8038
|
// src/cli/commands/cloud-status.ts
|
|
7179
|
-
import
|
|
8039
|
+
import path37 from "path";
|
|
7180
8040
|
var STATE_TEXT = {
|
|
7181
8041
|
in_sync: green,
|
|
7182
8042
|
local_changes: yellow,
|
|
@@ -7199,7 +8059,7 @@ function detail(paths, status) {
|
|
|
7199
8059
|
if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
|
|
7200
8060
|
if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
|
|
7201
8061
|
parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
|
|
7202
|
-
if (status.conflictRev !== void 0) parts.push(`merge ${
|
|
8062
|
+
if (status.conflictRev !== void 0) parts.push(`merge ${path37.relative(paths.root, incomingDir(paths, status.key))}`);
|
|
7203
8063
|
return parts.join(" \xB7 ");
|
|
7204
8064
|
}
|
|
7205
8065
|
async function printCloudStatus(paths, label) {
|
|
@@ -7347,8 +8207,17 @@ ${bold("Verify")}
|
|
|
7347
8207
|
[--page <id>] viewer to show while frames do not run (push does this
|
|
7348
8208
|
for the canvases it pushes)
|
|
7349
8209
|
shot <canvas>[/<screen>] [--page <id>] Screenshot screens with Chrome (PNG paths are printed);
|
|
7350
|
-
[--theme light|dark] [--out <dir>] @system or @system/<id> shoots the design system specimens
|
|
7351
|
-
[--overview]
|
|
8210
|
+
[--theme light|dark] [--out <dir>] @system or @system/<id> shoots the design system specimens;
|
|
8211
|
+
[--overview] [--board | --markup] --board shoots the idea boards drawn on (or --page's),
|
|
8212
|
+
[--max <px> | --full] one PNG per group of sketches; --markup the screens with
|
|
8213
|
+
markup, drawn over them. Those pictures are at most 1280 px
|
|
8214
|
+
on the long side; --max sets another limit, --full none
|
|
8215
|
+
|
|
8216
|
+
${bold("Drawings")} ${dim("idea boards of pages and markup over screens, drawn in the viewer; shared live in the cloud")}
|
|
8217
|
+
drawings [<canvas>[/<screen>]] [--json] Boards and markup with something on them: strokes and the
|
|
8218
|
+
text written on them (read through the preview server,
|
|
8219
|
+
which syncs them with the cloud when linked)
|
|
8220
|
+
drawings <canvas>/<screen> --clear Erase a screen's markup once it is dealt with
|
|
7352
8221
|
|
|
7353
8222
|
${bold("Share")}
|
|
7354
8223
|
build [canvas\u2026] [--out <dir>] [--tar] Static site of the canvases and the design system
|
|
@@ -7397,7 +8266,7 @@ ${bold("Environment")}
|
|
|
7397
8266
|
FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
|
|
7398
8267
|
`;
|
|
7399
8268
|
function projectPaths(rootOption, requireDesign = true) {
|
|
7400
|
-
const found = rootOption ?
|
|
8269
|
+
const found = rootOption ? path38.resolve(rootOption) : findProjectRoot();
|
|
7401
8270
|
if (!found) {
|
|
7402
8271
|
if (!requireDesign) return designPaths(process.cwd());
|
|
7403
8272
|
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
@@ -7405,6 +8274,15 @@ function projectPaths(rootOption, requireDesign = true) {
|
|
|
7405
8274
|
const paths = designPaths(found);
|
|
7406
8275
|
return paths;
|
|
7407
8276
|
}
|
|
8277
|
+
function drawingMax(max, full) {
|
|
8278
|
+
if (full && max !== void 0) throw new CliError("Pass --max or --full, not both");
|
|
8279
|
+
if (full) return 0;
|
|
8280
|
+
if (max === void 0) return void 0;
|
|
8281
|
+
const value = Number(max);
|
|
8282
|
+
if (!Number.isInteger(value) || value < 200 || value > 8e3)
|
|
8283
|
+
throw new CliError("--max takes a number of pixels, 200 to 8000");
|
|
8284
|
+
return value;
|
|
8285
|
+
}
|
|
7408
8286
|
async function main(argv) {
|
|
7409
8287
|
const { values, positionals } = parseArgs({
|
|
7410
8288
|
args: argv,
|
|
@@ -7433,6 +8311,11 @@ async function main(argv) {
|
|
|
7433
8311
|
theme: { type: "string" },
|
|
7434
8312
|
out: { type: "string" },
|
|
7435
8313
|
overview: { type: "boolean" },
|
|
8314
|
+
board: { type: "boolean" },
|
|
8315
|
+
markup: { type: "boolean" },
|
|
8316
|
+
clear: { type: "boolean" },
|
|
8317
|
+
max: { type: "string" },
|
|
8318
|
+
full: { type: "boolean" },
|
|
7436
8319
|
tar: { type: "boolean" },
|
|
7437
8320
|
new: { type: "string" },
|
|
7438
8321
|
resolved: { type: "string", multiple: true },
|
|
@@ -7450,7 +8333,7 @@ async function main(argv) {
|
|
|
7450
8333
|
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
7451
8334
|
switch (command) {
|
|
7452
8335
|
case "init":
|
|
7453
|
-
return runInit(values.root ?
|
|
8336
|
+
return runInit(values.root ? path38.resolve(values.root) : process.cwd(), {
|
|
7454
8337
|
name: values.name,
|
|
7455
8338
|
gitignore: !values["no-gitignore"]
|
|
7456
8339
|
});
|
|
@@ -7508,9 +8391,14 @@ async function main(argv) {
|
|
|
7508
8391
|
page: values.page,
|
|
7509
8392
|
theme: theme2,
|
|
7510
8393
|
out: values.out,
|
|
7511
|
-
overview: !!values.overview
|
|
8394
|
+
overview: !!values.overview,
|
|
8395
|
+
board: !!values.board,
|
|
8396
|
+
markup: !!values.markup,
|
|
8397
|
+
max: drawingMax(values.max, !!values.full)
|
|
7512
8398
|
});
|
|
7513
8399
|
}
|
|
8400
|
+
case "drawings":
|
|
8401
|
+
return runDrawings(projectPaths(values.root), rest[0], { json: !!values.json, clear: !!values.clear });
|
|
7514
8402
|
case "build":
|
|
7515
8403
|
return runBuild(projectPaths(values.root), rest, { out: values.out, tar: !!values.tar });
|
|
7516
8404
|
case "login":
|
|
@@ -7557,12 +8445,12 @@ function relaunchFromRealPath() {
|
|
|
7557
8445
|
let realScript;
|
|
7558
8446
|
let realCwd;
|
|
7559
8447
|
try {
|
|
7560
|
-
realScript =
|
|
7561
|
-
realCwd =
|
|
8448
|
+
realScript = fs39.realpathSync.native(script);
|
|
8449
|
+
realCwd = fs39.realpathSync.native(process.cwd());
|
|
7562
8450
|
} catch {
|
|
7563
8451
|
return false;
|
|
7564
8452
|
}
|
|
7565
|
-
const same = (a, b) =>
|
|
8453
|
+
const same = (a, b) => path38.resolve(a).toLowerCase() === path38.resolve(b).toLowerCase();
|
|
7566
8454
|
if (same(realScript, script) && same(realCwd, process.cwd())) return false;
|
|
7567
8455
|
const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
|
|
7568
8456
|
stdio: "inherit",
|