foss-design 0.1.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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/cli.js +3064 -0
- package/dist/runtime/base.css +8 -0
- package/dist/runtime/index.js +357 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/dist/viewer/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/dist/viewer/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/dist/viewer/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/dist/viewer/assets/index-Blh90C72.css +2 -0
- package/dist/viewer/assets/index-CN6gdSK-.js +166 -0
- package/dist/viewer/favicon.svg +1 -0
- package/dist/viewer/index.html +23 -0
- package/package.json +87 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3064 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import path18 from "path";
|
|
5
|
+
import { parseArgs } from "util";
|
|
6
|
+
|
|
7
|
+
// src/core/paths.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
import path2 from "path";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
|
|
13
|
+
// src/core/text.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
function titleize(stem) {
|
|
16
|
+
const words = stem.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[-_.]+/g, " ").trim().split(/\s+/);
|
|
17
|
+
const text = words.join(" ").toLowerCase();
|
|
18
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
19
|
+
}
|
|
20
|
+
function slugify(stem) {
|
|
21
|
+
return stem.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase() || "item";
|
|
22
|
+
}
|
|
23
|
+
function fileStem(file) {
|
|
24
|
+
const base = path.basename(file, path.extname(file));
|
|
25
|
+
if (base === "index") return path.basename(path.dirname(file));
|
|
26
|
+
return base;
|
|
27
|
+
}
|
|
28
|
+
function toPosix(p) {
|
|
29
|
+
return p.split(path.sep).join("/");
|
|
30
|
+
}
|
|
31
|
+
function parseFrontmatter(source) {
|
|
32
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(source);
|
|
33
|
+
if (!match) return { data: {}, body: source };
|
|
34
|
+
const data = {};
|
|
35
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
36
|
+
const pair = /^([\w-]+)\s*:\s*(.*)$/.exec(line);
|
|
37
|
+
if (pair) data[pair[1]] = pair[2].replace(/^["']|["']$/g, "").trim();
|
|
38
|
+
}
|
|
39
|
+
return { data, body: source.slice(match[0].length) };
|
|
40
|
+
}
|
|
41
|
+
function parseDocTags(source) {
|
|
42
|
+
const block = /^\s*(?:\/\*\*?([\s\S]*?)\*\/|<!--([\s\S]*?)-->)/.exec(source.replace(/^<!doctype[^>]*>\s*/i, ""));
|
|
43
|
+
const tags = {};
|
|
44
|
+
if (!block) return tags;
|
|
45
|
+
const lines = (block[1] ?? block[2] ?? "").split(/\r?\n/).map((line) => line.replace(/^\s*\*?\s?/, ""));
|
|
46
|
+
let current = null;
|
|
47
|
+
const flush = () => {
|
|
48
|
+
if (!current) return;
|
|
49
|
+
const list = tags[current.tag] ?? [];
|
|
50
|
+
list.push(current.value.join("\n").trim());
|
|
51
|
+
tags[current.tag] = list;
|
|
52
|
+
};
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const tag = /^@([\w-]+)\s*(.*)$/.exec(line.trim());
|
|
55
|
+
if (tag) {
|
|
56
|
+
flush();
|
|
57
|
+
current = { tag: tag[1], value: [tag[2]] };
|
|
58
|
+
} else if (current) current.value.push(line);
|
|
59
|
+
}
|
|
60
|
+
flush();
|
|
61
|
+
return tags;
|
|
62
|
+
}
|
|
63
|
+
function firstHeading(markdown) {
|
|
64
|
+
const match = /^#\s+(.+)$/m.exec(markdown);
|
|
65
|
+
return match ? match[1].trim() : null;
|
|
66
|
+
}
|
|
67
|
+
function escapeHtml(text) {
|
|
68
|
+
return text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
|
|
69
|
+
}
|
|
70
|
+
function scriptJson(value) {
|
|
71
|
+
return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/core/paths.ts
|
|
75
|
+
var DESIGN_DIR = ".design";
|
|
76
|
+
function designPaths(root) {
|
|
77
|
+
const design = path2.join(root, DESIGN_DIR);
|
|
78
|
+
const cache = path2.join(design, ".cache");
|
|
79
|
+
return {
|
|
80
|
+
root,
|
|
81
|
+
design,
|
|
82
|
+
config: path2.join(design, "design.json"),
|
|
83
|
+
system: path2.join(design, "system"),
|
|
84
|
+
canvases: path2.join(design, "canvas"),
|
|
85
|
+
cache,
|
|
86
|
+
entries: path2.join(cache, "entries"),
|
|
87
|
+
snapshots: path2.join(cache, "snapshots"),
|
|
88
|
+
shots: path2.join(cache, "shots")
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function findProjectRoot(from = process.cwd()) {
|
|
92
|
+
let dir = path2.resolve(from);
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
const candidate = path2.join(dir, DESIGN_DIR);
|
|
95
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return dir;
|
|
96
|
+
const parent = path2.dirname(dir);
|
|
97
|
+
if (parent === dir) return null;
|
|
98
|
+
dir = parent;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function relToRoot(paths, abs) {
|
|
102
|
+
return toPosix(path2.relative(paths.root, abs));
|
|
103
|
+
}
|
|
104
|
+
function relToDesign(paths, abs) {
|
|
105
|
+
return toPosix(path2.relative(paths.design, abs));
|
|
106
|
+
}
|
|
107
|
+
function isInside(parent, child) {
|
|
108
|
+
const rel = path2.relative(parent, child);
|
|
109
|
+
return rel === "" || !rel.startsWith("..") && !path2.isAbsolute(rel);
|
|
110
|
+
}
|
|
111
|
+
var PKG_ROOT = path2.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
|
112
|
+
var PKG = {
|
|
113
|
+
root: PKG_ROOT,
|
|
114
|
+
manifest: path2.join(PKG_ROOT, "package.json"),
|
|
115
|
+
runtime: path2.join(PKG_ROOT, "dist", "runtime", "index.js"),
|
|
116
|
+
baseCss: path2.join(PKG_ROOT, "dist", "runtime", "base.css"),
|
|
117
|
+
viewer: path2.join(PKG_ROOT, "dist", "viewer"),
|
|
118
|
+
cli: path2.join(PKG_ROOT, "dist", "cli.js")
|
|
119
|
+
};
|
|
120
|
+
function packageVersion() {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(fs.readFileSync(PKG.manifest, "utf8")).version ?? "0.0.0";
|
|
123
|
+
} catch {
|
|
124
|
+
return "0.0.0";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function resolveFrom(anchor, request) {
|
|
128
|
+
try {
|
|
129
|
+
return createRequire(anchor).resolve(request);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/cli/commands/build.ts
|
|
136
|
+
import { spawnSync } from "child_process";
|
|
137
|
+
import fs9 from "fs";
|
|
138
|
+
import path11 from "path";
|
|
139
|
+
|
|
140
|
+
// src/build/static.ts
|
|
141
|
+
import fs8 from "fs";
|
|
142
|
+
import path10 from "path";
|
|
143
|
+
import { build } from "vite";
|
|
144
|
+
|
|
145
|
+
// src/core/project.ts
|
|
146
|
+
import fs5 from "fs";
|
|
147
|
+
import path6 from "path";
|
|
148
|
+
|
|
149
|
+
// src/core/canvas.ts
|
|
150
|
+
import { createHash } from "crypto";
|
|
151
|
+
import fs2 from "fs";
|
|
152
|
+
import path3 from "path";
|
|
153
|
+
|
|
154
|
+
// src/shared/devices.ts
|
|
155
|
+
var DEVICES = {
|
|
156
|
+
phone: { width: 390, height: 844, label: "Phone" },
|
|
157
|
+
"phone-sm": { width: 375, height: 667, label: "Phone S" },
|
|
158
|
+
"phone-lg": { width: 430, height: 932, label: "Phone L" },
|
|
159
|
+
tablet: { width: 834, height: 1194, label: "Tablet" },
|
|
160
|
+
"tablet-landscape": { width: 1194, height: 834, label: "Tablet landscape" },
|
|
161
|
+
laptop: { width: 1280, height: 800, label: "Laptop" },
|
|
162
|
+
desktop: { width: 1440, height: 900, label: "Desktop" },
|
|
163
|
+
wide: { width: 1920, height: 1080, label: "Wide" }
|
|
164
|
+
};
|
|
165
|
+
var DEVICE_NAMES = Object.keys(DEVICES);
|
|
166
|
+
var DEFAULT_DEVICE = "desktop";
|
|
167
|
+
|
|
168
|
+
// src/core/schema.ts
|
|
169
|
+
import { z } from "zod";
|
|
170
|
+
var id = z.string().regex(/^[a-z0-9][a-z0-9_-]*$/i, 'use letters, digits, "-" and "_", starting with a letter or digit');
|
|
171
|
+
var theme = z.enum(["light", "dark"]);
|
|
172
|
+
var position = { x: z.number().optional(), y: z.number().optional() };
|
|
173
|
+
var size = {
|
|
174
|
+
device: z.enum(DEVICE_NAMES).optional(),
|
|
175
|
+
width: z.number().int().min(80).max(8e3).optional(),
|
|
176
|
+
height: z.union([z.number().int().min(80).max(4e4), z.literal("auto")]).optional()
|
|
177
|
+
};
|
|
178
|
+
var DesignConfigSchema = z.strictObject({
|
|
179
|
+
$schema: z.string().optional(),
|
|
180
|
+
name: z.string().min(1).optional(),
|
|
181
|
+
/** Import aliases for screens, relative to the project root: `{ "@/": "src/" }`. */
|
|
182
|
+
alias: z.record(z.string(), z.string()).optional(),
|
|
183
|
+
/** Extra folders Tailwind scans for class names, relative to the project root. */
|
|
184
|
+
sources: z.array(z.string()).optional(),
|
|
185
|
+
port: z.number().int().min(1).max(65535).optional()
|
|
186
|
+
});
|
|
187
|
+
var SystemConfigSchema = z.strictObject({
|
|
188
|
+
$schema: z.string().optional(),
|
|
189
|
+
name: z.string().min(1),
|
|
190
|
+
description: z.string().optional(),
|
|
191
|
+
/** Stylesheet URLs every screen loads, such as Google Fonts. */
|
|
192
|
+
fonts: z.array(z.string().url()).optional(),
|
|
193
|
+
/** A stylesheet that replaces the default Tailwind + tokens.css entry, relative to the system folder. */
|
|
194
|
+
stylesheet: z.string().optional(),
|
|
195
|
+
/** Text the typography specimen sets. */
|
|
196
|
+
sample: z.string().optional()
|
|
197
|
+
});
|
|
198
|
+
var ScreenItemSchema = z.strictObject({
|
|
199
|
+
type: z.literal("screen"),
|
|
200
|
+
id: id.optional(),
|
|
201
|
+
src: z.string().min(1),
|
|
202
|
+
title: z.string().optional(),
|
|
203
|
+
description: z.string().optional(),
|
|
204
|
+
props: z.record(z.string(), z.unknown()).optional(),
|
|
205
|
+
theme: theme.optional(),
|
|
206
|
+
/** Inject the design system stylesheet; defaults to the canvas setting. */
|
|
207
|
+
system: z.boolean().optional(),
|
|
208
|
+
...size,
|
|
209
|
+
...position
|
|
210
|
+
});
|
|
211
|
+
var UrlItemSchema = z.strictObject({
|
|
212
|
+
type: z.literal("url"),
|
|
213
|
+
id: id.optional(),
|
|
214
|
+
url: z.string().regex(/^https?:\/\//, "must be an http(s) URL"),
|
|
215
|
+
title: z.string().optional(),
|
|
216
|
+
description: z.string().optional(),
|
|
217
|
+
...size,
|
|
218
|
+
...position
|
|
219
|
+
});
|
|
220
|
+
var NoteItemSchema = z.strictObject({
|
|
221
|
+
type: z.literal("note"),
|
|
222
|
+
id: id.optional(),
|
|
223
|
+
text: z.string(),
|
|
224
|
+
title: z.string().optional(),
|
|
225
|
+
width: z.number().int().min(80).max(4e3).optional(),
|
|
226
|
+
tone: z.enum(["note", "plain"]).optional(),
|
|
227
|
+
...position
|
|
228
|
+
});
|
|
229
|
+
var ImageItemSchema = z.strictObject({
|
|
230
|
+
type: z.literal("image"),
|
|
231
|
+
id: id.optional(),
|
|
232
|
+
src: z.string().min(1),
|
|
233
|
+
title: z.string().optional(),
|
|
234
|
+
description: z.string().optional(),
|
|
235
|
+
width: z.number().int().min(16).max(8e3).optional(),
|
|
236
|
+
...position
|
|
237
|
+
});
|
|
238
|
+
var ItemSchema = z.preprocess(
|
|
239
|
+
(value) => value && typeof value === "object" && !Array.isArray(value) && !("type" in value) ? { type: "screen", ...value } : value,
|
|
240
|
+
z.discriminatedUnion("type", [ScreenItemSchema, UrlItemSchema, NoteItemSchema, ImageItemSchema])
|
|
241
|
+
);
|
|
242
|
+
var SectionSchema = z.strictObject({
|
|
243
|
+
id: id.optional(),
|
|
244
|
+
title: z.string().optional(),
|
|
245
|
+
description: z.string().optional(),
|
|
246
|
+
columns: z.number().int().min(1).max(50).optional(),
|
|
247
|
+
items: z.array(z.unknown())
|
|
248
|
+
});
|
|
249
|
+
var PageSchema = z.strictObject({
|
|
250
|
+
id,
|
|
251
|
+
title: z.string().min(1),
|
|
252
|
+
description: z.string().optional(),
|
|
253
|
+
sections: z.array(z.unknown()).optional(),
|
|
254
|
+
items: z.array(z.unknown()).optional()
|
|
255
|
+
}).refine((page) => !(page.sections && page.items), 'a page has either "sections" or free-layout "items", not both');
|
|
256
|
+
var CanvasSchema = z.strictObject({
|
|
257
|
+
$schema: z.string().optional(),
|
|
258
|
+
title: z.string().min(1),
|
|
259
|
+
description: z.string().optional(),
|
|
260
|
+
/** Inject the design system stylesheet into screens (default true). */
|
|
261
|
+
system: z.boolean().optional(),
|
|
262
|
+
/** Theme screens open in (default follows the viewer). */
|
|
263
|
+
theme: theme.optional(),
|
|
264
|
+
pages: z.array(z.unknown()).min(1, "add at least one page")
|
|
265
|
+
});
|
|
266
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/i;
|
|
267
|
+
function formatPath(base, path19) {
|
|
268
|
+
let out = base;
|
|
269
|
+
for (const part of path19) out += typeof part === "number" ? `[${part}]` : out ? `.${String(part)}` : String(part);
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
function describeZodError(error, base = "") {
|
|
273
|
+
return error.issues.map((issue) => ({ at: formatPath(base, issue.path), message: issue.message }));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/core/canvas.ts
|
|
277
|
+
var MODULE_EXT = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".mjs"]);
|
|
278
|
+
var HTML_EXT = /* @__PURE__ */ new Set([".html", ".htm"]);
|
|
279
|
+
function listCanvasIds(paths) {
|
|
280
|
+
if (!fs2.existsSync(paths.canvases)) return [];
|
|
281
|
+
return fs2.readdirSync(paths.canvases, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort();
|
|
282
|
+
}
|
|
283
|
+
function frameOf(input) {
|
|
284
|
+
const custom = input.width !== void 0 || input.height !== void 0;
|
|
285
|
+
const device = input.device ?? (custom ? void 0 : DEFAULT_DEVICE);
|
|
286
|
+
const base = DEVICES[input.device ?? DEFAULT_DEVICE];
|
|
287
|
+
return { width: input.width ?? base.width, height: input.height ?? base.height, device };
|
|
288
|
+
}
|
|
289
|
+
function loadCanvas(paths, canvasId, urls, snapshots) {
|
|
290
|
+
const dir = path3.join(paths.canvases, canvasId);
|
|
291
|
+
const file = path3.join(dir, "canvas.json");
|
|
292
|
+
const fileRel = relToRoot(paths, file);
|
|
293
|
+
const issues = [];
|
|
294
|
+
const screens = [];
|
|
295
|
+
const report = (severity, message, at) => issues.push({ severity, file: fileRel, message, at });
|
|
296
|
+
const doc = { id: canvasId, title: titleize(canvasId), pages: [], issues, updatedAt: 0 };
|
|
297
|
+
if (!ID_PATTERN.test(canvasId)) {
|
|
298
|
+
report("error", `canvas folder "${canvasId}" is not a valid id (letters, digits, "-", "_")`);
|
|
299
|
+
return { doc, screens };
|
|
300
|
+
}
|
|
301
|
+
let source;
|
|
302
|
+
try {
|
|
303
|
+
source = fs2.readFileSync(file, "utf8");
|
|
304
|
+
doc.updatedAt = fs2.statSync(file).mtimeMs;
|
|
305
|
+
} catch {
|
|
306
|
+
report("error", "canvas.json is missing");
|
|
307
|
+
return { doc, screens };
|
|
308
|
+
}
|
|
309
|
+
let json;
|
|
310
|
+
try {
|
|
311
|
+
json = JSON.parse(source);
|
|
312
|
+
} catch (error) {
|
|
313
|
+
report("error", `invalid JSON: ${error.message}`);
|
|
314
|
+
return { doc, screens };
|
|
315
|
+
}
|
|
316
|
+
const top = CanvasSchema.safeParse(json);
|
|
317
|
+
let rawPages = [];
|
|
318
|
+
let systemDefault = true;
|
|
319
|
+
if (top.success) {
|
|
320
|
+
doc.title = top.data.title;
|
|
321
|
+
doc.description = top.data.description;
|
|
322
|
+
doc.theme = top.data.theme;
|
|
323
|
+
systemDefault = top.data.system ?? true;
|
|
324
|
+
rawPages = top.data.pages;
|
|
325
|
+
} else {
|
|
326
|
+
for (const issue of describeZodError(top.error)) report("error", issue.message, issue.at);
|
|
327
|
+
const loose = json;
|
|
328
|
+
if (typeof loose?.title === "string") doc.title = loose.title;
|
|
329
|
+
if (Array.isArray(loose?.pages)) rawPages = loose.pages;
|
|
330
|
+
}
|
|
331
|
+
const itemIds = /* @__PURE__ */ new Set();
|
|
332
|
+
const pageIds = /* @__PURE__ */ new Set();
|
|
333
|
+
const claimId = (wanted, explicit, at) => {
|
|
334
|
+
if (!itemIds.has(wanted)) {
|
|
335
|
+
itemIds.add(wanted);
|
|
336
|
+
return wanted;
|
|
337
|
+
}
|
|
338
|
+
let n = 2;
|
|
339
|
+
while (itemIds.has(`${wanted}-${n}`)) n++;
|
|
340
|
+
const id2 = `${wanted}-${n}`;
|
|
341
|
+
itemIds.add(id2);
|
|
342
|
+
report(
|
|
343
|
+
explicit ? "error" : "warning",
|
|
344
|
+
explicit ? `duplicate item id "${wanted}"; renamed to "${id2}"` : `two items would be called "${wanted}"; this one is "${id2}" \u2014 give it an explicit "id"`,
|
|
345
|
+
at
|
|
346
|
+
);
|
|
347
|
+
return id2;
|
|
348
|
+
};
|
|
349
|
+
const resolveItem = (item, at) => {
|
|
350
|
+
if (item.type === "screen") {
|
|
351
|
+
const abs2 = path3.resolve(dir, item.src);
|
|
352
|
+
const ext = path3.extname(abs2).toLowerCase();
|
|
353
|
+
const format = HTML_EXT.has(ext) ? "html" : MODULE_EXT.has(ext) ? "module" : null;
|
|
354
|
+
if (!format) {
|
|
355
|
+
report("error", `"${item.src}" is not a screen: use .tsx, .jsx or .html`, `${at}.src`);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
if (!isInside(paths.design, abs2)) report("warning", `"${item.src}" is outside .design`, `${at}.src`);
|
|
359
|
+
const missing2 = !fs2.existsSync(abs2);
|
|
360
|
+
if (missing2) report("error", `file not found: ${item.src}`, `${at}.src`);
|
|
361
|
+
const id3 = claimId(item.id ?? slugify(fileStem(item.src)), item.id !== void 0, at);
|
|
362
|
+
const title = item.title ?? titleize(fileStem(item.src));
|
|
363
|
+
const snap = snapshots?.(canvasId, id3);
|
|
364
|
+
const system = item.system ?? systemDefault;
|
|
365
|
+
const theme2 = item.theme;
|
|
366
|
+
const frame = frameOf(item);
|
|
367
|
+
const rev = createHash("sha1").update(JSON.stringify([item.src, item.props ?? null, theme2 ?? null, system, frame.height === "auto"])).digest("hex").slice(0, 10);
|
|
368
|
+
screens.push({
|
|
369
|
+
key: `${canvasId}/${id3}`,
|
|
370
|
+
canvas: canvasId,
|
|
371
|
+
id: id3,
|
|
372
|
+
title,
|
|
373
|
+
file: abs2,
|
|
374
|
+
format,
|
|
375
|
+
props: item.props ?? {},
|
|
376
|
+
system,
|
|
377
|
+
theme: theme2,
|
|
378
|
+
autoHeight: frame.height === "auto"
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
kind: "screen",
|
|
382
|
+
id: id3,
|
|
383
|
+
title,
|
|
384
|
+
description: item.description,
|
|
385
|
+
x: item.x,
|
|
386
|
+
y: item.y,
|
|
387
|
+
src: item.src,
|
|
388
|
+
file: relToRoot(paths, abs2),
|
|
389
|
+
format,
|
|
390
|
+
url: format === "module" ? urls.screen(canvasId, id3) : urls.html(relToDesign(paths, abs2), canvasId, id3),
|
|
391
|
+
frame,
|
|
392
|
+
theme: theme2,
|
|
393
|
+
missing: missing2 || void 0,
|
|
394
|
+
rev,
|
|
395
|
+
snapshots: snap?.urls,
|
|
396
|
+
measuredHeight: snap?.height
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
if (item.type === "url") {
|
|
400
|
+
const id3 = claimId(item.id ?? slugify(new URL(item.url).hostname), item.id !== void 0, at);
|
|
401
|
+
return {
|
|
402
|
+
kind: "url",
|
|
403
|
+
id: id3,
|
|
404
|
+
title: item.title ?? item.url.replace(/^https?:\/\//, ""),
|
|
405
|
+
description: item.description,
|
|
406
|
+
x: item.x,
|
|
407
|
+
y: item.y,
|
|
408
|
+
url: item.url,
|
|
409
|
+
frame: frameOf(item)
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
if (item.type === "note") {
|
|
413
|
+
const id3 = claimId(item.id ?? "note", item.id !== void 0, at);
|
|
414
|
+
return {
|
|
415
|
+
kind: "note",
|
|
416
|
+
id: id3,
|
|
417
|
+
title: item.title ?? "",
|
|
418
|
+
x: item.x,
|
|
419
|
+
y: item.y,
|
|
420
|
+
text: item.text,
|
|
421
|
+
width: item.width ?? (item.tone === "plain" ? 560 : 320),
|
|
422
|
+
tone: item.tone ?? "note"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const remote = /^https?:\/\//.test(item.src);
|
|
426
|
+
const abs = remote ? "" : path3.resolve(dir, item.src);
|
|
427
|
+
const missing = !remote && !fs2.existsSync(abs);
|
|
428
|
+
if (missing) report("error", `image not found: ${item.src}`, `${at}.src`);
|
|
429
|
+
const id2 = claimId(item.id ?? slugify(fileStem(item.src)), item.id !== void 0, at);
|
|
430
|
+
return {
|
|
431
|
+
kind: "image",
|
|
432
|
+
id: id2,
|
|
433
|
+
title: item.title ?? titleize(fileStem(item.src)),
|
|
434
|
+
description: item.description,
|
|
435
|
+
x: item.x,
|
|
436
|
+
y: item.y,
|
|
437
|
+
url: remote ? item.src : urls.file(relToDesign(paths, abs)),
|
|
438
|
+
width: item.width,
|
|
439
|
+
missing: missing || void 0
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
const resolveItems = (raw, at) => {
|
|
443
|
+
const out = [];
|
|
444
|
+
raw.forEach((value, index) => {
|
|
445
|
+
const itemAt = `${at}[${index}]`;
|
|
446
|
+
const parsed = ItemSchema.safeParse(value);
|
|
447
|
+
if (!parsed.success) {
|
|
448
|
+
for (const issue of describeZodError(parsed.error, itemAt)) report("error", issue.message, issue.at);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const item = resolveItem(parsed.data, itemAt);
|
|
452
|
+
if (item) out.push(item);
|
|
453
|
+
});
|
|
454
|
+
return out;
|
|
455
|
+
};
|
|
456
|
+
rawPages.forEach((value, pageIndex) => {
|
|
457
|
+
const at = `pages[${pageIndex}]`;
|
|
458
|
+
const parsed = PageSchema.safeParse(value);
|
|
459
|
+
if (!parsed.success) {
|
|
460
|
+
for (const issue of describeZodError(parsed.error, at)) report("error", issue.message, issue.at);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const page = parsed.data;
|
|
464
|
+
if (pageIds.has(page.id)) {
|
|
465
|
+
report("error", `duplicate page id "${page.id}"`, `${at}.id`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
pageIds.add(page.id);
|
|
469
|
+
const sections = [];
|
|
470
|
+
if (page.items) {
|
|
471
|
+
sections.push({ id: "free", items: resolveItems(page.items, `${at}.items`) });
|
|
472
|
+
} else {
|
|
473
|
+
const sectionIds = /* @__PURE__ */ new Set();
|
|
474
|
+
(page.sections ?? []).forEach((rawSection, sectionIndex) => {
|
|
475
|
+
const sectionAt = `${at}.sections[${sectionIndex}]`;
|
|
476
|
+
const section = SectionSchema.safeParse(rawSection);
|
|
477
|
+
if (!section.success) {
|
|
478
|
+
for (const issue of describeZodError(section.error, sectionAt)) report("error", issue.message, issue.at);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
let sectionId = section.data.id ?? `section-${sectionIndex + 1}`;
|
|
482
|
+
if (sectionIds.has(sectionId)) sectionId = `${sectionId}-${sectionIndex + 1}`;
|
|
483
|
+
sectionIds.add(sectionId);
|
|
484
|
+
sections.push({
|
|
485
|
+
id: sectionId,
|
|
486
|
+
title: section.data.title,
|
|
487
|
+
description: section.data.description,
|
|
488
|
+
columns: section.data.columns,
|
|
489
|
+
items: resolveItems(section.data.items, `${sectionAt}.items`)
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
const out = {
|
|
494
|
+
id: page.id,
|
|
495
|
+
title: page.title,
|
|
496
|
+
description: page.description,
|
|
497
|
+
layout: page.items ? "free" : "sections",
|
|
498
|
+
sections
|
|
499
|
+
};
|
|
500
|
+
doc.pages.push(out);
|
|
501
|
+
});
|
|
502
|
+
return { doc, screens };
|
|
503
|
+
}
|
|
504
|
+
function summarize(canvas) {
|
|
505
|
+
const { doc } = canvas;
|
|
506
|
+
const screens = doc.pages.flatMap((page) => page.sections.flatMap((section) => section.items));
|
|
507
|
+
const firstScreen = screens.find((item) => item.kind === "screen" && item.snapshots);
|
|
508
|
+
return {
|
|
509
|
+
id: doc.id,
|
|
510
|
+
title: doc.title,
|
|
511
|
+
description: doc.description,
|
|
512
|
+
pages: doc.pages.length,
|
|
513
|
+
screens: screens.filter((item) => item.kind === "screen" || item.kind === "url").length,
|
|
514
|
+
updatedAt: doc.updatedAt,
|
|
515
|
+
issues: doc.issues.filter((issue) => issue.severity === "error").length,
|
|
516
|
+
cover: firstScreen && firstScreen.kind === "screen" ? firstScreen.snapshots?.light ?? firstScreen.snapshots?.dark : void 0
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/core/system.ts
|
|
521
|
+
import fs4 from "fs";
|
|
522
|
+
import path5 from "path";
|
|
523
|
+
|
|
524
|
+
// src/core/tokens.ts
|
|
525
|
+
import fs3 from "fs";
|
|
526
|
+
import path4 from "path";
|
|
527
|
+
import postcss from "postcss";
|
|
528
|
+
var NAMESPACES = [
|
|
529
|
+
["--color-", "color"],
|
|
530
|
+
["--font-weight-", "weight"],
|
|
531
|
+
["--font-", "font"],
|
|
532
|
+
["--text-shadow-", "shadow"],
|
|
533
|
+
["--text-", "text"],
|
|
534
|
+
["--leading-", "leading"],
|
|
535
|
+
["--tracking-", "tracking"],
|
|
536
|
+
["--spacing-", "spacing"],
|
|
537
|
+
["--radius-", "radius"],
|
|
538
|
+
["--inset-shadow-", "shadow"],
|
|
539
|
+
["--drop-shadow-", "shadow"],
|
|
540
|
+
["--shadow-", "shadow"],
|
|
541
|
+
["--blur-", "blur"],
|
|
542
|
+
["--ease-", "ease"],
|
|
543
|
+
["--animate-", "animation"],
|
|
544
|
+
["--duration-", "duration"],
|
|
545
|
+
["--breakpoint-", "breakpoint"],
|
|
546
|
+
["--container-", "breakpoint"]
|
|
547
|
+
];
|
|
548
|
+
var DEFAULT_GROUP = {
|
|
549
|
+
color: "Colors",
|
|
550
|
+
font: "Font families",
|
|
551
|
+
text: "Type scale",
|
|
552
|
+
weight: "Font weights",
|
|
553
|
+
leading: "Line heights",
|
|
554
|
+
tracking: "Letter spacing",
|
|
555
|
+
spacing: "Spacing",
|
|
556
|
+
radius: "Radii",
|
|
557
|
+
shadow: "Shadows",
|
|
558
|
+
blur: "Blur",
|
|
559
|
+
ease: "Easing",
|
|
560
|
+
animation: "Animations",
|
|
561
|
+
duration: "Durations",
|
|
562
|
+
breakpoint: "Breakpoints",
|
|
563
|
+
other: "Other"
|
|
564
|
+
};
|
|
565
|
+
var COLOR_FN = /^(?:#[0-9a-f]{3,8}\b|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color|color-mix|light-dark)\()/i;
|
|
566
|
+
var COLOR_NAMES = /* @__PURE__ */ new Set([
|
|
567
|
+
"transparent",
|
|
568
|
+
"currentcolor",
|
|
569
|
+
"black",
|
|
570
|
+
"white",
|
|
571
|
+
"red",
|
|
572
|
+
"green",
|
|
573
|
+
"blue",
|
|
574
|
+
"gray",
|
|
575
|
+
"grey",
|
|
576
|
+
"orange",
|
|
577
|
+
"yellow",
|
|
578
|
+
"purple",
|
|
579
|
+
"pink",
|
|
580
|
+
"teal",
|
|
581
|
+
"navy",
|
|
582
|
+
"silver"
|
|
583
|
+
]);
|
|
584
|
+
function isColor(value) {
|
|
585
|
+
const v = value.trim();
|
|
586
|
+
return COLOR_FN.test(v) || COLOR_NAMES.has(v.toLowerCase());
|
|
587
|
+
}
|
|
588
|
+
function namespaceOf(name) {
|
|
589
|
+
if (name === "--spacing") return { kind: "spacing", key: "" };
|
|
590
|
+
for (const [prefix, kind] of NAMESPACES) if (name.startsWith(prefix)) return { kind, key: name.slice(prefix.length) };
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
function inferKind(name, value) {
|
|
594
|
+
const n = name.toLowerCase();
|
|
595
|
+
if (isColor(value)) return "color";
|
|
596
|
+
if (/shadow/.test(n)) return "shadow";
|
|
597
|
+
if (/radius|rounded/.test(n)) return "radius";
|
|
598
|
+
if (/ease|easing/.test(n) || /^cubic-bezier\(|^steps\(/.test(value.trim())) return "ease";
|
|
599
|
+
if (/duration/.test(n) || /^-?[\d.]+m?s$/.test(value.trim())) return "duration";
|
|
600
|
+
if (/font|family/.test(n) && /["',]/.test(value)) return "font";
|
|
601
|
+
if (/space|spacing|gap|gutter/.test(n)) return "spacing";
|
|
602
|
+
return "other";
|
|
603
|
+
}
|
|
604
|
+
function isDarkSelector(selector) {
|
|
605
|
+
return /\[data-(?:theme|mode|color-scheme)=["']?dark["']?\]|\.dark\b|:is\(\.dark|\.theme-dark\b/.test(selector);
|
|
606
|
+
}
|
|
607
|
+
function isRootSelector(selector) {
|
|
608
|
+
return /(^|[\s,])(:root|html|:host|\*)(?=$|[\s,:[.])/.test(selector) || /^\s*:root/.test(selector);
|
|
609
|
+
}
|
|
610
|
+
function insideDarkMedia(node) {
|
|
611
|
+
let parent = node.parent;
|
|
612
|
+
while (parent && parent.type !== "root") {
|
|
613
|
+
if (parent.type === "atrule") {
|
|
614
|
+
const at = parent;
|
|
615
|
+
if (at.name === "media" && /prefers-color-scheme\s*:\s*dark/.test(at.params)) return true;
|
|
616
|
+
}
|
|
617
|
+
parent = parent.parent;
|
|
618
|
+
}
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
function scopeOf(container) {
|
|
622
|
+
if (container.type === "atrule") {
|
|
623
|
+
const at = container;
|
|
624
|
+
return at.name === "theme" ? "theme" : null;
|
|
625
|
+
}
|
|
626
|
+
if (container.type !== "rule") return null;
|
|
627
|
+
const rule = container;
|
|
628
|
+
if (isDarkSelector(rule.selector) || insideDarkMedia(rule)) return "dark";
|
|
629
|
+
if (isRootSelector(rule.selector)) return "root";
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
function commentText(node) {
|
|
633
|
+
return node.type === "comment" ? node.text.trim() : null;
|
|
634
|
+
}
|
|
635
|
+
function collect(css2, from) {
|
|
636
|
+
const root = postcss.parse(css2, { from });
|
|
637
|
+
const out = [];
|
|
638
|
+
const visit = (container) => {
|
|
639
|
+
const scope = scopeOf(container);
|
|
640
|
+
if (scope) {
|
|
641
|
+
let group;
|
|
642
|
+
let pending;
|
|
643
|
+
const nodes = container.nodes ?? [];
|
|
644
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
645
|
+
const node = nodes[i];
|
|
646
|
+
const text = commentText(node);
|
|
647
|
+
if (text !== null) {
|
|
648
|
+
const marker = /^@group\s+(.+)$/.exec(text);
|
|
649
|
+
if (marker) {
|
|
650
|
+
group = marker[1].trim();
|
|
651
|
+
pending = void 0;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
const prev = nodes[i - 1];
|
|
655
|
+
if (prev?.type === "decl" && !/\n/.test(node.raws.before ?? "")) {
|
|
656
|
+
const last = out[out.length - 1];
|
|
657
|
+
if (last && last.name === prev.prop && !last.description) last.description = text;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
pending = text;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (node.type === "decl") {
|
|
664
|
+
const decl = node;
|
|
665
|
+
if (decl.prop.startsWith("--") && !decl.prop.includes("*") && decl.value.trim() !== "initial") {
|
|
666
|
+
out.push({ name: decl.prop, value: decl.value.trim(), scope, group, description: pending });
|
|
667
|
+
}
|
|
668
|
+
pending = void 0;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
pending = void 0;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const node of container.nodes ?? []) {
|
|
675
|
+
if (node.type === "rule" || node.type === "atrule") visit(node);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
visit(root);
|
|
679
|
+
return out;
|
|
680
|
+
}
|
|
681
|
+
function readStylesheets(entry) {
|
|
682
|
+
const seen = /* @__PURE__ */ new Set();
|
|
683
|
+
const files = [];
|
|
684
|
+
const walk = (file) => {
|
|
685
|
+
if (seen.has(file) || !fs3.existsSync(file)) return;
|
|
686
|
+
seen.add(file);
|
|
687
|
+
const css2 = fs3.readFileSync(file, "utf8");
|
|
688
|
+
const imports = [...css2.matchAll(/@import\s+(?:url\()?\s*["']([^"']+)["']/g)].map((m) => m[1]);
|
|
689
|
+
for (const spec of imports) {
|
|
690
|
+
if (spec.startsWith("./") || spec.startsWith("../")) walk(path4.resolve(path4.dirname(file), spec));
|
|
691
|
+
}
|
|
692
|
+
files.push({ file, css: css2 });
|
|
693
|
+
};
|
|
694
|
+
walk(entry);
|
|
695
|
+
return files;
|
|
696
|
+
}
|
|
697
|
+
function closingParen(text, start) {
|
|
698
|
+
let depth = 1;
|
|
699
|
+
for (let i = start; i < text.length; i++) {
|
|
700
|
+
if (text[i] === "(") depth++;
|
|
701
|
+
else if (text[i] === ")" && --depth === 0) return i;
|
|
702
|
+
}
|
|
703
|
+
return -1;
|
|
704
|
+
}
|
|
705
|
+
function resolveValue(value, scope, depth = 0) {
|
|
706
|
+
if (depth > 12 || !value.includes("var(")) return value;
|
|
707
|
+
let out = "";
|
|
708
|
+
let i = 0;
|
|
709
|
+
for (; ; ) {
|
|
710
|
+
const at = value.indexOf("var(", i);
|
|
711
|
+
if (at === -1) break;
|
|
712
|
+
const end = closingParen(value, at + 4);
|
|
713
|
+
if (end === -1) break;
|
|
714
|
+
const inner = value.slice(at + 4, end);
|
|
715
|
+
let comma = -1;
|
|
716
|
+
let level = 0;
|
|
717
|
+
for (let j = 0; j < inner.length; j++) {
|
|
718
|
+
if (inner[j] === "(") level++;
|
|
719
|
+
else if (inner[j] === ")") level--;
|
|
720
|
+
else if (inner[j] === "," && level === 0) {
|
|
721
|
+
comma = j;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const name = (comma === -1 ? inner : inner.slice(0, comma)).trim();
|
|
726
|
+
const fallback = comma === -1 ? void 0 : inner.slice(comma + 1).trim();
|
|
727
|
+
const found = scope.get(name);
|
|
728
|
+
const replacement = found !== void 0 ? resolveValue(found, scope, depth + 1) : fallback !== void 0 ? resolveValue(fallback, scope, depth + 1) : value.slice(at, end + 1);
|
|
729
|
+
out += value.slice(i, at) + replacement;
|
|
730
|
+
i = end + 1;
|
|
731
|
+
}
|
|
732
|
+
return out + value.slice(i);
|
|
733
|
+
}
|
|
734
|
+
function directRef(value) {
|
|
735
|
+
const match = /^var\(\s*(--[\w-]+)\s*(?:,[\s\S]*)?\)$/.exec(value.trim());
|
|
736
|
+
return match ? match[1] : null;
|
|
737
|
+
}
|
|
738
|
+
function parseTokens(files) {
|
|
739
|
+
const decls = files.flatMap(({ file, css: css2 }) => {
|
|
740
|
+
try {
|
|
741
|
+
return collect(css2, file);
|
|
742
|
+
} catch {
|
|
743
|
+
return [];
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
const rootVars = /* @__PURE__ */ new Map();
|
|
747
|
+
const darkVars = /* @__PURE__ */ new Map();
|
|
748
|
+
const themeVars = /* @__PURE__ */ new Map();
|
|
749
|
+
for (const decl of decls) {
|
|
750
|
+
const map = decl.scope === "root" ? rootVars : decl.scope === "dark" ? darkVars : themeVars;
|
|
751
|
+
const prev = map.get(decl.name);
|
|
752
|
+
map.set(decl.name, {
|
|
753
|
+
...decl,
|
|
754
|
+
group: decl.group ?? prev?.group,
|
|
755
|
+
description: decl.description ?? prev?.description
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
const light = /* @__PURE__ */ new Map();
|
|
759
|
+
for (const [name, decl] of themeVars) light.set(name, decl.value);
|
|
760
|
+
for (const [name, decl] of rootVars) light.set(name, decl.value);
|
|
761
|
+
const dark = new Map(light);
|
|
762
|
+
for (const [name, decl] of darkVars) dark.set(name, decl.value);
|
|
763
|
+
const value = (raw, scope) => ({ raw, value: resolveValue(raw, scope) });
|
|
764
|
+
const darkOf = (name, raw, lightValue) => {
|
|
765
|
+
const own = darkVars.get(name);
|
|
766
|
+
const resolved = value(own?.value ?? raw, dark);
|
|
767
|
+
return resolved.value !== lightValue ? resolved : void 0;
|
|
768
|
+
};
|
|
769
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
770
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
771
|
+
const lineHeights = /* @__PURE__ */ new Map();
|
|
772
|
+
for (const [name, decl] of themeVars) {
|
|
773
|
+
const companion = /^(--text-[\w-]+?)--line-height$/.exec(name);
|
|
774
|
+
if (companion) {
|
|
775
|
+
lineHeights.set(companion[1], resolveValue(decl.value, light));
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (/--(?:letter-spacing|font-weight|font-feature-settings|font-variation-settings)$/.test(name)) continue;
|
|
779
|
+
const ns = namespaceOf(name);
|
|
780
|
+
const ref = directRef(decl.value);
|
|
781
|
+
const source = ref && rootVars.has(ref) ? rootVars.get(ref) : null;
|
|
782
|
+
const tokenName = source ? ref : name;
|
|
783
|
+
if (tokens.has(tokenName)) continue;
|
|
784
|
+
if (source) consumed.add(ref);
|
|
785
|
+
const raw = source ? source.value : decl.value;
|
|
786
|
+
const lightValue = value(raw, light);
|
|
787
|
+
const kind = ns?.kind ?? inferKind(tokenName, lightValue.value);
|
|
788
|
+
tokens.set(tokenName, {
|
|
789
|
+
name: tokenName,
|
|
790
|
+
kind,
|
|
791
|
+
group: source?.group ?? decl.group ?? DEFAULT_GROUP[kind],
|
|
792
|
+
description: source?.description ?? decl.description,
|
|
793
|
+
light: lightValue,
|
|
794
|
+
dark: darkOf(tokenName, raw, lightValue.value),
|
|
795
|
+
utility: ns ? ns.key : void 0
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
for (const [name, decl] of rootVars) {
|
|
799
|
+
if (consumed.has(name) || tokens.has(name)) continue;
|
|
800
|
+
const lightValue = value(decl.value, light);
|
|
801
|
+
const kind = namespaceOf(name)?.kind ?? inferKind(name, lightValue.value);
|
|
802
|
+
tokens.set(name, {
|
|
803
|
+
name,
|
|
804
|
+
kind,
|
|
805
|
+
group: decl.group ?? (kind === "other" ? "Variables" : DEFAULT_GROUP[kind]),
|
|
806
|
+
description: decl.description,
|
|
807
|
+
light: lightValue,
|
|
808
|
+
dark: darkOf(name, decl.value, lightValue.value)
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
for (const [name, lineHeight] of lineHeights) {
|
|
812
|
+
const token = tokens.get(name);
|
|
813
|
+
if (token) token.lineHeight = lineHeight;
|
|
814
|
+
}
|
|
815
|
+
return [...tokens.values()];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/core/system.ts
|
|
819
|
+
var SYSTEM_CANVAS = "@system";
|
|
820
|
+
var TYPOGRAPHY_ID = "@typography";
|
|
821
|
+
var SPECIMEN_EXT = /* @__PURE__ */ new Set([".tsx", ".jsx", ".html"]);
|
|
822
|
+
var COMPONENT_EXT = [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html"];
|
|
823
|
+
var IMAGE_EXT = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico"]);
|
|
824
|
+
var FONT_EXT = /* @__PURE__ */ new Set([".woff", ".woff2", ".ttf", ".otf"]);
|
|
825
|
+
function listFiles(dir, deep = false) {
|
|
826
|
+
if (!fs4.existsSync(dir)) return [];
|
|
827
|
+
const out = [];
|
|
828
|
+
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
829
|
+
if (entry.name.startsWith(".")) continue;
|
|
830
|
+
const full = path5.join(dir, entry.name);
|
|
831
|
+
if (entry.isDirectory()) {
|
|
832
|
+
if (deep) out.push(...listFiles(full, true));
|
|
833
|
+
} else out.push(full);
|
|
834
|
+
}
|
|
835
|
+
return out.sort();
|
|
836
|
+
}
|
|
837
|
+
function guessSources(componentsDir, stem) {
|
|
838
|
+
if (!fs4.existsSync(componentsDir)) return [];
|
|
839
|
+
const wanted = slugify(stem);
|
|
840
|
+
for (const file of listFiles(componentsDir)) {
|
|
841
|
+
const ext = path5.extname(file);
|
|
842
|
+
if (COMPONENT_EXT.includes(ext) && slugify(path5.basename(file, ext)) === wanted) return [file];
|
|
843
|
+
}
|
|
844
|
+
for (const entry of fs4.readdirSync(componentsDir, { withFileTypes: true })) {
|
|
845
|
+
if (entry.isDirectory() && slugify(entry.name) === wanted) {
|
|
846
|
+
const files = listFiles(path5.join(componentsDir, entry.name)).filter(
|
|
847
|
+
(f) => COMPONENT_EXT.includes(path5.extname(f))
|
|
848
|
+
);
|
|
849
|
+
if (files.length) return files;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
function loadSystem(paths, urls) {
|
|
855
|
+
if (!fs4.existsSync(paths.system)) return null;
|
|
856
|
+
const issues = [];
|
|
857
|
+
const configFile = path5.join(paths.system, "system.json");
|
|
858
|
+
const report = (severity, file, message, at) => issues.push({ severity, file: relToRoot(paths, file), message, at });
|
|
859
|
+
let config = null;
|
|
860
|
+
if (fs4.existsSync(configFile)) {
|
|
861
|
+
try {
|
|
862
|
+
const parsed = SystemConfigSchema.safeParse(JSON.parse(fs4.readFileSync(configFile, "utf8")));
|
|
863
|
+
if (parsed.success) config = parsed.data;
|
|
864
|
+
else for (const issue of describeZodError(parsed.error)) report("error", configFile, issue.message, issue.at);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
report("error", configFile, `invalid JSON: ${error.message}`);
|
|
867
|
+
}
|
|
868
|
+
} else {
|
|
869
|
+
report("warning", configFile, 'system.json is missing; run `design system init` or add { "name": "\u2026" }');
|
|
870
|
+
}
|
|
871
|
+
const tokensFile = path5.join(paths.system, "tokens.css");
|
|
872
|
+
let stylesheet = null;
|
|
873
|
+
if (config?.stylesheet) {
|
|
874
|
+
stylesheet = path5.resolve(paths.system, config.stylesheet);
|
|
875
|
+
if (!fs4.existsSync(stylesheet)) {
|
|
876
|
+
report("error", configFile, `stylesheet not found: ${config.stylesheet}`, "stylesheet");
|
|
877
|
+
stylesheet = null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const tokenEntry = stylesheet ?? (fs4.existsSync(tokensFile) ? tokensFile : null);
|
|
881
|
+
const tokens = tokenEntry ? parseTokens(readStylesheets(tokenEntry)) : [];
|
|
882
|
+
if (!tokenEntry) report("warning", tokensFile, "no tokens.css: screens get the default Tailwind theme");
|
|
883
|
+
const specimensDir = path5.join(paths.system, "specimens");
|
|
884
|
+
const componentsDir = path5.join(paths.system, "components");
|
|
885
|
+
const components = [];
|
|
886
|
+
const specimens = [];
|
|
887
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
888
|
+
for (const file of listFiles(specimensDir)) {
|
|
889
|
+
const ext = path5.extname(file).toLowerCase();
|
|
890
|
+
if (!SPECIMEN_EXT.has(ext)) continue;
|
|
891
|
+
const stem = path5.basename(file, ext);
|
|
892
|
+
let id2 = slugify(stem);
|
|
893
|
+
if (usedIds.has(id2)) id2 = `${id2}-${usedIds.size + 1}`;
|
|
894
|
+
usedIds.add(id2);
|
|
895
|
+
const tags = parseDocTags(fs4.readFileSync(file, "utf8"));
|
|
896
|
+
const sources = (tags.source ?? []).flatMap((value) => value.split(/[\s,]+/)).filter(Boolean).map((value) => path5.resolve(path5.dirname(file), value));
|
|
897
|
+
for (const source of sources) {
|
|
898
|
+
if (!fs4.existsSync(source)) report("warning", file, `@source not found: ${path5.relative(paths.system, source)}`);
|
|
899
|
+
}
|
|
900
|
+
const found = sources.filter((source) => fs4.existsSync(source));
|
|
901
|
+
const title = tags.title?.[0] || titleize(stem);
|
|
902
|
+
const format = ext === ".html" ? "html" : "module";
|
|
903
|
+
components.push({
|
|
904
|
+
id: id2,
|
|
905
|
+
title,
|
|
906
|
+
group: tags.group?.[0] || "Components",
|
|
907
|
+
description: tags.description?.[0] || void 0,
|
|
908
|
+
status: tags.status?.[0] || void 0,
|
|
909
|
+
specimen: relToRoot(paths, file),
|
|
910
|
+
sources: (found.length ? found : guessSources(componentsDir, stem)).map((source) => relToRoot(paths, source)),
|
|
911
|
+
url: format === "module" ? urls.screen(SYSTEM_CANVAS, id2) : urls.html(relToDesign(paths, file), SYSTEM_CANVAS, id2)
|
|
912
|
+
});
|
|
913
|
+
specimens.push({
|
|
914
|
+
key: `${SYSTEM_CANVAS}/${id2}`,
|
|
915
|
+
canvas: SYSTEM_CANVAS,
|
|
916
|
+
id: id2,
|
|
917
|
+
title,
|
|
918
|
+
file,
|
|
919
|
+
format,
|
|
920
|
+
props: {},
|
|
921
|
+
system: true,
|
|
922
|
+
autoHeight: true
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
const guidelines = [];
|
|
926
|
+
for (const file of listFiles(path5.join(paths.system, "guidelines"))) {
|
|
927
|
+
if (path5.extname(file).toLowerCase() !== ".md") continue;
|
|
928
|
+
const stem = path5.basename(file, ".md");
|
|
929
|
+
const { data, body } = parseFrontmatter(fs4.readFileSync(file, "utf8"));
|
|
930
|
+
const prefix = /^(\d+)[-_. ]+(.*)$/.exec(stem);
|
|
931
|
+
const order = data.order !== void 0 ? Number(data.order) : prefix ? Number(prefix[1]) : 1e3;
|
|
932
|
+
guidelines.push({
|
|
933
|
+
slug: slugify(prefix ? prefix[2] : stem),
|
|
934
|
+
title: data.title || firstHeading(body) || titleize(prefix ? prefix[2] : stem),
|
|
935
|
+
order: Number.isFinite(order) ? order : 1e3,
|
|
936
|
+
markdown: body
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
guidelines.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));
|
|
940
|
+
const assets = [];
|
|
941
|
+
const assetsDir = path5.join(paths.system, "assets");
|
|
942
|
+
for (const file of listFiles(assetsDir, true)) {
|
|
943
|
+
const ext = path5.extname(file).toLowerCase();
|
|
944
|
+
assets.push({
|
|
945
|
+
name: path5.basename(file),
|
|
946
|
+
path: path5.relative(paths.system, file).split(path5.sep).join("/"),
|
|
947
|
+
url: urls.file(relToDesign(paths, file)),
|
|
948
|
+
size: fs4.statSync(file).size,
|
|
949
|
+
kind: IMAGE_EXT.has(ext) ? "image" : FONT_EXT.has(ext) ? "font" : "other"
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
const hasType = tokens.some((token) => token.kind === "font" || token.kind === "text");
|
|
953
|
+
if (hasType) {
|
|
954
|
+
specimens.push({
|
|
955
|
+
key: `${SYSTEM_CANVAS}/${TYPOGRAPHY_ID}`,
|
|
956
|
+
canvas: SYSTEM_CANVAS,
|
|
957
|
+
id: TYPOGRAPHY_ID,
|
|
958
|
+
title: "Typography",
|
|
959
|
+
file: "",
|
|
960
|
+
format: "module",
|
|
961
|
+
props: {
|
|
962
|
+
tokens: tokens.filter((token) => ["font", "text", "weight", "leading", "tracking"].includes(token.kind)),
|
|
963
|
+
sample: config?.sample
|
|
964
|
+
},
|
|
965
|
+
system: true,
|
|
966
|
+
autoHeight: true,
|
|
967
|
+
builtin: "typography"
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
return {
|
|
971
|
+
doc: {
|
|
972
|
+
name: config?.name ?? "Design system",
|
|
973
|
+
description: config?.description,
|
|
974
|
+
fonts: config?.fonts ?? [],
|
|
975
|
+
tokens,
|
|
976
|
+
components,
|
|
977
|
+
guidelines,
|
|
978
|
+
assets,
|
|
979
|
+
typographyUrl: hasType ? urls.screen(SYSTEM_CANVAS, TYPOGRAPHY_ID) : null,
|
|
980
|
+
issues
|
|
981
|
+
},
|
|
982
|
+
config,
|
|
983
|
+
stylesheet,
|
|
984
|
+
tokensFile: fs4.existsSync(tokensFile) ? tokensFile : null,
|
|
985
|
+
specimens
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
function summarizeSystem(system) {
|
|
989
|
+
return {
|
|
990
|
+
name: system.doc.name,
|
|
991
|
+
description: system.doc.description,
|
|
992
|
+
tokens: system.doc.tokens.length,
|
|
993
|
+
components: system.doc.components.length,
|
|
994
|
+
guidelines: system.doc.guidelines.length,
|
|
995
|
+
issues: system.doc.issues.filter((issue) => issue.severity === "error").length
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/core/project.ts
|
|
1000
|
+
var DesignProject = class {
|
|
1001
|
+
constructor(paths, urls, snapshots = null) {
|
|
1002
|
+
this.paths = paths;
|
|
1003
|
+
this.urls = urls;
|
|
1004
|
+
this.snapshots = snapshots;
|
|
1005
|
+
}
|
|
1006
|
+
paths;
|
|
1007
|
+
urls;
|
|
1008
|
+
snapshots;
|
|
1009
|
+
configCache = null;
|
|
1010
|
+
systemCache;
|
|
1011
|
+
canvasCache = /* @__PURE__ */ new Map();
|
|
1012
|
+
config() {
|
|
1013
|
+
if (this.configCache) return this.configCache;
|
|
1014
|
+
const issues = [];
|
|
1015
|
+
let config = {};
|
|
1016
|
+
const file = this.paths.config;
|
|
1017
|
+
if (fs5.existsSync(file)) {
|
|
1018
|
+
try {
|
|
1019
|
+
const parsed = DesignConfigSchema.safeParse(JSON.parse(fs5.readFileSync(file, "utf8")));
|
|
1020
|
+
if (parsed.success) config = parsed.data;
|
|
1021
|
+
else
|
|
1022
|
+
for (const issue of describeZodError(parsed.error))
|
|
1023
|
+
issues.push({ severity: "error", file: relToRoot(this.paths, file), message: issue.message, at: issue.at });
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
issues.push({
|
|
1026
|
+
severity: "error",
|
|
1027
|
+
file: relToRoot(this.paths, file),
|
|
1028
|
+
message: `invalid JSON: ${error.message}`
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
this.configCache = { config, issues };
|
|
1033
|
+
return this.configCache;
|
|
1034
|
+
}
|
|
1035
|
+
name() {
|
|
1036
|
+
const configured = this.config().config.name;
|
|
1037
|
+
if (configured) return configured;
|
|
1038
|
+
try {
|
|
1039
|
+
const manifest = JSON.parse(fs5.readFileSync(path6.join(this.paths.root, "package.json"), "utf8"));
|
|
1040
|
+
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
1041
|
+
} catch {
|
|
1042
|
+
}
|
|
1043
|
+
return path6.basename(this.paths.root);
|
|
1044
|
+
}
|
|
1045
|
+
system() {
|
|
1046
|
+
if (this.systemCache === void 0) this.systemCache = loadSystem(this.paths, this.urls);
|
|
1047
|
+
return this.systemCache;
|
|
1048
|
+
}
|
|
1049
|
+
canvasIds() {
|
|
1050
|
+
return listCanvasIds(this.paths);
|
|
1051
|
+
}
|
|
1052
|
+
canvas(id2) {
|
|
1053
|
+
const cached = this.canvasCache.get(id2);
|
|
1054
|
+
if (cached) return cached;
|
|
1055
|
+
if (!fs5.existsSync(path6.join(this.paths.canvases, id2))) return null;
|
|
1056
|
+
const canvas = loadCanvas(this.paths, id2, this.urls, this.snapshots);
|
|
1057
|
+
this.canvasCache.set(id2, canvas);
|
|
1058
|
+
return canvas;
|
|
1059
|
+
}
|
|
1060
|
+
canvases() {
|
|
1061
|
+
return this.canvasIds().map((id2) => this.canvas(id2)).filter((canvas) => canvas !== null);
|
|
1062
|
+
}
|
|
1063
|
+
/** Every frame the server can render. */
|
|
1064
|
+
sources() {
|
|
1065
|
+
return [...this.system()?.specimens ?? [], ...this.canvases().flatMap((canvas) => canvas.screens)];
|
|
1066
|
+
}
|
|
1067
|
+
source(canvasId, id2) {
|
|
1068
|
+
const list = canvasId === SYSTEM_CANVAS ? this.system()?.specimens ?? [] : this.canvas(canvasId)?.screens ?? [];
|
|
1069
|
+
return list.find((source) => source.id === id2) ?? null;
|
|
1070
|
+
}
|
|
1071
|
+
info(isStatic) {
|
|
1072
|
+
const system = this.system();
|
|
1073
|
+
return {
|
|
1074
|
+
name: this.name(),
|
|
1075
|
+
root: isStatic ? "" : this.paths.root,
|
|
1076
|
+
static: isStatic,
|
|
1077
|
+
version: packageVersion(),
|
|
1078
|
+
system: system ? summarizeSystem(system) : null,
|
|
1079
|
+
canvases: this.canvases().map(summarize),
|
|
1080
|
+
issues: this.config().issues
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/** Every problem in the project, for `design check`. */
|
|
1084
|
+
issues() {
|
|
1085
|
+
return [
|
|
1086
|
+
...this.config().issues,
|
|
1087
|
+
...this.system()?.doc.issues ?? [],
|
|
1088
|
+
...this.canvases().flatMap((canvas) => canvas.doc.issues)
|
|
1089
|
+
];
|
|
1090
|
+
}
|
|
1091
|
+
invalidateConfig() {
|
|
1092
|
+
this.configCache = null;
|
|
1093
|
+
}
|
|
1094
|
+
invalidateSystem() {
|
|
1095
|
+
this.systemCache = void 0;
|
|
1096
|
+
}
|
|
1097
|
+
invalidateCanvas(id2) {
|
|
1098
|
+
if (id2) this.canvasCache.delete(id2);
|
|
1099
|
+
else this.canvasCache.clear();
|
|
1100
|
+
}
|
|
1101
|
+
invalidateAll() {
|
|
1102
|
+
this.invalidateConfig();
|
|
1103
|
+
this.invalidateSystem();
|
|
1104
|
+
this.invalidateCanvas();
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// src/core/sources.ts
|
|
1109
|
+
var encodePath = (rel) => rel.split("/").map(encodeURIComponent).join("/");
|
|
1110
|
+
var DEV_URLS = {
|
|
1111
|
+
screen: (canvas, id2) => `/_s/${encodeURIComponent(canvas)}/${encodeURIComponent(id2)}`,
|
|
1112
|
+
html: (rel, canvas, id2) => `/_fs/${encodePath(rel)}?__design=${encodeURIComponent(`${canvas}/${id2}`)}`,
|
|
1113
|
+
file: (rel) => `/_fs/${encodePath(rel)}`
|
|
1114
|
+
};
|
|
1115
|
+
var STATIC_URLS = {
|
|
1116
|
+
screen: (canvas, id2) => `_s/${encodeURIComponent(canvas)}/${encodeURIComponent(id2)}/`,
|
|
1117
|
+
html: (_rel, canvas, id2) => `_s/${encodeURIComponent(canvas)}/${encodeURIComponent(id2)}/`,
|
|
1118
|
+
file: (rel) => `_f/${encodePath(rel)}`
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
// src/server/entries.ts
|
|
1122
|
+
import fs6 from "fs";
|
|
1123
|
+
import path7 from "path";
|
|
1124
|
+
var SYSTEM_CSS = "system.css";
|
|
1125
|
+
var HTML_BOOT = "html-boot.js";
|
|
1126
|
+
function writeIfChanged(file, content) {
|
|
1127
|
+
try {
|
|
1128
|
+
if (fs6.readFileSync(file, "utf8") === content) return false;
|
|
1129
|
+
} catch {
|
|
1130
|
+
}
|
|
1131
|
+
fs6.mkdirSync(path7.dirname(file), { recursive: true });
|
|
1132
|
+
fs6.writeFileSync(file, content);
|
|
1133
|
+
return true;
|
|
1134
|
+
}
|
|
1135
|
+
function importPath(fromDir, file) {
|
|
1136
|
+
const rel = toPosix(path7.relative(fromDir, file));
|
|
1137
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1138
|
+
}
|
|
1139
|
+
var css = (file) => JSON.stringify(toPosix(file));
|
|
1140
|
+
var Entries = class {
|
|
1141
|
+
constructor(project) {
|
|
1142
|
+
this.project = project;
|
|
1143
|
+
}
|
|
1144
|
+
project;
|
|
1145
|
+
get dir() {
|
|
1146
|
+
return this.project.paths.entries;
|
|
1147
|
+
}
|
|
1148
|
+
systemCssFile() {
|
|
1149
|
+
return path7.join(this.dir, SYSTEM_CSS);
|
|
1150
|
+
}
|
|
1151
|
+
entryFile(source) {
|
|
1152
|
+
return path7.join(this.dir, source.canvas, `${source.id}.js`);
|
|
1153
|
+
}
|
|
1154
|
+
/** Tailwind with the design tokens, or the stylesheet system.json names; scans every file in `.design`. */
|
|
1155
|
+
systemCss() {
|
|
1156
|
+
const { paths } = this.project;
|
|
1157
|
+
const system = this.project.system();
|
|
1158
|
+
const { config } = this.project.config();
|
|
1159
|
+
const lines = ["/* Generated by foss-design. Edit .design/system/tokens.css instead. */"];
|
|
1160
|
+
if (system?.stylesheet) {
|
|
1161
|
+
lines.push(`@import ${css(system.stylesheet)};`);
|
|
1162
|
+
} else {
|
|
1163
|
+
const tailwind = resolveFrom(PKG.manifest, "tailwindcss/index.css");
|
|
1164
|
+
if (!tailwind) throw new Error("tailwindcss is missing from foss-design; reinstall the package");
|
|
1165
|
+
lines.push(`@import ${css(tailwind)} source(none);`, `@import ${css(PKG.baseCss)};`);
|
|
1166
|
+
if (system?.tokensFile) lines.push(`@import ${css(system.tokensFile)};`);
|
|
1167
|
+
}
|
|
1168
|
+
lines.push(`@source ${css(paths.design)};`, `@source not ${css(paths.cache)};`);
|
|
1169
|
+
lines.push(`@source not ${css(path7.join(paths.design, "node_modules"))};`);
|
|
1170
|
+
for (const extra of config.sources ?? []) lines.push(`@source ${css(path7.resolve(paths.root, extra))};`);
|
|
1171
|
+
return `${lines.join("\n")}
|
|
1172
|
+
`;
|
|
1173
|
+
}
|
|
1174
|
+
entrySource(source) {
|
|
1175
|
+
const file = this.entryFile(source);
|
|
1176
|
+
const dir = path7.dirname(file);
|
|
1177
|
+
const lines = [];
|
|
1178
|
+
if (source.system) lines.push(`import ${JSON.stringify(importPath(dir, this.systemCssFile()))}`);
|
|
1179
|
+
if (source.builtin === "typography") {
|
|
1180
|
+
lines.push(
|
|
1181
|
+
`import { hmr, mount, TypographySpecimen } from '@design/runtime'`,
|
|
1182
|
+
"hmr(import.meta.hot)",
|
|
1183
|
+
"mount(TypographySpecimen)"
|
|
1184
|
+
);
|
|
1185
|
+
} else {
|
|
1186
|
+
lines.push(
|
|
1187
|
+
`import { hmr, mount } from '@design/runtime'`,
|
|
1188
|
+
`import * as screen from ${JSON.stringify(importPath(dir, source.file))}`,
|
|
1189
|
+
"",
|
|
1190
|
+
`const Screen = screen.default ?? Object.values(screen).find((value) => typeof value === 'function')`,
|
|
1191
|
+
"if (!Screen) throw new Error(" + JSON.stringify(`${path7.basename(source.file)} has no component to render: export one as default`) + ")",
|
|
1192
|
+
"hmr(import.meta.hot)",
|
|
1193
|
+
"mount(Screen)"
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
return `// Generated by foss-design for ${source.key}.
|
|
1197
|
+
${lines.join("\n")}
|
|
1198
|
+
`;
|
|
1199
|
+
}
|
|
1200
|
+
/** Write every entry the project needs and drop the ones it no longer does. */
|
|
1201
|
+
sync() {
|
|
1202
|
+
writeIfChanged(this.systemCssFile(), this.systemCss());
|
|
1203
|
+
writeIfChanged(
|
|
1204
|
+
path7.join(this.dir, HTML_BOOT),
|
|
1205
|
+
`// Generated by foss-design.
|
|
1206
|
+
import { boot, hmr } from '@design/runtime'
|
|
1207
|
+
boot()
|
|
1208
|
+
hmr(import.meta.hot)
|
|
1209
|
+
`
|
|
1210
|
+
);
|
|
1211
|
+
const wanted = /* @__PURE__ */ new Set([this.systemCssFile(), path7.join(this.dir, HTML_BOOT)]);
|
|
1212
|
+
for (const source of this.project.sources()) {
|
|
1213
|
+
if (source.format !== "module") continue;
|
|
1214
|
+
const file = this.entryFile(source);
|
|
1215
|
+
wanted.add(file);
|
|
1216
|
+
writeIfChanged(file, this.entrySource(source));
|
|
1217
|
+
}
|
|
1218
|
+
this.prune(this.dir, wanted);
|
|
1219
|
+
}
|
|
1220
|
+
/** Entry files, for dependency pre-bundling. */
|
|
1221
|
+
files() {
|
|
1222
|
+
return this.project.sources().filter((source) => source.format === "module").map((source) => this.entryFile(source));
|
|
1223
|
+
}
|
|
1224
|
+
prune(dir, wanted) {
|
|
1225
|
+
if (!fs6.existsSync(dir)) return;
|
|
1226
|
+
for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
|
|
1227
|
+
const full = path7.join(dir, entry.name);
|
|
1228
|
+
if (entry.isDirectory()) {
|
|
1229
|
+
this.prune(full, wanted);
|
|
1230
|
+
if (fs6.readdirSync(full).length === 0) fs6.rmdirSync(full);
|
|
1231
|
+
} else if (!wanted.has(full)) fs6.rmSync(full);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
// src/server/html.ts
|
|
1237
|
+
function headTags(head) {
|
|
1238
|
+
const { source } = head;
|
|
1239
|
+
const config = {
|
|
1240
|
+
key: source.key,
|
|
1241
|
+
canvas: source.canvas,
|
|
1242
|
+
id: source.id,
|
|
1243
|
+
title: source.title,
|
|
1244
|
+
props: source.props,
|
|
1245
|
+
theme: source.theme,
|
|
1246
|
+
snapshots: head.snapshots,
|
|
1247
|
+
autoHeight: head.autoHeight
|
|
1248
|
+
};
|
|
1249
|
+
const fallback = source.theme ?? head.theme;
|
|
1250
|
+
const theme2 = `(function(){var q=new URLSearchParams(location.search).get('theme');var t=${JSON.stringify(source.theme ?? null)}||(q==='dark'||q==='light'?q:${JSON.stringify(fallback)});var r=document.documentElement;r.dataset.theme=t;r.classList.toggle('dark',t==='dark');r.style.colorScheme=t})()`;
|
|
1251
|
+
const tags = [`<script>window.__DESIGN__=${scriptJson(config)};${theme2}</script>`, '<link rel="icon" href="data:,">'];
|
|
1252
|
+
for (const href of head.fonts) tags.push(`<link rel="stylesheet" href="${escapeHtml(href)}">`);
|
|
1253
|
+
if (head.systemCss) tags.push(`<link rel="stylesheet" href="${escapeHtml(head.systemCss)}">`);
|
|
1254
|
+
if (head.boot) tags.push(`<script type="module" src="${escapeHtml(head.boot)}"></script>`);
|
|
1255
|
+
return tags.join("\n");
|
|
1256
|
+
}
|
|
1257
|
+
function moduleShell(head, entryUrl) {
|
|
1258
|
+
return `<!doctype html>
|
|
1259
|
+
<html lang="en">
|
|
1260
|
+
<head>
|
|
1261
|
+
<meta charset="utf-8">
|
|
1262
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1263
|
+
<title>${escapeHtml(head.source.title)}</title>
|
|
1264
|
+
${headTags(head)}
|
|
1265
|
+
</head>
|
|
1266
|
+
<body>
|
|
1267
|
+
<div id="root"></div>
|
|
1268
|
+
<script type="module" src="${escapeHtml(entryUrl)}"></script>
|
|
1269
|
+
</body>
|
|
1270
|
+
</html>
|
|
1271
|
+
`;
|
|
1272
|
+
}
|
|
1273
|
+
function optsOutOfSystem(html) {
|
|
1274
|
+
return /<meta\s+name=["']design:system["']\s+content=["']off["']/i.test(html);
|
|
1275
|
+
}
|
|
1276
|
+
function injectIntoHtml(html, head) {
|
|
1277
|
+
const tags = headTags(head);
|
|
1278
|
+
const open = /<head(\s[^>]*)?>/i.exec(html);
|
|
1279
|
+
if (open) return `${html.slice(0, open.index + open[0].length)}
|
|
1280
|
+
${tags}
|
|
1281
|
+
${html.slice(open.index + open[0].length)}`;
|
|
1282
|
+
const htmlOpen = /<html(\s[^>]*)?>/i.exec(html);
|
|
1283
|
+
if (htmlOpen)
|
|
1284
|
+
return html.slice(0, htmlOpen.index + htmlOpen[0].length) + `
|
|
1285
|
+
<head>
|
|
1286
|
+
${tags}
|
|
1287
|
+
</head>
|
|
1288
|
+
` + html.slice(htmlOpen.index + htmlOpen[0].length);
|
|
1289
|
+
return `<!doctype html>
|
|
1290
|
+
<html><head>
|
|
1291
|
+
${tags}
|
|
1292
|
+
</head><body>
|
|
1293
|
+
${html}
|
|
1294
|
+
</body></html>`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/server/links.ts
|
|
1298
|
+
import fs7 from "fs";
|
|
1299
|
+
import path8 from "path";
|
|
1300
|
+
var SHIPPED_PACKAGES = ["react", "react-dom", "motion", "lucide-react", "clsx", "tailwind-merge"];
|
|
1301
|
+
function findPackageDir(fromDir, name, skip) {
|
|
1302
|
+
let dir = path8.resolve(fromDir);
|
|
1303
|
+
for (; ; ) {
|
|
1304
|
+
const modules = path8.join(dir, "node_modules");
|
|
1305
|
+
const candidate = path8.join(modules, name);
|
|
1306
|
+
if (modules !== skip && fs7.existsSync(path8.join(candidate, "package.json"))) return candidate;
|
|
1307
|
+
const parent = path8.dirname(dir);
|
|
1308
|
+
if (parent === dir) return null;
|
|
1309
|
+
dir = parent;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function isLink(file) {
|
|
1313
|
+
try {
|
|
1314
|
+
return fs7.lstatSync(file).isSymbolicLink();
|
|
1315
|
+
} catch {
|
|
1316
|
+
return false;
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
function linkShippedPackages(paths) {
|
|
1320
|
+
const modules = path8.join(paths.design, "node_modules");
|
|
1321
|
+
const linked = [];
|
|
1322
|
+
const allow = /* @__PURE__ */ new Set();
|
|
1323
|
+
for (const name of SHIPPED_PACKAGES) {
|
|
1324
|
+
const link = path8.join(modules, name);
|
|
1325
|
+
const placedByUser = fs7.existsSync(link) && !isLink(link);
|
|
1326
|
+
const own = placedByUser ? link : findPackageDir(paths.design, name, modules);
|
|
1327
|
+
if (own) {
|
|
1328
|
+
allow.add(path8.dirname(fs7.realpathSync(own)));
|
|
1329
|
+
if (isLink(link)) fs7.unlinkSync(link);
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const shipped = findPackageDir(PKG_ROOT, name);
|
|
1333
|
+
if (!shipped) continue;
|
|
1334
|
+
const target2 = fs7.realpathSync(shipped);
|
|
1335
|
+
allow.add(path8.dirname(target2));
|
|
1336
|
+
let current2 = null;
|
|
1337
|
+
try {
|
|
1338
|
+
current2 = fs7.realpathSync(link);
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
if (current2 !== target2) {
|
|
1342
|
+
if (isLink(link)) fs7.unlinkSync(link);
|
|
1343
|
+
fs7.mkdirSync(path8.dirname(link), { recursive: true });
|
|
1344
|
+
fs7.symlinkSync(target2, link, process.platform === "win32" ? "junction" : "dir");
|
|
1345
|
+
}
|
|
1346
|
+
linked.push(name);
|
|
1347
|
+
}
|
|
1348
|
+
const self = path8.join(modules, "foss-design");
|
|
1349
|
+
const target = fs7.realpathSync(PKG_ROOT);
|
|
1350
|
+
let current = null;
|
|
1351
|
+
try {
|
|
1352
|
+
current = fs7.realpathSync(self);
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
1355
|
+
if (current !== target) {
|
|
1356
|
+
if (isLink(self)) fs7.unlinkSync(self);
|
|
1357
|
+
if (!fs7.existsSync(self)) {
|
|
1358
|
+
fs7.mkdirSync(modules, { recursive: true });
|
|
1359
|
+
fs7.symlinkSync(target, self, process.platform === "win32" ? "junction" : "dir");
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
allow.add(target);
|
|
1363
|
+
return { linked, allow: [...allow] };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/server/vite.ts
|
|
1367
|
+
import path9 from "path";
|
|
1368
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
1369
|
+
import react from "@vitejs/plugin-react";
|
|
1370
|
+
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1371
|
+
function aliases(project) {
|
|
1372
|
+
const { paths } = project;
|
|
1373
|
+
const list = [
|
|
1374
|
+
// A bare import, so the dev server pre-bundles the runtime with React wherever the package is installed.
|
|
1375
|
+
{ find: /^@design\/runtime$/, replacement: "foss-design/runtime" },
|
|
1376
|
+
{ find: /^@system\//, replacement: `${toPosix(paths.system)}/` }
|
|
1377
|
+
];
|
|
1378
|
+
for (const [key, target] of Object.entries(project.config().config.alias ?? {})) {
|
|
1379
|
+
const replacement = toPosix(path9.resolve(paths.root, target));
|
|
1380
|
+
list.push(
|
|
1381
|
+
key.endsWith("/") ? { find: new RegExp(`^${escapeRegExp(key)}`), replacement: `${replacement.replace(/\/$/, "")}/` } : { find: new RegExp(`^${escapeRegExp(key)}(?=/|$)`), replacement }
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
return list;
|
|
1385
|
+
}
|
|
1386
|
+
function baseConfig(project) {
|
|
1387
|
+
const { paths } = project;
|
|
1388
|
+
return {
|
|
1389
|
+
configFile: false,
|
|
1390
|
+
envDir: false,
|
|
1391
|
+
root: paths.design,
|
|
1392
|
+
cacheDir: path9.join(paths.cache, "vite"),
|
|
1393
|
+
publicDir: false,
|
|
1394
|
+
logLevel: "warn",
|
|
1395
|
+
clearScreen: false,
|
|
1396
|
+
resolve: {
|
|
1397
|
+
alias: aliases(project),
|
|
1398
|
+
// One React for screens, the runtime and project components: resolved from `.design`.
|
|
1399
|
+
dedupe: ["react", "react-dom"]
|
|
1400
|
+
},
|
|
1401
|
+
plugins: [react(), tailwindcss()]
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// src/build/static.ts
|
|
1406
|
+
var THEMES = ["light", "dark"];
|
|
1407
|
+
var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".jsx", ".mts", ".cts", ".html", ".htm", ".md", ".map"]);
|
|
1408
|
+
var BUNDLED_REF = /<(script|link|img|source|video|audio|image|use|input)\b[^>]*>/gi;
|
|
1409
|
+
var encodePath2 = (rel) => rel.split("/").map(encodeURIComponent).join("/");
|
|
1410
|
+
function relUrl(fromDir, file) {
|
|
1411
|
+
const rel = toPosix(path10.relative(fromDir, file));
|
|
1412
|
+
return rel.startsWith(".") ? rel : `./${rel}`;
|
|
1413
|
+
}
|
|
1414
|
+
function staticSnapshots(paths) {
|
|
1415
|
+
const copies = /* @__PURE__ */ new Map();
|
|
1416
|
+
const metaCache = /* @__PURE__ */ new Map();
|
|
1417
|
+
const meta = (canvas) => {
|
|
1418
|
+
let data = metaCache.get(canvas);
|
|
1419
|
+
if (!data) {
|
|
1420
|
+
try {
|
|
1421
|
+
data = JSON.parse(fs8.readFileSync(path10.join(paths.snapshots, canvas, "meta.json"), "utf8"));
|
|
1422
|
+
} catch {
|
|
1423
|
+
}
|
|
1424
|
+
data ??= {};
|
|
1425
|
+
metaCache.set(canvas, data);
|
|
1426
|
+
}
|
|
1427
|
+
return data;
|
|
1428
|
+
};
|
|
1429
|
+
const lookup = (canvas, id2) => {
|
|
1430
|
+
const entry = meta(canvas)[id2];
|
|
1431
|
+
if (!entry) return null;
|
|
1432
|
+
const urls = {};
|
|
1433
|
+
for (const theme2 of THEMES) {
|
|
1434
|
+
const file = path10.join(paths.snapshots, canvas, `${id2}.${theme2}.png`);
|
|
1435
|
+
if (!entry[theme2] || !fs8.existsSync(file)) continue;
|
|
1436
|
+
const rel = `_snap/${canvas}/${id2}.${theme2}.png`;
|
|
1437
|
+
copies.set(file, rel);
|
|
1438
|
+
urls[theme2] = `${encodePath2(rel)}?v=${entry[theme2]}`;
|
|
1439
|
+
}
|
|
1440
|
+
return { urls, height: entry.height };
|
|
1441
|
+
};
|
|
1442
|
+
return { lookup, copies };
|
|
1443
|
+
}
|
|
1444
|
+
function rewriteRefs(html, originalDir, stagedDir) {
|
|
1445
|
+
return html.replace(BUNDLED_REF, (tag, name) => {
|
|
1446
|
+
const lower = name.toLowerCase();
|
|
1447
|
+
if (lower === "script" && !/\btype\s*=\s*["']?module/i.test(tag)) return tag;
|
|
1448
|
+
if (lower === "link" && !/\brel\s*=\s*["']?(?:stylesheet|icon|apple-touch-icon|modulepreload)/i.test(tag))
|
|
1449
|
+
return tag;
|
|
1450
|
+
return tag.replace(
|
|
1451
|
+
/(\s(?:src|href|poster)\s*=\s*)(["'])([^"']*)\2/gi,
|
|
1452
|
+
(whole, attr, quote, value) => {
|
|
1453
|
+
if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|#|\{)/i.test(value)) return whole;
|
|
1454
|
+
const [pathname = "", suffix = ""] = /^([^?#]*)(.*)$/.exec(value).slice(1);
|
|
1455
|
+
const target = path10.resolve(originalDir, decodeURIComponent(pathname));
|
|
1456
|
+
if (!fs8.existsSync(target)) return whole;
|
|
1457
|
+
return `${attr}${quote}${relUrl(stagedDir, target)}${suffix}${quote}`;
|
|
1458
|
+
}
|
|
1459
|
+
);
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
function copyTree(from, to, filter) {
|
|
1463
|
+
if (!fs8.existsSync(from)) return;
|
|
1464
|
+
for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
|
|
1465
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1466
|
+
const source = path10.join(from, entry.name);
|
|
1467
|
+
const target = path10.join(to, entry.name);
|
|
1468
|
+
if (entry.isDirectory()) copyTree(source, target, filter);
|
|
1469
|
+
else if (filter(source) && !fs8.existsSync(target)) {
|
|
1470
|
+
fs8.mkdirSync(path10.dirname(target), { recursive: true });
|
|
1471
|
+
fs8.copyFileSync(source, target);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
function writeJson(file, value) {
|
|
1476
|
+
fs8.mkdirSync(path10.dirname(file), { recursive: true });
|
|
1477
|
+
fs8.writeFileSync(file, JSON.stringify(value));
|
|
1478
|
+
}
|
|
1479
|
+
async function buildSite(paths, only, out) {
|
|
1480
|
+
const snapshots = staticSnapshots(paths);
|
|
1481
|
+
const project = new DesignProject(paths, STATIC_URLS, snapshots.lookup);
|
|
1482
|
+
const known = project.canvasIds();
|
|
1483
|
+
const unknown = only.filter((id2) => !known.includes(id2));
|
|
1484
|
+
if (unknown.length) throw new Error(`No canvas ${unknown.map((id2) => `"${id2}"`).join(", ")}`);
|
|
1485
|
+
const canvasIds = only.length ? only : known;
|
|
1486
|
+
const canvases = canvasIds.map((id2) => project.canvas(id2)).filter((c) => c !== null);
|
|
1487
|
+
const system = project.system();
|
|
1488
|
+
const entries = new Entries(project);
|
|
1489
|
+
entries.sync();
|
|
1490
|
+
linkShippedPackages(paths);
|
|
1491
|
+
const staging = path10.join(paths.cache, "build", "src");
|
|
1492
|
+
fs8.rmSync(staging, { recursive: true, force: true });
|
|
1493
|
+
const fonts = system?.doc.fonts ?? [];
|
|
1494
|
+
const sources = [...system?.specimens ?? [], ...canvases.flatMap((canvas) => canvas.screens)];
|
|
1495
|
+
const inputs = [];
|
|
1496
|
+
const htmlScreens = [];
|
|
1497
|
+
for (const source of sources) {
|
|
1498
|
+
if (!source.builtin && !fs8.existsSync(source.file)) continue;
|
|
1499
|
+
const siteDir = path10.join("_s", source.canvas, source.id);
|
|
1500
|
+
const dir = path10.join(staging, siteDir);
|
|
1501
|
+
const file = path10.join(dir, "index.html");
|
|
1502
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1503
|
+
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
1504
|
+
const head = {
|
|
1505
|
+
source,
|
|
1506
|
+
theme: canvasTheme ?? "light",
|
|
1507
|
+
fonts,
|
|
1508
|
+
snapshots: false,
|
|
1509
|
+
autoHeight: source.autoHeight ?? false
|
|
1510
|
+
};
|
|
1511
|
+
if (source.format === "module") {
|
|
1512
|
+
fs8.writeFileSync(file, moduleShell(head, relUrl(dir, entries.entryFile(source))));
|
|
1513
|
+
} else {
|
|
1514
|
+
const raw = rewriteRefs(fs8.readFileSync(source.file, "utf8"), path10.dirname(source.file), dir);
|
|
1515
|
+
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
1516
|
+
const html = injectIntoHtml(raw, {
|
|
1517
|
+
...head,
|
|
1518
|
+
systemCss: withSystem ? relUrl(dir, entries.systemCssFile()) : void 0,
|
|
1519
|
+
boot: relUrl(dir, path10.join(entries.dir, HTML_BOOT))
|
|
1520
|
+
});
|
|
1521
|
+
fs8.writeFileSync(file, html);
|
|
1522
|
+
htmlScreens.push({ source, dir: siteDir });
|
|
1523
|
+
}
|
|
1524
|
+
inputs.push(file);
|
|
1525
|
+
}
|
|
1526
|
+
fs8.mkdirSync(out, { recursive: true });
|
|
1527
|
+
if (inputs.length) {
|
|
1528
|
+
const base = baseConfig(project);
|
|
1529
|
+
await build({
|
|
1530
|
+
...base,
|
|
1531
|
+
root: staging,
|
|
1532
|
+
base: "./",
|
|
1533
|
+
logLevel: "warn",
|
|
1534
|
+
build: {
|
|
1535
|
+
outDir: out,
|
|
1536
|
+
emptyOutDir: true,
|
|
1537
|
+
assetsDir: "_assets",
|
|
1538
|
+
target: "es2022",
|
|
1539
|
+
reportCompressedSize: false,
|
|
1540
|
+
chunkSizeWarningLimit: 8e3,
|
|
1541
|
+
rolldownOptions: { input: inputs }
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
} else {
|
|
1545
|
+
fs8.rmSync(out, { recursive: true, force: true });
|
|
1546
|
+
fs8.mkdirSync(out, { recursive: true });
|
|
1547
|
+
}
|
|
1548
|
+
for (const { source, dir } of htmlScreens) {
|
|
1549
|
+
copyTree(
|
|
1550
|
+
path10.dirname(source.file),
|
|
1551
|
+
path10.join(out, dir),
|
|
1552
|
+
(file) => !SOURCE_EXT.has(path10.extname(file).toLowerCase())
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
const files = /* @__PURE__ */ new Set();
|
|
1556
|
+
for (const canvas of canvases) {
|
|
1557
|
+
for (const page of canvas.doc.pages)
|
|
1558
|
+
for (const section of page.sections)
|
|
1559
|
+
for (const item of section.items) if (item.kind === "image" && item.url.startsWith("_f/")) files.add(item.url);
|
|
1560
|
+
}
|
|
1561
|
+
for (const asset of system?.doc.assets ?? []) files.add(asset.url);
|
|
1562
|
+
for (const url of files) {
|
|
1563
|
+
const rel = url.slice("_f/".length).split("/").map(decodeURIComponent).join("/");
|
|
1564
|
+
const from = path10.join(paths.design, rel);
|
|
1565
|
+
if (!fs8.existsSync(from)) continue;
|
|
1566
|
+
const to = path10.join(out, "_f", rel);
|
|
1567
|
+
fs8.mkdirSync(path10.dirname(to), { recursive: true });
|
|
1568
|
+
fs8.copyFileSync(from, to);
|
|
1569
|
+
}
|
|
1570
|
+
for (const [from, rel] of snapshots.copies) {
|
|
1571
|
+
const to = path10.join(out, rel);
|
|
1572
|
+
fs8.mkdirSync(path10.dirname(to), { recursive: true });
|
|
1573
|
+
fs8.copyFileSync(from, to);
|
|
1574
|
+
}
|
|
1575
|
+
const info = project.info(true);
|
|
1576
|
+
info.canvases = info.canvases.filter((canvas) => canvasIds.includes(canvas.id));
|
|
1577
|
+
writeJson(path10.join(out, "api", "project.json"), info);
|
|
1578
|
+
for (const canvas of canvases) writeJson(path10.join(out, "api", "canvas", `${canvas.doc.id}.json`), canvas.doc);
|
|
1579
|
+
if (system) {
|
|
1580
|
+
writeJson(path10.join(out, "api", "system.json"), system.doc);
|
|
1581
|
+
const texts = {};
|
|
1582
|
+
for (const component of system.doc.components) {
|
|
1583
|
+
for (const rel of [component.specimen, ...component.sources]) {
|
|
1584
|
+
const file = path10.join(paths.root, rel);
|
|
1585
|
+
if (!(rel in texts) && fs8.existsSync(file)) texts[rel] = fs8.readFileSync(file, "utf8");
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
writeJson(path10.join(out, "api", "sources.json"), texts);
|
|
1589
|
+
}
|
|
1590
|
+
fs8.rmSync(staging, { recursive: true, force: true });
|
|
1591
|
+
const errors = [
|
|
1592
|
+
...only.length ? [] : project.config().issues,
|
|
1593
|
+
...system?.doc.issues ?? [],
|
|
1594
|
+
...canvases.flatMap((canvas) => canvas.doc.issues)
|
|
1595
|
+
].filter((issue) => issue.severity === "error").length;
|
|
1596
|
+
return { out, canvases: canvasIds, frames: inputs.length, errors };
|
|
1597
|
+
}
|
|
1598
|
+
function copyViewer(viewerDir, out) {
|
|
1599
|
+
copyTree(viewerDir, out, () => true);
|
|
1600
|
+
const index = path10.join(out, "index.html");
|
|
1601
|
+
const html = fs8.readFileSync(index, "utf8");
|
|
1602
|
+
const flag = "<script>window.__DESIGN_STATIC__=true</script>";
|
|
1603
|
+
const head = /<head(\s[^>]*)?>/i.exec(html);
|
|
1604
|
+
fs8.writeFileSync(
|
|
1605
|
+
index,
|
|
1606
|
+
head ? html.slice(0, head.index + head[0].length) + flag + html.slice(head.index + head[0].length) : flag + html
|
|
1607
|
+
);
|
|
1608
|
+
return { absoluteAssets: /(?:src|href)=["']\/(?!\/)/.test(html) };
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/cli/log.ts
|
|
1612
|
+
var color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
1613
|
+
var wrap = (code) => (text) => color ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
1614
|
+
var bold = wrap("1");
|
|
1615
|
+
var dim = wrap("2");
|
|
1616
|
+
var red = wrap("31");
|
|
1617
|
+
var green = wrap("32");
|
|
1618
|
+
var yellow = wrap("33");
|
|
1619
|
+
var cyan = wrap("36");
|
|
1620
|
+
var CliError = class extends Error {
|
|
1621
|
+
constructor(message, code = 1) {
|
|
1622
|
+
super(message);
|
|
1623
|
+
this.code = code;
|
|
1624
|
+
}
|
|
1625
|
+
code;
|
|
1626
|
+
};
|
|
1627
|
+
for (const stream of [process.stdout, process.stderr]) {
|
|
1628
|
+
stream.on("error", (error) => {
|
|
1629
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
1630
|
+
throw error;
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
function print(line = "") {
|
|
1634
|
+
process.stdout.write(`${line}
|
|
1635
|
+
`);
|
|
1636
|
+
}
|
|
1637
|
+
function warn(line) {
|
|
1638
|
+
process.stderr.write(`${yellow("warning")} ${line}
|
|
1639
|
+
`);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/cli/commands/build.ts
|
|
1643
|
+
function pack(out) {
|
|
1644
|
+
const archive = `${out.replace(/[\\/]+$/, "")}.tar.gz`;
|
|
1645
|
+
fs9.rmSync(archive, { force: true });
|
|
1646
|
+
const entries = fs9.readdirSync(out);
|
|
1647
|
+
const result = spawnSync("tar", ["-czf", archive, "-C", out, ...entries], {
|
|
1648
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1649
|
+
env: { ...process.env, COPYFILE_DISABLE: "1" }
|
|
1650
|
+
});
|
|
1651
|
+
if (result.status !== 0)
|
|
1652
|
+
throw new CliError(`tar failed: ${result.stderr?.toString().trim() || result.error?.message}`);
|
|
1653
|
+
return archive;
|
|
1654
|
+
}
|
|
1655
|
+
async function runBuild(paths, canvases, options) {
|
|
1656
|
+
if (!fs9.existsSync(path11.join(PKG.viewer, "index.html"))) {
|
|
1657
|
+
throw new CliError("The viewer is missing from this foss-design install (dist/viewer). Rebuild or reinstall it.");
|
|
1658
|
+
}
|
|
1659
|
+
const out = path11.resolve(options.out ?? path11.join(paths.cache, "site"));
|
|
1660
|
+
if (out === paths.root || out === paths.design) throw new CliError(`Refusing to build into ${out}`);
|
|
1661
|
+
print(dim(`Building ${canvases.length ? canvases.join(", ") : "every canvas"} and the design system\u2026`));
|
|
1662
|
+
let result;
|
|
1663
|
+
try {
|
|
1664
|
+
result = await buildSite(paths, canvases, out);
|
|
1665
|
+
} catch (error) {
|
|
1666
|
+
throw new CliError(error.message);
|
|
1667
|
+
}
|
|
1668
|
+
const { absoluteAssets } = copyViewer(PKG.viewer, out);
|
|
1669
|
+
if (absoluteAssets) warn("the viewer references assets by absolute path; the site only works at the root of a host");
|
|
1670
|
+
print(`${bold("Site")} ${out}`);
|
|
1671
|
+
print(
|
|
1672
|
+
`${bold("Frames")} ${result.frames} ${dim(`(${result.canvases.length} canvas${result.canvases.length === 1 ? "" : "es"})`)}`
|
|
1673
|
+
);
|
|
1674
|
+
if (options.tar) print(`${bold("Archive")} ${pack(out)} ${dim("(index.html at the root)")}`);
|
|
1675
|
+
if (result.errors)
|
|
1676
|
+
warn(`${result.errors} error${result.errors === 1 ? "" : "s"} in the built canvases \u2014 \`design check\` lists them`);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// src/capture/chrome.ts
|
|
1680
|
+
import fs10 from "fs";
|
|
1681
|
+
var CANDIDATES = {
|
|
1682
|
+
darwin: [
|
|
1683
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1684
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
1685
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
1686
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
|
|
1687
|
+
],
|
|
1688
|
+
linux: [
|
|
1689
|
+
"/usr/bin/google-chrome",
|
|
1690
|
+
"/usr/bin/google-chrome-stable",
|
|
1691
|
+
"/usr/bin/chromium",
|
|
1692
|
+
"/usr/bin/chromium-browser",
|
|
1693
|
+
"/snap/bin/chromium",
|
|
1694
|
+
"/usr/bin/microsoft-edge"
|
|
1695
|
+
],
|
|
1696
|
+
win32: [
|
|
1697
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
1698
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
1699
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
1700
|
+
]
|
|
1701
|
+
};
|
|
1702
|
+
function findChrome() {
|
|
1703
|
+
const configured = process.env.DESIGN_CHROME;
|
|
1704
|
+
if (configured && fs10.existsSync(configured)) return configured;
|
|
1705
|
+
for (const candidate of CANDIDATES[process.platform] ?? []) if (fs10.existsSync(candidate)) return candidate;
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
async function launchChrome() {
|
|
1709
|
+
const executablePath = findChrome();
|
|
1710
|
+
if (!executablePath) {
|
|
1711
|
+
throw new Error(
|
|
1712
|
+
"No Chrome, Chromium or Edge found. Install one, or point DESIGN_CHROME at a Chromium-based browser binary."
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
const { chromium } = await import("playwright-core");
|
|
1716
|
+
return chromium.launch({ executablePath, headless: true, args: ["--hide-scrollbars", "--force-color-profile=srgb"] });
|
|
1717
|
+
}
|
|
1718
|
+
async function openFrame(page, url, options = {}) {
|
|
1719
|
+
const errors = [];
|
|
1720
|
+
page.on("pageerror", (error) => errors.push(error.message));
|
|
1721
|
+
page.on("console", (message) => {
|
|
1722
|
+
if (message.type() === "error") errors.push(message.text().replace(/^(?:\s*%[osdifcO])+\s*/, ""));
|
|
1723
|
+
});
|
|
1724
|
+
await page.goto(url, { waitUntil: "load", timeout: options.timeoutMs ?? 3e4 });
|
|
1725
|
+
let ready = options.waitForReady === false;
|
|
1726
|
+
if (!ready) {
|
|
1727
|
+
try {
|
|
1728
|
+
await page.waitForFunction(() => window.__DESIGN_READY__ === true, null, {
|
|
1729
|
+
timeout: options.timeoutMs ?? 2e4
|
|
1730
|
+
});
|
|
1731
|
+
ready = true;
|
|
1732
|
+
} catch {
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
await page.waitForTimeout(options.settleMs ?? 700);
|
|
1736
|
+
const runtimeErrors = await page.evaluate(() => window.__DESIGN_ERRORS__ ?? []).catch(() => []);
|
|
1737
|
+
for (const error of runtimeErrors) if (!errors.includes(error)) errors.push(error);
|
|
1738
|
+
const overlay = await page.evaluate(() => {
|
|
1739
|
+
const el = document.querySelector("vite-error-overlay");
|
|
1740
|
+
return el?.shadowRoot?.querySelector(".message")?.textContent ?? null;
|
|
1741
|
+
}).catch(() => null);
|
|
1742
|
+
if (overlay) errors.unshift(overlay.trim());
|
|
1743
|
+
const unique = errors.filter(
|
|
1744
|
+
(error, index) => error && !errors.some((other, j) => j !== index && other && other.length < error.length && error.includes(other))
|
|
1745
|
+
);
|
|
1746
|
+
return { errors: [...new Set(unique)], ready };
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// src/capture/frames.ts
|
|
1750
|
+
var AUTO_VIEWPORT_HEIGHT = 900;
|
|
1751
|
+
function withTheme(url, theme2) {
|
|
1752
|
+
return `${url}${url.includes("?") ? "&" : "?"}theme=${theme2}`;
|
|
1753
|
+
}
|
|
1754
|
+
function absolute(base, url) {
|
|
1755
|
+
return /^https?:\/\//.test(url) ? url : `${base.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
1756
|
+
}
|
|
1757
|
+
function frameJob(base, canvas, page, item, theme2) {
|
|
1758
|
+
const auto = item.frame.height === "auto";
|
|
1759
|
+
const pinned = item.kind === "screen" ? item.theme : void 0;
|
|
1760
|
+
const effective = pinned ?? theme2;
|
|
1761
|
+
return {
|
|
1762
|
+
key: `${canvas}/${item.id}`,
|
|
1763
|
+
canvas,
|
|
1764
|
+
id: item.id,
|
|
1765
|
+
page,
|
|
1766
|
+
url: item.kind === "url" ? item.url : withTheme(absolute(base, item.url), effective),
|
|
1767
|
+
width: item.frame.width,
|
|
1768
|
+
height: auto ? AUTO_VIEWPORT_HEIGHT : item.frame.height,
|
|
1769
|
+
fullPage: auto,
|
|
1770
|
+
theme: effective,
|
|
1771
|
+
waitForReady: item.kind === "screen"
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function isFrameItem(item) {
|
|
1775
|
+
return item.kind === "screen" || item.kind === "url";
|
|
1776
|
+
}
|
|
1777
|
+
function canvasFrames(doc, base, theme2, filter = {}) {
|
|
1778
|
+
const jobs = [];
|
|
1779
|
+
for (const page of doc.pages) {
|
|
1780
|
+
if (filter.page && page.id !== filter.page) continue;
|
|
1781
|
+
for (const section of page.sections) {
|
|
1782
|
+
for (const item of section.items) {
|
|
1783
|
+
if (!isFrameItem(item)) continue;
|
|
1784
|
+
if (filter.id && item.id !== filter.id) continue;
|
|
1785
|
+
if (item.kind === "url" && filter.includeUrls === false) continue;
|
|
1786
|
+
if (item.kind === "screen" && item.missing && filter.skipMissing !== false) continue;
|
|
1787
|
+
jobs.push(frameJob(base, doc.id, page.id, item, theme2));
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
return jobs;
|
|
1792
|
+
}
|
|
1793
|
+
function systemFrames(doc, base, theme2) {
|
|
1794
|
+
const specimens = doc.components.map((component) => ({ id: component.id, url: component.url }));
|
|
1795
|
+
if (doc.typographyUrl) specimens.push({ id: "@typography", url: doc.typographyUrl });
|
|
1796
|
+
return specimens.map(({ id: id2, url }) => ({
|
|
1797
|
+
key: `@system/${id2}`,
|
|
1798
|
+
canvas: "@system",
|
|
1799
|
+
id: id2,
|
|
1800
|
+
url: withTheme(absolute(base, url), theme2),
|
|
1801
|
+
width: 1200,
|
|
1802
|
+
height: 800,
|
|
1803
|
+
fullPage: true,
|
|
1804
|
+
theme: theme2,
|
|
1805
|
+
waitForReady: true
|
|
1806
|
+
}));
|
|
1807
|
+
}
|
|
1808
|
+
async function mapLimit(items, limit, fn) {
|
|
1809
|
+
const results = new Array(items.length);
|
|
1810
|
+
let next = 0;
|
|
1811
|
+
const worker = async () => {
|
|
1812
|
+
while (next < items.length) {
|
|
1813
|
+
const index = next++;
|
|
1814
|
+
results[index] = await fn(items[index], index);
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
1818
|
+
return results;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
// src/server/state.ts
|
|
1822
|
+
import fs11 from "fs";
|
|
1823
|
+
import path12 from "path";
|
|
1824
|
+
var stateFile = (paths) => path12.join(paths.cache, "server.json");
|
|
1825
|
+
var logFile = (paths) => path12.join(paths.cache, "server.log");
|
|
1826
|
+
function readState(paths) {
|
|
1827
|
+
try {
|
|
1828
|
+
return JSON.parse(fs11.readFileSync(stateFile(paths), "utf8"));
|
|
1829
|
+
} catch {
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function writeState(paths, state) {
|
|
1834
|
+
fs11.mkdirSync(paths.cache, { recursive: true });
|
|
1835
|
+
fs11.writeFileSync(stateFile(paths), `${JSON.stringify(state, null, 2)}
|
|
1836
|
+
`);
|
|
1837
|
+
}
|
|
1838
|
+
function clearState(paths, pid) {
|
|
1839
|
+
const state = readState(paths);
|
|
1840
|
+
if (pid !== void 0 && state && state.pid !== pid) return;
|
|
1841
|
+
fs11.rmSync(stateFile(paths), { force: true });
|
|
1842
|
+
}
|
|
1843
|
+
function pidAlive(pid) {
|
|
1844
|
+
try {
|
|
1845
|
+
process.kill(pid, 0);
|
|
1846
|
+
return true;
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
return error.code === "EPERM";
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
async function liveServer(paths) {
|
|
1852
|
+
const state = readState(paths);
|
|
1853
|
+
if (!state || !pidAlive(state.pid)) return null;
|
|
1854
|
+
try {
|
|
1855
|
+
const response = await fetch(`http://127.0.0.1:${state.port}/api/health`, { signal: AbortSignal.timeout(1500) });
|
|
1856
|
+
if (!response.ok) return null;
|
|
1857
|
+
const health = await response.json();
|
|
1858
|
+
return health.root === paths.root ? state : null;
|
|
1859
|
+
} catch {
|
|
1860
|
+
return null;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// src/cli/commands/preview.ts
|
|
1865
|
+
import { spawn } from "child_process";
|
|
1866
|
+
import fs15 from "fs";
|
|
1867
|
+
|
|
1868
|
+
// src/server/index.ts
|
|
1869
|
+
import fs14 from "fs";
|
|
1870
|
+
import http from "http";
|
|
1871
|
+
import path15 from "path";
|
|
1872
|
+
import sirv from "sirv";
|
|
1873
|
+
import { createServer, searchForWorkspaceRoot } from "vite";
|
|
1874
|
+
|
|
1875
|
+
// src/server/api.ts
|
|
1876
|
+
import fs12 from "fs";
|
|
1877
|
+
import path13 from "path";
|
|
1878
|
+
function sendJson(res, status, body) {
|
|
1879
|
+
res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
1880
|
+
res.end(JSON.stringify(body));
|
|
1881
|
+
}
|
|
1882
|
+
function readBody(req, limit = 25 * 1024 * 1024) {
|
|
1883
|
+
return new Promise((resolve, reject) => {
|
|
1884
|
+
const chunks = [];
|
|
1885
|
+
let size2 = 0;
|
|
1886
|
+
req.on("data", (chunk) => {
|
|
1887
|
+
size2 += chunk.length;
|
|
1888
|
+
if (size2 > limit) {
|
|
1889
|
+
reject(new Error("body too large"));
|
|
1890
|
+
req.destroy();
|
|
1891
|
+
} else chunks.push(chunk);
|
|
1892
|
+
});
|
|
1893
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1894
|
+
req.on("error", reject);
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
async function handleApi(ctx, req, res, url) {
|
|
1898
|
+
const { project, events, snapshots } = ctx;
|
|
1899
|
+
const route = url.pathname.slice("/api".length);
|
|
1900
|
+
const method = req.method ?? "GET";
|
|
1901
|
+
if (route === "/health") {
|
|
1902
|
+
sendJson(res, 200, { ok: true, root: project.paths.root, version: packageVersion(), pid: process.pid });
|
|
1903
|
+
return true;
|
|
1904
|
+
}
|
|
1905
|
+
if (route === "/events") {
|
|
1906
|
+
events.attach(req, res);
|
|
1907
|
+
return true;
|
|
1908
|
+
}
|
|
1909
|
+
if (route === "/project") {
|
|
1910
|
+
sendJson(res, 200, project.info(false));
|
|
1911
|
+
return true;
|
|
1912
|
+
}
|
|
1913
|
+
if (route === "/system") {
|
|
1914
|
+
const system = project.system();
|
|
1915
|
+
if (!system) sendJson(res, 404, { error: "This project has no design system yet (.design/system)." });
|
|
1916
|
+
else sendJson(res, 200, system.doc);
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1919
|
+
const canvasMatch = /^\/canvas\/([^/]+)$/.exec(route);
|
|
1920
|
+
if (canvasMatch) {
|
|
1921
|
+
const canvas = project.canvas(decodeURIComponent(canvasMatch[1]));
|
|
1922
|
+
if (!canvas) sendJson(res, 404, { error: `No canvas "${decodeURIComponent(canvasMatch[1])}"` });
|
|
1923
|
+
else sendJson(res, 200, canvas.doc);
|
|
1924
|
+
return true;
|
|
1925
|
+
}
|
|
1926
|
+
if (route === "/source") {
|
|
1927
|
+
const rel = url.searchParams.get("path") ?? "";
|
|
1928
|
+
const file = path13.resolve(project.paths.root, rel);
|
|
1929
|
+
if (!rel || !isInside(project.paths.root, file) || !fs12.existsSync(file) || fs12.statSync(file).isDirectory()) {
|
|
1930
|
+
sendJson(res, 404, { error: "not found" });
|
|
1931
|
+
return true;
|
|
1932
|
+
}
|
|
1933
|
+
if (fs12.statSync(file).size > 2 * 1024 * 1024) {
|
|
1934
|
+
sendJson(res, 413, { error: "file too large to show" });
|
|
1935
|
+
return true;
|
|
1936
|
+
}
|
|
1937
|
+
res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
1938
|
+
res.end(fs12.readFileSync(file, "utf8"));
|
|
1939
|
+
return true;
|
|
1940
|
+
}
|
|
1941
|
+
const snapshotPost = /^\/snapshots\/([^/]+)\/([^/]+)$/.exec(route);
|
|
1942
|
+
if (snapshotPost && method === "POST") {
|
|
1943
|
+
const canvas = decodeURIComponent(snapshotPost[1]);
|
|
1944
|
+
const id2 = decodeURIComponent(snapshotPost[2]);
|
|
1945
|
+
const theme2 = url.searchParams.get("theme") === "dark" ? "dark" : "light";
|
|
1946
|
+
const heightParam = Number(url.searchParams.get("height"));
|
|
1947
|
+
const height = Number.isFinite(heightParam) && heightParam > 0 ? Math.round(heightParam) : void 0;
|
|
1948
|
+
try {
|
|
1949
|
+
const body = await readBody(req);
|
|
1950
|
+
if (body.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a") throw new Error("not a PNG");
|
|
1951
|
+
const { url: snapshotUrl } = snapshots.save(canvas, id2, theme2, body, height);
|
|
1952
|
+
project.invalidateCanvas(canvas);
|
|
1953
|
+
events.send({ type: "snapshot", canvas, id: id2, theme: theme2, url: snapshotUrl, height });
|
|
1954
|
+
sendJson(res, 200, { url: snapshotUrl });
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
sendJson(res, 400, { error: error.message });
|
|
1957
|
+
}
|
|
1958
|
+
return true;
|
|
1959
|
+
}
|
|
1960
|
+
if (snapshotPost && method === "GET") {
|
|
1961
|
+
const file = snapshots.file(decodeURIComponent(snapshotPost[1]), decodeURIComponent(snapshotPost[2]));
|
|
1962
|
+
if (!file) {
|
|
1963
|
+
res.writeHead(404);
|
|
1964
|
+
res.end();
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=31536000, immutable" });
|
|
1968
|
+
fs12.createReadStream(file).pipe(res);
|
|
1969
|
+
return true;
|
|
1970
|
+
}
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
// src/server/events.ts
|
|
1975
|
+
var EventHub = class {
|
|
1976
|
+
clients = /* @__PURE__ */ new Set();
|
|
1977
|
+
timer;
|
|
1978
|
+
constructor() {
|
|
1979
|
+
this.timer = setInterval(() => {
|
|
1980
|
+
for (const client of this.clients) client.write(": ping\n\n");
|
|
1981
|
+
}, 2e4);
|
|
1982
|
+
this.timer.unref();
|
|
1983
|
+
}
|
|
1984
|
+
attach(req, res) {
|
|
1985
|
+
res.writeHead(200, {
|
|
1986
|
+
"content-type": "text/event-stream",
|
|
1987
|
+
"cache-control": "no-cache, no-transform",
|
|
1988
|
+
connection: "keep-alive"
|
|
1989
|
+
});
|
|
1990
|
+
res.write(": connected\n\n");
|
|
1991
|
+
this.clients.add(res);
|
|
1992
|
+
req.on("close", () => this.clients.delete(res));
|
|
1993
|
+
}
|
|
1994
|
+
send(event) {
|
|
1995
|
+
const data = `data: ${JSON.stringify(event)}
|
|
1996
|
+
|
|
1997
|
+
`;
|
|
1998
|
+
for (const client of this.clients) client.write(data);
|
|
1999
|
+
}
|
|
2000
|
+
close() {
|
|
2001
|
+
clearInterval(this.timer);
|
|
2002
|
+
for (const client of this.clients) client.end();
|
|
2003
|
+
this.clients.clear();
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
// src/server/snapshots.ts
|
|
2008
|
+
import fs13 from "fs";
|
|
2009
|
+
import path14 from "path";
|
|
2010
|
+
var SAFE = /^@?[a-z0-9][a-z0-9_-]*$/i;
|
|
2011
|
+
function isSafeSegment(value) {
|
|
2012
|
+
return SAFE.test(value);
|
|
2013
|
+
}
|
|
2014
|
+
var SnapshotStore = class {
|
|
2015
|
+
constructor(dir, urlBase = "/api/snapshots") {
|
|
2016
|
+
this.dir = dir;
|
|
2017
|
+
this.urlBase = urlBase;
|
|
2018
|
+
}
|
|
2019
|
+
dir;
|
|
2020
|
+
urlBase;
|
|
2021
|
+
meta = /* @__PURE__ */ new Map();
|
|
2022
|
+
metaFile(canvas) {
|
|
2023
|
+
return path14.join(this.dir, canvas, "meta.json");
|
|
2024
|
+
}
|
|
2025
|
+
load(canvas) {
|
|
2026
|
+
const cached = this.meta.get(canvas);
|
|
2027
|
+
if (cached) return cached;
|
|
2028
|
+
let data = {};
|
|
2029
|
+
try {
|
|
2030
|
+
data = JSON.parse(fs13.readFileSync(this.metaFile(canvas), "utf8"));
|
|
2031
|
+
} catch {
|
|
2032
|
+
}
|
|
2033
|
+
this.meta.set(canvas, data);
|
|
2034
|
+
return data;
|
|
2035
|
+
}
|
|
2036
|
+
url(canvas, id2, theme2, version) {
|
|
2037
|
+
return `${this.urlBase}/${encodeURIComponent(canvas)}/${encodeURIComponent(id2)}.${theme2}.png?v=${version}`;
|
|
2038
|
+
}
|
|
2039
|
+
lookup = (canvas, id2) => {
|
|
2040
|
+
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) return null;
|
|
2041
|
+
const entry = this.load(canvas)[id2];
|
|
2042
|
+
if (!entry) return null;
|
|
2043
|
+
const urls = {};
|
|
2044
|
+
if (entry.light) urls.light = this.url(canvas, id2, "light", entry.light);
|
|
2045
|
+
if (entry.dark) urls.dark = this.url(canvas, id2, "dark", entry.dark);
|
|
2046
|
+
return { urls, height: entry.height };
|
|
2047
|
+
};
|
|
2048
|
+
save(canvas, id2, theme2, png, height) {
|
|
2049
|
+
if (!isSafeSegment(canvas) || !isSafeSegment(id2)) throw new Error("bad snapshot key");
|
|
2050
|
+
const dir = path14.join(this.dir, canvas);
|
|
2051
|
+
fs13.mkdirSync(dir, { recursive: true });
|
|
2052
|
+
fs13.writeFileSync(path14.join(dir, `${id2}.${theme2}.png`), png);
|
|
2053
|
+
const data = this.load(canvas);
|
|
2054
|
+
const version = Date.now();
|
|
2055
|
+
data[id2] = { ...data[id2], [theme2]: version, ...height ? { height } : {} };
|
|
2056
|
+
fs13.writeFileSync(this.metaFile(canvas), JSON.stringify(data));
|
|
2057
|
+
return { url: this.url(canvas, id2, theme2, version) };
|
|
2058
|
+
}
|
|
2059
|
+
/** Absolute path of `<id>.<theme>.png`, or null. */
|
|
2060
|
+
file(canvas, name) {
|
|
2061
|
+
const match = /^(@?[a-z0-9][a-z0-9_-]*)\.(light|dark)\.png$/i.exec(name);
|
|
2062
|
+
if (!isSafeSegment(canvas) || !match) return null;
|
|
2063
|
+
const file = path14.join(this.dir, canvas, name);
|
|
2064
|
+
return fs13.existsSync(file) ? file : null;
|
|
2065
|
+
}
|
|
2066
|
+
};
|
|
2067
|
+
|
|
2068
|
+
// src/server/index.ts
|
|
2069
|
+
var DEFAULT_PORT = 0;
|
|
2070
|
+
async function listen(server, port, host) {
|
|
2071
|
+
const attempt = (candidate) => new Promise((resolve, reject) => {
|
|
2072
|
+
const onError = (error) => {
|
|
2073
|
+
server.off("listening", onListening);
|
|
2074
|
+
reject(error);
|
|
2075
|
+
};
|
|
2076
|
+
const onListening = () => {
|
|
2077
|
+
server.off("error", onError);
|
|
2078
|
+
resolve(server.address().port);
|
|
2079
|
+
};
|
|
2080
|
+
server.once("error", onError);
|
|
2081
|
+
server.once("listening", onListening);
|
|
2082
|
+
server.listen(candidate, host);
|
|
2083
|
+
});
|
|
2084
|
+
if (port === 0) return attempt(0);
|
|
2085
|
+
for (let candidate = port; candidate < port + 40; candidate++) {
|
|
2086
|
+
try {
|
|
2087
|
+
return await attempt(candidate);
|
|
2088
|
+
} catch (error) {
|
|
2089
|
+
if (error.code !== "EADDRINUSE") throw error;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
throw new Error(`No free port between ${port} and ${port + 40}`);
|
|
2093
|
+
}
|
|
2094
|
+
function notFoundPage(res, title, text) {
|
|
2095
|
+
res.writeHead(404, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
2096
|
+
res.end(
|
|
2097
|
+
`<!doctype html><meta charset="utf-8"><body style="margin:0;display:grid;place-items:center;min-height:100vh;font:14px/1.5 ui-monospace,Menlo,monospace;background:#f5f6f8;color:#3a414a"><div style="max-width:520px;padding:24px"><div style="font-weight:600;margin-bottom:6px">${escapeHtml(title)}</div>${escapeHtml(text)}</div></body>`
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
async function startDevServer(options) {
|
|
2101
|
+
const paths = designPaths(options.root);
|
|
2102
|
+
if (!fs14.existsSync(paths.design)) throw new Error(`No .design folder in ${options.root}; run \`design init\``);
|
|
2103
|
+
fs14.mkdirSync(paths.cache, { recursive: true });
|
|
2104
|
+
const snapshots = new SnapshotStore(paths.snapshots);
|
|
2105
|
+
const project = new DesignProject(paths, DEV_URLS, snapshots.lookup);
|
|
2106
|
+
const entries = new Entries(project);
|
|
2107
|
+
entries.sync();
|
|
2108
|
+
const links = linkShippedPackages(paths);
|
|
2109
|
+
const events = new EventHub();
|
|
2110
|
+
const httpServer = http.createServer();
|
|
2111
|
+
const base = baseConfig(project);
|
|
2112
|
+
const vite = await createServer({
|
|
2113
|
+
...base,
|
|
2114
|
+
base: "/_fs/",
|
|
2115
|
+
appType: "custom",
|
|
2116
|
+
server: {
|
|
2117
|
+
middlewareMode: { server: httpServer },
|
|
2118
|
+
// Without this Vite 8 opens HMR on its own port (24678), which a second preview then collides with.
|
|
2119
|
+
hmr: { server: httpServer },
|
|
2120
|
+
fs: {
|
|
2121
|
+
strict: true,
|
|
2122
|
+
allow: [searchForWorkspaceRoot(paths.root), paths.root, paths.design, PKG.root, ...links.allow]
|
|
2123
|
+
},
|
|
2124
|
+
watch: {
|
|
2125
|
+
ignored: ["**/.cache/vite/**", "**/.cache/snapshots/**", "**/.cache/shots/**", "**/.cache/build/**"]
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
optimizeDeps: {
|
|
2129
|
+
entries: entries.files().map((file) => toPosix(path15.relative(paths.design, file)))
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
const viewer = fs14.existsSync(PKG.viewer) ? sirv(PKG.viewer, { dev: true, etag: true }) : null;
|
|
2133
|
+
const viewerIndex = path15.join(PKG.viewer, "index.html");
|
|
2134
|
+
const serveViewerIndex = (res) => {
|
|
2135
|
+
const html = fs14.readFileSync(viewerIndex, "utf8").replace(/<head>/i, '<head>\n<base href="/">');
|
|
2136
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
2137
|
+
res.end(html);
|
|
2138
|
+
};
|
|
2139
|
+
const ctx = { project, events, snapshots };
|
|
2140
|
+
const systemCssUrl = `/${relToDesign(paths, entries.systemCssFile())}`;
|
|
2141
|
+
const bootUrl = `/${relToDesign(paths, path15.join(entries.dir, HTML_BOOT))}`;
|
|
2142
|
+
const frameHead = (source, extra = {}) => {
|
|
2143
|
+
const canvasTheme = source.canvas === SYSTEM_CANVAS ? void 0 : project.canvas(source.canvas)?.doc.theme;
|
|
2144
|
+
return {
|
|
2145
|
+
source,
|
|
2146
|
+
theme: canvasTheme ?? "light",
|
|
2147
|
+
fonts: project.system()?.doc.fonts ?? [],
|
|
2148
|
+
snapshots: source.canvas !== SYSTEM_CANVAS,
|
|
2149
|
+
autoHeight: source.autoHeight ?? false,
|
|
2150
|
+
...extra
|
|
2151
|
+
};
|
|
2152
|
+
};
|
|
2153
|
+
const serveModuleShell = async (req, res, url2) => {
|
|
2154
|
+
const [canvas = "", id2 = ""] = url2.pathname.slice("/_s/".length).split("/").map(decodeURIComponent);
|
|
2155
|
+
const source = project.source(canvas, id2);
|
|
2156
|
+
if (source?.format !== "module") {
|
|
2157
|
+
notFoundPage(res, "Screen not found", `No screen "${id2}" in "${canvas}". Check canvas.json.`);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
if (!fs14.existsSync(entries.entryFile(source))) entries.sync();
|
|
2161
|
+
const entryUrl = `/${relToDesign(paths, entries.entryFile(source))}`;
|
|
2162
|
+
const html = await vite.transformIndexHtml(url2.pathname + url2.search, moduleShell(frameHead(source), entryUrl));
|
|
2163
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
2164
|
+
res.end(html);
|
|
2165
|
+
void req;
|
|
2166
|
+
};
|
|
2167
|
+
const serveHtmlScreen = async (res, url2) => {
|
|
2168
|
+
const rel = decodeURIComponent(url2.pathname.slice("/_fs/".length));
|
|
2169
|
+
const file = path15.resolve(paths.design, rel);
|
|
2170
|
+
if (!isInside(paths.design, file) || !fs14.existsSync(file)) {
|
|
2171
|
+
notFoundPage(res, "Screen not found", `No file .design/${rel}`);
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const [canvas = "", id2 = ""] = (url2.searchParams.get("__design") ?? "").split("/");
|
|
2175
|
+
const found = project.source(canvas, id2);
|
|
2176
|
+
const source = found ?? {
|
|
2177
|
+
key: `${canvas}/${id2}`,
|
|
2178
|
+
canvas,
|
|
2179
|
+
id: id2,
|
|
2180
|
+
title: path15.basename(file),
|
|
2181
|
+
file,
|
|
2182
|
+
format: "html",
|
|
2183
|
+
props: {},
|
|
2184
|
+
system: true
|
|
2185
|
+
};
|
|
2186
|
+
const raw = fs14.readFileSync(file, "utf8");
|
|
2187
|
+
const withSystem = source.system && !optsOutOfSystem(raw);
|
|
2188
|
+
const html = injectIntoHtml(
|
|
2189
|
+
raw,
|
|
2190
|
+
frameHead(source, { systemCss: withSystem ? systemCssUrl : void 0, boot: bootUrl })
|
|
2191
|
+
);
|
|
2192
|
+
const transformed = await vite.transformIndexHtml(`/${rel}`, html, url2.pathname + url2.search);
|
|
2193
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
2194
|
+
res.end(transformed);
|
|
2195
|
+
};
|
|
2196
|
+
httpServer.on("request", (req, res) => {
|
|
2197
|
+
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
2198
|
+
const fail = (error) => {
|
|
2199
|
+
if (res.headersSent) return res.end();
|
|
2200
|
+
sendJson(res, 500, { error: error?.stack ?? String(error) });
|
|
2201
|
+
};
|
|
2202
|
+
try {
|
|
2203
|
+
if (url2.pathname.startsWith("/api/")) {
|
|
2204
|
+
handleApi(ctx, req, res, url2).then((handled) => {
|
|
2205
|
+
if (!handled) sendJson(res, 404, { error: "unknown endpoint" });
|
|
2206
|
+
}).catch(fail);
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (url2.pathname.startsWith("/_s/")) {
|
|
2210
|
+
serveModuleShell(req, res, url2).catch(fail);
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
if (url2.pathname.startsWith("/_fs/") && url2.searchParams.has("__design") && /\.html?$/i.test(url2.pathname)) {
|
|
2214
|
+
serveHtmlScreen(res, url2).catch(fail);
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
if (url2.pathname.startsWith("/_fs/")) {
|
|
2218
|
+
vite.middlewares(req, res, () => notFoundPage(res, "Not found", url2.pathname));
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (viewer) {
|
|
2222
|
+
if (url2.pathname === "/" || url2.pathname === "/index.html") serveViewerIndex(res);
|
|
2223
|
+
else
|
|
2224
|
+
viewer(
|
|
2225
|
+
req,
|
|
2226
|
+
res,
|
|
2227
|
+
() => path15.extname(url2.pathname) ? notFoundPage(res, "Not found", url2.pathname) : serveViewerIndex(res)
|
|
2228
|
+
);
|
|
2229
|
+
} else notFoundPage(res, "Viewer is not built", "Run `pnpm build` in the foss-design package.");
|
|
2230
|
+
} catch (error) {
|
|
2231
|
+
fail(error);
|
|
2232
|
+
}
|
|
2233
|
+
});
|
|
2234
|
+
const pending = /* @__PURE__ */ new Map();
|
|
2235
|
+
let flushTimer;
|
|
2236
|
+
const queue = (key, run) => {
|
|
2237
|
+
pending.set(key, run);
|
|
2238
|
+
clearTimeout(flushTimer);
|
|
2239
|
+
flushTimer = setTimeout(() => {
|
|
2240
|
+
const runs = [...pending.values()];
|
|
2241
|
+
pending.clear();
|
|
2242
|
+
try {
|
|
2243
|
+
for (const job of runs) job();
|
|
2244
|
+
entries.sync();
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
console.error("[design]", error.message);
|
|
2247
|
+
}
|
|
2248
|
+
}, 60);
|
|
2249
|
+
};
|
|
2250
|
+
vite.watcher.on("all", (event, file) => {
|
|
2251
|
+
const rel = toPosix(path15.relative(paths.design, file));
|
|
2252
|
+
if (rel.startsWith("..") || rel.startsWith(".cache/") || rel.startsWith("node_modules/")) return;
|
|
2253
|
+
if (rel === "design.json") {
|
|
2254
|
+
queue("config", () => {
|
|
2255
|
+
project.invalidateConfig();
|
|
2256
|
+
events.send({ type: "project" });
|
|
2257
|
+
});
|
|
2258
|
+
} else if (rel.startsWith("system/")) {
|
|
2259
|
+
queue("system", () => {
|
|
2260
|
+
project.invalidateSystem();
|
|
2261
|
+
events.send({ type: "system" });
|
|
2262
|
+
events.send({ type: "project" });
|
|
2263
|
+
});
|
|
2264
|
+
} else if (rel.startsWith("canvas/")) {
|
|
2265
|
+
const [, id2, ...rest] = rel.split("/");
|
|
2266
|
+
if (!id2) return;
|
|
2267
|
+
const structural = event !== "change" || rest.join("/") === "canvas.json";
|
|
2268
|
+
if (!structural) return;
|
|
2269
|
+
queue(`canvas:${id2}`, () => {
|
|
2270
|
+
project.invalidateCanvas(id2);
|
|
2271
|
+
events.send({ type: "canvas", id: id2 });
|
|
2272
|
+
events.send({ type: "project" });
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
const host = options.host ?? "127.0.0.1";
|
|
2277
|
+
const port = await listen(httpServer, options.port ?? project.config().config.port ?? DEFAULT_PORT, host);
|
|
2278
|
+
const url = `http://localhost:${port}`;
|
|
2279
|
+
if (options.record !== false) {
|
|
2280
|
+
writeState(paths, {
|
|
2281
|
+
pid: process.pid,
|
|
2282
|
+
port,
|
|
2283
|
+
url,
|
|
2284
|
+
root: paths.root,
|
|
2285
|
+
version: packageVersion(),
|
|
2286
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
let closed = false;
|
|
2290
|
+
return {
|
|
2291
|
+
url,
|
|
2292
|
+
port,
|
|
2293
|
+
project,
|
|
2294
|
+
async close() {
|
|
2295
|
+
if (closed) return;
|
|
2296
|
+
closed = true;
|
|
2297
|
+
events.close();
|
|
2298
|
+
await vite.close();
|
|
2299
|
+
await new Promise((resolve) => {
|
|
2300
|
+
httpServer.close(() => resolve());
|
|
2301
|
+
httpServer.closeAllConnections();
|
|
2302
|
+
});
|
|
2303
|
+
if (options.record !== false) clearState(paths, process.pid);
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// src/cli/commands/preview.ts
|
|
2309
|
+
function openBrowser(url) {
|
|
2310
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
2311
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
2312
|
+
child.on("error", () => {
|
|
2313
|
+
});
|
|
2314
|
+
child.unref();
|
|
2315
|
+
}
|
|
2316
|
+
function printUrls(paths, url) {
|
|
2317
|
+
const project = new DesignProject(paths, DEV_URLS);
|
|
2318
|
+
print(`${bold("Preview")} ${url}`);
|
|
2319
|
+
for (const id2 of project.canvasIds()) print(` ${dim("canvas")} ${url}/c/${id2}`);
|
|
2320
|
+
if (project.system()) print(` ${dim("system")} ${url}/system`);
|
|
2321
|
+
}
|
|
2322
|
+
async function waitForServer(paths, timeoutMs) {
|
|
2323
|
+
const until = Date.now() + timeoutMs;
|
|
2324
|
+
while (Date.now() < until) {
|
|
2325
|
+
const live = await liveServer(paths);
|
|
2326
|
+
if (live) return live;
|
|
2327
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
2328
|
+
}
|
|
2329
|
+
return null;
|
|
2330
|
+
}
|
|
2331
|
+
async function stopServer(paths) {
|
|
2332
|
+
const state = readState(paths);
|
|
2333
|
+
if (!state) return false;
|
|
2334
|
+
try {
|
|
2335
|
+
process.kill(state.pid, "SIGTERM");
|
|
2336
|
+
} catch {
|
|
2337
|
+
clearState(paths);
|
|
2338
|
+
return false;
|
|
2339
|
+
}
|
|
2340
|
+
const until = Date.now() + 5e3;
|
|
2341
|
+
while (Date.now() < until) {
|
|
2342
|
+
try {
|
|
2343
|
+
process.kill(state.pid, 0);
|
|
2344
|
+
} catch {
|
|
2345
|
+
break;
|
|
2346
|
+
}
|
|
2347
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2348
|
+
}
|
|
2349
|
+
clearState(paths);
|
|
2350
|
+
return true;
|
|
2351
|
+
}
|
|
2352
|
+
async function ensureServer(paths, options) {
|
|
2353
|
+
const live = await liveServer(paths);
|
|
2354
|
+
if (live && !options.restart) return live;
|
|
2355
|
+
if (live) await stopServer(paths);
|
|
2356
|
+
fs15.mkdirSync(paths.cache, { recursive: true });
|
|
2357
|
+
const log = fs15.openSync(logFile(paths), "w");
|
|
2358
|
+
const args = [PKG.cli, "preview", "--foreground", "--root", paths.root];
|
|
2359
|
+
if (options.port) args.push("--port", String(options.port));
|
|
2360
|
+
const child = spawn(process.execPath, args, { detached: true, stdio: ["ignore", log, log], cwd: paths.root });
|
|
2361
|
+
child.unref();
|
|
2362
|
+
const state = await waitForServer(paths, 4e4);
|
|
2363
|
+
if (!state) {
|
|
2364
|
+
const tail = fs15.existsSync(logFile(paths)) ? fs15.readFileSync(logFile(paths), "utf8").split("\n").slice(-25) : [];
|
|
2365
|
+
throw new CliError(`The preview server did not start.
|
|
2366
|
+
${tail.join("\n")}`);
|
|
2367
|
+
}
|
|
2368
|
+
return state;
|
|
2369
|
+
}
|
|
2370
|
+
async function runPreview(paths, options) {
|
|
2371
|
+
const openPath = typeof options.open === "string" ? options.open : "";
|
|
2372
|
+
if (options.foreground) {
|
|
2373
|
+
const server = await startDevServer({ root: paths.root, port: options.port });
|
|
2374
|
+
printUrls(paths, server.url);
|
|
2375
|
+
if (options.open) openBrowser(`${server.url}${openPath}`);
|
|
2376
|
+
const shutdown = () => {
|
|
2377
|
+
void server.close().finally(() => process.exit(0));
|
|
2378
|
+
};
|
|
2379
|
+
process.on("SIGINT", shutdown);
|
|
2380
|
+
process.on("SIGTERM", shutdown);
|
|
2381
|
+
return;
|
|
2382
|
+
}
|
|
2383
|
+
const state = await ensureServer(paths, options);
|
|
2384
|
+
printUrls(paths, state.url);
|
|
2385
|
+
if (options.open) openBrowser(`${state.url}${openPath}`);
|
|
2386
|
+
}
|
|
2387
|
+
async function runStop(paths) {
|
|
2388
|
+
print(await stopServer(paths) ? "Stopped the preview server." : "No preview server is running for this project.");
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// src/cli/commands/check.ts
|
|
2392
|
+
var plural = (count, word, many = `${word}s`) => `${count} ${count === 1 ? word : many}`;
|
|
2393
|
+
function printIssues(issues) {
|
|
2394
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2395
|
+
for (const issue of issues) byFile.set(issue.file, [...byFile.get(issue.file) ?? [], issue]);
|
|
2396
|
+
for (const [file, list] of byFile) {
|
|
2397
|
+
print(bold(file));
|
|
2398
|
+
for (const issue of list) {
|
|
2399
|
+
const tag = issue.severity === "error" ? red("error ") : yellow("warning");
|
|
2400
|
+
print(` ${tag} ${issue.at ? `${dim(issue.at)} ` : ""}${issue.message}`);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
async function renderFrames(jobs) {
|
|
2405
|
+
const browser = await launchChrome();
|
|
2406
|
+
try {
|
|
2407
|
+
return await mapLimit(jobs, 4, async (job) => {
|
|
2408
|
+
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
2409
|
+
let report;
|
|
2410
|
+
try {
|
|
2411
|
+
report = await openFrame(page, job.url, { waitForReady: job.waitForReady, settleMs: 300 });
|
|
2412
|
+
} catch (error) {
|
|
2413
|
+
report = { ready: false, errors: [error.message.split("\n")[0]] };
|
|
2414
|
+
} finally {
|
|
2415
|
+
await page.close().catch(() => {
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
return { key: job.key, url: job.url, ready: report.ready, errors: report.errors };
|
|
2419
|
+
});
|
|
2420
|
+
} finally {
|
|
2421
|
+
await browser.close();
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
async function runCheck(paths, canvases, options) {
|
|
2425
|
+
const project = new DesignProject(paths, DEV_URLS);
|
|
2426
|
+
const known = project.canvasIds();
|
|
2427
|
+
const unknown = canvases.filter((id2) => !known.includes(id2));
|
|
2428
|
+
if (unknown.length) {
|
|
2429
|
+
throw new CliError(
|
|
2430
|
+
`No canvas ${unknown.map((id2) => `"${id2}"`).join(", ")}. Canvases: ${known.join(", ") || "none yet"}`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
const issues = canvases.length ? canvases.flatMap((id2) => project.canvas(id2)?.doc.issues ?? []) : project.issues();
|
|
2434
|
+
let frames = [];
|
|
2435
|
+
if (options.render) {
|
|
2436
|
+
const wasRunning = await liveServer(paths);
|
|
2437
|
+
const server = await ensureServer(paths, {});
|
|
2438
|
+
if (!wasRunning && !options.json)
|
|
2439
|
+
print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
|
|
2440
|
+
const base = `http://127.0.0.1:${server.port}`;
|
|
2441
|
+
const docs = (canvases.length ? canvases : known).map((id2) => project.canvas(id2).doc);
|
|
2442
|
+
const jobs = docs.flatMap((doc) => canvasFrames(doc, base, doc.theme ?? "light", { includeUrls: false }));
|
|
2443
|
+
const system = project.system();
|
|
2444
|
+
if (!canvases.length && system) jobs.unshift(...systemFrames(system.doc, base, "light"));
|
|
2445
|
+
if (!options.json) print(dim(`Loading ${plural(jobs.length, "frame")} in Chrome via ${server.url}\u2026`));
|
|
2446
|
+
frames = await renderFrames(jobs);
|
|
2447
|
+
}
|
|
2448
|
+
const errors = issues.filter((issue) => issue.severity === "error").length;
|
|
2449
|
+
const warnings = issues.length - errors;
|
|
2450
|
+
const broken = frames.filter((frame) => !frame.ready || frame.errors.length);
|
|
2451
|
+
if (options.json) {
|
|
2452
|
+
print(JSON.stringify({ issues, frames }, null, 2));
|
|
2453
|
+
} else {
|
|
2454
|
+
if (issues.length) printIssues(issues);
|
|
2455
|
+
if (options.render) {
|
|
2456
|
+
if (issues.length) print();
|
|
2457
|
+
for (const frame of frames) {
|
|
2458
|
+
const ok = frame.ready && !frame.errors.length;
|
|
2459
|
+
print(`${ok ? green("\u2713") : red("\u2717")} ${frame.key} ${dim(frame.url)}`);
|
|
2460
|
+
if (!frame.ready) print(` ${red("did not finish rendering (no ready signal within 20s)")}`);
|
|
2461
|
+
for (const error of frame.errors) print(` ${red(error.split("\n")[0])}`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
const scope = canvases.length ? plural(canvases.length, "canvas", "canvases") : "the project";
|
|
2465
|
+
if (!errors && !warnings && !broken.length) {
|
|
2466
|
+
print(
|
|
2467
|
+
green(
|
|
2468
|
+
`\u2713 No problems in ${scope}${options.render ? `; ${plural(frames.length, "frame")} rendered cleanly` : ""}.`
|
|
2469
|
+
)
|
|
2470
|
+
);
|
|
2471
|
+
} else {
|
|
2472
|
+
const parts = [plural(errors, "error"), plural(warnings, "warning")];
|
|
2473
|
+
if (options.render) parts.push(`${broken.length} of ${plural(frames.length, "frame")} with problems`);
|
|
2474
|
+
print(errors || broken.length ? red(parts.join(", ")) : yellow(parts.join(", ")));
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
if (errors || broken.length) process.exitCode = 1;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// src/cli/commands/scaffold.ts
|
|
2481
|
+
import fs16 from "fs";
|
|
2482
|
+
import path16 from "path";
|
|
2483
|
+
|
|
2484
|
+
// src/cli/templates.ts
|
|
2485
|
+
var TOKENS_CSS = `/*
|
|
2486
|
+
* Design tokens. Every screen gets Tailwind v4 with these as utilities:
|
|
2487
|
+
* bg-surface, text-ink, border-rule, font-sans, rounded-control, text-body\u2026
|
|
2488
|
+
*
|
|
2489
|
+
* Light values live in :root, dark overrides in :root[data-theme="dark"],
|
|
2490
|
+
* and @theme inline maps them to Tailwind. A "@group Name" comment starts a
|
|
2491
|
+
* group in the viewer; a comment right above a token describes it.
|
|
2492
|
+
*/
|
|
2493
|
+
|
|
2494
|
+
:root {
|
|
2495
|
+
/* @group Surfaces */
|
|
2496
|
+
/* Page background */
|
|
2497
|
+
--bg: #f4f5f7;
|
|
2498
|
+
/* Cards, panels and inputs */
|
|
2499
|
+
--surface: #ffffff;
|
|
2500
|
+
/* Hover fills and quiet areas */
|
|
2501
|
+
--soft: #eceef1;
|
|
2502
|
+
|
|
2503
|
+
/* @group Text */
|
|
2504
|
+
--ink: #16181d;
|
|
2505
|
+
--ink-2: #3b4049;
|
|
2506
|
+
--muted: #6b7280;
|
|
2507
|
+
|
|
2508
|
+
/* @group Lines */
|
|
2509
|
+
--rule: #dfe2e7;
|
|
2510
|
+
--rule-strong: #b8bec7;
|
|
2511
|
+
|
|
2512
|
+
/* @group Accent */
|
|
2513
|
+
--accent: #2f5bea;
|
|
2514
|
+
--accent-ink: #ffffff;
|
|
2515
|
+
--accent-soft: #e6ecfd;
|
|
2516
|
+
|
|
2517
|
+
/* @group Status */
|
|
2518
|
+
--ok: #1f7a4d;
|
|
2519
|
+
--ok-soft: #e3f3ea;
|
|
2520
|
+
--danger: #c0352b;
|
|
2521
|
+
--danger-soft: #fbe9e7;
|
|
2522
|
+
|
|
2523
|
+
color-scheme: light;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
:root[data-theme="dark"] {
|
|
2527
|
+
--bg: #111317;
|
|
2528
|
+
--surface: #181b20;
|
|
2529
|
+
--soft: #20242a;
|
|
2530
|
+
--ink: #e8eaed;
|
|
2531
|
+
--ink-2: #c4c8cf;
|
|
2532
|
+
--muted: #8b929c;
|
|
2533
|
+
--rule: #272b32;
|
|
2534
|
+
--rule-strong: #434952;
|
|
2535
|
+
--accent: #7c9bff;
|
|
2536
|
+
--accent-ink: #0b1020;
|
|
2537
|
+
--accent-soft: #1c2645;
|
|
2538
|
+
--ok: #5cc18d;
|
|
2539
|
+
--ok-soft: #16301f;
|
|
2540
|
+
--danger: #f07b70;
|
|
2541
|
+
--danger-soft: #3a1c19;
|
|
2542
|
+
color-scheme: dark;
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
@theme inline {
|
|
2546
|
+
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
2547
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
2548
|
+
|
|
2549
|
+
--color-bg: var(--bg);
|
|
2550
|
+
--color-surface: var(--surface);
|
|
2551
|
+
--color-soft: var(--soft);
|
|
2552
|
+
--color-ink: var(--ink);
|
|
2553
|
+
--color-ink-2: var(--ink-2);
|
|
2554
|
+
--color-muted: var(--muted);
|
|
2555
|
+
--color-rule: var(--rule);
|
|
2556
|
+
--color-rule-strong: var(--rule-strong);
|
|
2557
|
+
--color-accent: var(--accent);
|
|
2558
|
+
--color-accent-ink: var(--accent-ink);
|
|
2559
|
+
--color-accent-soft: var(--accent-soft);
|
|
2560
|
+
--color-ok: var(--ok);
|
|
2561
|
+
--color-ok-soft: var(--ok-soft);
|
|
2562
|
+
--color-danger: var(--danger);
|
|
2563
|
+
--color-danger-soft: var(--danger-soft);
|
|
2564
|
+
|
|
2565
|
+
/* Captions, table meta */
|
|
2566
|
+
--text-caption: 12px;
|
|
2567
|
+
--text-caption--line-height: 16px;
|
|
2568
|
+
/* Default reading size */
|
|
2569
|
+
--text-body: 14px;
|
|
2570
|
+
--text-body--line-height: 20px;
|
|
2571
|
+
--text-title: 20px;
|
|
2572
|
+
--text-title--line-height: 28px;
|
|
2573
|
+
--text-display: 32px;
|
|
2574
|
+
--text-display--line-height: 40px;
|
|
2575
|
+
|
|
2576
|
+
/* Buttons and inputs */
|
|
2577
|
+
--radius-control: 6px;
|
|
2578
|
+
/* Cards and panels */
|
|
2579
|
+
--radius-card: 10px;
|
|
2580
|
+
|
|
2581
|
+
/* Menus and popovers */
|
|
2582
|
+
--shadow-pop: 0 12px 32px rgb(16 24 40 / 0.14);
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
@layer base {
|
|
2586
|
+
body {
|
|
2587
|
+
background: var(--bg);
|
|
2588
|
+
color: var(--ink);
|
|
2589
|
+
font-family: var(--font-sans);
|
|
2590
|
+
font-size: var(--text-body);
|
|
2591
|
+
line-height: var(--text-body--line-height);
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
`;
|
|
2595
|
+
var OVERVIEW_MD = `# Overview
|
|
2596
|
+
|
|
2597
|
+
What this product is, who it is for, and the three or four principles every screen follows.
|
|
2598
|
+
|
|
2599
|
+
## Principles
|
|
2600
|
+
|
|
2601
|
+
- **One thing per screen.** Each view has one primary action.
|
|
2602
|
+
- **Quiet chrome.** Neutral surfaces; the accent marks what the user can act on.
|
|
2603
|
+
|
|
2604
|
+
## Voice
|
|
2605
|
+
|
|
2606
|
+
How the interface talks: sentence case, plain verbs, no exclamation marks.
|
|
2607
|
+
`;
|
|
2608
|
+
var BUTTON_TSX = `import type { ButtonHTMLAttributes } from 'react'
|
|
2609
|
+
|
|
2610
|
+
export type ButtonKind = 'primary' | 'secondary' | 'ghost'
|
|
2611
|
+
|
|
2612
|
+
const KIND: Record<ButtonKind, string> = {
|
|
2613
|
+
primary: 'bg-accent text-accent-ink hover:opacity-90',
|
|
2614
|
+
secondary: 'border border-rule-strong bg-surface text-ink hover:bg-soft',
|
|
2615
|
+
ghost: 'text-ink-2 hover:bg-soft',
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
export function Button({
|
|
2619
|
+
kind = 'secondary',
|
|
2620
|
+
className = '',
|
|
2621
|
+
...rest
|
|
2622
|
+
}: ButtonHTMLAttributes<HTMLButtonElement> & { kind?: ButtonKind }) {
|
|
2623
|
+
return (
|
|
2624
|
+
<button
|
|
2625
|
+
type="button"
|
|
2626
|
+
className={\`inline-flex h-9 cursor-pointer items-center justify-center gap-2 rounded-control px-3.5 text-body font-medium transition disabled:cursor-not-allowed disabled:opacity-45 \${KIND[kind]} \${className}\`}
|
|
2627
|
+
{...rest}
|
|
2628
|
+
/>
|
|
2629
|
+
)
|
|
2630
|
+
}
|
|
2631
|
+
`;
|
|
2632
|
+
var BUTTON_SPECIMEN_TSX = `/**
|
|
2633
|
+
* @title Button
|
|
2634
|
+
* @group Actions
|
|
2635
|
+
* @description One primary action per view, secondary for the rest, ghost inside toolbars.
|
|
2636
|
+
* @source ../components/button.tsx
|
|
2637
|
+
*/
|
|
2638
|
+
import { Button } from '@system/components/button'
|
|
2639
|
+
|
|
2640
|
+
export default function ButtonSpecimen() {
|
|
2641
|
+
return (
|
|
2642
|
+
<div className="flex flex-col gap-6 p-8">
|
|
2643
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
2644
|
+
<Button kind="primary">Save changes</Button>
|
|
2645
|
+
<Button>Cancel</Button>
|
|
2646
|
+
<Button kind="ghost">More</Button>
|
|
2647
|
+
</div>
|
|
2648
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
2649
|
+
<Button kind="primary" disabled>
|
|
2650
|
+
Save changes
|
|
2651
|
+
</Button>
|
|
2652
|
+
<Button disabled>Cancel</Button>
|
|
2653
|
+
</div>
|
|
2654
|
+
</div>
|
|
2655
|
+
)
|
|
2656
|
+
}
|
|
2657
|
+
`;
|
|
2658
|
+
function canvasJson(title) {
|
|
2659
|
+
return `${JSON.stringify(
|
|
2660
|
+
{
|
|
2661
|
+
title,
|
|
2662
|
+
pages: [
|
|
2663
|
+
{
|
|
2664
|
+
id: "main",
|
|
2665
|
+
title: "Main",
|
|
2666
|
+
sections: [{ title: "Screens", items: [{ src: "screens/Main.tsx", device: "desktop" }] }]
|
|
2667
|
+
}
|
|
2668
|
+
]
|
|
2669
|
+
},
|
|
2670
|
+
null,
|
|
2671
|
+
2
|
|
2672
|
+
)}
|
|
2673
|
+
`;
|
|
2674
|
+
}
|
|
2675
|
+
function mainScreen(canvasId) {
|
|
2676
|
+
return `export default function Main() {
|
|
2677
|
+
return (
|
|
2678
|
+
<main className="grid min-h-screen place-items-center p-10">
|
|
2679
|
+
<div className="max-w-md text-center">
|
|
2680
|
+
<h1 className="text-2xl font-semibold">New canvas</h1>
|
|
2681
|
+
<p className="mt-2 text-sm opacity-70">
|
|
2682
|
+
Edit .design/canvas/${canvasId}/screens/Main.tsx and the canvas updates on save.
|
|
2683
|
+
</p>
|
|
2684
|
+
</div>
|
|
2685
|
+
</main>
|
|
2686
|
+
)
|
|
2687
|
+
}
|
|
2688
|
+
`;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// src/cli/commands/scaffold.ts
|
|
2692
|
+
function write(file, content, root) {
|
|
2693
|
+
if (fs16.existsSync(file)) {
|
|
2694
|
+
print(` ${dim("exists ")} ${path16.relative(root, file)}`);
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
fs16.mkdirSync(path16.dirname(file), { recursive: true });
|
|
2698
|
+
fs16.writeFileSync(file, content);
|
|
2699
|
+
print(` ${green("created")} ${path16.relative(root, file)}`);
|
|
2700
|
+
return true;
|
|
2701
|
+
}
|
|
2702
|
+
function ensureGitignored(root) {
|
|
2703
|
+
const file = path16.join(root, ".gitignore");
|
|
2704
|
+
const current = fs16.existsSync(file) ? fs16.readFileSync(file, "utf8") : "";
|
|
2705
|
+
const covered = current.split(/\r?\n/).map((line) => line.trim()).some((line) => [".design", ".design/", "/.design", "/.design/", ".design/*"].includes(line));
|
|
2706
|
+
if (covered) return "present";
|
|
2707
|
+
const prefix = current && !current.endsWith("\n") ? "\n" : "";
|
|
2708
|
+
fs16.writeFileSync(file, `${current}${prefix}${current ? "\n" : ""}# Design canvases (foss-design)
|
|
2709
|
+
.design/
|
|
2710
|
+
`);
|
|
2711
|
+
return "added";
|
|
2712
|
+
}
|
|
2713
|
+
function projectName(root) {
|
|
2714
|
+
try {
|
|
2715
|
+
const manifest = JSON.parse(fs16.readFileSync(path16.join(root, "package.json"), "utf8"));
|
|
2716
|
+
if (typeof manifest.name === "string" && manifest.name) return manifest.name;
|
|
2717
|
+
} catch {
|
|
2718
|
+
}
|
|
2719
|
+
return path16.basename(root);
|
|
2720
|
+
}
|
|
2721
|
+
function runInit(root, options) {
|
|
2722
|
+
const paths = designPaths(root);
|
|
2723
|
+
print(`Setting up ${DESIGN_DIR} in ${root}`);
|
|
2724
|
+
write(paths.config, `${JSON.stringify({ name: options.name ?? projectName(root) }, null, 2)}
|
|
2725
|
+
`, root);
|
|
2726
|
+
fs16.mkdirSync(paths.canvases, { recursive: true });
|
|
2727
|
+
if (options.gitignore) {
|
|
2728
|
+
const result = ensureGitignored(root);
|
|
2729
|
+
print(` ${result === "added" ? green("ignored") : dim("ignored")} .design/ in .gitignore`);
|
|
2730
|
+
}
|
|
2731
|
+
print();
|
|
2732
|
+
print("Next: `design system init` for a design system, `design new <canvas>` for a canvas, `design preview` to look.");
|
|
2733
|
+
}
|
|
2734
|
+
function runSystemInit(root, options) {
|
|
2735
|
+
const paths = designPaths(root);
|
|
2736
|
+
let base = projectName(root);
|
|
2737
|
+
try {
|
|
2738
|
+
base = JSON.parse(fs16.readFileSync(paths.config, "utf8")).name || base;
|
|
2739
|
+
} catch {
|
|
2740
|
+
}
|
|
2741
|
+
const name = options.name ?? `${base} design system`;
|
|
2742
|
+
print(`Scaffolding the design system in ${path16.relative(root, paths.system)}`);
|
|
2743
|
+
write(path16.join(paths.system, "system.json"), `${JSON.stringify({ name }, null, 2)}
|
|
2744
|
+
`, root);
|
|
2745
|
+
write(path16.join(paths.system, "tokens.css"), TOKENS_CSS, root);
|
|
2746
|
+
if (!options.empty) {
|
|
2747
|
+
write(path16.join(paths.system, "guidelines", "01-overview.md"), OVERVIEW_MD, root);
|
|
2748
|
+
write(path16.join(paths.system, "components", "button.tsx"), BUTTON_TSX, root);
|
|
2749
|
+
write(path16.join(paths.system, "specimens", "button.tsx"), BUTTON_SPECIMEN_TSX, root);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
function runNew(root, id2, options) {
|
|
2753
|
+
if (!ID_PATTERN.test(id2)) throw new CliError(`"${id2}" is not a valid canvas id: use letters, digits, "-" and "_"`);
|
|
2754
|
+
const paths = designPaths(root);
|
|
2755
|
+
const dir = path16.join(paths.canvases, id2);
|
|
2756
|
+
if (fs16.existsSync(path16.join(dir, "canvas.json"))) throw new CliError(`Canvas "${id2}" already exists in ${dir}`);
|
|
2757
|
+
print(`Creating canvas "${id2}"`);
|
|
2758
|
+
const title = options.title ?? titleize(id2);
|
|
2759
|
+
if (options.empty) {
|
|
2760
|
+
write(
|
|
2761
|
+
path16.join(dir, "canvas.json"),
|
|
2762
|
+
`${JSON.stringify({ title, pages: [{ id: "main", title: "Main", sections: [] }] }, null, 2)}
|
|
2763
|
+
`,
|
|
2764
|
+
root
|
|
2765
|
+
);
|
|
2766
|
+
fs16.mkdirSync(path16.join(dir, "screens"), { recursive: true });
|
|
2767
|
+
} else {
|
|
2768
|
+
write(path16.join(dir, "canvas.json"), canvasJson(title), root);
|
|
2769
|
+
write(path16.join(dir, "screens", "Main.tsx"), mainScreen(id2), root);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// src/cli/commands/shot.ts
|
|
2774
|
+
import fs17 from "fs";
|
|
2775
|
+
import path17 from "path";
|
|
2776
|
+
var OVERVIEW_TIMEOUT = 9e4;
|
|
2777
|
+
async function runShot(paths, target, options) {
|
|
2778
|
+
const [canvasId = "", screenId] = target.split("/");
|
|
2779
|
+
const project = new DesignProject(paths, DEV_URLS);
|
|
2780
|
+
if (canvasId === SYSTEM_CANVAS) return shootSystem(paths, project, screenId, options);
|
|
2781
|
+
const canvas = project.canvas(canvasId);
|
|
2782
|
+
if (!canvas) {
|
|
2783
|
+
const known = project.canvasIds();
|
|
2784
|
+
throw new CliError(`No canvas "${canvasId}". Canvases: ${known.join(", ") || "none yet"}`);
|
|
2785
|
+
}
|
|
2786
|
+
const { doc } = canvas;
|
|
2787
|
+
if (options.page && !doc.pages.some((page) => page.id === options.page)) {
|
|
2788
|
+
throw new CliError(`No page "${options.page}" in "${canvasId}". Pages: ${doc.pages.map((p) => p.id).join(", ")}`);
|
|
2789
|
+
}
|
|
2790
|
+
const theme2 = options.theme ?? doc.theme ?? "light";
|
|
2791
|
+
const outBase = path17.resolve(options.out ?? paths.shots);
|
|
2792
|
+
const outDir = path17.join(outBase, canvasId);
|
|
2793
|
+
fs17.mkdirSync(outDir, { recursive: true });
|
|
2794
|
+
const base = await serverBase(paths);
|
|
2795
|
+
const browser = await launchChrome();
|
|
2796
|
+
let failed = false;
|
|
2797
|
+
try {
|
|
2798
|
+
if (options.overview) {
|
|
2799
|
+
if (!fs17.existsSync(PKG.viewer)) throw new CliError("The viewer is not built, so there is no canvas to shoot.");
|
|
2800
|
+
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);
|
|
2801
|
+
for (const pageId of pages) {
|
|
2802
|
+
const page = await browser.newPage({ viewport: { width: 1920, height: 1200 } });
|
|
2803
|
+
const url = `${base}/c/${encodeURIComponent(canvasId)}?page=${encodeURIComponent(pageId)}&capture=1&theme=${theme2}`;
|
|
2804
|
+
await page.goto(url, { waitUntil: "load" });
|
|
2805
|
+
try {
|
|
2806
|
+
await page.waitForFunction(
|
|
2807
|
+
() => window.__DESIGN_CANVAS_READY__ === true,
|
|
2808
|
+
null,
|
|
2809
|
+
{ timeout: OVERVIEW_TIMEOUT }
|
|
2810
|
+
);
|
|
2811
|
+
} catch {
|
|
2812
|
+
warn(`page "${pageId}": not every frame reported ready within ${OVERVIEW_TIMEOUT / 1e3}s; shooting anyway`);
|
|
2813
|
+
}
|
|
2814
|
+
await page.waitForTimeout(400);
|
|
2815
|
+
const file = path17.join(outDir, `${pageId}.overview.png`);
|
|
2816
|
+
await page.screenshot({ path: file });
|
|
2817
|
+
await page.close();
|
|
2818
|
+
print(file);
|
|
2819
|
+
}
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
const jobs = canvasFrames(doc, base, theme2, { page: options.page, id: screenId });
|
|
2823
|
+
if (!jobs.length) {
|
|
2824
|
+
throw new CliError(
|
|
2825
|
+
screenId ? `No screen "${screenId}" in "${canvasId}"${options.page ? ` on page "${options.page}"` : ""}.` : `Nothing to shoot in "${canvasId}"${options.page ? ` on page "${options.page}"` : ""}.`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
failed = await shootFrames(browser, jobs, outDir);
|
|
2829
|
+
} finally {
|
|
2830
|
+
await browser.close();
|
|
2831
|
+
}
|
|
2832
|
+
if (failed) process.exitCode = 1;
|
|
2833
|
+
}
|
|
2834
|
+
async function serverBase(paths) {
|
|
2835
|
+
const wasRunning = await liveServer(paths);
|
|
2836
|
+
const server = await ensureServer(paths, {});
|
|
2837
|
+
if (!wasRunning) print(dim(`Started the preview server at ${server.url} (\`design stop\` stops it).`));
|
|
2838
|
+
return `http://127.0.0.1:${server.port}`;
|
|
2839
|
+
}
|
|
2840
|
+
async function shootFrames(browser, jobs, outDir) {
|
|
2841
|
+
let failed = false;
|
|
2842
|
+
const results = await mapLimit(jobs, 4, async (job) => {
|
|
2843
|
+
const page = await browser.newPage({ viewport: { width: job.width, height: job.height } });
|
|
2844
|
+
const file = path17.join(outDir, `${job.id}.${job.theme}.png`);
|
|
2845
|
+
try {
|
|
2846
|
+
const report = await openFrame(page, job.url, { waitForReady: job.waitForReady });
|
|
2847
|
+
await page.screenshot({ path: file, fullPage: job.fullPage });
|
|
2848
|
+
return { file, errors: report.ready ? report.errors : ["did not finish rendering", ...report.errors] };
|
|
2849
|
+
} catch (error) {
|
|
2850
|
+
return { file: null, errors: [error.message.split("\n")[0]] };
|
|
2851
|
+
} finally {
|
|
2852
|
+
await page.close().catch(() => {
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
results.forEach((result, index) => {
|
|
2857
|
+
const job = jobs[index];
|
|
2858
|
+
if (result.file) print(result.file);
|
|
2859
|
+
else print(`${red("\u2717")} ${job.key}: no screenshot`);
|
|
2860
|
+
for (const error of result.errors) {
|
|
2861
|
+
failed = true;
|
|
2862
|
+
print(` ${red(`${job.key}:`)} ${error.split("\n")[0]}`);
|
|
2863
|
+
}
|
|
2864
|
+
});
|
|
2865
|
+
return failed;
|
|
2866
|
+
}
|
|
2867
|
+
async function shootSystem(paths, project, id2, options) {
|
|
2868
|
+
const system = project.system();
|
|
2869
|
+
if (!system) throw new CliError("This project has no design system yet (.design/system).");
|
|
2870
|
+
if (options.overview || options.page) throw new CliError("--overview and --page apply to canvases, not @system");
|
|
2871
|
+
const theme2 = options.theme ?? "light";
|
|
2872
|
+
const all = systemFrames(system.doc, "http://placeholder", theme2);
|
|
2873
|
+
if (id2 && !all.some((job) => job.id === id2)) {
|
|
2874
|
+
throw new CliError(`No specimen "${id2}" in the design system. Specimens: ${all.map((job) => job.id).join(", ")}`);
|
|
2875
|
+
}
|
|
2876
|
+
const outDir = path17.join(path17.resolve(options.out ?? paths.shots), SYSTEM_CANVAS);
|
|
2877
|
+
fs17.mkdirSync(outDir, { recursive: true });
|
|
2878
|
+
const base = await serverBase(paths);
|
|
2879
|
+
const jobs = systemFrames(system.doc, base, theme2).filter((job) => !id2 || job.id === id2);
|
|
2880
|
+
const browser = await launchChrome();
|
|
2881
|
+
try {
|
|
2882
|
+
if (await shootFrames(browser, jobs, outDir)) process.exitCode = 1;
|
|
2883
|
+
} finally {
|
|
2884
|
+
await browser.close();
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/cli/commands/status.ts
|
|
2889
|
+
var plural2 = (count, word) => `${count} ${word}${count === 1 ? "" : "s"}`;
|
|
2890
|
+
async function runStatus(paths) {
|
|
2891
|
+
const project = new DesignProject(paths, DEV_URLS);
|
|
2892
|
+
const server = await liveServer(paths);
|
|
2893
|
+
const label = (text) => bold(text.padEnd(9));
|
|
2894
|
+
print(
|
|
2895
|
+
server ? `${label("Preview")}${green("running")} ${server.url} ${dim(`(pid ${server.pid}, since ${new Date(server.startedAt).toLocaleString()})`)}` : `${label("Preview")}${dim("not running \u2014 `design preview` starts it")}`
|
|
2896
|
+
);
|
|
2897
|
+
print(`${label("Project")}${project.name()} ${dim(paths.root)}`);
|
|
2898
|
+
const system = project.system();
|
|
2899
|
+
if (system) {
|
|
2900
|
+
const { doc } = system;
|
|
2901
|
+
print(
|
|
2902
|
+
`${label("System")}${doc.name} ${dim(
|
|
2903
|
+
`\u2014 ${plural2(doc.tokens.length, "token")}, ${plural2(doc.components.length, "component")}, ${plural2(doc.guidelines.length, "guideline")}`
|
|
2904
|
+
)}`
|
|
2905
|
+
);
|
|
2906
|
+
} else print(`${label("System")}${dim("none \u2014 `design system init` scaffolds one")}`);
|
|
2907
|
+
const canvases = project.canvases();
|
|
2908
|
+
if (!canvases.length) print(`${label("Canvases")}${dim("none \u2014 `design new <canvas>` scaffolds one")}`);
|
|
2909
|
+
else {
|
|
2910
|
+
print(label("Canvases"));
|
|
2911
|
+
const width = Math.max(...canvases.map((canvas) => canvas.doc.id.length));
|
|
2912
|
+
for (const { doc } of canvases) {
|
|
2913
|
+
const items = doc.pages.flatMap((page) => page.sections.flatMap((section) => section.items));
|
|
2914
|
+
const frames = items.filter((item) => item.kind === "screen" || item.kind === "url").length;
|
|
2915
|
+
const errors2 = doc.issues.filter((issue) => issue.severity === "error").length;
|
|
2916
|
+
const url = server ? dim(` ${server.url}/c/${doc.id}`) : "";
|
|
2917
|
+
print(
|
|
2918
|
+
` ${doc.id.padEnd(width)} ${doc.title} ${dim(`${plural2(doc.pages.length, "page")} \xB7 ${plural2(frames, "screen")}`)}` + (errors2 ? ` ${red(plural2(errors2, "error"))}` : "") + url
|
|
2919
|
+
);
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
const issues = project.issues();
|
|
2923
|
+
const errors = issues.filter((issue) => issue.severity === "error").length;
|
|
2924
|
+
const warnings = issues.length - errors;
|
|
2925
|
+
const summary = `${plural2(errors, "error")}, ${plural2(warnings, "warning")}`;
|
|
2926
|
+
print(
|
|
2927
|
+
`${label("Issues")}${errors ? red(summary) : warnings ? yellow(summary) : green(summary)}` + (issues.length ? dim(" \u2014 `design check` lists them") : "")
|
|
2928
|
+
);
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
// src/cli/index.ts
|
|
2932
|
+
var HELP = `${bold("foss-design")} ${dim(`v${packageVersion()}`)} \u2014 design canvases and design systems in .design
|
|
2933
|
+
|
|
2934
|
+
${bold("Usage")} design <command> [options]
|
|
2935
|
+
|
|
2936
|
+
${bold("Project")}
|
|
2937
|
+
init [--name <name>] [--no-gitignore] Create .design and add it to .gitignore
|
|
2938
|
+
system init [--name <name>] [--empty] Scaffold .design/system: tokens, a guideline, a component
|
|
2939
|
+
new <canvas> [--title <title>] [--empty] Scaffold .design/canvas/<canvas>
|
|
2940
|
+
|
|
2941
|
+
${bold("Preview")}
|
|
2942
|
+
preview [--open [path]] [--port <n>] Start the viewer in the background on a free port (or reuse
|
|
2943
|
+
it) and print its URL; --open also opens a browser
|
|
2944
|
+
[--restart] [--foreground]
|
|
2945
|
+
stop Stop the viewer
|
|
2946
|
+
status Viewer state and a project summary
|
|
2947
|
+
|
|
2948
|
+
${bold("Verify")}
|
|
2949
|
+
check [canvas\u2026] [--render] [--json] Validate canvas.json and the system; --render loads every
|
|
2950
|
+
screen in Chrome and reports runtime errors
|
|
2951
|
+
shot <canvas>[/<screen>] [--page <id>] Screenshot screens with Chrome (PNG paths are printed);
|
|
2952
|
+
[--theme light|dark] [--out <dir>] @system or @system/<id> shoots the design system specimens
|
|
2953
|
+
[--overview]
|
|
2954
|
+
|
|
2955
|
+
${bold("Share")}
|
|
2956
|
+
build [canvas\u2026] [--out <dir>] [--tar] Static site of the canvases and the design system
|
|
2957
|
+
|
|
2958
|
+
${bold("Options")}
|
|
2959
|
+
--root <dir> Project folder (default: the nearest folder with .design, or the current one)
|
|
2960
|
+
-h, --help This help
|
|
2961
|
+
-v, --version Version
|
|
2962
|
+
`;
|
|
2963
|
+
function projectPaths(rootOption, requireDesign = true) {
|
|
2964
|
+
const found = rootOption ? path18.resolve(rootOption) : findProjectRoot();
|
|
2965
|
+
if (!found) {
|
|
2966
|
+
if (!requireDesign) return designPaths(process.cwd());
|
|
2967
|
+
throw new CliError("No .design folder here or above. Run `design init` in the project root first.");
|
|
2968
|
+
}
|
|
2969
|
+
const paths = designPaths(found);
|
|
2970
|
+
return paths;
|
|
2971
|
+
}
|
|
2972
|
+
async function main(argv) {
|
|
2973
|
+
const { values, positionals } = parseArgs({
|
|
2974
|
+
args: argv,
|
|
2975
|
+
allowPositionals: true,
|
|
2976
|
+
strict: true,
|
|
2977
|
+
options: {
|
|
2978
|
+
root: { type: "string" },
|
|
2979
|
+
name: { type: "string" },
|
|
2980
|
+
title: { type: "string" },
|
|
2981
|
+
"no-gitignore": { type: "boolean" },
|
|
2982
|
+
empty: { type: "boolean" },
|
|
2983
|
+
open: { type: "boolean" },
|
|
2984
|
+
port: { type: "string" },
|
|
2985
|
+
restart: { type: "boolean" },
|
|
2986
|
+
foreground: { type: "boolean" },
|
|
2987
|
+
render: { type: "boolean" },
|
|
2988
|
+
json: { type: "boolean" },
|
|
2989
|
+
page: { type: "string" },
|
|
2990
|
+
theme: { type: "string" },
|
|
2991
|
+
out: { type: "string" },
|
|
2992
|
+
overview: { type: "boolean" },
|
|
2993
|
+
tar: { type: "boolean" },
|
|
2994
|
+
help: { type: "boolean", short: "h" },
|
|
2995
|
+
version: { type: "boolean", short: "v" }
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
const [command, ...rest] = positionals;
|
|
2999
|
+
if (values.version) return print(packageVersion());
|
|
3000
|
+
if (values.help || !command || command === "help") return print(HELP);
|
|
3001
|
+
const port = values.port ? Number(values.port) : void 0;
|
|
3002
|
+
if (port !== void 0 && !Number.isInteger(port)) throw new CliError("--port takes a number");
|
|
3003
|
+
const theme2 = values.theme === "dark" ? "dark" : values.theme === "light" ? "light" : void 0;
|
|
3004
|
+
if (values.theme && !theme2) throw new CliError("--theme is light or dark");
|
|
3005
|
+
switch (command) {
|
|
3006
|
+
case "init":
|
|
3007
|
+
return runInit(values.root ? path18.resolve(values.root) : process.cwd(), {
|
|
3008
|
+
name: values.name,
|
|
3009
|
+
gitignore: !values["no-gitignore"]
|
|
3010
|
+
});
|
|
3011
|
+
case "system": {
|
|
3012
|
+
if (rest[0] !== "init") throw new CliError("Usage: design system init [--name <name>] [--empty]");
|
|
3013
|
+
return runSystemInit(projectPaths(values.root).root, { name: values.name, empty: !!values.empty });
|
|
3014
|
+
}
|
|
3015
|
+
case "new": {
|
|
3016
|
+
if (!rest[0]) throw new CliError("Usage: design new <canvas> [--title <title>]");
|
|
3017
|
+
return runNew(projectPaths(values.root).root, rest[0], { title: values.title, empty: !!values.empty });
|
|
3018
|
+
}
|
|
3019
|
+
case "preview": {
|
|
3020
|
+
const openPath = rest[0]?.startsWith("/") ? rest[0] : void 0;
|
|
3021
|
+
return runPreview(projectPaths(values.root), {
|
|
3022
|
+
open: values.open ? openPath ?? true : false,
|
|
3023
|
+
port,
|
|
3024
|
+
foreground: !!values.foreground,
|
|
3025
|
+
restart: !!values.restart
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
case "stop":
|
|
3029
|
+
return runStop(projectPaths(values.root));
|
|
3030
|
+
case "status":
|
|
3031
|
+
return runStatus(projectPaths(values.root));
|
|
3032
|
+
case "check":
|
|
3033
|
+
return runCheck(projectPaths(values.root), rest, { render: !!values.render, json: !!values.json });
|
|
3034
|
+
case "shot": {
|
|
3035
|
+
if (!rest[0]) throw new CliError("Usage: design shot <canvas>[/<screen>] [--page <id>] [--theme light|dark]");
|
|
3036
|
+
return runShot(projectPaths(values.root), rest[0], {
|
|
3037
|
+
page: values.page,
|
|
3038
|
+
theme: theme2,
|
|
3039
|
+
out: values.out,
|
|
3040
|
+
overview: !!values.overview
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
case "build":
|
|
3044
|
+
return runBuild(projectPaths(values.root), rest, { out: values.out, tar: !!values.tar });
|
|
3045
|
+
default:
|
|
3046
|
+
throw new CliError(`Unknown command "${command}". Run \`design --help\`.`);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
3050
|
+
if (error instanceof CliError) {
|
|
3051
|
+
process.stderr.write(`${red("error")} ${error.message}
|
|
3052
|
+
`);
|
|
3053
|
+
process.exit(error.code);
|
|
3054
|
+
}
|
|
3055
|
+
if (error.code?.startsWith("ERR_PARSE_ARGS")) {
|
|
3056
|
+
process.stderr.write(`${red("error")} ${error.message}
|
|
3057
|
+
Run \`design --help\`.
|
|
3058
|
+
`);
|
|
3059
|
+
process.exit(2);
|
|
3060
|
+
}
|
|
3061
|
+
process.stderr.write(`${red("error")} ${error?.stack ?? String(error)}
|
|
3062
|
+
`);
|
|
3063
|
+
process.exit(1);
|
|
3064
|
+
});
|