foss-design 0.6.1 → 0.8.0
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 +1337 -381
- package/dist/runtime/index.js +7 -2
- package/dist/viewer/assets/index-COUW2y60.js +158 -0
- package/dist/viewer/assets/index-CiqSYqfx.css +2 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer-lib/index.js +83 -5
- package/dist/viewer-lib/styles.css +9 -0
- package/dist/viewer-lib/types/src/shared/types.d.ts +2 -0
- package/dist/viewer-lib/types/viewer/src/lib/source.d.ts +2 -0
- package/package.json +1 -1
- package/dist/viewer/assets/index-BL2Cxj98.css +0 -2
- package/dist/viewer/assets/index-D6sHcSw6.js +0 -158
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import
|
|
4
|
+
import path30 from "path";
|
|
5
5
|
import { parseArgs } from "util";
|
|
6
6
|
|
|
7
7
|
// src/core/paths.ts
|
|
@@ -134,12 +134,12 @@ function resolveFrom(anchor, request) {
|
|
|
134
134
|
|
|
135
135
|
// src/cli/commands/build.ts
|
|
136
136
|
import { spawnSync } from "child_process";
|
|
137
|
-
import
|
|
138
|
-
import
|
|
137
|
+
import fs13 from "fs";
|
|
138
|
+
import path13 from "path";
|
|
139
139
|
|
|
140
140
|
// src/build/static.ts
|
|
141
|
-
import
|
|
142
|
-
import
|
|
141
|
+
import fs12 from "fs";
|
|
142
|
+
import path12 from "path";
|
|
143
143
|
import { build } from "vite";
|
|
144
144
|
|
|
145
145
|
// src/core/png.ts
|
|
@@ -307,8 +307,8 @@ function ensureThumb(file) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
// src/core/project.ts
|
|
310
|
-
import
|
|
311
|
-
import
|
|
310
|
+
import fs8 from "fs";
|
|
311
|
+
import path8 from "path";
|
|
312
312
|
|
|
313
313
|
// src/core/canvas.ts
|
|
314
314
|
import { createHash } from "crypto";
|
|
@@ -346,8 +346,8 @@ function routeError(pattern) {
|
|
|
346
346
|
}
|
|
347
347
|
return null;
|
|
348
348
|
}
|
|
349
|
-
function split(
|
|
350
|
-
return
|
|
349
|
+
function split(path31) {
|
|
350
|
+
return path31.split("/").filter(Boolean);
|
|
351
351
|
}
|
|
352
352
|
function compileRoute(pattern) {
|
|
353
353
|
return {
|
|
@@ -366,8 +366,8 @@ function decode2(part) {
|
|
|
366
366
|
return part;
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
-
function scoreRoute(route2,
|
|
370
|
-
const parts = split(
|
|
369
|
+
function scoreRoute(route2, path31) {
|
|
370
|
+
const parts = split(path31.split(/[?#]/)[0] ?? "").map(decode2);
|
|
371
371
|
const walk2 = (s, p) => {
|
|
372
372
|
if (s === route2.segments.length) return p === parts.length ? 0 : null;
|
|
373
373
|
const segment = route2.segments[s];
|
|
@@ -385,11 +385,11 @@ function scoreRoute(route2, path27) {
|
|
|
385
385
|
};
|
|
386
386
|
return walk2(0, 0);
|
|
387
387
|
}
|
|
388
|
-
function matchRoute(entries,
|
|
388
|
+
function matchRoute(entries, path31) {
|
|
389
389
|
let best = null;
|
|
390
390
|
for (const entry of entries) {
|
|
391
391
|
for (const route2 of entry.routes) {
|
|
392
|
-
const score = scoreRoute(route2,
|
|
392
|
+
const score = scoreRoute(route2, path31);
|
|
393
393
|
if (score !== null && (!best || score > best.score)) best = { score, value: entry.value };
|
|
394
394
|
}
|
|
395
395
|
}
|
|
@@ -426,6 +426,8 @@ var DesignConfigSchema = z.strictObject({
|
|
|
426
426
|
alias: z.record(z.string(), z.string()).optional(),
|
|
427
427
|
/** Extra folders Tailwind scans for class names, relative to the project root. */
|
|
428
428
|
sources: z.array(z.string()).optional(),
|
|
429
|
+
/** A monorepo's app package, relative to the project root: screens import its dependencies and its React. */
|
|
430
|
+
app: z.string().min(1).optional(),
|
|
429
431
|
/** A folder served at the root of every screen, like Vite's `public`, relative to the project root. */
|
|
430
432
|
public: z.string().min(1).optional(),
|
|
431
433
|
port: z.number().int().min(1).max(65535).optional()
|
|
@@ -512,9 +514,9 @@ var CanvasSchema = z.strictObject({
|
|
|
512
514
|
pages: z.array(z.unknown()).min(1, "add at least one page")
|
|
513
515
|
});
|
|
514
516
|
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
515
|
-
function formatPath(base,
|
|
517
|
+
function formatPath(base, path31) {
|
|
516
518
|
let out = base;
|
|
517
|
-
for (const part of
|
|
519
|
+
for (const part of path31) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
|
|
518
520
|
return out;
|
|
519
521
|
}
|
|
520
522
|
function describeZodError(error, base = "") {
|
|
@@ -831,33 +833,80 @@ function summarize(canvas) {
|
|
|
831
833
|
};
|
|
832
834
|
}
|
|
833
835
|
|
|
834
|
-
// src/core/
|
|
836
|
+
// src/core/icon.ts
|
|
835
837
|
import fs4 from "fs";
|
|
836
838
|
import path4 from "path";
|
|
839
|
+
var ICON_EXTENSIONS = ["svg", "png", "webp"];
|
|
840
|
+
var MAX_ICON_BYTES = 256 * 1024;
|
|
841
|
+
var iconName = (ext) => `icon.${ext}`;
|
|
842
|
+
function iconFile(paths) {
|
|
843
|
+
for (const ext of ICON_EXTENSIONS) {
|
|
844
|
+
const file = path4.join(paths.system, iconName(ext));
|
|
845
|
+
if (fs4.existsSync(file) && fs4.statSync(file).isFile()) return file;
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
function sniffIcon(data) {
|
|
850
|
+
if (data.length >= 8 && data.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
|
|
851
|
+
return "png";
|
|
852
|
+
if (data.length >= 12 && data.toString("latin1", 0, 4) === "RIFF" && data.toString("latin1", 8, 12) === "WEBP")
|
|
853
|
+
return "webp";
|
|
854
|
+
const head = data.subarray(0, 4096).toString("utf8").replace(/^/, "");
|
|
855
|
+
const rest = head.replace(/^(?:\s|<\?xml[\s\S]*?\?>|<!--[\s\S]*?-->|<!DOCTYPE[^>]*>)*/i, "");
|
|
856
|
+
return /^<svg[\s>]/i.test(rest) ? "svg" : null;
|
|
857
|
+
}
|
|
858
|
+
function iconProblem(data, ext) {
|
|
859
|
+
const kind = sniffIcon(data);
|
|
860
|
+
if (!kind) return "the icon must be an SVG, PNG or WebP image";
|
|
861
|
+
if (ext && ext !== kind) return `the file is ${kind.toUpperCase()}, not ${ext.toUpperCase()}`;
|
|
862
|
+
if (data.length > MAX_ICON_BYTES)
|
|
863
|
+
return `the icon is ${Math.ceil(data.length / 1024)} KB; the limit is ${MAX_ICON_BYTES / 1024} KB`;
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
function writeIcon(paths, data) {
|
|
867
|
+
for (const ext2 of ICON_EXTENSIONS) fs4.rmSync(path4.join(paths.system, iconName(ext2)), { force: true });
|
|
868
|
+
if (!data) return null;
|
|
869
|
+
const ext = sniffIcon(data);
|
|
870
|
+
fs4.mkdirSync(paths.system, { recursive: true });
|
|
871
|
+
const file = path4.join(paths.system, iconName(ext));
|
|
872
|
+
fs4.writeFileSync(file, data);
|
|
873
|
+
return file;
|
|
874
|
+
}
|
|
875
|
+
function onlyIcon(dir) {
|
|
876
|
+
try {
|
|
877
|
+
return fs4.readdirSync(dir).filter((name) => !name.startsWith(".")).every((name) => ICON_EXTENSIONS.some((ext) => name === iconName(ext)));
|
|
878
|
+
} catch {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/core/public.ts
|
|
884
|
+
import fs5 from "fs";
|
|
885
|
+
import path5 from "path";
|
|
837
886
|
function publicFiles(dir) {
|
|
838
887
|
const files = [];
|
|
839
888
|
let root;
|
|
840
889
|
try {
|
|
841
|
-
root =
|
|
890
|
+
root = fs5.realpathSync(dir);
|
|
842
891
|
} catch {
|
|
843
892
|
return files;
|
|
844
893
|
}
|
|
845
894
|
const visit = (current) => {
|
|
846
895
|
let entries;
|
|
847
896
|
try {
|
|
848
|
-
entries =
|
|
897
|
+
entries = fs5.readdirSync(current, { withFileTypes: true });
|
|
849
898
|
} catch {
|
|
850
899
|
return;
|
|
851
900
|
}
|
|
852
901
|
for (const entry of entries) {
|
|
853
902
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
854
|
-
const full =
|
|
903
|
+
const full = path5.join(current, entry.name);
|
|
855
904
|
if (entry.isDirectory()) visit(full);
|
|
856
|
-
else if (entry.isFile()) files.push(toPosix(
|
|
905
|
+
else if (entry.isFile()) files.push(toPosix(path5.relative(dir, full)));
|
|
857
906
|
else if (entry.isSymbolicLink()) {
|
|
858
907
|
try {
|
|
859
|
-
const target =
|
|
860
|
-
if (isInside(root, target) &&
|
|
908
|
+
const target = fs5.realpathSync(full);
|
|
909
|
+
if (isInside(root, target) && fs5.statSync(target).isFile()) files.push(toPosix(path5.relative(dir, full)));
|
|
861
910
|
} catch {
|
|
862
911
|
}
|
|
863
912
|
}
|
|
@@ -868,12 +917,12 @@ function publicFiles(dir) {
|
|
|
868
917
|
}
|
|
869
918
|
|
|
870
919
|
// src/core/system.ts
|
|
871
|
-
import
|
|
872
|
-
import
|
|
920
|
+
import fs7 from "fs";
|
|
921
|
+
import path7 from "path";
|
|
873
922
|
|
|
874
923
|
// src/core/tokens.ts
|
|
875
|
-
import
|
|
876
|
-
import
|
|
924
|
+
import fs6 from "fs";
|
|
925
|
+
import path6 from "path";
|
|
877
926
|
import postcss from "postcss";
|
|
878
927
|
var NAMESPACES = [
|
|
879
928
|
["--color-", "color"],
|
|
@@ -1032,12 +1081,12 @@ function readStylesheets(entry) {
|
|
|
1032
1081
|
const seen = /* @__PURE__ */ new Set();
|
|
1033
1082
|
const files = [];
|
|
1034
1083
|
const walk2 = (file) => {
|
|
1035
|
-
if (seen.has(file) || !
|
|
1084
|
+
if (seen.has(file) || !fs6.existsSync(file)) return;
|
|
1036
1085
|
seen.add(file);
|
|
1037
|
-
const css2 =
|
|
1086
|
+
const css2 = fs6.readFileSync(file, "utf8");
|
|
1038
1087
|
const imports = [...css2.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/g)].map((m) => m[1]);
|
|
1039
1088
|
for (const spec of imports) {
|
|
1040
|
-
if (spec.startsWith("./") || spec.startsWith("../")) walk2(
|
|
1089
|
+
if (spec.startsWith("./") || spec.startsWith("../")) walk2(path6.resolve(path6.dirname(file), spec));
|
|
1041
1090
|
}
|
|
1042
1091
|
files.push({ file, css: css2 });
|
|
1043
1092
|
};
|
|
@@ -1173,11 +1222,11 @@ var COMPONENT_EXT = [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html"];
|
|
|
1173
1222
|
var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
|
|
1174
1223
|
var FONT_EXT = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
|
|
1175
1224
|
function listFiles(dir, deep = false) {
|
|
1176
|
-
if (!
|
|
1225
|
+
if (!fs7.existsSync(dir)) return [];
|
|
1177
1226
|
const out = [];
|
|
1178
|
-
for (const entry of
|
|
1227
|
+
for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
|
|
1179
1228
|
if (entry.name.startsWith(".")) continue;
|
|
1180
|
-
const full =
|
|
1229
|
+
const full = path7.join(dir, entry.name);
|
|
1181
1230
|
if (entry.isDirectory()) {
|
|
1182
1231
|
if (deep) out.push(...listFiles(full, true));
|
|
1183
1232
|
} else out.push(full);
|
|
@@ -1185,16 +1234,16 @@ function listFiles(dir, deep = false) {
|
|
|
1185
1234
|
return out.sort();
|
|
1186
1235
|
}
|
|
1187
1236
|
function guessSources(componentsDir, stem) {
|
|
1188
|
-
if (!
|
|
1237
|
+
if (!fs7.existsSync(componentsDir)) return [];
|
|
1189
1238
|
const wanted = slugify(stem);
|
|
1190
1239
|
for (const file of listFiles(componentsDir)) {
|
|
1191
|
-
const ext =
|
|
1192
|
-
if (COMPONENT_EXT.includes(ext) && slugify(
|
|
1240
|
+
const ext = path7.extname(file);
|
|
1241
|
+
if (COMPONENT_EXT.includes(ext) && slugify(path7.basename(file, ext)) === wanted) return [file];
|
|
1193
1242
|
}
|
|
1194
|
-
for (const entry of
|
|
1243
|
+
for (const entry of fs7.readdirSync(componentsDir, { withFileTypes: true })) {
|
|
1195
1244
|
if (entry.isDirectory() && slugify(entry.name) === wanted) {
|
|
1196
|
-
const files = listFiles(
|
|
1197
|
-
(f) => COMPONENT_EXT.includes(
|
|
1245
|
+
const files = listFiles(path7.join(componentsDir, entry.name)).filter(
|
|
1246
|
+
(f) => COMPONENT_EXT.includes(path7.extname(f))
|
|
1198
1247
|
);
|
|
1199
1248
|
if (files.length) return files;
|
|
1200
1249
|
}
|
|
@@ -1202,14 +1251,14 @@ function guessSources(componentsDir, stem) {
|
|
|
1202
1251
|
return [];
|
|
1203
1252
|
}
|
|
1204
1253
|
function loadSystem(paths, urls) {
|
|
1205
|
-
if (!
|
|
1254
|
+
if (!fs7.existsSync(paths.system) || onlyIcon(paths.system)) return null;
|
|
1206
1255
|
const issues = [];
|
|
1207
|
-
const configFile =
|
|
1256
|
+
const configFile = path7.join(paths.system, "system.json");
|
|
1208
1257
|
const report = (severity, file, message, at) => issues.push({ severity, file: relToRoot(paths, file), message, at });
|
|
1209
1258
|
let config = null;
|
|
1210
|
-
if (
|
|
1259
|
+
if (fs7.existsSync(configFile)) {
|
|
1211
1260
|
try {
|
|
1212
|
-
const parsed = SystemConfigSchema.safeParse(JSON.parse(
|
|
1261
|
+
const parsed = SystemConfigSchema.safeParse(JSON.parse(fs7.readFileSync(configFile, "utf8")));
|
|
1213
1262
|
if (parsed.success) config = parsed.data;
|
|
1214
1263
|
else for (const issue of describeZodError(parsed.error)) report("error", configFile, issue.message, issue.at);
|
|
1215
1264
|
} catch (error) {
|
|
@@ -1218,36 +1267,36 @@ function loadSystem(paths, urls) {
|
|
|
1218
1267
|
} else {
|
|
1219
1268
|
report("warning", configFile, 'system.json is missing; run `design system init` or add { "name": "\u2026" }');
|
|
1220
1269
|
}
|
|
1221
|
-
const tokensFile =
|
|
1270
|
+
const tokensFile = path7.join(paths.system, "tokens.css");
|
|
1222
1271
|
let stylesheet = null;
|
|
1223
1272
|
if (config?.stylesheet) {
|
|
1224
|
-
stylesheet =
|
|
1225
|
-
if (!
|
|
1273
|
+
stylesheet = path7.resolve(paths.system, config.stylesheet);
|
|
1274
|
+
if (!fs7.existsSync(stylesheet)) {
|
|
1226
1275
|
report("error", configFile, `stylesheet not found: ${config.stylesheet}`, "stylesheet");
|
|
1227
1276
|
stylesheet = null;
|
|
1228
1277
|
}
|
|
1229
1278
|
}
|
|
1230
|
-
const tokenEntry = stylesheet ?? (
|
|
1279
|
+
const tokenEntry = stylesheet ?? (fs7.existsSync(tokensFile) ? tokensFile : null);
|
|
1231
1280
|
const tokens = tokenEntry ? parseTokens(readStylesheets(tokenEntry)) : [];
|
|
1232
1281
|
if (!tokenEntry) report("warning", tokensFile, "no tokens.css: screens get the default Tailwind theme");
|
|
1233
|
-
const specimensDir =
|
|
1234
|
-
const componentsDir =
|
|
1282
|
+
const specimensDir = path7.join(paths.system, "specimens");
|
|
1283
|
+
const componentsDir = path7.join(paths.system, "components");
|
|
1235
1284
|
const components = [];
|
|
1236
1285
|
const specimens = [];
|
|
1237
1286
|
const usedIds = /* @__PURE__ */ new Set();
|
|
1238
1287
|
for (const file of listFiles(specimensDir)) {
|
|
1239
|
-
const ext =
|
|
1288
|
+
const ext = path7.extname(file).toLowerCase();
|
|
1240
1289
|
if (!SPECIMEN_EXT.has(ext)) continue;
|
|
1241
|
-
const stem =
|
|
1290
|
+
const stem = path7.basename(file, ext);
|
|
1242
1291
|
let id2 = slugify(stem);
|
|
1243
1292
|
if (usedIds.has(id2)) id2 = `${id2}-${usedIds.size + 1}`;
|
|
1244
1293
|
usedIds.add(id2);
|
|
1245
|
-
const tags = parseDocTags(
|
|
1246
|
-
const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) =>
|
|
1294
|
+
const tags = parseDocTags(fs7.readFileSync(file, "utf8"));
|
|
1295
|
+
const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path7.resolve(path7.dirname(file), value));
|
|
1247
1296
|
for (const source of sources) {
|
|
1248
|
-
if (!
|
|
1297
|
+
if (!fs7.existsSync(source)) report("warning", file, `@source not found: ${path7.relative(paths.system, source)}`);
|
|
1249
1298
|
}
|
|
1250
|
-
const found = sources.filter((source) =>
|
|
1299
|
+
const found = sources.filter((source) => fs7.existsSync(source));
|
|
1251
1300
|
const title = tags.title?.[0] || titleize(stem);
|
|
1252
1301
|
const format = ext === ".html" ? "html" : "module";
|
|
1253
1302
|
components.push({
|
|
@@ -1273,10 +1322,10 @@ function loadSystem(paths, urls) {
|
|
|
1273
1322
|
});
|
|
1274
1323
|
}
|
|
1275
1324
|
const guidelines = [];
|
|
1276
|
-
for (const file of listFiles(
|
|
1277
|
-
if (
|
|
1278
|
-
const stem =
|
|
1279
|
-
const { data, body } = parseFrontmatter(
|
|
1325
|
+
for (const file of listFiles(path7.join(paths.system, "guidelines"))) {
|
|
1326
|
+
if (path7.extname(file).toLowerCase() !== ".md") continue;
|
|
1327
|
+
const stem = path7.basename(file, ".md");
|
|
1328
|
+
const { data, body } = parseFrontmatter(fs7.readFileSync(file, "utf8"));
|
|
1280
1329
|
const prefix = /^(\d+)[-_. ]+(.*)$/.exec(stem);
|
|
1281
1330
|
const order = data.order !== void 0 ? Number(data.order) : prefix ? Number(prefix[1]) : 1e3;
|
|
1282
1331
|
guidelines.push({
|
|
@@ -1288,14 +1337,14 @@ function loadSystem(paths, urls) {
|
|
|
1288
1337
|
}
|
|
1289
1338
|
guidelines.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
1290
1339
|
const assets = [];
|
|
1291
|
-
const assetsDir =
|
|
1340
|
+
const assetsDir = path7.join(paths.system, "assets");
|
|
1292
1341
|
for (const file of listFiles(assetsDir, true)) {
|
|
1293
|
-
const ext =
|
|
1342
|
+
const ext = path7.extname(file).toLowerCase();
|
|
1294
1343
|
assets.push({
|
|
1295
|
-
name:
|
|
1296
|
-
path:
|
|
1344
|
+
name: path7.basename(file),
|
|
1345
|
+
path: path7.relative(paths.system, file).split(path7.sep).join("/"),
|
|
1297
1346
|
url: urls.file(relToDesign(paths, file)),
|
|
1298
|
-
size:
|
|
1347
|
+
size: fs7.statSync(file).size,
|
|
1299
1348
|
kind: IMAGE_EXT.has(ext) ? "image" : FONT_EXT.has(ext) ? "font" : "other"
|
|
1300
1349
|
});
|
|
1301
1350
|
}
|
|
@@ -1331,7 +1380,7 @@ function loadSystem(paths, urls) {
|
|
|
1331
1380
|
},
|
|
1332
1381
|
config,
|
|
1333
1382
|
stylesheet,
|
|
1334
|
-
tokensFile:
|
|
1383
|
+
tokensFile: fs7.existsSync(tokensFile) ? tokensFile : null,
|
|
1335
1384
|
specimens
|
|
1336
1385
|
};
|
|
1337
1386
|
}
|
|
@@ -1348,10 +1397,10 @@ function summarizeSystem(system) {
|
|
|
1348
1397
|
|
|
1349
1398
|
// src/core/project.ts
|
|
1350
1399
|
function publicProblem(root, value) {
|
|
1351
|
-
const dir =
|
|
1352
|
-
if (!
|
|
1353
|
-
const real =
|
|
1354
|
-
const realRoot =
|
|
1400
|
+
const dir = path8.resolve(root, value);
|
|
1401
|
+
if (!fs8.existsSync(dir) || !fs8.statSync(dir).isDirectory()) return `no folder "${value}"`;
|
|
1402
|
+
const real = fs8.realpathSync(dir);
|
|
1403
|
+
const realRoot = fs8.realpathSync(root);
|
|
1355
1404
|
if (real === realRoot || !isInside(realRoot, real)) return `"${value}" is not a folder inside the project`;
|
|
1356
1405
|
return null;
|
|
1357
1406
|
}
|
|
@@ -1373,9 +1422,9 @@ var DesignProject = class {
|
|
|
1373
1422
|
const issues = [];
|
|
1374
1423
|
let config = {};
|
|
1375
1424
|
const file = this.paths.config;
|
|
1376
|
-
if (
|
|
1425
|
+
if (fs8.existsSync(file)) {
|
|
1377
1426
|
try {
|
|
1378
|
-
const parsed = DesignConfigSchema.safeParse(JSON.parse(
|
|
1427
|
+
const parsed = DesignConfigSchema.safeParse(JSON.parse(fs8.readFileSync(file, "utf8")));
|
|
1379
1428
|
if (parsed.success) config = parsed.data;
|
|
1380
1429
|
else
|
|
1381
1430
|
for (const issue of describeZodError(parsed.error))
|
|
@@ -1388,6 +1437,14 @@ var DesignProject = class {
|
|
|
1388
1437
|
});
|
|
1389
1438
|
}
|
|
1390
1439
|
}
|
|
1440
|
+
if (config.app !== void 0) {
|
|
1441
|
+
const dir = path8.resolve(this.paths.root, config.app);
|
|
1442
|
+
const problem = !fs8.existsSync(path8.join(dir, "node_modules")) ? `no node_modules in "${config.app}"; install the app's dependencies there` : isInside(this.paths.root, dir) ? null : `"${config.app}" is not a folder inside the project`;
|
|
1443
|
+
if (problem) {
|
|
1444
|
+
issues.push({ severity: "error", file: relToRoot(this.paths, file), at: "app", message: problem });
|
|
1445
|
+
config = { ...config, app: void 0 };
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1391
1448
|
if (config.public !== void 0) {
|
|
1392
1449
|
const problem = publicProblem(this.paths.root, config.public);
|
|
1393
1450
|
if (problem) {
|
|
@@ -1398,10 +1455,15 @@ var DesignProject = class {
|
|
|
1398
1455
|
this.configCache = { config, issues };
|
|
1399
1456
|
return this.configCache;
|
|
1400
1457
|
}
|
|
1458
|
+
/** The app package from design.json `app`, when it is usable. */
|
|
1459
|
+
appDir() {
|
|
1460
|
+
const configured = this.config().config.app;
|
|
1461
|
+
return configured === void 0 ? null : path8.resolve(this.paths.root, configured);
|
|
1462
|
+
}
|
|
1401
1463
|
/** The public folder from design.json, when it is a folder inside the project. */
|
|
1402
1464
|
publicDir() {
|
|
1403
1465
|
const configured = this.config().config.public;
|
|
1404
|
-
return configured === void 0 ? null :
|
|
1466
|
+
return configured === void 0 ? null : path8.resolve(this.paths.root, configured);
|
|
1405
1467
|
}
|
|
1406
1468
|
/** The public folder's files, relative to it; `invalidatePublic` when they change. */
|
|
1407
1469
|
publicFiles() {
|
|
@@ -1421,11 +1483,11 @@ var DesignProject = class {
|
|
|
1421
1483
|
const configured = this.config().config.name;
|
|
1422
1484
|
if (configured) return configured;
|
|
1423
1485
|
try {
|
|
1424
|
-
const manifest = JSON.parse(
|
|
1486
|
+
const manifest = JSON.parse(fs8.readFileSync(path8.join(this.paths.root, "package.json"), "utf8"));
|
|
1425
1487
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
1426
1488
|
} catch {
|
|
1427
1489
|
}
|
|
1428
|
-
return
|
|
1490
|
+
return path8.basename(this.paths.root);
|
|
1429
1491
|
}
|
|
1430
1492
|
system() {
|
|
1431
1493
|
if (this.systemCache === void 0) this.systemCache = loadSystem(this.paths, this.urls);
|
|
@@ -1437,7 +1499,7 @@ var DesignProject = class {
|
|
|
1437
1499
|
canvas(id2) {
|
|
1438
1500
|
const cached = this.canvasCache.get(id2);
|
|
1439
1501
|
if (cached) return cached;
|
|
1440
|
-
if (!
|
|
1502
|
+
if (!fs8.existsSync(path8.join(this.paths.canvases, id2))) return null;
|
|
1441
1503
|
const canvas = loadCanvas(this.paths, id2, this.urls, this.snapshots);
|
|
1442
1504
|
this.canvasCache.set(id2, canvas);
|
|
1443
1505
|
return canvas;
|
|
@@ -1462,13 +1524,26 @@ var DesignProject = class {
|
|
|
1462
1524
|
version: packageVersion(),
|
|
1463
1525
|
system: system ? summarizeSystem(system) : null,
|
|
1464
1526
|
canvases: this.canvases().map(summarize),
|
|
1465
|
-
issues: this.config().issues
|
|
1527
|
+
issues: this.config().issues,
|
|
1528
|
+
icon: this.iconUrl()
|
|
1466
1529
|
};
|
|
1467
1530
|
}
|
|
1531
|
+
/** The icon's URL, with its change time so a new icon is fetched anew. */
|
|
1532
|
+
iconUrl() {
|
|
1533
|
+
const file = iconFile(this.paths);
|
|
1534
|
+
if (!file) return null;
|
|
1535
|
+
return `${this.urls.file(relToDesign(this.paths, file))}?v=${Math.round(fs8.statSync(file).mtimeMs)}`;
|
|
1536
|
+
}
|
|
1537
|
+
iconIssues() {
|
|
1538
|
+
const file = iconFile(this.paths);
|
|
1539
|
+
const problem = file ? iconProblem(fs8.readFileSync(file), path8.extname(file).slice(1)) : null;
|
|
1540
|
+
return problem ? [{ severity: "error", file: relToRoot(this.paths, file), message: problem }] : [];
|
|
1541
|
+
}
|
|
1468
1542
|
/** Every problem in the project, for `design check`. */
|
|
1469
1543
|
issues() {
|
|
1470
1544
|
return [
|
|
1471
1545
|
...this.config().issues,
|
|
1546
|
+
...this.iconIssues(),
|
|
1472
1547
|
...this.system()?.doc.issues ?? [],
|
|
1473
1548
|
...this.canvases().flatMap((canvas) => canvas.doc.issues)
|
|
1474
1549
|
];
|
|
@@ -1508,21 +1583,21 @@ var STATIC_URLS = {
|
|
|
1508
1583
|
};
|
|
1509
1584
|
|
|
1510
1585
|
// src/server/entries.ts
|
|
1511
|
-
import
|
|
1512
|
-
import
|
|
1586
|
+
import fs9 from "fs";
|
|
1587
|
+
import path9 from "path";
|
|
1513
1588
|
var SYSTEM_CSS = "system.css";
|
|
1514
1589
|
var HTML_BOOT = "html-boot.js";
|
|
1515
1590
|
function writeIfChanged(file, content) {
|
|
1516
1591
|
try {
|
|
1517
|
-
if (
|
|
1592
|
+
if (fs9.readFileSync(file, "utf8") === content) return false;
|
|
1518
1593
|
} catch {
|
|
1519
1594
|
}
|
|
1520
|
-
|
|
1521
|
-
|
|
1595
|
+
fs9.mkdirSync(path9.dirname(file), { recursive: true });
|
|
1596
|
+
fs9.writeFileSync(file, content);
|
|
1522
1597
|
return true;
|
|
1523
1598
|
}
|
|
1524
1599
|
function importPath(fromDir, file) {
|
|
1525
|
-
const rel = toPosix(
|
|
1600
|
+
const rel = toPosix(path9.relative(fromDir, file));
|
|
1526
1601
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1527
1602
|
}
|
|
1528
1603
|
var css = (file) => JSON.stringify(toPosix(file));
|
|
@@ -1535,10 +1610,10 @@ var Entries = class {
|
|
|
1535
1610
|
return this.project.paths.entries;
|
|
1536
1611
|
}
|
|
1537
1612
|
systemCssFile() {
|
|
1538
|
-
return
|
|
1613
|
+
return path9.join(this.dir, SYSTEM_CSS);
|
|
1539
1614
|
}
|
|
1540
1615
|
entryFile(source) {
|
|
1541
|
-
return
|
|
1616
|
+
return path9.join(this.dir, source.canvas, `${source.id}.js`);
|
|
1542
1617
|
}
|
|
1543
1618
|
/** Tailwind with the design tokens, or the stylesheet system.json names; scans every file in `.design`. */
|
|
1544
1619
|
systemCss() {
|
|
@@ -1555,14 +1630,14 @@ var Entries = class {
|
|
|
1555
1630
|
if (system?.tokensFile) lines.push(`@import ${css(system.tokensFile)};`);
|
|
1556
1631
|
}
|
|
1557
1632
|
lines.push(`@source ${css(paths.design)};`, `@source not ${css(paths.cache)};`);
|
|
1558
|
-
lines.push(`@source not ${css(
|
|
1559
|
-
for (const extra of config.sources ?? []) lines.push(`@source ${css(
|
|
1633
|
+
lines.push(`@source not ${css(path9.join(paths.design, "node_modules"))};`);
|
|
1634
|
+
for (const extra of config.sources ?? []) lines.push(`@source ${css(path9.resolve(paths.root, extra))};`);
|
|
1560
1635
|
return `${lines.join("\n")}
|
|
1561
1636
|
`;
|
|
1562
1637
|
}
|
|
1563
1638
|
entrySource(source) {
|
|
1564
1639
|
const file = this.entryFile(source);
|
|
1565
|
-
const dir =
|
|
1640
|
+
const dir = path9.dirname(file);
|
|
1566
1641
|
const lines = [];
|
|
1567
1642
|
if (source.system) lines.push(`import ${JSON.stringify(importPath(dir, this.systemCssFile()))}`);
|
|
1568
1643
|
if (source.builtin === "typography") {
|
|
@@ -1577,7 +1652,7 @@ var Entries = class {
|
|
|
1577
1652
|
`import * as screen from ${JSON.stringify(importPath(dir, source.file))}`,
|
|
1578
1653
|
"",
|
|
1579
1654
|
`const Screen = screen.default ?? Object.values(screen).find((value) => typeof value === 'function')`,
|
|
1580
|
-
"if (!Screen) throw new Error(" + JSON.stringify(`${
|
|
1655
|
+
"if (!Screen) throw new Error(" + JSON.stringify(`${path9.basename(source.file)} has no component to render: export one as default`) + ")",
|
|
1581
1656
|
"hmr(import.meta.hot)",
|
|
1582
1657
|
"mount(Screen)"
|
|
1583
1658
|
);
|
|
@@ -1590,14 +1665,14 @@ ${lines.join("\n")}
|
|
|
1590
1665
|
sync() {
|
|
1591
1666
|
writeIfChanged(this.systemCssFile(), this.systemCss());
|
|
1592
1667
|
writeIfChanged(
|
|
1593
|
-
|
|
1668
|
+
path9.join(this.dir, HTML_BOOT),
|
|
1594
1669
|
`// Generated by foss-design.
|
|
1595
1670
|
import { boot, hmr } from '@design/runtime'
|
|
1596
1671
|
boot()
|
|
1597
1672
|
hmr(import.meta.hot)
|
|
1598
1673
|
`
|
|
1599
1674
|
);
|
|
1600
|
-
const wanted = /* @__PURE__ */ new Set([this.systemCssFile(),
|
|
1675
|
+
const wanted = /* @__PURE__ */ new Set([this.systemCssFile(), path9.join(this.dir, HTML_BOOT)]);
|
|
1601
1676
|
for (const source of this.project.sources()) {
|
|
1602
1677
|
if (source.format !== "module") continue;
|
|
1603
1678
|
const file = this.entryFile(source);
|
|
@@ -1612,21 +1687,21 @@ hmr(import.meta.hot)
|
|
|
1612
1687
|
* Vite bundle again and fail the requests in flight (504 Outdated Optimize Dep).
|
|
1613
1688
|
*/
|
|
1614
1689
|
files() {
|
|
1615
|
-
const files = [
|
|
1690
|
+
const files = [path9.join(this.dir, HTML_BOOT)];
|
|
1616
1691
|
for (const source of this.project.sources()) {
|
|
1617
1692
|
if (source.format === "module") files.push(this.entryFile(source));
|
|
1618
|
-
else if (!source.builtin &&
|
|
1693
|
+
else if (!source.builtin && fs9.existsSync(source.file)) files.push(source.file);
|
|
1619
1694
|
}
|
|
1620
1695
|
return files;
|
|
1621
1696
|
}
|
|
1622
1697
|
prune(dir, wanted) {
|
|
1623
|
-
if (!
|
|
1624
|
-
for (const entry of
|
|
1625
|
-
const full =
|
|
1698
|
+
if (!fs9.existsSync(dir)) return;
|
|
1699
|
+
for (const entry of fs9.readdirSync(dir, { withFileTypes: true })) {
|
|
1700
|
+
const full = path9.join(dir, entry.name);
|
|
1626
1701
|
if (entry.isDirectory()) {
|
|
1627
1702
|
this.prune(full, wanted);
|
|
1628
|
-
if (
|
|
1629
|
-
} else if (!wanted.has(full))
|
|
1703
|
+
if (fs9.readdirSync(full).length === 0) fs9.rmdirSync(full);
|
|
1704
|
+
} else if (!wanted.has(full)) fs9.rmSync(full);
|
|
1630
1705
|
}
|
|
1631
1706
|
}
|
|
1632
1707
|
};
|
|
@@ -1694,68 +1769,132 @@ ${html}
|
|
|
1694
1769
|
}
|
|
1695
1770
|
|
|
1696
1771
|
// src/server/links.ts
|
|
1697
|
-
import
|
|
1698
|
-
import
|
|
1772
|
+
import fs10 from "fs";
|
|
1773
|
+
import path10 from "path";
|
|
1699
1774
|
var SHIPPED_PACKAGES = ["react", "react-dom", "motion", "lucide-react", "clsx", "tailwind-merge"];
|
|
1700
1775
|
function findPackageDir(fromDir, name, skip) {
|
|
1701
|
-
let dir =
|
|
1776
|
+
let dir = path10.resolve(fromDir);
|
|
1702
1777
|
for (; ; ) {
|
|
1703
|
-
const modules =
|
|
1704
|
-
const candidate =
|
|
1705
|
-
if (modules !== skip &&
|
|
1706
|
-
const parent =
|
|
1778
|
+
const modules = path10.join(dir, "node_modules");
|
|
1779
|
+
const candidate = path10.join(modules, name);
|
|
1780
|
+
if (modules !== skip && fs10.existsSync(path10.join(candidate, "package.json"))) return candidate;
|
|
1781
|
+
const parent = path10.dirname(dir);
|
|
1707
1782
|
if (parent === dir) return null;
|
|
1708
1783
|
dir = parent;
|
|
1709
1784
|
}
|
|
1710
1785
|
}
|
|
1711
1786
|
function isLink(file) {
|
|
1712
1787
|
try {
|
|
1713
|
-
return
|
|
1788
|
+
return fs10.lstatSync(file).isSymbolicLink();
|
|
1714
1789
|
} catch {
|
|
1715
1790
|
return false;
|
|
1716
1791
|
}
|
|
1717
1792
|
}
|
|
1718
|
-
function
|
|
1719
|
-
|
|
1793
|
+
function ensureLink(link, target) {
|
|
1794
|
+
let current = null;
|
|
1795
|
+
try {
|
|
1796
|
+
current = fs10.realpathSync(link);
|
|
1797
|
+
} catch {
|
|
1798
|
+
}
|
|
1799
|
+
if (current === target) return;
|
|
1800
|
+
if (isLink(link)) fs10.unlinkSync(link);
|
|
1801
|
+
fs10.mkdirSync(path10.dirname(link), { recursive: true });
|
|
1802
|
+
fs10.symlinkSync(target, link, process.platform === "win32" ? "junction" : "dir");
|
|
1803
|
+
}
|
|
1804
|
+
function appPackages(appDir) {
|
|
1805
|
+
const found = /* @__PURE__ */ new Map();
|
|
1806
|
+
const modules = path10.join(appDir, "node_modules");
|
|
1807
|
+
const add = (name, dir) => {
|
|
1808
|
+
try {
|
|
1809
|
+
if (fs10.existsSync(path10.join(dir, "package.json"))) found.set(name, fs10.realpathSync(dir));
|
|
1810
|
+
} catch {
|
|
1811
|
+
}
|
|
1812
|
+
};
|
|
1813
|
+
let entries = [];
|
|
1814
|
+
try {
|
|
1815
|
+
entries = fs10.readdirSync(modules, { withFileTypes: true });
|
|
1816
|
+
} catch {
|
|
1817
|
+
return found;
|
|
1818
|
+
}
|
|
1819
|
+
for (const entry of entries) {
|
|
1820
|
+
if (entry.name.startsWith(".")) continue;
|
|
1821
|
+
const dir = path10.join(modules, entry.name);
|
|
1822
|
+
if (!entry.name.startsWith("@")) {
|
|
1823
|
+
add(entry.name, dir);
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
try {
|
|
1827
|
+
for (const scoped of fs10.readdirSync(dir))
|
|
1828
|
+
if (!scoped.startsWith(".")) add(`${entry.name}/${scoped}`, path10.join(dir, scoped));
|
|
1829
|
+
} catch {
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return found;
|
|
1833
|
+
}
|
|
1834
|
+
var APP_LINKS = ".foss-design-app.json";
|
|
1835
|
+
function linkShippedPackages(paths, appDir = null) {
|
|
1836
|
+
const modules = path10.join(paths.design, "node_modules");
|
|
1720
1837
|
const linked = [];
|
|
1721
1838
|
const allow = /* @__PURE__ */ new Set();
|
|
1839
|
+
const app = appDir ? appPackages(appDir) : /* @__PURE__ */ new Map();
|
|
1840
|
+
const placedByUser = (link) => fs10.existsSync(link) && !isLink(link);
|
|
1722
1841
|
for (const name of SHIPPED_PACKAGES) {
|
|
1723
|
-
const link =
|
|
1724
|
-
|
|
1725
|
-
const own = placedByUser ? link : findPackageDir(paths.design, name, modules);
|
|
1842
|
+
const link = path10.join(modules, name);
|
|
1843
|
+
if (app.has(name) && !placedByUser(link)) continue;
|
|
1844
|
+
const own = placedByUser(link) ? link : findPackageDir(paths.design, name, modules);
|
|
1726
1845
|
if (own) {
|
|
1727
|
-
allow.add(
|
|
1728
|
-
if (isLink(link))
|
|
1846
|
+
allow.add(path10.dirname(fs10.realpathSync(own)));
|
|
1847
|
+
if (isLink(link)) fs10.unlinkSync(link);
|
|
1729
1848
|
continue;
|
|
1730
1849
|
}
|
|
1731
1850
|
const shipped = findPackageDir(PKG_ROOT, name);
|
|
1732
1851
|
if (!shipped) continue;
|
|
1733
|
-
const target2 =
|
|
1734
|
-
allow.add(
|
|
1735
|
-
|
|
1736
|
-
try {
|
|
1737
|
-
current2 = fs9.realpathSync(link);
|
|
1738
|
-
} catch {
|
|
1739
|
-
}
|
|
1740
|
-
if (current2 !== target2) {
|
|
1741
|
-
if (isLink(link)) fs9.unlinkSync(link);
|
|
1742
|
-
fs9.mkdirSync(path9.dirname(link), { recursive: true });
|
|
1743
|
-
fs9.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
|
|
1744
|
-
}
|
|
1852
|
+
const target2 = fs10.realpathSync(shipped);
|
|
1853
|
+
allow.add(path10.dirname(target2));
|
|
1854
|
+
ensureLink(link, target2);
|
|
1745
1855
|
linked.push(name);
|
|
1746
1856
|
}
|
|
1747
|
-
const
|
|
1748
|
-
const
|
|
1857
|
+
const fromApp = [];
|
|
1858
|
+
for (const [name, target2] of app) {
|
|
1859
|
+
const link = path10.join(modules, name);
|
|
1860
|
+
if (placedByUser(link)) continue;
|
|
1861
|
+
ensureLink(link, target2);
|
|
1862
|
+
allow.add(path10.dirname(target2));
|
|
1863
|
+
fromApp.push(name);
|
|
1864
|
+
}
|
|
1865
|
+
const record2 = path10.join(modules, APP_LINKS);
|
|
1866
|
+
let before = [];
|
|
1867
|
+
try {
|
|
1868
|
+
before = JSON.parse(fs10.readFileSync(record2, "utf8"));
|
|
1869
|
+
} catch {
|
|
1870
|
+
}
|
|
1871
|
+
for (const name of before) {
|
|
1872
|
+
const link = path10.join(modules, name);
|
|
1873
|
+
if (!app.has(name) && !SHIPPED_PACKAGES.includes(name) && isLink(link)) {
|
|
1874
|
+
fs10.unlinkSync(link);
|
|
1875
|
+
if (name.startsWith("@"))
|
|
1876
|
+
try {
|
|
1877
|
+
fs10.rmdirSync(path10.dirname(link));
|
|
1878
|
+
} catch {
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (fromApp.length || before.length) {
|
|
1883
|
+
fs10.mkdirSync(modules, { recursive: true });
|
|
1884
|
+
fs10.writeFileSync(record2, JSON.stringify(fromApp.sort()));
|
|
1885
|
+
}
|
|
1886
|
+
const self = path10.join(modules, "foss-design");
|
|
1887
|
+
const target = fs10.realpathSync(PKG_ROOT);
|
|
1749
1888
|
let current = null;
|
|
1750
1889
|
try {
|
|
1751
|
-
current =
|
|
1890
|
+
current = fs10.realpathSync(self);
|
|
1752
1891
|
} catch {
|
|
1753
1892
|
}
|
|
1754
1893
|
if (current !== target) {
|
|
1755
|
-
if (isLink(self))
|
|
1756
|
-
if (!
|
|
1757
|
-
|
|
1758
|
-
|
|
1894
|
+
if (isLink(self)) fs10.unlinkSync(self);
|
|
1895
|
+
if (!fs10.existsSync(self)) {
|
|
1896
|
+
fs10.mkdirSync(modules, { recursive: true });
|
|
1897
|
+
fs10.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
|
|
1759
1898
|
}
|
|
1760
1899
|
}
|
|
1761
1900
|
allow.add(target);
|
|
@@ -1764,8 +1903,8 @@ function linkShippedPackages(paths) {
|
|
|
1764
1903
|
|
|
1765
1904
|
// src/server/vite.ts
|
|
1766
1905
|
import { createHash as createHash2 } from "crypto";
|
|
1767
|
-
import
|
|
1768
|
-
import
|
|
1906
|
+
import fs11 from "fs";
|
|
1907
|
+
import path11 from "path";
|
|
1769
1908
|
import tailwindcss from "@tailwindcss/vite";
|
|
1770
1909
|
import react from "@vitejs/plugin-react";
|
|
1771
1910
|
var escapeRegExp = (text2) => text2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1777,30 +1916,31 @@ function aliases(project) {
|
|
|
1777
1916
|
{ find: /^@system\//, replacement: `${toPosix(paths.system)}/` }
|
|
1778
1917
|
];
|
|
1779
1918
|
for (const [key, target] of Object.entries(project.config().config.alias ?? {})) {
|
|
1780
|
-
const replacement = toPosix(
|
|
1919
|
+
const replacement = toPosix(path11.resolve(paths.root, target));
|
|
1781
1920
|
list.push(
|
|
1782
1921
|
key.endsWith("/") ? { find: new RegExp(`^${escapeRegExp(key)}`), replacement: `${replacement.replace(/\/$/, "")}/` } : { find: new RegExp(`^${escapeRegExp(key)}(?=/|$)`), replacement }
|
|
1783
1922
|
);
|
|
1784
1923
|
}
|
|
1785
1924
|
return list;
|
|
1786
1925
|
}
|
|
1787
|
-
function dropStaleDepCache(paths) {
|
|
1788
|
-
const dir =
|
|
1789
|
-
const stamp =
|
|
1926
|
+
function dropStaleDepCache(paths, appDir = null) {
|
|
1927
|
+
const dir = path11.join(paths.cache, "vite");
|
|
1928
|
+
const stamp = path11.join(paths.cache, "vite-runtime");
|
|
1790
1929
|
let runtime = "";
|
|
1791
1930
|
try {
|
|
1792
|
-
runtime = createHash2("sha256").update(
|
|
1931
|
+
runtime = createHash2("sha256").update(fs11.readFileSync(PKG.runtime)).digest("hex");
|
|
1793
1932
|
} catch {
|
|
1794
1933
|
}
|
|
1934
|
+
if (appDir) runtime += ` ${appDir}`;
|
|
1795
1935
|
let seen = "";
|
|
1796
1936
|
try {
|
|
1797
|
-
seen =
|
|
1937
|
+
seen = fs11.readFileSync(stamp, "utf8");
|
|
1798
1938
|
} catch {
|
|
1799
1939
|
}
|
|
1800
1940
|
if (seen === runtime) return;
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1941
|
+
fs11.rmSync(dir, { recursive: true, force: true });
|
|
1942
|
+
fs11.mkdirSync(paths.cache, { recursive: true });
|
|
1943
|
+
fs11.writeFileSync(stamp, runtime);
|
|
1804
1944
|
}
|
|
1805
1945
|
function baseConfig(project) {
|
|
1806
1946
|
const { paths } = project;
|
|
@@ -1808,7 +1948,7 @@ function baseConfig(project) {
|
|
|
1808
1948
|
configFile: false,
|
|
1809
1949
|
envDir: false,
|
|
1810
1950
|
root: paths.design,
|
|
1811
|
-
cacheDir:
|
|
1951
|
+
cacheDir: path11.join(paths.cache, "vite"),
|
|
1812
1952
|
// design.json `public`: served under the frames' base, so HTML and CSS paths such as `/logo.png` resolve.
|
|
1813
1953
|
publicDir: project.publicDir() ?? false,
|
|
1814
1954
|
logLevel: "warn",
|
|
@@ -1828,7 +1968,7 @@ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".jsx", ".mts", ".cts",
|
|
|
1828
1968
|
var BUNDLED_REF = /<(script|link|img|source|video|audio|image|use|input)\b[^>]*>/gi;
|
|
1829
1969
|
var encodePath2 = (rel) => rel.split("/").map(encodeURIComponent).join("/");
|
|
1830
1970
|
function relUrl(fromDir, file) {
|
|
1831
|
-
const rel = toPosix(
|
|
1971
|
+
const rel = toPosix(path12.relative(fromDir, file));
|
|
1832
1972
|
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1833
1973
|
}
|
|
1834
1974
|
function staticSnapshots(paths) {
|
|
@@ -1838,7 +1978,7 @@ function staticSnapshots(paths) {
|
|
|
1838
1978
|
let data = metaCache.get(canvas);
|
|
1839
1979
|
if (!data) {
|
|
1840
1980
|
try {
|
|
1841
|
-
data = JSON.parse(
|
|
1981
|
+
data = JSON.parse(fs12.readFileSync(path12.join(paths.snapshots, canvas, "meta.json"), "utf8"));
|
|
1842
1982
|
} catch {
|
|
1843
1983
|
}
|
|
1844
1984
|
data ??= {};
|
|
@@ -1852,8 +1992,8 @@ function staticSnapshots(paths) {
|
|
|
1852
1992
|
const urls = {};
|
|
1853
1993
|
const thumbs = {};
|
|
1854
1994
|
for (const theme2 of THEMES) {
|
|
1855
|
-
const file =
|
|
1856
|
-
if (!entry[theme2] || !
|
|
1995
|
+
const file = path12.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
|
|
1996
|
+
if (!entry[theme2] || !fs12.existsSync(file)) continue;
|
|
1857
1997
|
const rel = `_snap/${canvas}/${id2}.${theme2}.png`;
|
|
1858
1998
|
copies.set(file, rel);
|
|
1859
1999
|
urls[theme2] = `${encodePath2(rel)}?v=${entry[theme2]}`;
|
|
@@ -1874,32 +2014,32 @@ function rewriteRefs(html, originalDir, stagedDir) {
|
|
|
1874
2014
|
return tag;
|
|
1875
2015
|
return tag.replace(
|
|
1876
2016
|
/(\s(?:src|href|poster)\s*=\s*)(["'])([^"']*)\2/gi,
|
|
1877
|
-
(whole,
|
|
2017
|
+
(whole, attr2, quote, value) => {
|
|
1878
2018
|
if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
|
|
1879
2019
|
const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
|
|
1880
|
-
const target =
|
|
1881
|
-
if (!
|
|
1882
|
-
return `${
|
|
2020
|
+
const target = path12.resolve(originalDir, decodeURIComponent(pathname));
|
|
2021
|
+
if (!fs12.existsSync(target)) return whole;
|
|
2022
|
+
return `${attr2}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
|
|
1883
2023
|
}
|
|
1884
2024
|
);
|
|
1885
2025
|
});
|
|
1886
2026
|
}
|
|
1887
2027
|
function copyTree(from, to, filter) {
|
|
1888
|
-
if (!
|
|
1889
|
-
for (const entry of
|
|
2028
|
+
if (!fs12.existsSync(from)) return;
|
|
2029
|
+
for (const entry of fs12.readdirSync(from, { withFileTypes: true })) {
|
|
1890
2030
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1891
|
-
const source =
|
|
1892
|
-
const target =
|
|
2031
|
+
const source = path12.join(from, entry.name);
|
|
2032
|
+
const target = path12.join(to, entry.name);
|
|
1893
2033
|
if (entry.isDirectory()) copyTree(source, target, filter);
|
|
1894
|
-
else if (filter(source) && !
|
|
1895
|
-
|
|
1896
|
-
|
|
2034
|
+
else if (filter(source) && !fs12.existsSync(target)) {
|
|
2035
|
+
fs12.mkdirSync(path12.dirname(target), { recursive: true });
|
|
2036
|
+
fs12.copyFileSync(source, target);
|
|
1897
2037
|
}
|
|
1898
2038
|
}
|
|
1899
2039
|
}
|
|
1900
2040
|
function writeJson(file, value) {
|
|
1901
|
-
|
|
1902
|
-
|
|
2041
|
+
fs12.mkdirSync(path12.dirname(file), { recursive: true });
|
|
2042
|
+
fs12.writeFileSync(file, JSON.stringify(value));
|
|
1903
2043
|
}
|
|
1904
2044
|
async function buildSite(paths, out, options = {}) {
|
|
1905
2045
|
const snapshots = staticSnapshots(paths);
|
|
@@ -1914,21 +2054,21 @@ async function buildSite(paths, out, options = {}) {
|
|
|
1914
2054
|
const system = options.includeSystem === false ? null : loadedSystem;
|
|
1915
2055
|
const entries = new Entries(project);
|
|
1916
2056
|
entries.sync();
|
|
1917
|
-
linkShippedPackages(paths);
|
|
2057
|
+
linkShippedPackages(paths, project.appDir());
|
|
1918
2058
|
const publicDir = project.publicDir();
|
|
1919
2059
|
const publicFiles2 = project.publicFiles();
|
|
1920
|
-
const staging =
|
|
1921
|
-
|
|
2060
|
+
const staging = path12.join(paths.cache, "build", "src");
|
|
2061
|
+
fs12.rmSync(staging, { recursive: true, force: true });
|
|
1922
2062
|
const fonts = loadedSystem?.doc.fonts ?? [];
|
|
1923
2063
|
const sources = [...system?.specimens ?? [], ...canvases.flatMap((canvas) => canvas.screens)];
|
|
1924
2064
|
const inputs = [];
|
|
1925
2065
|
const htmlScreens = [];
|
|
1926
2066
|
for (const source of sources) {
|
|
1927
|
-
if (!source.builtin && !
|
|
1928
|
-
const siteDir =
|
|
1929
|
-
const dir =
|
|
1930
|
-
const file =
|
|
1931
|
-
|
|
2067
|
+
if (!source.builtin && !fs12.existsSync(source.file)) continue;
|
|
2068
|
+
const siteDir = path12.join("_s", source.canvas, source.id);
|
|
2069
|
+
const dir = path12.join(staging, siteDir);
|
|
2070
|
+
const file = path12.join(dir, "index.html");
|
|
2071
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
1932
2072
|
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
1933
2073
|
const head = {
|
|
1934
2074
|
source,
|
|
@@ -1937,24 +2077,24 @@ async function buildSite(paths, out, options = {}) {
|
|
|
1937
2077
|
snapshots: false,
|
|
1938
2078
|
autoHeight: source.autoHeight ?? false,
|
|
1939
2079
|
// The public folder lands at the site root, which frames reach through `../`.
|
|
1940
|
-
public: publicFiles2.length ? { base: `${toPosix(
|
|
2080
|
+
public: publicFiles2.length ? { base: `${toPosix(path12.relative(dir, staging))}/`, files: publicFiles2 } : void 0
|
|
1941
2081
|
};
|
|
1942
2082
|
if (source.format === "module") {
|
|
1943
|
-
|
|
2083
|
+
fs12.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
|
|
1944
2084
|
} else {
|
|
1945
|
-
const raw = rewriteRefs(
|
|
2085
|
+
const raw = rewriteRefs(fs12.readFileSync(source.file, "utf8"), path12.dirname(source.file), dir);
|
|
1946
2086
|
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
1947
2087
|
const html = injectIntoHtml(raw, {
|
|
1948
2088
|
...head,
|
|
1949
2089
|
systemCss: withSystem ? relUrl(dir, entries.systemCssFile()) : void 0,
|
|
1950
|
-
boot: relUrl(dir,
|
|
2090
|
+
boot: relUrl(dir, path12.join(entries.dir, HTML_BOOT))
|
|
1951
2091
|
});
|
|
1952
|
-
|
|
2092
|
+
fs12.writeFileSync(file, html);
|
|
1953
2093
|
htmlScreens.push({ source, dir: siteDir });
|
|
1954
2094
|
}
|
|
1955
2095
|
inputs.push(file);
|
|
1956
2096
|
}
|
|
1957
|
-
|
|
2097
|
+
fs12.mkdirSync(out, { recursive: true });
|
|
1958
2098
|
if (inputs.length) {
|
|
1959
2099
|
const base = baseConfig(project);
|
|
1960
2100
|
await build({
|
|
@@ -1975,14 +2115,14 @@ async function buildSite(paths, out, options = {}) {
|
|
|
1975
2115
|
}
|
|
1976
2116
|
});
|
|
1977
2117
|
} else {
|
|
1978
|
-
|
|
1979
|
-
|
|
2118
|
+
fs12.rmSync(out, { recursive: true, force: true });
|
|
2119
|
+
fs12.mkdirSync(out, { recursive: true });
|
|
1980
2120
|
}
|
|
1981
2121
|
for (const { source, dir } of htmlScreens) {
|
|
1982
2122
|
copyTree(
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
(file) => !SOURCE_EXT.has(
|
|
2123
|
+
path12.dirname(source.file),
|
|
2124
|
+
path12.join(out, dir),
|
|
2125
|
+
(file) => !SOURCE_EXT.has(path12.extname(file).toLowerCase())
|
|
1986
2126
|
);
|
|
1987
2127
|
}
|
|
1988
2128
|
const files = /* @__PURE__ */ new Set();
|
|
@@ -1992,38 +2132,40 @@ async function buildSite(paths, out, options = {}) {
|
|
|
1992
2132
|
for (const item of section.items) if (item.kind === "image" && item.url.startsWith("_f/")) files.add(item.url);
|
|
1993
2133
|
}
|
|
1994
2134
|
for (const asset of system?.doc.assets ?? []) files.add(asset.url);
|
|
2135
|
+
const icon = project.iconUrl();
|
|
2136
|
+
if (icon) files.add(icon.split("?")[0]);
|
|
1995
2137
|
for (const url of files) {
|
|
1996
2138
|
const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
|
|
1997
|
-
const from =
|
|
1998
|
-
if (!
|
|
1999
|
-
const to =
|
|
2000
|
-
|
|
2001
|
-
|
|
2139
|
+
const from = path12.join(paths.design, rel);
|
|
2140
|
+
if (!fs12.existsSync(from)) continue;
|
|
2141
|
+
const to = path12.join(out, "_f", rel);
|
|
2142
|
+
fs12.mkdirSync(path12.dirname(to), { recursive: true });
|
|
2143
|
+
fs12.copyFileSync(from, to);
|
|
2002
2144
|
}
|
|
2003
2145
|
const info = project.info(true);
|
|
2004
2146
|
const snapshotDirs = canvasIds.map((id2) => `_snap/${id2}/`);
|
|
2005
2147
|
for (const [from, rel] of snapshots.copies) {
|
|
2006
2148
|
if (!snapshotDirs.some((dir) => rel.startsWith(dir))) continue;
|
|
2007
|
-
const to =
|
|
2008
|
-
|
|
2009
|
-
|
|
2149
|
+
const to = path12.join(out, rel);
|
|
2150
|
+
fs12.mkdirSync(path12.dirname(to), { recursive: true });
|
|
2151
|
+
fs12.copyFileSync(from, to);
|
|
2010
2152
|
}
|
|
2011
2153
|
if (!system) info.system = null;
|
|
2012
2154
|
info.canvases = info.canvases.filter((canvas) => canvasIds.includes(canvas.id));
|
|
2013
|
-
writeJson(
|
|
2014
|
-
for (const canvas of canvases) writeJson(
|
|
2155
|
+
writeJson(path12.join(out, "api", "project.json"), info);
|
|
2156
|
+
for (const canvas of canvases) writeJson(path12.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
|
|
2015
2157
|
if (system) {
|
|
2016
|
-
writeJson(
|
|
2158
|
+
writeJson(path12.join(out, "api", "system.json"), system.doc);
|
|
2017
2159
|
const texts = {};
|
|
2018
2160
|
for (const component of system.doc.components) {
|
|
2019
2161
|
for (const rel of [component.specimen, ...component.sources]) {
|
|
2020
|
-
const file =
|
|
2021
|
-
if (!(rel in texts) &&
|
|
2162
|
+
const file = path12.join(paths.root, rel);
|
|
2163
|
+
if (!(rel in texts) && fs12.existsSync(file)) texts[rel] = fs12.readFileSync(file, "utf8");
|
|
2022
2164
|
}
|
|
2023
2165
|
}
|
|
2024
|
-
writeJson(
|
|
2166
|
+
writeJson(path12.join(out, "api", "sources.json"), texts);
|
|
2025
2167
|
}
|
|
2026
|
-
|
|
2168
|
+
fs12.rmSync(staging, { recursive: true, force: true });
|
|
2027
2169
|
const errors = [
|
|
2028
2170
|
...only ? [] : project.config().issues,
|
|
2029
2171
|
...system?.doc.issues ?? [],
|
|
@@ -2032,21 +2174,21 @@ async function buildSite(paths, out, options = {}) {
|
|
|
2032
2174
|
const { absoluteAssets } = options.includeViewer === false ? { absoluteAssets: false } : copyViewer(PKG.viewer, out);
|
|
2033
2175
|
if (publicDir) {
|
|
2034
2176
|
for (const rel of publicFiles2) {
|
|
2035
|
-
const to =
|
|
2036
|
-
if (
|
|
2037
|
-
|
|
2038
|
-
|
|
2177
|
+
const to = path12.join(out, rel);
|
|
2178
|
+
if (fs12.existsSync(to)) continue;
|
|
2179
|
+
fs12.mkdirSync(path12.dirname(to), { recursive: true });
|
|
2180
|
+
fs12.copyFileSync(path12.join(publicDir, rel), to);
|
|
2039
2181
|
}
|
|
2040
2182
|
}
|
|
2041
2183
|
return { out, canvases: canvasIds, frames: inputs.length, errors, absoluteAssets };
|
|
2042
2184
|
}
|
|
2043
2185
|
function copyViewer(viewerDir, out) {
|
|
2044
2186
|
copyTree(viewerDir, out, () => true);
|
|
2045
|
-
const index =
|
|
2046
|
-
const html =
|
|
2187
|
+
const index = path12.join(out, "index.html");
|
|
2188
|
+
const html = fs12.readFileSync(index, "utf8");
|
|
2047
2189
|
const flag2 = "<script>window.__DESIGN_STATIC__=true</script>";
|
|
2048
2190
|
const head = /<head(\s[^>]*)?>/i.exec(html);
|
|
2049
|
-
|
|
2191
|
+
fs12.writeFileSync(
|
|
2050
2192
|
index,
|
|
2051
2193
|
head ? html.slice(0, head.index + head[0].length) + flag2 + html.slice(head.index + head[0].length) : flag2 + html
|
|
2052
2194
|
);
|
|
@@ -2128,8 +2270,8 @@ function progress() {
|
|
|
2128
2270
|
// src/cli/commands/build.ts
|
|
2129
2271
|
function pack(out) {
|
|
2130
2272
|
const archive = `${out.replace(/[\\/]+$/, "")}.tar.gz`;
|
|
2131
|
-
|
|
2132
|
-
const entries =
|
|
2273
|
+
fs13.rmSync(archive, { force: true });
|
|
2274
|
+
const entries = fs13.readdirSync(out);
|
|
2133
2275
|
const result = spawnSync("tar", ["-czf", archive, "-C", out, ...entries], {
|
|
2134
2276
|
stdio: ["ignore", "ignore", "pipe"],
|
|
2135
2277
|
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
@@ -2139,10 +2281,10 @@ function pack(out) {
|
|
|
2139
2281
|
return archive;
|
|
2140
2282
|
}
|
|
2141
2283
|
async function runBuild(paths, canvases, options) {
|
|
2142
|
-
if (!
|
|
2284
|
+
if (!fs13.existsSync(path13.join(PKG.viewer, "index.html"))) {
|
|
2143
2285
|
throw new CliError("The viewer is missing from this foss-design install (dist/viewer). Rebuild or reinstall it.");
|
|
2144
2286
|
}
|
|
2145
|
-
const out =
|
|
2287
|
+
const out = path13.resolve(options.out ?? path13.join(paths.cache, "site"));
|
|
2146
2288
|
if (out === paths.root || out === paths.design) throw new CliError(`Refusing to build into ${out}`);
|
|
2147
2289
|
print(dim(`Building ${canvases.length ? canvases.join(", ") : "every canvas"} and the design system\u2026`));
|
|
2148
2290
|
let result;
|
|
@@ -2167,8 +2309,8 @@ import os from "os";
|
|
|
2167
2309
|
|
|
2168
2310
|
// src/cloud/units.ts
|
|
2169
2311
|
import { createHash as createHash3 } from "crypto";
|
|
2170
|
-
import
|
|
2171
|
-
import
|
|
2312
|
+
import fs14 from "fs";
|
|
2313
|
+
import path14 from "path";
|
|
2172
2314
|
var SYSTEM_UNIT = "system";
|
|
2173
2315
|
var CANVAS_PREFIX = "canvas/";
|
|
2174
2316
|
var LOCAL_ONLY = /* @__PURE__ */ new Set(["cloud.json", "node_modules"]);
|
|
@@ -2196,7 +2338,7 @@ function caseClashes(keys) {
|
|
|
2196
2338
|
}
|
|
2197
2339
|
function unitRoot(paths, key) {
|
|
2198
2340
|
const id2 = unitCanvasId(key);
|
|
2199
|
-
return id2 === null ? paths.design :
|
|
2341
|
+
return id2 === null ? paths.design : path14.join(paths.canvases, id2);
|
|
2200
2342
|
}
|
|
2201
2343
|
function unitRelative(key, rel) {
|
|
2202
2344
|
const id2 = unitCanvasId(key);
|
|
@@ -2214,7 +2356,7 @@ function hashBuffer(data) {
|
|
|
2214
2356
|
return createHash3("sha256").update(data).digest("hex");
|
|
2215
2357
|
}
|
|
2216
2358
|
function hashFile(file) {
|
|
2217
|
-
const data =
|
|
2359
|
+
const data = fs14.readFileSync(file);
|
|
2218
2360
|
return { hash: hashBuffer(data), size: data.length };
|
|
2219
2361
|
}
|
|
2220
2362
|
var warnedLinks = /* @__PURE__ */ new Set();
|
|
@@ -2227,34 +2369,34 @@ function warnLink(rel) {
|
|
|
2227
2369
|
function walk(dir, base, out) {
|
|
2228
2370
|
let entries;
|
|
2229
2371
|
try {
|
|
2230
|
-
entries =
|
|
2372
|
+
entries = fs14.readdirSync(dir, { withFileTypes: true });
|
|
2231
2373
|
} catch {
|
|
2232
2374
|
return;
|
|
2233
2375
|
}
|
|
2234
2376
|
for (const entry of entries) {
|
|
2235
2377
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2236
|
-
const full =
|
|
2378
|
+
const full = path14.join(dir, entry.name);
|
|
2237
2379
|
let isFile = entry.isFile();
|
|
2238
2380
|
let isDir = entry.isDirectory();
|
|
2239
2381
|
if (entry.isSymbolicLink()) {
|
|
2240
2382
|
isDir = false;
|
|
2241
2383
|
isFile = false;
|
|
2242
2384
|
try {
|
|
2243
|
-
const target =
|
|
2244
|
-
const root =
|
|
2245
|
-
if (target.startsWith(root +
|
|
2246
|
-
else warnLink(toPosix(
|
|
2385
|
+
const target = fs14.realpathSync(full);
|
|
2386
|
+
const root = fs14.realpathSync(base);
|
|
2387
|
+
if (target.startsWith(root + path14.sep)) isFile = fs14.statSync(target).isFile();
|
|
2388
|
+
else warnLink(toPosix(path14.relative(base, full)));
|
|
2247
2389
|
} catch {
|
|
2248
2390
|
}
|
|
2249
2391
|
}
|
|
2250
2392
|
if (isDir) walk(full, base, out);
|
|
2251
|
-
else if (isFile) out[toPosix(
|
|
2393
|
+
else if (isFile) out[toPosix(path14.relative(base, full))] = hashFile(full);
|
|
2252
2394
|
}
|
|
2253
2395
|
}
|
|
2254
2396
|
function unitManifest(paths, key) {
|
|
2255
2397
|
const manifest = {};
|
|
2256
2398
|
if (key === SYSTEM_UNIT) {
|
|
2257
|
-
if (
|
|
2399
|
+
if (fs14.existsSync(paths.config) && fs14.statSync(paths.config).isFile())
|
|
2258
2400
|
manifest["design.json"] = hashFile(paths.config);
|
|
2259
2401
|
walk(paths.system, paths.design, manifest);
|
|
2260
2402
|
} else walk(unitRoot(paths, key), paths.design, manifest);
|
|
@@ -2263,13 +2405,13 @@ function unitManifest(paths, key) {
|
|
|
2263
2405
|
function buildManifest(dir) {
|
|
2264
2406
|
const manifest = {};
|
|
2265
2407
|
const visit = (current) => {
|
|
2266
|
-
for (const entry of
|
|
2267
|
-
const full =
|
|
2408
|
+
for (const entry of fs14.readdirSync(current, { withFileTypes: true })) {
|
|
2409
|
+
const full = path14.join(current, entry.name);
|
|
2268
2410
|
if (entry.isDirectory()) visit(full);
|
|
2269
|
-
else if (entry.isFile()) manifest[toPosix(
|
|
2411
|
+
else if (entry.isFile()) manifest[toPosix(path14.relative(dir, full))] = hashFile(full);
|
|
2270
2412
|
}
|
|
2271
2413
|
};
|
|
2272
|
-
if (
|
|
2414
|
+
if (fs14.existsSync(dir)) visit(dir);
|
|
2273
2415
|
return sortManifest(manifest);
|
|
2274
2416
|
}
|
|
2275
2417
|
function sortManifest(manifest) {
|
|
@@ -2278,18 +2420,18 @@ function sortManifest(manifest) {
|
|
|
2278
2420
|
function scanLocal(paths) {
|
|
2279
2421
|
const units = /* @__PURE__ */ new Map();
|
|
2280
2422
|
const notSynced = [];
|
|
2281
|
-
if (!
|
|
2423
|
+
if (!fs14.existsSync(paths.design)) return { units, notSynced };
|
|
2282
2424
|
const system = unitManifest(paths, SYSTEM_UNIT);
|
|
2283
2425
|
if (Object.keys(system).length) units.set(SYSTEM_UNIT, { key: SYSTEM_UNIT, kind: "system", manifest: system });
|
|
2284
|
-
for (const entry of
|
|
2426
|
+
for (const entry of fs14.readdirSync(paths.design, { withFileTypes: true })) {
|
|
2285
2427
|
const { name } = entry;
|
|
2286
2428
|
if (name.startsWith(".") || LOCAL_ONLY.has(name)) continue;
|
|
2287
2429
|
if (name === "design.json" && entry.isFile()) continue;
|
|
2288
2430
|
if ((name === "system" || name === "canvas") && entry.isDirectory()) continue;
|
|
2289
2431
|
notSynced.push(entry.isDirectory() ? `${name}/` : name);
|
|
2290
2432
|
}
|
|
2291
|
-
if (
|
|
2292
|
-
for (const entry of
|
|
2433
|
+
if (fs14.existsSync(paths.canvases) && fs14.statSync(paths.canvases).isDirectory()) {
|
|
2434
|
+
for (const entry of fs14.readdirSync(paths.canvases, { withFileTypes: true })) {
|
|
2293
2435
|
if (entry.name.startsWith(".")) continue;
|
|
2294
2436
|
if (!entry.isDirectory() || !ID_PATTERN.test(entry.name)) {
|
|
2295
2437
|
notSynced.push(`canvas/${entry.name}${entry.isDirectory() ? "/" : ""}`);
|
|
@@ -2303,11 +2445,11 @@ function scanLocal(paths) {
|
|
|
2303
2445
|
return { units, notSynced: notSynced.sort() };
|
|
2304
2446
|
}
|
|
2305
2447
|
function unitBuildDir(paths, key) {
|
|
2306
|
-
return
|
|
2448
|
+
return path14.join(paths.cache, "cloud", "build", ...key.split("/"));
|
|
2307
2449
|
}
|
|
2308
2450
|
async function buildUnit(paths, key, out = unitBuildDir(paths, key)) {
|
|
2309
2451
|
const id2 = unitCanvasId(key);
|
|
2310
|
-
|
|
2452
|
+
fs14.rmSync(out, { recursive: true, force: true });
|
|
2311
2453
|
return buildSite(paths, out, {
|
|
2312
2454
|
canvases: id2 === null ? [] : [id2],
|
|
2313
2455
|
includeSystem: id2 === null,
|
|
@@ -2438,7 +2580,7 @@ var CloudClient = class {
|
|
|
2438
2580
|
}
|
|
2439
2581
|
host;
|
|
2440
2582
|
token;
|
|
2441
|
-
async send(method,
|
|
2583
|
+
async send(method, path31, init) {
|
|
2442
2584
|
const headers = {
|
|
2443
2585
|
"User-Agent": `foss-design/${packageVersion()} (${os.platform()})`,
|
|
2444
2586
|
Accept: "application/json"
|
|
@@ -2446,7 +2588,7 @@ var CloudClient = class {
|
|
|
2446
2588
|
if (this.token) headers.Authorization = `Bearer ${this.token}`;
|
|
2447
2589
|
if (init.type) headers["Content-Type"] = init.type;
|
|
2448
2590
|
try {
|
|
2449
|
-
return await fetch(`${this.host}/api${
|
|
2591
|
+
return await fetch(`${this.host}/api${path31}`, {
|
|
2450
2592
|
method,
|
|
2451
2593
|
headers,
|
|
2452
2594
|
body: init.body,
|
|
@@ -2467,8 +2609,8 @@ var CloudClient = class {
|
|
|
2467
2609
|
throw errorFor(this.host, response.status, body, response.headers.get("retry-after"));
|
|
2468
2610
|
}
|
|
2469
2611
|
/** JSON in, JSON out; any non-2xx answer throws a CloudError. */
|
|
2470
|
-
async request(method,
|
|
2471
|
-
const response = await this.send(method,
|
|
2612
|
+
async request(method, path31, body) {
|
|
2613
|
+
const response = await this.send(method, path31, {
|
|
2472
2614
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
2473
2615
|
type: body === void 0 ? void 0 : "application/json",
|
|
2474
2616
|
timeout: JSON_TIMEOUT
|
|
@@ -2480,7 +2622,7 @@ var CloudClient = class {
|
|
|
2480
2622
|
try {
|
|
2481
2623
|
return JSON.parse(text2);
|
|
2482
2624
|
} catch {
|
|
2483
|
-
throw new CloudError(`${this.host} answered ${method} ${
|
|
2625
|
+
throw new CloudError(`${this.host} answered ${method} ${path31} with something other than JSON`, 0, "bad_response");
|
|
2484
2626
|
}
|
|
2485
2627
|
}
|
|
2486
2628
|
/** POST /device/code. */
|
|
@@ -2614,9 +2756,9 @@ var CloudClient = class {
|
|
|
2614
2756
|
};
|
|
2615
2757
|
|
|
2616
2758
|
// src/cloud/credentials.ts
|
|
2617
|
-
import
|
|
2759
|
+
import fs15 from "fs";
|
|
2618
2760
|
import os2 from "os";
|
|
2619
|
-
import
|
|
2761
|
+
import path15 from "path";
|
|
2620
2762
|
var DEFAULT_CLOUD = "https://app.fossdesign.dev";
|
|
2621
2763
|
function normalizeHost(value) {
|
|
2622
2764
|
let url;
|
|
@@ -2633,14 +2775,14 @@ function cloudHost() {
|
|
|
2633
2775
|
return normalizeHost(process.env.FOSS_DESIGN_CLOUD || DEFAULT_CLOUD);
|
|
2634
2776
|
}
|
|
2635
2777
|
function credentialsFile() {
|
|
2636
|
-
const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME ||
|
|
2637
|
-
return
|
|
2778
|
+
const base = process.platform === "win32" && process.env.APPDATA ? process.env.APPDATA : process.env.XDG_CONFIG_HOME || path15.join(os2.homedir(), ".config");
|
|
2779
|
+
return path15.join(base, "foss-design", "credentials.json");
|
|
2638
2780
|
}
|
|
2639
2781
|
function readAll() {
|
|
2640
2782
|
const file = credentialsFile();
|
|
2641
|
-
if (!
|
|
2783
|
+
if (!fs15.existsSync(file)) return {};
|
|
2642
2784
|
try {
|
|
2643
|
-
const data = JSON.parse(
|
|
2785
|
+
const data = JSON.parse(fs15.readFileSync(file, "utf8"));
|
|
2644
2786
|
return data && typeof data === "object" && !Array.isArray(data) ? data : {};
|
|
2645
2787
|
} catch (error) {
|
|
2646
2788
|
throw new CliError(`${file} is not valid JSON (${error.message}); fix or delete it`);
|
|
@@ -2648,12 +2790,12 @@ function readAll() {
|
|
|
2648
2790
|
}
|
|
2649
2791
|
function writeAll(data) {
|
|
2650
2792
|
const file = credentialsFile();
|
|
2651
|
-
|
|
2793
|
+
fs15.mkdirSync(path15.dirname(file), { recursive: true, mode: 448 });
|
|
2652
2794
|
const temp = `${file}.${process.pid}.tmp`;
|
|
2653
|
-
|
|
2795
|
+
fs15.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
|
|
2654
2796
|
`, { mode: 384 });
|
|
2655
|
-
|
|
2656
|
-
|
|
2797
|
+
fs15.chmodSync(temp, 384);
|
|
2798
|
+
fs15.renameSync(temp, file);
|
|
2657
2799
|
}
|
|
2658
2800
|
function getCredential(host) {
|
|
2659
2801
|
const entry = readAll()[host];
|
|
@@ -2676,17 +2818,17 @@ function requireCredential(host) {
|
|
|
2676
2818
|
}
|
|
2677
2819
|
|
|
2678
2820
|
// src/cloud/state.ts
|
|
2679
|
-
import
|
|
2680
|
-
import
|
|
2681
|
-
var linkFile = (paths) =>
|
|
2682
|
-
var cloudCache = (paths) =>
|
|
2683
|
-
var incomingDir = (paths, key) =>
|
|
2821
|
+
import fs16 from "fs";
|
|
2822
|
+
import path16 from "path";
|
|
2823
|
+
var linkFile = (paths) => path16.join(paths.design, "cloud.json");
|
|
2824
|
+
var cloudCache = (paths) => path16.join(paths.cache, "cloud");
|
|
2825
|
+
var incomingDir = (paths, key) => path16.join(cloudCache(paths), "incoming", ...key.split("/"));
|
|
2684
2826
|
function readLink(paths) {
|
|
2685
2827
|
const file = linkFile(paths);
|
|
2686
|
-
if (!
|
|
2828
|
+
if (!fs16.existsSync(file)) return null;
|
|
2687
2829
|
let data;
|
|
2688
2830
|
try {
|
|
2689
|
-
data = JSON.parse(
|
|
2831
|
+
data = JSON.parse(fs16.readFileSync(file, "utf8"));
|
|
2690
2832
|
} catch (error) {
|
|
2691
2833
|
throw new CliError(`${file} is not valid JSON (${error.message}); run \`design link <project>\` again`);
|
|
2692
2834
|
}
|
|
@@ -2716,9 +2858,9 @@ function writeLink(paths, link) {
|
|
|
2716
2858
|
units: sorted(link.units),
|
|
2717
2859
|
conflicts: sorted(link.conflicts)
|
|
2718
2860
|
};
|
|
2719
|
-
|
|
2861
|
+
fs16.writeFileSync(temp, `${JSON.stringify(data, null, 2)}
|
|
2720
2862
|
`);
|
|
2721
|
-
|
|
2863
|
+
fs16.renameSync(temp, file);
|
|
2722
2864
|
}
|
|
2723
2865
|
var hashes = (manifest) => Object.fromEntries(Object.entries(manifest).map(([file, entry]) => [file, entry.hash]));
|
|
2724
2866
|
function diffFiles(a, b) {
|
|
@@ -2796,8 +2938,8 @@ var byUnit = (a, b) => a === SYSTEM_UNIT ? -1 : b === SYSTEM_UNIT ? 1 : a < b ?
|
|
|
2796
2938
|
var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
|
|
2797
2939
|
|
|
2798
2940
|
// src/cloud/pull.ts
|
|
2799
|
-
import
|
|
2800
|
-
import
|
|
2941
|
+
import fs17 from "fs";
|
|
2942
|
+
import path17 from "path";
|
|
2801
2943
|
|
|
2802
2944
|
// src/capture/frames.ts
|
|
2803
2945
|
var AUTO_VIEWPORT_HEIGHT = 900;
|
|
@@ -2880,34 +3022,34 @@ function checkManifest(key, manifest) {
|
|
|
2880
3022
|
}
|
|
2881
3023
|
}
|
|
2882
3024
|
function writeInside(root, rel, data) {
|
|
2883
|
-
const file =
|
|
3025
|
+
const file = path17.join(root, rel);
|
|
2884
3026
|
if (!isInside(root, file)) throw new CliError(`Refusing to write outside ${root}: ${rel}`);
|
|
2885
|
-
|
|
2886
|
-
if (!isInside(
|
|
3027
|
+
fs17.mkdirSync(path17.dirname(file), { recursive: true });
|
|
3028
|
+
if (!isInside(fs17.realpathSync(root), fs17.realpathSync(path17.dirname(file))))
|
|
2887
3029
|
throw new CliError(`Refusing to write through a link out of ${root}: ${rel}`);
|
|
2888
|
-
if (
|
|
2889
|
-
|
|
3030
|
+
if (fs17.existsSync(file) && fs17.lstatSync(file).isSymbolicLink()) fs17.unlinkSync(file);
|
|
3031
|
+
fs17.writeFileSync(file, data);
|
|
2890
3032
|
}
|
|
2891
3033
|
function pruneEmpty(dir, stop) {
|
|
2892
3034
|
let current = dir;
|
|
2893
3035
|
while (isInside(stop, current) && current !== stop) {
|
|
2894
3036
|
try {
|
|
2895
|
-
if (
|
|
2896
|
-
|
|
3037
|
+
if (fs17.readdirSync(current).length) return;
|
|
3038
|
+
fs17.rmdirSync(current);
|
|
2897
3039
|
} catch {
|
|
2898
3040
|
return;
|
|
2899
3041
|
}
|
|
2900
|
-
current =
|
|
3042
|
+
current = path17.dirname(current);
|
|
2901
3043
|
}
|
|
2902
3044
|
}
|
|
2903
3045
|
function applyUnit(paths, key, local, target, blobs) {
|
|
2904
3046
|
const counts = { added: 0, changed: 0, removed: 0 };
|
|
2905
|
-
const stop = key === SYSTEM_UNIT ? paths.design :
|
|
3047
|
+
const stop = key === SYSTEM_UNIT ? paths.design : path17.dirname(unitRoot(paths, key));
|
|
2906
3048
|
for (const rel of Object.keys(local)) {
|
|
2907
3049
|
if (rel in target) continue;
|
|
2908
|
-
const file =
|
|
2909
|
-
|
|
2910
|
-
pruneEmpty(
|
|
3050
|
+
const file = path17.join(paths.design, rel);
|
|
3051
|
+
fs17.rmSync(file, { force: true });
|
|
3052
|
+
pruneEmpty(path17.dirname(file), stop);
|
|
2911
3053
|
counts.removed++;
|
|
2912
3054
|
}
|
|
2913
3055
|
for (const [rel, entry] of Object.entries(target)) {
|
|
@@ -2970,7 +3112,7 @@ async function pull(ctx, options) {
|
|
|
2970
3112
|
} else if (!s.ahead) {
|
|
2971
3113
|
if (s.conflictRev !== void 0) {
|
|
2972
3114
|
delete link.conflicts[key];
|
|
2973
|
-
|
|
3115
|
+
fs17.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
2974
3116
|
}
|
|
2975
3117
|
report.units.push({ ...reportFor("up_to_date"), rev: s.baseRev });
|
|
2976
3118
|
} else if (!s.changed.length) {
|
|
@@ -3000,14 +3142,14 @@ async function pull(ctx, options) {
|
|
|
3000
3142
|
headRev: unit.headRev,
|
|
3001
3143
|
incoming: incomingDir(paths, key)
|
|
3002
3144
|
};
|
|
3003
|
-
entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${
|
|
3004
|
-
if (s.conflictRev === unit.headRev &&
|
|
3145
|
+
entry.message = `changed here and in the cloud; the cloud version (r${unit.headRev}) is in ${path17.relative(paths.root, entry.incoming)}`;
|
|
3146
|
+
if (s.conflictRev === unit.headRev && fs17.existsSync(entry.incoming)) report.units.push(entry);
|
|
3005
3147
|
else plans.push({ key, kind: "incoming", remote: unit, report: entry });
|
|
3006
3148
|
}
|
|
3007
3149
|
}
|
|
3008
3150
|
const byHash = /* @__PURE__ */ new Map();
|
|
3009
3151
|
for (const unit of scan.units.values())
|
|
3010
|
-
for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash,
|
|
3152
|
+
for (const [rel, entry] of Object.entries(unit.manifest)) byHash.set(entry.hash, path17.join(paths.design, rel));
|
|
3011
3153
|
const needed = /* @__PURE__ */ new Set();
|
|
3012
3154
|
for (const plan of plans) {
|
|
3013
3155
|
if (!plan.remote) continue;
|
|
@@ -3018,7 +3160,7 @@ async function pull(ctx, options) {
|
|
|
3018
3160
|
const download = [];
|
|
3019
3161
|
for (const hash of needed) {
|
|
3020
3162
|
const file = byHash.get(hash);
|
|
3021
|
-
if (file) blobs.set(hash,
|
|
3163
|
+
if (file) blobs.set(hash, fs17.readFileSync(file));
|
|
3022
3164
|
else download.push(hash);
|
|
3023
3165
|
}
|
|
3024
3166
|
if (download.length) {
|
|
@@ -3044,11 +3186,11 @@ async function pull(ctx, options) {
|
|
|
3044
3186
|
if (plan.remote) link.units[key] = { rev: plan.remote.headRev, files: hashes(plan.remote.manifest) };
|
|
3045
3187
|
else delete link.units[key];
|
|
3046
3188
|
delete link.conflicts[key];
|
|
3047
|
-
|
|
3189
|
+
fs17.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3048
3190
|
} else {
|
|
3049
3191
|
const dir = incomingDir(paths, key);
|
|
3050
|
-
|
|
3051
|
-
|
|
3192
|
+
fs17.rmSync(dir, { recursive: true, force: true });
|
|
3193
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
3052
3194
|
for (const [rel, file] of Object.entries(plan.remote.manifest))
|
|
3053
3195
|
writeInside(dir, unitRelative(key, rel), blobs.get(file.hash));
|
|
3054
3196
|
link.conflicts[key] = plan.remote.headRev;
|
|
@@ -3069,8 +3211,8 @@ async function pull(ctx, options) {
|
|
|
3069
3211
|
}
|
|
3070
3212
|
|
|
3071
3213
|
// src/cloud/push.ts
|
|
3072
|
-
import
|
|
3073
|
-
import
|
|
3214
|
+
import fs18 from "fs";
|
|
3215
|
+
import path18 from "path";
|
|
3074
3216
|
function blockedReport(status, message) {
|
|
3075
3217
|
return {
|
|
3076
3218
|
unit: status.key,
|
|
@@ -3176,7 +3318,7 @@ async function push(ctx, options) {
|
|
|
3176
3318
|
for (const key of toPush) {
|
|
3177
3319
|
const s = status(key);
|
|
3178
3320
|
if (s.conflictRev !== void 0) {
|
|
3179
|
-
const dir =
|
|
3321
|
+
const dir = path18.relative(paths.root, incomingDir(paths, key));
|
|
3180
3322
|
blocked.push(
|
|
3181
3323
|
blockedReport(s, `pull left the cloud version in ${dir}; merge it, then \`design push --resolved ${key}\``)
|
|
3182
3324
|
);
|
|
@@ -3220,7 +3362,7 @@ async function push(ctx, options) {
|
|
|
3220
3362
|
const files = /* @__PURE__ */ new Map();
|
|
3221
3363
|
const addFiles = (manifest, dir) => {
|
|
3222
3364
|
for (const [rel, entry] of Object.entries(manifest))
|
|
3223
|
-
if (!files.has(entry.hash)) files.set(entry.hash,
|
|
3365
|
+
if (!files.has(entry.hash)) files.set(entry.hash, path18.join(dir, rel));
|
|
3224
3366
|
};
|
|
3225
3367
|
for (const key of pushing) {
|
|
3226
3368
|
const id2 = unitCanvasId(key);
|
|
@@ -3252,7 +3394,7 @@ async function push(ctx, options) {
|
|
|
3252
3394
|
if (missing.length) {
|
|
3253
3395
|
const unknown = missing.find((hash) => !files.has(hash));
|
|
3254
3396
|
if (unknown) throw new CliError(`The cloud asked for ${unknown.slice(0, 12)}, which is not part of this push`);
|
|
3255
|
-
const bytes = missing.reduce((sum, hash) => sum +
|
|
3397
|
+
const bytes = missing.reduce((sum, hash) => sum + fs18.statSync(files.get(hash)).size, 0);
|
|
3256
3398
|
const bar = ctx.progress();
|
|
3257
3399
|
const started = Date.now();
|
|
3258
3400
|
let sentFiles = 0;
|
|
@@ -3261,12 +3403,12 @@ async function push(ctx, options) {
|
|
|
3261
3403
|
bar.update(line());
|
|
3262
3404
|
await mapLimit(missing, TRANSFERS, async (hash) => {
|
|
3263
3405
|
const file = files.get(hash);
|
|
3264
|
-
const data =
|
|
3406
|
+
const data = fs18.readFileSync(file);
|
|
3265
3407
|
try {
|
|
3266
3408
|
await client.putBlob(link.project, hash, data);
|
|
3267
3409
|
} catch (error) {
|
|
3268
3410
|
if (error instanceof CloudError && error.status === 413)
|
|
3269
|
-
throw new CliError(`${
|
|
3411
|
+
throw new CliError(`${path18.relative(process.cwd(), file)} (${megabytes(data.length)}): ${error.message}`);
|
|
3270
3412
|
throw error;
|
|
3271
3413
|
}
|
|
3272
3414
|
sentFiles++;
|
|
@@ -3288,7 +3430,7 @@ async function push(ctx, options) {
|
|
|
3288
3430
|
url: webUrl(link, unit.key)
|
|
3289
3431
|
});
|
|
3290
3432
|
}
|
|
3291
|
-
for (const key of resolved)
|
|
3433
|
+
for (const key of resolved) fs18.rmSync(incomingDir(paths, key), { recursive: true, force: true });
|
|
3292
3434
|
writeLink(paths, link);
|
|
3293
3435
|
report.units.sort((a, b) => byUnit(a.unit, b.unit));
|
|
3294
3436
|
return report;
|
|
@@ -3299,7 +3441,7 @@ function refuseLegacyGo(paths, keys) {
|
|
|
3299
3441
|
for (const key of keys) {
|
|
3300
3442
|
if (!key.startsWith("canvas/")) continue;
|
|
3301
3443
|
const id2 = key.slice("canvas/".length);
|
|
3302
|
-
for (const file of canvasCodeFiles(
|
|
3444
|
+
for (const file of canvasCodeFiles(path18.join(paths.canvases, id2), project.canvas(id2)?.screens ?? [])) {
|
|
3303
3445
|
for (const line of legacyGoLines(file)) hits.add(`${relToRoot(paths, file)}:${line}`);
|
|
3304
3446
|
}
|
|
3305
3447
|
}
|
|
@@ -3620,7 +3762,7 @@ async function runMe(paths, options) {
|
|
|
3620
3762
|
}
|
|
3621
3763
|
|
|
3622
3764
|
// src/capture/chrome.ts
|
|
3623
|
-
import
|
|
3765
|
+
import fs19 from "fs";
|
|
3624
3766
|
var CANDIDATES = {
|
|
3625
3767
|
darwin: [
|
|
3626
3768
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
@@ -3644,8 +3786,8 @@ var CANDIDATES = {
|
|
|
3644
3786
|
};
|
|
3645
3787
|
function findChrome() {
|
|
3646
3788
|
const configured = process.env.DESIGN_CHROME;
|
|
3647
|
-
if (configured &&
|
|
3648
|
-
for (const candidate of CANDIDATES[process.platform] ?? []) if (
|
|
3789
|
+
if (configured && fs19.existsSync(configured)) return configured;
|
|
3790
|
+
for (const candidate of CANDIDATES[process.platform] ?? []) if (fs19.existsSync(candidate)) return candidate;
|
|
3649
3791
|
return null;
|
|
3650
3792
|
}
|
|
3651
3793
|
async function launchChrome() {
|
|
@@ -3676,13 +3818,19 @@ async function openFrame(page, url, options = {}) {
|
|
|
3676
3818
|
record2(text2);
|
|
3677
3819
|
});
|
|
3678
3820
|
let ready = false;
|
|
3821
|
+
let reloads = 0;
|
|
3822
|
+
const onNavigate = (frame) => {
|
|
3823
|
+
if (frame === page.mainFrame()) reloads++;
|
|
3824
|
+
};
|
|
3679
3825
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
3680
3826
|
errors = [];
|
|
3827
|
+
reloads = 0;
|
|
3681
3828
|
ready = options.waitForReady === false;
|
|
3682
3829
|
const turnedAway = new Promise((resolve) => {
|
|
3683
3830
|
outdated = () => resolve("outdated");
|
|
3684
3831
|
});
|
|
3685
3832
|
await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
|
|
3833
|
+
page.on("framenavigated", onNavigate);
|
|
3686
3834
|
if (!ready) {
|
|
3687
3835
|
const signal = page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
|
|
3688
3836
|
timeout: options.timeoutMs ?? 2e4
|
|
@@ -3691,10 +3839,12 @@ async function openFrame(page, url, options = {}) {
|
|
|
3691
3839
|
});
|
|
3692
3840
|
ready = await Promise.race([signal, turnedAway]).catch(() => null) === "ready";
|
|
3693
3841
|
}
|
|
3694
|
-
if (
|
|
3842
|
+
if (ready || options.waitForReady === false) await page.waitForTimeout(options.settleMs ?? 700);
|
|
3843
|
+
page.off("framenavigated", onNavigate);
|
|
3844
|
+
if (!reloads && !errors.some((error) => OUTDATED_DEP.test(error))) break;
|
|
3695
3845
|
}
|
|
3846
|
+
if (reloads) errors.push(`the page reloaded itself ${reloads === 1 ? "once" : `${reloads} times`} while it loaded`);
|
|
3696
3847
|
const held = ready ? 0 : await page.evaluate(() => window.__DESIGN_HOLDS__ ?? 0).catch(() => 0);
|
|
3697
|
-
await page.waitForTimeout(options.settleMs ?? 700);
|
|
3698
3848
|
const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
|
|
3699
3849
|
for (const error of runtimeErrors) if (!errors.includes(error)) errors.push(error);
|
|
3700
3850
|
const overlay = await page.evaluate(() => {
|
|
@@ -3709,26 +3859,26 @@ async function openFrame(page, url, options = {}) {
|
|
|
3709
3859
|
}
|
|
3710
3860
|
|
|
3711
3861
|
// src/server/state.ts
|
|
3712
|
-
import
|
|
3713
|
-
import
|
|
3714
|
-
var stateFile = (paths) =>
|
|
3715
|
-
var logFile = (paths) =>
|
|
3862
|
+
import fs20 from "fs";
|
|
3863
|
+
import path19 from "path";
|
|
3864
|
+
var stateFile = (paths) => path19.join(paths.cache, "server.json");
|
|
3865
|
+
var logFile = (paths) => path19.join(paths.cache, "server.log");
|
|
3716
3866
|
function readState(paths) {
|
|
3717
3867
|
try {
|
|
3718
|
-
return JSON.parse(
|
|
3868
|
+
return JSON.parse(fs20.readFileSync(stateFile(paths), "utf8"));
|
|
3719
3869
|
} catch {
|
|
3720
3870
|
return null;
|
|
3721
3871
|
}
|
|
3722
3872
|
}
|
|
3723
3873
|
function writeState(paths, state) {
|
|
3724
|
-
|
|
3725
|
-
|
|
3874
|
+
fs20.mkdirSync(paths.cache, { recursive: true });
|
|
3875
|
+
fs20.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
|
|
3726
3876
|
`);
|
|
3727
3877
|
}
|
|
3728
3878
|
function clearState(paths, pid) {
|
|
3729
3879
|
const state = readState(paths);
|
|
3730
3880
|
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
3731
|
-
|
|
3881
|
+
fs20.rmSync(stateFile(paths), { force: true });
|
|
3732
3882
|
}
|
|
3733
3883
|
function pidAlive(pid) {
|
|
3734
3884
|
try {
|
|
@@ -3753,18 +3903,18 @@ async function liveServer(paths) {
|
|
|
3753
3903
|
|
|
3754
3904
|
// src/cli/commands/preview.ts
|
|
3755
3905
|
import { spawn } from "child_process";
|
|
3756
|
-
import
|
|
3906
|
+
import fs24 from "fs";
|
|
3757
3907
|
|
|
3758
3908
|
// src/server/index.ts
|
|
3759
|
-
import
|
|
3909
|
+
import fs23 from "fs";
|
|
3760
3910
|
import http from "http";
|
|
3761
|
-
import
|
|
3911
|
+
import path22 from "path";
|
|
3762
3912
|
import sirv from "sirv";
|
|
3763
3913
|
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
3764
3914
|
|
|
3765
3915
|
// src/server/api.ts
|
|
3766
|
-
import
|
|
3767
|
-
import
|
|
3916
|
+
import fs21 from "fs";
|
|
3917
|
+
import path20 from "path";
|
|
3768
3918
|
function sendJson(res, status, body) {
|
|
3769
3919
|
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
3770
3920
|
res.end(JSON.stringify(body));
|
|
@@ -3815,17 +3965,34 @@ async function handleApi(ctx, req, res, url) {
|
|
|
3815
3965
|
}
|
|
3816
3966
|
if (route2 === "/source") {
|
|
3817
3967
|
const rel = url.searchParams.get("path") ?? "";
|
|
3818
|
-
const file =
|
|
3819
|
-
if (!rel || !isInside(project.paths.root, file) || !
|
|
3968
|
+
const file = path20.resolve(project.paths.root, rel);
|
|
3969
|
+
if (!rel || !isInside(project.paths.root, file) || !fs21.existsSync(file) || fs21.statSync(file).isDirectory()) {
|
|
3820
3970
|
sendJson(res, 404, { error: "not found" });
|
|
3821
3971
|
return true;
|
|
3822
3972
|
}
|
|
3823
|
-
if (
|
|
3973
|
+
if (fs21.statSync(file).size > 2 * 1024 * 1024) {
|
|
3824
3974
|
sendJson(res, 413, { error: "file too large to show" });
|
|
3825
3975
|
return true;
|
|
3826
3976
|
}
|
|
3827
3977
|
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
3828
|
-
res.end(
|
|
3978
|
+
res.end(fs21.readFileSync(file, "utf8"));
|
|
3979
|
+
return true;
|
|
3980
|
+
}
|
|
3981
|
+
if (route2 === "/icon" && (method === "PUT" || method === "DELETE")) {
|
|
3982
|
+
try {
|
|
3983
|
+
const body = method === "PUT" ? await readBody(req, MAX_ICON_BYTES * 4) : null;
|
|
3984
|
+
const problem = body ? iconProblem(body) : null;
|
|
3985
|
+
if (problem) {
|
|
3986
|
+
sendJson(res, 400, { error: problem });
|
|
3987
|
+
return true;
|
|
3988
|
+
}
|
|
3989
|
+
writeIcon(project.paths, body);
|
|
3990
|
+
project.invalidateSystem();
|
|
3991
|
+
events.send({ type: "project" });
|
|
3992
|
+
sendJson(res, 200, { icon: project.iconUrl() });
|
|
3993
|
+
} catch (error) {
|
|
3994
|
+
sendJson(res, 400, { error: error.message });
|
|
3995
|
+
}
|
|
3829
3996
|
return true;
|
|
3830
3997
|
}
|
|
3831
3998
|
const snapshotPost = /^\/snapshots\/([^/]+)\/([^/]+)$/.exec(route2);
|
|
@@ -3855,7 +4022,7 @@ async function handleApi(ctx, req, res, url) {
|
|
|
3855
4022
|
return true;
|
|
3856
4023
|
}
|
|
3857
4024
|
res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
|
|
3858
|
-
|
|
4025
|
+
fs21.createReadStream(file).pipe(res);
|
|
3859
4026
|
return true;
|
|
3860
4027
|
}
|
|
3861
4028
|
return false;
|
|
@@ -3895,8 +4062,8 @@ var EventHub = class {
|
|
|
3895
4062
|
};
|
|
3896
4063
|
|
|
3897
4064
|
// src/server/snapshots.ts
|
|
3898
|
-
import
|
|
3899
|
-
import
|
|
4065
|
+
import fs22 from "fs";
|
|
4066
|
+
import path21 from "path";
|
|
3900
4067
|
var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
|
|
3901
4068
|
function isSafeSegment(value) {
|
|
3902
4069
|
return SAFE.test(value);
|
|
@@ -3910,14 +4077,14 @@ var SnapshotStore = class {
|
|
|
3910
4077
|
urlBase;
|
|
3911
4078
|
meta = /* @__PURE__ */ new Map();
|
|
3912
4079
|
metaFile(canvas) {
|
|
3913
|
-
return
|
|
4080
|
+
return path21.join(this.dir, canvas, "meta.json");
|
|
3914
4081
|
}
|
|
3915
4082
|
load(canvas) {
|
|
3916
4083
|
const cached = this.meta.get(canvas);
|
|
3917
4084
|
if (cached) return cached;
|
|
3918
4085
|
let data = {};
|
|
3919
4086
|
try {
|
|
3920
|
-
data = JSON.parse(
|
|
4087
|
+
data = JSON.parse(fs22.readFileSync(this.metaFile(canvas), "utf8"));
|
|
3921
4088
|
} catch {
|
|
3922
4089
|
}
|
|
3923
4090
|
this.meta.set(canvas, data);
|
|
@@ -3943,21 +4110,21 @@ var SnapshotStore = class {
|
|
|
3943
4110
|
};
|
|
3944
4111
|
save(canvas, id2, theme2, png, height) {
|
|
3945
4112
|
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
|
|
3946
|
-
const dir =
|
|
3947
|
-
|
|
3948
|
-
|
|
4113
|
+
const dir = path21.join(this.dir, canvas);
|
|
4114
|
+
fs22.mkdirSync(dir, { recursive: true });
|
|
4115
|
+
fs22.writeFileSync(path21.join(dir, `${id2}.${theme2}.png`), png);
|
|
3949
4116
|
const data = this.load(canvas);
|
|
3950
4117
|
const version = Date.now();
|
|
3951
4118
|
data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
|
|
3952
|
-
|
|
4119
|
+
fs22.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
|
|
3953
4120
|
return { url: this.url(canvas, id2, theme2, version) };
|
|
3954
4121
|
}
|
|
3955
4122
|
/** Absolute path of `<id>.<theme>.png`, or of its thumbnail for `<id>.<theme>.thumb.png` (made on first use); null when missing. */
|
|
3956
4123
|
file(canvas, name) {
|
|
3957
4124
|
const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)(\.thumb)?\.png$/i.exec(name);
|
|
3958
4125
|
if (!isSafeSegment(canvas) || !match) return null;
|
|
3959
|
-
const file =
|
|
3960
|
-
if (!
|
|
4126
|
+
const file = path21.join(this.dir, canvas, `${match[1]}.${match[2]}.png`);
|
|
4127
|
+
if (!fs22.existsSync(file)) return null;
|
|
3961
4128
|
return match[3] ? ensureThumb(file) : file;
|
|
3962
4129
|
}
|
|
3963
4130
|
};
|
|
@@ -3996,16 +4163,16 @@ function notFoundPage(res, title, text2) {
|
|
|
3996
4163
|
}
|
|
3997
4164
|
async function startDevServer(options) {
|
|
3998
4165
|
const paths = designPaths(options.root);
|
|
3999
|
-
if (!
|
|
4000
|
-
|
|
4166
|
+
if (!fs23.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
|
|
4167
|
+
fs23.mkdirSync(paths.cache, { recursive: true });
|
|
4001
4168
|
const snapshots = new SnapshotStore(paths.snapshots);
|
|
4002
4169
|
const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
|
|
4003
4170
|
const entries = new Entries(project);
|
|
4004
4171
|
entries.sync();
|
|
4005
|
-
const links = linkShippedPackages(paths);
|
|
4172
|
+
const links = linkShippedPackages(paths, project.appDir());
|
|
4006
4173
|
const events = new EventHub();
|
|
4007
4174
|
const httpServer = http.createServer();
|
|
4008
|
-
dropStaleDepCache(paths);
|
|
4175
|
+
dropStaleDepCache(paths, project.appDir());
|
|
4009
4176
|
const base = baseConfig(project);
|
|
4010
4177
|
const vite = await createServer({
|
|
4011
4178
|
...base,
|
|
@@ -4020,11 +4187,20 @@ async function startDevServer(options) {
|
|
|
4020
4187
|
allow: [searchForWorkspaceRoot(paths.root), paths.root, paths.design, PKG.root, ...links.allow]
|
|
4021
4188
|
},
|
|
4022
4189
|
watch: {
|
|
4023
|
-
|
|
4024
|
-
|
|
4190
|
+
// `.cache` is ours (server.log, snapshots, builds): a change there must not reload frames. The
|
|
4191
|
+
// generated entries are modules the frames load, so those stay watched.
|
|
4192
|
+
ignored: (file) => {
|
|
4193
|
+
const rel = toPosix(path22.relative(paths.cache, file));
|
|
4194
|
+
const inCache = rel !== "" && !rel.startsWith("..") && !path22.isAbsolute(rel);
|
|
4195
|
+
return inCache && rel !== "entries" && !rel.startsWith("entries/");
|
|
4196
|
+
}
|
|
4197
|
+
},
|
|
4198
|
+
// Vite forwards frames' console warnings to the server when an agent runs it; the runtime
|
|
4199
|
+
// already reports errors, and the log they would fill sits in `.cache`.
|
|
4200
|
+
forwardConsole: false
|
|
4025
4201
|
},
|
|
4026
4202
|
optimizeDeps: {
|
|
4027
|
-
entries: entries.files().map((file) => toPosix(
|
|
4203
|
+
entries: entries.files().map((file) => toPosix(path22.relative(paths.design, file)))
|
|
4028
4204
|
}
|
|
4029
4205
|
});
|
|
4030
4206
|
const publicDir = project.publicDir();
|
|
@@ -4036,16 +4212,16 @@ async function startDevServer(options) {
|
|
|
4036
4212
|
return null;
|
|
4037
4213
|
}
|
|
4038
4214
|
};
|
|
4039
|
-
const viewer =
|
|
4040
|
-
const viewerIndex =
|
|
4215
|
+
const viewer = fs23.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
|
|
4216
|
+
const viewerIndex = path22.join(PKG.viewer, "index.html");
|
|
4041
4217
|
const serveViewerIndex = (res) => {
|
|
4042
|
-
const html =
|
|
4218
|
+
const html = fs23.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
|
|
4043
4219
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
4044
4220
|
res.end(html);
|
|
4045
4221
|
};
|
|
4046
4222
|
const ctx = { project, events, snapshots };
|
|
4047
4223
|
const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
|
|
4048
|
-
const bootUrl = `/${relToDesign(paths,
|
|
4224
|
+
const bootUrl = `/${relToDesign(paths, path22.join(entries.dir, HTML_BOOT))}`;
|
|
4049
4225
|
const frameHead = (source, extra = {}) => {
|
|
4050
4226
|
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
4051
4227
|
return {
|
|
@@ -4066,7 +4242,7 @@ async function startDevServer(options) {
|
|
|
4066
4242
|
notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
|
|
4067
4243
|
return;
|
|
4068
4244
|
}
|
|
4069
|
-
if (!
|
|
4245
|
+
if (!fs23.existsSync(entries.entryFile(source))) entries.sync();
|
|
4070
4246
|
const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
|
|
4071
4247
|
const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
|
|
4072
4248
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -4075,8 +4251,8 @@ async function startDevServer(options) {
|
|
|
4075
4251
|
};
|
|
4076
4252
|
const serveHtmlScreen = async (res, url2) => {
|
|
4077
4253
|
const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
|
|
4078
|
-
const file =
|
|
4079
|
-
if (!isInside(paths.design, file) || !
|
|
4254
|
+
const file = path22.resolve(paths.design, rel);
|
|
4255
|
+
if (!isInside(paths.design, file) || !fs23.existsSync(file)) {
|
|
4080
4256
|
notFoundPage(res, "Screen not found", `No file .design/${rel}`);
|
|
4081
4257
|
return;
|
|
4082
4258
|
}
|
|
@@ -4086,13 +4262,13 @@ async function startDevServer(options) {
|
|
|
4086
4262
|
key: `${canvas}/${id2}`,
|
|
4087
4263
|
canvas,
|
|
4088
4264
|
id: id2,
|
|
4089
|
-
title:
|
|
4265
|
+
title: path22.basename(file),
|
|
4090
4266
|
file,
|
|
4091
4267
|
format: "html",
|
|
4092
4268
|
props: {},
|
|
4093
4269
|
system: true
|
|
4094
4270
|
};
|
|
4095
|
-
const raw =
|
|
4271
|
+
const raw = fs23.readFileSync(file, "utf8");
|
|
4096
4272
|
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
4097
4273
|
const html = injectIntoHtml(
|
|
4098
4274
|
raw,
|
|
@@ -4125,7 +4301,7 @@ async function startDevServer(options) {
|
|
|
4125
4301
|
}
|
|
4126
4302
|
if (url2.pathname.startsWith("/_fs/")) {
|
|
4127
4303
|
const rel = publicRel(url2.pathname.slice("/_fs/".length));
|
|
4128
|
-
if (publicDir && rel !== null && !project.isPublicFile(rel) &&
|
|
4304
|
+
if (publicDir && rel !== null && !project.isPublicFile(rel) && fs23.existsSync(path22.join(publicDir, rel))) {
|
|
4129
4305
|
notFoundPage(res, "Not found", url2.pathname);
|
|
4130
4306
|
return;
|
|
4131
4307
|
}
|
|
@@ -4137,7 +4313,7 @@ async function startDevServer(options) {
|
|
|
4137
4313
|
const rel = publicRel(url2.pathname.slice(1));
|
|
4138
4314
|
if (servePublic && rel !== null && project.isPublicFile(rel))
|
|
4139
4315
|
servePublic(req, res, () => notFoundPage(res, "Not found", url2.pathname));
|
|
4140
|
-
else if (
|
|
4316
|
+
else if (path22.extname(url2.pathname)) notFoundPage(res, "Not found", url2.pathname);
|
|
4141
4317
|
else serveViewerIndex(res);
|
|
4142
4318
|
};
|
|
4143
4319
|
if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
|
|
@@ -4165,7 +4341,7 @@ async function startDevServer(options) {
|
|
|
4165
4341
|
};
|
|
4166
4342
|
vite.watcher.on("all", (event, file) => {
|
|
4167
4343
|
if (publicDir && isInside(publicDir, file)) project.invalidatePublic();
|
|
4168
|
-
const rel = toPosix(
|
|
4344
|
+
const rel = toPosix(path22.relative(paths.design, file));
|
|
4169
4345
|
if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
|
|
4170
4346
|
if (rel === "design.json") {
|
|
4171
4347
|
queue("config", () => {
|
|
@@ -4273,15 +4449,15 @@ async function ensureServer(paths, options) {
|
|
|
4273
4449
|
if (stale)
|
|
4274
4450
|
print(dim(`Restarting the preview server: it runs foss-design ${live2.version}, this is ${packageVersion()}.`));
|
|
4275
4451
|
if (live2) await stopServer(paths);
|
|
4276
|
-
|
|
4277
|
-
const log =
|
|
4452
|
+
fs24.mkdirSync(paths.cache, { recursive: true });
|
|
4453
|
+
const log = fs24.openSync(logFile(paths), "w");
|
|
4278
4454
|
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
4279
4455
|
if (options.port) args.push("--port", String(options.port));
|
|
4280
4456
|
const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
|
|
4281
4457
|
child.unref();
|
|
4282
4458
|
const state = await waitForServer(paths, 4e4);
|
|
4283
4459
|
if (!state) {
|
|
4284
|
-
const tail =
|
|
4460
|
+
const tail = fs24.existsSync(logFile(paths)) ? fs24.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
|
|
4285
4461
|
throw new CliError(`The preview server did not start.
|
|
4286
4462
|
${tail.join("\n")}`);
|
|
4287
4463
|
}
|
|
@@ -4331,7 +4507,7 @@ async function renderFrames(jobs) {
|
|
|
4331
4507
|
try {
|
|
4332
4508
|
report = await openFrame(page, job.url, { waitForReady: job.waitForReady, settleMs: 300 });
|
|
4333
4509
|
links = await page.evaluate(
|
|
4334
|
-
() => [...document.querySelectorAll("a[href], area[href]")].map((link) => ({
|
|
4510
|
+
() => [...document.querySelectorAll("a[href]:not([download]), area[href]:not([download])")].map((link) => ({
|
|
4335
4511
|
href: link.getAttribute("href") ?? "",
|
|
4336
4512
|
text: (link.textContent || link.getAttribute("aria-label") || "").replace(/\s+/g, " ").trim().slice(0, 40)
|
|
4337
4513
|
}))
|
|
@@ -4460,9 +4636,9 @@ function printLinkIssues(issues, project) {
|
|
|
4460
4636
|
}
|
|
4461
4637
|
|
|
4462
4638
|
// src/cli/commands/cloud.ts
|
|
4463
|
-
import
|
|
4639
|
+
import fs25 from "fs";
|
|
4464
4640
|
import os3 from "os";
|
|
4465
|
-
import
|
|
4641
|
+
import path23 from "path";
|
|
4466
4642
|
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4467
4643
|
async function runLogin() {
|
|
4468
4644
|
const host = cloudHost();
|
|
@@ -4563,7 +4739,7 @@ async function runLink(paths, wanted, options) {
|
|
|
4563
4739
|
print(`${dim("Already linked to")} ${project.name} ${dim(`(${project.id}, ${project.role})`)}`);
|
|
4564
4740
|
} else {
|
|
4565
4741
|
writeLink(paths, { host, project: project.id, units: {}, conflicts: {} });
|
|
4566
|
-
|
|
4742
|
+
fs25.rmSync(path23.join(cloudCache(paths), "incoming"), { recursive: true, force: true });
|
|
4567
4743
|
const replaced = current ? dim(` (replacing the link to ${current.project})`) : "";
|
|
4568
4744
|
print(`${green("linked")} .design \u2192 ${bold(project.name)} ${dim(`(${project.id}, ${project.role})`)}${replaced}`);
|
|
4569
4745
|
}
|
|
@@ -4587,9 +4763,774 @@ async function runLink(paths, wanted, options) {
|
|
|
4587
4763
|
else if (remote) print(dim("Everything is in sync."));
|
|
4588
4764
|
}
|
|
4589
4765
|
|
|
4766
|
+
// src/cli/commands/icon.ts
|
|
4767
|
+
import fs26 from "fs";
|
|
4768
|
+
import path24 from "path";
|
|
4769
|
+
function runIcon(paths, file, options) {
|
|
4770
|
+
if (options.remove) {
|
|
4771
|
+
const had = iconFile(paths);
|
|
4772
|
+
writeIcon(paths, null);
|
|
4773
|
+
print(had ? `${green("Removed")} ${path24.relative(process.cwd(), had)}` : "The project has no icon.");
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
if (!file) {
|
|
4777
|
+
const current = iconFile(paths);
|
|
4778
|
+
if (!current) return print("The project has no icon. Set one with `design icon <file.svg|png|webp>`.");
|
|
4779
|
+
return print(`${path24.relative(process.cwd(), current)} ${dim(`${Math.ceil(fs26.statSync(current).size / 1024)} KB`)}`);
|
|
4780
|
+
}
|
|
4781
|
+
let data;
|
|
4782
|
+
try {
|
|
4783
|
+
data = fs26.readFileSync(file);
|
|
4784
|
+
} catch {
|
|
4785
|
+
throw new CliError(`No file ${file}`);
|
|
4786
|
+
}
|
|
4787
|
+
const problem = iconProblem(data, path24.extname(file).slice(1).toLowerCase() || void 0);
|
|
4788
|
+
if (problem) throw new CliError(`${file}: ${problem}`);
|
|
4789
|
+
const written = writeIcon(paths, data);
|
|
4790
|
+
print(`${green("Set")} the project icon: ${path24.relative(process.cwd(), written)}`);
|
|
4791
|
+
print(dim("It syncs with the design system unit: `design push` takes it to the cloud."));
|
|
4792
|
+
}
|
|
4793
|
+
|
|
4794
|
+
// src/cli/commands/import.ts
|
|
4795
|
+
import fs28 from "fs";
|
|
4796
|
+
import path26 from "path";
|
|
4797
|
+
|
|
4798
|
+
// src/import/claude-design.ts
|
|
4799
|
+
import fs27 from "fs";
|
|
4800
|
+
import path25 from "path";
|
|
4801
|
+
|
|
4802
|
+
// src/import/markup.ts
|
|
4803
|
+
var VOID = /* @__PURE__ */ new Set([
|
|
4804
|
+
"area",
|
|
4805
|
+
"base",
|
|
4806
|
+
"br",
|
|
4807
|
+
"col",
|
|
4808
|
+
"embed",
|
|
4809
|
+
"hr",
|
|
4810
|
+
"img",
|
|
4811
|
+
"input",
|
|
4812
|
+
"link",
|
|
4813
|
+
"meta",
|
|
4814
|
+
"source",
|
|
4815
|
+
"track",
|
|
4816
|
+
"wbr"
|
|
4817
|
+
]);
|
|
4818
|
+
var RAW = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
|
|
4819
|
+
var ATTR = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/y;
|
|
4820
|
+
function parseMarkup(source) {
|
|
4821
|
+
const root = { type: "element", tag: "#root", attrs: [], children: [] };
|
|
4822
|
+
const stack = [root];
|
|
4823
|
+
const top = () => stack[stack.length - 1];
|
|
4824
|
+
let i = 0;
|
|
4825
|
+
while (i < source.length) {
|
|
4826
|
+
const lt = source.indexOf("<", i);
|
|
4827
|
+
if (lt === -1) {
|
|
4828
|
+
top().children.push({ type: "text", text: source.slice(i) });
|
|
4829
|
+
break;
|
|
4830
|
+
}
|
|
4831
|
+
if (lt > i) top().children.push({ type: "text", text: source.slice(i, lt) });
|
|
4832
|
+
if (source.startsWith("<!--", lt)) {
|
|
4833
|
+
const end = source.indexOf("-->", lt + 4);
|
|
4834
|
+
i = end === -1 ? source.length : end + 3;
|
|
4835
|
+
continue;
|
|
4836
|
+
}
|
|
4837
|
+
if (source[lt + 1] === "!" || source[lt + 1] === "?") {
|
|
4838
|
+
const end = source.indexOf(">", lt);
|
|
4839
|
+
i = end === -1 ? source.length : end + 1;
|
|
4840
|
+
continue;
|
|
4841
|
+
}
|
|
4842
|
+
if (source[lt + 1] === "/") {
|
|
4843
|
+
const end = source.indexOf(">", lt);
|
|
4844
|
+
const tag = source.slice(lt + 2, end === -1 ? source.length : end).trim();
|
|
4845
|
+
i = end === -1 ? source.length : end + 1;
|
|
4846
|
+
for (let depth = stack.length - 1; depth > 0; depth--) {
|
|
4847
|
+
if (stack[depth].tag.toLowerCase() === tag.toLowerCase()) {
|
|
4848
|
+
stack.length = depth;
|
|
4849
|
+
break;
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
continue;
|
|
4853
|
+
}
|
|
4854
|
+
const name = /^[A-Za-z][\w:-]*/.exec(source.slice(lt + 1));
|
|
4855
|
+
if (!name) {
|
|
4856
|
+
top().children.push({ type: "text", text: "<" });
|
|
4857
|
+
i = lt + 1;
|
|
4858
|
+
continue;
|
|
4859
|
+
}
|
|
4860
|
+
const element = { type: "element", tag: name[0], attrs: [], children: [] };
|
|
4861
|
+
let at = lt + 1 + name[0].length;
|
|
4862
|
+
let selfClosing = false;
|
|
4863
|
+
for (; ; ) {
|
|
4864
|
+
while (at < source.length && /\s/.test(source[at])) at++;
|
|
4865
|
+
if (at >= source.length) break;
|
|
4866
|
+
if (source[at] === ">") {
|
|
4867
|
+
at++;
|
|
4868
|
+
break;
|
|
4869
|
+
}
|
|
4870
|
+
if (source.startsWith("/>", at)) {
|
|
4871
|
+
selfClosing = true;
|
|
4872
|
+
at += 2;
|
|
4873
|
+
break;
|
|
4874
|
+
}
|
|
4875
|
+
ATTR.lastIndex = at;
|
|
4876
|
+
const match = ATTR.exec(source);
|
|
4877
|
+
if (!match) {
|
|
4878
|
+
at++;
|
|
4879
|
+
continue;
|
|
4880
|
+
}
|
|
4881
|
+
element.attrs.push([match[1], match[2] ?? match[3] ?? match[4] ?? null]);
|
|
4882
|
+
at = ATTR.lastIndex;
|
|
4883
|
+
}
|
|
4884
|
+
top().children.push(element);
|
|
4885
|
+
const lower = element.tag.toLowerCase();
|
|
4886
|
+
if (RAW.has(lower) && !selfClosing) {
|
|
4887
|
+
const close = source.toLowerCase().indexOf(`</${lower}`, at);
|
|
4888
|
+
const end = close === -1 ? source.length : close;
|
|
4889
|
+
element.children.push({ type: "text", text: source.slice(at, end) });
|
|
4890
|
+
const gt = close === -1 ? -1 : source.indexOf(">", close);
|
|
4891
|
+
i = gt === -1 ? source.length : gt + 1;
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
if (!selfClosing && !VOID.has(lower)) stack.push(element);
|
|
4895
|
+
i = at;
|
|
4896
|
+
}
|
|
4897
|
+
return root;
|
|
4898
|
+
}
|
|
4899
|
+
function findElement(node, test) {
|
|
4900
|
+
for (const child of node.children) {
|
|
4901
|
+
if (child.type !== "element") continue;
|
|
4902
|
+
if (test(child)) return child;
|
|
4903
|
+
const found = findElement(child, test);
|
|
4904
|
+
if (found) return found;
|
|
4905
|
+
}
|
|
4906
|
+
return null;
|
|
4907
|
+
}
|
|
4908
|
+
var attr = (element, name) => element.attrs.find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
|
|
4909
|
+
var NAMED = {
|
|
4910
|
+
amp: "&",
|
|
4911
|
+
lt: "<",
|
|
4912
|
+
gt: ">",
|
|
4913
|
+
quot: '"',
|
|
4914
|
+
apos: "'",
|
|
4915
|
+
nbsp: "\xA0",
|
|
4916
|
+
thinsp: "\u2009",
|
|
4917
|
+
ensp: "\u2002",
|
|
4918
|
+
emsp: "\u2003",
|
|
4919
|
+
mdash: "\u2014",
|
|
4920
|
+
ndash: "\u2013",
|
|
4921
|
+
minus: "\u2212",
|
|
4922
|
+
hellip: "\u2026",
|
|
4923
|
+
middot: "\xB7",
|
|
4924
|
+
bull: "\u2022",
|
|
4925
|
+
laquo: "\xAB",
|
|
4926
|
+
raquo: "\xBB",
|
|
4927
|
+
lsquo: "\u2018",
|
|
4928
|
+
rsquo: "\u2019",
|
|
4929
|
+
ldquo: "\u201C",
|
|
4930
|
+
rdquo: "\u201D",
|
|
4931
|
+
times: "\xD7",
|
|
4932
|
+
divide: "\xF7",
|
|
4933
|
+
deg: "\xB0",
|
|
4934
|
+
copy: "\xA9",
|
|
4935
|
+
reg: "\xAE",
|
|
4936
|
+
trade: "\u2122",
|
|
4937
|
+
euro: "\u20AC",
|
|
4938
|
+
pound: "\xA3",
|
|
4939
|
+
yen: "\xA5",
|
|
4940
|
+
cent: "\xA2",
|
|
4941
|
+
sect: "\xA7",
|
|
4942
|
+
para: "\xB6",
|
|
4943
|
+
plusmn: "\xB1",
|
|
4944
|
+
le: "\u2264",
|
|
4945
|
+
ge: "\u2265",
|
|
4946
|
+
ne: "\u2260",
|
|
4947
|
+
larr: "\u2190",
|
|
4948
|
+
rarr: "\u2192",
|
|
4949
|
+
uarr: "\u2191",
|
|
4950
|
+
darr: "\u2193",
|
|
4951
|
+
harr: "\u2194",
|
|
4952
|
+
check: "\u2713",
|
|
4953
|
+
zwj: "\u200D",
|
|
4954
|
+
zwnj: "\u200C",
|
|
4955
|
+
shy: "\xAD"
|
|
4956
|
+
};
|
|
4957
|
+
function decodeEntities(text2) {
|
|
4958
|
+
return text2.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]*);/gi, (whole, body) => {
|
|
4959
|
+
if (body[0] === "#") {
|
|
4960
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number(body.slice(1));
|
|
4961
|
+
return Number.isFinite(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
4962
|
+
}
|
|
4963
|
+
return NAMED[body.toLowerCase()] ?? whole;
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4966
|
+
|
|
4967
|
+
// src/import/claude-design.ts
|
|
4968
|
+
var EXT = ".dc.html";
|
|
4969
|
+
var kebab = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
|
|
4970
|
+
var identifier = (name) => {
|
|
4971
|
+
const clean = name.replace(/[^\w$]/g, "_");
|
|
4972
|
+
return /^[A-Za-z_$]/.test(clean) ? clean : `_${clean}`;
|
|
4973
|
+
};
|
|
4974
|
+
var KEYWORDS = new Set(
|
|
4975
|
+
"break case catch class const continue debugger default delete do else export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static await async of undefined NaN Infinity arguments eval Math JSON Number String Boolean Object Array Date Intl RegExp Error Map Set Symbol Promise BigInt console window document globalThis location navigator parseInt parseFloat isNaN isFinite encodeURIComponent decodeURIComponent encodeURI decodeURI setTimeout clearTimeout setInterval clearInterval".split(" ")
|
|
4976
|
+
);
|
|
4977
|
+
function freeNames(expression, into) {
|
|
4978
|
+
const code = expression.replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"/g, '""').replace(
|
|
4979
|
+
/`(?:\\.|\$\{([^}]*)\}|[^`\\])*`/g,
|
|
4980
|
+
(whole) => [...whole.matchAll(/\$\{([^}]*)\}/g)].map((match) => ` ${match[1]} `).join("")
|
|
4981
|
+
);
|
|
4982
|
+
for (const match of code.matchAll(/(?<![.\w$])[A-Za-z_$][\w$]*/g)) {
|
|
4983
|
+
const name = match[0];
|
|
4984
|
+
const after = code.slice(match.index + name.length);
|
|
4985
|
+
if (/^\s*:(?!:)/.test(after) && /[{,]\s*$/.test(code.slice(0, match.index))) continue;
|
|
4986
|
+
if (!KEYWORDS.has(name)) into.add(name);
|
|
4987
|
+
}
|
|
4988
|
+
}
|
|
4989
|
+
var WHOLE = /^\s*\{\{([\s\S]+?)\}\}\s*$/;
|
|
4990
|
+
var PARTS = /\{\{([\s\S]+?)\}\}/g;
|
|
4991
|
+
var RENAME = {
|
|
4992
|
+
class: "className",
|
|
4993
|
+
for: "htmlFor",
|
|
4994
|
+
tabindex: "tabIndex",
|
|
4995
|
+
readonly: "readOnly",
|
|
4996
|
+
maxlength: "maxLength",
|
|
4997
|
+
minlength: "minLength",
|
|
4998
|
+
colspan: "colSpan",
|
|
4999
|
+
rowspan: "rowSpan",
|
|
5000
|
+
autocomplete: "autoComplete",
|
|
5001
|
+
autocapitalize: "autoCapitalize",
|
|
5002
|
+
autocorrect: "autoCorrect",
|
|
5003
|
+
autosave: "autoSave",
|
|
5004
|
+
enterkeyhint: "enterKeyHint",
|
|
5005
|
+
accesskey: "accessKey",
|
|
5006
|
+
charset: "charSet",
|
|
5007
|
+
formaction: "formAction",
|
|
5008
|
+
formmethod: "formMethod",
|
|
5009
|
+
formnovalidate: "formNoValidate",
|
|
5010
|
+
formtarget: "formTarget",
|
|
5011
|
+
hreflang: "hrefLang",
|
|
5012
|
+
itemprop: "itemProp",
|
|
5013
|
+
itemscope: "itemScope",
|
|
5014
|
+
itemtype: "itemType",
|
|
5015
|
+
marginheight: "marginHeight",
|
|
5016
|
+
marginwidth: "marginWidth",
|
|
5017
|
+
mediagroup: "mediaGroup",
|
|
5018
|
+
nomodule: "noModule",
|
|
5019
|
+
radiogroup: "radioGroup",
|
|
5020
|
+
srcdoc: "srcDoc",
|
|
5021
|
+
srclang: "srcLang",
|
|
5022
|
+
popovertarget: "popoverTarget",
|
|
5023
|
+
popovertargetaction: "popoverTargetAction",
|
|
5024
|
+
autofocus: "autoFocus",
|
|
5025
|
+
autoplay: "autoPlay",
|
|
5026
|
+
crossorigin: "crossOrigin",
|
|
5027
|
+
srcset: "srcSet",
|
|
5028
|
+
enctype: "encType",
|
|
5029
|
+
novalidate: "noValidate",
|
|
5030
|
+
contenteditable: "contentEditable",
|
|
5031
|
+
spellcheck: "spellCheck",
|
|
5032
|
+
datetime: "dateTime",
|
|
5033
|
+
allowfullscreen: "allowFullScreen",
|
|
5034
|
+
frameborder: "frameBorder",
|
|
5035
|
+
cellpadding: "cellPadding",
|
|
5036
|
+
cellspacing: "cellSpacing",
|
|
5037
|
+
inputmode: "inputMode",
|
|
5038
|
+
usemap: "useMap",
|
|
5039
|
+
referrerpolicy: "referrerPolicy",
|
|
5040
|
+
playsinline: "playsInline",
|
|
5041
|
+
viewbox: "viewBox",
|
|
5042
|
+
"accept-charset": "acceptCharset",
|
|
5043
|
+
"http-equiv": "httpEquiv",
|
|
5044
|
+
"xlink:href": "xlinkHref",
|
|
5045
|
+
"xmlns:xlink": "xmlnsXlink",
|
|
5046
|
+
"xml:space": "xmlSpace",
|
|
5047
|
+
"xml:lang": "xmlLang"
|
|
5048
|
+
};
|
|
5049
|
+
var BOOLEAN = /* @__PURE__ */ new Set([
|
|
5050
|
+
"checked",
|
|
5051
|
+
"disabled",
|
|
5052
|
+
"selected",
|
|
5053
|
+
"readonly",
|
|
5054
|
+
"hidden",
|
|
5055
|
+
"open",
|
|
5056
|
+
"required",
|
|
5057
|
+
"multiple",
|
|
5058
|
+
"autofocus",
|
|
5059
|
+
"novalidate",
|
|
5060
|
+
"playsinline",
|
|
5061
|
+
"muted",
|
|
5062
|
+
"loop",
|
|
5063
|
+
"autoplay",
|
|
5064
|
+
"controls",
|
|
5065
|
+
"default",
|
|
5066
|
+
"reversed",
|
|
5067
|
+
"allowfullscreen",
|
|
5068
|
+
"async",
|
|
5069
|
+
"defer",
|
|
5070
|
+
"inert"
|
|
5071
|
+
]);
|
|
5072
|
+
var NO_TEXT = /* @__PURE__ */ new Set(["table", "thead", "tbody", "tfoot", "tr", "colgroup", "select", "optgroup", "datalist"]);
|
|
5073
|
+
function propName(name) {
|
|
5074
|
+
const lower = name.toLowerCase();
|
|
5075
|
+
if (RENAME[lower]) return RENAME[lower];
|
|
5076
|
+
if (/^on[A-Z]/.test(name)) return name;
|
|
5077
|
+
if (lower.startsWith("aria-") || lower.startsWith("data-")) return lower;
|
|
5078
|
+
if (!/^[A-Za-z][\w:-]*$/.test(name)) return null;
|
|
5079
|
+
return name.includes("-") ? name.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) : name;
|
|
5080
|
+
}
|
|
5081
|
+
var str2 = (value) => JSON.stringify(value);
|
|
5082
|
+
function interpolated(raw, reads) {
|
|
5083
|
+
if (!raw.includes("{{")) return str2(decodeEntities(raw));
|
|
5084
|
+
let out = "`";
|
|
5085
|
+
let last = 0;
|
|
5086
|
+
for (const match of raw.matchAll(PARTS)) {
|
|
5087
|
+
out += decodeEntities(raw.slice(last, match.index)).replace(/[`\\]|\$\{/g, (c) => `\\${c}`);
|
|
5088
|
+
freeNames(match[1], reads);
|
|
5089
|
+
out += "${dc.text(" + match[1].trim() + ")}";
|
|
5090
|
+
last = match.index + match[0].length;
|
|
5091
|
+
}
|
|
5092
|
+
return `${out + decodeEntities(raw.slice(last)).replace(/[`\\]|\$\{/g, (c) => `\\${c}`)}\``;
|
|
5093
|
+
}
|
|
5094
|
+
function linkTarget(value, ctx) {
|
|
5095
|
+
const match = /^(?:\.\/)?([^/?#]+)\.dc\.html(#.*)?$/.exec(value);
|
|
5096
|
+
if (!match) return null;
|
|
5097
|
+
const route2 = ctx.routes.get(match[1]);
|
|
5098
|
+
if (route2) return route2;
|
|
5099
|
+
ctx.warn(`links to ${match[1]}${EXT}, which is no board; the link goes nowhere (#)`);
|
|
5100
|
+
return "#";
|
|
5101
|
+
}
|
|
5102
|
+
function attrsJsx(element, ctx, extra = []) {
|
|
5103
|
+
const tag = element.tag.toLowerCase();
|
|
5104
|
+
const handled = new Set(element.attrs.map(([name]) => name));
|
|
5105
|
+
const hasHandler = [...handled].some((name) => /^on(Change|Input)$/.test(name));
|
|
5106
|
+
const out = [...extra];
|
|
5107
|
+
for (const [name, raw] of element.attrs) {
|
|
5108
|
+
const lower = name.toLowerCase();
|
|
5109
|
+
if (lower.startsWith("hint-") || lower === "xmlns") continue;
|
|
5110
|
+
let prop = propName(name);
|
|
5111
|
+
if (!prop) {
|
|
5112
|
+
ctx.warn(`dropped the attribute "${name}"`);
|
|
5113
|
+
continue;
|
|
5114
|
+
}
|
|
5115
|
+
if (!hasHandler && (tag === "input" || tag === "textarea") && (prop === "value" || prop === "checked"))
|
|
5116
|
+
prop = prop === "value" ? "defaultValue" : "defaultChecked";
|
|
5117
|
+
if (tag === "option" && prop === "selected") continue;
|
|
5118
|
+
if (raw === null) {
|
|
5119
|
+
out.push(BOOLEAN.has(lower) ? prop : `${prop}=""`);
|
|
5120
|
+
continue;
|
|
5121
|
+
}
|
|
5122
|
+
const whole = WHOLE.exec(raw);
|
|
5123
|
+
if (prop === "style") {
|
|
5124
|
+
const body = interpolated(raw, ctx.reads);
|
|
5125
|
+
out.push(`style={dc.css(${body})}`);
|
|
5126
|
+
continue;
|
|
5127
|
+
}
|
|
5128
|
+
if (whole) {
|
|
5129
|
+
const expression = whole[1].trim();
|
|
5130
|
+
freeNames(expression, ctx.reads);
|
|
5131
|
+
if (/^on[A-Z]/.test(prop)) out.push(`${prop}={${expression}}`);
|
|
5132
|
+
else if (BOOLEAN.has(lower)) out.push(`${prop}={dc.flag(${expression})}`);
|
|
5133
|
+
else out.push(`${prop}={dc.value(${expression})}`);
|
|
5134
|
+
continue;
|
|
5135
|
+
}
|
|
5136
|
+
if (/^on[a-z]/.test(name)) {
|
|
5137
|
+
ctx.warn(`dropped the inline handler ${name}="\u2026"; handlers come from renderVals()`);
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
if (BOOLEAN.has(lower)) {
|
|
5141
|
+
out.push(prop);
|
|
5142
|
+
continue;
|
|
5143
|
+
}
|
|
5144
|
+
if (lower === "href" && !raw.includes("{{")) {
|
|
5145
|
+
const target = linkTarget(raw, ctx);
|
|
5146
|
+
if (target !== null) {
|
|
5147
|
+
out.push(`href=${str2(target)}`);
|
|
5148
|
+
continue;
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
if (raw.includes("/_blob/"))
|
|
5152
|
+
ctx.warn("uses an uploaded Claude Design asset (/_blob/\u2026) that the export does not contain");
|
|
5153
|
+
out.push(`${prop}={${interpolated(raw, ctx.reads)}}`);
|
|
5154
|
+
}
|
|
5155
|
+
return out.length ? ` ${out.join(" ")}` : "";
|
|
5156
|
+
}
|
|
5157
|
+
function selectedOption(select) {
|
|
5158
|
+
const option = findElement(select, (el) => el.tag.toLowerCase() === "option" && attr(el, "selected") !== void 0);
|
|
5159
|
+
if (!option) return null;
|
|
5160
|
+
const value = attr(option, "value");
|
|
5161
|
+
if (value !== void 0 && value !== null) return value;
|
|
5162
|
+
return option.children.map((child) => child.type === "text" ? child.text : "").join("").trim();
|
|
5163
|
+
}
|
|
5164
|
+
function childrenJsx(nodes, ctx, parent) {
|
|
5165
|
+
const out = [];
|
|
5166
|
+
for (const node of nodes) {
|
|
5167
|
+
if (node.type === "text") {
|
|
5168
|
+
if (!node.text.trim()) {
|
|
5169
|
+
if (node.text && !NO_TEXT.has(parent)) out.push('{" "}');
|
|
5170
|
+
continue;
|
|
5171
|
+
}
|
|
5172
|
+
if (NO_TEXT.has(parent)) continue;
|
|
5173
|
+
let last = 0;
|
|
5174
|
+
for (const match of node.text.matchAll(PARTS)) {
|
|
5175
|
+
const before = node.text.slice(last, match.index);
|
|
5176
|
+
if (before) out.push(`{${str2(decodeEntities(before))}}`);
|
|
5177
|
+
freeNames(match[1], ctx.reads);
|
|
5178
|
+
out.push(`{dc.text(${match[1].trim()})}`);
|
|
5179
|
+
last = match.index + match[0].length;
|
|
5180
|
+
}
|
|
5181
|
+
const rest = node.text.slice(last);
|
|
5182
|
+
if (rest) out.push(`{${str2(decodeEntities(rest))}}`);
|
|
5183
|
+
continue;
|
|
5184
|
+
}
|
|
5185
|
+
out.push(elementJsx(node, ctx));
|
|
5186
|
+
}
|
|
5187
|
+
while (out[0] === '{" "}') out.shift();
|
|
5188
|
+
while (out[out.length - 1] === '{" "}') out.pop();
|
|
5189
|
+
return out.join("");
|
|
5190
|
+
}
|
|
5191
|
+
function elementJsx(element, ctx) {
|
|
5192
|
+
const tag = element.tag;
|
|
5193
|
+
const lower = tag.toLowerCase();
|
|
5194
|
+
if (lower === "sc-for") {
|
|
5195
|
+
const list = attr(element, "list") ?? "[]";
|
|
5196
|
+
const as = attr(element, "as") || "item";
|
|
5197
|
+
const expression = (WHOLE.exec(list)?.[1] ?? list).trim();
|
|
5198
|
+
freeNames(expression, ctx.reads);
|
|
5199
|
+
const body = childrenJsx(element.children, ctx, lower);
|
|
5200
|
+
return `{dc.list(${expression}).map((${identifier(as)}, index) => (<dc.Fragment key={index}>${body}</dc.Fragment>))}`;
|
|
5201
|
+
}
|
|
5202
|
+
if (lower === "sc-if") {
|
|
5203
|
+
const value = attr(element, "value") ?? "false";
|
|
5204
|
+
const expression = (WHOLE.exec(value)?.[1] ?? value).trim();
|
|
5205
|
+
freeNames(expression, ctx.reads);
|
|
5206
|
+
return `{dc.flag(${expression}) ? (<>${childrenJsx(element.children, ctx, lower)}</>) : null}`;
|
|
5207
|
+
}
|
|
5208
|
+
if (lower === "dc-import") {
|
|
5209
|
+
const name = attr(element, "name") ?? "";
|
|
5210
|
+
if (!ctx.known.has(name)) {
|
|
5211
|
+
ctx.warn(`imports ${name}${EXT}, which is not in the export`);
|
|
5212
|
+
return "";
|
|
5213
|
+
}
|
|
5214
|
+
const component = `DC_${identifier(name)}`;
|
|
5215
|
+
ctx.imports.add(name);
|
|
5216
|
+
const props = element.attrs.filter(([key]) => key !== "name" && !key.startsWith("hint-")).map(([key, raw]) => {
|
|
5217
|
+
if (raw === null) return `${str2(key)}: true`;
|
|
5218
|
+
const whole = WHOLE.exec(raw);
|
|
5219
|
+
if (whole) {
|
|
5220
|
+
freeNames(whole[1], ctx.reads);
|
|
5221
|
+
return `${str2(key)}: (${whole[1].trim()})`;
|
|
5222
|
+
}
|
|
5223
|
+
return `${str2(key)}: ${interpolated(raw, ctx.reads)}`;
|
|
5224
|
+
});
|
|
5225
|
+
return `<${component} {...{${props.join(", ")}}} />`;
|
|
5226
|
+
}
|
|
5227
|
+
if (lower === "script") {
|
|
5228
|
+
ctx.warn("dropped a <script> inside the template");
|
|
5229
|
+
return "";
|
|
5230
|
+
}
|
|
5231
|
+
if (lower === "title" || lower === "helmet") return "";
|
|
5232
|
+
if (lower === "style") {
|
|
5233
|
+
const css2 = element.children.map((child) => child.type === "text" ? child.text : "").join("");
|
|
5234
|
+
return `<style>{${interpolated(css2, ctx.reads)}}</style>`;
|
|
5235
|
+
}
|
|
5236
|
+
if (lower === "textarea") {
|
|
5237
|
+
const text2 = element.children.map((child) => child.type === "text" ? child.text : "").join("");
|
|
5238
|
+
const extra2 = text2 ? [`defaultValue={${interpolated(text2, ctx.reads)}}`] : [];
|
|
5239
|
+
return `<textarea${attrsJsx(element, ctx, extra2)} />`;
|
|
5240
|
+
}
|
|
5241
|
+
const extra = [];
|
|
5242
|
+
if (lower === "select") {
|
|
5243
|
+
const selected = selectedOption(element);
|
|
5244
|
+
if (selected !== null && attr(element, "value") === void 0)
|
|
5245
|
+
extra.push(`defaultValue={${interpolated(selected, ctx.reads)}}`);
|
|
5246
|
+
}
|
|
5247
|
+
const attrs = attrsJsx(element, ctx, extra);
|
|
5248
|
+
const children = childrenJsx(element.children, ctx, lower);
|
|
5249
|
+
return children ? `<${tag}${attrs}>${children}</${tag}>` : `<${tag}${attrs} />`;
|
|
5250
|
+
}
|
|
5251
|
+
function convertFile(name, source, ctx) {
|
|
5252
|
+
const document2 = parseMarkup(source);
|
|
5253
|
+
const root = findElement(document2, (el) => el.tag.toLowerCase() === "x-dc");
|
|
5254
|
+
const script = findElement(
|
|
5255
|
+
document2,
|
|
5256
|
+
(el) => el.tag.toLowerCase() === "script" && attr(el, "data-dc-script") !== void 0
|
|
5257
|
+
);
|
|
5258
|
+
const file = { ...ctx, name, reads: /* @__PURE__ */ new Set(), imports: /* @__PURE__ */ new Set() };
|
|
5259
|
+
if (!root) throw new Error("no <x-dc> element");
|
|
5260
|
+
const helmet = /<helmet>([\s\S]*?)<\/helmet>/i.exec(source)?.[1]?.trim() ?? "";
|
|
5261
|
+
const body = childrenJsx(root.children, file, "x-dc");
|
|
5262
|
+
let spec = {};
|
|
5263
|
+
const rawSpec = script ? attr(script, "data-props") : null;
|
|
5264
|
+
if (rawSpec) {
|
|
5265
|
+
try {
|
|
5266
|
+
spec = JSON.parse(decodeEntities(rawSpec));
|
|
5267
|
+
} catch {
|
|
5268
|
+
file.warn("has data-props that are not JSON; the props keep no defaults");
|
|
5269
|
+
}
|
|
5270
|
+
}
|
|
5271
|
+
const code = script?.children.map((child) => child.type === "text" ? child.text : "").join("") ?? "";
|
|
5272
|
+
const className = /class\s+([A-Za-z_$][\w$]*)\s+extends\s+DCLogic/.exec(code)?.[1];
|
|
5273
|
+
const logic = className ? `const __Logic = (() => {
|
|
5274
|
+
${code.trim()}
|
|
5275
|
+
return ${className}
|
|
5276
|
+
})()` : "const __Logic = null";
|
|
5277
|
+
const reads = [...file.reads].filter((read) => !read.startsWith("__") && read !== "dc").sort();
|
|
5278
|
+
const lines = [
|
|
5279
|
+
`// Imported from Claude Design (${name}${EXT}).`,
|
|
5280
|
+
`import * as dc from './dc'`,
|
|
5281
|
+
...[...file.imports].sort().map((dep) => `import DC_${identifier(dep)} from './${dep}'`),
|
|
5282
|
+
`const DCLogic = dc.DCLogic`,
|
|
5283
|
+
"",
|
|
5284
|
+
helmet ? `dc.head(${str2(helmet)})
|
|
5285
|
+
` : "",
|
|
5286
|
+
`const __spec = ${JSON.stringify(spec)}`,
|
|
5287
|
+
"",
|
|
5288
|
+
logic,
|
|
5289
|
+
"",
|
|
5290
|
+
`export default function ${identifier(name)}(__props) {`,
|
|
5291
|
+
` const __scope = dc.useLogic(__Logic, __spec, __props)`,
|
|
5292
|
+
reads.length ? ` const { ${reads.join(", ")} } = __scope` : "",
|
|
5293
|
+
` return (<>${body}</>)`,
|
|
5294
|
+
"}",
|
|
5295
|
+
""
|
|
5296
|
+
];
|
|
5297
|
+
return lines.filter((line, index) => line !== "" || lines[index - 1] !== "").join("\n");
|
|
5298
|
+
}
|
|
5299
|
+
var DC_HELPERS = `// Imported from Claude Design: what the converted screens need. This file is the canvas's own.
|
|
5300
|
+
import { Fragment, useReducer, useRef } from 'react'
|
|
5301
|
+
import { useTheme } from '@design/runtime'
|
|
5302
|
+
|
|
5303
|
+
export { Fragment }
|
|
5304
|
+
|
|
5305
|
+
/** The base of each screen's logic class: props, state and setState, as Claude Design had them. */
|
|
5306
|
+
export class DCLogic {
|
|
5307
|
+
constructor(props) {
|
|
5308
|
+
this.props = props
|
|
5309
|
+
this.state = {}
|
|
5310
|
+
}
|
|
5311
|
+
setState(patch) {
|
|
5312
|
+
const next = typeof patch === 'function' ? patch(this.state, this.props) : patch
|
|
5313
|
+
this.state = { ...this.state, ...next }
|
|
5314
|
+
this.__update?.()
|
|
5315
|
+
}
|
|
5316
|
+
renderVals() {
|
|
5317
|
+
return {}
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5321
|
+
const themed = (spec) => Array.isArray(spec?.theme?.options) && spec.theme.options.includes('dark')
|
|
5322
|
+
|
|
5323
|
+
function convert(field, value) {
|
|
5324
|
+
if (!field || typeof value !== 'string') return value
|
|
5325
|
+
if (field.editor === 'int' || field.editor === 'number') return Number(value)
|
|
5326
|
+
if (field.editor === 'bool' || field.editor === 'boolean') return value === 'true' || value === ''
|
|
5327
|
+
return value
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5330
|
+
/** Props (defaults, then what the canvas or a parent passed), the logic's values, for the template. */
|
|
5331
|
+
export function useLogic(Logic, spec, given) {
|
|
5332
|
+
const theme = useTheme()
|
|
5333
|
+
const [, update] = useReducer((n) => n + 1, 0)
|
|
5334
|
+
const props = {}
|
|
5335
|
+
for (const [key, field] of Object.entries(spec ?? {}))
|
|
5336
|
+
if (!key.startsWith('$') && field && typeof field === 'object' && 'default' in field) props[key] = field.default
|
|
5337
|
+
for (const [key, value] of Object.entries(given ?? {})) props[key] = convert(spec?.[key], value)
|
|
5338
|
+
// A theme prop follows the viewer's light/dark switch unless the canvas pins it.
|
|
5339
|
+
if (themed(spec) && given?.theme === undefined) props.theme = theme
|
|
5340
|
+
const logic = useRef(null)
|
|
5341
|
+
if (Logic && !logic.current) logic.current = new Logic(props)
|
|
5342
|
+
let vals = {}
|
|
5343
|
+
if (logic.current) {
|
|
5344
|
+
logic.current.props = props
|
|
5345
|
+
logic.current.__update = update
|
|
5346
|
+
vals = logic.current.renderVals?.() ?? {}
|
|
5347
|
+
}
|
|
5348
|
+
return { props, ...props, ...vals }
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5351
|
+
const heads = new Set()
|
|
5352
|
+
/** The template's <helmet>: fonts and styles, added to the page once. */
|
|
5353
|
+
export function head(html) {
|
|
5354
|
+
if (heads.has(html) || typeof document === 'undefined') return
|
|
5355
|
+
heads.add(html)
|
|
5356
|
+
document.head.insertAdjacentHTML('beforeend', html)
|
|
5357
|
+
}
|
|
5358
|
+
|
|
5359
|
+
/** An inline style string as a React style object. */
|
|
5360
|
+
export function css(text) {
|
|
5361
|
+
const style = {}
|
|
5362
|
+
let depth = 0
|
|
5363
|
+
let quote = ''
|
|
5364
|
+
let start = 0
|
|
5365
|
+
const declarations = []
|
|
5366
|
+
for (let i = 0; i <= text.length; i++) {
|
|
5367
|
+
const c = text[i]
|
|
5368
|
+
if (quote) {
|
|
5369
|
+
if (c === quote) quote = ''
|
|
5370
|
+
} else if (c === '"' || c === "'") quote = c
|
|
5371
|
+
else if (c === '(') depth++
|
|
5372
|
+
else if (c === ')') depth--
|
|
5373
|
+
else if ((c === ';' && depth === 0) || i === text.length) {
|
|
5374
|
+
declarations.push(text.slice(start, i))
|
|
5375
|
+
start = i + 1
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
for (const declaration of declarations) {
|
|
5379
|
+
const colon = declaration.indexOf(':')
|
|
5380
|
+
if (colon === -1) continue
|
|
5381
|
+
const name = declaration.slice(0, colon).trim()
|
|
5382
|
+
const value = declaration.slice(colon + 1).trim()
|
|
5383
|
+
if (!name || !value) continue
|
|
5384
|
+
const key = name.startsWith('--') ? name : name.toLowerCase().replace(/^-ms-/, 'ms-').replace(/-([a-z])/g, (_, c) => c.toUpperCase())
|
|
5385
|
+
style[key] = value
|
|
5386
|
+
}
|
|
5387
|
+
return style
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5390
|
+
/** A template value in text: nothing for null, undefined and false. */
|
|
5391
|
+
export const text = (value) => (value == null || value === false ? '' : value)
|
|
5392
|
+
/** An attribute value: left out for null, undefined and false. */
|
|
5393
|
+
export const value = (v) => (v == null || v === false ? undefined : v)
|
|
5394
|
+
/** A boolean attribute or condition. */
|
|
5395
|
+
export const flag = (v) => v === true || v === 'true' || (!!v && v !== 'false')
|
|
5396
|
+
/** What a loop runs over. */
|
|
5397
|
+
export const list = (v) => (Array.isArray(v) ? v : v == null ? [] : Array.from(v))
|
|
5398
|
+
`;
|
|
5399
|
+
function boardId(file, used) {
|
|
5400
|
+
let base = kebab(file.slice(0, -EXT.length)) || "screen";
|
|
5401
|
+
if (!ID_PATTERN.test(base)) base = `s-${base}`;
|
|
5402
|
+
let id2 = base;
|
|
5403
|
+
for (let n2 = 2; used.has(id2); n2++) id2 = `${base}-${n2}`;
|
|
5404
|
+
used.add(id2);
|
|
5405
|
+
return id2;
|
|
5406
|
+
}
|
|
5407
|
+
function importClaudeDesign(dir, options = {}) {
|
|
5408
|
+
const canvasFile = path25.join(dir, "canvas.json");
|
|
5409
|
+
if (!fs27.existsSync(canvasFile))
|
|
5410
|
+
throw new Error(`No canvas.json in ${dir}: point at the exported Claude Design project folder`);
|
|
5411
|
+
const source = JSON.parse(fs27.readFileSync(canvasFile, "utf8"));
|
|
5412
|
+
const templates = fs27.readdirSync(dir).filter((file) => file.endsWith(EXT));
|
|
5413
|
+
if (!templates.length) throw new Error(`No ${EXT} files in ${dir}`);
|
|
5414
|
+
const known = new Set(templates.map((file) => file.slice(0, -EXT.length)));
|
|
5415
|
+
const warnings = /* @__PURE__ */ new Map();
|
|
5416
|
+
const boards = source.boards ?? {};
|
|
5417
|
+
const order = [...(source.order ?? []).filter((file) => file in boards), ...Object.keys(boards)];
|
|
5418
|
+
const used = /* @__PURE__ */ new Set();
|
|
5419
|
+
const ids = /* @__PURE__ */ new Map();
|
|
5420
|
+
const routes = /* @__PURE__ */ new Map();
|
|
5421
|
+
for (const file of new Set(order)) {
|
|
5422
|
+
if (!known.has(file.slice(0, -EXT.length))) continue;
|
|
5423
|
+
const id2 = boardId(file, used);
|
|
5424
|
+
ids.set(file, id2);
|
|
5425
|
+
routes.set(file.slice(0, -EXT.length), `/${id2}`);
|
|
5426
|
+
}
|
|
5427
|
+
const files = /* @__PURE__ */ new Map();
|
|
5428
|
+
files.set("dc.jsx", DC_HELPERS);
|
|
5429
|
+
for (const file of templates) {
|
|
5430
|
+
const name = file.slice(0, -EXT.length);
|
|
5431
|
+
const warn2 = (message) => {
|
|
5432
|
+
const set = warnings.get(message) ?? /* @__PURE__ */ new Set();
|
|
5433
|
+
set.add(file);
|
|
5434
|
+
warnings.set(message, set);
|
|
5435
|
+
};
|
|
5436
|
+
try {
|
|
5437
|
+
files.set(
|
|
5438
|
+
`${name}.jsx`,
|
|
5439
|
+
convertFile(name, fs27.readFileSync(path25.join(dir, file), "utf8"), { known, routes, warn: warn2 })
|
|
5440
|
+
);
|
|
5441
|
+
} catch (error) {
|
|
5442
|
+
warn2(`could not be converted: ${error.message}`);
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
const pageList = source.pages?.length ? source.pages : [{ id: "main", name: source.title ?? "Main" }];
|
|
5446
|
+
const pages = pageList.map((page, index) => ({
|
|
5447
|
+
id: ID_PATTERN.test(page.id) ? page.id : `page-${index + 1}`,
|
|
5448
|
+
title: page.name || page.id,
|
|
5449
|
+
items: []
|
|
5450
|
+
}));
|
|
5451
|
+
const pageOf = (id2) => pages[Math.max(
|
|
5452
|
+
0,
|
|
5453
|
+
pageList.findIndex((page) => page.id === id2)
|
|
5454
|
+
)];
|
|
5455
|
+
let notes = 0;
|
|
5456
|
+
for (const [key, note] of Object.entries(source.notes ?? {})) {
|
|
5457
|
+
if (!note.text) continue;
|
|
5458
|
+
const heading = note.kind === "title1" ? "# " : note.kind === "title2" ? "## " : note.kind === "title3" ? "### " : "";
|
|
5459
|
+
pageOf(note.page).items.push({
|
|
5460
|
+
id: `note-${boardId(`${key}${EXT}`, used)}`,
|
|
5461
|
+
type: "note",
|
|
5462
|
+
tone: heading ? "plain" : "note",
|
|
5463
|
+
text: `${heading}${note.text}`,
|
|
5464
|
+
width: Math.round(Math.min(Math.max(note.maxW ?? note.w ?? 480, 80), 4e3)),
|
|
5465
|
+
x: note.x ?? 0,
|
|
5466
|
+
y: note.y ?? 0
|
|
5467
|
+
});
|
|
5468
|
+
notes++;
|
|
5469
|
+
}
|
|
5470
|
+
let screens = 0;
|
|
5471
|
+
for (const [file, id2] of ids) {
|
|
5472
|
+
const board = boards[file];
|
|
5473
|
+
if (!files.has(`${file.slice(0, -EXT.length)}.jsx`)) continue;
|
|
5474
|
+
pageOf(board.page).items.push({
|
|
5475
|
+
id: id2,
|
|
5476
|
+
src: `screens/${file.slice(0, -EXT.length)}.jsx`,
|
|
5477
|
+
title: board.title || file.slice(0, -EXT.length),
|
|
5478
|
+
route: routes.get(file.slice(0, -EXT.length)),
|
|
5479
|
+
width: Math.round(Math.min(Math.max(board.w ?? 1440, 80), 8e3)),
|
|
5480
|
+
height: Math.round(Math.min(Math.max(board.h ?? 900, 80), 4e4)),
|
|
5481
|
+
x: board.x ?? 0,
|
|
5482
|
+
y: board.y ?? 0
|
|
5483
|
+
});
|
|
5484
|
+
screens++;
|
|
5485
|
+
}
|
|
5486
|
+
const canvas = {
|
|
5487
|
+
title: options.title ?? source.title ?? path25.basename(path25.resolve(dir)),
|
|
5488
|
+
description: "Imported from Claude Design.",
|
|
5489
|
+
// The templates bring their own styles; the design system's reset would restyle them.
|
|
5490
|
+
system: false,
|
|
5491
|
+
pages: pages.filter((page) => page.items.length)
|
|
5492
|
+
};
|
|
5493
|
+
return { canvas, files, screens, notes, warnings };
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
// src/cli/commands/import.ts
|
|
5497
|
+
var slug = (text2) => text2.normalize("NFKD").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-").toLowerCase();
|
|
5498
|
+
function runImport(paths, from, options) {
|
|
5499
|
+
const dir = path26.resolve(from);
|
|
5500
|
+
if (!fs28.existsSync(dir) || !fs28.statSync(dir).isDirectory()) throw new CliError(`No folder ${from}`);
|
|
5501
|
+
let result;
|
|
5502
|
+
try {
|
|
5503
|
+
result = importClaudeDesign(dir, { title: options.title });
|
|
5504
|
+
} catch (error) {
|
|
5505
|
+
throw new CliError(error.message);
|
|
5506
|
+
}
|
|
5507
|
+
const id2 = options.canvas ?? (slug(String(result.canvas.title)) || slug(path26.basename(dir)) || "imported");
|
|
5508
|
+
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is no canvas id; pass one with --canvas <id>`);
|
|
5509
|
+
const out = path26.join(paths.canvases, id2);
|
|
5510
|
+
if (fs28.existsSync(out)) throw new CliError(`.design/canvas/${id2} exists; pick another id with --canvas <id>`);
|
|
5511
|
+
fs28.mkdirSync(path26.join(out, "screens"), { recursive: true });
|
|
5512
|
+
for (const [name, content] of result.files) fs28.writeFileSync(path26.join(out, "screens", name), content);
|
|
5513
|
+
fs28.writeFileSync(path26.join(out, "canvas.json"), `${JSON.stringify(result.canvas, null, 2)}
|
|
5514
|
+
`);
|
|
5515
|
+
print(`${green("Imported")} ${bold(String(result.canvas.title))} \u2192 .design/canvas/${id2}`);
|
|
5516
|
+
print(
|
|
5517
|
+
` ${result.screens} screens and ${result.notes} notes on ${result.canvas.pages.length} pages; ${result.files.size - 1} templates as React modules in screens/, helpers in screens/dc.jsx`
|
|
5518
|
+
);
|
|
5519
|
+
if (result.warnings.size) {
|
|
5520
|
+
print();
|
|
5521
|
+
for (const [message, files] of result.warnings) {
|
|
5522
|
+
const list = [...files];
|
|
5523
|
+
print(`${yellow("warning")} ${message}`);
|
|
5524
|
+
print(` ${dim(`${list.slice(0, 6).join(", ")}${list.length > 6 ? ` and ${list.length - 6} more` : ""}`)}`);
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
print();
|
|
5528
|
+
print(`Next: \`design check ${id2} --render\`, then \`design preview --open /c/${id2}\`.`);
|
|
5529
|
+
}
|
|
5530
|
+
|
|
4590
5531
|
// src/cli/commands/scaffold.ts
|
|
4591
|
-
import
|
|
4592
|
-
import
|
|
5532
|
+
import fs29 from "fs";
|
|
5533
|
+
import path27 from "path";
|
|
4593
5534
|
|
|
4594
5535
|
// src/cli/templates.ts
|
|
4595
5536
|
var TOKENS_CSS = `/*
|
|
@@ -4800,89 +5741,91 @@ function mainScreen(canvasId) {
|
|
|
4800
5741
|
|
|
4801
5742
|
// src/cli/commands/scaffold.ts
|
|
4802
5743
|
function write(file, content, root) {
|
|
4803
|
-
if (
|
|
4804
|
-
print(` ${dim("exists ")} ${
|
|
5744
|
+
if (fs29.existsSync(file)) {
|
|
5745
|
+
print(` ${dim("exists ")} ${path27.relative(root, file)}`);
|
|
4805
5746
|
return false;
|
|
4806
5747
|
}
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
print(` ${green("created")} ${
|
|
5748
|
+
fs29.mkdirSync(path27.dirname(file), { recursive: true });
|
|
5749
|
+
fs29.writeFileSync(file, content);
|
|
5750
|
+
print(` ${green("created")} ${path27.relative(root, file)}`);
|
|
4810
5751
|
return true;
|
|
4811
5752
|
}
|
|
4812
5753
|
function ensureGitignored(root) {
|
|
4813
|
-
const file =
|
|
4814
|
-
const current =
|
|
5754
|
+
const file = path27.join(root, ".gitignore");
|
|
5755
|
+
const current = fs29.existsSync(file) ? fs29.readFileSync(file, "utf8") : "";
|
|
4815
5756
|
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
4816
5757
|
if (covered) return "present";
|
|
4817
5758
|
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
4818
|
-
|
|
5759
|
+
fs29.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
4819
5760
|
.design/
|
|
4820
5761
|
`);
|
|
4821
5762
|
return "added";
|
|
4822
5763
|
}
|
|
4823
5764
|
function projectName(root) {
|
|
4824
5765
|
try {
|
|
4825
|
-
const manifest = JSON.parse(
|
|
5766
|
+
const manifest = JSON.parse(fs29.readFileSync(path27.join(root, "package.json"), "utf8"));
|
|
4826
5767
|
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
4827
5768
|
} catch {
|
|
4828
5769
|
}
|
|
4829
|
-
return
|
|
5770
|
+
return path27.basename(root);
|
|
4830
5771
|
}
|
|
4831
5772
|
function runInit(root, options) {
|
|
4832
5773
|
const paths = designPaths(root);
|
|
4833
5774
|
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
4834
5775
|
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
4835
5776
|
`, root);
|
|
4836
|
-
|
|
5777
|
+
fs29.mkdirSync(paths.canvases, { recursive: true });
|
|
4837
5778
|
if (options.gitignore) {
|
|
4838
5779
|
const result = ensureGitignored(root);
|
|
4839
5780
|
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
4840
5781
|
}
|
|
4841
5782
|
print();
|
|
4842
|
-
print(
|
|
5783
|
+
print(
|
|
5784
|
+
"Next: `design system init` for a design system, `design new <canvas>` for a canvas, `design icon <file.svg>` for the project icon, `design preview` to look."
|
|
5785
|
+
);
|
|
4843
5786
|
}
|
|
4844
5787
|
function runSystemInit(root, options) {
|
|
4845
5788
|
const paths = designPaths(root);
|
|
4846
5789
|
let base = projectName(root);
|
|
4847
5790
|
try {
|
|
4848
|
-
base = JSON.parse(
|
|
5791
|
+
base = JSON.parse(fs29.readFileSync(paths.config, "utf8")).name || base;
|
|
4849
5792
|
} catch {
|
|
4850
5793
|
}
|
|
4851
5794
|
const name = options.name ?? `${base} design system`;
|
|
4852
|
-
print(`Scaffolding the design system in ${
|
|
4853
|
-
write(
|
|
5795
|
+
print(`Scaffolding the design system in ${path27.relative(root, paths.system)}`);
|
|
5796
|
+
write(path27.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
4854
5797
|
`, root);
|
|
4855
|
-
write(
|
|
5798
|
+
write(path27.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
4856
5799
|
if (!options.empty) {
|
|
4857
|
-
write(
|
|
4858
|
-
write(
|
|
4859
|
-
write(
|
|
5800
|
+
write(path27.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
5801
|
+
write(path27.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
5802
|
+
write(path27.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
4860
5803
|
}
|
|
4861
5804
|
}
|
|
4862
5805
|
function runNew(root, id2, options) {
|
|
4863
5806
|
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
4864
5807
|
const paths = designPaths(root);
|
|
4865
|
-
const dir =
|
|
4866
|
-
if (
|
|
5808
|
+
const dir = path27.join(paths.canvases, id2);
|
|
5809
|
+
if (fs29.existsSync(path27.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
4867
5810
|
print(`Creating canvas "${id2}"`);
|
|
4868
5811
|
const title = options.title ?? titleize(id2);
|
|
4869
5812
|
if (options.empty) {
|
|
4870
5813
|
write(
|
|
4871
|
-
|
|
5814
|
+
path27.join(dir, "canvas.json"),
|
|
4872
5815
|
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
4873
5816
|
`,
|
|
4874
5817
|
root
|
|
4875
5818
|
);
|
|
4876
|
-
|
|
5819
|
+
fs29.mkdirSync(path27.join(dir, "screens"), { recursive: true });
|
|
4877
5820
|
} else {
|
|
4878
|
-
write(
|
|
4879
|
-
write(
|
|
5821
|
+
write(path27.join(dir, "canvas.json"), canvasJson(title), root);
|
|
5822
|
+
write(path27.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
4880
5823
|
}
|
|
4881
5824
|
}
|
|
4882
5825
|
|
|
4883
5826
|
// src/cli/commands/shot.ts
|
|
4884
|
-
import
|
|
4885
|
-
import
|
|
5827
|
+
import fs30 from "fs";
|
|
5828
|
+
import path28 from "path";
|
|
4886
5829
|
var OVERVIEW_TIMEOUT = 9e4;
|
|
4887
5830
|
async function runShot(paths, target, options) {
|
|
4888
5831
|
const [canvasId = "", screenId] = target.split("/");
|
|
@@ -4898,15 +5841,15 @@ async function runShot(paths, target, options) {
|
|
|
4898
5841
|
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
4899
5842
|
}
|
|
4900
5843
|
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
4901
|
-
const outBase =
|
|
4902
|
-
const outDir =
|
|
4903
|
-
|
|
5844
|
+
const outBase = path28.resolve(options.out ?? paths.shots);
|
|
5845
|
+
const outDir = path28.join(outBase, canvasId);
|
|
5846
|
+
fs30.mkdirSync(outDir, { recursive: true });
|
|
4904
5847
|
const base = await serverBase(paths);
|
|
4905
5848
|
const browser = await launchChrome();
|
|
4906
5849
|
let failed = false;
|
|
4907
5850
|
try {
|
|
4908
5851
|
if (options.overview) {
|
|
4909
|
-
if (!
|
|
5852
|
+
if (!fs30.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
4910
5853
|
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);
|
|
4911
5854
|
for (const pageId of pages) {
|
|
4912
5855
|
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
@@ -4922,7 +5865,7 @@ async function runShot(paths, target, options) {
|
|
|
4922
5865
|
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
4923
5866
|
}
|
|
4924
5867
|
await page.waitForTimeout(400);
|
|
4925
|
-
const file =
|
|
5868
|
+
const file = path28.join(outDir, `${pageId}.overview.png`);
|
|
4926
5869
|
await page.screenshot({ path: file });
|
|
4927
5870
|
await page.close();
|
|
4928
5871
|
print(file);
|
|
@@ -4951,7 +5894,7 @@ async function shootFrames(browser, jobs, outDir) {
|
|
|
4951
5894
|
let failed = false;
|
|
4952
5895
|
const results = await mapLimit(jobs, 4, async (job) => {
|
|
4953
5896
|
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
4954
|
-
const file =
|
|
5897
|
+
const file = path28.join(outDir, `${job.id}.${job.theme}.png`);
|
|
4955
5898
|
try {
|
|
4956
5899
|
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
4957
5900
|
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
@@ -4983,8 +5926,8 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
4983
5926
|
if (id2 && !all.some((job) => job.id === id2)) {
|
|
4984
5927
|
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
4985
5928
|
}
|
|
4986
|
-
const outDir =
|
|
4987
|
-
|
|
5929
|
+
const outDir = path28.join(path28.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
5930
|
+
fs30.mkdirSync(outDir, { recursive: true });
|
|
4988
5931
|
const base = await serverBase(paths);
|
|
4989
5932
|
const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
|
|
4990
5933
|
const browser = await launchChrome();
|
|
@@ -4996,7 +5939,7 @@ async function shootSystem(paths, project, id2, options) {
|
|
|
4996
5939
|
}
|
|
4997
5940
|
|
|
4998
5941
|
// src/cli/commands/cloud-status.ts
|
|
4999
|
-
import
|
|
5942
|
+
import path29 from "path";
|
|
5000
5943
|
var STATE_TEXT = {
|
|
5001
5944
|
in_sync: green,
|
|
5002
5945
|
local_changes: yellow,
|
|
@@ -5019,7 +5962,7 @@ function detail(paths, status) {
|
|
|
5019
5962
|
if (status.state === "remote_ahead" && !status.local && !status.baseRev) parts.push("not pulled yet");
|
|
5020
5963
|
if (status.state === "remote_ahead" && status.changed.length && !status.deletedRemotely)
|
|
5021
5964
|
parts.push(`${plural(status.changed.length, "file")} changed here; the cloud only rebuilt it`);
|
|
5022
|
-
if (status.conflictRev !== void 0) parts.push(`merge ${
|
|
5965
|
+
if (status.conflictRev !== void 0) parts.push(`merge ${path29.relative(paths.root, incomingDir(paths, status.key))}`);
|
|
5023
5966
|
return parts.join(" \xB7 ");
|
|
5024
5967
|
}
|
|
5025
5968
|
async function printCloudStatus(paths, label) {
|
|
@@ -5134,6 +6077,11 @@ ${bold("Project")}
|
|
|
5134
6077
|
init [--name <name>] [--no-gitignore] Create .design and add it to .gitignore
|
|
5135
6078
|
system init [--name <name>] [--empty] Scaffold .design/system: tokens, a guideline, a component
|
|
5136
6079
|
new <canvas> [--title <title>] [--empty] Scaffold .design/canvas/<canvas>
|
|
6080
|
+
icon [<file>] [--remove] Show, set or remove the project icon: an SVG, PNG or WebP of
|
|
6081
|
+
at most 256 KB, kept as .design/system/icon.* and pushed
|
|
6082
|
+
with the design system
|
|
6083
|
+
import <folder> [--canvas <id>] Turn an exported Claude Design project (canvas.json and
|
|
6084
|
+
[--title <title>] *.dc.html) into a canvas of React screens, once
|
|
5137
6085
|
canvases [--json] Every canvas: pages and screens; with a cloud link also its
|
|
5138
6086
|
revision, sync state, web link and public link
|
|
5139
6087
|
|
|
@@ -5193,7 +6141,7 @@ ${bold("Environment")}
|
|
|
5193
6141
|
FOSS_DESIGN_CLOUD Cloud to sign in and link to (default https://app.fossdesign.dev)
|
|
5194
6142
|
`;
|
|
5195
6143
|
function projectPaths(rootOption, requireDesign = true) {
|
|
5196
|
-
const found = rootOption ?
|
|
6144
|
+
const found = rootOption ? path30.resolve(rootOption) : findProjectRoot();
|
|
5197
6145
|
if (!found) {
|
|
5198
6146
|
if (!requireDesign) return designPaths(process.cwd());
|
|
5199
6147
|
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
@@ -5210,6 +6158,8 @@ async function main(argv) {
|
|
|
5210
6158
|
root: { type: "string" },
|
|
5211
6159
|
name: { type: "string" },
|
|
5212
6160
|
title: { type: "string" },
|
|
6161
|
+
canvas: { type: "string" },
|
|
6162
|
+
remove: { type: "boolean" },
|
|
5213
6163
|
"no-gitignore": { type: "boolean" },
|
|
5214
6164
|
empty: { type: "boolean" },
|
|
5215
6165
|
open: { type: "boolean" },
|
|
@@ -5239,7 +6189,7 @@ async function main(argv) {
|
|
|
5239
6189
|
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
5240
6190
|
switch (command) {
|
|
5241
6191
|
case "init":
|
|
5242
|
-
return runInit(values.root ?
|
|
6192
|
+
return runInit(values.root ? path30.resolve(values.root) : process.cwd(), {
|
|
5243
6193
|
name: values.name,
|
|
5244
6194
|
gitignore: !values["no-gitignore"]
|
|
5245
6195
|
});
|
|
@@ -5251,6 +6201,12 @@ async function main(argv) {
|
|
|
5251
6201
|
if (!rest[0]) throw new CliError("Usage: design new <canvas> [--title <title>]");
|
|
5252
6202
|
return runNew(projectPaths(values.root).root, rest[0], { title: values.title, empty: !!values.empty });
|
|
5253
6203
|
}
|
|
6204
|
+
case "icon":
|
|
6205
|
+
return runIcon(projectPaths(values.root), rest[0], { remove: !!values.remove });
|
|
6206
|
+
case "import": {
|
|
6207
|
+
if (!rest[0]) throw new CliError("Usage: design import <folder> [--canvas <id>] [--title <title>]");
|
|
6208
|
+
return runImport(projectPaths(values.root), rest[0], { canvas: values.canvas, title: values.title });
|
|
6209
|
+
}
|
|
5254
6210
|
case "preview": {
|
|
5255
6211
|
const openPath = rest[0]?.startsWith("/") ? rest[0] : void 0;
|
|
5256
6212
|
return runPreview(projectPaths(values.root), {
|