foss-design 0.9.14 → 0.9.16
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 +863 -281
- package/dist/runtime/index.js +223 -16
- package/dist/runtime/page.js +1 -0
- package/dist/viewer/assets/{index-xErZjcMe.js → index-BBL77yhi.js} +6 -4
- package/dist/viewer/assets/index-DscYGSGL.css +2 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer-lib/index.js +94 -58
- package/dist/viewer-lib/styles.css +114 -7
- package/package.json +1 -1
- package/dist/viewer/assets/index-Dg5nDlgF.css +0 -2
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 fs40 from "fs";
|
|
6
|
+
import path39 from "path";
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
8
|
|
|
9
9
|
// src/core/paths.ts
|
|
@@ -23,9 +23,9 @@ function slugify(stem) {
|
|
|
23
23
|
return stem.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase() || "item";
|
|
24
24
|
}
|
|
25
25
|
function fileStem(file) {
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
return
|
|
26
|
+
const base2 = path.basename(file, path.extname(file));
|
|
27
|
+
if (base2 === "index") return path.basename(path.dirname(file));
|
|
28
|
+
return base2;
|
|
29
29
|
}
|
|
30
30
|
function toPosix(p) {
|
|
31
31
|
return p.split(path.sep).join("/");
|
|
@@ -115,6 +115,7 @@ var PKG = {
|
|
|
115
115
|
root: PKG_ROOT,
|
|
116
116
|
manifest: path2.join(PKG_ROOT, "package.json"),
|
|
117
117
|
runtime: path2.join(PKG_ROOT, "dist", "runtime", "index.js"),
|
|
118
|
+
pageRuntime: path2.join(PKG_ROOT, "dist", "runtime", "page.js"),
|
|
118
119
|
baseCss: path2.join(PKG_ROOT, "dist", "runtime", "base.css"),
|
|
119
120
|
viewer: path2.join(PKG_ROOT, "dist", "viewer"),
|
|
120
121
|
cli: path2.join(PKG_ROOT, "dist", "cli.js")
|
|
@@ -348,8 +349,8 @@ function routeError(pattern) {
|
|
|
348
349
|
}
|
|
349
350
|
return null;
|
|
350
351
|
}
|
|
351
|
-
function split(
|
|
352
|
-
return
|
|
352
|
+
function split(path40) {
|
|
353
|
+
return path40.split("/").filter(Boolean);
|
|
353
354
|
}
|
|
354
355
|
function compileRoute(pattern) {
|
|
355
356
|
return {
|
|
@@ -368,41 +369,41 @@ function decode2(part) {
|
|
|
368
369
|
return part;
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
|
-
function scoreRoute(route2,
|
|
372
|
-
const parts = split(
|
|
373
|
-
const
|
|
372
|
+
function scoreRoute(route2, path40) {
|
|
373
|
+
const parts = split(path40.split(/[?#]/)[0] ?? "").map(decode2);
|
|
374
|
+
const walk3 = (s, p) => {
|
|
374
375
|
if (s === route2.segments.length) return p === parts.length ? 0 : null;
|
|
375
376
|
const segment = route2.segments[s];
|
|
376
377
|
if (segment.kind === "splat") return 0;
|
|
377
378
|
if (segment.kind === "static") {
|
|
378
379
|
if (parts[p] !== segment.value) return null;
|
|
379
|
-
const rest =
|
|
380
|
+
const rest = walk3(s + 1, p + 1);
|
|
380
381
|
return rest === null ? null : rest + 4;
|
|
381
382
|
}
|
|
382
383
|
if (p < parts.length) {
|
|
383
|
-
const rest =
|
|
384
|
+
const rest = walk3(s + 1, p + 1);
|
|
384
385
|
if (rest !== null) return rest + 2;
|
|
385
386
|
}
|
|
386
|
-
return segment.optional ?
|
|
387
|
+
return segment.optional ? walk3(s + 1, p) : null;
|
|
387
388
|
};
|
|
388
|
-
return
|
|
389
|
+
return walk3(0, 0);
|
|
389
390
|
}
|
|
390
|
-
function matchRoute(entries,
|
|
391
|
+
function matchRoute(entries, path40) {
|
|
391
392
|
let best = null;
|
|
392
393
|
for (const entry of entries) {
|
|
393
394
|
for (const route2 of entry.routes) {
|
|
394
|
-
const score = scoreRoute(route2,
|
|
395
|
+
const score = scoreRoute(route2, path40);
|
|
395
396
|
if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
|
|
396
397
|
}
|
|
397
398
|
}
|
|
398
399
|
return best?.value ?? null;
|
|
399
400
|
}
|
|
400
|
-
function linkPath(href,
|
|
401
|
+
function linkPath(href, base2 = "/") {
|
|
401
402
|
const raw = href.trim();
|
|
402
403
|
if (!raw || raw.startsWith("#")) return null;
|
|
403
404
|
if (/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("//")) return null;
|
|
404
405
|
const origin = "http://app.invalid";
|
|
405
|
-
const url = new URL(raw, `${origin}${
|
|
406
|
+
const url = new URL(raw, `${origin}${base2.replace(/\/:[^/]+\??|\/\*$/g, "/x")}`);
|
|
406
407
|
return url.pathname + url.search + url.hash;
|
|
407
408
|
}
|
|
408
409
|
|
|
@@ -432,6 +433,11 @@ var DesignConfigSchema = z.strictObject({
|
|
|
432
433
|
app: z.string().min(1).optional(),
|
|
433
434
|
/** A folder served at the root of every screen, like Vite's `public`, relative to the project root. */
|
|
434
435
|
public: z.string().min(1).optional(),
|
|
436
|
+
/**
|
|
437
|
+
* Plugins screens get from the app's own Vite config, by name (`vite-plugin-svgr`): the config is
|
|
438
|
+
* the app's code, which this only picks from.
|
|
439
|
+
*/
|
|
440
|
+
vitePlugins: z.array(z.string().min(1)).optional(),
|
|
435
441
|
port: z.number().int().min(1).max(65535).optional()
|
|
436
442
|
});
|
|
437
443
|
var SystemConfigSchema = z.strictObject({
|
|
@@ -518,13 +524,13 @@ var CanvasSchema = z.strictObject({
|
|
|
518
524
|
pages: z.array(z.unknown()).min(1, "add at least one page")
|
|
519
525
|
});
|
|
520
526
|
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
521
|
-
function formatPath(
|
|
522
|
-
let out =
|
|
523
|
-
for (const part of
|
|
527
|
+
function formatPath(base2, path40) {
|
|
528
|
+
let out = base2;
|
|
529
|
+
for (const part of path40) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
|
|
524
530
|
return out;
|
|
525
531
|
}
|
|
526
|
-
function describeZodError(error,
|
|
527
|
-
return error.issues.map((issue) => ({ at: formatPath(
|
|
532
|
+
function describeZodError(error, base2 = "") {
|
|
533
|
+
return error.issues.map((issue) => ({ at: formatPath(base2, issue.path), message: issue.message }));
|
|
528
534
|
}
|
|
529
535
|
|
|
530
536
|
// src/core/canvas.ts
|
|
@@ -538,8 +544,8 @@ var routesOf = (route2) => route2 === void 0 ? void 0 : Array.isArray(route2) ?
|
|
|
538
544
|
function frameOf(input) {
|
|
539
545
|
const custom = input.width !== void 0 || input.height !== void 0;
|
|
540
546
|
const device = input.device ?? (custom ? void 0 : DEFAULT_DEVICE);
|
|
541
|
-
const
|
|
542
|
-
return { width: input.width ??
|
|
547
|
+
const base2 = DEVICES[input.device ?? DEFAULT_DEVICE];
|
|
548
|
+
return { width: input.width ?? base2.width, height: input.height ?? base2.height, device };
|
|
543
549
|
}
|
|
544
550
|
function loadCanvas(paths, canvasId, urls, snapshots) {
|
|
545
551
|
const dir = path3.join(paths.canvases, canvasId);
|
|
@@ -778,11 +784,11 @@ function resolveCover(cover, dir, screens, paths, urls, report) {
|
|
|
778
784
|
report("error", `cover "${cover}" does not exist`, "cover");
|
|
779
785
|
return void 0;
|
|
780
786
|
}
|
|
781
|
-
const { size:
|
|
782
|
-
if (
|
|
787
|
+
const { size: size3, mtimeMs } = fs3.statSync(file);
|
|
788
|
+
if (size3 > MAX_COVER_BYTES) {
|
|
783
789
|
report(
|
|
784
790
|
"error",
|
|
785
|
-
`cover "${cover}" is ${Math.ceil(
|
|
791
|
+
`cover "${cover}" is ${Math.ceil(size3 / 1024)} KB; the limit is ${MAX_COVER_BYTES / 1024} KB`,
|
|
786
792
|
"cover"
|
|
787
793
|
);
|
|
788
794
|
return void 0;
|
|
@@ -1128,17 +1134,17 @@ function collect(css2, from) {
|
|
|
1128
1134
|
function readStylesheets(entry) {
|
|
1129
1135
|
const seen = /* @__PURE__ */ new Set();
|
|
1130
1136
|
const files = [];
|
|
1131
|
-
const
|
|
1137
|
+
const walk3 = (file) => {
|
|
1132
1138
|
if (seen.has(file) || !fs6.existsSync(file)) return;
|
|
1133
1139
|
seen.add(file);
|
|
1134
1140
|
const css2 = fs6.readFileSync(file, "utf8");
|
|
1135
1141
|
const imports = [...css2.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/g)].map((m) => m[1]);
|
|
1136
1142
|
for (const spec of imports) {
|
|
1137
|
-
if (spec.startsWith("./") || spec.startsWith("../"))
|
|
1143
|
+
if (spec.startsWith("./") || spec.startsWith("../")) walk3(path6.resolve(path6.dirname(file), spec));
|
|
1138
1144
|
}
|
|
1139
1145
|
files.push({ file, css: css2 });
|
|
1140
1146
|
};
|
|
1141
|
-
|
|
1147
|
+
walk3(entry);
|
|
1142
1148
|
return files;
|
|
1143
1149
|
}
|
|
1144
1150
|
function closingParen(text2, start) {
|
|
@@ -1684,6 +1690,30 @@ var Entries = class {
|
|
|
1684
1690
|
entryFile(source) {
|
|
1685
1691
|
return path9.join(this.dir, source.canvas, `${source.id}.js`);
|
|
1686
1692
|
}
|
|
1693
|
+
/**
|
|
1694
|
+
* The app's own folder for a stylesheet of the app (system.json `stylesheet` outside `.design`):
|
|
1695
|
+
* design.json `app`, else the package the stylesheet belongs to. In the app's build Tailwind finds
|
|
1696
|
+
* the app's classes from the app's root; here the root is `.design`, so it is named as a source.
|
|
1697
|
+
*/
|
|
1698
|
+
appSourceDir() {
|
|
1699
|
+
const { paths } = this.project;
|
|
1700
|
+
const stylesheet = this.project.system()?.stylesheet;
|
|
1701
|
+
if (!stylesheet || isInside(paths.design, stylesheet)) return null;
|
|
1702
|
+
const app = this.project.appDir();
|
|
1703
|
+
if (app) return app;
|
|
1704
|
+
for (let dir = path9.dirname(stylesheet); isInside(paths.root, dir); dir = path9.dirname(dir)) {
|
|
1705
|
+
if (fs9.existsSync(path9.join(dir, "package.json"))) return dir;
|
|
1706
|
+
if (dir === paths.root) break;
|
|
1707
|
+
}
|
|
1708
|
+
return paths.root;
|
|
1709
|
+
}
|
|
1710
|
+
/** Every folder the system stylesheet has Tailwind scan: `.design`, the app's, design.json `sources`. */
|
|
1711
|
+
scannedDirs() {
|
|
1712
|
+
const { paths } = this.project;
|
|
1713
|
+
const extra = (this.project.config().config.sources ?? []).map((dir) => path9.resolve(paths.root, dir));
|
|
1714
|
+
const app = this.appSourceDir();
|
|
1715
|
+
return [paths.design, ...app ? [app] : [], ...extra];
|
|
1716
|
+
}
|
|
1687
1717
|
/** Tailwind with the design tokens, or the stylesheet system.json names; scans every file in `.design`. */
|
|
1688
1718
|
systemCss() {
|
|
1689
1719
|
const { paths } = this.project;
|
|
@@ -1692,6 +1722,8 @@ var Entries = class {
|
|
|
1692
1722
|
const lines = ["/* Generated by foss-design. Edit .design/system/tokens.css instead. */"];
|
|
1693
1723
|
if (system?.stylesheet) {
|
|
1694
1724
|
lines.push(`@import ${css(system.stylesheet)};`);
|
|
1725
|
+
const app = this.appSourceDir();
|
|
1726
|
+
if (app) lines.push(`@source ${css(app)};`);
|
|
1695
1727
|
} else {
|
|
1696
1728
|
const tailwind = resolveFrom(PKG.manifest, "tailwindcss/index.css");
|
|
1697
1729
|
if (!tailwind) throw new Error("tailwindcss is missing from foss-design; reinstall the package");
|
|
@@ -1790,7 +1822,7 @@ function headTags(head) {
|
|
|
1790
1822
|
public: head.public
|
|
1791
1823
|
};
|
|
1792
1824
|
const fallback = source.theme ?? head.theme;
|
|
1793
|
-
const theme2 = `(function(){var q=new URLSearchParams(location.search).get('theme');var t=${JSON.stringify(source.theme ?? null)}||(q==='dark'||q==='light'?q:${JSON.stringify(fallback)});var r=document.documentElement;r.dataset.theme=t;r.classList.toggle('dark',t==='dark');r.style.colorScheme=t})()`;
|
|
1825
|
+
const theme2 = `(function(){var q=new URLSearchParams(location.search).get('theme');var t=${JSON.stringify(source.theme ?? null)}||(q==='dark'||q==='light'?q:${JSON.stringify(fallback)});var r=document.documentElement;r.dataset.theme=r.dataset.mode=r.dataset.colorScheme=t;r.classList.toggle('dark',t==='dark');r.classList.toggle('theme-dark',t==='dark');r.style.colorScheme=t})()`;
|
|
1794
1826
|
const tags = [`<script>window.__DESIGN__=${scriptJson(config)};${theme2}</script>`, '<link rel="icon" href="data:,">'];
|
|
1795
1827
|
for (const href of head.fonts) tags.push(`<link rel="stylesheet" href="${escapeHtml(href)}">`);
|
|
1796
1828
|
if (head.systemCss) tags.push(`<link rel="stylesheet" href="${escapeHtml(head.systemCss)}">`);
|
|
@@ -1977,6 +2009,7 @@ import fs12 from "fs";
|
|
|
1977
2009
|
import path12 from "path";
|
|
1978
2010
|
import tailwindcss from "@tailwindcss/vite";
|
|
1979
2011
|
import react from "@vitejs/plugin-react";
|
|
2012
|
+
import { loadConfigFromFile } from "vite";
|
|
1980
2013
|
|
|
1981
2014
|
// src/server/css-guard.ts
|
|
1982
2015
|
import fs11 from "fs";
|
|
@@ -2012,9 +2045,9 @@ function targets(params) {
|
|
|
2012
2045
|
return [...out];
|
|
2013
2046
|
}
|
|
2014
2047
|
var isPathLike = (spec) => spec.startsWith(".") || path11.isAbsolute(spec);
|
|
2015
|
-
function existing(
|
|
2048
|
+
function existing(base2, exts) {
|
|
2016
2049
|
for (const ext of exts) {
|
|
2017
|
-
const file =
|
|
2050
|
+
const file = base2 + ext;
|
|
2018
2051
|
try {
|
|
2019
2052
|
if (fs11.statSync(file).isFile()) return fs11.realpathSync(file);
|
|
2020
2053
|
} catch {
|
|
@@ -2057,8 +2090,8 @@ function designCodeGuard(design) {
|
|
|
2057
2090
|
},
|
|
2058
2091
|
(file) => file.endsWith(".css")
|
|
2059
2092
|
);
|
|
2060
|
-
const
|
|
2061
|
-
return { style, script };
|
|
2093
|
+
const script2 = resolveWith({ ...config?.resolve }, (file) => !file.endsWith(".css"));
|
|
2094
|
+
return { style, script: script2 };
|
|
2062
2095
|
};
|
|
2063
2096
|
const candidates = async (spec, importer, resolve, exts) => {
|
|
2064
2097
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -2076,13 +2109,13 @@ function designCodeGuard(design) {
|
|
|
2076
2109
|
}
|
|
2077
2110
|
return [...found];
|
|
2078
2111
|
};
|
|
2079
|
-
const
|
|
2080
|
-
const { style, script } = resolvers(environment);
|
|
2112
|
+
const check2 = async (environment, file, code, seen) => {
|
|
2113
|
+
const { style, script: script2 } = resolvers(environment);
|
|
2081
2114
|
for (const match of code.matchAll(LOADS_CODE)) {
|
|
2082
2115
|
const directive = match[1] ? `@${match[1]}` : "use()";
|
|
2083
2116
|
const specs = match[1] ? targets(match[2] ?? "") : [match[4] ?? ""];
|
|
2084
2117
|
for (const spec of specs) {
|
|
2085
|
-
for (const target of await candidates(spec, file,
|
|
2118
|
+
for (const target of await candidates(spec, file, script2, JS_EXT)) {
|
|
2086
2119
|
if (!pulled(target)) continue;
|
|
2087
2120
|
throw new Error(
|
|
2088
2121
|
`${shown(file)}: \`${match[0].trim()}\` loads ${shown(target)}, which is refused. ${directive} runs its code in Node on this machine, and code under .design can come from a teammate through \`design pull\`. Put the plugin in the app\u2019s own source (outside .design) or name an installed package.`
|
|
@@ -2102,7 +2135,7 @@ function designCodeGuard(design) {
|
|
|
2102
2135
|
} catch {
|
|
2103
2136
|
continue;
|
|
2104
2137
|
}
|
|
2105
|
-
await
|
|
2138
|
+
await check2(environment, target, text2, seen);
|
|
2106
2139
|
}
|
|
2107
2140
|
}
|
|
2108
2141
|
}
|
|
@@ -2116,7 +2149,7 @@ function designCodeGuard(design) {
|
|
|
2116
2149
|
async transform(code, id2) {
|
|
2117
2150
|
if (!isStylesheetId(id2)) return null;
|
|
2118
2151
|
const file = path11.resolve(idFile(id2));
|
|
2119
|
-
await
|
|
2152
|
+
await check2(this.environment, file, code, /* @__PURE__ */ new Set([file]));
|
|
2120
2153
|
return null;
|
|
2121
2154
|
}
|
|
2122
2155
|
};
|
|
@@ -2140,6 +2173,41 @@ function aliases(project) {
|
|
|
2140
2173
|
}
|
|
2141
2174
|
return list;
|
|
2142
2175
|
}
|
|
2176
|
+
var INSTALL_FILES = [
|
|
2177
|
+
"package.json",
|
|
2178
|
+
"pnpm-lock.yaml",
|
|
2179
|
+
"package-lock.json",
|
|
2180
|
+
"npm-shrinkwrap.json",
|
|
2181
|
+
"yarn.lock",
|
|
2182
|
+
"bun.lock",
|
|
2183
|
+
"bun.lockb",
|
|
2184
|
+
".pnp.cjs",
|
|
2185
|
+
"node_modules/.pnpm/lock.yaml",
|
|
2186
|
+
"node_modules/.modules.yaml",
|
|
2187
|
+
"node_modules/.package-lock.json",
|
|
2188
|
+
"node_modules/.yarn-state.yml",
|
|
2189
|
+
"node_modules/.yarn-integrity"
|
|
2190
|
+
];
|
|
2191
|
+
function installStamp(paths, appDir) {
|
|
2192
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2193
|
+
const parts = [];
|
|
2194
|
+
for (const start of appDir ? [paths.root, appDir] : [paths.root]) {
|
|
2195
|
+
for (let dir = path12.resolve(start); ; dir = path12.dirname(dir)) {
|
|
2196
|
+
for (const name of INSTALL_FILES) {
|
|
2197
|
+
const file = path12.join(dir, name);
|
|
2198
|
+
if (seen.has(file)) continue;
|
|
2199
|
+
seen.add(file);
|
|
2200
|
+
try {
|
|
2201
|
+
const stat = fs12.statSync(file);
|
|
2202
|
+
parts.push(`${file}:${stat.size}:${stat.mtimeMs}`);
|
|
2203
|
+
} catch {
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (path12.dirname(dir) === dir) break;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return createHash3("sha256").update(parts.join("\n")).digest("hex").slice(0, 16);
|
|
2210
|
+
}
|
|
2143
2211
|
function dropStaleDepCache(paths, appDir = null) {
|
|
2144
2212
|
const dir = path12.join(paths.cache, "vite");
|
|
2145
2213
|
const stamp = path12.join(paths.cache, "vite-runtime");
|
|
@@ -2149,6 +2217,7 @@ function dropStaleDepCache(paths, appDir = null) {
|
|
|
2149
2217
|
} catch {
|
|
2150
2218
|
}
|
|
2151
2219
|
if (appDir) runtime += ` ${appDir}`;
|
|
2220
|
+
runtime += ` ${installStamp(paths, appDir)}`;
|
|
2152
2221
|
let seen = "";
|
|
2153
2222
|
try {
|
|
2154
2223
|
seen = fs12.readFileSync(stamp, "utf8");
|
|
@@ -2159,11 +2228,82 @@ function dropStaleDepCache(paths, appDir = null) {
|
|
|
2159
2228
|
fs12.mkdirSync(paths.cache, { recursive: true });
|
|
2160
2229
|
fs12.writeFileSync(stamp, runtime);
|
|
2161
2230
|
}
|
|
2162
|
-
|
|
2231
|
+
var VITE_CONFIGS = [
|
|
2232
|
+
"vite.config.ts",
|
|
2233
|
+
"vite.config.mts",
|
|
2234
|
+
"vite.config.js",
|
|
2235
|
+
"vite.config.mjs",
|
|
2236
|
+
"vite.config.cts",
|
|
2237
|
+
"vite.config.cjs"
|
|
2238
|
+
];
|
|
2239
|
+
var OWN_PLUGIN = /^(?:vite:react|@tailwindcss\/vite)/;
|
|
2240
|
+
function appViteConfig(project) {
|
|
2241
|
+
for (const dir of [project.appDir(), project.paths.root]) {
|
|
2242
|
+
if (!dir) continue;
|
|
2243
|
+
for (const name of VITE_CONFIGS) if (fs12.existsSync(path12.join(dir, name))) return path12.join(dir, name);
|
|
2244
|
+
}
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
async function flatten(list, out = []) {
|
|
2248
|
+
for (const option of list ?? []) {
|
|
2249
|
+
const value = await option;
|
|
2250
|
+
if (Array.isArray(value)) await flatten(value, out);
|
|
2251
|
+
else if (value && typeof value === "object" && "name" in value) out.push(value);
|
|
2252
|
+
}
|
|
2253
|
+
return out;
|
|
2254
|
+
}
|
|
2255
|
+
async function appPlugins(project, command) {
|
|
2256
|
+
const wanted = project.config().config.vitePlugins ?? [];
|
|
2257
|
+
if (!wanted.length) return { plugins: [], problems: [] };
|
|
2258
|
+
const file = appViteConfig(project);
|
|
2259
|
+
if (!file)
|
|
2260
|
+
return {
|
|
2261
|
+
plugins: [],
|
|
2262
|
+
problems: [
|
|
2263
|
+
`design.json vitePlugins: no vite.config.* in ${project.appDir() ? "the app package or " : ""}the project root to take them from`
|
|
2264
|
+
]
|
|
2265
|
+
};
|
|
2266
|
+
let all;
|
|
2267
|
+
try {
|
|
2268
|
+
const loaded = await loadConfigFromFile(
|
|
2269
|
+
{ command, mode: command === "serve" ? "development" : "production", isSsrBuild: false, isPreview: false },
|
|
2270
|
+
file,
|
|
2271
|
+
path12.dirname(file),
|
|
2272
|
+
"silent"
|
|
2273
|
+
);
|
|
2274
|
+
all = await flatten(loaded?.config.plugins);
|
|
2275
|
+
} catch (error) {
|
|
2276
|
+
return {
|
|
2277
|
+
plugins: [],
|
|
2278
|
+
problems: [
|
|
2279
|
+
`design.json vitePlugins: ${path12.relative(project.paths.root, file)} did not load: ${error.message}`
|
|
2280
|
+
]
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
const plugins = [];
|
|
2284
|
+
const problems = [];
|
|
2285
|
+
for (const name of wanted) {
|
|
2286
|
+
if (OWN_PLUGIN.test(name)) {
|
|
2287
|
+
problems.push(`design.json vitePlugins: "${name}" is foss-design's own (React and Tailwind are always on)`);
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
const found = all.filter((plugin) => plugin.name === name || plugin.name.startsWith(`${name}:`));
|
|
2291
|
+
if (found.length) plugins.push(...found);
|
|
2292
|
+
else
|
|
2293
|
+
problems.push(
|
|
2294
|
+
`design.json vitePlugins: no "${name}" in ${path12.relative(project.paths.root, file)}; it has ${all.map((plugin) => plugin.name).join(", ") || "no plugins"}`
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
return { plugins, problems };
|
|
2298
|
+
}
|
|
2299
|
+
function baseConfig(project, extra = []) {
|
|
2163
2300
|
const { paths } = project;
|
|
2164
2301
|
return {
|
|
2165
2302
|
configFile: false,
|
|
2166
|
-
|
|
2303
|
+
// The app's `.env`, `.env.local` and `.env.<mode>` files, as its own dev server and build read
|
|
2304
|
+
// them: screens get the same `import.meta.env.VITE_*` (a CDN host, a public API key). Only the
|
|
2305
|
+
// `VITE_` ones reach the browser, as in the app's build; a push bakes them into the cloud's copy.
|
|
2306
|
+
envDir: project.appDir() ?? paths.root,
|
|
2167
2307
|
root: paths.design,
|
|
2168
2308
|
cacheDir: path12.join(paths.cache, "vite"),
|
|
2169
2309
|
// design.json `public`: served under the frames' base, so HTML and CSS paths such as `/logo.png` resolve.
|
|
@@ -2180,7 +2320,7 @@ function baseConfig(project) {
|
|
|
2180
2320
|
css: { postcss: {} },
|
|
2181
2321
|
// Pulled code runs in the browser only: the guard refuses stylesheets that would load code from
|
|
2182
2322
|
// `.design` in Node (Tailwind's `@plugin` and `@config`), before Tailwind reads them.
|
|
2183
|
-
plugins: [react(), designCodeGuard(paths.design), tailwindcss()]
|
|
2323
|
+
plugins: [react(), designCodeGuard(paths.design), ...extra, tailwindcss()]
|
|
2184
2324
|
};
|
|
2185
2325
|
}
|
|
2186
2326
|
|
|
@@ -2319,12 +2459,15 @@ async function buildSite(paths, out, options = {}) {
|
|
|
2319
2459
|
fs13.mkdirSync(out, { recursive: true });
|
|
2320
2460
|
const moduleMap = {};
|
|
2321
2461
|
if (inputs.length) {
|
|
2322
|
-
const
|
|
2462
|
+
const fromApp = await appPlugins(project, "build");
|
|
2463
|
+
for (const problem of fromApp.problems) process.stderr.write(`warning: ${problem}
|
|
2464
|
+
`);
|
|
2465
|
+
const base2 = baseConfig(project, fromApp.plugins);
|
|
2323
2466
|
await build({
|
|
2324
|
-
...
|
|
2467
|
+
...base2,
|
|
2325
2468
|
plugins: [
|
|
2326
|
-
...
|
|
2327
|
-
|
|
2469
|
+
...base2.plugins ?? [],
|
|
2470
|
+
{
|
|
2328
2471
|
name: "design-module-map",
|
|
2329
2472
|
generateBundle(_, bundle) {
|
|
2330
2473
|
for (const [file, chunk2] of Object.entries(bundle)) {
|
|
@@ -2332,7 +2475,7 @@ async function buildSite(paths, out, options = {}) {
|
|
|
2332
2475
|
moduleMap[file] = (chunk2.moduleIds ?? []).map((id2) => id2.split("?")[0]).filter((id2) => path13.isAbsolute(id2) && !id2.includes(`${path13.sep}node_modules${path13.sep}`)).map((id2) => toPosix(path13.relative(paths.root, id2))).filter((rel) => !rel.startsWith(".."));
|
|
2333
2476
|
}
|
|
2334
2477
|
}
|
|
2335
|
-
}
|
|
2478
|
+
}
|
|
2336
2479
|
],
|
|
2337
2480
|
root: staging,
|
|
2338
2481
|
base: "./",
|
|
@@ -2418,7 +2561,8 @@ async function buildSite(paths, out, options = {}) {
|
|
|
2418
2561
|
fs13.copyFileSync(path13.join(publicDir, rel), to);
|
|
2419
2562
|
}
|
|
2420
2563
|
}
|
|
2421
|
-
|
|
2564
|
+
const modules = [...new Set(Object.values(moduleMap).flat())].sort();
|
|
2565
|
+
return { out, canvases: canvasIds, frames: inputs.length, errors, absoluteAssets, modules };
|
|
2422
2566
|
}
|
|
2423
2567
|
function copyViewer(viewerDir, out) {
|
|
2424
2568
|
copyTree(viewerDir, out, () => true);
|
|
@@ -2600,7 +2744,7 @@ function warnLink(rel) {
|
|
|
2600
2744
|
process.stderr.write(`warning: .design/${rel} links outside .design; it is not synced
|
|
2601
2745
|
`);
|
|
2602
2746
|
}
|
|
2603
|
-
function walk(dir,
|
|
2747
|
+
function walk(dir, base2, out) {
|
|
2604
2748
|
let entries;
|
|
2605
2749
|
try {
|
|
2606
2750
|
entries = fs15.readdirSync(dir, { withFileTypes: true });
|
|
@@ -2617,14 +2761,14 @@ function walk(dir, base, out) {
|
|
|
2617
2761
|
isFile = false;
|
|
2618
2762
|
try {
|
|
2619
2763
|
const target = fs15.realpathSync(full);
|
|
2620
|
-
const root = fs15.realpathSync(
|
|
2764
|
+
const root = fs15.realpathSync(base2);
|
|
2621
2765
|
if (target.startsWith(root + path15.sep)) isFile = fs15.statSync(target).isFile();
|
|
2622
|
-
else warnLink(toPosix(path15.relative(
|
|
2766
|
+
else warnLink(toPosix(path15.relative(base2, full)));
|
|
2623
2767
|
} catch {
|
|
2624
2768
|
}
|
|
2625
2769
|
}
|
|
2626
|
-
if (isDir) walk(full,
|
|
2627
|
-
else if (isFile) out[toPosix(path15.relative(
|
|
2770
|
+
if (isDir) walk(full, base2, out);
|
|
2771
|
+
else if (isFile) out[toPosix(path15.relative(base2, full))] = hashFile(full);
|
|
2628
2772
|
}
|
|
2629
2773
|
}
|
|
2630
2774
|
function unitManifest(paths, key) {
|
|
@@ -2759,6 +2903,7 @@ function parseProject(value, fallbackRole) {
|
|
|
2759
2903
|
id: id2,
|
|
2760
2904
|
name: text(data.name) ?? id2,
|
|
2761
2905
|
role: text(data.role) ?? fallbackRole,
|
|
2906
|
+
kind: data.kind === "pages" ? "pages" : "design",
|
|
2762
2907
|
archived: flag(data.archived, data.archivedAt, data.archived_at),
|
|
2763
2908
|
banned: flag(data.banned, data.bannedAt, data.banned_at),
|
|
2764
2909
|
icon: /[?&]v=([0-9a-f]{64})/.exec(text(data.icon) ?? "")?.[1] ?? null
|
|
@@ -2785,6 +2930,21 @@ function parseUnit(value) {
|
|
|
2785
2930
|
})
|
|
2786
2931
|
};
|
|
2787
2932
|
}
|
|
2933
|
+
function parsePage(value) {
|
|
2934
|
+
const data = record(value);
|
|
2935
|
+
const slug2 = str(data.slug);
|
|
2936
|
+
if (!slug2) return null;
|
|
2937
|
+
return {
|
|
2938
|
+
slug: slug2,
|
|
2939
|
+
title: text(data.title) ?? slug2,
|
|
2940
|
+
headRev: num(data.headRev) ?? 0,
|
|
2941
|
+
bytes: num(data.bytes) ?? 0,
|
|
2942
|
+
updatedAt: str(data.updatedAt) ?? "",
|
|
2943
|
+
archived: flag(data.archivedAt),
|
|
2944
|
+
banned: flag(data.bannedAt),
|
|
2945
|
+
publicId: str(data.publicId) ?? null
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2788
2948
|
function parseRevision(value) {
|
|
2789
2949
|
const data = record(value);
|
|
2790
2950
|
const rev = num(data.rev);
|
|
@@ -2802,6 +2962,21 @@ function parseRevision(value) {
|
|
|
2802
2962
|
rollbackAllowed: data.rollbackAllowed === true
|
|
2803
2963
|
};
|
|
2804
2964
|
}
|
|
2965
|
+
var NOT_CONNECTED = /* @__PURE__ */ new Set([
|
|
2966
|
+
"ETIMEDOUT",
|
|
2967
|
+
"ECONNREFUSED",
|
|
2968
|
+
"ENOTFOUND",
|
|
2969
|
+
"EAI_AGAIN",
|
|
2970
|
+
"ENETUNREACH",
|
|
2971
|
+
"EHOSTUNREACH",
|
|
2972
|
+
"ENETDOWN",
|
|
2973
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
2974
|
+
]);
|
|
2975
|
+
var CONNECT_RETRIES = 3;
|
|
2976
|
+
function networkCode(error) {
|
|
2977
|
+
const cause = error.cause;
|
|
2978
|
+
return cause?.code ?? cause?.errors?.find((inner) => inner.code)?.code;
|
|
2979
|
+
}
|
|
2805
2980
|
async function withRetries(fn, retries = 3) {
|
|
2806
2981
|
for (let attempt = 0; ; attempt++) {
|
|
2807
2982
|
try {
|
|
@@ -2821,24 +2996,34 @@ var CloudClient = class {
|
|
|
2821
2996
|
}
|
|
2822
2997
|
host;
|
|
2823
2998
|
token;
|
|
2824
|
-
async send(method,
|
|
2999
|
+
async send(method, path40, init) {
|
|
2825
3000
|
const headers = {
|
|
2826
3001
|
"User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
|
|
2827
3002
|
Accept: "application/json"
|
|
2828
3003
|
};
|
|
2829
3004
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
2830
3005
|
if (init.type) headers["Content-Type"] = init.type;
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
3006
|
+
for (let attempt = 0; ; attempt++) {
|
|
3007
|
+
try {
|
|
3008
|
+
return await fetch(`${this.host}/api${path40}`, {
|
|
3009
|
+
method,
|
|
3010
|
+
headers,
|
|
3011
|
+
body: init.body,
|
|
3012
|
+
signal: AbortSignal.timeout(init.timeout)
|
|
3013
|
+
});
|
|
3014
|
+
} catch (error) {
|
|
3015
|
+
const timedOut = error.name === "TimeoutError";
|
|
3016
|
+
const code = networkCode(error);
|
|
3017
|
+
const again = !timedOut && (NOT_CONNECTED.has(code ?? "") || method === "GET" || method === "HEAD");
|
|
3018
|
+
if (again && attempt < CONNECT_RETRIES) {
|
|
3019
|
+
await sleep(1e3 * 2 ** attempt);
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
const cause = error.cause;
|
|
3023
|
+
const reason = timedOut ? "timed out" : code ?? cause?.message ?? error.message;
|
|
3024
|
+
const tries = attempt ? ` (tried ${attempt + 1} times)` : "";
|
|
3025
|
+
throw new CloudError(`Could not reach ${this.host}: ${reason}${tries}`, 0, "network");
|
|
3026
|
+
}
|
|
2842
3027
|
}
|
|
2843
3028
|
}
|
|
2844
3029
|
async fail(response) {
|
|
@@ -2850,8 +3035,8 @@ var CloudClient = class {
|
|
|
2850
3035
|
throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
|
|
2851
3036
|
}
|
|
2852
3037
|
/** JSON in, JSON out; any non-2xx answer throws a CloudError. */
|
|
2853
|
-
async request(method,
|
|
2854
|
-
const response = await this.send(method,
|
|
3038
|
+
async request(method, path40, body) {
|
|
3039
|
+
const response = await this.send(method, path40, {
|
|
2855
3040
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
2856
3041
|
type: body === void 0 ? void 0 : "application/json",
|
|
2857
3042
|
timeout: JSON_TIMEOUT
|
|
@@ -2863,7 +3048,7 @@ var CloudClient = class {
|
|
|
2863
3048
|
try {
|
|
2864
3049
|
return JSON.parse(text2);
|
|
2865
3050
|
} catch {
|
|
2866
|
-
throw new CloudError(`${this.host} answered ${method} ${
|
|
3051
|
+
throw new CloudError(`${this.host} answered ${method} ${path40} with something other than JSON`, 0, "bad_response");
|
|
2867
3052
|
}
|
|
2868
3053
|
}
|
|
2869
3054
|
/** POST /device/code. */
|
|
@@ -2898,8 +3083,8 @@ var CloudClient = class {
|
|
|
2898
3083
|
const list = (value, role) => (Array.isArray(value) ? value : []).map((item) => parseProject(item, role)).filter((item) => item !== null);
|
|
2899
3084
|
return { owned: list(body.owned, "owner"), shared: list(body.shared, "member") };
|
|
2900
3085
|
}
|
|
2901
|
-
async createProject(name) {
|
|
2902
|
-
const body = record(await this.request("POST", "/projects", { name }));
|
|
3086
|
+
async createProject(name, kind = "design") {
|
|
3087
|
+
const body = record(await this.request("POST", "/projects", kind === "design" ? { name } : { name, kind }));
|
|
2903
3088
|
const project = parseProject(body.project ?? body, "owner");
|
|
2904
3089
|
if (!project) throw new CloudError(`${this.host} did not return the new project`, 0, "bad_response");
|
|
2905
3090
|
return project;
|
|
@@ -2981,6 +3166,21 @@ var CloudClient = class {
|
|
|
2981
3166
|
);
|
|
2982
3167
|
return { publicId: str(body.publicId) ?? null };
|
|
2983
3168
|
}
|
|
3169
|
+
/** The pages of a pages project, last changed first. */
|
|
3170
|
+
async pages(projectId) {
|
|
3171
|
+
const body = record(await this.request("GET", `/projects/${encodeURIComponent(projectId)}/pages`));
|
|
3172
|
+
return (Array.isArray(body.pages) ? body.pages : []).map(parsePage).filter((page) => !!page);
|
|
3173
|
+
}
|
|
3174
|
+
/** Publish a page (its link stays across versions) or turn its link off (owner only). */
|
|
3175
|
+
async pageAction(projectId, slug2, action) {
|
|
3176
|
+
const body = record(
|
|
3177
|
+
await this.request(
|
|
3178
|
+
"POST",
|
|
3179
|
+
`/projects/${encodeURIComponent(projectId)}/pages/${encodeURIComponent(slug2)}/${action}`
|
|
3180
|
+
)
|
|
3181
|
+
);
|
|
3182
|
+
return { publicId: str(body.publicId) ?? null };
|
|
3183
|
+
}
|
|
2984
3184
|
/** POST /blobs/missing, in batches: the hashes the cloud does not have yet. */
|
|
2985
3185
|
async missing(projectId, hashes2) {
|
|
2986
3186
|
const missing = [];
|
|
@@ -3054,8 +3254,8 @@ function cloudHost() {
|
|
|
3054
3254
|
return normalizeHost(process.env.FOSS_DESIGN_CLOUD || DEFAULT_CLOUD);
|
|
3055
3255
|
}
|
|
3056
3256
|
function credentialsFile() {
|
|
3057
|
-
const
|
|
3058
|
-
return path16.join(
|
|
3257
|
+
const base2 = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path16.join(os2.homedir(), ".config");
|
|
3258
|
+
return path16.join(base2, "foss-design", "credentials.json");
|
|
3059
3259
|
}
|
|
3060
3260
|
function readAll() {
|
|
3061
3261
|
const file = credentialsFile();
|
|
@@ -3114,9 +3314,10 @@ function readLink(paths) {
|
|
|
3114
3314
|
if (typeof data?.host !== "string" || typeof data.project !== "string")
|
|
3115
3315
|
throw new CliError(`${file} has no host or project; run \`design link <project>\` again`);
|
|
3116
3316
|
const units = {};
|
|
3117
|
-
for (const [key,
|
|
3118
|
-
if (!isUnitKey(key) || typeof
|
|
3119
|
-
units[key] = { rev:
|
|
3317
|
+
for (const [key, base2] of Object.entries(data.units ?? {})) {
|
|
3318
|
+
if (!isUnitKey(key) || typeof base2?.rev !== "number") continue;
|
|
3319
|
+
units[key] = { rev: base2.rev, files: base2.files && typeof base2.files === "object" ? base2.files : {} };
|
|
3320
|
+
if (base2.deps && typeof base2.deps === "object") units[key].deps = base2.deps;
|
|
3120
3321
|
}
|
|
3121
3322
|
const conflicts = {};
|
|
3122
3323
|
for (const [key, rev] of Object.entries(data.conflicts ?? {})) if (typeof rev === "number") conflicts[key] = rev;
|
|
@@ -3156,23 +3357,24 @@ function diffFiles(a, b) {
|
|
|
3156
3357
|
for (const file of /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])) if (a[file] !== b[file]) out.push(file);
|
|
3157
3358
|
return out.sort();
|
|
3158
3359
|
}
|
|
3159
|
-
function unitStatus(key, link, local, remote) {
|
|
3160
|
-
const
|
|
3161
|
-
const baseRev =
|
|
3360
|
+
function unitStatus(key, link, local, remote, design) {
|
|
3361
|
+
const base2 = link.units[key];
|
|
3362
|
+
const baseRev = base2?.rev ?? 0;
|
|
3162
3363
|
const localFiles = hashes(local ?? {});
|
|
3163
3364
|
const remoteUnit = remote?.get(key);
|
|
3164
3365
|
const remoteFiles = remoteUnit ? hashes(remoteUnit.manifest) : {};
|
|
3165
3366
|
const deletedRemotely = !!remote && !remoteUnit && baseRev > 0;
|
|
3166
3367
|
const ahead = remote ? remoteUnit ? remoteUnit.headRev > baseRev : baseRev > 0 : false;
|
|
3167
|
-
let changed = diffFiles(localFiles,
|
|
3368
|
+
let changed = diffFiles(localFiles, base2?.files ?? {});
|
|
3168
3369
|
if (ahead) changed = changed.filter((file) => localFiles[file] !== remoteFiles[file]);
|
|
3169
|
-
const remoteChanged = ahead ? diffFiles(remoteFiles,
|
|
3370
|
+
const remoteChanged = ahead ? diffFiles(remoteFiles, base2?.files ?? {}) : [];
|
|
3371
|
+
const usesChanged = design ? changedDeps(design, base2?.deps) : [];
|
|
3170
3372
|
const conflictRev = link.conflicts[key];
|
|
3171
3373
|
let state;
|
|
3172
3374
|
if (remoteUnit?.archived) state = "archived";
|
|
3173
3375
|
else if (conflictRev !== void 0 || changed.length && remoteChanged.length && !deletedRemotely) state = "conflict";
|
|
3174
3376
|
else if (ahead) state = "remote_ahead";
|
|
3175
|
-
else if (changed.length) state = "local_changes";
|
|
3377
|
+
else if (changed.length || usesChanged.length) state = "local_changes";
|
|
3176
3378
|
else state = "in_sync";
|
|
3177
3379
|
return {
|
|
3178
3380
|
key,
|
|
@@ -3181,12 +3383,25 @@ function unitStatus(key, link, local, remote) {
|
|
|
3181
3383
|
headRev: remoteUnit ? remoteUnit.headRev : null,
|
|
3182
3384
|
local: Object.keys(localFiles).length > 0,
|
|
3183
3385
|
changed,
|
|
3386
|
+
usesChanged,
|
|
3184
3387
|
ahead,
|
|
3185
3388
|
remoteChanged,
|
|
3186
3389
|
deletedRemotely,
|
|
3187
3390
|
conflictRev
|
|
3188
3391
|
};
|
|
3189
3392
|
}
|
|
3393
|
+
function changedDeps(design, deps) {
|
|
3394
|
+
const out = [];
|
|
3395
|
+
for (const [rel, hash] of Object.entries(deps ?? {})) {
|
|
3396
|
+
let now = null;
|
|
3397
|
+
try {
|
|
3398
|
+
now = hashBuffer(fs17.readFileSync(path17.join(design, rel)));
|
|
3399
|
+
} catch {
|
|
3400
|
+
}
|
|
3401
|
+
if (now !== hash) out.push(rel);
|
|
3402
|
+
}
|
|
3403
|
+
return out.sort();
|
|
3404
|
+
}
|
|
3190
3405
|
function allUnitKeys(link, local, remote) {
|
|
3191
3406
|
const keys = /* @__PURE__ */ new Set([...local.keys(), ...Object.keys(link.units), ...Object.keys(link.conflicts)]);
|
|
3192
3407
|
for (const key of remote?.keys() ?? []) keys.add(key);
|
|
@@ -3207,10 +3422,10 @@ function rate(bytes, started) {
|
|
|
3207
3422
|
}
|
|
3208
3423
|
var seconds = (started) => `${((Date.now() - started) / 1e3).toFixed(1)}s`;
|
|
3209
3424
|
function webUrl(link, unit) {
|
|
3210
|
-
const
|
|
3211
|
-
if (!unit) return
|
|
3212
|
-
if (unit === SYSTEM_UNIT) return `${
|
|
3213
|
-
return `${
|
|
3425
|
+
const base2 = `${link.host.replace(/\/+$/, "")}/p/${encodeURIComponent(link.project)}`;
|
|
3426
|
+
if (!unit) return base2;
|
|
3427
|
+
if (unit === SYSTEM_UNIT) return `${base2}/system`;
|
|
3428
|
+
return `${base2}/c/${encodeURIComponent(unit.slice("canvas/".length))}`;
|
|
3214
3429
|
}
|
|
3215
3430
|
function newReport(command, link) {
|
|
3216
3431
|
return { ok: true, command, host: link.host, project: link.project, url: webUrl(link), units: [], hints: [] };
|
|
@@ -3513,10 +3728,10 @@ var AUTO_VIEWPORT_HEIGHT = 900;
|
|
|
3513
3728
|
function withTheme(url, theme2) {
|
|
3514
3729
|
return `${url}${url.includes("?") ? "&" : "?"}theme=${theme2}`;
|
|
3515
3730
|
}
|
|
3516
|
-
function absolute(
|
|
3517
|
-
return /^https?:\/\//.test(url) ? url : `${
|
|
3731
|
+
function absolute(base2, url) {
|
|
3732
|
+
return /^https?:\/\//.test(url) ? url : `${base2.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
3518
3733
|
}
|
|
3519
|
-
function frameJob(
|
|
3734
|
+
function frameJob(base2, canvas, page, item, theme2) {
|
|
3520
3735
|
const auto = item.frame.height === "auto";
|
|
3521
3736
|
const pinned = item.kind === "screen" ? item.theme : void 0;
|
|
3522
3737
|
const effective = pinned ?? theme2;
|
|
@@ -3525,7 +3740,7 @@ function frameJob(base, canvas, page, item, theme2) {
|
|
|
3525
3740
|
canvas,
|
|
3526
3741
|
id: item.id,
|
|
3527
3742
|
page,
|
|
3528
|
-
url: item.kind === "url" ? item.url : withTheme(absolute(
|
|
3743
|
+
url: item.kind === "url" ? item.url : withTheme(absolute(base2, item.url), effective),
|
|
3529
3744
|
width: item.frame.width,
|
|
3530
3745
|
height: auto ? AUTO_VIEWPORT_HEIGHT : item.frame.height,
|
|
3531
3746
|
fullPage: auto,
|
|
@@ -3536,7 +3751,7 @@ function frameJob(base, canvas, page, item, theme2) {
|
|
|
3536
3751
|
function isFrameItem(item) {
|
|
3537
3752
|
return item.kind === "screen" || item.kind === "url";
|
|
3538
3753
|
}
|
|
3539
|
-
function canvasFrames(doc,
|
|
3754
|
+
function canvasFrames(doc, base2, theme2, filter = {}) {
|
|
3540
3755
|
const jobs = [];
|
|
3541
3756
|
for (const page of doc.pages) {
|
|
3542
3757
|
if (filter.page && page.id !== filter.page) continue;
|
|
@@ -3546,20 +3761,20 @@ function canvasFrames(doc, base, theme2, filter = {}) {
|
|
|
3546
3761
|
if (filter.id && item.id !== filter.id) continue;
|
|
3547
3762
|
if (item.kind === "url" && filter.includeUrls === false) continue;
|
|
3548
3763
|
if (item.kind === "screen" && item.missing && filter.skipMissing !== false) continue;
|
|
3549
|
-
jobs.push(frameJob(
|
|
3764
|
+
jobs.push(frameJob(base2, doc.id, page.id, item, theme2));
|
|
3550
3765
|
}
|
|
3551
3766
|
}
|
|
3552
3767
|
}
|
|
3553
3768
|
return jobs;
|
|
3554
3769
|
}
|
|
3555
|
-
function systemFrames(doc,
|
|
3770
|
+
function systemFrames(doc, base2, theme2) {
|
|
3556
3771
|
const specimens = doc.components.map((component) => ({ id: component.id, url: component.url }));
|
|
3557
3772
|
if (doc.typographyUrl) specimens.push({ id: "@typography", url: doc.typographyUrl });
|
|
3558
3773
|
return specimens.map(({ id: id2, url }) => ({
|
|
3559
3774
|
key: `@system/${id2}`,
|
|
3560
3775
|
canvas: "@system",
|
|
3561
3776
|
id: id2,
|
|
3562
|
-
url: withTheme(absolute(
|
|
3777
|
+
url: withTheme(absolute(base2, url), theme2),
|
|
3563
3778
|
width: 1200,
|
|
3564
3779
|
height: 800,
|
|
3565
3780
|
fullPage: true,
|
|
@@ -3788,7 +4003,7 @@ function keep(paths, key, rel, side, data) {
|
|
|
3788
4003
|
fs23.mkdirSync(path22.dirname(file), { recursive: true });
|
|
3789
4004
|
fs23.writeFileSync(file, data);
|
|
3790
4005
|
}
|
|
3791
|
-
function mergeUnit(paths, key, headRev,
|
|
4006
|
+
function mergeUnit(paths, key, headRev, base2, here, cloud2, blob) {
|
|
3792
4007
|
const files = { added: 0, changed: 0, removed: 0, merged: 0 };
|
|
3793
4008
|
const conflicts = [];
|
|
3794
4009
|
const stop = key === SYSTEM_UNIT ? paths.design : path22.dirname(unitRoot(paths, key));
|
|
@@ -3799,8 +4014,8 @@ function mergeUnit(paths, key, headRev, base, here, cloud2, blob) {
|
|
|
3799
4014
|
fs23.rmSync(file, { force: true });
|
|
3800
4015
|
pruneEmpty(path22.dirname(file), stop);
|
|
3801
4016
|
};
|
|
3802
|
-
for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(
|
|
3803
|
-
const b =
|
|
4017
|
+
for (const rel of [.../* @__PURE__ */ new Set([...Object.keys(base2), ...Object.keys(here), ...Object.keys(cloud2)])].sort()) {
|
|
4018
|
+
const b = base2[rel];
|
|
3804
4019
|
const h = here[rel]?.hash;
|
|
3805
4020
|
const c = cloud2[rel]?.hash;
|
|
3806
4021
|
if (h === c || c === b) continue;
|
|
@@ -3876,8 +4091,8 @@ function finishMerge(paths, link, key, accept = false) {
|
|
|
3876
4091
|
// src/cloud/meta.ts
|
|
3877
4092
|
import fs24 from "fs";
|
|
3878
4093
|
import path23 from "path";
|
|
3879
|
-
async function syncProjectMeta(paths,
|
|
3880
|
-
const remote = await
|
|
4094
|
+
async function syncProjectMeta(paths, client2, link) {
|
|
4095
|
+
const remote = await client2.project(link.project);
|
|
3881
4096
|
if (!remote) return [];
|
|
3882
4097
|
const lines = [];
|
|
3883
4098
|
link.name = remote.name;
|
|
@@ -3888,17 +4103,17 @@ async function syncProjectMeta(paths, client, link) {
|
|
|
3888
4103
|
link.icon = cloud2;
|
|
3889
4104
|
return lines;
|
|
3890
4105
|
}
|
|
3891
|
-
const
|
|
3892
|
-
const localChanged =
|
|
3893
|
-
const cloudChanged =
|
|
4106
|
+
const base2 = link.icon;
|
|
4107
|
+
const localChanged = base2 === void 0 ? cloud2 === null : local !== base2;
|
|
4108
|
+
const cloudChanged = base2 === void 0 ? local === null : cloud2 !== base2;
|
|
3894
4109
|
if (localChanged && !cloudChanged) {
|
|
3895
4110
|
try {
|
|
3896
4111
|
if (file) {
|
|
3897
4112
|
const ext = path23.extname(file).slice(1);
|
|
3898
|
-
await
|
|
4113
|
+
await client2.setProjectIcon(link.project, { data: fs24.readFileSync(file), type: ICON_TYPES[ext] });
|
|
3899
4114
|
lines.push("The project icon went up to the cloud.");
|
|
3900
4115
|
} else {
|
|
3901
|
-
await
|
|
4116
|
+
await client2.setProjectIcon(link.project, null);
|
|
3902
4117
|
lines.push("The project icon was removed in the cloud too.");
|
|
3903
4118
|
}
|
|
3904
4119
|
link.icon = local;
|
|
@@ -3908,7 +4123,7 @@ async function syncProjectMeta(paths, client, link) {
|
|
|
3908
4123
|
}
|
|
3909
4124
|
return lines;
|
|
3910
4125
|
}
|
|
3911
|
-
const icon = cloud2 ? await
|
|
4126
|
+
const icon = cloud2 ? await client2.projectIcon(link.project) : null;
|
|
3912
4127
|
if (icon && !sniffIcon(icon.data))
|
|
3913
4128
|
return [...lines, "The cloud sent a project icon that is no image; it was left out."];
|
|
3914
4129
|
writeIcon(paths, icon?.data ?? null);
|
|
@@ -3947,14 +4162,14 @@ function applyUnit(paths, key, local, target, blobs) {
|
|
|
3947
4162
|
return counts;
|
|
3948
4163
|
}
|
|
3949
4164
|
async function pull(ctx, options) {
|
|
3950
|
-
const { paths, client } = ctx;
|
|
4165
|
+
const { paths, client: client2 } = ctx;
|
|
3951
4166
|
const link = structuredClone(ctx.link);
|
|
3952
4167
|
const report = newReport("pull", link);
|
|
3953
4168
|
for (const line of await syncProjectMeta(ctx.paths, ctx.client, link)) report.hints.push(line);
|
|
3954
4169
|
const selected = unique(options.units.map(unitArg));
|
|
3955
4170
|
const theirs = unique(options.theirs.map(unitArg));
|
|
3956
4171
|
const scan = scanLocal(paths);
|
|
3957
|
-
const remote = await
|
|
4172
|
+
const remote = await client2.units(link.project);
|
|
3958
4173
|
const local = (key) => scan.units.get(key)?.manifest ?? {};
|
|
3959
4174
|
const keys = selected.length || theirs.length ? unique([...selected, ...theirs]) : allUnitKeys(link, /* @__PURE__ */ new Map(), remote);
|
|
3960
4175
|
const clashes = caseClashes([...remote.keys(), ...scan.units.keys(), ...Object.keys(link.units)]);
|
|
@@ -4042,9 +4257,9 @@ async function pull(ctx, options) {
|
|
|
4042
4257
|
checkManifest(plan.key, plan.remote.manifest);
|
|
4043
4258
|
for (const entry of Object.values(plan.remote.manifest)) needed.add(entry.hash);
|
|
4044
4259
|
if (plan.kind !== "merge") continue;
|
|
4045
|
-
const
|
|
4260
|
+
const base2 = link.units[plan.key]?.files ?? {};
|
|
4046
4261
|
const here = local(plan.key);
|
|
4047
|
-
for (const [rel, hash] of Object.entries(
|
|
4262
|
+
for (const [rel, hash] of Object.entries(base2)) {
|
|
4048
4263
|
const h = here[rel]?.hash;
|
|
4049
4264
|
const c = plan.remote.manifest[rel]?.hash;
|
|
4050
4265
|
if (h && c && h !== hash && c !== hash && h !== c) bases.add(hash);
|
|
@@ -4066,7 +4281,7 @@ async function pull(ctx, options) {
|
|
|
4066
4281
|
const line = () => `Downloading ${gotFiles}/${download.length} files \xB7 ${megabytes(gotBytes)}${rate(gotBytes, started)}`;
|
|
4067
4282
|
bar.update(line());
|
|
4068
4283
|
await mapLimit(download, TRANSFERS, async (hash) => {
|
|
4069
|
-
const data = await
|
|
4284
|
+
const data = await client2.getBlob(link.project, hash);
|
|
4070
4285
|
blobs.set(hash, data);
|
|
4071
4286
|
gotFiles++;
|
|
4072
4287
|
gotBytes += data.length;
|
|
@@ -4079,7 +4294,7 @@ async function pull(ctx, options) {
|
|
|
4079
4294
|
TRANSFERS,
|
|
4080
4295
|
async (hash) => {
|
|
4081
4296
|
try {
|
|
4082
|
-
blobs.set(hash, await
|
|
4297
|
+
blobs.set(hash, await client2.getBlob(link.project, hash));
|
|
4083
4298
|
} catch {
|
|
4084
4299
|
}
|
|
4085
4300
|
}
|
|
@@ -4144,14 +4359,14 @@ function blockedReport(status, message) {
|
|
|
4144
4359
|
};
|
|
4145
4360
|
}
|
|
4146
4361
|
async function push(ctx, options) {
|
|
4147
|
-
const { paths, client, log } = ctx;
|
|
4362
|
+
const { paths, client: client2, log } = ctx;
|
|
4148
4363
|
const link = structuredClone(ctx.link);
|
|
4149
4364
|
const report = newReport("push", link);
|
|
4150
4365
|
for (const line of await syncProjectMeta(ctx.paths, ctx.client, link)) report.hints.push(line);
|
|
4151
4366
|
const selected = unique(options.units.map(unitArg));
|
|
4152
4367
|
const resolved = unique(options.resolved.map(unitArg));
|
|
4153
4368
|
const scan = scanLocal(paths);
|
|
4154
|
-
const remote = await
|
|
4369
|
+
const remote = await client2.units(link.project);
|
|
4155
4370
|
const clashes = caseClashes([...scan.units.keys(), ...remote.keys()]);
|
|
4156
4371
|
if (clashes.size) {
|
|
4157
4372
|
const pairs = [...clashes].filter(([a, b]) => a < b).map(([a, b]) => `${a} / ${b}`);
|
|
@@ -4175,7 +4390,7 @@ async function push(ctx, options) {
|
|
|
4175
4390
|
link.units[key] = { rev: head, files: link.units[key]?.files ?? {} };
|
|
4176
4391
|
delete link.conflicts[key];
|
|
4177
4392
|
}
|
|
4178
|
-
const status = (key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote);
|
|
4393
|
+
const status = (key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote, paths.design);
|
|
4179
4394
|
for (const key of selected) {
|
|
4180
4395
|
if (!scan.units.has(key) && !remote.has(key) && !link.units[key]) {
|
|
4181
4396
|
const known = [...scan.units.keys()].join(", ") || "none";
|
|
@@ -4194,7 +4409,7 @@ async function push(ctx, options) {
|
|
|
4194
4409
|
candidates = [...resolved];
|
|
4195
4410
|
for (const key of scan.units.keys()) {
|
|
4196
4411
|
const s = status(key);
|
|
4197
|
-
if (s.changed.length || s.conflictRev !== void 0) candidates.push(key);
|
|
4412
|
+
if (s.changed.length || s.usesChanged.length || s.conflictRev !== void 0) candidates.push(key);
|
|
4198
4413
|
}
|
|
4199
4414
|
candidates = unique(candidates);
|
|
4200
4415
|
}
|
|
@@ -4285,7 +4500,8 @@ async function push(ctx, options) {
|
|
|
4285
4500
|
}
|
|
4286
4501
|
const pushing = toPush.filter((key) => {
|
|
4287
4502
|
const s = status(key);
|
|
4288
|
-
if (systemPush || resolved.includes(key) || s.changed.length || !remote.has(key))
|
|
4503
|
+
if (systemPush || resolved.includes(key) || s.changed.length || s.usesChanged.length || !remote.has(key))
|
|
4504
|
+
return true;
|
|
4289
4505
|
report.units.push({ unit: key, action: "unchanged", rev: s.baseRev, url: webUrl(link, key) });
|
|
4290
4506
|
return false;
|
|
4291
4507
|
});
|
|
@@ -4295,6 +4511,7 @@ async function push(ctx, options) {
|
|
|
4295
4511
|
const project = new DesignProject(paths, STATIC_URLS);
|
|
4296
4512
|
const systemRev = remote.get(SYSTEM_UNIT)?.headRev ?? 0;
|
|
4297
4513
|
const units = [];
|
|
4514
|
+
const deps = /* @__PURE__ */ new Map();
|
|
4298
4515
|
const files = /* @__PURE__ */ new Map();
|
|
4299
4516
|
const addFiles = (manifest, dir) => {
|
|
4300
4517
|
for (const [rel, entry] of Object.entries(manifest))
|
|
@@ -4315,6 +4532,12 @@ async function push(ctx, options) {
|
|
|
4315
4532
|
);
|
|
4316
4533
|
const source = scan.units.get(key).manifest;
|
|
4317
4534
|
const build2 = buildManifest(unitBuildDir(paths, key));
|
|
4535
|
+
deps.set(key, borrowedFiles(paths, key, result.modules));
|
|
4536
|
+
const uses = status(key).usesChanged;
|
|
4537
|
+
if (uses.length && !status(key).changed.length)
|
|
4538
|
+
report.hints.push(
|
|
4539
|
+
`${key} is pushed for files it uses from outside its folder: ${uses.slice(0, 3).join(", ")}${uses.length > 3 ? ` and ${uses.length - 3} more` : ""}`
|
|
4540
|
+
);
|
|
4318
4541
|
addFiles(source, paths.design);
|
|
4319
4542
|
addFiles(build2, unitBuildDir(paths, key));
|
|
4320
4543
|
units.push({
|
|
@@ -4326,7 +4549,7 @@ async function push(ctx, options) {
|
|
|
4326
4549
|
systemRev: id2 === null ? null : systemRev
|
|
4327
4550
|
});
|
|
4328
4551
|
}
|
|
4329
|
-
const missing = await
|
|
4552
|
+
const missing = await client2.missing(link.project, [...files.keys()]);
|
|
4330
4553
|
if (missing.length) {
|
|
4331
4554
|
const unknown = missing.find((hash) => !files.has(hash));
|
|
4332
4555
|
if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
|
|
@@ -4341,7 +4564,7 @@ async function push(ctx, options) {
|
|
|
4341
4564
|
const file = files.get(hash);
|
|
4342
4565
|
const data = fs26.readFileSync(file);
|
|
4343
4566
|
try {
|
|
4344
|
-
await
|
|
4567
|
+
await client2.putBlob(link.project, hash, data);
|
|
4345
4568
|
} catch (error) {
|
|
4346
4569
|
if (error instanceof CloudError && error.status === 413)
|
|
4347
4570
|
throw new CliError(`${path25.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
|
|
@@ -4356,7 +4579,7 @@ async function push(ctx, options) {
|
|
|
4356
4579
|
log(`Pushing ${plural(units.length, "unit")}\u2026`);
|
|
4357
4580
|
let revs2;
|
|
4358
4581
|
try {
|
|
4359
|
-
revs2 = await
|
|
4582
|
+
revs2 = await client2.push(link.project, units);
|
|
4360
4583
|
} catch (error) {
|
|
4361
4584
|
if (!(error instanceof CloudError) || error.status !== 409 || error.error !== "conflict") throw error;
|
|
4362
4585
|
report.ok = false;
|
|
@@ -4368,7 +4591,12 @@ async function push(ctx, options) {
|
|
|
4368
4591
|
}
|
|
4369
4592
|
for (const unit of units) {
|
|
4370
4593
|
const rev = revs2.get(unit.key) ?? unit.baseRev + 1;
|
|
4371
|
-
|
|
4594
|
+
const used = deps.get(unit.key);
|
|
4595
|
+
link.units[unit.key] = {
|
|
4596
|
+
rev,
|
|
4597
|
+
files: hashes(unit.source),
|
|
4598
|
+
...used && Object.keys(used).length ? { deps: used } : {}
|
|
4599
|
+
};
|
|
4372
4600
|
report.units.push({
|
|
4373
4601
|
unit: unit.key,
|
|
4374
4602
|
action: unit.baseRev ? "pushed" : "created",
|
|
@@ -4382,6 +4610,19 @@ async function push(ctx, options) {
|
|
|
4382
4610
|
report.units.sort((a, b) => byUnit(a.unit, b.unit));
|
|
4383
4611
|
return report;
|
|
4384
4612
|
}
|
|
4613
|
+
function borrowedFiles(paths, key, modules) {
|
|
4614
|
+
const own = [unitRoot(paths, key), paths.system, paths.config];
|
|
4615
|
+
const out = {};
|
|
4616
|
+
for (const rel of modules) {
|
|
4617
|
+
const file = path25.join(paths.root, rel);
|
|
4618
|
+
if (own.some((dir) => file === dir || isInside(dir, file)) || isInside(paths.cache, file)) continue;
|
|
4619
|
+
try {
|
|
4620
|
+
out[toPosix(path25.relative(paths.design, file))] = hashFile(file).hash;
|
|
4621
|
+
} catch {
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
return out;
|
|
4625
|
+
}
|
|
4385
4626
|
function refuseLegacyGo(paths, keys) {
|
|
4386
4627
|
const project = new DesignProject(paths, STATIC_URLS);
|
|
4387
4628
|
const hits = /* @__PURE__ */ new Set();
|
|
@@ -4435,16 +4676,16 @@ var record2 = (value) => value && typeof value === "object" && !Array.isArray(va
|
|
|
4435
4676
|
function parseStroke(value) {
|
|
4436
4677
|
const data = record2(value);
|
|
4437
4678
|
if (!data) return null;
|
|
4438
|
-
const { id: id2, kind, ink, size:
|
|
4679
|
+
const { id: id2, kind, ink, size: size3, points, text: text2 } = data;
|
|
4439
4680
|
if (typeof id2 !== "string" || !STROKE_ID.test(id2)) return null;
|
|
4440
4681
|
if (!STROKE_KINDS.includes(kind) || !INKS.includes(ink)) return null;
|
|
4441
|
-
if (typeof
|
|
4682
|
+
if (typeof size3 !== "number" || !(size3 >= 0.5 && size3 <= 400)) return null;
|
|
4442
4683
|
if (!Array.isArray(points) || points.length > MAX_NUMBERS) return null;
|
|
4443
4684
|
if (!points.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && Math.abs(n2) <= MAX_COORD)) return null;
|
|
4444
4685
|
const need = kind === "pen" ? points.length >= 3 && points.length % 3 === 0 : points.length === (kind === "text" ? 2 : 4);
|
|
4445
4686
|
if (!need) return null;
|
|
4446
4687
|
if (kind === "text" && (typeof text2 !== "string" || !text2.trim() || text2.length > MAX_TEXT)) return null;
|
|
4447
|
-
const stroke = { id: id2, kind, ink, size:
|
|
4688
|
+
const stroke = { id: id2, kind, ink, size: size3, points };
|
|
4448
4689
|
if (kind === "text") stroke.text = text2;
|
|
4449
4690
|
return stroke;
|
|
4450
4691
|
}
|
|
@@ -4579,15 +4820,15 @@ function drawingGroups(strokes) {
|
|
|
4579
4820
|
function parseLive(value) {
|
|
4580
4821
|
const data = record2(value);
|
|
4581
4822
|
if (!data) return null;
|
|
4582
|
-
const { id: id2, kind, ink, size:
|
|
4823
|
+
const { id: id2, kind, ink, size: size3, text: text2, at, points } = data;
|
|
4583
4824
|
if (typeof id2 !== "string" || !STROKE_ID.test(id2)) return null;
|
|
4584
4825
|
if (!STROKE_KINDS.includes(kind) || !INKS.includes(ink)) return null;
|
|
4585
|
-
if (typeof
|
|
4826
|
+
if (typeof size3 !== "number" || !(size3 >= 0.5 && size3 <= 400)) return null;
|
|
4586
4827
|
if (typeof at !== "number" || !Number.isInteger(at) || at < 0 || at > MAX_NUMBERS) return null;
|
|
4587
4828
|
if (!Array.isArray(points) || points.length > MAX_NUMBERS) return null;
|
|
4588
4829
|
if (!points.every((n2) => typeof n2 === "number" && Number.isFinite(n2) && Math.abs(n2) <= MAX_COORD)) return null;
|
|
4589
4830
|
if (text2 !== void 0 && (typeof text2 !== "string" || text2.length > MAX_TEXT)) return null;
|
|
4590
|
-
const stroke = { id: id2, kind, ink, size:
|
|
4831
|
+
const stroke = { id: id2, kind, ink, size: size3, at, points };
|
|
4591
4832
|
if (typeof text2 === "string") stroke.text = text2;
|
|
4592
4833
|
return stroke;
|
|
4593
4834
|
}
|
|
@@ -4657,10 +4898,10 @@ function sendJson(res, status, body) {
|
|
|
4657
4898
|
function readBody(req, limit = 25 * 1024 * 1024) {
|
|
4658
4899
|
return new Promise((resolve, reject) => {
|
|
4659
4900
|
const chunks = [];
|
|
4660
|
-
let
|
|
4901
|
+
let size3 = 0;
|
|
4661
4902
|
req.on("data", (chunk2) => {
|
|
4662
|
-
|
|
4663
|
-
if (
|
|
4903
|
+
size3 += chunk2.length;
|
|
4904
|
+
if (size3 > limit) {
|
|
4664
4905
|
reject(new Error("body too large"));
|
|
4665
4906
|
req.destroy();
|
|
4666
4907
|
} else chunks.push(chunk2);
|
|
@@ -4845,6 +5086,8 @@ var Room = class {
|
|
|
4845
5086
|
cloud = null;
|
|
4846
5087
|
written = null;
|
|
4847
5088
|
saveTimer;
|
|
5089
|
+
/** A change not on disk yet: only such a change is written, so looking at a canvas writes nothing. */
|
|
5090
|
+
dirty = false;
|
|
4848
5091
|
idleTimer;
|
|
4849
5092
|
get file() {
|
|
4850
5093
|
return drawingsFile(this.hub.paths, this.canvas);
|
|
@@ -4885,39 +5128,39 @@ var Room = class {
|
|
|
4885
5128
|
this.idleTimer.unref();
|
|
4886
5129
|
}
|
|
4887
5130
|
join(socket) {
|
|
4888
|
-
const
|
|
4889
|
-
this.clients.set(socket,
|
|
5131
|
+
const client2 = { peer: this.hub.nextPeer(), live: /* @__PURE__ */ new Map() };
|
|
5132
|
+
this.clients.set(socket, client2);
|
|
4890
5133
|
this.connectCloud();
|
|
4891
|
-
send(socket, { type: "hello", peer:
|
|
5134
|
+
send(socket, { type: "hello", peer: client2.peer, canDraw: this.canDraw(), drawings: this.drawings });
|
|
4892
5135
|
socket.on("message", (data, isBinary) => {
|
|
4893
5136
|
if (isBinary) return;
|
|
4894
5137
|
const message = parseClientMessage(data.toString());
|
|
4895
|
-
if (message) this.fromClient(socket,
|
|
5138
|
+
if (message) this.fromClient(socket, client2, message);
|
|
4896
5139
|
else send(socket, { type: "error", message: "Not a drawings message." });
|
|
4897
5140
|
});
|
|
4898
5141
|
socket.on("close", () => {
|
|
4899
5142
|
this.clients.delete(socket);
|
|
4900
|
-
for (const [id2, key] of
|
|
4901
|
-
this.broadcast({ type: "gone", peer:
|
|
5143
|
+
for (const [id2, key] of client2.live) this.cloud?.send({ type: "live-end", key, id: id2 });
|
|
5144
|
+
this.broadcast({ type: "gone", peer: client2.peer });
|
|
4902
5145
|
this.release();
|
|
4903
5146
|
});
|
|
4904
5147
|
socket.on("error", () => socket.terminate());
|
|
4905
5148
|
}
|
|
4906
|
-
fromClient(socket,
|
|
5149
|
+
fromClient(socket, client2, message) {
|
|
4907
5150
|
if (!this.canDraw()) {
|
|
4908
5151
|
send(socket, { type: "error", message: "You can look at these drawings but not draw on them." });
|
|
4909
5152
|
return;
|
|
4910
5153
|
}
|
|
4911
5154
|
if (message.type === "ops") {
|
|
4912
|
-
this.change(message.key, { add: message.add, erase: message.erase },
|
|
5155
|
+
this.change(message.key, { add: message.add, erase: message.erase }, client2.peer);
|
|
4913
5156
|
send(socket, { type: "ack", id: message.id });
|
|
4914
5157
|
} else if (message.type === "live") {
|
|
4915
|
-
|
|
4916
|
-
this.broadcast({ ...message, peer:
|
|
5158
|
+
client2.live.set(message.stroke.id, message.key);
|
|
5159
|
+
this.broadcast({ ...message, peer: client2.peer }, socket);
|
|
4917
5160
|
this.cloud?.send(message);
|
|
4918
5161
|
} else {
|
|
4919
|
-
|
|
4920
|
-
this.broadcast({ ...message, peer:
|
|
5162
|
+
client2.live.delete(message.id);
|
|
5163
|
+
this.broadcast({ ...message, peer: client2.peer }, socket);
|
|
4921
5164
|
this.cloud?.send(message);
|
|
4922
5165
|
}
|
|
4923
5166
|
}
|
|
@@ -4951,8 +5194,8 @@ var Room = class {
|
|
|
4951
5194
|
}
|
|
4952
5195
|
/** The cloud's answer came in, or its verdict on drawing changed: everyone here starts over from it. */
|
|
4953
5196
|
rehello() {
|
|
4954
|
-
for (const [socket,
|
|
4955
|
-
send(socket, { type: "hello", peer:
|
|
5197
|
+
for (const [socket, client2] of this.clients)
|
|
5198
|
+
send(socket, { type: "hello", peer: client2.peer, canDraw: this.canDraw(), drawings: this.drawings });
|
|
4956
5199
|
}
|
|
4957
5200
|
broadcast(message, except) {
|
|
4958
5201
|
const data = JSON.stringify(message);
|
|
@@ -4960,11 +5203,14 @@ var Room = class {
|
|
|
4960
5203
|
if (socket !== except && socket.readyState === WebSocket.OPEN) socket.send(data);
|
|
4961
5204
|
}
|
|
4962
5205
|
save() {
|
|
5206
|
+
this.dirty = true;
|
|
4963
5207
|
clearTimeout(this.saveTimer);
|
|
4964
5208
|
this.saveTimer = setTimeout(() => this.flush(), 150);
|
|
4965
5209
|
}
|
|
4966
5210
|
flush() {
|
|
4967
5211
|
clearTimeout(this.saveTimer);
|
|
5212
|
+
if (!this.dirty) return;
|
|
5213
|
+
this.dirty = false;
|
|
4968
5214
|
const dir = path27.dirname(this.file);
|
|
4969
5215
|
if (!fs28.existsSync(dir)) return;
|
|
4970
5216
|
const drawings = Object.fromEntries(
|
|
@@ -5035,10 +5281,10 @@ var CloudRelay = class {
|
|
|
5035
5281
|
/** Cloud peers with strokes on the way here, cleared when the socket drops. */
|
|
5036
5282
|
peers = /* @__PURE__ */ new Set();
|
|
5037
5283
|
url() {
|
|
5038
|
-
const
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
return
|
|
5284
|
+
const base2 = new URL(this.host);
|
|
5285
|
+
base2.protocol = base2.protocol === "https:" ? "wss:" : "ws:";
|
|
5286
|
+
base2.pathname = `/api/projects/${encodeURIComponent(this.project)}/canvases/${encodeURIComponent(this.room.canvas)}/drawings`;
|
|
5287
|
+
return base2.href;
|
|
5042
5288
|
}
|
|
5043
5289
|
connect() {
|
|
5044
5290
|
if (this.closed) return;
|
|
@@ -5219,7 +5465,7 @@ var EventHub = class {
|
|
|
5219
5465
|
timer;
|
|
5220
5466
|
constructor() {
|
|
5221
5467
|
this.timer = setInterval(() => {
|
|
5222
|
-
for (const
|
|
5468
|
+
for (const client2 of this.clients) client2.write(": ping\n\n");
|
|
5223
5469
|
}, 2e4);
|
|
5224
5470
|
this.timer.unref();
|
|
5225
5471
|
}
|
|
@@ -5237,11 +5483,11 @@ var EventHub = class {
|
|
|
5237
5483
|
const data = `data: ${JSON.stringify(event)}
|
|
5238
5484
|
|
|
5239
5485
|
`;
|
|
5240
|
-
for (const
|
|
5486
|
+
for (const client2 of this.clients) client2.write(data);
|
|
5241
5487
|
}
|
|
5242
5488
|
close() {
|
|
5243
5489
|
clearInterval(this.timer);
|
|
5244
|
-
for (const
|
|
5490
|
+
for (const client2 of this.clients) client2.end();
|
|
5245
5491
|
this.clients.clear();
|
|
5246
5492
|
}
|
|
5247
5493
|
};
|
|
@@ -5260,8 +5506,8 @@ function readState(paths) {
|
|
|
5260
5506
|
}
|
|
5261
5507
|
}
|
|
5262
5508
|
function registryDir() {
|
|
5263
|
-
const
|
|
5264
|
-
return path28.join(
|
|
5509
|
+
const base2 = 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");
|
|
5510
|
+
return path28.join(base2, "foss-design", "previews");
|
|
5265
5511
|
}
|
|
5266
5512
|
var entryFile = (pid) => path28.join(registryDir(), `${pid}.json`);
|
|
5267
5513
|
function writeState(paths, state) {
|
|
@@ -5391,9 +5637,12 @@ async function startDevServer(options) {
|
|
|
5391
5637
|
const httpServer = http2.createServer();
|
|
5392
5638
|
const listening = () => httpServer.address()?.port ?? 0;
|
|
5393
5639
|
dropStaleDepCache(paths, project.appDir());
|
|
5394
|
-
const
|
|
5640
|
+
const deps = installStamp(paths, project.appDir());
|
|
5641
|
+
const fromApp = await appPlugins(project, "serve");
|
|
5642
|
+
for (const problem of fromApp.problems) console.warn(`warning: ${problem}`);
|
|
5643
|
+
const base2 = baseConfig(project, fromApp.plugins);
|
|
5395
5644
|
const vite = await createServer({
|
|
5396
|
-
...
|
|
5645
|
+
...base2,
|
|
5397
5646
|
base: "/_fs/",
|
|
5398
5647
|
appType: "custom",
|
|
5399
5648
|
server: {
|
|
@@ -5575,6 +5824,19 @@ async function startDevServer(options) {
|
|
|
5575
5824
|
}
|
|
5576
5825
|
}, 60);
|
|
5577
5826
|
};
|
|
5827
|
+
const scanned = entries.scannedDirs();
|
|
5828
|
+
vite.watcher.add(scanned.filter((dir) => !isInside(paths.design, dir)));
|
|
5829
|
+
let cssTimer;
|
|
5830
|
+
const rebuildCss = () => {
|
|
5831
|
+
clearTimeout(cssTimer);
|
|
5832
|
+
cssTimer = setTimeout(() => {
|
|
5833
|
+
for (const mod of vite.moduleGraph.getModulesByFile(entries.systemCssFile()) ?? []) void vite.reloadModule(mod);
|
|
5834
|
+
}, 80);
|
|
5835
|
+
};
|
|
5836
|
+
vite.watcher.on("add", (file) => {
|
|
5837
|
+
if (file.split(path29.sep).includes("node_modules") || isInside(paths.cache, file)) return;
|
|
5838
|
+
if (scanned.some((dir) => isInside(dir, file))) rebuildCss();
|
|
5839
|
+
});
|
|
5578
5840
|
vite.watcher.on("all", (event, file) => {
|
|
5579
5841
|
if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
|
|
5580
5842
|
const rel = toPosix(path29.relative(paths.design, file));
|
|
@@ -5616,7 +5878,8 @@ async function startDevServer(options) {
|
|
|
5616
5878
|
url,
|
|
5617
5879
|
root: paths.root,
|
|
5618
5880
|
version: packageVersion(),
|
|
5619
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5881
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5882
|
+
deps
|
|
5620
5883
|
});
|
|
5621
5884
|
}
|
|
5622
5885
|
let closed = false;
|
|
@@ -5686,9 +5949,11 @@ async function stopServer(paths) {
|
|
|
5686
5949
|
async function ensureServer(paths, options) {
|
|
5687
5950
|
const live2 = await liveServer(paths);
|
|
5688
5951
|
const stale = live2 && live2.version !== packageVersion();
|
|
5689
|
-
|
|
5952
|
+
const installed = live2?.deps !== void 0 && live2.deps !== installStamp(paths, new DesignProject(paths, DEV_URLS).appDir());
|
|
5953
|
+
if (live2 && !options.restart && !stale && !installed) return live2;
|
|
5690
5954
|
if (stale)
|
|
5691
5955
|
print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
|
|
5956
|
+
else if (installed) print(dim("Restarting the preview server: the app's packages changed since it started."));
|
|
5692
5957
|
if (live2) await stopServer(paths);
|
|
5693
5958
|
const recorded = readState(paths);
|
|
5694
5959
|
if (recorded && pidAlive(recorded.pid)) await stopPreview(recorded);
|
|
@@ -5887,8 +6152,8 @@ function canvasArg(value, usage) {
|
|
|
5887
6152
|
if (key === SYSTEM_UNIT) throw new CliError(`The design system is not a canvas. Usage: ${usage}`);
|
|
5888
6153
|
return key.slice("canvas/".length);
|
|
5889
6154
|
}
|
|
5890
|
-
async function remoteCanvas(
|
|
5891
|
-
const unit = (await
|
|
6155
|
+
async function remoteCanvas(client2, project, canvas) {
|
|
6156
|
+
const unit = (await client2.units(project)).get(canvasUnit(canvas));
|
|
5892
6157
|
if (!unit) throw new CliError(`${canvas} is not in the cloud yet: \`design push ${canvas}\` uploads it`);
|
|
5893
6158
|
return unit;
|
|
5894
6159
|
}
|
|
@@ -5919,7 +6184,7 @@ async function runCanvases(paths, options) {
|
|
|
5919
6184
|
const rows = ids.map((id2) => {
|
|
5920
6185
|
const doc = local.find((entry) => entry.id === id2);
|
|
5921
6186
|
const unit = remote?.get(canvasUnit(id2));
|
|
5922
|
-
const status = link && remote ? unitStatus(canvasUnit(id2), link, scan?.units.get(canvasUnit(id2))?.manifest, remote) : null;
|
|
6187
|
+
const status = link && remote ? unitStatus(canvasUnit(id2), link, scan?.units.get(canvasUnit(id2))?.manifest, remote, paths.design) : null;
|
|
5923
6188
|
const frames = doc?.pages.flatMap((page) => page.sections.flatMap((section) => section.items)).filter((item) => item.kind === "screen" || item.kind === "url");
|
|
5924
6189
|
return {
|
|
5925
6190
|
id: id2,
|
|
@@ -5955,8 +6220,8 @@ async function runCanvases(paths, options) {
|
|
|
5955
6220
|
}
|
|
5956
6221
|
async function runUrl(paths, value, options) {
|
|
5957
6222
|
const canvas = canvasArg(value, "design url <canvas>");
|
|
5958
|
-
const { link, client } = cloud(paths);
|
|
5959
|
-
const unit = await remoteCanvas(
|
|
6223
|
+
const { link, client: client2 } = cloud(paths);
|
|
6224
|
+
const unit = await remoteCanvas(client2, link.project, canvas);
|
|
5960
6225
|
const url = webUrl(link, canvasUnit(canvas));
|
|
5961
6226
|
const pub = unit.publicId ? publicUrl(link.host, unit.publicId) : null;
|
|
5962
6227
|
const screens = unit.screens.map((s) => ({ screen: s.item, publicUrl: screenUrl(link.host, s.publicId) }));
|
|
@@ -5974,12 +6239,12 @@ async function runPublish(paths, value, publish) {
|
|
|
5974
6239
|
const screen = value && slash > 0 ? value.slice(slash + 1) : null;
|
|
5975
6240
|
const canvas = canvasArg(value && slash > 0 ? value.slice(0, slash) : value, usage);
|
|
5976
6241
|
if (screen === "") throw new CliError(`Usage: ${usage}`);
|
|
5977
|
-
const { link, client } = cloud(paths);
|
|
5978
|
-
const unit = await remoteCanvas(
|
|
6242
|
+
const { link, client: client2 } = cloud(paths);
|
|
6243
|
+
const unit = await remoteCanvas(client2, link.project, canvas);
|
|
5979
6244
|
if (screen) {
|
|
5980
6245
|
if (!publish && !unit.screens.some((s) => s.item === screen))
|
|
5981
6246
|
return print(dim(`${canvas}/${screen} has no public link`));
|
|
5982
|
-
const { publicId: publicId2 } = await
|
|
6247
|
+
const { publicId: publicId2 } = await client2.screenAction(link.project, canvas, screen, verb);
|
|
5983
6248
|
if (publish) {
|
|
5984
6249
|
print(
|
|
5985
6250
|
`${green("\u2713")} Published ${bold(`${canvas}/${screen}`)} on its own: anyone with the link sees this screen alone`
|
|
@@ -5988,9 +6253,9 @@ async function runPublish(paths, value, publish) {
|
|
|
5988
6253
|
} else print(`${green("\u2713")} ${bold(`${canvas}/${screen}`)} has no public link now`);
|
|
5989
6254
|
return;
|
|
5990
6255
|
}
|
|
5991
|
-
const { publicId } = await
|
|
6256
|
+
const { publicId } = await client2.canvasAction(link.project, canvas, verb);
|
|
5992
6257
|
if (publish) {
|
|
5993
|
-
const id2 = publicId ?? (await remoteCanvas(
|
|
6258
|
+
const id2 = publicId ?? (await remoteCanvas(client2, link.project, canvas)).publicId;
|
|
5994
6259
|
print(`${green("\u2713")} Published ${bold(canvas)}: anyone with the link can view it`);
|
|
5995
6260
|
if (id2) print(` ${cyan(publicUrl(link.host, id2))}`);
|
|
5996
6261
|
} else print(`${green("\u2713")} ${bold(canvas)} has no public link now; members still open it in the project`);
|
|
@@ -5998,9 +6263,9 @@ async function runPublish(paths, value, publish) {
|
|
|
5998
6263
|
async function runArchive(paths, value, archive) {
|
|
5999
6264
|
const verb = archive ? "archive" : "unarchive";
|
|
6000
6265
|
const canvas = canvasArg(value, `design ${verb} <canvas>`);
|
|
6001
|
-
const { link, client } = cloud(paths);
|
|
6002
|
-
await remoteCanvas(
|
|
6003
|
-
await
|
|
6266
|
+
const { link, client: client2 } = cloud(paths);
|
|
6267
|
+
await remoteCanvas(client2, link.project, canvas);
|
|
6268
|
+
await client2.canvasAction(link.project, canvas, verb);
|
|
6004
6269
|
print(
|
|
6005
6270
|
archive ? `${green("\u2713")} Archived ${bold(canvas)} in the cloud; its history is kept. \`design unarchive ${canvas}\` brings it back.` : `${green("\u2713")} ${bold(canvas)} is active again ${dim(webUrl(link, canvasUnit(canvas)))}`
|
|
6006
6271
|
);
|
|
@@ -6008,10 +6273,10 @@ async function runArchive(paths, value, archive) {
|
|
|
6008
6273
|
async function runHistory(paths, value, options) {
|
|
6009
6274
|
if (!value) throw new CliError("Usage: design history <canvas|system>");
|
|
6010
6275
|
const key = unitArg(value);
|
|
6011
|
-
const { link, client } = cloud(paths);
|
|
6276
|
+
const { link, client: client2 } = cloud(paths);
|
|
6012
6277
|
let revisions;
|
|
6013
6278
|
try {
|
|
6014
|
-
revisions = await
|
|
6279
|
+
revisions = await client2.revisions(link.project, key);
|
|
6015
6280
|
} catch (error) {
|
|
6016
6281
|
if (error instanceof CloudError && error.status === 404)
|
|
6017
6282
|
throw new CliError(`${key} is not in the cloud yet: \`design push\` uploads it`);
|
|
@@ -6039,8 +6304,8 @@ async function runRollback(paths, value, revArg) {
|
|
|
6039
6304
|
const rev = Number((revArg ?? "").replace(/^r/i, ""));
|
|
6040
6305
|
if (!Number.isInteger(rev) || rev < 1)
|
|
6041
6306
|
throw new CliError("Usage: design rollback <canvas> <rev> (see `design history <canvas>`)");
|
|
6042
|
-
const { link, client } = cloud(paths);
|
|
6043
|
-
const head = await
|
|
6307
|
+
const { link, client: client2 } = cloud(paths);
|
|
6308
|
+
const head = await client2.rollback(link.project, canvasUnit(canvas), rev);
|
|
6044
6309
|
print(`${green("\u2713")} ${bold(canvas)}: r${rev} is current again as r${head} in the cloud`);
|
|
6045
6310
|
await runPull(paths, [canvas], { theirs: [], json: false });
|
|
6046
6311
|
}
|
|
@@ -6050,18 +6315,18 @@ async function runMe(paths, options) {
|
|
|
6050
6315
|
const link = paths ? readLink(paths) : null;
|
|
6051
6316
|
const host = link?.host ?? cloudHost();
|
|
6052
6317
|
const credential = requireCredential(host);
|
|
6053
|
-
const
|
|
6318
|
+
const client2 = new CloudClient(host, credential.token);
|
|
6054
6319
|
const [me, usage, activity] = await Promise.all([
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6320
|
+
client2.request("GET", "/me").then(obj),
|
|
6321
|
+
client2.request("GET", "/me/usage").then(obj),
|
|
6322
|
+
client2.request("GET", "/me/activity").then(obj)
|
|
6058
6323
|
]);
|
|
6059
6324
|
const plan = obj(usage.plan);
|
|
6060
6325
|
const limits = obj(usage.limits);
|
|
6061
6326
|
const totals = obj(activity.totals);
|
|
6062
6327
|
let linked = null;
|
|
6063
6328
|
if (link) {
|
|
6064
|
-
const projects = await
|
|
6329
|
+
const projects = await client2.projects().catch(() => null);
|
|
6065
6330
|
const project = projects && [...projects.owned, ...projects.shared].find((p) => p.id === link.project);
|
|
6066
6331
|
linked = project ? { id: project.id, name: project.name, role: project.role } : { id: link.project, name: "", role: "" };
|
|
6067
6332
|
}
|
|
@@ -6187,9 +6452,11 @@ async function runCheck(paths, targets2, options) {
|
|
|
6187
6452
|
);
|
|
6188
6453
|
}
|
|
6189
6454
|
const issues = canvases.length ? canvases.flatMap((id2) => project.canvas(id2)?.doc.issues ?? []) : project.issues();
|
|
6455
|
+
for (const message of (await appPlugins(project, "serve")).problems)
|
|
6456
|
+
issues.push({ severity: "warning", file: relToRoot(paths, paths.config), message });
|
|
6190
6457
|
let frames = [];
|
|
6191
6458
|
if (options.render) {
|
|
6192
|
-
let
|
|
6459
|
+
let base2;
|
|
6193
6460
|
let docOf = (id2) => project.canvas(id2).doc;
|
|
6194
6461
|
let systemDoc = project.system()?.doc ?? null;
|
|
6195
6462
|
let close = async () => {
|
|
@@ -6202,24 +6469,24 @@ async function runCheck(paths, targets2, options) {
|
|
|
6202
6469
|
});
|
|
6203
6470
|
docOf = (id2) => build2.read(`api/canvas/${id2}.json`) ?? project.canvas(id2).doc;
|
|
6204
6471
|
systemDoc = canvases.length ? null : build2.read("api/system.json");
|
|
6205
|
-
|
|
6472
|
+
base2 = build2.base;
|
|
6206
6473
|
close = build2.close;
|
|
6207
|
-
if (!options.json) print(dim(`Serving the build at ${
|
|
6474
|
+
if (!options.json) print(dim(`Serving the build at ${base2}`));
|
|
6208
6475
|
} else {
|
|
6209
6476
|
const wasRunning = await liveServer(paths);
|
|
6210
6477
|
const server = await ensureServer(paths, {});
|
|
6211
6478
|
if (!wasRunning && !options.json)
|
|
6212
6479
|
print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
|
|
6213
|
-
|
|
6480
|
+
base2 = `http://127.0.0.1:${server.port}`;
|
|
6214
6481
|
}
|
|
6215
6482
|
const jobs = (canvases.length ? canvases : known).flatMap((id2) => {
|
|
6216
6483
|
const doc = docOf(id2);
|
|
6217
6484
|
const theme2 = doc.theme ?? "light";
|
|
6218
6485
|
const screens = picked.get(id2);
|
|
6219
6486
|
const filter = { includeUrls: false, page: options.page };
|
|
6220
|
-
return screens ? [...screens].flatMap((screen) => canvasFrames(doc,
|
|
6487
|
+
return screens ? [...screens].flatMap((screen) => canvasFrames(doc, base2, theme2, { ...filter, id: screen })) : canvasFrames(doc, base2, theme2, filter);
|
|
6221
6488
|
});
|
|
6222
|
-
if (!canvases.length && systemDoc) jobs.unshift(...systemFrames(systemDoc,
|
|
6489
|
+
if (!canvases.length && systemDoc) jobs.unshift(...systemFrames(systemDoc, base2, "light"));
|
|
6223
6490
|
if (!options.json) print(dim(`Loading ${plural2(jobs.length, "frame")} in Chrome\u2026`));
|
|
6224
6491
|
try {
|
|
6225
6492
|
frames = await renderFrames(jobs);
|
|
@@ -6331,8 +6598,8 @@ import path30 from "path";
|
|
|
6331
6598
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6332
6599
|
async function runLogin() {
|
|
6333
6600
|
const host = cloudHost();
|
|
6334
|
-
const
|
|
6335
|
-
const code = await
|
|
6601
|
+
const client2 = new CloudClient(host, null);
|
|
6602
|
+
const code = await client2.deviceCode(os5.hostname());
|
|
6336
6603
|
const url = new URL(code.verification_uri_complete || code.verification_uri, host).toString();
|
|
6337
6604
|
const expiresIn = code.expires_in ?? 600;
|
|
6338
6605
|
print(`${bold("Sign in to foss-design Cloud")} ${dim(host)}`);
|
|
@@ -6351,7 +6618,7 @@ async function runLogin() {
|
|
|
6351
6618
|
if (Date.now() > deadline) throw new CliError("The sign-in code expired. Run `design login` again.");
|
|
6352
6619
|
let result;
|
|
6353
6620
|
try {
|
|
6354
|
-
result = await
|
|
6621
|
+
result = await client2.deviceToken(code.device_code);
|
|
6355
6622
|
} catch (error) {
|
|
6356
6623
|
if (error instanceof CloudError && error.status === 0) continue;
|
|
6357
6624
|
throw error;
|
|
@@ -6384,7 +6651,11 @@ function printProjects(title, projects, linked) {
|
|
|
6384
6651
|
const width = Math.max(...projects.map((project) => project.id.length));
|
|
6385
6652
|
const nameWidth = Math.max(...projects.map((project) => project.name.length));
|
|
6386
6653
|
for (const project of projects) {
|
|
6387
|
-
const flags = [
|
|
6654
|
+
const flags = [
|
|
6655
|
+
project.kind === "pages" && dim("pages"),
|
|
6656
|
+
project.archived && yellow("archived"),
|
|
6657
|
+
project.banned && yellow("banned")
|
|
6658
|
+
].filter(Boolean);
|
|
6388
6659
|
print(
|
|
6389
6660
|
` ${project.id.padEnd(width)} ${project.name.padEnd(nameWidth)} ${dim(project.role.padEnd(6))}` + (flags.length ? ` ${flags.join(" ")}` : "") + (project.id === linked ? ` ${green("linked")}` : "")
|
|
6390
6661
|
);
|
|
@@ -6394,16 +6665,16 @@ async function runLink(paths, wanted, options) {
|
|
|
6394
6665
|
if (wanted && options.newName !== void 0)
|
|
6395
6666
|
throw new CliError("Pass a project or --new <name>, not both. `design link` lists your projects.");
|
|
6396
6667
|
const host = cloudHost();
|
|
6397
|
-
const
|
|
6668
|
+
const client2 = new CloudClient(host, requireCredential(host).token);
|
|
6398
6669
|
const current = readLink(paths);
|
|
6399
6670
|
let project;
|
|
6400
6671
|
if (options.newName !== void 0) {
|
|
6401
6672
|
const name = options.newName.trim();
|
|
6402
6673
|
if (!name) throw new CliError("--new takes a project name");
|
|
6403
|
-
project = await
|
|
6674
|
+
project = await client2.createProject(name);
|
|
6404
6675
|
print(`${green("created")} ${project.name} ${dim(`(${project.id})`)}`);
|
|
6405
6676
|
} else {
|
|
6406
|
-
const { owned, shared } = await
|
|
6677
|
+
const { owned, shared } = await client2.projects();
|
|
6407
6678
|
if (!wanted) {
|
|
6408
6679
|
const linked = current?.host === host ? current.project : void 0;
|
|
6409
6680
|
print(`${bold("Projects on")} ${host}`);
|
|
@@ -6424,6 +6695,10 @@ async function runLink(paths, wanted, options) {
|
|
|
6424
6695
|
project = found;
|
|
6425
6696
|
}
|
|
6426
6697
|
if (project.banned) throw new CliError(`${project.name} is blocked by moderation and cannot be linked.`);
|
|
6698
|
+
if (project.kind === "pages")
|
|
6699
|
+
throw new CliError(
|
|
6700
|
+
`${project.name} is a pages project: \`design page\` publishes into it, and .design links to a project of canvases.`
|
|
6701
|
+
);
|
|
6427
6702
|
if (current?.host === host && current.project === project.id) {
|
|
6428
6703
|
print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
|
|
6429
6704
|
} else {
|
|
@@ -6434,12 +6709,12 @@ async function runLink(paths, wanted, options) {
|
|
|
6434
6709
|
}
|
|
6435
6710
|
if (project.archived) print(yellow("The project is archived: unarchive it in the web app before pushing or pulling."));
|
|
6436
6711
|
const link = readLink(paths);
|
|
6437
|
-
for (const line of await syncProjectMeta(paths,
|
|
6712
|
+
for (const line of await syncProjectMeta(paths, client2, link)) print(dim(line));
|
|
6438
6713
|
writeLink(paths, link);
|
|
6439
|
-
const remote = project.archived ? null : await
|
|
6714
|
+
const remote = project.archived ? null : await client2.units(project.id);
|
|
6440
6715
|
const scan = scanLocal(paths);
|
|
6441
6716
|
const states = allUnitKeys(link, scan.units, remote).map(
|
|
6442
|
-
(key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote).state
|
|
6717
|
+
(key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote, paths.design).state
|
|
6443
6718
|
);
|
|
6444
6719
|
const count = (state) => states.filter((item) => item === state).length;
|
|
6445
6720
|
const ahead = count("remote_ahead");
|
|
@@ -6456,10 +6731,10 @@ async function runLink(paths, wanted, options) {
|
|
|
6456
6731
|
|
|
6457
6732
|
// src/cli/commands/drawings.ts
|
|
6458
6733
|
import http3 from "http";
|
|
6459
|
-
function call(port, method,
|
|
6734
|
+
function call(port, method, path40) {
|
|
6460
6735
|
return new Promise((resolve, reject) => {
|
|
6461
6736
|
const req = http3.request(
|
|
6462
|
-
{ host: "127.0.0.1", port, method, path:
|
|
6737
|
+
{ host: "127.0.0.1", port, method, path: path40, headers: { host: `localhost:${port}` }, timeout: 3e4 },
|
|
6463
6738
|
(res) => {
|
|
6464
6739
|
const chunks = [];
|
|
6465
6740
|
res.on("data", (chunk2) => chunks.push(chunk2));
|
|
@@ -6576,10 +6851,10 @@ async function toCloud(paths, file) {
|
|
|
6576
6851
|
print(yellow("Not signed in: the icon goes to the cloud on the next `design push` after `design login`."));
|
|
6577
6852
|
return;
|
|
6578
6853
|
}
|
|
6579
|
-
const
|
|
6854
|
+
const client2 = new CloudClient(link.host, credential.token);
|
|
6580
6855
|
try {
|
|
6581
6856
|
const ext = file ? path31.extname(file).slice(1) : null;
|
|
6582
|
-
await
|
|
6857
|
+
await client2.setProjectIcon(
|
|
6583
6858
|
link.project,
|
|
6584
6859
|
file && ext ? { data: fs33.readFileSync(file), type: ICON_TYPES[ext] } : null
|
|
6585
6860
|
);
|
|
@@ -7076,7 +7351,7 @@ function elementJsx(element, ctx) {
|
|
|
7076
7351
|
function convertFile(name, source, ctx) {
|
|
7077
7352
|
const document2 = parseMarkup(source);
|
|
7078
7353
|
const root = findElement(document2, (el) => el.tag.toLowerCase() === "x-dc");
|
|
7079
|
-
const
|
|
7354
|
+
const script2 = findElement(
|
|
7080
7355
|
document2,
|
|
7081
7356
|
(el) => el.tag.toLowerCase() === "script" && attr(el, "data-dc-script") !== void 0
|
|
7082
7357
|
);
|
|
@@ -7085,7 +7360,7 @@ function convertFile(name, source, ctx) {
|
|
|
7085
7360
|
const helmet = /<helmet>([\s\S]*?)<\/helmet>/i.exec(source)?.[1]?.trim() ?? "";
|
|
7086
7361
|
const body = childrenJsx(root.children, file, "x-dc");
|
|
7087
7362
|
let spec = {};
|
|
7088
|
-
const rawSpec =
|
|
7363
|
+
const rawSpec = script2 ? attr(script2, "data-props") : null;
|
|
7089
7364
|
if (rawSpec) {
|
|
7090
7365
|
try {
|
|
7091
7366
|
spec = JSON.parse(decodeEntities(rawSpec));
|
|
@@ -7093,7 +7368,7 @@ function convertFile(name, source, ctx) {
|
|
|
7093
7368
|
file.warn("has data-props that are not JSON; the props keep no defaults");
|
|
7094
7369
|
}
|
|
7095
7370
|
}
|
|
7096
|
-
const code =
|
|
7371
|
+
const code = script2?.children.map((child) => child.type === "text" ? child.text : "").join("") ?? "";
|
|
7097
7372
|
const className = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+DCLogic/.exec(code)?.[1];
|
|
7098
7373
|
const logic = className ? `const __Logic = (() => {
|
|
7099
7374
|
${code.trim()}
|
|
@@ -7222,10 +7497,10 @@ export const flag = (v) => v === true || v === 'true' || (!!v && v !== 'false')
|
|
|
7222
7497
|
export const list = (v) => (Array.isArray(v) ? v : v == null ? [] : Array.from(v))
|
|
7223
7498
|
`;
|
|
7224
7499
|
function boardId(file, used) {
|
|
7225
|
-
let
|
|
7226
|
-
if (!ID_PATTERN.test(
|
|
7227
|
-
let id2 =
|
|
7228
|
-
for (let n2 = 2; used.has(id2); n2++) id2 = `${
|
|
7500
|
+
let base2 = kebab(file.slice(0, -EXT.length)) || "screen";
|
|
7501
|
+
if (!ID_PATTERN.test(base2)) base2 = `s-${base2}`;
|
|
7502
|
+
let id2 = base2;
|
|
7503
|
+
for (let n2 = 2; used.has(id2); n2++) id2 = `${base2}-${n2}`;
|
|
7229
7504
|
used.add(id2);
|
|
7230
7505
|
return id2;
|
|
7231
7506
|
}
|
|
@@ -7486,15 +7761,297 @@ async function runMerge(paths, targets2, options) {
|
|
|
7486
7761
|
}
|
|
7487
7762
|
}
|
|
7488
7763
|
|
|
7489
|
-
// src/
|
|
7764
|
+
// src/cloud/page.ts
|
|
7490
7765
|
import fs36 from "fs";
|
|
7766
|
+
import path35 from "path";
|
|
7767
|
+
var PAGE_SLUG = /^[a-z0-9][a-z0-9_-]{0,99}$/;
|
|
7768
|
+
var PAGE_ENTRY = "index.html";
|
|
7769
|
+
var HTML = /\.html?$/i;
|
|
7770
|
+
var MAX_TITLE = 200;
|
|
7771
|
+
function slugFor(target) {
|
|
7772
|
+
const base2 = path35.basename(path35.resolve(target)).replace(HTML, "");
|
|
7773
|
+
const slug2 = base2.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9_-]+/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 100).replace(/[-_]+$/, "");
|
|
7774
|
+
return slug2 || "page";
|
|
7775
|
+
}
|
|
7776
|
+
var script = null;
|
|
7777
|
+
function pageScript() {
|
|
7778
|
+
if (script === null) {
|
|
7779
|
+
let code;
|
|
7780
|
+
try {
|
|
7781
|
+
code = fs36.readFileSync(PKG.pageRuntime, "utf8").trim();
|
|
7782
|
+
} catch {
|
|
7783
|
+
throw new CliError(`${PKG.pageRuntime} is missing; this foss-design install is incomplete.`);
|
|
7784
|
+
}
|
|
7785
|
+
script = `<script data-foss-design>${code.replace(/<\/script/gi, "<\\/script")}</script>`;
|
|
7786
|
+
}
|
|
7787
|
+
return script;
|
|
7788
|
+
}
|
|
7789
|
+
function withPageScript(html) {
|
|
7790
|
+
const tag = pageScript();
|
|
7791
|
+
const at = (pattern) => {
|
|
7792
|
+
const match = pattern.exec(html);
|
|
7793
|
+
return match ? match.index + match[0].length : -1;
|
|
7794
|
+
};
|
|
7795
|
+
let index = at(/<head(?:\s[^>]*)?>/i);
|
|
7796
|
+
if (index < 0) index = at(/<html(?:\s[^>]*)?>/i);
|
|
7797
|
+
if (index < 0) index = at(/<!doctype[^>]*>/i);
|
|
7798
|
+
if (index < 0) index = 0;
|
|
7799
|
+
return html.slice(0, index) + tag + html.slice(index);
|
|
7800
|
+
}
|
|
7801
|
+
var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
7802
|
+
function htmlTitle(html) {
|
|
7803
|
+
const raw = /<title(?:\s[^>]*)?>([\s\S]*?)<\/title>/i.exec(html)?.[1];
|
|
7804
|
+
if (raw === void 0) return null;
|
|
7805
|
+
const text2 = raw.replace(/<[^>]*>/g, "").replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, body) => {
|
|
7806
|
+
if (body[0] !== "#") return ENTITIES[body.toLowerCase()] ?? whole;
|
|
7807
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number(body.slice(1));
|
|
7808
|
+
return Number.isFinite(code) && code > 0 && code < 1114112 ? String.fromCodePoint(code) : " ";
|
|
7809
|
+
}).replace(/\s+/g, " ").trim().slice(0, MAX_TITLE);
|
|
7810
|
+
return text2 || null;
|
|
7811
|
+
}
|
|
7812
|
+
function siblingsLoaded(file, html) {
|
|
7813
|
+
const dir = path35.dirname(file);
|
|
7814
|
+
const found = /* @__PURE__ */ new Set();
|
|
7815
|
+
for (const match of html.matchAll(/\b(?:src|href|poster)\s*=\s*["']([^"'#?]+)/gi)) {
|
|
7816
|
+
const ref = match[1].trim();
|
|
7817
|
+
if (!ref || /^[a-z][a-z0-9+.-]*:/i.test(ref) || ref.startsWith("/") || ref.startsWith("//")) continue;
|
|
7818
|
+
let rel;
|
|
7819
|
+
try {
|
|
7820
|
+
rel = decodeURIComponent(ref);
|
|
7821
|
+
} catch {
|
|
7822
|
+
continue;
|
|
7823
|
+
}
|
|
7824
|
+
const full = path35.resolve(dir, rel);
|
|
7825
|
+
if (full !== file && fs36.existsSync(full) && fs36.statSync(full).isFile()) found.add(rel);
|
|
7826
|
+
}
|
|
7827
|
+
return [...found];
|
|
7828
|
+
}
|
|
7829
|
+
function walk2(dir, root, out, skipped) {
|
|
7830
|
+
for (const entry of fs36.readdirSync(dir, { withFileTypes: true })) {
|
|
7831
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
7832
|
+
const full = path35.join(dir, entry.name);
|
|
7833
|
+
const rel = path35.relative(root, full).split(path35.sep).join("/");
|
|
7834
|
+
if (entry.isSymbolicLink()) {
|
|
7835
|
+
try {
|
|
7836
|
+
const target = fs36.realpathSync(full);
|
|
7837
|
+
if (target.startsWith(fs36.realpathSync(root) + path35.sep) && fs36.statSync(target).isFile()) out.push(rel);
|
|
7838
|
+
else skipped.push(rel);
|
|
7839
|
+
} catch {
|
|
7840
|
+
skipped.push(rel);
|
|
7841
|
+
}
|
|
7842
|
+
} else if (entry.isDirectory()) walk2(full, root, out, skipped);
|
|
7843
|
+
else if (entry.isFile()) out.push(rel);
|
|
7844
|
+
}
|
|
7845
|
+
}
|
|
7846
|
+
function readPage(target) {
|
|
7847
|
+
const full = path35.resolve(target);
|
|
7848
|
+
let stat;
|
|
7849
|
+
try {
|
|
7850
|
+
stat = fs36.statSync(full);
|
|
7851
|
+
} catch {
|
|
7852
|
+
throw new CliError(`${target} does not exist`);
|
|
7853
|
+
}
|
|
7854
|
+
const files = /* @__PURE__ */ new Map();
|
|
7855
|
+
const warnings = [];
|
|
7856
|
+
const add = (rel, file) => {
|
|
7857
|
+
const data = fs36.readFileSync(file);
|
|
7858
|
+
files.set(rel, HTML.test(rel) ? Buffer.from(withPageScript(data.toString("utf8"))) : data);
|
|
7859
|
+
};
|
|
7860
|
+
let entryHtml;
|
|
7861
|
+
if (stat.isDirectory()) {
|
|
7862
|
+
const list = [];
|
|
7863
|
+
const skipped = [];
|
|
7864
|
+
walk2(full, full, list, skipped);
|
|
7865
|
+
if (!list.includes(PAGE_ENTRY)) {
|
|
7866
|
+
const pages = list.filter((rel) => HTML.test(rel) && !rel.includes("/"));
|
|
7867
|
+
throw new CliError(
|
|
7868
|
+
`${target} has no ${PAGE_ENTRY}, which a page opens with.` + (pages.length === 1 ? ` Rename ${pages[0]} to ${PAGE_ENTRY}, or pass that file alone.` : "")
|
|
7869
|
+
);
|
|
7870
|
+
}
|
|
7871
|
+
for (const rel of list) add(rel, path35.join(full, rel));
|
|
7872
|
+
if (skipped.length) warnings.push(`Not uploaded, they link outside ${target}: ${skipped.slice(0, 5).join(", ")}`);
|
|
7873
|
+
entryHtml = fs36.readFileSync(path35.join(full, PAGE_ENTRY), "utf8");
|
|
7874
|
+
} else {
|
|
7875
|
+
if (!HTML.test(full))
|
|
7876
|
+
throw new CliError(`${target} is not an HTML file; pass an .html file or a folder with ${PAGE_ENTRY}`);
|
|
7877
|
+
entryHtml = fs36.readFileSync(full, "utf8");
|
|
7878
|
+
add(PAGE_ENTRY, full);
|
|
7879
|
+
const loaded = siblingsLoaded(full, entryHtml);
|
|
7880
|
+
if (loaded.length)
|
|
7881
|
+
warnings.push(
|
|
7882
|
+
`${path35.basename(full)} loads ${loaded.slice(0, 3).join(", ")}${loaded.length > 3 ? ` and ${loaded.length - 3} more` : ""} next to it, which a single file does not take along: pass the folder (with the page as ${PAGE_ENTRY}) instead.`
|
|
7883
|
+
);
|
|
7884
|
+
}
|
|
7885
|
+
const manifest = {};
|
|
7886
|
+
let bytes = 0;
|
|
7887
|
+
for (const [rel, data] of files) {
|
|
7888
|
+
manifest[rel] = { hash: hashBuffer(data), size: data.length };
|
|
7889
|
+
bytes += data.length;
|
|
7890
|
+
}
|
|
7891
|
+
return { files, manifest, title: htmlTitle(entryHtml), bytes, warnings };
|
|
7892
|
+
}
|
|
7893
|
+
|
|
7894
|
+
// src/cli/commands/pages.ts
|
|
7895
|
+
var DEFAULT_NAME = "Pages";
|
|
7896
|
+
var base = (host) => host.replace(/\/+$/, "");
|
|
7897
|
+
var size2 = (bytes) => bytes < 1024 * 1024 ? `${Math.max(1, Math.round(bytes / 1024))} KB` : megabytes(bytes);
|
|
7898
|
+
var pageUrl = (host, project, slug2) => `${base(host)}/p/${project}/pages/${encodeURIComponent(slug2)}`;
|
|
7899
|
+
var publicUrl2 = (host, publicId) => `${base(host)}/a/${publicId}`;
|
|
7900
|
+
function client() {
|
|
7901
|
+
const host = cloudHost();
|
|
7902
|
+
return { host, client: new CloudClient(host, requireCredential(host).token) };
|
|
7903
|
+
}
|
|
7904
|
+
async function pagesProject(client2, wanted, options) {
|
|
7905
|
+
const { owned, shared } = await client2.projects();
|
|
7906
|
+
if (wanted) {
|
|
7907
|
+
const all = [...owned, ...shared];
|
|
7908
|
+
const byName = all.filter((project) => project.name === wanted);
|
|
7909
|
+
const found = all.find((project) => project.id === wanted) ?? (byName.length === 1 ? byName[0] : void 0);
|
|
7910
|
+
if (!found)
|
|
7911
|
+
throw new CliError(
|
|
7912
|
+
byName.length > 1 ? `Several projects are called "${wanted}"; pass its id (\`design pages\` names them).` : `No project "${wanted}" among yours on ${client2.host}.`
|
|
7913
|
+
);
|
|
7914
|
+
if (found.kind !== "pages")
|
|
7915
|
+
throw new CliError(`${found.name} holds canvases, not pages; pages go to a pages project.`);
|
|
7916
|
+
return found;
|
|
7917
|
+
}
|
|
7918
|
+
const mine = owned.filter((project) => project.kind === "pages" && !project.archived);
|
|
7919
|
+
if (mine.length === 1) return mine[0];
|
|
7920
|
+
if (mine.length > 1)
|
|
7921
|
+
throw new CliError(
|
|
7922
|
+
`You have several pages projects; pass one with --project:
|
|
7923
|
+
${mine.map((p) => ` ${p.id} ${p.name}`).join("\n")}`
|
|
7924
|
+
);
|
|
7925
|
+
if (!options.create) return null;
|
|
7926
|
+
const made = await client2.createProject(DEFAULT_NAME, "pages");
|
|
7927
|
+
if (made.kind !== "pages") throw new CliError(`${client2.host} does not host pages yet.`);
|
|
7928
|
+
options.log(`${green("created")} the pages project ${bold(made.name)} ${dim(`(${made.id})`)}`);
|
|
7929
|
+
return made;
|
|
7930
|
+
}
|
|
7931
|
+
function check(project) {
|
|
7932
|
+
if (project.banned) throw new CliError(`${project.name} is blocked by moderation.`);
|
|
7933
|
+
if (project.archived) throw new CliError(`${project.name} is archived; unarchive it in the web app first.`);
|
|
7934
|
+
if (project.role === "viewer") throw new CliError(`You can view ${project.name} but not publish to it.`);
|
|
7935
|
+
}
|
|
7936
|
+
async function runPage(target, options) {
|
|
7937
|
+
if (!target) throw new CliError("Usage: design page <file.html|folder> [--name <slug>] [--public]");
|
|
7938
|
+
const slug2 = options.name ?? slugFor(target);
|
|
7939
|
+
if (!PAGE_SLUG.test(slug2))
|
|
7940
|
+
throw new CliError(`--name takes lowercase letters, digits, - and _ (up to 100, starting with a letter or digit)`);
|
|
7941
|
+
const bundle = readPage(target);
|
|
7942
|
+
const log = options.json ? () => {
|
|
7943
|
+
} : (line) => print(line);
|
|
7944
|
+
for (const warning of bundle.warnings) log(yellow(warning));
|
|
7945
|
+
const { host, client: cloud2 } = client();
|
|
7946
|
+
const project = await pagesProject(cloud2, options.project, { create: true, log });
|
|
7947
|
+
check(project);
|
|
7948
|
+
if (options.public && project.role !== "owner")
|
|
7949
|
+
throw new CliError(`Only the owner of ${project.name} gives pages a public link; publish without --public.`);
|
|
7950
|
+
const title = options.title?.trim() || bundle.title || slug2;
|
|
7951
|
+
const byHash = new Map([...bundle.files].map(([rel, data2]) => [bundle.manifest[rel].hash, data2]));
|
|
7952
|
+
const missing = await cloud2.missing(project.id, [...byHash.keys()]);
|
|
7953
|
+
if (missing.length) {
|
|
7954
|
+
const bar = options.json ? null : progress();
|
|
7955
|
+
let sent = 0;
|
|
7956
|
+
await mapLimit(missing, TRANSFERS, async (hash) => {
|
|
7957
|
+
const data2 = byHash.get(hash);
|
|
7958
|
+
if (!data2) throw new CliError(`The cloud asked for ${hash.slice(0, 12)}, which is not part of this page`);
|
|
7959
|
+
await cloud2.putBlob(project.id, hash, data2);
|
|
7960
|
+
bar?.update(`Uploading ${++sent}/${missing.length} files`);
|
|
7961
|
+
});
|
|
7962
|
+
bar?.done(`Uploaded ${plural(missing.length, "file")}`);
|
|
7963
|
+
}
|
|
7964
|
+
let rev = 0;
|
|
7965
|
+
let page;
|
|
7966
|
+
for (let attempt = 0; ; attempt++) {
|
|
7967
|
+
page = (await cloud2.pages(project.id)).find((item) => item.slug === slug2);
|
|
7968
|
+
if (page?.banned) throw new CliError(`The page ${slug2} is blocked by moderation.`);
|
|
7969
|
+
if (page?.archived) throw new CliError(`The page ${slug2} is archived; unarchive it in the web app first.`);
|
|
7970
|
+
try {
|
|
7971
|
+
const revs2 = await cloud2.push(project.id, [
|
|
7972
|
+
{
|
|
7973
|
+
key: `page/${slug2}`,
|
|
7974
|
+
baseRev: page?.headRev ?? 0,
|
|
7975
|
+
title,
|
|
7976
|
+
source: {},
|
|
7977
|
+
build: bundle.manifest,
|
|
7978
|
+
systemRev: null
|
|
7979
|
+
}
|
|
7980
|
+
]);
|
|
7981
|
+
rev = revs2.get(`page/${slug2}`) ?? (page?.headRev ?? 0) + 1;
|
|
7982
|
+
break;
|
|
7983
|
+
} catch (error) {
|
|
7984
|
+
if (attempt < 2 && error instanceof CloudError && error.status === 409 && error.error === "conflict") continue;
|
|
7985
|
+
throw error;
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
const publicId = options.public ? (await cloud2.pageAction(project.id, slug2, "publish")).publicId : page?.publicId;
|
|
7989
|
+
const data = {
|
|
7990
|
+
ok: true,
|
|
7991
|
+
project: { id: project.id, name: project.name },
|
|
7992
|
+
page: { slug: slug2, title, rev, bytes: bundle.bytes, files: bundle.files.size },
|
|
7993
|
+
url: pageUrl(host, project.id, slug2),
|
|
7994
|
+
publicUrl: publicId ? publicUrl2(host, publicId) : null,
|
|
7995
|
+
warnings: bundle.warnings
|
|
7996
|
+
};
|
|
7997
|
+
if (options.json) return print(JSON.stringify(data, null, 2));
|
|
7998
|
+
print(
|
|
7999
|
+
`${green("\u2713")} ${rev > 1 ? "Updated" : "Published"} ${bold(title)} ${dim(`(${slug2}, r${rev}, ${plural(bundle.files.size, "file")}, ${size2(bundle.bytes)})`)} in ${project.name}`
|
|
8000
|
+
);
|
|
8001
|
+
print(` ${dim("Page ")} ${data.url}`);
|
|
8002
|
+
if (data.publicUrl) print(` ${dim("Public")} ${data.publicUrl}`);
|
|
8003
|
+
else
|
|
8004
|
+
print(
|
|
8005
|
+
dim(
|
|
8006
|
+
project.role === "owner" ? " Only the project\u2019s members see it; --public gives it a link anyone can open." : " Only the project\u2019s members see it; its owner can give it a public link."
|
|
8007
|
+
)
|
|
8008
|
+
);
|
|
8009
|
+
}
|
|
8010
|
+
async function runPages(options) {
|
|
8011
|
+
const { host, client: cloud2 } = client();
|
|
8012
|
+
const project = await pagesProject(cloud2, options.project, { create: false, log: () => {
|
|
8013
|
+
} });
|
|
8014
|
+
if (!project) {
|
|
8015
|
+
if (options.json) return print(JSON.stringify({ project: null, pages: [] }, null, 2));
|
|
8016
|
+
return print(dim("No pages project yet: `design page <file.html>` makes one and publishes the page."));
|
|
8017
|
+
}
|
|
8018
|
+
const pages = await cloud2.pages(project.id);
|
|
8019
|
+
if (options.json)
|
|
8020
|
+
return print(
|
|
8021
|
+
JSON.stringify(
|
|
8022
|
+
{
|
|
8023
|
+
project: { id: project.id, name: project.name, role: project.role },
|
|
8024
|
+
pages: pages.map((page) => ({
|
|
8025
|
+
...page,
|
|
8026
|
+
url: pageUrl(host, project.id, page.slug),
|
|
8027
|
+
publicUrl: page.publicId ? publicUrl2(host, page.publicId) : null
|
|
8028
|
+
}))
|
|
8029
|
+
},
|
|
8030
|
+
null,
|
|
8031
|
+
2
|
|
8032
|
+
)
|
|
8033
|
+
);
|
|
8034
|
+
print(`${bold(project.name)} ${dim(`${plural(pages.length, "page")} \xB7 ${base(host)}/p/${project.id}`)}`);
|
|
8035
|
+
if (!pages.length) return print(dim(" none yet: `design page <file.html>` publishes one"));
|
|
8036
|
+
const width = Math.max(...pages.map((page) => page.slug.length));
|
|
8037
|
+
for (const page of pages) {
|
|
8038
|
+
const state = page.banned ? yellow("blocked") : page.archived ? yellow("archived") : "";
|
|
8039
|
+
print(
|
|
8040
|
+
` ${page.slug.padEnd(width)} ${page.title} ${dim(`r${page.headRev} \xB7 ${size2(page.bytes)} \xB7 ${ago(page.updatedAt)}`)}${state ? ` ${state}` : ""}`
|
|
8041
|
+
);
|
|
8042
|
+
if (page.publicId) print(` ${" ".repeat(width)} ${dim("public")} ${publicUrl2(host, page.publicId)}`);
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
|
|
8046
|
+
// src/cli/commands/rename.ts
|
|
8047
|
+
import fs37 from "fs";
|
|
7491
8048
|
async function runRename(paths, name) {
|
|
7492
8049
|
const trimmed = name.trim();
|
|
7493
8050
|
if (!trimmed) throw new CliError("Usage: design rename <name>");
|
|
7494
8051
|
const link = readLink(paths);
|
|
7495
8052
|
if (link) {
|
|
7496
|
-
const
|
|
7497
|
-
const project = await
|
|
8053
|
+
const client2 = new CloudClient(link.host, requireCredential(link.host).token);
|
|
8054
|
+
const project = await client2.renameProject(link.project, trimmed);
|
|
7498
8055
|
link.name = project.name;
|
|
7499
8056
|
writeLink(paths, link);
|
|
7500
8057
|
print(`${green("Renamed")} the cloud project to ${project.name}`);
|
|
@@ -7502,17 +8059,17 @@ async function runRename(paths, name) {
|
|
|
7502
8059
|
}
|
|
7503
8060
|
let config = {};
|
|
7504
8061
|
try {
|
|
7505
|
-
config = JSON.parse(
|
|
8062
|
+
config = JSON.parse(fs37.readFileSync(paths.config, "utf8"));
|
|
7506
8063
|
} catch {
|
|
7507
8064
|
}
|
|
7508
|
-
|
|
8065
|
+
fs37.writeFileSync(paths.config, `${JSON.stringify({ ...config, name: trimmed }, null, 2)}
|
|
7509
8066
|
`);
|
|
7510
8067
|
print(`${green("Renamed")} the project to ${trimmed} (.design/design.json)`);
|
|
7511
8068
|
}
|
|
7512
8069
|
|
|
7513
8070
|
// src/cli/commands/scaffold.ts
|
|
7514
|
-
import
|
|
7515
|
-
import
|
|
8071
|
+
import fs38 from "fs";
|
|
8072
|
+
import path36 from "path";
|
|
7516
8073
|
|
|
7517
8074
|
// src/cli/templates.ts
|
|
7518
8075
|
var TOKENS_CSS = `/*
|
|
@@ -7723,40 +8280,40 @@ function mainScreen(canvasId) {
|
|
|
7723
8280
|
|
|
7724
8281
|
// src/cli/commands/scaffold.ts
|
|
7725
8282
|
function write(file, content, root) {
|
|
7726
|
-
if (
|
|
7727
|
-
print(` ${dim("exists ")} ${
|
|
8283
|
+
if (fs38.existsSync(file)) {
|
|
8284
|
+
print(` ${dim("exists ")} ${path36.relative(root, file)}`);
|
|
7728
8285
|
return false;
|
|
7729
8286
|
}
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
print(` ${green("created")} ${
|
|
8287
|
+
fs38.mkdirSync(path36.dirname(file), { recursive: true });
|
|
8288
|
+
fs38.writeFileSync(file, content);
|
|
8289
|
+
print(` ${green("created")} ${path36.relative(root, file)}`);
|
|
7733
8290
|
return true;
|
|
7734
8291
|
}
|
|
7735
8292
|
function ensureGitignored(root) {
|
|
7736
|
-
const file =
|
|
7737
|
-
const current =
|
|
8293
|
+
const file = path36.join(root, ".gitignore");
|
|
8294
|
+
const current = fs38.existsSync(file) ? fs38.readFileSync(file, "utf8") : "";
|
|
7738
8295
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
7739
8296
|
if (covered) return "present";
|
|
7740
8297
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
7741
|
-
|
|
8298
|
+
fs38.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
7742
8299
|
.design/
|
|
7743
8300
|
`);
|
|
7744
8301
|
return "added";
|
|
7745
8302
|
}
|
|
7746
8303
|
function projectName(root) {
|
|
7747
8304
|
try {
|
|
7748
|
-
const manifest = JSON.parse(
|
|
8305
|
+
const manifest = JSON.parse(fs38.readFileSync(path36.join(root, "package.json"), "utf8"));
|
|
7749
8306
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
7750
8307
|
} catch {
|
|
7751
8308
|
}
|
|
7752
|
-
return
|
|
8309
|
+
return path36.basename(root);
|
|
7753
8310
|
}
|
|
7754
8311
|
function runInit(root, options) {
|
|
7755
8312
|
const paths = designPaths(root);
|
|
7756
8313
|
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
7757
8314
|
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
7758
8315
|
`, root);
|
|
7759
|
-
|
|
8316
|
+
fs38.mkdirSync(paths.canvases, { recursive: true });
|
|
7760
8317
|
if (options.gitignore) {
|
|
7761
8318
|
const result = ensureGitignored(root);
|
|
7762
8319
|
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
@@ -7768,46 +8325,46 @@ function runInit(root, options) {
|
|
|
7768
8325
|
}
|
|
7769
8326
|
function runSystemInit(root, options) {
|
|
7770
8327
|
const paths = designPaths(root);
|
|
7771
|
-
let
|
|
8328
|
+
let base2 = projectName(root);
|
|
7772
8329
|
try {
|
|
7773
|
-
|
|
8330
|
+
base2 = JSON.parse(fs38.readFileSync(paths.config, "utf8")).name || base2;
|
|
7774
8331
|
} catch {
|
|
7775
8332
|
}
|
|
7776
|
-
const name = options.name ?? `${
|
|
7777
|
-
print(`Scaffolding the design system in ${
|
|
7778
|
-
write(
|
|
8333
|
+
const name = options.name ?? `${base2} design system`;
|
|
8334
|
+
print(`Scaffolding the design system in ${path36.relative(root, paths.system)}`);
|
|
8335
|
+
write(path36.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
7779
8336
|
`, root);
|
|
7780
|
-
write(
|
|
8337
|
+
write(path36.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
7781
8338
|
if (!options.empty) {
|
|
7782
|
-
write(
|
|
7783
|
-
write(
|
|
7784
|
-
write(
|
|
8339
|
+
write(path36.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
8340
|
+
write(path36.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
8341
|
+
write(path36.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
7785
8342
|
}
|
|
7786
8343
|
}
|
|
7787
8344
|
function runNew(root, id2, options) {
|
|
7788
8345
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
7789
8346
|
const paths = designPaths(root);
|
|
7790
|
-
const dir =
|
|
7791
|
-
if (
|
|
8347
|
+
const dir = path36.join(paths.canvases, id2);
|
|
8348
|
+
if (fs38.existsSync(path36.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
7792
8349
|
print(`Creating canvas "${id2}"`);
|
|
7793
8350
|
const title = options.title ?? titleize(id2);
|
|
7794
8351
|
if (options.empty) {
|
|
7795
8352
|
write(
|
|
7796
|
-
|
|
8353
|
+
path36.join(dir, "canvas.json"),
|
|
7797
8354
|
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
7798
8355
|
`,
|
|
7799
8356
|
root
|
|
7800
8357
|
);
|
|
7801
|
-
|
|
8358
|
+
fs38.mkdirSync(path36.join(dir, "screens"), { recursive: true });
|
|
7802
8359
|
} else {
|
|
7803
|
-
write(
|
|
7804
|
-
write(
|
|
8360
|
+
write(path36.join(dir, "canvas.json"), canvasJson(title), root);
|
|
8361
|
+
write(path36.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
7805
8362
|
}
|
|
7806
8363
|
}
|
|
7807
8364
|
|
|
7808
8365
|
// src/cli/commands/shot.ts
|
|
7809
|
-
import
|
|
7810
|
-
import
|
|
8366
|
+
import fs39 from "fs";
|
|
8367
|
+
import path37 from "path";
|
|
7811
8368
|
var OVERVIEW_TIMEOUT = 9e4;
|
|
7812
8369
|
var DRAWING_TIMEOUT = 45e3;
|
|
7813
8370
|
var DRAWING_MAX_SIDE = 1280;
|
|
@@ -7833,16 +8390,16 @@ async function runShot(paths, target, options) {
|
|
|
7833
8390
|
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
7834
8391
|
}
|
|
7835
8392
|
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
7836
|
-
const outBase =
|
|
7837
|
-
const outDir =
|
|
7838
|
-
|
|
7839
|
-
const
|
|
8393
|
+
const outBase = path37.resolve(options.out ?? paths.shots);
|
|
8394
|
+
const outDir = path37.join(outBase, canvasId);
|
|
8395
|
+
fs39.mkdirSync(outDir, { recursive: true });
|
|
8396
|
+
const base2 = await serverBase(paths);
|
|
7840
8397
|
const browser = await launchChrome();
|
|
7841
8398
|
let failed = false;
|
|
7842
8399
|
try {
|
|
7843
8400
|
if (options.board || options.markup) {
|
|
7844
|
-
if (!
|
|
7845
|
-
const drawingsFailed = await shootDrawings(browser,
|
|
8401
|
+
if (!fs39.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there are no drawings to shoot.");
|
|
8402
|
+
const drawingsFailed = await shootDrawings(browser, base2, canvasId, outDir, theme2, {
|
|
7846
8403
|
board: !!options.board,
|
|
7847
8404
|
page: options.page,
|
|
7848
8405
|
screen: screenId,
|
|
@@ -7852,11 +8409,11 @@ async function runShot(paths, target, options) {
|
|
|
7852
8409
|
return;
|
|
7853
8410
|
}
|
|
7854
8411
|
if (options.overview) {
|
|
7855
|
-
if (!
|
|
8412
|
+
if (!fs39.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
7856
8413
|
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);
|
|
7857
8414
|
for (const pageId of pages) {
|
|
7858
8415
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
7859
|
-
const url = `${
|
|
8416
|
+
const url = `${base2}/c/${encodeURIComponent(canvasId)}?page=${encodeURIComponent(pageId)}&capture=1&theme=${theme2}`;
|
|
7860
8417
|
await page.goto(url, { waitUntil: "load" });
|
|
7861
8418
|
try {
|
|
7862
8419
|
await page.waitForFunction(
|
|
@@ -7868,14 +8425,14 @@ async function runShot(paths, target, options) {
|
|
|
7868
8425
|
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
7869
8426
|
}
|
|
7870
8427
|
await page.waitForTimeout(400);
|
|
7871
|
-
const file =
|
|
8428
|
+
const file = path37.join(outDir, `${pageId}.overview.png`);
|
|
7872
8429
|
await page.screenshot({ path: file });
|
|
7873
8430
|
await page.close();
|
|
7874
8431
|
print(file);
|
|
7875
8432
|
}
|
|
7876
8433
|
return;
|
|
7877
8434
|
}
|
|
7878
|
-
const jobs = canvasFrames(doc,
|
|
8435
|
+
const jobs = canvasFrames(doc, base2, theme2, { page: options.page, id: screenId });
|
|
7879
8436
|
if (!jobs.length) {
|
|
7880
8437
|
throw new CliError(
|
|
7881
8438
|
screenId ? `No screen "${screenId}" in "${canvasId}"${options.page ? ` on page "${options.page}"` : ""}.` : `Nothing to shoot in "${canvasId}"${options.page ? ` on page "${options.page}"` : ""}.`
|
|
@@ -7887,8 +8444,8 @@ async function runShot(paths, target, options) {
|
|
|
7887
8444
|
}
|
|
7888
8445
|
if (failed) process.exitCode = 1;
|
|
7889
8446
|
}
|
|
7890
|
-
async function shootDrawings(browser,
|
|
7891
|
-
const port = Number(new URL(
|
|
8447
|
+
async function shootDrawings(browser, base2, canvasId, outDir, theme2, want) {
|
|
8448
|
+
const port = Number(new URL(base2).port);
|
|
7892
8449
|
const { drawings, cloud: cloud2 } = await readDrawings(port, canvasId);
|
|
7893
8450
|
if (cloud2 === "offline" || cloud2 === "connecting")
|
|
7894
8451
|
warn("the cloud did not answer in time: these are the drawings on this machine");
|
|
@@ -7906,21 +8463,21 @@ async function shootDrawings(browser, base, canvasId, outDir, theme2, want) {
|
|
|
7906
8463
|
const kind = want.board ? "board" : "markup";
|
|
7907
8464
|
const page = await browser.newPage({ viewport: { width: 1280, height: 900 }, colorScheme: theme2 });
|
|
7908
8465
|
try {
|
|
7909
|
-
const url = `${
|
|
8466
|
+
const url = `${base2}/c/${encodeURIComponent(canvasId)}/${kind}/${encodeURIComponent(id2)}?capture=1&theme=${theme2}&max=${want.max}`;
|
|
7910
8467
|
await page.goto(url, { waitUntil: "load" });
|
|
7911
8468
|
await page.waitForFunction(
|
|
7912
8469
|
() => window.__DESIGN_DRAWING_READY__ === true,
|
|
7913
8470
|
null,
|
|
7914
8471
|
{ timeout: DRAWING_TIMEOUT }
|
|
7915
8472
|
);
|
|
7916
|
-
const
|
|
8473
|
+
const size3 = await page.evaluate(
|
|
7917
8474
|
() => window.__DESIGN_DRAWING_SIZE__
|
|
7918
8475
|
);
|
|
7919
|
-
if (
|
|
7920
|
-
await page.setViewportSize({ width: Math.round(
|
|
8476
|
+
if (size3?.width && size3.height)
|
|
8477
|
+
await page.setViewportSize({ width: Math.round(size3.width), height: Math.min(Math.round(size3.height), 16e3) });
|
|
7921
8478
|
await page.waitForTimeout(200);
|
|
7922
8479
|
if (!want.board) {
|
|
7923
|
-
const file =
|
|
8480
|
+
const file = path37.join(outDir, `${id2}.markup.${theme2}.png`);
|
|
7924
8481
|
await page.screenshot({ path: file });
|
|
7925
8482
|
print(file);
|
|
7926
8483
|
continue;
|
|
@@ -7929,7 +8486,7 @@ async function shootDrawings(browser, base, canvasId, outDir, theme2, want) {
|
|
|
7929
8486
|
() => window.__DESIGN_DRAWING_PARTS__ ?? 0
|
|
7930
8487
|
);
|
|
7931
8488
|
for (let part = 0; part < parts; part++) {
|
|
7932
|
-
const file =
|
|
8489
|
+
const file = path37.join(outDir, parts > 1 ? `${id2}.board.${part + 1}.${theme2}.png` : `${id2}.board.${theme2}.png`);
|
|
7933
8490
|
await page.locator(`[data-part="${part}"]`).screenshot({ path: file });
|
|
7934
8491
|
print(file);
|
|
7935
8492
|
}
|
|
@@ -7953,7 +8510,7 @@ async function shootFrames(browser, jobs, outDir) {
|
|
|
7953
8510
|
let failed = false;
|
|
7954
8511
|
const results = await mapLimit(jobs, 4, async (job) => {
|
|
7955
8512
|
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
7956
|
-
const file =
|
|
8513
|
+
const file = path37.join(outDir, `${job.id}.${job.theme}.png`);
|
|
7957
8514
|
try {
|
|
7958
8515
|
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
7959
8516
|
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
@@ -7985,10 +8542,10 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
7985
8542
|
if (id2 && !all.some((job) => job.id === id2)) {
|
|
7986
8543
|
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
7987
8544
|
}
|
|
7988
|
-
const outDir =
|
|
7989
|
-
|
|
7990
|
-
const
|
|
7991
|
-
const jobs = systemFrames(system.doc,
|
|
8545
|
+
const outDir = path37.join(path37.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
8546
|
+
fs39.mkdirSync(outDir, { recursive: true });
|
|
8547
|
+
const base2 = await serverBase(paths);
|
|
8548
|
+
const jobs = systemFrames(system.doc, base2, theme2).filter((job) => !id2 || job.id === id2);
|
|
7992
8549
|
const browser = await launchChrome();
|
|
7993
8550
|
try {
|
|
7994
8551
|
if (await shootFrames(browser, jobs, outDir)) process.exitCode = 1;
|
|
@@ -8036,7 +8593,7 @@ async function runSnapshots(paths, targets2, options) {
|
|
|
8036
8593
|
}
|
|
8037
8594
|
|
|
8038
8595
|
// src/cli/commands/cloud-status.ts
|
|
8039
|
-
import
|
|
8596
|
+
import path38 from "path";
|
|
8040
8597
|
var STATE_TEXT = {
|
|
8041
8598
|
in_sync: green,
|
|
8042
8599
|
local_changes: yellow,
|
|
@@ -8055,11 +8612,15 @@ function detail(paths, status) {
|
|
|
8055
8612
|
if (!status.baseRev && status.headRev === null) parts.push("new");
|
|
8056
8613
|
else if (!status.local) parts.push("deleted here");
|
|
8057
8614
|
else if (status.changed.length) parts.push(plural(status.changed.length, "file"));
|
|
8615
|
+
if (status.usesChanged.length)
|
|
8616
|
+
parts.push(
|
|
8617
|
+
`uses ${plural(status.usesChanged.length, "changed file")} from outside: ${status.usesChanged.slice(0, 2).join(", ")}${status.usesChanged.length > 2 ? ", \u2026" : ""}`
|
|
8618
|
+
);
|
|
8058
8619
|
}
|
|
8059
8620
|
if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
|
|
8060
8621
|
if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
|
|
8061
8622
|
parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
|
|
8062
|
-
if (status.conflictRev !== void 0) parts.push(`merge ${
|
|
8623
|
+
if (status.conflictRev !== void 0) parts.push(`merge ${path38.relative(paths.root, incomingDir(paths, status.key))}`);
|
|
8063
8624
|
return parts.join(" \xB7 ");
|
|
8064
8625
|
}
|
|
8065
8626
|
async function printCloudStatus(paths, label) {
|
|
@@ -8072,17 +8633,17 @@ async function printCloudStatus(paths, label) {
|
|
|
8072
8633
|
}
|
|
8073
8634
|
const host = link?.host ?? cloudHost();
|
|
8074
8635
|
const credential = getCredential(host);
|
|
8075
|
-
const
|
|
8636
|
+
const client2 = credential ? new CloudClient(host, credential.token) : null;
|
|
8076
8637
|
let remote = null;
|
|
8077
8638
|
let problem = null;
|
|
8078
|
-
if (!
|
|
8639
|
+
if (!client2 || !credential) {
|
|
8079
8640
|
problem = "not signed in";
|
|
8080
8641
|
print(`${label("Cloud")}${dim(`not signed in to ${host} \u2014 \`design login\``)}`);
|
|
8081
8642
|
} else {
|
|
8082
8643
|
try {
|
|
8083
|
-
const me = await
|
|
8644
|
+
const me = await client2.me();
|
|
8084
8645
|
print(`${label("Cloud")}${me.email || credential.email} ${dim(host)}`);
|
|
8085
|
-
if (link) remote = await
|
|
8646
|
+
if (link) remote = await client2.units(link.project);
|
|
8086
8647
|
} catch (error) {
|
|
8087
8648
|
problem = error.message;
|
|
8088
8649
|
if (error instanceof CloudError && error.status === 401)
|
|
@@ -8095,9 +8656,9 @@ async function printCloudStatus(paths, label) {
|
|
|
8095
8656
|
return;
|
|
8096
8657
|
}
|
|
8097
8658
|
let name = link.project;
|
|
8098
|
-
if (
|
|
8659
|
+
if (client2 && !problem) {
|
|
8099
8660
|
try {
|
|
8100
|
-
const { owned, shared } = await
|
|
8661
|
+
const { owned, shared } = await client2.projects();
|
|
8101
8662
|
const found = [...owned, ...shared].find((project) => project.id === link.project);
|
|
8102
8663
|
if (found) name = `${found.name} ${dim(`(${found.id}, ${found.role}${found.archived ? ", archived" : ""})`)}`;
|
|
8103
8664
|
} catch {
|
|
@@ -8107,7 +8668,7 @@ async function printCloudStatus(paths, label) {
|
|
|
8107
8668
|
if (problem) print(`${" ".repeat(9)}${yellow(`cloud state unknown (${problem}); local changes only`)}`);
|
|
8108
8669
|
const scan = scanLocal(paths);
|
|
8109
8670
|
const statuses = allUnitKeys(link, scan.units, remote).map(
|
|
8110
|
-
(key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote)
|
|
8671
|
+
(key) => unitStatus(key, link, scan.units.get(key)?.manifest, remote, paths.design)
|
|
8111
8672
|
);
|
|
8112
8673
|
if (!statuses.length) print(`${label("Units")}${dim("none yet")}`);
|
|
8113
8674
|
else {
|
|
@@ -8257,6 +8818,14 @@ ${bold("Cloud")} ${dim("units: system (design.json + system/) and canvas/<id>;
|
|
|
8257
8818
|
history kept
|
|
8258
8819
|
unarchive <canvas> Bring an archived canvas back
|
|
8259
8820
|
|
|
8821
|
+
${bold("Pages")} ${dim("HTML pages published to the cloud, each at its own link; no .design needed")}
|
|
8822
|
+
page <file.html|folder> [--name <slug>] Publish an HTML file, or a folder with an index.html and
|
|
8823
|
+
[--title <title>] [--project <id>] what it loads, as a page of your pages project (made on
|
|
8824
|
+
[--public] [--json] first use). The same name again is a new version at the
|
|
8825
|
+
same links. --public gives it a link anyone can open
|
|
8826
|
+
(owner only); links to other sites open in a new tab
|
|
8827
|
+
pages [--project <id>] [--json] The pages: name, title, version, size, public link
|
|
8828
|
+
|
|
8260
8829
|
${bold("Options")}
|
|
8261
8830
|
--root <dir> Project folder (default: the nearest folder with .design, or the current one)
|
|
8262
8831
|
-h, --help This help
|
|
@@ -8266,7 +8835,7 @@ ${bold("Environment")}
|
|
|
8266
8835
|
FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
|
|
8267
8836
|
`;
|
|
8268
8837
|
function projectPaths(rootOption, requireDesign = true) {
|
|
8269
|
-
const found = rootOption ?
|
|
8838
|
+
const found = rootOption ? path39.resolve(rootOption) : findProjectRoot();
|
|
8270
8839
|
if (!found) {
|
|
8271
8840
|
if (!requireDesign) return designPaths(process.cwd());
|
|
8272
8841
|
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
@@ -8318,6 +8887,8 @@ async function main(argv) {
|
|
|
8318
8887
|
full: { type: "boolean" },
|
|
8319
8888
|
tar: { type: "boolean" },
|
|
8320
8889
|
new: { type: "string" },
|
|
8890
|
+
project: { type: "string" },
|
|
8891
|
+
public: { type: "boolean" },
|
|
8321
8892
|
resolved: { type: "string", multiple: true },
|
|
8322
8893
|
theirs: { type: "string", multiple: true },
|
|
8323
8894
|
help: { type: "boolean", short: "h" },
|
|
@@ -8333,7 +8904,7 @@ async function main(argv) {
|
|
|
8333
8904
|
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
8334
8905
|
switch (command) {
|
|
8335
8906
|
case "init":
|
|
8336
|
-
return runInit(values.root ?
|
|
8907
|
+
return runInit(values.root ? path39.resolve(values.root) : process.cwd(), {
|
|
8337
8908
|
name: values.name,
|
|
8338
8909
|
gitignore: !values["no-gitignore"]
|
|
8339
8910
|
});
|
|
@@ -8430,6 +9001,17 @@ async function main(argv) {
|
|
|
8430
9001
|
case "archive":
|
|
8431
9002
|
case "unarchive":
|
|
8432
9003
|
return runArchive(projectPaths(values.root), rest[0], command === "archive");
|
|
9004
|
+
case "page":
|
|
9005
|
+
if (rest.length > 1) throw new CliError("Usage: design page <file.html|folder> [--name <slug>] [--public]");
|
|
9006
|
+
return runPage(rest[0], {
|
|
9007
|
+
name: values.name,
|
|
9008
|
+
title: values.title,
|
|
9009
|
+
project: values.project,
|
|
9010
|
+
public: !!values.public,
|
|
9011
|
+
json: !!values.json
|
|
9012
|
+
});
|
|
9013
|
+
case "pages":
|
|
9014
|
+
return runPages({ project: values.project, json: !!values.json });
|
|
8433
9015
|
case "push":
|
|
8434
9016
|
return runPush(projectPaths(values.root), rest, { resolved: values.resolved ?? [], json: !!values.json });
|
|
8435
9017
|
case "pull":
|
|
@@ -8440,18 +9022,18 @@ async function main(argv) {
|
|
|
8440
9022
|
}
|
|
8441
9023
|
function relaunchFromRealPath() {
|
|
8442
9024
|
if (process.platform !== "win32" || process.env.FOSS_DESIGN_RELAUNCHED) return false;
|
|
8443
|
-
const
|
|
8444
|
-
if (!
|
|
9025
|
+
const script2 = process.argv[1];
|
|
9026
|
+
if (!script2) return false;
|
|
8445
9027
|
let realScript;
|
|
8446
9028
|
let realCwd;
|
|
8447
9029
|
try {
|
|
8448
|
-
realScript =
|
|
8449
|
-
realCwd =
|
|
9030
|
+
realScript = fs40.realpathSync.native(script2);
|
|
9031
|
+
realCwd = fs40.realpathSync.native(process.cwd());
|
|
8450
9032
|
} catch {
|
|
8451
9033
|
return false;
|
|
8452
9034
|
}
|
|
8453
|
-
const same = (a, b) =>
|
|
8454
|
-
if (same(realScript,
|
|
9035
|
+
const same = (a, b) => path39.resolve(a).toLowerCase() === path39.resolve(b).toLowerCase();
|
|
9036
|
+
if (same(realScript, script2) && same(realCwd, process.cwd())) return false;
|
|
8455
9037
|
const result = spawnSync3(process.execPath, [...process.execArgv, realScript, ...process.argv.slice(2)], {
|
|
8456
9038
|
stdio: "inherit",
|
|
8457
9039
|
cwd: realCwd,
|