pattern-dump 0.1.0-alpha.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 +8 -0
- package/README.md +92 -0
- package/dist/dev-ui/main.css +1825 -0
- package/dist/dev-ui/main.js +3054 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/pd.js +4759 -0
- package/dist/types/config/index.d.ts +145 -0
- package/dist/types/contracts/dev-protocol.d.ts +624 -0
- package/dist/types/contracts/index.d.ts +294 -0
- package/package.json +76 -0
- package/templates/base/README.md +77 -0
- package/templates/base/_gitignore +3 -0
- package/templates/base/pattern-dump.config.ts +36 -0
- package/templates/base/src/scripts/globals.d.ts +6 -0
- package/templates/base/src/styles/base/_tokens.scss +8 -0
- package/templates/base/src/styles/base/document.scss +18 -0
- package/templates/base/themes/dark.theme.scss +8 -0
- package/templates/base/themes/light.theme.scss +8 -0
- package/templates/base/tsconfig.json +19 -0
- package/templates/examples/src/patterns/atoms/button/button.hbs +1 -0
- package/templates/examples/src/patterns/atoms/button/button.json +1 -0
- package/templates/examples/src/patterns/atoms/button/button.md +21 -0
- package/templates/examples/src/patterns/atoms/button/button.schema.json +11 -0
- package/templates/examples/src/patterns/atoms/button/button~icon.hbs +5 -0
- package/templates/examples/src/patterns/atoms/button/button~secondary.json +1 -0
- package/templates/examples/src/patterns/atoms/index.md +6 -0
- package/templates/examples/src/patterns/index.md +3 -0
- package/templates/examples/src/patterns/molecules/index.md +6 -0
- package/templates/examples/src/patterns/molecules/search-field/input/input.hbs +4 -0
- package/templates/examples/src/patterns/molecules/search-field/input/input.json +1 -0
- package/templates/examples/src/patterns/molecules/search-field/input/input.md +8 -0
- package/templates/examples/src/patterns/molecules/search-field/input/input.schema.json +10 -0
- package/templates/examples/src/patterns/molecules/search-field/search-field.hbs +5 -0
- package/templates/examples/src/patterns/molecules/search-field/search-field.json +1 -0
- package/templates/examples/src/patterns/molecules/search-field/search-field.md +21 -0
- package/templates/examples/src/patterns/molecules/search-field/search-field.schema.json +11 -0
- package/templates/examples/src/patterns/pages/example/example.hbs +11 -0
- package/templates/examples/src/patterns/pages/example/example.json +8 -0
- package/templates/examples/src/patterns/pages/example/example.md +11 -0
- package/templates/examples/src/patterns/pages/example/example.schema.json +14 -0
- package/templates/examples/src/patterns/pages/index.md +6 -0
- package/templates/examples/src/scripts/search-field.ts +16 -0
- package/templates/examples/src/styles/atoms/button.scss +36 -0
- package/templates/examples/src/styles/molecules/search-field/input.scss +21 -0
- package/templates/examples/src/styles/molecules/search-field.scss +13 -0
- package/templates/examples/src/styles/pages/example.scss +28 -0
package/dist/pd.js
ADDED
|
@@ -0,0 +1,4759 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// packages/discovery/src/index.ts
|
|
4
|
+
import { readdir } from "node:fs/promises";
|
|
5
|
+
import { join, basename, dirname, relative } from "node:path";
|
|
6
|
+
function globToRegExp(glob) {
|
|
7
|
+
let re = "";
|
|
8
|
+
for (let i = 0; i < glob.length; i++) {
|
|
9
|
+
const ch = glob[i];
|
|
10
|
+
if (ch === "*" && glob[i + 1] === "*") {
|
|
11
|
+
if (glob[i + 2] === "/") {
|
|
12
|
+
re += "(?:.*/)?";
|
|
13
|
+
i += 2;
|
|
14
|
+
} else {
|
|
15
|
+
re += ".*";
|
|
16
|
+
i += 1;
|
|
17
|
+
}
|
|
18
|
+
} else if (ch === "*") re += "[^/]*";
|
|
19
|
+
else if (ch === "?") re += "[^/]";
|
|
20
|
+
else re += /[.+^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
21
|
+
}
|
|
22
|
+
return new RegExp(`^${re}$`);
|
|
23
|
+
}
|
|
24
|
+
function collapseComponentId(relNoExt) {
|
|
25
|
+
const stem = basename(relNoExt);
|
|
26
|
+
const parent = dirname(relNoExt);
|
|
27
|
+
return stem === basename(parent) ? parent : relNoExt;
|
|
28
|
+
}
|
|
29
|
+
function deriveHandle(id) {
|
|
30
|
+
const parts = id.split("/");
|
|
31
|
+
return `${parts[0]}-${parts[parts.length - 1]}`;
|
|
32
|
+
}
|
|
33
|
+
async function buildDiscoveryMap(roots) {
|
|
34
|
+
const rootList = typeof roots === "string" ? [{ dir: roots }] : roots;
|
|
35
|
+
const allHbsFiles = [];
|
|
36
|
+
const allVarJsonFiles = [];
|
|
37
|
+
const rootOf = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const [index, root] of rootList.entries()) {
|
|
39
|
+
const excludes = (root.exclude ?? []).map(globToRegExp);
|
|
40
|
+
const excluded = (abs, isDir) => {
|
|
41
|
+
if (!excludes.length) return false;
|
|
42
|
+
const rel = relative(root.dir, abs).split("\\").join("/");
|
|
43
|
+
return excludes.some((re) => re.test(rel) || isDir && re.test(`${rel}/`));
|
|
44
|
+
};
|
|
45
|
+
const hbs = [];
|
|
46
|
+
const varJson = [];
|
|
47
|
+
const collectFiles = async (dir) => {
|
|
48
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
49
|
+
for (const e of entries) {
|
|
50
|
+
if (e.name.startsWith(".")) continue;
|
|
51
|
+
const full = join(dir, e.name);
|
|
52
|
+
if (excluded(full, e.isDirectory())) continue;
|
|
53
|
+
if (e.isDirectory()) {
|
|
54
|
+
await collectFiles(full);
|
|
55
|
+
} else if (e.name.endsWith(".hbs")) {
|
|
56
|
+
hbs.push(full);
|
|
57
|
+
} else if (e.name.includes("~") && e.name.endsWith(".json")) {
|
|
58
|
+
varJson.push(full);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const topEntries = await readdir(root.dir, { withFileTypes: true }).catch(() => []);
|
|
63
|
+
for (const e of topEntries) {
|
|
64
|
+
const full = join(root.dir, e.name);
|
|
65
|
+
if (e.isDirectory() && !e.name.startsWith(".") && !excluded(full, true)) await collectFiles(full);
|
|
66
|
+
}
|
|
67
|
+
hbs.sort();
|
|
68
|
+
varJson.sort();
|
|
69
|
+
for (const f of [...hbs, ...varJson]) rootOf.set(f, { root, index });
|
|
70
|
+
allHbsFiles.push(...hbs);
|
|
71
|
+
allVarJsonFiles.push(...varJson);
|
|
72
|
+
}
|
|
73
|
+
const rootDir = (abs) => rootOf.get(abs).root.dir;
|
|
74
|
+
const dirFileSets = /* @__PURE__ */ new Map();
|
|
75
|
+
const allDirs = new Set([...allHbsFiles, ...allVarJsonFiles].map((fp) => dirname(fp)));
|
|
76
|
+
await Promise.all([...allDirs].map(async (dir) => {
|
|
77
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
78
|
+
dirFileSets.set(dir, new Set(entries.filter((e) => !e.isDirectory()).map((e) => e.name)));
|
|
79
|
+
}));
|
|
80
|
+
const discoveryMap = /* @__PURE__ */ new Map();
|
|
81
|
+
const handleMap = /* @__PURE__ */ new Map();
|
|
82
|
+
const collisions = [];
|
|
83
|
+
const overrides = [];
|
|
84
|
+
function deriveId(absPath) {
|
|
85
|
+
return collapseComponentId(relative(rootDir(absPath), absPath).replace(/\.hbs$/, ""));
|
|
86
|
+
}
|
|
87
|
+
function buildEntry(absPath) {
|
|
88
|
+
const dir = dirname(absPath);
|
|
89
|
+
const stem = basename(absPath, ".hbs");
|
|
90
|
+
const fileSet = dirFileSets.get(dir) ?? /* @__PURE__ */ new Set();
|
|
91
|
+
const source = rootOf.get(absPath)?.root.source;
|
|
92
|
+
return {
|
|
93
|
+
...source ? { source } : {},
|
|
94
|
+
hbsPath: absPath,
|
|
95
|
+
jsonPath: fileSet.has(`${stem}.json`) ? join(dir, `${stem}.json`) : void 0,
|
|
96
|
+
mdPath: fileSet.has(`${stem}.md`) ? join(dir, `${stem}.md`) : void 0,
|
|
97
|
+
schemaPath: fileSet.has(`${stem}.schema.json`) ? join(dir, `${stem}.schema.json`) : void 0,
|
|
98
|
+
ownMarkup: true
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function registerEntry(id, entry, absPath) {
|
|
102
|
+
const handle = deriveHandle(id);
|
|
103
|
+
if (handleMap.has(handle)) {
|
|
104
|
+
const incumbentId = handleMap.get(handle);
|
|
105
|
+
const incumbentPath = discoveryMap.get(incumbentId).hbsPath;
|
|
106
|
+
const incumbentRoot = rootOf.get(incumbentPath)?.index ?? 0;
|
|
107
|
+
const newcomerRoot = rootOf.get(absPath)?.index ?? 0;
|
|
108
|
+
if (newcomerRoot > incumbentRoot) {
|
|
109
|
+
overrides.push({
|
|
110
|
+
handle,
|
|
111
|
+
id: incumbentId,
|
|
112
|
+
path: incumbentPath,
|
|
113
|
+
shadowedPath: absPath,
|
|
114
|
+
shadowedSource: rootOf.get(absPath).root.source ?? ""
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
collisions.push({ handle, incumbentId, incumbentPath, quarantinedPath: absPath, quarantinedId: id });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
discoveryMap.set(id, entry);
|
|
122
|
+
handleMap.set(handle, id);
|
|
123
|
+
}
|
|
124
|
+
for (const absPath of allHbsFiles) {
|
|
125
|
+
if (basename(absPath, ".hbs").includes("~")) continue;
|
|
126
|
+
registerEntry(deriveId(absPath), buildEntry(absPath), absPath);
|
|
127
|
+
}
|
|
128
|
+
const variantHbsByBase = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const absPath of allHbsFiles) {
|
|
130
|
+
const stem = basename(absPath, ".hbs");
|
|
131
|
+
if (!stem.includes("~")) continue;
|
|
132
|
+
const dir = dirname(absPath);
|
|
133
|
+
const baseStem = stem.split("~")[0];
|
|
134
|
+
const relBase = relative(rootDir(absPath), join(dir, baseStem));
|
|
135
|
+
const parentRel = dirname(relBase);
|
|
136
|
+
const baseId = baseStem === basename(parentRel) ? parentRel : relBase;
|
|
137
|
+
const arr = variantHbsByBase.get(baseId) ?? [];
|
|
138
|
+
arr.push(absPath);
|
|
139
|
+
variantHbsByBase.set(baseId, arr);
|
|
140
|
+
}
|
|
141
|
+
for (const [baseId, varPaths] of variantHbsByBase) {
|
|
142
|
+
const hasBase = discoveryMap.has(baseId);
|
|
143
|
+
if (!hasBase) {
|
|
144
|
+
const variantTokens = varPaths.map((fp) => basename(fp, ".hbs").split("~")[1]);
|
|
145
|
+
const handle = deriveHandle(baseId);
|
|
146
|
+
if (!handleMap.has(handle)) {
|
|
147
|
+
const varDir = dirname(varPaths[0]);
|
|
148
|
+
const baseName = basename(varPaths[0], ".hbs").split("~")[0];
|
|
149
|
+
const fileSet = dirFileSets.get(varDir) ?? /* @__PURE__ */ new Set();
|
|
150
|
+
const mdPath = fileSet.has(`${baseName}.md`) ? join(varDir, `${baseName}.md`) : void 0;
|
|
151
|
+
const source = rootOf.get(varPaths[0])?.root.source;
|
|
152
|
+
discoveryMap.set(baseId, { ...source ? { source } : {}, hbsPath: "", ownMarkup: false, variants: variantTokens, mdPath });
|
|
153
|
+
handleMap.set(handle, baseId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const absPath of varPaths) {
|
|
157
|
+
const stem = basename(absPath, ".hbs");
|
|
158
|
+
const variantToken = stem.split("~")[1];
|
|
159
|
+
const variantId = `${baseId}~${variantToken}`;
|
|
160
|
+
const variantHandle = `${deriveHandle(baseId)}~${variantToken}`;
|
|
161
|
+
if (!handleMap.has(variantHandle)) {
|
|
162
|
+
discoveryMap.set(variantId, buildEntry(absPath));
|
|
163
|
+
handleMap.set(variantHandle, variantId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const jsonPath of allVarJsonFiles) {
|
|
168
|
+
const jsonStem = basename(jsonPath, ".json");
|
|
169
|
+
const parts = jsonStem.split("~");
|
|
170
|
+
const baseName = parts[0];
|
|
171
|
+
const variantToken = parts[1];
|
|
172
|
+
const dir = dirname(jsonPath);
|
|
173
|
+
const relBase = relative(rootDir(jsonPath), join(dir, baseName));
|
|
174
|
+
const parentRel = dirname(relBase);
|
|
175
|
+
const baseId = baseName === basename(parentRel) ? parentRel : relBase;
|
|
176
|
+
const variantId = `${baseId}~${variantToken}`;
|
|
177
|
+
if (discoveryMap.has(variantId)) continue;
|
|
178
|
+
const baseEntry = discoveryMap.get(baseId);
|
|
179
|
+
if (!baseEntry?.hbsPath) continue;
|
|
180
|
+
const variantHandle = `${deriveHandle(baseId)}~${variantToken}`;
|
|
181
|
+
if (!handleMap.has(variantHandle)) {
|
|
182
|
+
const source = rootOf.get(jsonPath)?.root.source;
|
|
183
|
+
discoveryMap.set(variantId, { ...source ? { source } : {}, hbsPath: baseEntry.hbsPath, jsonPath, ownMarkup: false });
|
|
184
|
+
handleMap.set(variantHandle, variantId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const hbsPathToId = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const [id, entry] of discoveryMap) {
|
|
189
|
+
if (entry.hbsPath) hbsPathToId.set(entry.hbsPath, id);
|
|
190
|
+
}
|
|
191
|
+
const quarantineMap = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const col of collisions) {
|
|
193
|
+
quarantineMap.set(col.quarantinedId, col);
|
|
194
|
+
}
|
|
195
|
+
return { discoveryMap, handleMap, collisions, hbsPathToId, quarantineMap, overrides };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// packages/renderer/src/highlight.ts
|
|
199
|
+
import { createHighlighter } from "shiki";
|
|
200
|
+
import { transformerStyleToClass } from "@shikijs/transformers";
|
|
201
|
+
|
|
202
|
+
// packages/renderer/src/themes/oklch.ts
|
|
203
|
+
import { rgb, formatHex, inGamut, clampChroma } from "culori";
|
|
204
|
+
var inRGB = inGamut("rgb");
|
|
205
|
+
function oklchHex(i) {
|
|
206
|
+
const col = { mode: "oklch", l: i.l, c: i.c, h: i.h };
|
|
207
|
+
const ok = inRGB(rgb(col));
|
|
208
|
+
const hex = formatHex(clampChroma(col, "oklch", "rgb"));
|
|
209
|
+
return { hex, inGamut: ok };
|
|
210
|
+
}
|
|
211
|
+
function computePalette(themeName, intents) {
|
|
212
|
+
const out = {};
|
|
213
|
+
const bad = [];
|
|
214
|
+
for (const key of Object.keys(intents)) {
|
|
215
|
+
const e = oklchHex(intents[key]);
|
|
216
|
+
if (!e.inGamut) bad.push(`${key} (${e.hex})`);
|
|
217
|
+
out[key] = e.hex;
|
|
218
|
+
}
|
|
219
|
+
if (bad.length) {
|
|
220
|
+
throw new Error(
|
|
221
|
+
`[${themeName}] OKLCH tokens out of sRGB gamut (would clamp/lie): ${bad.join(", ")}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// packages/renderer/src/themes/mcm-dark.ts
|
|
228
|
+
var INTENTS = {
|
|
229
|
+
tag: { l: 0.76, c: 0.11, h: 190 },
|
|
230
|
+
// teal — HTML tag names
|
|
231
|
+
keyword: { l: 0.78, c: 0.145, h: 78 },
|
|
232
|
+
// gold-amber — keywords/storage + Handlebars #each/#if/else
|
|
233
|
+
attr: { l: 0.72, c: 0.09, h: 100 },
|
|
234
|
+
// khaki — attribute names (the quiet rung)
|
|
235
|
+
string: { l: 0.88, c: 0.15, h: 122 },
|
|
236
|
+
// olive — strings (brightest, the CVD "up" rung)
|
|
237
|
+
hbExpr: { l: 0.68, c: 0.155, h: 48 },
|
|
238
|
+
// terracotta — {{expression}} identifier
|
|
239
|
+
constant: { l: 0.74, c: 0.1, h: 265 },
|
|
240
|
+
// periwinkle — constants/numbers (distinct cool rung)
|
|
241
|
+
comment: { l: 0.55, c: 0.025, h: 70 },
|
|
242
|
+
// warm-grey — comments
|
|
243
|
+
base: { l: 0.86, c: 0.015, h: 80 }
|
|
244
|
+
// warm off-white — base fg / punctuation / muted
|
|
245
|
+
};
|
|
246
|
+
var c = computePalette("pd-mcm-dark", INTENTS);
|
|
247
|
+
var mcmDark = {
|
|
248
|
+
name: "pd-mcm-dark",
|
|
249
|
+
type: "dark",
|
|
250
|
+
colors: {
|
|
251
|
+
"editor.background": "#22292E",
|
|
252
|
+
"editor.foreground": c.base
|
|
253
|
+
},
|
|
254
|
+
// Ordered least→most specific; Shiki resolves by longest matching scope, so the Handlebars
|
|
255
|
+
// overrides below win over the general `support.constant` / `variable` rules.
|
|
256
|
+
tokenColors: [
|
|
257
|
+
{ scope: ["comment", "comment.block.handlebars", "punctuation.definition.comment"], settings: { foreground: c.comment } },
|
|
258
|
+
{ scope: ["string", "punctuation.definition.string", "string.quoted"], settings: { foreground: c.string } },
|
|
259
|
+
{ scope: ["keyword", "keyword.control", "storage", "storage.type", "storage.modifier"], settings: { foreground: c.keyword } },
|
|
260
|
+
{ scope: ["constant", "constant.numeric", "constant.language", "support.constant", "variable.other.constant", "constant.character"], settings: { foreground: c.constant } },
|
|
261
|
+
{ scope: ["entity.name.tag", "entity.name.tag.html", "entity.name.tag.block.any.html", "entity.name.tag.inline.any.html"], settings: { foreground: c.tag } },
|
|
262
|
+
{ scope: ["entity.other.attribute-name", "entity.other.attribute-name.html"], settings: { foreground: c.attr } },
|
|
263
|
+
{ scope: ["variable", "support.other.variable"], settings: { foreground: c.base } },
|
|
264
|
+
{ scope: ["punctuation.definition.tag", "meta.tag", "keyword.operator", "punctuation.separator", "punctuation.terminator"], settings: { foreground: c.base } },
|
|
265
|
+
// Handlebars {{expression}} identifier — the terracotta token (more specific than `variable`).
|
|
266
|
+
{ scope: ["variable.parameter.handlebars"], settings: { foreground: c.hbExpr } },
|
|
267
|
+
// Handlebars {{ }} delimiters — muted to base (more specific than `support.constant`).
|
|
268
|
+
{ scope: ["support.constant.handlebars"], settings: { foreground: c.base } }
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// packages/renderer/src/themes/monokai-light.ts
|
|
273
|
+
var CONTROLLED = {
|
|
274
|
+
varParam: { l: 0.74, c: 0.14, h: 58 },
|
|
275
|
+
// lightened args — breaks the deuteranopia collapse → #ec9247
|
|
276
|
+
hbExpr: { l: 0.62, c: 0.15, h: 48 }
|
|
277
|
+
// {{expression}} light terracotta → #cc6526
|
|
278
|
+
};
|
|
279
|
+
var c2 = computePalette("pd-monokai-light", CONTROLLED);
|
|
280
|
+
var M = {
|
|
281
|
+
base: "#49483e",
|
|
282
|
+
// base/global fg + variables + punctuation + {{ }} delimiters
|
|
283
|
+
comment: "#75715E",
|
|
284
|
+
string: "#998f2f",
|
|
285
|
+
constant: "#684d99",
|
|
286
|
+
// numeric / language / other constants
|
|
287
|
+
keyword: "#f9005a",
|
|
288
|
+
// keyword / storage / tag (hot pink — loud, cool; flagged for warm-tune)
|
|
289
|
+
support: "#0089b3",
|
|
290
|
+
// storage.type / support.function/constant/type (teal-blue)
|
|
291
|
+
name: "#679c00"
|
|
292
|
+
// class/type/function names + attribute names (olive-green)
|
|
293
|
+
};
|
|
294
|
+
var monokaiLight = {
|
|
295
|
+
name: "pd-monokai-light",
|
|
296
|
+
type: "light",
|
|
297
|
+
colors: {
|
|
298
|
+
"editor.background": "#fafafa",
|
|
299
|
+
"editor.foreground": M.base
|
|
300
|
+
},
|
|
301
|
+
// Ordered least→most specific; the Handlebars overrides win by longest-scope match.
|
|
302
|
+
tokenColors: [
|
|
303
|
+
{ scope: ["comment", "comment.block.handlebars", "punctuation.definition.comment"], settings: { foreground: M.comment } },
|
|
304
|
+
{ scope: ["string", "punctuation.definition.string", "string.quoted"], settings: { foreground: M.string } },
|
|
305
|
+
{ scope: ["keyword", "keyword.control", "storage", "storage.modifier", "entity.name.tag", "entity.name.tag.html", "entity.name.tag.block.any.html", "entity.name.tag.inline.any.html"], settings: { foreground: M.keyword } },
|
|
306
|
+
{ scope: ["storage.type", "support", "support.function", "support.constant", "support.type", "support.class"], settings: { foreground: M.support } },
|
|
307
|
+
{ scope: ["constant", "constant.numeric", "constant.language", "constant.character", "variable.other.constant"], settings: { foreground: M.constant } },
|
|
308
|
+
{ scope: ["entity.name.class", "entity.name.type", "entity.name.function", "entity.other.attribute-name", "entity.other.attribute-name.html"], settings: { foreground: M.name } },
|
|
309
|
+
{ scope: ["variable.parameter"], settings: { foreground: c2.varParam } },
|
|
310
|
+
// lightened (CVD)
|
|
311
|
+
{ scope: ["variable", "support.other.variable"], settings: { foreground: M.base } },
|
|
312
|
+
{ scope: ["punctuation.definition.tag", "meta.tag", "keyword.operator", "punctuation.separator", "punctuation.terminator"], settings: { foreground: M.base } },
|
|
313
|
+
// Handlebars {{expression}} identifier — light terracotta (more specific than variable.parameter).
|
|
314
|
+
{ scope: ["variable.parameter.handlebars"], settings: { foreground: c2.hbExpr } },
|
|
315
|
+
// Handlebars {{ }} delimiters — muted to base (more specific than support.constant).
|
|
316
|
+
{ scope: ["support.constant.handlebars"], settings: { foreground: M.base } }
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// packages/renderer/src/highlight.ts
|
|
321
|
+
var SUPPORTED_LANGS = [
|
|
322
|
+
"handlebars",
|
|
323
|
+
"html",
|
|
324
|
+
"json",
|
|
325
|
+
"scss",
|
|
326
|
+
"css",
|
|
327
|
+
"typescript",
|
|
328
|
+
"javascript",
|
|
329
|
+
"markdown"
|
|
330
|
+
];
|
|
331
|
+
var LANG_ALIASES = {
|
|
332
|
+
hbs: "handlebars",
|
|
333
|
+
ts: "typescript",
|
|
334
|
+
js: "javascript",
|
|
335
|
+
md: "markdown"
|
|
336
|
+
};
|
|
337
|
+
var LANG_SET = /* @__PURE__ */ new Set([...SUPPORTED_LANGS, ...Object.keys(LANG_ALIASES)]);
|
|
338
|
+
var _highlighter = null;
|
|
339
|
+
var _transformer = transformerStyleToClass();
|
|
340
|
+
async function initHighlighter() {
|
|
341
|
+
if (_highlighter) return;
|
|
342
|
+
_highlighter = await createHighlighter({
|
|
343
|
+
// Custom pair: light = Monokai-Light-derived, dark = warm MCM. Dual-variable output via
|
|
344
|
+
// defaultColor:false and the OS-bound @media switch are unchanged (drop-in theme objects).
|
|
345
|
+
themes: [monokaiLight, mcmDark],
|
|
346
|
+
langs: [...SUPPORTED_LANGS]
|
|
347
|
+
});
|
|
348
|
+
const WARMUP = {
|
|
349
|
+
handlebars: `{{> header}}
|
|
350
|
+
<div class="{{mod}}">
|
|
351
|
+
{{#each items}}<span data-id="{{id}}">{{title}}</span>{{/each}}
|
|
352
|
+
{{#if ok}}yes{{else}}no{{/if}}
|
|
353
|
+
</div>`,
|
|
354
|
+
typescript: `import { x } from "./y";
|
|
355
|
+
// comment
|
|
356
|
+
const s: string = "hi";
|
|
357
|
+
type T = { n: number; ok: boolean };
|
|
358
|
+
function f(a: number): T | null { return a > 0 ? { n: a, ok: true } : null; }`,
|
|
359
|
+
scss: `.a { color: #fff; font-size: 1rem; &:hover { background: var(--c); } }
|
|
360
|
+
@mixin m($v: 0) { margin: $v; }
|
|
361
|
+
@media (max-width: 768px) { .a { display: none; } }`,
|
|
362
|
+
json: `{"k":"v","n":42,"b":true,"x":null,"a":[1,"two",false]}`,
|
|
363
|
+
html: `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"></head><body class="c"><!-- note --><p data-x="1">hi</p></body></html>`
|
|
364
|
+
};
|
|
365
|
+
for (const [lang, code] of Object.entries(WARMUP)) {
|
|
366
|
+
_highlightRaw(code, lang);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function highlight(code, lang) {
|
|
370
|
+
return _highlightRaw(code, lang);
|
|
371
|
+
}
|
|
372
|
+
function getShikiCSS() {
|
|
373
|
+
const vars = _transformer.getCSS();
|
|
374
|
+
const base2 = [
|
|
375
|
+
".shiki{background-color:var(--shiki-light-bg);overflow-x:auto}",
|
|
376
|
+
".shiki code{display:block;padding:.75em 1em;font-family:monospace;font-size:1rem;line-height:1.6;white-space:pre;tab-size:2}",
|
|
377
|
+
".shiki span{color:var(--shiki-light)}",
|
|
378
|
+
"@media(prefers-color-scheme:dark){",
|
|
379
|
+
".shiki{background-color:var(--shiki-dark-bg)}",
|
|
380
|
+
".shiki span{color:var(--shiki-dark)}",
|
|
381
|
+
"}"
|
|
382
|
+
].join("");
|
|
383
|
+
return vars + base2;
|
|
384
|
+
}
|
|
385
|
+
function wrapWithCopyButton(highlightedHtml, rawCode) {
|
|
386
|
+
const escaped = rawCode.replace(/&/g, "&").replace(/"/g, """);
|
|
387
|
+
return `<div class="shiki-container" data-raw="${escaped}"><button class="copy-btn" type="button" aria-label="Copy source">Copy</button>${highlightedHtml}</div>`;
|
|
388
|
+
}
|
|
389
|
+
function _highlightRaw(code, lang) {
|
|
390
|
+
const h = _highlighter;
|
|
391
|
+
if (!h) return _plainPre(code);
|
|
392
|
+
const resolved = LANG_ALIASES[lang] ?? lang;
|
|
393
|
+
if (!LANG_SET.has(lang) && !LANG_SET.has(resolved)) {
|
|
394
|
+
return _plainPre(code);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
return h.codeToHtml(code, {
|
|
398
|
+
lang: resolved,
|
|
399
|
+
themes: { light: "pd-monokai-light", dark: "pd-mcm-dark" },
|
|
400
|
+
defaultColor: false,
|
|
401
|
+
transformers: [_transformer]
|
|
402
|
+
});
|
|
403
|
+
} catch {
|
|
404
|
+
return _plainPre(code);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function _plainPre(code) {
|
|
408
|
+
const esc = code.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
409
|
+
return `<pre class="shiki shiki-plain"><code>${esc}</code></pre>`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// packages/renderer/src/index.ts
|
|
413
|
+
import { HtmlValidate } from "html-validate";
|
|
414
|
+
import Handlebars from "handlebars";
|
|
415
|
+
import { readFile } from "node:fs/promises";
|
|
416
|
+
var htmlValidator = new HtmlValidate({
|
|
417
|
+
rules: {
|
|
418
|
+
"element-permitted-content": "error",
|
|
419
|
+
"element-permitted-parent": "error"
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
function deepMerge(target, ...sources) {
|
|
423
|
+
for (const source of sources) {
|
|
424
|
+
if (!source) continue;
|
|
425
|
+
for (const [key, value] of Object.entries(source)) {
|
|
426
|
+
const targetVal = target[key];
|
|
427
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value) && targetVal !== null && typeof targetVal === "object" && !Array.isArray(targetVal)) {
|
|
428
|
+
deepMerge(targetVal, value);
|
|
429
|
+
} else {
|
|
430
|
+
target[key] = value;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return target;
|
|
435
|
+
}
|
|
436
|
+
var safePart = (n) => n.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
|
|
437
|
+
function registerHelpers(env, mintId, getLastId, projectHelpers, helperRegistrars) {
|
|
438
|
+
env.registerHelper("id", (name) => mintId(name));
|
|
439
|
+
env.registerHelper("getId", (name) => mintId(name));
|
|
440
|
+
env.registerHelper("getLastId", () => getLastId());
|
|
441
|
+
env.registerHelper("getLastNumericId", () => {
|
|
442
|
+
const id = getLastId();
|
|
443
|
+
const m = id.match(/(\d+)$/);
|
|
444
|
+
return m ? m[1] : "0";
|
|
445
|
+
});
|
|
446
|
+
env.registerHelper("eq", (a, b) => a === b);
|
|
447
|
+
env.registerHelper("ne", (a, b) => a !== b);
|
|
448
|
+
env.registerHelper("lt", (a, b) => a < b);
|
|
449
|
+
env.registerHelper("gt", (a, b) => a > b);
|
|
450
|
+
env.registerHelper("lte", (a, b) => a <= b);
|
|
451
|
+
env.registerHelper("gte", (a, b) => a >= b);
|
|
452
|
+
env.registerHelper("and", (a, b) => Boolean(a) && Boolean(b));
|
|
453
|
+
env.registerHelper("or", (a, b) => Boolean(a) || Boolean(b));
|
|
454
|
+
env.registerHelper("not", (a) => !a);
|
|
455
|
+
env.registerHelper("isdefined", (v) => v !== void 0 && v !== null);
|
|
456
|
+
env.registerHelper(
|
|
457
|
+
"defaultValue",
|
|
458
|
+
(v, fallback) => v !== void 0 && v !== null ? v : fallback
|
|
459
|
+
);
|
|
460
|
+
env.registerHelper("ifCond", function(a, op, b, options) {
|
|
461
|
+
let result = false;
|
|
462
|
+
switch (op) {
|
|
463
|
+
case "==":
|
|
464
|
+
case "===":
|
|
465
|
+
result = a === b;
|
|
466
|
+
break;
|
|
467
|
+
case "!=":
|
|
468
|
+
case "!==":
|
|
469
|
+
result = a !== b;
|
|
470
|
+
break;
|
|
471
|
+
case "<":
|
|
472
|
+
result = a < b;
|
|
473
|
+
break;
|
|
474
|
+
case ">":
|
|
475
|
+
result = a > b;
|
|
476
|
+
break;
|
|
477
|
+
case "<=":
|
|
478
|
+
result = a <= b;
|
|
479
|
+
break;
|
|
480
|
+
case ">=":
|
|
481
|
+
result = a >= b;
|
|
482
|
+
break;
|
|
483
|
+
case "&&":
|
|
484
|
+
result = Boolean(a) && Boolean(b);
|
|
485
|
+
break;
|
|
486
|
+
case "||":
|
|
487
|
+
result = Boolean(a) || Boolean(b);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
return result ? options.fn(this) : options.inverse(this);
|
|
491
|
+
});
|
|
492
|
+
if (projectHelpers) {
|
|
493
|
+
for (const [name, fn] of Object.entries(projectHelpers)) {
|
|
494
|
+
env.registerHelper(name, fn);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (helperRegistrars) {
|
|
498
|
+
for (const register of helperRegistrars) {
|
|
499
|
+
try {
|
|
500
|
+
register(env);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
console.warn("[renderer] project helper registrar failed:", e);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
var createRenderer = (options) => {
|
|
508
|
+
return async (input) => {
|
|
509
|
+
const { patternsDir } = options;
|
|
510
|
+
let discoveryMap = options.discoveryMap;
|
|
511
|
+
let handleMap = options.handleMap;
|
|
512
|
+
if (!discoveryMap || !handleMap) {
|
|
513
|
+
const maps = await buildDiscoveryMap(options.patternRoots ?? patternsDir);
|
|
514
|
+
discoveryMap ??= maps.discoveryMap;
|
|
515
|
+
handleMap ??= maps.handleMap;
|
|
516
|
+
}
|
|
517
|
+
const entry = discoveryMap.get(input.component);
|
|
518
|
+
if (!entry) throw new Error(`[renderer] Component not found: "${input.component}"`);
|
|
519
|
+
let templateHbsPath = entry.hbsPath;
|
|
520
|
+
let variantJsonPath;
|
|
521
|
+
if (input.variant) {
|
|
522
|
+
const variantEntry = discoveryMap.get(`${input.component}~${input.variant}`);
|
|
523
|
+
if (!variantEntry) throw new Error(`[renderer] Variant not found: "${input.component}~${input.variant}"`);
|
|
524
|
+
if (variantEntry.ownMarkup && variantEntry.hbsPath) {
|
|
525
|
+
templateHbsPath = variantEntry.hbsPath;
|
|
526
|
+
}
|
|
527
|
+
variantJsonPath = variantEntry.jsonPath;
|
|
528
|
+
}
|
|
529
|
+
const templateSrc = templateHbsPath ? await readFile(templateHbsPath, "utf-8") : "";
|
|
530
|
+
let defaultData = {};
|
|
531
|
+
if (entry.jsonPath) {
|
|
532
|
+
try {
|
|
533
|
+
defaultData = JSON.parse(await readFile(entry.jsonPath, "utf-8"));
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
let variantData;
|
|
538
|
+
if (variantJsonPath) {
|
|
539
|
+
variantData = JSON.parse(await readFile(variantJsonPath, "utf-8"));
|
|
540
|
+
}
|
|
541
|
+
const data = deepMerge(
|
|
542
|
+
structuredClone(options.globalData ?? {}),
|
|
543
|
+
defaultData,
|
|
544
|
+
variantData,
|
|
545
|
+
input.data
|
|
546
|
+
);
|
|
547
|
+
const env = Handlebars.create();
|
|
548
|
+
const componentSlug = safePart(input.component.replace(/[\\/]/g, "-"));
|
|
549
|
+
let currentBase = `pd-${componentSlug}`;
|
|
550
|
+
const idCache = /* @__PURE__ */ new Map();
|
|
551
|
+
let lastMintedId = "";
|
|
552
|
+
const mintId = (name) => {
|
|
553
|
+
const key = `${currentBase}:${String(name)}`;
|
|
554
|
+
if (!idCache.has(key)) {
|
|
555
|
+
idCache.set(key, `${currentBase}-${safePart(String(name))}`);
|
|
556
|
+
}
|
|
557
|
+
const id = idCache.get(key);
|
|
558
|
+
lastMintedId = id;
|
|
559
|
+
return id;
|
|
560
|
+
};
|
|
561
|
+
const invocationCounters = /* @__PURE__ */ new Map();
|
|
562
|
+
registerHelpers(env, mintId, () => lastMintedId, options.helpers, options.helperRegistrars);
|
|
563
|
+
for (const [flatName, compId] of handleMap) {
|
|
564
|
+
const compEntry = discoveryMap.get(compId);
|
|
565
|
+
if (!compEntry?.hbsPath) continue;
|
|
566
|
+
try {
|
|
567
|
+
const partialSrc = await readFile(compEntry.hbsPath, "utf-8");
|
|
568
|
+
let childDefaults = {};
|
|
569
|
+
if (compEntry.jsonPath) {
|
|
570
|
+
try {
|
|
571
|
+
childDefaults = JSON.parse(await readFile(compEntry.jsonPath, "utf-8"));
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const childTemplate = env.compile(partialSrc);
|
|
576
|
+
const capturedDefaults = childDefaults;
|
|
577
|
+
const partialSlug = safePart(compId.replace(/[\\/]/g, "-"));
|
|
578
|
+
env.registerPartial(
|
|
579
|
+
flatName,
|
|
580
|
+
function(context, opts) {
|
|
581
|
+
const suppliedBase = opts?.hash?.base != null ? safePart(String(opts.hash.base)) : null;
|
|
582
|
+
let thisBase;
|
|
583
|
+
if (suppliedBase !== null) {
|
|
584
|
+
thisBase = suppliedBase;
|
|
585
|
+
} else {
|
|
586
|
+
const counterKey = `${flatName}:${currentBase}`;
|
|
587
|
+
const n = invocationCounters.get(counterKey) ?? 0;
|
|
588
|
+
invocationCounters.set(counterKey, n + 1);
|
|
589
|
+
thisBase = n === 0 ? currentBase : `pd-${partialSlug}-${n}`;
|
|
590
|
+
}
|
|
591
|
+
const savedBase = currentBase;
|
|
592
|
+
currentBase = thisBase;
|
|
593
|
+
const merged = deepMerge(
|
|
594
|
+
structuredClone(capturedDefaults),
|
|
595
|
+
context != null && typeof context === "object" && !Array.isArray(context) ? context : {}
|
|
596
|
+
);
|
|
597
|
+
const runtime = opts?.fn ? { partials: { ...env.partials, "@partial-block": opts.fn } } : void 0;
|
|
598
|
+
const result = childTemplate(merged, runtime);
|
|
599
|
+
currentBase = savedBase;
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const html = env.compile(templateSrc)(data, {
|
|
607
|
+
partials: { ...env.partials, "@partial-block": () => "" }
|
|
608
|
+
});
|
|
609
|
+
const diagnostics = [];
|
|
610
|
+
const htmlReport = htmlValidator.validateStringSync(html);
|
|
611
|
+
for (const result of htmlReport.results) {
|
|
612
|
+
for (const msg of result.messages) {
|
|
613
|
+
diagnostics.push({
|
|
614
|
+
kind: "HTML_INVALID",
|
|
615
|
+
severity: "warning",
|
|
616
|
+
source: "html",
|
|
617
|
+
message: msg.message,
|
|
618
|
+
// The specific html-validate rule (e.g. "element-permitted-content") — the granular rule id
|
|
619
|
+
// the diagnostics graph keys its rule nodes on, distinct from the coarse "HTML_INVALID" kind.
|
|
620
|
+
...msg.ruleId ? { ruleId: msg.ruleId } : {},
|
|
621
|
+
...msg.line != null ? { location: { line: msg.line, column: msg.column } } : {}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
if (!entry.schemaPath) throw new Error("no schema");
|
|
627
|
+
const schemaText = await readFile(entry.schemaPath, "utf-8");
|
|
628
|
+
const schema = JSON.parse(schemaText);
|
|
629
|
+
for (const key of schema.required ?? []) {
|
|
630
|
+
if (data[key] === void 0 || data[key] === null) {
|
|
631
|
+
diagnostics.push({
|
|
632
|
+
kind: "UNFILLED_REQUIRED",
|
|
633
|
+
severity: "error",
|
|
634
|
+
source: "schema",
|
|
635
|
+
message: `Required field "${key}" is absent in the merged data`,
|
|
636
|
+
path: `#/properties/${key}`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
for (const [key, def] of Object.entries(schema.properties ?? {})) {
|
|
641
|
+
const val = data[key];
|
|
642
|
+
if (val === void 0 || val === null || !def.type) continue;
|
|
643
|
+
const types = Array.isArray(def.type) ? def.type : [def.type];
|
|
644
|
+
const jsType = typeof val;
|
|
645
|
+
const jsonType = jsType === "number" ? Number.isInteger(val) ? "integer" : "number" : jsType;
|
|
646
|
+
if (!types.includes(jsonType) && !types.includes(jsType)) {
|
|
647
|
+
diagnostics.push({
|
|
648
|
+
kind: "SHAPE_MISMATCH",
|
|
649
|
+
severity: "warning",
|
|
650
|
+
source: "schema",
|
|
651
|
+
message: `Field "${key}" expected ${types.join(" | ")} but got ${jsType}`,
|
|
652
|
+
path: `#/properties/${key}`
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
const meta = diagnostics.length > 0 ? { componentId: input.component, variant: input.variant, diagnostics } : void 0;
|
|
659
|
+
return { html, document: buildDocument(html, options), ...meta ? { meta } : {} };
|
|
660
|
+
};
|
|
661
|
+
};
|
|
662
|
+
function buildDocument(html, options) {
|
|
663
|
+
const bodyAttr = options.defaultTheme ? ` data-theme="${options.defaultTheme}"` : "";
|
|
664
|
+
return `<!DOCTYPE html>
|
|
665
|
+
<html lang="en">
|
|
666
|
+
<head>
|
|
667
|
+
<meta charset="UTF-8">
|
|
668
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
669
|
+
</head>
|
|
670
|
+
<body${bodyAttr}>
|
|
671
|
+
${html}
|
|
672
|
+
</body>
|
|
673
|
+
</html>`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// packages/build/src/index.ts
|
|
677
|
+
import * as sass from "sass";
|
|
678
|
+
import postcss from "postcss";
|
|
679
|
+
import autoprefixer from "autoprefixer";
|
|
680
|
+
import { build as esbuildBuild } from "esbuild";
|
|
681
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
682
|
+
import { join as join2, extname, resolve } from "node:path";
|
|
683
|
+
import { fileURLToPath } from "node:url";
|
|
684
|
+
async function collectScssFiles(dir) {
|
|
685
|
+
const files = [];
|
|
686
|
+
async function walk(current) {
|
|
687
|
+
let entries;
|
|
688
|
+
try {
|
|
689
|
+
entries = await readdir2(current, { withFileTypes: true });
|
|
690
|
+
} catch {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
for (const entry of entries) {
|
|
694
|
+
const full = join2(current, entry.name);
|
|
695
|
+
if (entry.isDirectory()) {
|
|
696
|
+
await walk(full);
|
|
697
|
+
} else if (extname(entry.name) === ".scss" && !entry.name.startsWith("_")) {
|
|
698
|
+
files.push(full);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
await walk(dir);
|
|
703
|
+
return files;
|
|
704
|
+
}
|
|
705
|
+
async function compileSassFileWithDeps(filePath) {
|
|
706
|
+
const result = sass.compile(filePath);
|
|
707
|
+
const processed = await postcss([autoprefixer]).process(result.css, { from: void 0 });
|
|
708
|
+
const deps = result.loadedUrls.filter((u) => u.protocol === "file:").map((u) => fileURLToPath(u)).filter((p) => resolve(p) !== resolve(filePath));
|
|
709
|
+
return { css: processed.css, deps };
|
|
710
|
+
}
|
|
711
|
+
async function compileSassFile(filePath) {
|
|
712
|
+
const { css } = await compileSassFileWithDeps(filePath);
|
|
713
|
+
return css;
|
|
714
|
+
}
|
|
715
|
+
async function createSassDependencyTracker(dirs) {
|
|
716
|
+
const sourceFiles = [];
|
|
717
|
+
for (const dir of dirs) {
|
|
718
|
+
sourceFiles.push(...await collectScssFiles(dir));
|
|
719
|
+
}
|
|
720
|
+
const perFileCss = /* @__PURE__ */ new Map();
|
|
721
|
+
const reverseDeps = /* @__PURE__ */ new Map();
|
|
722
|
+
function addDeps(sourceFile, deps) {
|
|
723
|
+
for (const dep of deps) {
|
|
724
|
+
const abs = resolve(dep);
|
|
725
|
+
if (!reverseDeps.has(abs)) reverseDeps.set(abs, /* @__PURE__ */ new Set());
|
|
726
|
+
reverseDeps.get(abs).add(sourceFile);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function removeDeps(sourceFile) {
|
|
730
|
+
for (const dependents of reverseDeps.values()) {
|
|
731
|
+
dependents.delete(sourceFile);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const file of sourceFiles) {
|
|
735
|
+
try {
|
|
736
|
+
const { css, deps } = await compileSassFileWithDeps(file);
|
|
737
|
+
perFileCss.set(file, css);
|
|
738
|
+
addDeps(file, deps);
|
|
739
|
+
} catch (e) {
|
|
740
|
+
console.warn(`[build] SCSS compile error in ${file}:`, e);
|
|
741
|
+
perFileCss.set(file, "");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const combineCss = () => sourceFiles.map((f) => perFileCss.get(f) ?? "").join("\n");
|
|
745
|
+
return {
|
|
746
|
+
get css() {
|
|
747
|
+
return combineCss();
|
|
748
|
+
},
|
|
749
|
+
async update(changedFile) {
|
|
750
|
+
const abs = resolve(changedFile);
|
|
751
|
+
const filesToRecompile = /* @__PURE__ */ new Set();
|
|
752
|
+
if (perFileCss.has(abs)) filesToRecompile.add(abs);
|
|
753
|
+
const dependents = reverseDeps.get(abs);
|
|
754
|
+
if (dependents) {
|
|
755
|
+
for (const dep of dependents) filesToRecompile.add(dep);
|
|
756
|
+
}
|
|
757
|
+
const recompiledFiles = [];
|
|
758
|
+
const errors = [];
|
|
759
|
+
for (const file of filesToRecompile) {
|
|
760
|
+
try {
|
|
761
|
+
const { css, deps } = await compileSassFileWithDeps(file);
|
|
762
|
+
perFileCss.set(file, css);
|
|
763
|
+
removeDeps(file);
|
|
764
|
+
addDeps(file, deps);
|
|
765
|
+
recompiledFiles.push(file);
|
|
766
|
+
} catch (e) {
|
|
767
|
+
console.warn(`[build] SCSS compile error in ${file}:`, e);
|
|
768
|
+
errors.push({ file, message: e instanceof Error ? e.message : String(e) });
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return { css: combineCss(), recompiledFiles, errors };
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
async function compileScriptFile(entryPoint) {
|
|
776
|
+
const result = await esbuildBuild({
|
|
777
|
+
entryPoints: [entryPoint],
|
|
778
|
+
bundle: true,
|
|
779
|
+
format: "esm",
|
|
780
|
+
platform: "browser",
|
|
781
|
+
write: false,
|
|
782
|
+
sourcemap: false
|
|
783
|
+
});
|
|
784
|
+
return result.outputFiles[0].text;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// packages/hotreloader/src/index.ts
|
|
788
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
789
|
+
|
|
790
|
+
// packages/contracts/src/dev-protocol.ts
|
|
791
|
+
var SCOPE_BOUNDARY = `## Scope \u2014 the boundary is part of the artifact
|
|
792
|
+
|
|
793
|
+
**Covers** \u2014 machine-detectable, static, single-render (no browser layout):
|
|
794
|
+
- **HTML content-model** (html-validate) \u2014 nesting/permitted-content violations.
|
|
795
|
+
- **Structural a11y** (axe, alwaysOn tier) \u2014 labels, names, ARIA, roles, image-alt.
|
|
796
|
+
- **Schema** \u2014 required/shape violations against \`component.schema.json\`.
|
|
797
|
+
- **Render outcome** \u2014 blank / error / ok (metadata, never itself a defect).
|
|
798
|
+
|
|
799
|
+
**Deliberately NOT covered \u2014 named boundaries, not misses:**
|
|
800
|
+
- **Rendered-contrast (axe needsRendering tier)** \u2014 contrast, aria-hidden-focus, duplicate-id-aria.
|
|
801
|
+
These require real browser layout/paint; the static jsdom sweep cannot produce them. The
|
|
802
|
+
per-component panel shows them (live iframe); this aggregate does not. Fewer rule types here is a
|
|
803
|
+
scope boundary, not a gap.
|
|
804
|
+
- **Structural/graph findings** \u2014 dead subtrees, a shared-base "component", flattened variant families.
|
|
805
|
+
Those are the style graph's lens, not this one.
|
|
806
|
+
- **PD reporting gaps** \u2014 PD under-claiming (e.g. missing provenance) is not a subject defect.
|
|
807
|
+
- **Interactive / behavioural** \u2014 focus traps, keyboard flow, escape handling. Single static render only.
|
|
808
|
+
- **Data-masked** \u2014 a component that renders empty for want of data won't throw the violations it would
|
|
809
|
+
when populated; findings are annotated with render outcome wherever it is blank.
|
|
810
|
+
|
|
811
|
+
A component with zero findings is **not "clean" \u2014 it is "clean of static machine-detectable defects."**
|
|
812
|
+
That is the honest claim; never "clean".`;
|
|
813
|
+
function buildAuditRollup(graph, opts = {}) {
|
|
814
|
+
const total = graph.components.length;
|
|
815
|
+
const affected = new Set(graph.occurrences.map((o) => o.component));
|
|
816
|
+
const cleanPct = total > 0 ? Math.round(100 * (total - affected.size) / total) : 100;
|
|
817
|
+
const outcomes = graph.components.reduce((a, c3) => {
|
|
818
|
+
a[c3.render] = (a[c3.render] ?? 0) + 1;
|
|
819
|
+
return a;
|
|
820
|
+
}, {});
|
|
821
|
+
const perRule = /* @__PURE__ */ new Map();
|
|
822
|
+
for (const o of graph.occurrences) {
|
|
823
|
+
const k = `${o.engine} ${o.ruleId}`;
|
|
824
|
+
const r = perRule.get(k) ?? { engine: o.engine, ruleId: o.ruleId, occ: 0, comps: /* @__PURE__ */ new Set(), tags: /* @__PURE__ */ new Set() };
|
|
825
|
+
r.occ += 1;
|
|
826
|
+
r.comps.add(o.component);
|
|
827
|
+
r.tags.add(o.tag);
|
|
828
|
+
perRule.set(k, r);
|
|
829
|
+
}
|
|
830
|
+
const rulesByBlast = [...perRule.values()].sort((a, b) => b.comps.size - a.comps.size || b.occ - a.occ || `${a.engine}:${a.ruleId}`.localeCompare(`${b.engine}:${b.ruleId}`)).map((r) => ({ engine: r.engine, ruleId: r.ruleId, components: r.comps.size, occurrences: r.occ, tags: [...r.tags].sort() }));
|
|
831
|
+
const occByComp = /* @__PURE__ */ new Map();
|
|
832
|
+
for (const o of graph.occurrences) {
|
|
833
|
+
const a = occByComp.get(o.component) ?? [];
|
|
834
|
+
a.push(o);
|
|
835
|
+
occByComp.set(o.component, a);
|
|
836
|
+
}
|
|
837
|
+
const perComponent = [];
|
|
838
|
+
for (const c3 of graph.components) {
|
|
839
|
+
const occ = occByComp.get(c3.id) ?? [];
|
|
840
|
+
if (occ.length === 0 && c3.render === "ok") continue;
|
|
841
|
+
perComponent.push({
|
|
842
|
+
id: c3.id,
|
|
843
|
+
render: c3.render,
|
|
844
|
+
occurrences: [...occ].sort((a, b) => `${a.engine}:${a.ruleId}`.localeCompare(`${b.engine}:${b.ruleId}`)).map((o) => ({ engine: o.engine, ruleId: o.ruleId, tag: o.tag, message: o.message, ...o.domPath ? { domPath: o.domPath } : {} }))
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
...opts.subjectSha ? { subjectSha: opts.subjectSha } : {},
|
|
849
|
+
headline: `${graph.occurrences.length} machine-detectable static defects across ${affected.size} of ${total} components \u2014 ${cleanPct}% clean of static machine-detectable defects.`,
|
|
850
|
+
totals: { components: total, affected: affected.size, cleanPct, defects: graph.occurrences.length },
|
|
851
|
+
renderOutcomes: { ok: outcomes["ok"] ?? 0, blank: outcomes["blank"] ?? 0, error: outcomes["error"] ?? 0 },
|
|
852
|
+
rulesByBlast,
|
|
853
|
+
perComponent,
|
|
854
|
+
scope: SCOPE_BOUNDARY
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
function previewUrl(id, variant, theme) {
|
|
858
|
+
const params = new URLSearchParams();
|
|
859
|
+
if (variant) params.set("variant", variant);
|
|
860
|
+
if (theme) params.set("theme", theme);
|
|
861
|
+
const q = params.toString() ? `?${params.toString()}` : "";
|
|
862
|
+
return `${PD_ROUTE_PREFIX.preview}${id}${q}`;
|
|
863
|
+
}
|
|
864
|
+
function renderedSourceUrl(id, variant) {
|
|
865
|
+
const q = variant ? `?variant=${encodeURIComponent(variant)}` : "";
|
|
866
|
+
return `${PD_ROUTE_PREFIX.source}${id}/rendered${q}`;
|
|
867
|
+
}
|
|
868
|
+
function dataSourceUrl(id, variant) {
|
|
869
|
+
const q = variant ? `?variant=${encodeURIComponent(variant)}` : "";
|
|
870
|
+
return `${PD_ROUTE_PREFIX.source}${id}/data${q}`;
|
|
871
|
+
}
|
|
872
|
+
function overviewUrl(path) {
|
|
873
|
+
return `${PD_ROUTE_PREFIX.overview}${path}`;
|
|
874
|
+
}
|
|
875
|
+
var PD_ROUTES = {
|
|
876
|
+
/** GET → ComponentTree */
|
|
877
|
+
tree: "/api/tree",
|
|
878
|
+
/** GET → AuditResponse (500 → ApiError) */
|
|
879
|
+
audit: "/api/audit",
|
|
880
|
+
/** GET → ThemeSummary[] */
|
|
881
|
+
themes: "/api/themes",
|
|
882
|
+
/** GET → VersionInfo */
|
|
883
|
+
version: "/api/version",
|
|
884
|
+
/** GET → ShellClientConfig */
|
|
885
|
+
config: "/api/config",
|
|
886
|
+
/** The shell bundle, stylesheet and vendored assets. */
|
|
887
|
+
mainJs: "/_pd/main.js",
|
|
888
|
+
mainCss: "/_pd/main.css",
|
|
889
|
+
axeCore: "/_pd/axe-core.js",
|
|
890
|
+
/** GET → RuleTiers */
|
|
891
|
+
ruleTiers: "/_pd/rule-tiers.json",
|
|
892
|
+
shikiCss: "/_pd/shiki.css",
|
|
893
|
+
/** Document-base layer (reset + font-face). */
|
|
894
|
+
baseCss: "/_pd/base.css"
|
|
895
|
+
};
|
|
896
|
+
var PD_ROUTE_PREFIX = {
|
|
897
|
+
component: "/api/component/",
|
|
898
|
+
overview: "/api/overview/",
|
|
899
|
+
preview: "/preview/",
|
|
900
|
+
source: "/source/",
|
|
901
|
+
componentCss: "/_pd/components/",
|
|
902
|
+
themeCss: "/_pd/themes/",
|
|
903
|
+
script: "/_pd/scripts/",
|
|
904
|
+
assets: "/assets/"
|
|
905
|
+
};
|
|
906
|
+
function componentUrl(id, variant) {
|
|
907
|
+
const q = variant ? `?variant=${encodeURIComponent(variant)}` : "";
|
|
908
|
+
return `${PD_ROUTE_PREFIX.component}${id}${q}`;
|
|
909
|
+
}
|
|
910
|
+
function componentCssUrl(id, version) {
|
|
911
|
+
const q = version !== void 0 ? `?v=${version}` : "";
|
|
912
|
+
return `${PD_ROUTE_PREFIX.componentCss}${id}.css${q}`;
|
|
913
|
+
}
|
|
914
|
+
function themeCssUrl(name) {
|
|
915
|
+
return `${PD_ROUTE_PREFIX.themeCss}${encodeURIComponent(name)}.css`;
|
|
916
|
+
}
|
|
917
|
+
function scriptUrl(id, version) {
|
|
918
|
+
const q = version !== void 0 ? `?v=${version}` : "";
|
|
919
|
+
return `${PD_ROUTE_PREFIX.script}${encodeURIComponent(id)}.js${q}`;
|
|
920
|
+
}
|
|
921
|
+
function normalizeBasePath(basePath) {
|
|
922
|
+
const trimmed = (basePath ?? "").trim().replace(/^\/+|\/+$/g, "");
|
|
923
|
+
return trimmed ? `/${trimmed}/` : "/";
|
|
924
|
+
}
|
|
925
|
+
function staticPathFor(liveUrl) {
|
|
926
|
+
const url = new URL(liveUrl, "http://pd.invalid");
|
|
927
|
+
const path = url.pathname;
|
|
928
|
+
const variant = url.searchParams.get("variant") || void 0;
|
|
929
|
+
const suffix = (ext) => variant ? `~${variant}${ext}` : `index${ext}`;
|
|
930
|
+
const under = (prefix) => path.slice(prefix.length);
|
|
931
|
+
if (path.startsWith(PD_ROUTE_PREFIX.component)) return `api/component/${under(PD_ROUTE_PREFIX.component)}/${suffix(".json")}`;
|
|
932
|
+
if (path.startsWith(PD_ROUTE_PREFIX.overview)) return `api/overview/${under(PD_ROUTE_PREFIX.overview)}/index.json`;
|
|
933
|
+
if (path.startsWith("/api/")) return `${path.slice(1)}.json`;
|
|
934
|
+
if (path.startsWith(PD_ROUTE_PREFIX.preview)) return `preview/${under(PD_ROUTE_PREFIX.preview)}/${suffix(".html")}`;
|
|
935
|
+
if (path.startsWith(PD_ROUTE_PREFIX.source)) {
|
|
936
|
+
const rest = under(PD_ROUTE_PREFIX.source);
|
|
937
|
+
const facet = rest.endsWith("/rendered") ? "rendered" : rest.endsWith("/data") ? "data" : null;
|
|
938
|
+
if (facet) {
|
|
939
|
+
const id = rest.slice(0, -(facet.length + 1));
|
|
940
|
+
return `source/${id}/${facet}${variant ? `~${variant}` : ""}.html`;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (path.startsWith("/_pd/") || path.startsWith(PD_ROUTE_PREFIX.assets)) return path.slice(1);
|
|
944
|
+
const route = path.replace(/^\/+|\/+$/g, "");
|
|
945
|
+
return route ? `${route}/index.html` : "index.html";
|
|
946
|
+
}
|
|
947
|
+
var PD_PROTOCOL_VERSION = 1;
|
|
948
|
+
var PD_PROTOCOL_HEADER = "X-PD-Protocol";
|
|
949
|
+
|
|
950
|
+
// packages/hotreloader/src/index.ts
|
|
951
|
+
function createHotReloader(port2 = Number(process.env["PD_WS_PORT"] ?? 4001)) {
|
|
952
|
+
const wss = new WebSocketServer({ port: port2 });
|
|
953
|
+
const clients = /* @__PURE__ */ new Set();
|
|
954
|
+
const listening = new Promise((resolve13, reject) => {
|
|
955
|
+
wss.once("listening", () => resolve13());
|
|
956
|
+
wss.once("error", reject);
|
|
957
|
+
});
|
|
958
|
+
listening.catch(() => {
|
|
959
|
+
});
|
|
960
|
+
wss.on("error", () => {
|
|
961
|
+
});
|
|
962
|
+
let retained = [];
|
|
963
|
+
wss.on("connection", (ws) => {
|
|
964
|
+
clients.add(ws);
|
|
965
|
+
ws.on("close", () => clients.delete(ws));
|
|
966
|
+
ws.on("error", () => clients.delete(ws));
|
|
967
|
+
const hello = { type: "hello", protocol: PD_PROTOCOL_VERSION };
|
|
968
|
+
ws.send(JSON.stringify(hello));
|
|
969
|
+
for (const message of retained) ws.send(JSON.stringify(message));
|
|
970
|
+
});
|
|
971
|
+
return {
|
|
972
|
+
port: port2,
|
|
973
|
+
listening,
|
|
974
|
+
push(message) {
|
|
975
|
+
const data = JSON.stringify(message);
|
|
976
|
+
for (const client of clients) {
|
|
977
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
978
|
+
client.send(data);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
retain(messages) {
|
|
983
|
+
retained = [...messages];
|
|
984
|
+
},
|
|
985
|
+
close() {
|
|
986
|
+
return new Promise((resolve13, reject) => wss.close((err) => err ? reject(err) : resolve13()));
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// packages/dev-server/src/index.ts
|
|
992
|
+
import { createServer } from "node:http";
|
|
993
|
+
import { stat as stat5 } from "node:fs/promises";
|
|
994
|
+
import { join as join19 } from "node:path";
|
|
995
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
996
|
+
|
|
997
|
+
// packages/dev-server/src/composition.ts
|
|
998
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
999
|
+
|
|
1000
|
+
// packages/dev-server/src/hbs-partials.ts
|
|
1001
|
+
function parsePartialsFromHbs(hbs) {
|
|
1002
|
+
const partials = /* @__PURE__ */ new Set();
|
|
1003
|
+
for (const [, name] of hbs.matchAll(/\{\{#?>\s*"([^"]+)"/g)) {
|
|
1004
|
+
partials.add(name);
|
|
1005
|
+
}
|
|
1006
|
+
for (const [, name] of hbs.matchAll(/\{\{#?>\s*([a-zA-Z][a-zA-Z0-9-]*)/g)) {
|
|
1007
|
+
partials.add(name);
|
|
1008
|
+
}
|
|
1009
|
+
return [...partials];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// packages/dev-server/src/composition.ts
|
|
1013
|
+
async function buildCompositionRegistry(patternsDir, flatNameToPath, discoveryMap) {
|
|
1014
|
+
const uses = /* @__PURE__ */ new Map();
|
|
1015
|
+
const usedBy = /* @__PURE__ */ new Map();
|
|
1016
|
+
for (const id of flatNameToPath.values()) {
|
|
1017
|
+
if (!uses.has(id)) uses.set(id, []);
|
|
1018
|
+
if (!usedBy.has(id)) usedBy.set(id, []);
|
|
1019
|
+
}
|
|
1020
|
+
for (const componentId of flatNameToPath.values()) {
|
|
1021
|
+
const hbsPath = discoveryMap.get(componentId)?.hbsPath ?? "";
|
|
1022
|
+
let hbs = "";
|
|
1023
|
+
try {
|
|
1024
|
+
hbs = hbsPath ? await readFile2(hbsPath, "utf-8") : "";
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
const partialNames = parsePartialsFromHbs(hbs);
|
|
1028
|
+
const usedSet = /* @__PURE__ */ new Set();
|
|
1029
|
+
for (const partialName of partialNames) {
|
|
1030
|
+
const childId = flatNameToPath.get(partialName);
|
|
1031
|
+
if (childId && childId !== componentId) {
|
|
1032
|
+
usedSet.add(childId);
|
|
1033
|
+
const arr = usedBy.get(childId) ?? [];
|
|
1034
|
+
if (!arr.includes(componentId)) {
|
|
1035
|
+
arr.push(componentId);
|
|
1036
|
+
usedBy.set(childId, arr);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
uses.set(componentId, [...usedSet].sort());
|
|
1041
|
+
}
|
|
1042
|
+
for (const [k, v] of usedBy) usedBy.set(k, [...new Set(v)].sort());
|
|
1043
|
+
return { uses, usedBy };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// packages/dev-server/src/themes.ts
|
|
1047
|
+
import { readdir as readdir3, readFile as readFile3 } from "node:fs/promises";
|
|
1048
|
+
import { join as join3 } from "node:path";
|
|
1049
|
+
import postcss2 from "postcss";
|
|
1050
|
+
async function discoverThemeFiles(themesDirs) {
|
|
1051
|
+
const found = /* @__PURE__ */ new Map();
|
|
1052
|
+
for (const dir of Array.isArray(themesDirs) ? themesDirs : [themesDirs]) {
|
|
1053
|
+
const entries = await readdir3(dir, { withFileTypes: true }).catch(() => []);
|
|
1054
|
+
for (const e of entries) {
|
|
1055
|
+
if (e.isDirectory() || !e.name.endsWith(".theme.scss")) continue;
|
|
1056
|
+
const name = e.name.slice(0, -".theme.scss".length);
|
|
1057
|
+
if (!found.has(name)) found.set(name, dir);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return [...found].map(([name, dir]) => ({ name, dir }));
|
|
1061
|
+
}
|
|
1062
|
+
async function discoverThemes(themesDirs) {
|
|
1063
|
+
return (await discoverThemeFiles(themesDirs)).map(({ name }) => ({ name }));
|
|
1064
|
+
}
|
|
1065
|
+
async function extractThemeTokens(name, compiledCss, slots, themesDir) {
|
|
1066
|
+
const result = Object.fromEntries(slots.map((s) => [s, null]));
|
|
1067
|
+
const jsonPath = join3(themesDir, `${name}.theme.json`);
|
|
1068
|
+
try {
|
|
1069
|
+
const raw = JSON.parse(await readFile3(jsonPath, "utf-8"));
|
|
1070
|
+
for (const slot of slots) {
|
|
1071
|
+
const key = slot.replace(/^--/, "");
|
|
1072
|
+
if (key in raw) result[slot] = raw[key];
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
if (err.code !== "ENOENT") {
|
|
1077
|
+
console.warn(`[pd] extractThemeTokens: failed to parse ${jsonPath}:`, err);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const wanted = `[data-theme=${name}]`;
|
|
1081
|
+
const unquote2 = (selector) => selector.replace(/=\s*(["'])(.*?)\1\s*\]/g, "=$2]").replace(/\s+/g, "");
|
|
1082
|
+
const root = postcss2.parse(compiledCss);
|
|
1083
|
+
root.walkRules((rule) => {
|
|
1084
|
+
if (!rule.selectors.some((sel) => unquote2(sel) === wanted)) return;
|
|
1085
|
+
rule.walkDecls((decl) => {
|
|
1086
|
+
if (slots.includes(decl.prop)) result[decl.prop] = decl.value;
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
return result;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// packages/dev-server/src/discovery-index.ts
|
|
1093
|
+
import { resolve as resolve2 } from "node:path";
|
|
1094
|
+
function baseComponentIds(dr) {
|
|
1095
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1096
|
+
for (const id of dr.discoveryMap.keys()) {
|
|
1097
|
+
if (!id.includes("~")) ids.add(id);
|
|
1098
|
+
}
|
|
1099
|
+
return ids;
|
|
1100
|
+
}
|
|
1101
|
+
function resolveComponentForFile(filePath, dr) {
|
|
1102
|
+
const absPath = resolve2(filePath);
|
|
1103
|
+
const hbsId = dr.hbsPathToId.get(absPath);
|
|
1104
|
+
if (hbsId !== void 0) return hbsId;
|
|
1105
|
+
for (const [id, entry] of dr.discoveryMap) {
|
|
1106
|
+
if (entry.jsonPath === absPath) {
|
|
1107
|
+
return id.includes("~") ? id.split("~")[0] : id;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return null;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// packages/dev-server/src/project-loaders.ts
|
|
1114
|
+
import { readdir as readdir4, readFile as readFile4, stat } from "node:fs/promises";
|
|
1115
|
+
import { join as join4 } from "node:path";
|
|
1116
|
+
import { pathToFileURL } from "node:url";
|
|
1117
|
+
async function loadHelperRegistrars(helpersPath) {
|
|
1118
|
+
const registrars = [];
|
|
1119
|
+
let files = [];
|
|
1120
|
+
try {
|
|
1121
|
+
const st = await stat(helpersPath);
|
|
1122
|
+
if (st.isDirectory()) {
|
|
1123
|
+
const entries = await readdir4(helpersPath, { withFileTypes: true });
|
|
1124
|
+
files = entries.filter((e) => e.isFile() && /\.(c?js|mjs)$/.test(e.name) && !e.name.startsWith(".")).map((e) => join4(helpersPath, e.name)).sort();
|
|
1125
|
+
} else {
|
|
1126
|
+
files = [helpersPath];
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
console.warn(`[dev-server] helpers path not found: ${helpersPath}`);
|
|
1130
|
+
return registrars;
|
|
1131
|
+
}
|
|
1132
|
+
for (const file of files) {
|
|
1133
|
+
try {
|
|
1134
|
+
const mod = await import(pathToFileURL(file).href);
|
|
1135
|
+
const def = mod["default"] ?? mod;
|
|
1136
|
+
if (typeof def === "function") {
|
|
1137
|
+
registrars.push(def);
|
|
1138
|
+
} else if (def && typeof def === "object") {
|
|
1139
|
+
const named = Object.entries(def).filter(([, v]) => typeof v === "function");
|
|
1140
|
+
if (named.length) {
|
|
1141
|
+
registrars.push((hb) => {
|
|
1142
|
+
const h = hb;
|
|
1143
|
+
for (const [name, fn] of named) h.registerHelper(name, fn);
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
} catch (e) {
|
|
1148
|
+
console.warn(`[dev-server] skipped helper module ${file}: ${oneLine(e)}`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return registrars;
|
|
1152
|
+
}
|
|
1153
|
+
function oneLine(e) {
|
|
1154
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1155
|
+
return (message.split("\n")[0] ?? "").trim() || "unknown error";
|
|
1156
|
+
}
|
|
1157
|
+
async function loadGlobalData(dataDirs) {
|
|
1158
|
+
const merged = {};
|
|
1159
|
+
for (const dataDir of Array.isArray(dataDirs) ? dataDirs : [dataDirs]) {
|
|
1160
|
+
let files = [];
|
|
1161
|
+
try {
|
|
1162
|
+
const entries = await readdir4(dataDir, { withFileTypes: true });
|
|
1163
|
+
files = entries.filter((e) => e.isFile() && e.name.endsWith(".json") && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
1164
|
+
} catch {
|
|
1165
|
+
console.warn(`[dev-server] dataDir not found: ${dataDir}`);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
for (const name of files) {
|
|
1169
|
+
try {
|
|
1170
|
+
const obj = JSON.parse(await readFile4(join4(dataDir, name), "utf-8"));
|
|
1171
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
1172
|
+
deepMerge(merged, obj);
|
|
1173
|
+
}
|
|
1174
|
+
} catch (e) {
|
|
1175
|
+
const reason = e instanceof SyntaxError ? `invalid JSON (${oneLine(e)})` : oneLine(e);
|
|
1176
|
+
console.warn(`[dev-server] skipped _data file ${name}: ${reason}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return merged;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// packages/dev-server/src/surfacing.ts
|
|
1184
|
+
import { mkdir, readFile as readFile5, writeFile } from "node:fs/promises";
|
|
1185
|
+
import { join as join6 } from "node:path";
|
|
1186
|
+
|
|
1187
|
+
// packages/dev-server/src/flattening.ts
|
|
1188
|
+
import { parse as parseHtml } from "node-html-parser";
|
|
1189
|
+
var isElement = (n) => n.nodeType === 1;
|
|
1190
|
+
function staticClassTokens(el) {
|
|
1191
|
+
const raw = el.attributes?.class ?? "";
|
|
1192
|
+
return raw.replace(/\{\{[^}]*\}\}/g, " ").split(/\s+/).map((t) => t.replace(/-+$/, "")).filter((t) => t && !t.includes("{") && !t.includes("}"));
|
|
1193
|
+
}
|
|
1194
|
+
function toSkelNode(el) {
|
|
1195
|
+
const attrs = Object.keys(el.attributes ?? {}).sort();
|
|
1196
|
+
const children = [];
|
|
1197
|
+
for (const child of el.childNodes) {
|
|
1198
|
+
if (isElement(child)) children.push(toSkelNode(child));
|
|
1199
|
+
}
|
|
1200
|
+
return { tag: el.tagName ? el.tagName.toLowerCase() : "", attrs, children };
|
|
1201
|
+
}
|
|
1202
|
+
function toPreorder(nodes, depth, out) {
|
|
1203
|
+
for (const n of nodes) {
|
|
1204
|
+
out.push(`${depth}<${n.tag}${n.attrs.length ? " " + n.attrs.join(",") : ""}>`);
|
|
1205
|
+
toPreorder(n.children, depth + 1, out);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
function stripHbsComments(hbs) {
|
|
1209
|
+
return hbs.replace(/\{\{!--[\s\S]*?--\}\}/g, "").replace(/\{\{![\s\S]*?\}\}/g, "");
|
|
1210
|
+
}
|
|
1211
|
+
function deriveSkeleton(hbs) {
|
|
1212
|
+
const doc = parseHtml(stripHbsComments(hbs));
|
|
1213
|
+
const topElements = doc.childNodes.filter(isElement);
|
|
1214
|
+
const tree = topElements.map(toSkelNode);
|
|
1215
|
+
const preorder = [];
|
|
1216
|
+
toPreorder(tree, 0, preorder);
|
|
1217
|
+
const staticClasses = /* @__PURE__ */ new Set();
|
|
1218
|
+
for (const el of doc.querySelectorAll("[class]")) {
|
|
1219
|
+
for (const tok of staticClassTokens(el)) staticClasses.add(tok);
|
|
1220
|
+
}
|
|
1221
|
+
const root = topElements.length ? staticClassTokens(topElements[0])[0] ?? null : null;
|
|
1222
|
+
return { tree, preorder, root, staticClasses: [...staticClasses] };
|
|
1223
|
+
}
|
|
1224
|
+
function shingles(tokens, k) {
|
|
1225
|
+
const set = /* @__PURE__ */ new Set();
|
|
1226
|
+
if (tokens.length < k) {
|
|
1227
|
+
if (tokens.length) set.add(tokens.join(""));
|
|
1228
|
+
return set;
|
|
1229
|
+
}
|
|
1230
|
+
for (let i = 0; i + k <= tokens.length; i++) set.add(tokens.slice(i, i + k).join(""));
|
|
1231
|
+
return set;
|
|
1232
|
+
}
|
|
1233
|
+
function jaccard(a, b) {
|
|
1234
|
+
if (a.size === 0 && b.size === 0) return 1;
|
|
1235
|
+
let inter = 0;
|
|
1236
|
+
for (const x of a) if (b.has(x)) inter++;
|
|
1237
|
+
return inter / (a.size + b.size - inter);
|
|
1238
|
+
}
|
|
1239
|
+
function levenshtein(a, b) {
|
|
1240
|
+
const m = a.length, n = b.length;
|
|
1241
|
+
if (m === 0) return n;
|
|
1242
|
+
if (n === 0) return m;
|
|
1243
|
+
let prev = new Array(n + 1);
|
|
1244
|
+
let curr = new Array(n + 1);
|
|
1245
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
1246
|
+
for (let i = 1; i <= m; i++) {
|
|
1247
|
+
curr[0] = i;
|
|
1248
|
+
for (let j = 1; j <= n; j++) {
|
|
1249
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1250
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
1251
|
+
}
|
|
1252
|
+
[prev, curr] = [curr, prev];
|
|
1253
|
+
}
|
|
1254
|
+
return prev[n];
|
|
1255
|
+
}
|
|
1256
|
+
function defaultMetric(shingleK = 2) {
|
|
1257
|
+
return {
|
|
1258
|
+
name: `jaccard(k=${shingleK})+levenshtein`,
|
|
1259
|
+
prefilter: (a, b) => jaccard(shingles(a.preorder, shingleK), shingles(b.preorder, shingleK)),
|
|
1260
|
+
confirm: (a, b) => {
|
|
1261
|
+
const max = Math.max(a.preorder.length, b.preorder.length);
|
|
1262
|
+
if (max === 0) return 1;
|
|
1263
|
+
return 1 - levenshtein(a.preorder, b.preorder) / max;
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
var DEFAULTS = {
|
|
1268
|
+
theta: 0.95,
|
|
1269
|
+
// centre of the empty [0.89,1.0] score gap — see FlatteningOpts.theta
|
|
1270
|
+
prefilterCutoff: 0.6,
|
|
1271
|
+
shingleK: 2,
|
|
1272
|
+
minClusterSize: 2,
|
|
1273
|
+
rootMatch: "exact"
|
|
1274
|
+
// full root-class match — emphasis families (hoo-button vs -primary) stay distinct
|
|
1275
|
+
};
|
|
1276
|
+
function rootKey(root, mode) {
|
|
1277
|
+
if (mode === "exact") return root;
|
|
1278
|
+
const i = root.lastIndexOf("-");
|
|
1279
|
+
return i > 0 ? root.slice(0, i) : root;
|
|
1280
|
+
}
|
|
1281
|
+
var UnionFind = class {
|
|
1282
|
+
parent = /* @__PURE__ */ new Map();
|
|
1283
|
+
find(x) {
|
|
1284
|
+
if (!this.parent.has(x)) this.parent.set(x, x);
|
|
1285
|
+
let r = x;
|
|
1286
|
+
while (this.parent.get(r) !== r) r = this.parent.get(r);
|
|
1287
|
+
let c3 = x;
|
|
1288
|
+
while (this.parent.get(c3) !== r) {
|
|
1289
|
+
const nxt = this.parent.get(c3);
|
|
1290
|
+
this.parent.set(c3, r);
|
|
1291
|
+
c3 = nxt;
|
|
1292
|
+
}
|
|
1293
|
+
return r;
|
|
1294
|
+
}
|
|
1295
|
+
union(a, b) {
|
|
1296
|
+
const ra = this.find(a), rb = this.find(b);
|
|
1297
|
+
if (ra !== rb) this.parent.set(ra, rb);
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
function detectFlattening(inputs, opts = {}) {
|
|
1301
|
+
const o = { ...DEFAULTS, ...opts };
|
|
1302
|
+
const metric = opts.metric ?? defaultMetric(o.shingleK);
|
|
1303
|
+
const hasEdge = opts.hasEdge ?? (() => false);
|
|
1304
|
+
const sorted = [...inputs].sort((a, b) => a.id.localeCompare(b.id));
|
|
1305
|
+
const skel = /* @__PURE__ */ new Map();
|
|
1306
|
+
for (const { id, hbs } of sorted) skel.set(id, deriveSkeleton(hbs));
|
|
1307
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1308
|
+
for (const { id } of sorted) {
|
|
1309
|
+
const s = skel.get(id);
|
|
1310
|
+
if (!s.root) continue;
|
|
1311
|
+
const key = rootKey(s.root, o.rootMatch);
|
|
1312
|
+
(buckets.get(key) ?? buckets.set(key, []).get(key)).push(id);
|
|
1313
|
+
}
|
|
1314
|
+
const clusters = [];
|
|
1315
|
+
for (const [, ids] of [...buckets].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
1316
|
+
if (ids.length < o.minClusterSize) continue;
|
|
1317
|
+
const uf = new UnionFind();
|
|
1318
|
+
for (const id of ids) uf.find(id);
|
|
1319
|
+
for (let i = 0; i < ids.length; i++) {
|
|
1320
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
1321
|
+
const a = ids[i], b = ids[j];
|
|
1322
|
+
const sa = skel.get(a), sb = skel.get(b);
|
|
1323
|
+
if (metric.prefilter(sa, sb) < o.prefilterCutoff) continue;
|
|
1324
|
+
if (metric.confirm(sa, sb) < o.theta) continue;
|
|
1325
|
+
if (hasEdge(a, b)) continue;
|
|
1326
|
+
uf.union(a, b);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1330
|
+
for (const id of ids) {
|
|
1331
|
+
const r = uf.find(id);
|
|
1332
|
+
(groups.get(r) ?? groups.set(r, []).get(r)).push(id);
|
|
1333
|
+
}
|
|
1334
|
+
for (const members of groups.values()) {
|
|
1335
|
+
if (members.length < o.minClusterSize) continue;
|
|
1336
|
+
members.sort((a, b) => a.localeCompare(b));
|
|
1337
|
+
clusters.push(buildCluster(members, skel));
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
clusters.sort((a, b) => a.root.localeCompare(b.root) || a.members[0].localeCompare(b.members[0]));
|
|
1341
|
+
return clusters;
|
|
1342
|
+
}
|
|
1343
|
+
function buildCluster(members, skel) {
|
|
1344
|
+
const memberClasses = {};
|
|
1345
|
+
const freq = /* @__PURE__ */ new Map();
|
|
1346
|
+
for (const id of members) {
|
|
1347
|
+
const classes = [...skel.get(id).staticClasses].sort();
|
|
1348
|
+
memberClasses[id] = classes;
|
|
1349
|
+
for (const c3 of classes) freq.set(c3, (freq.get(c3) ?? 0) + 1);
|
|
1350
|
+
}
|
|
1351
|
+
const half = members.length / 2;
|
|
1352
|
+
const modal = new Set([...freq].filter(([, n]) => n > half).map(([c3]) => c3));
|
|
1353
|
+
const deltas = [];
|
|
1354
|
+
for (const id of members) {
|
|
1355
|
+
const mine = new Set(memberClasses[id]);
|
|
1356
|
+
const extra = [...mine].filter((c3) => !modal.has(c3)).sort();
|
|
1357
|
+
const missing = [...modal].filter((c3) => !mine.has(c3)).sort();
|
|
1358
|
+
if (extra.length || missing.length) deltas.push({ id, extraClasses: extra, missingClasses: missing });
|
|
1359
|
+
}
|
|
1360
|
+
const root = skel.get(members[0]).root ?? "";
|
|
1361
|
+
return { root, members, skeleton: skel.get(members[0]).preorder, memberClasses, deltas };
|
|
1362
|
+
}
|
|
1363
|
+
function renderFlatteningReport(clusters, meta = {}) {
|
|
1364
|
+
const head = [
|
|
1365
|
+
"# Flattened templates (structural)",
|
|
1366
|
+
"",
|
|
1367
|
+
"> **Scope:** this detects **structural flattening** \u2014 clusters of components that are near-duplicate copies of one template, flattened along a class-token. It is a separate corpus lens: **not** the diagnostics graph, **not** a11y, **not** HTML validation.",
|
|
1368
|
+
">",
|
|
1369
|
+
"> Every finding is `[SUBJECT-DEBT]` \u2014 the subject flattened its own templates; PD reports the sprawl and grows no model to absorb it. One derived graph (composition) sharpens another (flattening): a wrapper that *includes* a base is excluded as legitimate composition, not a copy.",
|
|
1370
|
+
">",
|
|
1371
|
+
"> **Boundary:** this catches **class-token** flattening \u2014 copies with identical structure that differ only in class values. A copy whose **markup** also drifted is not distinguishable from a genuinely-distinct sibling family by structural similarity alone; those need review, not a lower threshold. Absence of a cluster is not proof of no duplication.",
|
|
1372
|
+
">",
|
|
1373
|
+
'> **Reading the count:** a component being *in a cluster* is not the same as a distinct flattening problem. With exact root-match, one logical family whose emphasis is baked into the root class (`hoo-button` vs `hoo-button-primary`) is reported as **separate clusters** \u2014 so the cluster count and the in-cluster percentage **over-count** distinct problems (the emphasis under-reporting). Read the number as "templates touched by flattening," never "N distinct defects."',
|
|
1374
|
+
""
|
|
1375
|
+
];
|
|
1376
|
+
if (meta.subjectSha) head.push(`_subject: \`${meta.subjectSha}\`_`, "");
|
|
1377
|
+
if (meta.generatedAt) head.push(`_generated: ${meta.generatedAt}_`, "");
|
|
1378
|
+
if (clusters.length === 0) {
|
|
1379
|
+
return [...head, "No flattened template clusters detected.", ""].join("\n");
|
|
1380
|
+
}
|
|
1381
|
+
const totalMembers = clusters.reduce((n, c3) => n + c3.members.length, 0);
|
|
1382
|
+
head.push(
|
|
1383
|
+
`${clusters.length} flattened cluster${clusters.length > 1 ? "s" : ""} detected across ${totalMembers} components.`,
|
|
1384
|
+
""
|
|
1385
|
+
);
|
|
1386
|
+
const body = clusters.flatMap((c3) => {
|
|
1387
|
+
const lines = [
|
|
1388
|
+
`## \`${c3.root}\` \u2014 ${c3.members.length} copies \`[SUBJECT-DEBT]\``,
|
|
1389
|
+
"",
|
|
1390
|
+
"**Members:**",
|
|
1391
|
+
...c3.members.map((m) => `- \`${m}\``),
|
|
1392
|
+
"",
|
|
1393
|
+
"**Shared skeleton:**",
|
|
1394
|
+
"",
|
|
1395
|
+
"```",
|
|
1396
|
+
...c3.skeleton,
|
|
1397
|
+
"```",
|
|
1398
|
+
""
|
|
1399
|
+
];
|
|
1400
|
+
if (c3.deltas.length) {
|
|
1401
|
+
lines.push("**Drift (per-member deltas from the cluster's modal class set):**");
|
|
1402
|
+
for (const d of c3.deltas) {
|
|
1403
|
+
const parts = [];
|
|
1404
|
+
if (d.extraClasses.length) parts.push(`+ ${d.extraClasses.map((x) => `\`${x}\``).join(" ")}`);
|
|
1405
|
+
if (d.missingClasses.length) parts.push(`\u2212 ${d.missingClasses.map((x) => `\`${x}\``).join(" ")}`);
|
|
1406
|
+
lines.push(`- \`${d.id}\`: ${parts.join(" ")}`);
|
|
1407
|
+
}
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
} else {
|
|
1410
|
+
lines.push("_No class-token drift between members (byte-clean copies)._", "");
|
|
1411
|
+
}
|
|
1412
|
+
return lines;
|
|
1413
|
+
});
|
|
1414
|
+
return [...head, ...body].join("\n");
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// packages/dev-server/src/scratch.ts
|
|
1418
|
+
import { createHash } from "node:crypto";
|
|
1419
|
+
import { tmpdir } from "node:os";
|
|
1420
|
+
import { join as join5, resolve as resolve3 } from "node:path";
|
|
1421
|
+
function pdScratchDir(root) {
|
|
1422
|
+
const key = createHash("sha1").update(resolve3(root)).digest("hex").slice(0, 16);
|
|
1423
|
+
return join5(tmpdir(), "pattern-dump", key);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// packages/dev-server/src/surfacing.ts
|
|
1427
|
+
async function surfaceCollisions(collisions, cwd, hotreloader) {
|
|
1428
|
+
const messages = collisions.map((col) => ({ type: "collision", handle: col.handle, incumbentPath: col.incumbentPath, quarantinedPath: col.quarantinedPath }));
|
|
1429
|
+
hotreloader.retain(messages);
|
|
1430
|
+
for (const [i, col] of collisions.entries()) {
|
|
1431
|
+
hotreloader.push(messages[i]);
|
|
1432
|
+
console.warn(`[dev-server] HANDLE_COLLISION: "${col.handle}" \u2014 incumbent: ${col.incumbentPath} \u2014 quarantined: ${col.quarantinedPath}`);
|
|
1433
|
+
}
|
|
1434
|
+
const md = collisions.length === 0 ? "# Conflicts\n\nNo handle collisions detected.\n" : [
|
|
1435
|
+
"# Conflicts",
|
|
1436
|
+
"",
|
|
1437
|
+
`${collisions.length} handle collision${collisions.length > 1 ? "s" : ""} detected. Quarantined components are excluded from the tree until the collision is resolved.`,
|
|
1438
|
+
"",
|
|
1439
|
+
...collisions.flatMap((c3) => [
|
|
1440
|
+
`## \`${c3.handle}\``,
|
|
1441
|
+
`- **incumbent**: \`${c3.incumbentPath}\``,
|
|
1442
|
+
`- **quarantined**: \`${c3.quarantinedPath}\``,
|
|
1443
|
+
""
|
|
1444
|
+
])
|
|
1445
|
+
].join("\n");
|
|
1446
|
+
try {
|
|
1447
|
+
const dir = pdScratchDir(cwd);
|
|
1448
|
+
await mkdir(dir, { recursive: true });
|
|
1449
|
+
await writeFile(join6(dir, "CONFLICTS.generated.md"), md, "utf-8");
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
console.warn("[dev-server] Could not write CONFLICTS.generated.md to scratch dir:", e);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
async function surfaceFlattening(discoveryMap, registry, cwd) {
|
|
1455
|
+
const inputs = [];
|
|
1456
|
+
for (const [id, entry] of discoveryMap) {
|
|
1457
|
+
if (id.includes("~")) continue;
|
|
1458
|
+
if (entry.source) continue;
|
|
1459
|
+
if (!entry.hbsPath) continue;
|
|
1460
|
+
try {
|
|
1461
|
+
inputs.push({ id, hbs: await readFile5(entry.hbsPath, "utf-8") });
|
|
1462
|
+
} catch {
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
const hasEdge = (a, b) => (registry.uses.get(a)?.includes(b) ?? false) || (registry.uses.get(b)?.includes(a) ?? false);
|
|
1466
|
+
const clusters = detectFlattening(inputs, { hasEdge });
|
|
1467
|
+
const md = renderFlatteningReport(clusters);
|
|
1468
|
+
if (clusters.length) {
|
|
1469
|
+
const members = clusters.reduce((n, c3) => n + c3.members.length, 0);
|
|
1470
|
+
console.warn(`[dev-server] FLATTENED: ${clusters.length} cluster(s) across ${members} components \u2014 see FLATTENED.generated.md`);
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
const dir = pdScratchDir(cwd);
|
|
1474
|
+
await mkdir(dir, { recursive: true });
|
|
1475
|
+
await writeFile(join6(dir, "FLATTENED.generated.md"), md, "utf-8");
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
console.warn("[dev-server] Could not write FLATTENED.generated.md to scratch dir:", e);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// packages/dev-server/src/diagnostics.ts
|
|
1482
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1483
|
+
import { readFile as readFile6, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
1484
|
+
import { join as join7 } from "node:path";
|
|
1485
|
+
|
|
1486
|
+
// packages/a11y/src/producer.ts
|
|
1487
|
+
import { createRequire } from "node:module";
|
|
1488
|
+
import { JSDOM } from "jsdom";
|
|
1489
|
+
|
|
1490
|
+
// packages/a11y/src/rule-tiers.generated.json
|
|
1491
|
+
var rule_tiers_generated_default = {
|
|
1492
|
+
_note: "Generated by packages/a11y/src/classifier.ts \u2014 do not hand-edit. Re-run: nx run a11y:classify",
|
|
1493
|
+
generatedAt: "2026-06-08T12:39:19.990Z",
|
|
1494
|
+
axeVersion: "4.11.4",
|
|
1495
|
+
alwaysOn: [
|
|
1496
|
+
"image-alt",
|
|
1497
|
+
"label",
|
|
1498
|
+
"button-name",
|
|
1499
|
+
"link-name",
|
|
1500
|
+
"aria-required-attr",
|
|
1501
|
+
"aria-valid-attr",
|
|
1502
|
+
"aria-valid-attr-value",
|
|
1503
|
+
"autocomplete-valid",
|
|
1504
|
+
"duplicate-id"
|
|
1505
|
+
],
|
|
1506
|
+
needsRendering: [
|
|
1507
|
+
"aria-hidden-focus",
|
|
1508
|
+
"duplicate-id-aria",
|
|
1509
|
+
"color-contrast"
|
|
1510
|
+
],
|
|
1511
|
+
investigate: []
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
// packages/a11y/src/producer.ts
|
|
1515
|
+
var require2 = createRequire(import.meta.url);
|
|
1516
|
+
var axe = require2("axe-core");
|
|
1517
|
+
var SEVERITY_MAP = {
|
|
1518
|
+
critical: "error",
|
|
1519
|
+
serious: "error",
|
|
1520
|
+
moderate: "warning",
|
|
1521
|
+
minor: "warning"
|
|
1522
|
+
};
|
|
1523
|
+
async function runA11yProducer(html) {
|
|
1524
|
+
const dom = new JSDOM(
|
|
1525
|
+
`<!DOCTYPE html><html lang="en"><body><main>${html}</main></body></html>`,
|
|
1526
|
+
{ url: "http://localhost" }
|
|
1527
|
+
);
|
|
1528
|
+
const { violations } = await axe.run(
|
|
1529
|
+
dom.window.document.querySelector("main"),
|
|
1530
|
+
{ runOnly: { type: "rule", values: rule_tiers_generated_default.alwaysOn } }
|
|
1531
|
+
);
|
|
1532
|
+
const diagnostics = [];
|
|
1533
|
+
for (const v of violations) {
|
|
1534
|
+
for (const node of v.nodes) {
|
|
1535
|
+
diagnostics.push({
|
|
1536
|
+
kind: "A11Y",
|
|
1537
|
+
source: "axe",
|
|
1538
|
+
severity: SEVERITY_MAP[v.impact ?? ""] ?? "warning",
|
|
1539
|
+
message: node.failureSummary ?? v.description,
|
|
1540
|
+
ruleId: v.id,
|
|
1541
|
+
target: node.target.join(" > "),
|
|
1542
|
+
nodeHtml: node.html,
|
|
1543
|
+
help: v.help,
|
|
1544
|
+
helpUrl: v.helpUrl
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return diagnostics;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// packages/dev-server/src/diagnostics.ts
|
|
1552
|
+
async function checkComponent(componentId, variant, render, quarantineEntry) {
|
|
1553
|
+
if (quarantineEntry) {
|
|
1554
|
+
return {
|
|
1555
|
+
previewHtml: "",
|
|
1556
|
+
diagnostics: [{
|
|
1557
|
+
kind: "HANDLE_COLLISION",
|
|
1558
|
+
severity: "error",
|
|
1559
|
+
source: "discovery",
|
|
1560
|
+
message: `quarantined; handle \`${quarantineEntry.handle}\` is held by \`${quarantineEntry.incumbentId}\`; rename to resolve.`
|
|
1561
|
+
}]
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
const result = await render({ component: componentId, variant });
|
|
1566
|
+
const previewHtml = result.html;
|
|
1567
|
+
const renderDiags = result.meta?.diagnostics ?? [];
|
|
1568
|
+
const a11yDiags = await runA11yProducer(previewHtml);
|
|
1569
|
+
return { previewHtml, diagnostics: [...renderDiags, ...a11yDiags] };
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
return {
|
|
1572
|
+
previewHtml: "",
|
|
1573
|
+
diagnostics: [{
|
|
1574
|
+
kind: "RENDER_ERROR",
|
|
1575
|
+
severity: "error",
|
|
1576
|
+
source: "render",
|
|
1577
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1578
|
+
}]
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function engineForDiagnostic(diag) {
|
|
1583
|
+
switch (diag.source) {
|
|
1584
|
+
case "axe":
|
|
1585
|
+
return "axe";
|
|
1586
|
+
case "html":
|
|
1587
|
+
return "html-validate";
|
|
1588
|
+
case "schema":
|
|
1589
|
+
return "schema";
|
|
1590
|
+
case "discovery":
|
|
1591
|
+
return "discovery";
|
|
1592
|
+
default:
|
|
1593
|
+
return "render";
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
function ruleIdForDiagnostic(diag) {
|
|
1597
|
+
return diag.ruleId ?? diag.kind;
|
|
1598
|
+
}
|
|
1599
|
+
var PD_GAP_SIGNATURES = [
|
|
1600
|
+
// (none yet — add a predicate only when a specific finding is proven to be PD's own fault)
|
|
1601
|
+
];
|
|
1602
|
+
function tagForDiagnostic(diag) {
|
|
1603
|
+
return PD_GAP_SIGNATURES.some((match) => match(diag)) ? "PD-GAP" : "SUBJECT-DEBT";
|
|
1604
|
+
}
|
|
1605
|
+
function renderOutcomeFor(previewHtml, diagnostics) {
|
|
1606
|
+
if (diagnostics.some((d) => d.kind === "RENDER_ERROR")) return "error";
|
|
1607
|
+
return previewHtml.trim() === "" ? "blank" : "ok";
|
|
1608
|
+
}
|
|
1609
|
+
async function auditComponent(id, render, quarantineEntry) {
|
|
1610
|
+
const { previewHtml, diagnostics } = await checkComponent(id, void 0, render, quarantineEntry);
|
|
1611
|
+
const occurrences = diagnostics.map((diag) => {
|
|
1612
|
+
const domPath = diag.kind === "A11Y" ? diag.target : void 0;
|
|
1613
|
+
return {
|
|
1614
|
+
engine: engineForDiagnostic(diag),
|
|
1615
|
+
ruleId: ruleIdForDiagnostic(diag),
|
|
1616
|
+
component: id,
|
|
1617
|
+
severity: diag.severity,
|
|
1618
|
+
message: diag.message,
|
|
1619
|
+
...domPath ? { domPath } : {},
|
|
1620
|
+
tag: tagForDiagnostic(diag)
|
|
1621
|
+
};
|
|
1622
|
+
});
|
|
1623
|
+
return { node: { id, render: renderOutcomeFor(previewHtml, diagnostics) }, occurrences };
|
|
1624
|
+
}
|
|
1625
|
+
function assembleGraph(audits) {
|
|
1626
|
+
const components = audits.map((a) => a.node).sort((a, b) => a.id.localeCompare(b.id));
|
|
1627
|
+
const occurrences = audits.flatMap((a) => a.occurrences);
|
|
1628
|
+
const ruleNodes = /* @__PURE__ */ new Map();
|
|
1629
|
+
for (const o of occurrences) {
|
|
1630
|
+
const key = `${o.engine} ${o.ruleId}`;
|
|
1631
|
+
if (!ruleNodes.has(key)) ruleNodes.set(key, { engine: o.engine, ruleId: o.ruleId, displayKey: `${o.engine}:${o.ruleId}` });
|
|
1632
|
+
}
|
|
1633
|
+
const rules = [...ruleNodes.values()].sort((a, b) => a.engine === b.engine ? a.ruleId.localeCompare(b.ruleId) : a.engine.localeCompare(b.engine));
|
|
1634
|
+
return { rules, components, occurrences };
|
|
1635
|
+
}
|
|
1636
|
+
var GRAPH_ARTIFACT_VERSION = 1;
|
|
1637
|
+
function graphArtifactPath(root) {
|
|
1638
|
+
return join7(pdScratchDir(root), "diagnostics-graph.json");
|
|
1639
|
+
}
|
|
1640
|
+
async function readDiagnosticsArtifact(root) {
|
|
1641
|
+
try {
|
|
1642
|
+
const parsed = JSON.parse(await readFile6(graphArtifactPath(root), "utf-8"));
|
|
1643
|
+
return parsed.version === GRAPH_ARTIFACT_VERSION ? parsed : null;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
async function writeDiagnosticsArtifact(root, art) {
|
|
1649
|
+
await mkdir2(pdScratchDir(root), { recursive: true });
|
|
1650
|
+
await writeFile2(graphArtifactPath(root), JSON.stringify(art));
|
|
1651
|
+
}
|
|
1652
|
+
async function componentContentHash(id, discoveryMap, globalDataDigest) {
|
|
1653
|
+
const entry = discoveryMap.get(id);
|
|
1654
|
+
const hbs = entry?.hbsPath ? await readFile6(entry.hbsPath, "utf-8").catch(() => "") : "";
|
|
1655
|
+
const json2 = entry?.jsonPath ? await readFile6(entry.jsonPath, "utf-8").catch(() => "") : "";
|
|
1656
|
+
return createHash2("sha1").update(id).update(" ").update(hbs).update(" ").update(json2).update(" ").update(globalDataDigest).digest("hex");
|
|
1657
|
+
}
|
|
1658
|
+
async function refreshDiagnosticsGraph(root, componentIds, render, quarantineMap, discoveryMap, globalData) {
|
|
1659
|
+
const prior = await readDiagnosticsArtifact(root);
|
|
1660
|
+
const globalDataDigest = createHash2("sha1").update(JSON.stringify(globalData ?? {})).digest("hex");
|
|
1661
|
+
const priorNodes = new Map((prior?.graph.components ?? []).map((c3) => [c3.id, c3]));
|
|
1662
|
+
const priorOcc = /* @__PURE__ */ new Map();
|
|
1663
|
+
for (const o of prior?.graph.occurrences ?? []) {
|
|
1664
|
+
const arr = priorOcc.get(o.component) ?? [];
|
|
1665
|
+
arr.push(o);
|
|
1666
|
+
priorOcc.set(o.component, arr);
|
|
1667
|
+
}
|
|
1668
|
+
const slices = /* @__PURE__ */ new Map();
|
|
1669
|
+
const hashes = {};
|
|
1670
|
+
const reaudited = [];
|
|
1671
|
+
for (const id of [...componentIds].sort()) {
|
|
1672
|
+
const h = await componentContentHash(id, discoveryMap, globalDataDigest);
|
|
1673
|
+
hashes[id] = h;
|
|
1674
|
+
const priorNode = priorNodes.get(id);
|
|
1675
|
+
if (prior && prior.hashes[id] === h && priorNode) {
|
|
1676
|
+
slices.set(id, { node: priorNode, occurrences: priorOcc.get(id) ?? [] });
|
|
1677
|
+
} else {
|
|
1678
|
+
slices.set(id, await auditComponent(id, render, quarantineMap.get(id)));
|
|
1679
|
+
reaudited.push(id);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
const graph = assembleGraph([...slices.values()]);
|
|
1683
|
+
await writeDiagnosticsArtifact(root, { version: GRAPH_ARTIFACT_VERSION, hashes, graph });
|
|
1684
|
+
return { graph, slices, reaudited, reused: slices.size - reaudited.length };
|
|
1685
|
+
}
|
|
1686
|
+
async function persistSlices(root, slices, discoveryMap, globalData) {
|
|
1687
|
+
const globalDataDigest = createHash2("sha1").update(JSON.stringify(globalData ?? {})).digest("hex");
|
|
1688
|
+
const hashes = {};
|
|
1689
|
+
for (const id of slices.keys()) hashes[id] = await componentContentHash(id, discoveryMap, globalDataDigest);
|
|
1690
|
+
await writeDiagnosticsArtifact(root, { version: GRAPH_ARTIFACT_VERSION, hashes, graph: assembleGraph([...slices.values()]) });
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// packages/dev-server/src/server-config.ts
|
|
1694
|
+
import { basename as basename2, join as join8, resolve as resolve4 } from "node:path";
|
|
1695
|
+
var DEFAULT_SWATCH_SLOTS = ["--bodyBackground", "--themePrimary", "--accent"];
|
|
1696
|
+
function resolveServerConfig(config, cwd, opts = {}, packages = []) {
|
|
1697
|
+
const port2 = opts.port ?? Number(process.env["PD_PORT"] ?? 4e3);
|
|
1698
|
+
const wsPort = opts.wsPort ?? Number(process.env["PD_WS_PORT"] ?? port2 + 1);
|
|
1699
|
+
const patternsDir = resolve4(cwd, config.patternsDir);
|
|
1700
|
+
const stylesDir = resolve4(cwd, config.styles.dir);
|
|
1701
|
+
const stylesEntries = config.styles.entry ? (Array.isArray(config.styles.entry) ? config.styles.entry : [config.styles.entry]).map((e) => resolve4(stylesDir, e)) : void 0;
|
|
1702
|
+
const inPackage = (p, rel) => join8(p.dir, rel);
|
|
1703
|
+
const asList = (v) => v === void 0 ? [] : Array.isArray(v) ? v : [v];
|
|
1704
|
+
const patternRoots = [
|
|
1705
|
+
{ dir: patternsDir, ...config.exclude?.length ? { exclude: config.exclude } : {} },
|
|
1706
|
+
...packages.filter((p) => p.manifest.patterns).map((p) => ({
|
|
1707
|
+
dir: inPackage(p, p.manifest.patterns),
|
|
1708
|
+
source: p.name,
|
|
1709
|
+
...p.exclude.length ? { exclude: p.exclude } : {}
|
|
1710
|
+
}))
|
|
1711
|
+
];
|
|
1712
|
+
const styleRoots = [
|
|
1713
|
+
{ dir: stylesDir, entries: stylesEntries },
|
|
1714
|
+
...packages.filter((p) => p.manifest.styles).map((p) => {
|
|
1715
|
+
const dir = inPackage(p, p.manifest.styles.dir);
|
|
1716
|
+
const entries = asList(p.manifest.styles.entry).map((e) => resolve4(dir, e));
|
|
1717
|
+
return { dir, entries: entries.length ? entries : void 0, source: p.name };
|
|
1718
|
+
})
|
|
1719
|
+
];
|
|
1720
|
+
const themePackages = packages.filter((p) => p.manifest.themes);
|
|
1721
|
+
const themeDirs = [
|
|
1722
|
+
...config.themes ? [resolve4(cwd, config.themes.dir)] : [],
|
|
1723
|
+
...themePackages.map((p) => inPackage(p, p.manifest.themes.dir))
|
|
1724
|
+
];
|
|
1725
|
+
const switches = [config.themes?.switch, ...themePackages.map((p) => p.manifest.themes.switch)];
|
|
1726
|
+
const themeSwitch = switches.includes("stylesheet") ? "stylesheet" : "attribute";
|
|
1727
|
+
const assetRoots = [
|
|
1728
|
+
...asList(config.assets?.dir).map((d) => resolve4(cwd, d)),
|
|
1729
|
+
...packages.flatMap((p) => (p.manifest.assets ?? []).map((a) => inPackage(p, a)))
|
|
1730
|
+
];
|
|
1731
|
+
const globalScripts = [.../* @__PURE__ */ new Set([
|
|
1732
|
+
...packages.flatMap((p) => p.manifest.globalScripts ?? []),
|
|
1733
|
+
...config.globalScripts ?? []
|
|
1734
|
+
])];
|
|
1735
|
+
const scriptsDirs = [
|
|
1736
|
+
...config.scriptsDir ? [resolve4(cwd, config.scriptsDir)] : [],
|
|
1737
|
+
...packages.filter((p) => p.manifest.scripts).map((p) => inPackage(p, p.manifest.scripts))
|
|
1738
|
+
];
|
|
1739
|
+
const documentBaseEnabled = Array.isArray(config.documentBase) && config.documentBase.length > 0 || packages.some((p) => (p.manifest.styles?.documentBase?.length ?? 0) > 0);
|
|
1740
|
+
return {
|
|
1741
|
+
port: port2,
|
|
1742
|
+
wsPort,
|
|
1743
|
+
cwd,
|
|
1744
|
+
config,
|
|
1745
|
+
packages,
|
|
1746
|
+
patternRoots,
|
|
1747
|
+
patternDirs: patternRoots.map((r) => r.dir),
|
|
1748
|
+
styleRoots,
|
|
1749
|
+
dataDirs: [
|
|
1750
|
+
...packages.filter((p) => p.manifest.data).map((p) => inPackage(p, p.manifest.data)),
|
|
1751
|
+
...config.dataDir ? [resolve4(cwd, config.dataDir)] : []
|
|
1752
|
+
],
|
|
1753
|
+
helperPaths: [
|
|
1754
|
+
...packages.filter((p) => p.manifest.helpers).map((p) => inPackage(p, p.manifest.helpers)),
|
|
1755
|
+
...config.helpers ? [resolve4(cwd, config.helpers)] : []
|
|
1756
|
+
],
|
|
1757
|
+
scriptsDirs,
|
|
1758
|
+
patternsDir,
|
|
1759
|
+
stylesDir,
|
|
1760
|
+
stylesEntries,
|
|
1761
|
+
themeDirs,
|
|
1762
|
+
themesDir: themeDirs[0] ?? null,
|
|
1763
|
+
defaultTheme: config.themes?.default ?? themePackages[0]?.manifest.themes?.default,
|
|
1764
|
+
assetRoots,
|
|
1765
|
+
assetRootNames: assetRoots.map((r) => basename2(r)),
|
|
1766
|
+
assetsDir: assetRoots[0] ?? null,
|
|
1767
|
+
assetsCss: config.assets?.css ?? [],
|
|
1768
|
+
scriptsDir: scriptsDirs[0] ?? null,
|
|
1769
|
+
globalScripts,
|
|
1770
|
+
withGlobalScripts: (own) => [.../* @__PURE__ */ new Set([...globalScripts, ...own ?? []])],
|
|
1771
|
+
overviewsEnabled: config.overviews !== false,
|
|
1772
|
+
documentBaseEnabled,
|
|
1773
|
+
themeSwitch,
|
|
1774
|
+
swatchSlots: config.themes?.swatch?.slots ?? DEFAULT_SWATCH_SLOTS,
|
|
1775
|
+
packageSwatchSlots: DEFAULT_SWATCH_SLOTS
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// packages/dev-server/src/packages.ts
|
|
1780
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
1781
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
1782
|
+
function findPackageDir(name, root) {
|
|
1783
|
+
let dir = root;
|
|
1784
|
+
for (; ; ) {
|
|
1785
|
+
const candidate = join9(dir, "node_modules", name);
|
|
1786
|
+
if (existsSync(join9(candidate, "package.json"))) return candidate;
|
|
1787
|
+
const parent = dirname2(dir);
|
|
1788
|
+
if (parent === dir) return null;
|
|
1789
|
+
dir = parent;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function loadPatternPackages(references, root) {
|
|
1793
|
+
const out = [];
|
|
1794
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1795
|
+
const visit = (name, extraExclude, direct, from) => {
|
|
1796
|
+
if (seen.has(name)) return;
|
|
1797
|
+
const dir = findPackageDir(name, from);
|
|
1798
|
+
if (!dir) {
|
|
1799
|
+
if (!direct) return;
|
|
1800
|
+
throw new Error(`pattern package "${name}" is not installed \u2014 run: npm install --save-dev ${name}`);
|
|
1801
|
+
}
|
|
1802
|
+
const pkg = JSON.parse(readFileSync(join9(dir, "package.json"), "utf8"));
|
|
1803
|
+
if (!pkg.patternDump) {
|
|
1804
|
+
if (!direct) return;
|
|
1805
|
+
throw new Error(
|
|
1806
|
+
`"${name}" has no "patternDump" field in its package.json, so Pattern Dump cannot tell where its patterns, styles and themes are. See PackageManifest in @pattern-dump/contracts.`
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
seen.add(name);
|
|
1810
|
+
out.push({
|
|
1811
|
+
name,
|
|
1812
|
+
version: pkg.version ?? "0.0.0",
|
|
1813
|
+
dir,
|
|
1814
|
+
manifest: pkg.patternDump,
|
|
1815
|
+
exclude: [...pkg.patternDump.exclude ?? [], ...extraExclude],
|
|
1816
|
+
direct
|
|
1817
|
+
});
|
|
1818
|
+
for (const peer of Object.keys(pkg.peerDependencies ?? {})) visit(peer, [], false, dir);
|
|
1819
|
+
};
|
|
1820
|
+
for (const ref of references ?? []) {
|
|
1821
|
+
const { name, exclude } = typeof ref === "string" ? { name: ref, exclude: [] } : { name: ref.name, exclude: ref.exclude ?? [] };
|
|
1822
|
+
visit(name, exclude, true, root);
|
|
1823
|
+
}
|
|
1824
|
+
return out;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// packages/dev-server/src/audit.ts
|
|
1828
|
+
async function runDiagnosticsAudit(config, cwd) {
|
|
1829
|
+
const cfg = resolveServerConfig(config, cwd, {}, loadPatternPackages(config.packages, cwd));
|
|
1830
|
+
const { patternsDir, patternRoots, patternDirs, themesDir, defaultTheme } = cfg;
|
|
1831
|
+
const helperRegistrars = [];
|
|
1832
|
+
for (const path of cfg.helperPaths) helperRegistrars.push(...await loadHelperRegistrars(path));
|
|
1833
|
+
const globalData = cfg.dataDirs.length ? await loadGlobalData(cfg.dataDirs) : void 0;
|
|
1834
|
+
const render = createRenderer({
|
|
1835
|
+
patternsDir,
|
|
1836
|
+
patternRoots,
|
|
1837
|
+
themesDir: themesDir ?? void 0,
|
|
1838
|
+
defaultTheme,
|
|
1839
|
+
...helperRegistrars.length ? { helperRegistrars } : {},
|
|
1840
|
+
...globalData ? { globalData } : {}
|
|
1841
|
+
});
|
|
1842
|
+
const discovery = await buildDiscoveryMap(patternRoots);
|
|
1843
|
+
const { graph, reaudited, reused } = await refreshDiagnosticsGraph(
|
|
1844
|
+
cwd,
|
|
1845
|
+
baseComponentIds(discovery),
|
|
1846
|
+
render,
|
|
1847
|
+
discovery.quarantineMap,
|
|
1848
|
+
discovery.discoveryMap,
|
|
1849
|
+
globalData
|
|
1850
|
+
);
|
|
1851
|
+
const registry = await buildCompositionRegistry(patternDirs, discovery.handleMap, discovery.discoveryMap);
|
|
1852
|
+
await surfaceFlattening(discovery.discoveryMap, registry, cwd);
|
|
1853
|
+
return { graph, reaudited, reused, artifactPath: graphArtifactPath(cwd) };
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// packages/dev-server/src/static-build.ts
|
|
1857
|
+
import { cp, mkdir as mkdir3, rm, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1858
|
+
import { dirname as dirname3, join as join10, relative as relative2, resolve as resolve5, sep } from "node:path";
|
|
1859
|
+
|
|
1860
|
+
// packages/dev-server/src/http-utils.ts
|
|
1861
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
1862
|
+
function mimeType(ext) {
|
|
1863
|
+
const map = {
|
|
1864
|
+
".css": "text/css",
|
|
1865
|
+
".js": "application/javascript",
|
|
1866
|
+
".json": "application/json",
|
|
1867
|
+
".html": "text/html",
|
|
1868
|
+
".svg": "image/svg+xml",
|
|
1869
|
+
".png": "image/png",
|
|
1870
|
+
".jpg": "image/jpeg",
|
|
1871
|
+
".jpeg": "image/jpeg",
|
|
1872
|
+
".gif": "image/gif",
|
|
1873
|
+
".webp": "image/webp",
|
|
1874
|
+
".ico": "image/x-icon",
|
|
1875
|
+
".woff": "font/woff",
|
|
1876
|
+
".woff2": "font/woff2",
|
|
1877
|
+
".ttf": "font/ttf",
|
|
1878
|
+
".otf": "font/otf",
|
|
1879
|
+
".mp4": "video/mp4",
|
|
1880
|
+
".webm": "video/webm"
|
|
1881
|
+
};
|
|
1882
|
+
return map[ext.toLowerCase()] ?? "application/octet-stream";
|
|
1883
|
+
}
|
|
1884
|
+
function escapeHtml(s) {
|
|
1885
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1886
|
+
}
|
|
1887
|
+
function escapeAttr(s) {
|
|
1888
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
1889
|
+
}
|
|
1890
|
+
function json(res, status, data) {
|
|
1891
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1892
|
+
res.end(JSON.stringify(data));
|
|
1893
|
+
}
|
|
1894
|
+
async function serveFile(res, filePath, contentType) {
|
|
1895
|
+
try {
|
|
1896
|
+
const content = await readFile7(filePath);
|
|
1897
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
1898
|
+
res.end(content);
|
|
1899
|
+
} catch {
|
|
1900
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
1901
|
+
res.end("Not found");
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function buildShellHtml(runtime) {
|
|
1905
|
+
return `<!DOCTYPE html>
|
|
1906
|
+
<html lang="en">
|
|
1907
|
+
<head>
|
|
1908
|
+
<meta charset="UTF-8">
|
|
1909
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1910
|
+
<title>Pattern Dump Dev</title>
|
|
1911
|
+
<link rel="stylesheet" href="${PD_ROUTES.mainCss}">
|
|
1912
|
+
<link rel="stylesheet" href="${PD_ROUTES.shikiCss}">
|
|
1913
|
+
</head>
|
|
1914
|
+
<body>
|
|
1915
|
+
<div id="app"></div>
|
|
1916
|
+
<script>window.__pd=${JSON.stringify(runtime).replace(/</g, "\\u003c")};</script>
|
|
1917
|
+
<script type="module" src="${PD_ROUTES.mainJs}"></script>
|
|
1918
|
+
</body>
|
|
1919
|
+
</html>`;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// packages/dev-server/src/static-build.ts
|
|
1923
|
+
var PD_PREFIXES = /(^|["'(\s,=])\/(_pd|api|preview|source|assets)\//g;
|
|
1924
|
+
function withBasePath(text, basePath) {
|
|
1925
|
+
if (basePath === "/") return text;
|
|
1926
|
+
return text.replace(PD_PREFIXES, (_m, lead, head) => `${lead}${basePath}${head}/`);
|
|
1927
|
+
}
|
|
1928
|
+
var isText = (contentType) => /\b(text\/html|text\/css|application\/json)\b/.test(contentType);
|
|
1929
|
+
function diskPath(outDir, urlPath) {
|
|
1930
|
+
return join10(outDir, ...urlPath.split("/").map((seg) => decodeURIComponent(seg)));
|
|
1931
|
+
}
|
|
1932
|
+
async function buildStaticSite(config, root, opts) {
|
|
1933
|
+
const started = performance.now();
|
|
1934
|
+
const logLine = opts.log ?? (() => {
|
|
1935
|
+
});
|
|
1936
|
+
const log = (line) => logLine(`${line} (${((performance.now() - started) / 1e3).toFixed(1)}s)`);
|
|
1937
|
+
const basePath = normalizeBasePath(opts.basePath ?? config.build?.basePath);
|
|
1938
|
+
const includeAudit = opts.audit ?? config.build?.audit ?? true;
|
|
1939
|
+
const outDir = resolve5(opts.outDir);
|
|
1940
|
+
const cfg = resolveServerConfig(config, root, {}, loadPatternPackages(config.packages, root));
|
|
1941
|
+
const inside = (child, parent) => {
|
|
1942
|
+
const rel = relative2(parent, child);
|
|
1943
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-zA-Z]:/.test(rel);
|
|
1944
|
+
};
|
|
1945
|
+
const project = resolve5(root);
|
|
1946
|
+
if (inside(project, outDir)) throw new Error(`pd build: --out ${outDir} would contain the project itself \u2014 choose a folder inside or beside it, like ./dist`);
|
|
1947
|
+
for (const source of [cfg.patternsDir, cfg.stylesDir, ...cfg.themeDirs, ...cfg.assetRoots]) {
|
|
1948
|
+
if (inside(outDir, source) || inside(source, outDir)) {
|
|
1949
|
+
throw new Error(`pd build: --out ${outDir} overlaps ${source}, which Pattern Dump reads \u2014 choose another folder`);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
await rm(outDir, { recursive: true, force: true });
|
|
1953
|
+
await mkdir3(outDir, { recursive: true });
|
|
1954
|
+
const server = await startDevServer(config, root, { port: 0, live: false });
|
|
1955
|
+
log("\u2026 server ready");
|
|
1956
|
+
const origin = `http://127.0.0.1:${server.port}`;
|
|
1957
|
+
const report = { outDir, basePath, files: 0, bytes: 0, components: 0, warnings: [], ms: 0 };
|
|
1958
|
+
const done = /* @__PURE__ */ new Set();
|
|
1959
|
+
async function capture(liveUrl, { optional = false } = {}) {
|
|
1960
|
+
const file = staticPathFor(liveUrl);
|
|
1961
|
+
if (done.has(file)) return null;
|
|
1962
|
+
done.add(file);
|
|
1963
|
+
const res = await fetch(`${origin}${liveUrl}`);
|
|
1964
|
+
if (res.status !== 200) {
|
|
1965
|
+
if (res.status >= 500 || !optional) {
|
|
1966
|
+
const reason = res.status >= 500 ? `: ${((await res.text()).split("\n")[0] ?? "").slice(0, 160)}` : "";
|
|
1967
|
+
report.warnings.push(`${res.status} ${liveUrl}${reason}`);
|
|
1968
|
+
}
|
|
1969
|
+
return null;
|
|
1970
|
+
}
|
|
1971
|
+
const type = res.headers.get("content-type") ?? "";
|
|
1972
|
+
const target = diskPath(outDir, file);
|
|
1973
|
+
await mkdir3(dirname3(target), { recursive: true });
|
|
1974
|
+
if (isText(type)) {
|
|
1975
|
+
const body = await res.text();
|
|
1976
|
+
const out = withBasePath(body, basePath);
|
|
1977
|
+
await writeFile3(target, out);
|
|
1978
|
+
report.bytes += Buffer.byteLength(out);
|
|
1979
|
+
report.files++;
|
|
1980
|
+
return body;
|
|
1981
|
+
}
|
|
1982
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1983
|
+
await writeFile3(target, buffer);
|
|
1984
|
+
report.bytes += buffer.length;
|
|
1985
|
+
report.files++;
|
|
1986
|
+
return /javascript/.test(type) ? buffer.toString("utf8") : null;
|
|
1987
|
+
}
|
|
1988
|
+
async function store(liveUrl, body) {
|
|
1989
|
+
const file = staticPathFor(liveUrl);
|
|
1990
|
+
if (done.has(file)) return;
|
|
1991
|
+
done.add(file);
|
|
1992
|
+
const target = diskPath(outDir, file);
|
|
1993
|
+
await mkdir3(dirname3(target), { recursive: true });
|
|
1994
|
+
const out = withBasePath(body, basePath);
|
|
1995
|
+
await writeFile3(target, out);
|
|
1996
|
+
report.bytes += Buffer.byteLength(out);
|
|
1997
|
+
report.files++;
|
|
1998
|
+
}
|
|
1999
|
+
const referenced = (html) => [...html.matchAll(/(?:href|src)="(\/_pd\/(?:components|themes|scripts)\/[^"]+)"/g)].map((m) => m[1].replace(/&/g, "&"));
|
|
2000
|
+
try {
|
|
2001
|
+
for (const url of [
|
|
2002
|
+
PD_ROUTES.mainJs,
|
|
2003
|
+
PD_ROUTES.mainCss,
|
|
2004
|
+
PD_ROUTES.shikiCss,
|
|
2005
|
+
PD_ROUTES.baseCss,
|
|
2006
|
+
PD_ROUTES.axeCore,
|
|
2007
|
+
PD_ROUTES.ruleTiers,
|
|
2008
|
+
PD_ROUTES.config,
|
|
2009
|
+
PD_ROUTES.version
|
|
2010
|
+
]) {
|
|
2011
|
+
await capture(url);
|
|
2012
|
+
}
|
|
2013
|
+
const tree = JSON.parse(await capture(PD_ROUTES.tree));
|
|
2014
|
+
const themes = JSON.parse(await capture(PD_ROUTES.themes));
|
|
2015
|
+
for (const t of themes) await capture(themeCssUrl(t.name));
|
|
2016
|
+
log(`\u2026 ${themes.length} themes`);
|
|
2017
|
+
const components = [];
|
|
2018
|
+
const folders = [];
|
|
2019
|
+
const walk = (nodes) => nodes.forEach((n) => {
|
|
2020
|
+
if (n.kind === "component") components.push(n.path);
|
|
2021
|
+
if (n.children?.length) folders.push(n.path);
|
|
2022
|
+
walk(n.children ?? []);
|
|
2023
|
+
});
|
|
2024
|
+
walk(tree.roots);
|
|
2025
|
+
report.components = components.length;
|
|
2026
|
+
const routes = /* @__PURE__ */ new Set(["/"]);
|
|
2027
|
+
for (const id of components) {
|
|
2028
|
+
routes.add(`/${id}`);
|
|
2029
|
+
const baseText = await capture(componentUrl(id));
|
|
2030
|
+
if (!baseText) continue;
|
|
2031
|
+
const base2 = JSON.parse(baseText);
|
|
2032
|
+
if (base2.variants.length) routes.add(`/${id}/variants`);
|
|
2033
|
+
for (const variant of [void 0, ...base2.variants.map((v) => v.name)]) {
|
|
2034
|
+
const detailText = variant ? await capture(componentUrl(id, variant)) : baseText;
|
|
2035
|
+
if (detailText) {
|
|
2036
|
+
const { document } = JSON.parse(detailText);
|
|
2037
|
+
await store(previewUrl(id, variant), document);
|
|
2038
|
+
for (const ref of referenced(document)) await capture(ref);
|
|
2039
|
+
}
|
|
2040
|
+
await capture(renderedSourceUrl(id, variant), { optional: true });
|
|
2041
|
+
await capture(dataSourceUrl(id, variant), { optional: true });
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
log(`\u2026 ${components.length} components with their variants, previews and source views`);
|
|
2045
|
+
for (const path of folders) {
|
|
2046
|
+
routes.add(`/${path}`);
|
|
2047
|
+
await capture(overviewUrl(path), { optional: true });
|
|
2048
|
+
}
|
|
2049
|
+
log(`\u2026 ${folders.length} folder overviews`);
|
|
2050
|
+
if (includeAudit) {
|
|
2051
|
+
await capture(PD_ROUTES.audit);
|
|
2052
|
+
log("\u2026 corpus audit");
|
|
2053
|
+
} else {
|
|
2054
|
+
log("\u2026 corpus audit left out (build.audit: false / --no-audit)");
|
|
2055
|
+
}
|
|
2056
|
+
for (const [i, rootDir] of cfg.assetRoots.entries()) {
|
|
2057
|
+
const exists = await stat2(rootDir).then((s) => s.isDirectory(), () => false);
|
|
2058
|
+
if (!exists) continue;
|
|
2059
|
+
await cp(rootDir, join10(outDir, "assets", cfg.assetRootNames[i]), { recursive: true });
|
|
2060
|
+
if (cfg.assetRoots.length === 1) await cp(rootDir, join10(outDir, "assets"), { recursive: true, force: false });
|
|
2061
|
+
}
|
|
2062
|
+
const runtime = {
|
|
2063
|
+
mode: "static",
|
|
2064
|
+
basePath,
|
|
2065
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2066
|
+
pdVersion: opts.pdVersion,
|
|
2067
|
+
...includeAudit ? {} : { audit: false }
|
|
2068
|
+
};
|
|
2069
|
+
const shell = withBasePath(buildShellHtml(runtime), basePath);
|
|
2070
|
+
for (const route of routes) {
|
|
2071
|
+
const target = diskPath(outDir, staticPathFor(route));
|
|
2072
|
+
await mkdir3(dirname3(target), { recursive: true });
|
|
2073
|
+
await writeFile3(target, shell);
|
|
2074
|
+
report.files++;
|
|
2075
|
+
report.bytes += Buffer.byteLength(shell);
|
|
2076
|
+
}
|
|
2077
|
+
await writeFile3(join10(outDir, ".nojekyll"), "");
|
|
2078
|
+
report.files++;
|
|
2079
|
+
} finally {
|
|
2080
|
+
await server.close();
|
|
2081
|
+
}
|
|
2082
|
+
report.ms = Math.round(performance.now() - started);
|
|
2083
|
+
return report;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// packages/dev-server/src/server-state.ts
|
|
2087
|
+
function createServerState() {
|
|
2088
|
+
return {
|
|
2089
|
+
lastComponentId: "",
|
|
2090
|
+
scssModuleGraph: null,
|
|
2091
|
+
classOwnerIndex: null,
|
|
2092
|
+
baseProvenance: null,
|
|
2093
|
+
scssGraphDirty: true,
|
|
2094
|
+
perComponentCss: /* @__PURE__ */ new Map(),
|
|
2095
|
+
baseCss: "",
|
|
2096
|
+
compositionRegistry: null,
|
|
2097
|
+
discoveryResult: null,
|
|
2098
|
+
auditSlices: null,
|
|
2099
|
+
liveReauditCount: 0,
|
|
2100
|
+
themeCss: /* @__PURE__ */ new Map(),
|
|
2101
|
+
themeTokens: /* @__PURE__ */ new Map(),
|
|
2102
|
+
scriptCache: /* @__PURE__ */ new Map()
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// packages/dev-server/src/routes.ts
|
|
2107
|
+
import { basename as basename4, extname as extname2, join as join17 } from "node:path";
|
|
2108
|
+
import { readFile as readFile15, stat as stat4 } from "node:fs/promises";
|
|
2109
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
2110
|
+
|
|
2111
|
+
// packages/dev-server/src/component-tree.ts
|
|
2112
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
2113
|
+
import { join as join13 } from "node:path";
|
|
2114
|
+
|
|
2115
|
+
// packages/dev-server/src/layout-mode.ts
|
|
2116
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
2117
|
+
import { join as join12 } from "node:path";
|
|
2118
|
+
|
|
2119
|
+
// packages/dev-server/src/pattern-dirs.ts
|
|
2120
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
2121
|
+
import { join as join11 } from "node:path";
|
|
2122
|
+
async function readFolderFile(dirs, rel) {
|
|
2123
|
+
for (const dir of typeof dirs === "string" ? [dirs] : dirs) {
|
|
2124
|
+
try {
|
|
2125
|
+
return await readFile8(join11(dir, rel), "utf-8");
|
|
2126
|
+
} catch {
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
return null;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// packages/dev-server/src/layout-mode.ts
|
|
2133
|
+
var FULL_CATEGORIES = /* @__PURE__ */ new Set(["pages", "templates"]);
|
|
2134
|
+
async function resolveLayoutMode(componentId, patternsDir, discoveryMap) {
|
|
2135
|
+
const parts = componentId.split("/");
|
|
2136
|
+
const category = parts[0];
|
|
2137
|
+
let layout = FULL_CATEGORIES.has(category) ? "full" : "centered";
|
|
2138
|
+
const categoryIndex = await readFolderFile(patternsDir, join12(category, "index.md"));
|
|
2139
|
+
if (categoryIndex !== null) {
|
|
2140
|
+
const m = categoryIndex.match(/^preview:\s*(centered|full)$/m);
|
|
2141
|
+
if (m) layout = m[1];
|
|
2142
|
+
}
|
|
2143
|
+
const mdPath = discoveryMap?.get(componentId)?.mdPath;
|
|
2144
|
+
if (mdPath) {
|
|
2145
|
+
try {
|
|
2146
|
+
const md = await readFile9(mdPath, "utf-8");
|
|
2147
|
+
const m = md.match(/^preview:\s*(centered|full)$/m);
|
|
2148
|
+
if (m) layout = m[1];
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
return layout;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// packages/dev-server/src/frontmatter.ts
|
|
2156
|
+
var unquote = (s) => {
|
|
2157
|
+
const t = s.trim();
|
|
2158
|
+
return t.length >= 2 && (t[0] === '"' || t[0] === "'") && t.at(-1) === t[0] ? t.slice(1, -1) : t;
|
|
2159
|
+
};
|
|
2160
|
+
function readFrontmatter(md) {
|
|
2161
|
+
const lines = md.split(/\r?\n/);
|
|
2162
|
+
if (lines[0]?.trim() !== "---") return {};
|
|
2163
|
+
const end = lines.findIndex((l, i) => i > 0 && l.trim() === "---");
|
|
2164
|
+
if (end < 0) return {};
|
|
2165
|
+
const out = {};
|
|
2166
|
+
let listKey = null;
|
|
2167
|
+
for (const line of lines.slice(1, end)) {
|
|
2168
|
+
const item = line.match(/^\s*-\s+(.*)$/);
|
|
2169
|
+
if (item && listKey) {
|
|
2170
|
+
out[listKey].push(unquote(item[1]));
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
const kv = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
|
|
2174
|
+
if (!kv) {
|
|
2175
|
+
listKey = null;
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
const [, key, raw] = kv;
|
|
2179
|
+
const value = raw.trim();
|
|
2180
|
+
if (value === "") {
|
|
2181
|
+
out[key] = [];
|
|
2182
|
+
listKey = key;
|
|
2183
|
+
} else if (value.startsWith("[") && value.endsWith("]")) {
|
|
2184
|
+
out[key] = value.slice(1, -1).split(",").map(unquote).filter(Boolean);
|
|
2185
|
+
listKey = null;
|
|
2186
|
+
} else {
|
|
2187
|
+
out[key] = unquote(value);
|
|
2188
|
+
listKey = null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return out;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
// packages/dev-server/src/tree-order.ts
|
|
2195
|
+
function parseOrder(value) {
|
|
2196
|
+
if (value === void 0) return void 0;
|
|
2197
|
+
if (Array.isArray(value)) return { list: value };
|
|
2198
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) return { weight: Number(value) };
|
|
2199
|
+
const list = value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2200
|
+
return list.length ? { list } : void 0;
|
|
2201
|
+
}
|
|
2202
|
+
var collator = new Intl.Collator("en", { numeric: true });
|
|
2203
|
+
var REST = "...";
|
|
2204
|
+
function orderSiblings(items, list) {
|
|
2205
|
+
const position = /* @__PURE__ */ new Map();
|
|
2206
|
+
list?.forEach((name, i) => {
|
|
2207
|
+
if (!position.has(name)) position.set(name, i);
|
|
2208
|
+
});
|
|
2209
|
+
const restPosition = position.get(REST) ?? Infinity;
|
|
2210
|
+
const positionOf = (item) => list ? position.get(item.segment) ?? restPosition : 0;
|
|
2211
|
+
return [...items].sort((a, b) => {
|
|
2212
|
+
const ra = a.weight ?? 0;
|
|
2213
|
+
const rb = b.weight ?? 0;
|
|
2214
|
+
if (ra !== rb) return ra - rb;
|
|
2215
|
+
const pa = positionOf(a);
|
|
2216
|
+
const pb = positionOf(b);
|
|
2217
|
+
if (pa !== pb) return pa - pb;
|
|
2218
|
+
return collator.compare(a.segment, b.segment) || (a.segment < b.segment ? -1 : a.segment > b.segment ? 1 : 0);
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// packages/dev-server/src/component-tree.ts
|
|
2223
|
+
function directChildPaths(parentPath, allIds) {
|
|
2224
|
+
const prefix = `${parentPath}/`;
|
|
2225
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2226
|
+
for (const id of allIds) {
|
|
2227
|
+
if (id === parentPath || !id.startsWith(prefix)) continue;
|
|
2228
|
+
const segment = id.slice(prefix.length).split("/")[0];
|
|
2229
|
+
seen.add(`${parentPath}/${segment}`);
|
|
2230
|
+
}
|
|
2231
|
+
return [...seen];
|
|
2232
|
+
}
|
|
2233
|
+
function folderTitle(segment) {
|
|
2234
|
+
const words = segment.split(/[-_\s]+/).filter(Boolean);
|
|
2235
|
+
if (words.length === 0) return segment;
|
|
2236
|
+
const text = words.join(" ");
|
|
2237
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
2238
|
+
}
|
|
2239
|
+
var readText = (path) => readFile10(path, "utf-8").catch(() => null);
|
|
2240
|
+
function orderOf(md) {
|
|
2241
|
+
return md === null ? void 0 : parseOrder(readFrontmatter(md)["order"]);
|
|
2242
|
+
}
|
|
2243
|
+
function hasPublicComponent(node) {
|
|
2244
|
+
if (node.kind === "component" && node.visibility !== "private") return true;
|
|
2245
|
+
return node.children?.some(hasPublicComponent) ?? false;
|
|
2246
|
+
}
|
|
2247
|
+
async function buildComponentTree(patternsDir, overviewsEnabled, discoveryMap) {
|
|
2248
|
+
const baseIds = [...discoveryMap.keys()].filter((id) => !id.includes("~"));
|
|
2249
|
+
const categories = [...new Set(baseIds.map((id) => id.split("/")[0]))];
|
|
2250
|
+
const built = [];
|
|
2251
|
+
for (const cat of categories) {
|
|
2252
|
+
const catIds = baseIds.filter((id) => id === cat || id.startsWith(`${cat}/`));
|
|
2253
|
+
const node = await buildTreeNodeFromMap(cat, catIds, patternsDir, overviewsEnabled, discoveryMap);
|
|
2254
|
+
if (node) built.push(node);
|
|
2255
|
+
}
|
|
2256
|
+
const rootOrder = orderOf(await readFolderFile(patternsDir, "index.md"));
|
|
2257
|
+
const list = rootOrder && "list" in rootOrder ? rootOrder.list : void 0;
|
|
2258
|
+
return { roots: orderSiblings(built, list).map((b) => b.node) };
|
|
2259
|
+
}
|
|
2260
|
+
async function buildSubtree(folderPath, patternsDir, discoveryMap) {
|
|
2261
|
+
const ids = [...discoveryMap.keys()].filter((id) => !id.includes("~") && (id === folderPath || id.startsWith(`${folderPath}/`)));
|
|
2262
|
+
if (ids.length === 0) return null;
|
|
2263
|
+
return (await buildTreeNodeFromMap(folderPath, ids, patternsDir, true, discoveryMap))?.node ?? null;
|
|
2264
|
+
}
|
|
2265
|
+
async function buildTreeNodeFromMap(nodePath, idsUnderPath, patternsDir, overviewsEnabled, discoveryMap) {
|
|
2266
|
+
const name = nodePath.split("/").pop();
|
|
2267
|
+
if (name.startsWith(".")) return null;
|
|
2268
|
+
const entry = discoveryMap.get(nodePath);
|
|
2269
|
+
const isComponent = entry !== void 0;
|
|
2270
|
+
let title = name;
|
|
2271
|
+
let status = "draft";
|
|
2272
|
+
let visibility;
|
|
2273
|
+
let weight;
|
|
2274
|
+
if (isComponent && entry.mdPath) {
|
|
2275
|
+
const md = await readText(entry.mdPath);
|
|
2276
|
+
if (md !== null) {
|
|
2277
|
+
const t = md.match(/^title:\s*(.+)$/m);
|
|
2278
|
+
const s = md.match(/^status:\s*(.+)$/m);
|
|
2279
|
+
const v = md.match(/^visibility:\s*(public|private)$/m);
|
|
2280
|
+
if (t) title = t[1].trim();
|
|
2281
|
+
if (s) {
|
|
2282
|
+
const sv = s[1].trim();
|
|
2283
|
+
if (sv === "stable" || sv === "deprecated" || sv === "draft") status = sv;
|
|
2284
|
+
}
|
|
2285
|
+
if (v) visibility = v[1];
|
|
2286
|
+
const o = orderOf(md);
|
|
2287
|
+
if (o && "weight" in o) weight = o.weight;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const indexMd = await readFolderFile(patternsDir, join13(nodePath, "index.md"));
|
|
2291
|
+
const folderOrder = orderOf(indexMd);
|
|
2292
|
+
const built = [];
|
|
2293
|
+
for (const childPath of directChildPaths(nodePath, idsUnderPath)) {
|
|
2294
|
+
const childIds = idsUnderPath.filter((id) => id === childPath || id.startsWith(`${childPath}/`));
|
|
2295
|
+
const child = await buildTreeNodeFromMap(childPath, childIds, patternsDir, overviewsEnabled, discoveryMap);
|
|
2296
|
+
if (child) built.push(child);
|
|
2297
|
+
}
|
|
2298
|
+
const children = orderSiblings(built, folderOrder && "list" in folderOrder ? folderOrder.list : void 0).map((b) => b.node);
|
|
2299
|
+
const kind = isComponent ? "component" : "group";
|
|
2300
|
+
let layout;
|
|
2301
|
+
if (isComponent) layout = await resolveLayoutMode(nodePath, patternsDir, discoveryMap);
|
|
2302
|
+
let hasOverview = false;
|
|
2303
|
+
if (!isComponent) {
|
|
2304
|
+
let titled = false;
|
|
2305
|
+
if (indexMd !== null) {
|
|
2306
|
+
const t = indexMd.match(/^title:\s*(.+)$/m);
|
|
2307
|
+
if (t) {
|
|
2308
|
+
title = t[1].trim();
|
|
2309
|
+
titled = true;
|
|
2310
|
+
}
|
|
2311
|
+
if (!visibility) {
|
|
2312
|
+
const v = indexMd.match(/^visibility:\s*(public|private)$/m);
|
|
2313
|
+
if (v) visibility = v[1];
|
|
2314
|
+
}
|
|
2315
|
+
if (folderOrder && "weight" in folderOrder) weight = folderOrder.weight;
|
|
2316
|
+
}
|
|
2317
|
+
if (!titled) title = folderTitle(name);
|
|
2318
|
+
const optedOut = indexMd !== null && /^overview:\s*false$/m.test(indexMd);
|
|
2319
|
+
hasOverview = overviewsEnabled && !optedOut && children.some(hasPublicComponent);
|
|
2320
|
+
}
|
|
2321
|
+
const node = {
|
|
2322
|
+
path: nodePath,
|
|
2323
|
+
title,
|
|
2324
|
+
kind,
|
|
2325
|
+
hasOverview,
|
|
2326
|
+
...isComponent ? { status, layout } : {},
|
|
2327
|
+
...isComponent && entry.source ? { source: entry.source } : {},
|
|
2328
|
+
...visibility ? { visibility } : {},
|
|
2329
|
+
...children.length ? { children } : {}
|
|
2330
|
+
};
|
|
2331
|
+
return { node, segment: name, ...weight !== void 0 ? { weight } : {} };
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// packages/dev-server/src/detail.ts
|
|
2335
|
+
import { readFile as readFile14 } from "node:fs/promises";
|
|
2336
|
+
|
|
2337
|
+
// packages/dev-server/src/markdown.ts
|
|
2338
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
2339
|
+
import MarkdownIt from "markdown-it";
|
|
2340
|
+
var _md = new MarkdownIt({ html: false, linkify: false });
|
|
2341
|
+
_md.validateLink = (url) => {
|
|
2342
|
+
const t = url.trim();
|
|
2343
|
+
return /^(https?|mailto|tel):/i.test(t) || /^[.#/]/.test(t) || t.startsWith("pattern:");
|
|
2344
|
+
};
|
|
2345
|
+
_md.renderer.rules["fence"] = (tokens, idx, _opts, env) => {
|
|
2346
|
+
const map = env.resolvedFences;
|
|
2347
|
+
return map?.get(idx) ?? _mdPlainPre(tokens[idx]?.content ?? "");
|
|
2348
|
+
};
|
|
2349
|
+
function _mdPlainPre(code) {
|
|
2350
|
+
return `<pre><code>${escapeHtml(code)}</code></pre>`;
|
|
2351
|
+
}
|
|
2352
|
+
function _parsePatternSpec(spec) {
|
|
2353
|
+
const tildeIdx = spec.indexOf("~");
|
|
2354
|
+
return {
|
|
2355
|
+
compId: tildeIdx >= 0 ? spec.slice(0, tildeIdx) : spec,
|
|
2356
|
+
variant: tildeIdx >= 0 ? spec.slice(tildeIdx + 1) : void 0
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
function _resolvePatternLinks(tokens, discoveryMap) {
|
|
2360
|
+
for (const blockTok of tokens) {
|
|
2361
|
+
if (blockTok.type !== "inline" || !blockTok.children) continue;
|
|
2362
|
+
for (const child of blockTok.children) {
|
|
2363
|
+
if (child.type !== "link_open") continue;
|
|
2364
|
+
const href = child.attrGet("href") ?? "";
|
|
2365
|
+
if (!href.startsWith("pattern:")) continue;
|
|
2366
|
+
const { compId, variant } = _parsePatternSpec(href.slice("pattern:".length));
|
|
2367
|
+
const fullId = variant ? `${compId}~${variant}` : compId;
|
|
2368
|
+
if (discoveryMap.has(fullId)) {
|
|
2369
|
+
child.attrSet("href", "/" + compId + (variant ? "#" + variant : ""));
|
|
2370
|
+
child.attrPush(["class", "pattern-link"]);
|
|
2371
|
+
child.attrPush(["data-pattern-id", compId]);
|
|
2372
|
+
if (variant) child.attrPush(["data-variant", variant]);
|
|
2373
|
+
} else {
|
|
2374
|
+
child.attrSet("href", "#");
|
|
2375
|
+
child.attrPush(["class", "pattern-link--unresolved"]);
|
|
2376
|
+
child.attrPush(["aria-disabled", "true"]);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
async function renderDoc(body, renderComponent, discoveryMap) {
|
|
2382
|
+
const tokens = _md.parse(body, {});
|
|
2383
|
+
_resolvePatternLinks(tokens, discoveryMap);
|
|
2384
|
+
const resolvedFences = /* @__PURE__ */ new Map();
|
|
2385
|
+
await Promise.all(
|
|
2386
|
+
tokens.flatMap(
|
|
2387
|
+
(tok, idx) => tok.type === "fence" ? [_resolveFence(tok.info, tok.content, idx, renderComponent, resolvedFences, discoveryMap)] : []
|
|
2388
|
+
)
|
|
2389
|
+
);
|
|
2390
|
+
return _md.renderer.render(tokens, _md.options, { resolvedFences });
|
|
2391
|
+
}
|
|
2392
|
+
function _isRendererNotFound(e) {
|
|
2393
|
+
return e instanceof Error && e.message.startsWith("[renderer]") && e.message.includes("not found");
|
|
2394
|
+
}
|
|
2395
|
+
async function _resolveFence(info, content, idx, renderComponent, out, discoveryMap) {
|
|
2396
|
+
const lang = info.trim();
|
|
2397
|
+
const code = content;
|
|
2398
|
+
if (lang === "pattern" || lang.startsWith("pattern ")) {
|
|
2399
|
+
const spec = lang.startsWith("pattern ") ? lang.slice("pattern ".length).trim() : code.split("\n")[0]?.trim() ?? "";
|
|
2400
|
+
if (!spec) {
|
|
2401
|
+
out.set(idx, `<pre><code></code></pre>`);
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
const { compId, variant } = _parsePatternSpec(spec);
|
|
2405
|
+
try {
|
|
2406
|
+
const html = await renderComponent(compId, variant);
|
|
2407
|
+
out.set(idx, `<div class="doc-example">${html}</div>`);
|
|
2408
|
+
} catch (e) {
|
|
2409
|
+
if (!_isRendererNotFound(e)) console.warn(`[dev-server] Pattern block render error [${spec}]:`, e);
|
|
2410
|
+
out.set(idx, `<p class="doc-error">[pattern: ${escapeHtml(spec)} \u2014 render error]</p>`);
|
|
2411
|
+
}
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (lang === "src-template" || lang.startsWith("src-template ")) {
|
|
2415
|
+
const spec = lang.startsWith("src-template ") ? lang.slice("src-template ".length).trim() : code.split("\n")[0]?.trim() ?? "";
|
|
2416
|
+
if (!spec) {
|
|
2417
|
+
out.set(idx, _mdPlainPre(""));
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
const { compId } = _parsePatternSpec(spec);
|
|
2421
|
+
const entry = discoveryMap.get(compId);
|
|
2422
|
+
if (!entry?.hbsPath) {
|
|
2423
|
+
out.set(idx, `<p class="doc-error">[src-template: ${escapeHtml(spec)} \u2014 no template file]</p>`);
|
|
2424
|
+
return;
|
|
2425
|
+
}
|
|
2426
|
+
try {
|
|
2427
|
+
const src = await readFile11(entry.hbsPath, "utf-8");
|
|
2428
|
+
out.set(idx, wrapWithCopyButton(highlight(src, "handlebars"), src));
|
|
2429
|
+
} catch (e) {
|
|
2430
|
+
console.warn(`[dev-server] src-template read error [${spec}]:`, e);
|
|
2431
|
+
out.set(idx, `<p class="doc-error">[src-template: ${escapeHtml(spec)} \u2014 read error]</p>`);
|
|
2432
|
+
}
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
if (lang === "src-data" || lang.startsWith("src-data ")) {
|
|
2436
|
+
const spec = lang.startsWith("src-data ") ? lang.slice("src-data ".length).trim() : code.split("\n")[0]?.trim() ?? "";
|
|
2437
|
+
if (!spec) {
|
|
2438
|
+
out.set(idx, _mdPlainPre(""));
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
const { compId, variant } = _parsePatternSpec(spec);
|
|
2442
|
+
const lookupId = variant ? `${compId}~${variant}` : compId;
|
|
2443
|
+
try {
|
|
2444
|
+
if (variant) {
|
|
2445
|
+
const variantEntry = discoveryMap.get(lookupId);
|
|
2446
|
+
if (!variantEntry) {
|
|
2447
|
+
out.set(idx, `<p class="doc-error">[src-data: ${escapeHtml(spec)} \u2014 unknown variant]</p>`);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
const baseJsonPath = discoveryMap.get(compId)?.jsonPath;
|
|
2451
|
+
const variantJsonPath = variantEntry.jsonPath;
|
|
2452
|
+
const baseData = baseJsonPath ? JSON.parse(await readFile11(baseJsonPath, "utf-8")) : {};
|
|
2453
|
+
const variantData = variantJsonPath ? JSON.parse(await readFile11(variantJsonPath, "utf-8")) : {};
|
|
2454
|
+
const merged = deepMerge(structuredClone(baseData), variantData);
|
|
2455
|
+
const src = JSON.stringify(merged, null, 2);
|
|
2456
|
+
out.set(idx, wrapWithCopyButton(highlight(src, "json"), src));
|
|
2457
|
+
} else {
|
|
2458
|
+
const jsonPath = discoveryMap.get(compId)?.jsonPath;
|
|
2459
|
+
if (!jsonPath) {
|
|
2460
|
+
out.set(idx, `<p class="doc-error">[src-data: ${escapeHtml(spec)} \u2014 no data file]</p>`);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
const src = await readFile11(jsonPath, "utf-8");
|
|
2464
|
+
out.set(idx, wrapWithCopyButton(highlight(src, "json"), src));
|
|
2465
|
+
}
|
|
2466
|
+
} catch (e) {
|
|
2467
|
+
console.warn(`[dev-server] src-data read error [${spec}]:`, e);
|
|
2468
|
+
out.set(idx, `<p class="doc-error">[src-data: ${escapeHtml(spec)} \u2014 read error]</p>`);
|
|
2469
|
+
}
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
if (lang === "src-html" || lang.startsWith("src-html ")) {
|
|
2473
|
+
const spec = lang.startsWith("src-html ") ? lang.slice("src-html ".length).trim() : code.split("\n")[0]?.trim() ?? "";
|
|
2474
|
+
if (!spec) {
|
|
2475
|
+
out.set(idx, _mdPlainPre(""));
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const { compId, variant } = _parsePatternSpec(spec);
|
|
2479
|
+
try {
|
|
2480
|
+
const html = await renderComponent(compId, variant);
|
|
2481
|
+
out.set(idx, wrapWithCopyButton(highlight(html, "html"), html));
|
|
2482
|
+
} catch (e) {
|
|
2483
|
+
if (!_isRendererNotFound(e)) console.warn(`[dev-server] src-html render error [${spec}]:`, e);
|
|
2484
|
+
out.set(idx, `<p class="doc-error">[src-html: ${escapeHtml(spec)} \u2014 render error]</p>`);
|
|
2485
|
+
}
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
const highlighted = highlight(code, lang);
|
|
2489
|
+
out.set(idx, wrapWithCopyButton(highlighted, code));
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// packages/dev-server/src/css-closure.ts
|
|
2493
|
+
import { readFile as readFile12 } from "node:fs/promises";
|
|
2494
|
+
import { dirname as dirname5, resolve as resolve7 } from "node:path";
|
|
2495
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2496
|
+
import postcss4 from "postcss";
|
|
2497
|
+
import * as sass3 from "sass";
|
|
2498
|
+
import { SourceMapConsumer as SourceMapConsumer2 } from "source-map";
|
|
2499
|
+
import { parse as parseHtml2 } from "node-html-parser";
|
|
2500
|
+
|
|
2501
|
+
// packages/dev-server/src/style-index.ts
|
|
2502
|
+
import { dirname as dirname4, relative as relative3, resolve as resolve6 } from "node:path";
|
|
2503
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2504
|
+
import postcss3 from "postcss";
|
|
2505
|
+
import { SourceMapConsumer } from "source-map";
|
|
2506
|
+
import * as sass2 from "sass";
|
|
2507
|
+
|
|
2508
|
+
// packages/dev-server/src/timing.ts
|
|
2509
|
+
var verboseTiming = !!process.env["PD_VERBOSE_TIMING"];
|
|
2510
|
+
|
|
2511
|
+
// packages/dev-server/src/style-index.ts
|
|
2512
|
+
function resolveScssToComponent(absPath, stylesDir, componentIds) {
|
|
2513
|
+
const rel = relative3(stylesDir, absPath);
|
|
2514
|
+
let normalized = rel.replace(/\.scss$/, "").split("/").map((seg) => seg.replace(/^\d+[-_]/, "").replace(/^_/, "")).join("/");
|
|
2515
|
+
if (normalized.endsWith("/index")) normalized = normalized.slice(0, -6);
|
|
2516
|
+
const id = collapseComponentId(normalized);
|
|
2517
|
+
return componentIds.has(id) ? id : null;
|
|
2518
|
+
}
|
|
2519
|
+
function resolveSharedModulePath(absPath, stylesDir, keepBarrel = false) {
|
|
2520
|
+
const rel = relative3(stylesDir, absPath);
|
|
2521
|
+
if (rel.startsWith("..")) return null;
|
|
2522
|
+
const parts = rel.replace(/\.scss$/, "").split("/");
|
|
2523
|
+
const normalized = parts.map(
|
|
2524
|
+
(seg, i) => i === parts.length - 1 ? seg.replace(/^_/, "") : seg
|
|
2525
|
+
);
|
|
2526
|
+
if (!keepBarrel && normalized[normalized.length - 1] === "index") return null;
|
|
2527
|
+
return normalized.join("/");
|
|
2528
|
+
}
|
|
2529
|
+
function extractClassesFromCompiled(consumer, root, entryFile, stylesDir, componentIds) {
|
|
2530
|
+
const classMap = /* @__PURE__ */ new Map();
|
|
2531
|
+
const baseModules = /* @__PURE__ */ new Map();
|
|
2532
|
+
root.walkRules((rule) => {
|
|
2533
|
+
const classMatches = rule.selector.match(/\.([\w-]+)/g);
|
|
2534
|
+
if (!classMatches) return;
|
|
2535
|
+
const pos = rule.source?.start;
|
|
2536
|
+
if (!pos) return;
|
|
2537
|
+
const original = consumer.originalPositionFor({ line: pos.line, column: pos.column - 1 });
|
|
2538
|
+
if (!original.source) return;
|
|
2539
|
+
const originPath = original.source.startsWith("file://") ? fileURLToPath2(original.source) : resolve6(dirname4(entryFile), original.source);
|
|
2540
|
+
const resolved = resolveScssToComponent(originPath, stylesDir, componentIds);
|
|
2541
|
+
const owner = resolved ?? "__base__";
|
|
2542
|
+
const modulePath = resolved === null ? resolveSharedModulePath(originPath, stylesDir, true) : null;
|
|
2543
|
+
for (const cls of classMatches) {
|
|
2544
|
+
const name = cls.slice(1);
|
|
2545
|
+
if (!classMap.has(name)) classMap.set(name, owner);
|
|
2546
|
+
if (modulePath && classMap.get(name) === "__base__" && !baseModules.has(name)) {
|
|
2547
|
+
baseModules.set(name, modulePath);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
});
|
|
2551
|
+
return { classMap, baseModules };
|
|
2552
|
+
}
|
|
2553
|
+
async function attributeClassesFromFile(entryFile, stylesDir, componentIds) {
|
|
2554
|
+
try {
|
|
2555
|
+
const compiled = sass2.compile(entryFile, { sourceMap: true });
|
|
2556
|
+
if (!compiled.sourceMap) return null;
|
|
2557
|
+
const consumer = new SourceMapConsumer(compiled.sourceMap);
|
|
2558
|
+
const root = postcss3.parse(compiled.css);
|
|
2559
|
+
const { classMap } = extractClassesFromCompiled(consumer, root, entryFile, stylesDir, componentIds);
|
|
2560
|
+
return classMap;
|
|
2561
|
+
} catch {
|
|
2562
|
+
return null;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
async function patchClassOwnerIndex(entryFile, changedCompId, stylesDir, componentIds, index) {
|
|
2566
|
+
for (const [cls, owner] of index) {
|
|
2567
|
+
if (owner === changedCompId) index.delete(cls);
|
|
2568
|
+
}
|
|
2569
|
+
const fresh = await attributeClassesFromFile(entryFile, stylesDir, componentIds);
|
|
2570
|
+
if (!fresh) return false;
|
|
2571
|
+
for (const [name, owner] of fresh) {
|
|
2572
|
+
if (owner === changedCompId && !index.has(name)) index.set(name, owner);
|
|
2573
|
+
}
|
|
2574
|
+
return true;
|
|
2575
|
+
}
|
|
2576
|
+
async function buildAllStyleData(roots, componentIds, entryOverride) {
|
|
2577
|
+
if (verboseTiming) console.time("[dev-server:timing] full classOwnerIndex build");
|
|
2578
|
+
const styleUsesModules = /* @__PURE__ */ new Map();
|
|
2579
|
+
const classIndex = /* @__PURE__ */ new Map();
|
|
2580
|
+
const baseProvenance = /* @__PURE__ */ new Map();
|
|
2581
|
+
const componentCssBuckets = /* @__PURE__ */ new Map();
|
|
2582
|
+
const baseCssByRoot = [];
|
|
2583
|
+
for (const id of componentIds) styleUsesModules.set(id, []);
|
|
2584
|
+
const rootList = typeof roots === "string" ? [{ dir: roots, ...entryOverride ? { entries: entryOverride } : {} }] : roots;
|
|
2585
|
+
for (const styleRoot of rootList) {
|
|
2586
|
+
const stylesDir = styleRoot.dir;
|
|
2587
|
+
const baseCssBucket = /* @__PURE__ */ new Set();
|
|
2588
|
+
baseCssByRoot.push([]);
|
|
2589
|
+
let entryFiles = [];
|
|
2590
|
+
if (styleRoot.entries && styleRoot.entries.length > 0) {
|
|
2591
|
+
entryFiles = [...styleRoot.entries];
|
|
2592
|
+
} else {
|
|
2593
|
+
try {
|
|
2594
|
+
entryFiles = await collectScssFiles(stylesDir);
|
|
2595
|
+
} catch {
|
|
2596
|
+
entryFiles = [];
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
entryFiles.sort();
|
|
2600
|
+
for (const entryFile of entryFiles) {
|
|
2601
|
+
let originOwnerFor2 = function(node) {
|
|
2602
|
+
const pos = node.source?.start;
|
|
2603
|
+
if (!pos) return null;
|
|
2604
|
+
const original = consumer.originalPositionFor({
|
|
2605
|
+
line: pos.line,
|
|
2606
|
+
column: pos.column - 1
|
|
2607
|
+
});
|
|
2608
|
+
if (!original.source) return null;
|
|
2609
|
+
const originPath = original.source.startsWith("file://") ? fileURLToPath2(original.source) : resolve6(dirname4(entryFile), original.source);
|
|
2610
|
+
const originComp = resolveScssToComponent(originPath, stylesDir, componentIds);
|
|
2611
|
+
return originComp ?? "__base__";
|
|
2612
|
+
};
|
|
2613
|
+
var originOwnerFor = originOwnerFor2;
|
|
2614
|
+
const compOwner = resolveScssToComponent(entryFile, stylesDir, componentIds);
|
|
2615
|
+
let compiled;
|
|
2616
|
+
try {
|
|
2617
|
+
compiled = sass2.compile(entryFile, { sourceMap: true });
|
|
2618
|
+
} catch {
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
if (compOwner) {
|
|
2622
|
+
const deps = compiled.loadedUrls.filter((u) => u.protocol === "file:").map((u) => fileURLToPath2(u)).filter((p) => resolve6(p) !== resolve6(entryFile));
|
|
2623
|
+
for (const dep of deps) {
|
|
2624
|
+
const depComp = resolveScssToComponent(dep, stylesDir, componentIds);
|
|
2625
|
+
if (depComp !== null && depComp !== compOwner) {
|
|
2626
|
+
const rawRel = relative3(stylesDir, dep).replace(/\.scss$/, "");
|
|
2627
|
+
const modPath = rawRel.endsWith("/_index") ? rawRel.slice(0, -7) : rawRel;
|
|
2628
|
+
if (modPath) {
|
|
2629
|
+
const mArr = styleUsesModules.get(compOwner) ?? [];
|
|
2630
|
+
if (!mArr.includes(modPath)) {
|
|
2631
|
+
mArr.push(modPath);
|
|
2632
|
+
styleUsesModules.set(compOwner, mArr);
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
} else if (depComp === null) {
|
|
2636
|
+
const modPath = resolveSharedModulePath(dep, stylesDir);
|
|
2637
|
+
if (modPath) {
|
|
2638
|
+
const mArr = styleUsesModules.get(compOwner) ?? [];
|
|
2639
|
+
if (!mArr.includes(modPath)) {
|
|
2640
|
+
mArr.push(modPath);
|
|
2641
|
+
styleUsesModules.set(compOwner, mArr);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
const rawMap = compiled.sourceMap;
|
|
2648
|
+
if (!rawMap) continue;
|
|
2649
|
+
const consumer = new SourceMapConsumer(rawMap);
|
|
2650
|
+
const root = postcss3.parse(compiled.css);
|
|
2651
|
+
root.each((node) => {
|
|
2652
|
+
const owner = originOwnerFor2(node);
|
|
2653
|
+
if (owner === null) return;
|
|
2654
|
+
const nodeText = node.toString();
|
|
2655
|
+
if (owner === "__base__") {
|
|
2656
|
+
baseCssBucket.add(nodeText);
|
|
2657
|
+
} else {
|
|
2658
|
+
const bucket = componentCssBuckets.get(owner) ?? /* @__PURE__ */ new Set();
|
|
2659
|
+
bucket.add(nodeText);
|
|
2660
|
+
componentCssBuckets.set(owner, bucket);
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
const { classMap: perFileClasses, baseModules: perFileBaseModules } = extractClassesFromCompiled(consumer, root, entryFile, stylesDir, componentIds);
|
|
2664
|
+
for (const [name, owner] of perFileClasses) {
|
|
2665
|
+
if (!classIndex.has(name)) classIndex.set(name, owner);
|
|
2666
|
+
}
|
|
2667
|
+
for (const [name, mod] of perFileBaseModules) {
|
|
2668
|
+
if (!baseProvenance.has(name)) baseProvenance.set(name, mod);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
baseCssByRoot[baseCssByRoot.length - 1] = [...baseCssBucket];
|
|
2672
|
+
}
|
|
2673
|
+
for (const [k, v] of styleUsesModules) styleUsesModules.set(k, [...new Set(v)].sort());
|
|
2674
|
+
const componentCssMap = /* @__PURE__ */ new Map();
|
|
2675
|
+
for (const [id, bucket] of componentCssBuckets) {
|
|
2676
|
+
componentCssMap.set(id, [...bucket].join("\n"));
|
|
2677
|
+
}
|
|
2678
|
+
if (verboseTiming) console.timeEnd("[dev-server:timing] full classOwnerIndex build");
|
|
2679
|
+
return {
|
|
2680
|
+
scssModuleGraph: { styleUsesModules },
|
|
2681
|
+
classOwnerIndex: classIndex,
|
|
2682
|
+
baseProvenance,
|
|
2683
|
+
componentCssMap,
|
|
2684
|
+
// Packages' base rules first (later roots), the project's last — its own base wins the cascade.
|
|
2685
|
+
baseCss: [...baseCssByRoot].reverse().flat().join("\n")
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// packages/dev-server/src/css-closure.ts
|
|
2690
|
+
async function recomputeComponentCss(entryFile, compId, stylesDir, componentIds) {
|
|
2691
|
+
try {
|
|
2692
|
+
const compiled = sass3.compile(entryFile, { sourceMap: true });
|
|
2693
|
+
const rawMap = compiled.sourceMap;
|
|
2694
|
+
if (!rawMap) return null;
|
|
2695
|
+
const consumer = new SourceMapConsumer2(rawMap);
|
|
2696
|
+
const root = postcss4.parse(compiled.css);
|
|
2697
|
+
const bucket = /* @__PURE__ */ new Set();
|
|
2698
|
+
root.each((node) => {
|
|
2699
|
+
const pos = node.source?.start;
|
|
2700
|
+
if (!pos) return;
|
|
2701
|
+
const original = consumer.originalPositionFor({ line: pos.line, column: pos.column - 1 });
|
|
2702
|
+
if (!original.source) return;
|
|
2703
|
+
const originPath = original.source.startsWith("file://") ? fileURLToPath3(original.source) : resolve7(dirname5(entryFile), original.source);
|
|
2704
|
+
const originComp = resolveScssToComponent(originPath, stylesDir, componentIds);
|
|
2705
|
+
if (originComp === compId) bucket.add(node.toString());
|
|
2706
|
+
});
|
|
2707
|
+
return [...bucket].join("\n");
|
|
2708
|
+
} catch {
|
|
2709
|
+
return null;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
async function computeCssClosure(componentId, compReg, patternsDir, classOwnerIndex, discoveryMap) {
|
|
2713
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2714
|
+
const result = [];
|
|
2715
|
+
async function dfs(id) {
|
|
2716
|
+
if (visited.has(id)) return;
|
|
2717
|
+
visited.add(id);
|
|
2718
|
+
const compUses = compReg.uses.get(id) ?? [];
|
|
2719
|
+
const hbsPath = discoveryMap.get(id)?.hbsPath ?? "";
|
|
2720
|
+
let hbs = "";
|
|
2721
|
+
try {
|
|
2722
|
+
hbs = hbsPath ? await readFile12(hbsPath, "utf-8") : "";
|
|
2723
|
+
} catch {
|
|
2724
|
+
}
|
|
2725
|
+
const { styleUses } = deriveClassBorrowingFields(hbs, id, classOwnerIndex);
|
|
2726
|
+
for (const dep of [.../* @__PURE__ */ new Set([...compUses, ...styleUses])]) {
|
|
2727
|
+
await dfs(dep);
|
|
2728
|
+
}
|
|
2729
|
+
result.push(id);
|
|
2730
|
+
}
|
|
2731
|
+
await dfs(componentId);
|
|
2732
|
+
return result;
|
|
2733
|
+
}
|
|
2734
|
+
function withRenderedClassOwners(closureIds, renderedHtml, classOwnerIndex) {
|
|
2735
|
+
if (closureIds.length === 0) return closureIds;
|
|
2736
|
+
const present = new Set(closureIds);
|
|
2737
|
+
const extra = /* @__PURE__ */ new Set();
|
|
2738
|
+
for (const cls of extractUsedClasses(renderedHtml)) {
|
|
2739
|
+
const owner = classOwnerIndex.get(cls);
|
|
2740
|
+
if (owner && owner !== "__base__" && !present.has(owner)) extra.add(owner);
|
|
2741
|
+
}
|
|
2742
|
+
if (extra.size === 0) return closureIds;
|
|
2743
|
+
const self = closureIds[closureIds.length - 1];
|
|
2744
|
+
return [...closureIds.slice(0, -1), ...[...extra].sort(), self];
|
|
2745
|
+
}
|
|
2746
|
+
function extractUsedClasses(hbsSource) {
|
|
2747
|
+
const root = parseHtml2(hbsSource);
|
|
2748
|
+
const classes = /* @__PURE__ */ new Set();
|
|
2749
|
+
for (const el of root.querySelectorAll("[class]")) {
|
|
2750
|
+
const raw = el.getAttribute("class") ?? "";
|
|
2751
|
+
for (const token of raw.split(/\s+/)) {
|
|
2752
|
+
if (token && !token.includes("{{") && !token.includes("}}")) {
|
|
2753
|
+
classes.add(token);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
return [...classes];
|
|
2758
|
+
}
|
|
2759
|
+
function deriveClassBorrowingFields(hbsSource, componentId, classOwnerIndex, baseProvenance) {
|
|
2760
|
+
const usedClasses = extractUsedClasses(hbsSource);
|
|
2761
|
+
const styleUses = [...new Set(
|
|
2762
|
+
usedClasses.map((cls) => classOwnerIndex.get(cls)).filter((id) => id !== void 0 && id !== componentId && id !== "__base__")
|
|
2763
|
+
)].sort();
|
|
2764
|
+
const styleModules = baseProvenance ? [...new Set(
|
|
2765
|
+
usedClasses.filter((cls) => classOwnerIndex.get(cls) === "__base__").map((cls) => baseProvenance.get(cls)).filter((m) => m !== void 0)
|
|
2766
|
+
)].sort() : [];
|
|
2767
|
+
const unattributedClasses = [...new Set(
|
|
2768
|
+
usedClasses.filter((cls) => !classOwnerIndex.has(cls))
|
|
2769
|
+
)].sort();
|
|
2770
|
+
const utilityClasses = false;
|
|
2771
|
+
const inlineStyles = /style\s*=|<style[\s>]/i.test(hbsSource);
|
|
2772
|
+
return { styleUses, styleModules, inlineStyles, utilityClasses, unattributedClasses };
|
|
2773
|
+
}
|
|
2774
|
+
function linkableClosure(closureIds, perComponentCss) {
|
|
2775
|
+
return closureIds.filter((id) => !!perComponentCss.get(id));
|
|
2776
|
+
}
|
|
2777
|
+
|
|
2778
|
+
// packages/dev-server/src/requires.ts
|
|
2779
|
+
import { readFile as readFile13 } from "node:fs/promises";
|
|
2780
|
+
import { join as join14, basename as basename3 } from "node:path";
|
|
2781
|
+
function normalizeScriptId(raw) {
|
|
2782
|
+
let s = raw.trim().replace(/^\[(.+)\]$/, "$1").trim();
|
|
2783
|
+
s = basename3(s);
|
|
2784
|
+
s = s.replace(/\.(ts|js)$/, "");
|
|
2785
|
+
return s;
|
|
2786
|
+
}
|
|
2787
|
+
function parseFrontmatterList(md, key) {
|
|
2788
|
+
const values = [];
|
|
2789
|
+
const re = new RegExp(`^${key}:\\s*(.+)$`, "mg");
|
|
2790
|
+
for (const [, val] of md.matchAll(re)) {
|
|
2791
|
+
values.push(val.trim());
|
|
2792
|
+
}
|
|
2793
|
+
return values;
|
|
2794
|
+
}
|
|
2795
|
+
async function aggregateRequires(componentId, patternsDir, flatNameToPath, discoveryMap, visited = /* @__PURE__ */ new Set()) {
|
|
2796
|
+
if (visited.has(componentId)) return { scripts: [], assets: [] };
|
|
2797
|
+
visited.add(componentId);
|
|
2798
|
+
const entry = discoveryMap.get(componentId);
|
|
2799
|
+
let ownScripts = [];
|
|
2800
|
+
let ownAssets = [];
|
|
2801
|
+
if (entry?.mdPath) {
|
|
2802
|
+
try {
|
|
2803
|
+
const md = await readFile13(entry.mdPath, "utf-8");
|
|
2804
|
+
ownScripts = parseFrontmatterList(md, "requireScript").map(normalizeScriptId);
|
|
2805
|
+
ownAssets = parseFrontmatterList(md, "requireAssets").map((v) => v.trim().replace(/^\[(.+)\]$/, "$1").trim());
|
|
2806
|
+
} catch {
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
const allScripts = [...ownScripts];
|
|
2810
|
+
const allAssets = [...ownAssets];
|
|
2811
|
+
const hbsPath = entry?.hbsPath ?? "";
|
|
2812
|
+
let hbs = "";
|
|
2813
|
+
try {
|
|
2814
|
+
hbs = hbsPath ? await readFile13(hbsPath, "utf-8") : "";
|
|
2815
|
+
} catch {
|
|
2816
|
+
}
|
|
2817
|
+
for (const flatName of parsePartialsFromHbs(hbs)) {
|
|
2818
|
+
const childPath = flatNameToPath.get(flatName);
|
|
2819
|
+
if (childPath) {
|
|
2820
|
+
const child = await aggregateRequires(childPath, patternsDir, flatNameToPath, discoveryMap, visited);
|
|
2821
|
+
allScripts.push(...child.scripts);
|
|
2822
|
+
allAssets.push(...child.assets);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
return {
|
|
2826
|
+
scripts: [...new Set(allScripts)],
|
|
2827
|
+
assets: [...new Set(allAssets)]
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
async function loadSvgAssets(ids, assetsDir) {
|
|
2831
|
+
const contents = [];
|
|
2832
|
+
for (const id of ids) {
|
|
2833
|
+
for (const candidate of [`${id}.svg`, `images/${id}.svg`]) {
|
|
2834
|
+
try {
|
|
2835
|
+
const raw = await readFile13(join14(assetsDir, candidate), "utf-8");
|
|
2836
|
+
contents.push(hideSvg(raw));
|
|
2837
|
+
break;
|
|
2838
|
+
} catch {
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
return contents;
|
|
2843
|
+
}
|
|
2844
|
+
function hideSvg(svg) {
|
|
2845
|
+
return svg.replace(/<svg(\s|>)/, '<svg$1style="display:none;position:absolute" aria-hidden="true" ');
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
// packages/dev-server/src/detail.ts
|
|
2849
|
+
async function buildComponentDetail(componentId, patternsDir, renderFn, tracker, defaultTheme, flatNameToPath, compositionReg, scssGraph, classOwnerIndex, discoveryMap, baseProvenance) {
|
|
2850
|
+
const parts = componentId.split("/");
|
|
2851
|
+
const name = parts[parts.length - 1];
|
|
2852
|
+
const category = parts[0];
|
|
2853
|
+
const entry = discoveryMap?.get(componentId);
|
|
2854
|
+
let title = name;
|
|
2855
|
+
let description = "";
|
|
2856
|
+
let status = "draft";
|
|
2857
|
+
let deprecatedUse;
|
|
2858
|
+
let rawMd = "";
|
|
2859
|
+
if (entry?.mdPath) {
|
|
2860
|
+
try {
|
|
2861
|
+
rawMd = await readFile14(entry.mdPath, "utf-8");
|
|
2862
|
+
const t = rawMd.match(/^title:\s*(.+)$/m);
|
|
2863
|
+
const d = rawMd.match(/^description:\s*(.+)$/m);
|
|
2864
|
+
const s = rawMd.match(/^status:\s*(.+)$/m);
|
|
2865
|
+
const u = rawMd.match(/^deprecated-use:\s*(.+)$/m);
|
|
2866
|
+
if (t) title = t[1].trim();
|
|
2867
|
+
if (d) description = d[1].trim();
|
|
2868
|
+
if (s) {
|
|
2869
|
+
const sv = s[1].trim();
|
|
2870
|
+
if (sv === "stable" || sv === "deprecated" || sv === "draft") status = sv;
|
|
2871
|
+
}
|
|
2872
|
+
if (u) deprecatedUse = u[1].trim();
|
|
2873
|
+
} catch {
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
if (!rawMd && componentId.includes("~")) {
|
|
2877
|
+
const baseId = componentId.split("~")[0];
|
|
2878
|
+
const baseEntry = discoveryMap?.get(baseId);
|
|
2879
|
+
if (baseEntry?.mdPath) {
|
|
2880
|
+
try {
|
|
2881
|
+
rawMd = await readFile14(baseEntry.mdPath, "utf-8");
|
|
2882
|
+
} catch {
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const layout = await resolveLayoutMode(componentId, patternsDir, discoveryMap);
|
|
2887
|
+
const variantPrefix = `${componentId}~`;
|
|
2888
|
+
const variants = discoveryMap ? [...discoveryMap.keys()].filter((id) => id.startsWith(variantPrefix)).map((id) => {
|
|
2889
|
+
const variantToken = id.slice(variantPrefix.length);
|
|
2890
|
+
return { name: variantToken };
|
|
2891
|
+
}).sort((a, b) => a.name.localeCompare(b.name)) : [];
|
|
2892
|
+
const hbsPath = entry?.hbsPath ?? "";
|
|
2893
|
+
let hbs = "";
|
|
2894
|
+
try {
|
|
2895
|
+
hbs = hbsPath ? await readFile14(hbsPath, "utf-8") : "";
|
|
2896
|
+
} catch {
|
|
2897
|
+
}
|
|
2898
|
+
const hbsHighlighted = wrapWithCopyButton(highlight(hbs, "handlebars"), hbs);
|
|
2899
|
+
let jsonStr;
|
|
2900
|
+
let jsonHighlighted;
|
|
2901
|
+
if (entry?.jsonPath) {
|
|
2902
|
+
try {
|
|
2903
|
+
jsonStr = await readFile14(entry.jsonPath, "utf-8");
|
|
2904
|
+
jsonHighlighted = wrapWithCopyButton(highlight(jsonStr, "json"), jsonStr);
|
|
2905
|
+
} catch {
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
let schemaStr;
|
|
2909
|
+
let schemaHighlighted;
|
|
2910
|
+
if (entry?.schemaPath) {
|
|
2911
|
+
try {
|
|
2912
|
+
schemaStr = await readFile14(entry.schemaPath, "utf-8");
|
|
2913
|
+
schemaHighlighted = wrapWithCopyButton(highlight(schemaStr, "json"), schemaStr);
|
|
2914
|
+
} catch {
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
const source = {
|
|
2918
|
+
hbs,
|
|
2919
|
+
hbsHighlighted,
|
|
2920
|
+
...jsonStr !== void 0 ? { json: jsonStr, jsonHighlighted } : {},
|
|
2921
|
+
...schemaStr !== void 0 ? { schema: schemaStr, schemaHighlighted } : {},
|
|
2922
|
+
...rawMd ? { md: rawMd } : {}
|
|
2923
|
+
};
|
|
2924
|
+
const stripFrontmatter = rawMd.replace(/^---[\s\S]*?---\n?/, "").trim();
|
|
2925
|
+
const doc = stripFrontmatter ? await renderDoc(
|
|
2926
|
+
stripFrontmatter,
|
|
2927
|
+
(id, variant) => renderFn({ component: id, variant }).then((r) => r.html),
|
|
2928
|
+
discoveryMap ?? /* @__PURE__ */ new Map()
|
|
2929
|
+
) : void 0;
|
|
2930
|
+
let requireScripts;
|
|
2931
|
+
let requireAssets;
|
|
2932
|
+
if (flatNameToPath && discoveryMap) {
|
|
2933
|
+
const requires = await aggregateRequires(componentId, patternsDir, flatNameToPath, discoveryMap);
|
|
2934
|
+
if (requires.scripts.length) requireScripts = requires.scripts;
|
|
2935
|
+
if (requires.assets.length) requireAssets = requires.assets;
|
|
2936
|
+
}
|
|
2937
|
+
const classBorrowing = classOwnerIndex ? deriveClassBorrowingFields(hbs, componentId, classOwnerIndex, baseProvenance) : { styleUses: [], styleModules: [], inlineStyles: false, utilityClasses: false, unattributedClasses: [] };
|
|
2938
|
+
const relationships = {
|
|
2939
|
+
available: true,
|
|
2940
|
+
uses: compositionReg?.uses.get(componentId) ?? [],
|
|
2941
|
+
usedBy: compositionReg?.usedBy.get(componentId) ?? [],
|
|
2942
|
+
styleUses: classBorrowing.styleUses,
|
|
2943
|
+
styleUsesModules: scssGraph?.styleUsesModules.get(componentId) ?? [],
|
|
2944
|
+
styleModules: classBorrowing.styleModules,
|
|
2945
|
+
inlineStyles: classBorrowing.inlineStyles,
|
|
2946
|
+
utilityClasses: classBorrowing.utilityClasses,
|
|
2947
|
+
unattributedClasses: classBorrowing.unattributedClasses
|
|
2948
|
+
};
|
|
2949
|
+
return {
|
|
2950
|
+
id: componentId,
|
|
2951
|
+
title,
|
|
2952
|
+
description,
|
|
2953
|
+
category,
|
|
2954
|
+
status,
|
|
2955
|
+
layout,
|
|
2956
|
+
...deprecatedUse ? { deprecatedUse } : {},
|
|
2957
|
+
variants,
|
|
2958
|
+
source,
|
|
2959
|
+
doc,
|
|
2960
|
+
...requireScripts ? { requireScripts } : {},
|
|
2961
|
+
...requireAssets ? { requireAssets } : {},
|
|
2962
|
+
relationships,
|
|
2963
|
+
diagnostics: []
|
|
2964
|
+
// populated by the HTTP handler after render()
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// packages/dev-server/src/overview.ts
|
|
2969
|
+
import { join as join15 } from "node:path";
|
|
2970
|
+
async function buildFolderOverview(folderPath, patternsDir, renderFn, discoveryMap) {
|
|
2971
|
+
const category = folderPath.split("/")[0];
|
|
2972
|
+
let folderLayout = FULL_CATEGORIES.has(category) ? "full" : "centered";
|
|
2973
|
+
let doc;
|
|
2974
|
+
try {
|
|
2975
|
+
const rawIndex = await readFolderFile(patternsDir, join15(folderPath, "index.md"));
|
|
2976
|
+
if (rawIndex === null) throw new Error("no index.md");
|
|
2977
|
+
const m = rawIndex.match(/^preview:\s*(centered|full)$/m);
|
|
2978
|
+
if (m) folderLayout = m[1];
|
|
2979
|
+
const stripped = rawIndex.replace(/^---[\s\S]*?---\n?/, "").trim();
|
|
2980
|
+
if (stripped) {
|
|
2981
|
+
doc = await renderDoc(
|
|
2982
|
+
stripped,
|
|
2983
|
+
(id, variant) => renderFn({ component: id, variant }).then((r) => r.html),
|
|
2984
|
+
discoveryMap
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
} catch {
|
|
2988
|
+
}
|
|
2989
|
+
const components = await collectPublicLeaves(folderPath, patternsDir, discoveryMap);
|
|
2990
|
+
return { path: folderPath, doc, layout: folderLayout, components };
|
|
2991
|
+
}
|
|
2992
|
+
async function collectPublicLeaves(folderPath, patternsDir, discoveryMap) {
|
|
2993
|
+
const root = await buildSubtree(folderPath, patternsDir, discoveryMap);
|
|
2994
|
+
const items = [];
|
|
2995
|
+
const walk = (node) => {
|
|
2996
|
+
if (node.kind === "component" && node.visibility !== "private") {
|
|
2997
|
+
items.push({
|
|
2998
|
+
id: node.path,
|
|
2999
|
+
title: node.title,
|
|
3000
|
+
status: node.status ?? "draft",
|
|
3001
|
+
layout: node.layout ?? "centered",
|
|
3002
|
+
...node.source ? { source: node.source } : {}
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
node.children?.forEach(walk);
|
|
3006
|
+
};
|
|
3007
|
+
if (root) walk(root);
|
|
3008
|
+
return items;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
// packages/dev-server/src/preview-document.ts
|
|
3012
|
+
import { stat as stat3 } from "node:fs/promises";
|
|
3013
|
+
import { join as join16 } from "node:path";
|
|
3014
|
+
async function buildScriptVersions(ids, dirs) {
|
|
3015
|
+
const map = /* @__PURE__ */ new Map();
|
|
3016
|
+
await Promise.all(ids.map(async (id) => {
|
|
3017
|
+
for (const dir of Array.isArray(dirs) ? dirs : [dirs]) {
|
|
3018
|
+
try {
|
|
3019
|
+
const { mtimeMs } = await stat3(join16(dir, `${id}.ts`));
|
|
3020
|
+
map.set(id, mtimeMs);
|
|
3021
|
+
return;
|
|
3022
|
+
} catch {
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}));
|
|
3026
|
+
return map;
|
|
3027
|
+
}
|
|
3028
|
+
function buildPreviewDocument(html, closureIds, layout, themeNames, defaultTheme, requireScripts, svgContents, assetsCss, componentId, baseHref, scriptVersions, linkBase = false, themeSwitch = "attribute") {
|
|
3029
|
+
const bodyAttr = defaultTheme ? ` data-theme="${escapeAttr(defaultTheme)}"` : "";
|
|
3030
|
+
const layoutClass = layout === "full" ? "layout-full" : "layout-centered";
|
|
3031
|
+
const widthPost = layout === "centered" ? `window.parent.postMessage({type:'pd-width',width:(function(){var m=document.querySelector('main')||document.body,cs=getComputedStyle(document.body);return Math.ceil(m.scrollWidth+(parseFloat(cs.marginLeft)||0)+(parseFloat(cs.marginRight)||0));}())},'*');` : "";
|
|
3032
|
+
const heightMeasure = `document.documentElement.scrollHeight`;
|
|
3033
|
+
const resizeScript = layout === "centered" || layout === "full" ? `<script>(function(){var _t;new ResizeObserver(function(){clearTimeout(_t);_t=setTimeout(function(){window.parent.postMessage({type:'pd-height',height:${heightMeasure}},'*');${widthPost}},50);}).observe(document.body);})();</script>` : "";
|
|
3034
|
+
const baseTag = baseHref ? `<base href="${escapeAttr(baseHref)}">` : "";
|
|
3035
|
+
const baseLink = linkBase ? `<link rel="stylesheet" href="${PD_ROUTES.baseCss}">` : "";
|
|
3036
|
+
const assetCssLinks = (assetsCss ?? []).map((p) => `<link rel="stylesheet" href="/assets/${p}">`).join("\n");
|
|
3037
|
+
const themeLinks = themeSwitch === "stylesheet" ? defaultTheme ? `<link rel="stylesheet" href="${themeCssUrl(defaultTheme)}" data-pd-theme="${escapeAttr(defaultTheme)}">` : "" : themeNames.map((n) => `<link rel="stylesheet" href="${themeCssUrl(n)}">`).join("\n");
|
|
3038
|
+
const closureLinks = closureIds.map((id) => `<link rel="stylesheet" href="${componentCssUrl(id)}" data-pd-comp="${escapeAttr(id)}">`).join("\n");
|
|
3039
|
+
const symbolSets = (svgContents ?? []).join("\n");
|
|
3040
|
+
const scriptTags = (requireScripts ?? []).map((src) => {
|
|
3041
|
+
if (src.includes("://")) return `<script type="module" src="${src}"></script>`;
|
|
3042
|
+
const v = scriptVersions?.get(src);
|
|
3043
|
+
return `<script type="module" src="${scriptUrl(src, v ?? void 0)}"></script>`;
|
|
3044
|
+
}).join("\n");
|
|
3045
|
+
const componentAttr = componentId ? ` data-component-id="${escapeAttr(componentId)}"` : "";
|
|
3046
|
+
const layoutStyle = layout === "full" ? ' style="width:100%;max-width:none"' : ' style="max-width:max-content;margin-inline:auto"';
|
|
3047
|
+
return `<!DOCTYPE html>
|
|
3048
|
+
<html lang="en">
|
|
3049
|
+
<head>
|
|
3050
|
+
<meta charset="UTF-8">
|
|
3051
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3052
|
+
${baseTag}${baseLink}
|
|
3053
|
+
${assetCssLinks}
|
|
3054
|
+
${themeLinks}
|
|
3055
|
+
${closureLinks}
|
|
3056
|
+
</head>
|
|
3057
|
+
<body${bodyAttr}>
|
|
3058
|
+
${symbolSets}
|
|
3059
|
+
<main class="${layoutClass}"${layoutStyle}${componentAttr}>${html}</main>
|
|
3060
|
+
${resizeScript}
|
|
3061
|
+
${scriptTags}
|
|
3062
|
+
</body>
|
|
3063
|
+
</html>`;
|
|
3064
|
+
}
|
|
3065
|
+
function normalizeAssetRefs(html, rootNames) {
|
|
3066
|
+
if (rootNames.length === 0) return html;
|
|
3067
|
+
const alt = rootNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
3068
|
+
return html.replace(new RegExp(`(?:\\.\\.?/)+(${alt})/`, "g"), "/assets/$1/");
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// packages/dev-server/src/assets.ts
|
|
3072
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
3073
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
3074
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3075
|
+
function resolveDevUiDist(moduleUrl, exists = existsSync2) {
|
|
3076
|
+
const packaged = fileURLToPath4(new URL("./dev-ui/", moduleUrl));
|
|
3077
|
+
if (exists(`${packaged}main.js`)) return packaged;
|
|
3078
|
+
return fileURLToPath4(new URL("../../../apps/dev-ui/dist/", moduleUrl));
|
|
3079
|
+
}
|
|
3080
|
+
function resolveAxeCore(moduleUrl) {
|
|
3081
|
+
return createRequire2(moduleUrl).resolve("axe-core/axe.min.js");
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// packages/dev-server/src/routes.ts
|
|
3085
|
+
async function handleRequest(req, res, ctx) {
|
|
3086
|
+
const { cfg, state, render, tracker, globalData, devUiDist: devUiDist2 } = ctx;
|
|
3087
|
+
const {
|
|
3088
|
+
patternRoots,
|
|
3089
|
+
patternDirs,
|
|
3090
|
+
styleRoots,
|
|
3091
|
+
themesDir,
|
|
3092
|
+
defaultTheme,
|
|
3093
|
+
assetRoots,
|
|
3094
|
+
assetRootNames,
|
|
3095
|
+
assetsDir,
|
|
3096
|
+
assetsCss,
|
|
3097
|
+
scriptsDirs,
|
|
3098
|
+
withGlobalScripts,
|
|
3099
|
+
overviewsEnabled,
|
|
3100
|
+
documentBaseEnabled,
|
|
3101
|
+
themeSwitch,
|
|
3102
|
+
config,
|
|
3103
|
+
cwd,
|
|
3104
|
+
wsPort
|
|
3105
|
+
} = cfg;
|
|
3106
|
+
res.setHeader(PD_PROTOCOL_HEADER, String(PD_PROTOCOL_VERSION));
|
|
3107
|
+
try {
|
|
3108
|
+
const url = new URL(req.url ?? "/", "http://x");
|
|
3109
|
+
const pathname = url.pathname;
|
|
3110
|
+
if (pathname === PD_ROUTES.mainJs) {
|
|
3111
|
+
return await serveFile(res, join17(devUiDist2, "main.js"), "application/javascript");
|
|
3112
|
+
}
|
|
3113
|
+
if (pathname === PD_ROUTES.mainCss) {
|
|
3114
|
+
return await serveFile(res, join17(devUiDist2, "main.css"), "text/css");
|
|
3115
|
+
}
|
|
3116
|
+
if (pathname === PD_ROUTES.axeCore) {
|
|
3117
|
+
return await serveFile(res, resolveAxeCore(import.meta.url), "application/javascript");
|
|
3118
|
+
}
|
|
3119
|
+
if (pathname === PD_ROUTES.ruleTiers) {
|
|
3120
|
+
json(res, 200, rule_tiers_generated_default);
|
|
3121
|
+
return;
|
|
3122
|
+
}
|
|
3123
|
+
if (pathname === PD_ROUTES.shikiCss) {
|
|
3124
|
+
const css = getShikiCSS();
|
|
3125
|
+
res.writeHead(200, { "Content-Type": "text/css" });
|
|
3126
|
+
res.end(css);
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
if (pathname === PD_ROUTES.baseCss) {
|
|
3130
|
+
res.writeHead(200, { "Content-Type": "text/css" });
|
|
3131
|
+
res.end(state.baseCss);
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.componentCss) && pathname.endsWith(".css")) {
|
|
3135
|
+
const componentId = pathname.slice(PD_ROUTE_PREFIX.componentCss.length, -".css".length);
|
|
3136
|
+
const css = state.perComponentCss.get(componentId) ?? "";
|
|
3137
|
+
res.writeHead(200, { "Content-Type": "text/css" });
|
|
3138
|
+
res.end(css);
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.themeCss) && pathname.endsWith(".css")) {
|
|
3142
|
+
if (!themesDir) {
|
|
3143
|
+
res.writeHead(404);
|
|
3144
|
+
res.end();
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
const themeName = decodeURIComponent(
|
|
3148
|
+
basename4(pathname, ".css")
|
|
3149
|
+
);
|
|
3150
|
+
if (state.themeCss.has(themeName)) {
|
|
3151
|
+
res.writeHead(200, { "Content-Type": "text/css" });
|
|
3152
|
+
res.end(state.themeCss.get(themeName));
|
|
3153
|
+
} else {
|
|
3154
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3155
|
+
res.end("Theme not found");
|
|
3156
|
+
}
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.script) && pathname.endsWith(".js")) {
|
|
3160
|
+
if (scriptsDirs.length === 0) {
|
|
3161
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3162
|
+
res.end("No scriptsDir configured");
|
|
3163
|
+
return;
|
|
3164
|
+
}
|
|
3165
|
+
const scriptId = normalizeScriptId(decodeURIComponent(basename4(pathname, ".js")));
|
|
3166
|
+
try {
|
|
3167
|
+
if (!state.scriptCache.has(scriptId)) {
|
|
3168
|
+
const entryPoint = scriptsDirs.map((d) => join17(d, `${scriptId}.ts`)).find((f) => existsSync3(f)) ?? join17(scriptsDirs[0], `${scriptId}.ts`);
|
|
3169
|
+
const js = await compileScriptFile(entryPoint);
|
|
3170
|
+
state.scriptCache.set(scriptId, js);
|
|
3171
|
+
}
|
|
3172
|
+
res.writeHead(200, { "Content-Type": "application/javascript" });
|
|
3173
|
+
res.end(state.scriptCache.get(scriptId));
|
|
3174
|
+
} catch (e) {
|
|
3175
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
3176
|
+
res.end(`Script compile error: ${String(e)}`);
|
|
3177
|
+
}
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.assets) && assetRoots.length > 0) {
|
|
3181
|
+
const tail = decodeURIComponent(pathname.slice(PD_ROUTE_PREFIX.assets.length));
|
|
3182
|
+
const firstSeg = tail.split("/")[0];
|
|
3183
|
+
const namedIdx = assetRootNames.indexOf(firstSeg);
|
|
3184
|
+
const assetPath = namedIdx >= 0 ? join17(assetRoots[namedIdx], tail.slice(firstSeg.length + 1)) : join17(assetRoots[0], tail);
|
|
3185
|
+
try {
|
|
3186
|
+
await stat4(assetPath);
|
|
3187
|
+
return await serveFile(res, assetPath, mimeType(extname2(assetPath)));
|
|
3188
|
+
} catch {
|
|
3189
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3190
|
+
res.end("Asset not found");
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
if (pathname === PD_ROUTES.tree) {
|
|
3195
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3196
|
+
const tree = await buildComponentTree(patternDirs, overviewsEnabled, state.discoveryResult.discoveryMap);
|
|
3197
|
+
json(res, 200, tree);
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
if (pathname === PD_ROUTES.audit) {
|
|
3201
|
+
try {
|
|
3202
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3203
|
+
const discoveryMap = state.discoveryResult.discoveryMap;
|
|
3204
|
+
let graph;
|
|
3205
|
+
if (!state.auditSlices) {
|
|
3206
|
+
if (verboseTiming) console.time("[dev-server:timing] diagnostics seed");
|
|
3207
|
+
const r = await refreshDiagnosticsGraph(
|
|
3208
|
+
cwd,
|
|
3209
|
+
baseComponentIds(state.discoveryResult),
|
|
3210
|
+
render,
|
|
3211
|
+
state.discoveryResult.quarantineMap,
|
|
3212
|
+
discoveryMap,
|
|
3213
|
+
globalData
|
|
3214
|
+
);
|
|
3215
|
+
state.auditSlices = r.slices;
|
|
3216
|
+
graph = r.graph;
|
|
3217
|
+
if (verboseTiming) {
|
|
3218
|
+
console.timeEnd("[dev-server:timing] diagnostics seed");
|
|
3219
|
+
console.log(`[dev-server:timing] diagnostics seed: re-audited ${r.reaudited.length}, reused ${r.reused} (of ${state.auditSlices.size})`);
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
graph = assembleGraph([...state.auditSlices.values()]);
|
|
3223
|
+
await persistSlices(cwd, state.auditSlices, discoveryMap, globalData);
|
|
3224
|
+
}
|
|
3225
|
+
json(res, 200, { ...graph, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), _liveReaudits: state.liveReauditCount });
|
|
3226
|
+
} catch (e) {
|
|
3227
|
+
json(res, 500, { error: e instanceof Error ? e.message : String(e) });
|
|
3228
|
+
}
|
|
3229
|
+
return;
|
|
3230
|
+
}
|
|
3231
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.component)) {
|
|
3232
|
+
const componentId = pathname.slice(PD_ROUTE_PREFIX.component.length);
|
|
3233
|
+
const variant = url.searchParams.get("variant") ?? void 0;
|
|
3234
|
+
state.lastComponentId = componentId;
|
|
3235
|
+
try {
|
|
3236
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3237
|
+
const { handleMap } = state.discoveryResult;
|
|
3238
|
+
if (state.scssGraphDirty) {
|
|
3239
|
+
const ids = baseComponentIds(state.discoveryResult);
|
|
3240
|
+
const styleData = await buildAllStyleData(styleRoots, ids);
|
|
3241
|
+
state.scssModuleGraph = styleData.scssModuleGraph;
|
|
3242
|
+
state.classOwnerIndex = styleData.classOwnerIndex;
|
|
3243
|
+
state.baseProvenance = styleData.baseProvenance;
|
|
3244
|
+
state.perComponentCss = styleData.componentCssMap;
|
|
3245
|
+
state.baseCss = styleData.baseCss;
|
|
3246
|
+
state.scssGraphDirty = false;
|
|
3247
|
+
}
|
|
3248
|
+
if (!state.compositionRegistry) {
|
|
3249
|
+
state.compositionRegistry = await buildCompositionRegistry(patternDirs, handleMap, state.discoveryResult.discoveryMap);
|
|
3250
|
+
}
|
|
3251
|
+
const compReg = state.compositionRegistry;
|
|
3252
|
+
const detail = await buildComponentDetail(
|
|
3253
|
+
componentId,
|
|
3254
|
+
patternDirs,
|
|
3255
|
+
render,
|
|
3256
|
+
tracker,
|
|
3257
|
+
defaultTheme,
|
|
3258
|
+
handleMap,
|
|
3259
|
+
compReg,
|
|
3260
|
+
state.scssModuleGraph ?? void 0,
|
|
3261
|
+
state.classOwnerIndex ?? void 0,
|
|
3262
|
+
state.discoveryResult.discoveryMap,
|
|
3263
|
+
state.baseProvenance ?? void 0
|
|
3264
|
+
);
|
|
3265
|
+
const quarantineEntry = state.discoveryResult.quarantineMap.get(componentId);
|
|
3266
|
+
const { previewHtml, diagnostics } = await checkComponent(componentId, variant, render, quarantineEntry);
|
|
3267
|
+
const themeNames = themesDir ? [...state.themeCss.keys()] : [];
|
|
3268
|
+
const svgContents = detail.requireAssets && assetsDir ? await loadSvgAssets(detail.requireAssets, assetsDir) : [];
|
|
3269
|
+
const closureIds = withRenderedClassOwners(await computeCssClosure(
|
|
3270
|
+
componentId,
|
|
3271
|
+
compReg,
|
|
3272
|
+
patternDirs,
|
|
3273
|
+
state.classOwnerIndex ?? /* @__PURE__ */ new Map(),
|
|
3274
|
+
state.discoveryResult.discoveryMap
|
|
3275
|
+
), previewHtml, state.classOwnerIndex ?? /* @__PURE__ */ new Map());
|
|
3276
|
+
const previewScripts = withGlobalScripts(detail.requireScripts);
|
|
3277
|
+
const scriptVersions = scriptsDirs.length && previewScripts.length ? await buildScriptVersions(previewScripts, scriptsDirs) : void 0;
|
|
3278
|
+
const previewDoc = buildPreviewDocument(
|
|
3279
|
+
normalizeAssetRefs(previewHtml, assetRootNames),
|
|
3280
|
+
linkableClosure(closureIds, state.perComponentCss),
|
|
3281
|
+
detail.layout,
|
|
3282
|
+
themeNames,
|
|
3283
|
+
defaultTheme,
|
|
3284
|
+
previewScripts,
|
|
3285
|
+
svgContents,
|
|
3286
|
+
assetsCss,
|
|
3287
|
+
componentId,
|
|
3288
|
+
void 0,
|
|
3289
|
+
scriptVersions,
|
|
3290
|
+
documentBaseEnabled,
|
|
3291
|
+
themeSwitch
|
|
3292
|
+
);
|
|
3293
|
+
json(res, 200, { ...detail, document: previewDoc, diagnostics });
|
|
3294
|
+
} catch (e) {
|
|
3295
|
+
json(res, 404, { error: e instanceof Error ? e.message : String(e) });
|
|
3296
|
+
}
|
|
3297
|
+
return;
|
|
3298
|
+
}
|
|
3299
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.overview)) {
|
|
3300
|
+
const folderPath = pathname.slice(PD_ROUTE_PREFIX.overview.length);
|
|
3301
|
+
try {
|
|
3302
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3303
|
+
const overview = await buildFolderOverview(folderPath, patternDirs, render, state.discoveryResult.discoveryMap);
|
|
3304
|
+
json(res, 200, overview);
|
|
3305
|
+
} catch (e) {
|
|
3306
|
+
json(res, 404, { error: e instanceof Error ? e.message : String(e) });
|
|
3307
|
+
}
|
|
3308
|
+
return;
|
|
3309
|
+
}
|
|
3310
|
+
if (pathname === PD_ROUTES.themes) {
|
|
3311
|
+
const themes = cfg.themeDirs.length ? await discoverThemes(cfg.themeDirs) : [];
|
|
3312
|
+
const enriched = themes.map((t) => ({
|
|
3313
|
+
...t,
|
|
3314
|
+
tokens: state.themeTokens.get(t.name)
|
|
3315
|
+
}));
|
|
3316
|
+
json(res, 200, enriched);
|
|
3317
|
+
return;
|
|
3318
|
+
}
|
|
3319
|
+
if (pathname === PD_ROUTES.version) {
|
|
3320
|
+
json(res, 200, {
|
|
3321
|
+
startedAt: ctx.boot.startedAt,
|
|
3322
|
+
serverBundleMtime: ctx.boot.serverBundleMtime,
|
|
3323
|
+
protocol: PD_PROTOCOL_VERSION
|
|
3324
|
+
});
|
|
3325
|
+
return;
|
|
3326
|
+
}
|
|
3327
|
+
if (pathname === PD_ROUTES.config) {
|
|
3328
|
+
json(res, 200, { shell: config.shell ?? null, themeSwitch, defaultTheme: defaultTheme ?? "" });
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.preview)) {
|
|
3332
|
+
const componentId = pathname.slice(PD_ROUTE_PREFIX.preview.length);
|
|
3333
|
+
const variant = url.searchParams.get("variant") ?? void 0;
|
|
3334
|
+
const themeParam = url.searchParams.get("theme") ?? void 0;
|
|
3335
|
+
const knownThemes = themesDir ? [...state.themeCss.keys()] : [];
|
|
3336
|
+
const effectiveTheme = themeParam && knownThemes.includes(themeParam) ? themeParam : defaultTheme;
|
|
3337
|
+
try {
|
|
3338
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3339
|
+
const { handleMap: previewHandleMap, discoveryMap: previewDiscoveryMap } = state.discoveryResult;
|
|
3340
|
+
const [{ html }, layout] = await Promise.all([
|
|
3341
|
+
render({ component: componentId, variant }),
|
|
3342
|
+
resolveLayoutMode(componentId, patternDirs, previewDiscoveryMap)
|
|
3343
|
+
]);
|
|
3344
|
+
const themeNames = themesDir ? [...state.themeCss.keys()] : [];
|
|
3345
|
+
if (!state.compositionRegistry) {
|
|
3346
|
+
state.compositionRegistry = await buildCompositionRegistry(patternDirs, previewHandleMap, previewDiscoveryMap);
|
|
3347
|
+
}
|
|
3348
|
+
const requires = await aggregateRequires(componentId, patternDirs, previewHandleMap, previewDiscoveryMap);
|
|
3349
|
+
const svgContents = assetsDir ? await loadSvgAssets(requires.assets, assetsDir) : [];
|
|
3350
|
+
const closureIds = withRenderedClassOwners(await computeCssClosure(
|
|
3351
|
+
componentId,
|
|
3352
|
+
state.compositionRegistry,
|
|
3353
|
+
patternDirs,
|
|
3354
|
+
state.classOwnerIndex ?? /* @__PURE__ */ new Map(),
|
|
3355
|
+
previewDiscoveryMap
|
|
3356
|
+
), html, state.classOwnerIndex ?? /* @__PURE__ */ new Map());
|
|
3357
|
+
const previewScripts = withGlobalScripts(requires.scripts);
|
|
3358
|
+
const scriptVersions = scriptsDirs.length && previewScripts.length ? await buildScriptVersions(previewScripts, scriptsDirs) : void 0;
|
|
3359
|
+
const doc = buildPreviewDocument(
|
|
3360
|
+
normalizeAssetRefs(html, assetRootNames),
|
|
3361
|
+
linkableClosure(closureIds, state.perComponentCss),
|
|
3362
|
+
layout,
|
|
3363
|
+
themeNames,
|
|
3364
|
+
effectiveTheme,
|
|
3365
|
+
previewScripts,
|
|
3366
|
+
svgContents,
|
|
3367
|
+
assetsCss,
|
|
3368
|
+
componentId,
|
|
3369
|
+
void 0,
|
|
3370
|
+
scriptVersions,
|
|
3371
|
+
documentBaseEnabled,
|
|
3372
|
+
themeSwitch
|
|
3373
|
+
);
|
|
3374
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3375
|
+
res.end(doc);
|
|
3376
|
+
} catch (e) {
|
|
3377
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3378
|
+
res.end(`Render error: ${String(e)}`);
|
|
3379
|
+
}
|
|
3380
|
+
return;
|
|
3381
|
+
}
|
|
3382
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.source) && pathname.endsWith("/rendered")) {
|
|
3383
|
+
const componentId = pathname.slice(PD_ROUTE_PREFIX.source.length, -"/rendered".length);
|
|
3384
|
+
const variant = url.searchParams.get("variant") ?? void 0;
|
|
3385
|
+
try {
|
|
3386
|
+
const { html } = await render({ component: componentId, variant });
|
|
3387
|
+
const highlighted = wrapWithCopyButton(highlight(html, "html"), html);
|
|
3388
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3389
|
+
res.end(highlighted);
|
|
3390
|
+
} catch (e) {
|
|
3391
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
3392
|
+
res.end(`Render error: ${String(e)}`);
|
|
3393
|
+
}
|
|
3394
|
+
return;
|
|
3395
|
+
}
|
|
3396
|
+
if (pathname.startsWith(PD_ROUTE_PREFIX.source) && pathname.endsWith("/data")) {
|
|
3397
|
+
const componentId = pathname.slice(PD_ROUTE_PREFIX.source.length, -"/data".length);
|
|
3398
|
+
const variant = url.searchParams.get("variant") ?? void 0;
|
|
3399
|
+
try {
|
|
3400
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3401
|
+
const lookupId = variant ? `${componentId}~${variant}` : componentId;
|
|
3402
|
+
const jsonFile = state.discoveryResult.discoveryMap.get(lookupId)?.jsonPath;
|
|
3403
|
+
if (!jsonFile) throw new Error(`No data file for ${lookupId}`);
|
|
3404
|
+
const jsonStr = await readFile15(jsonFile, "utf-8");
|
|
3405
|
+
const highlighted = wrapWithCopyButton(highlight(jsonStr, "json"), jsonStr);
|
|
3406
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3407
|
+
res.end(highlighted);
|
|
3408
|
+
} catch (e) {
|
|
3409
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
3410
|
+
res.end(`Data file not found: ${String(e)}`);
|
|
3411
|
+
}
|
|
3412
|
+
return;
|
|
3413
|
+
}
|
|
3414
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
3415
|
+
res.end(buildShellHtml({ mode: "live", wsPort }));
|
|
3416
|
+
} catch (e) {
|
|
3417
|
+
console.error("[dev-server] Unhandled request error:", e);
|
|
3418
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
3419
|
+
res.end("Internal Server Error");
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
// packages/dev-server/src/ports.ts
|
|
3424
|
+
var PortInUseError = class extends Error {
|
|
3425
|
+
constructor(port2, message) {
|
|
3426
|
+
super(message);
|
|
3427
|
+
this.port = port2;
|
|
3428
|
+
this.name = "PortInUseError";
|
|
3429
|
+
}
|
|
3430
|
+
port;
|
|
3431
|
+
code = "EADDRINUSE";
|
|
3432
|
+
};
|
|
3433
|
+
var isAddressInUse = (e) => typeof e === "object" && e !== null && e.code === "EADDRINUSE";
|
|
3434
|
+
function describeBindError(e, port2, role, ports) {
|
|
3435
|
+
if (!isAddressInUse(e)) return e;
|
|
3436
|
+
const what = role === "workbench" ? "the workbench" : "live reload";
|
|
3437
|
+
const advice = role === "live reload" && ports.wsChosen ? "pd dev --ws-port <a free port>" : `pd dev --port ${ports.http + 100} (live reload then uses ${ports.http + 101})`;
|
|
3438
|
+
return new PortInUseError(
|
|
3439
|
+
port2,
|
|
3440
|
+
`Port ${port2} is already in use, so ${what} cannot start. Another \`pd dev\` is probably still running \u2014 stop it, or start this one on other ports:
|
|
3441
|
+
${advice}`
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
// packages/dev-server/src/watch.ts
|
|
3446
|
+
import { basename as basename5, extname as extname3, relative as relative4, resolve as resolve8 } from "node:path";
|
|
3447
|
+
import chokidar from "chokidar";
|
|
3448
|
+
function startWatching(ctx) {
|
|
3449
|
+
const { cfg, state, render, tracker, hotreloader } = ctx;
|
|
3450
|
+
const { patternsDir, stylesDir, patternRoots, patternDirs, swatchSlots, cwd } = cfg;
|
|
3451
|
+
const themesDir = cfg.config.themes ? cfg.themesDir : null;
|
|
3452
|
+
const scriptsDir = cfg.config.scriptsDir ? cfg.scriptsDirs[0] ?? null : null;
|
|
3453
|
+
const watchDirs = [patternsDir, stylesDir, ...themesDir ? [themesDir] : [], ...scriptsDir ? [scriptsDir] : []];
|
|
3454
|
+
const watcher = chokidar.watch(watchDirs, { ignoreInitial: true, persistent: true });
|
|
3455
|
+
async function handleComponentScss(filePath) {
|
|
3456
|
+
let recompiledFiles;
|
|
3457
|
+
let compileErrors;
|
|
3458
|
+
try {
|
|
3459
|
+
({ recompiledFiles, errors: compileErrors } = await tracker.update(filePath));
|
|
3460
|
+
} catch (e) {
|
|
3461
|
+
hotreloader.push({ type: "error", scope: "build", message: `SCSS error: ${String(e)}` });
|
|
3462
|
+
console.error("[dev-server] SCSS error:", e);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
for (const { file, message } of compileErrors) {
|
|
3466
|
+
hotreloader.push({
|
|
3467
|
+
type: "error",
|
|
3468
|
+
scope: "build",
|
|
3469
|
+
message: `SCSS ${basename5(file)} \u2014 ${message.split("\n")[0]}`
|
|
3470
|
+
});
|
|
3471
|
+
}
|
|
3472
|
+
state.scssGraphDirty = true;
|
|
3473
|
+
if (!state.discoveryResult) state.discoveryResult = await buildDiscoveryMap(patternRoots);
|
|
3474
|
+
const allIds = baseComponentIds(state.discoveryResult);
|
|
3475
|
+
const timestamp = Date.now();
|
|
3476
|
+
const effectiveFiles = recompiledFiles.length > 0 ? recompiledFiles : [resolve8(filePath)];
|
|
3477
|
+
const pending = [];
|
|
3478
|
+
for (const recompiledFile of effectiveFiles) {
|
|
3479
|
+
const compId = resolveScssToComponent(resolve8(recompiledFile), stylesDir, allIds);
|
|
3480
|
+
if (compId) {
|
|
3481
|
+
const newCss = await recomputeComponentCss(recompiledFile, compId, stylesDir, allIds);
|
|
3482
|
+
if (newCss !== null) {
|
|
3483
|
+
state.perComponentCss.set(compId, newCss);
|
|
3484
|
+
pending.push({
|
|
3485
|
+
type: "css-link-swap",
|
|
3486
|
+
componentId: compId,
|
|
3487
|
+
href: componentCssUrl(compId, timestamp)
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (state.classOwnerIndex) {
|
|
3493
|
+
if (verboseTiming) console.time("[dev-server:timing] classOwnerIndex patch");
|
|
3494
|
+
for (const recompiledFile of effectiveFiles) {
|
|
3495
|
+
const compId = resolveScssToComponent(resolve8(recompiledFile), stylesDir, allIds);
|
|
3496
|
+
if (compId) {
|
|
3497
|
+
await patchClassOwnerIndex(
|
|
3498
|
+
resolve8(recompiledFile),
|
|
3499
|
+
compId,
|
|
3500
|
+
stylesDir,
|
|
3501
|
+
allIds,
|
|
3502
|
+
state.classOwnerIndex
|
|
3503
|
+
);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
if (verboseTiming) console.timeEnd("[dev-server:timing] classOwnerIndex patch");
|
|
3507
|
+
}
|
|
3508
|
+
let orderedHrefs;
|
|
3509
|
+
if (state.lastComponentId && state.discoveryResult && state.compositionRegistry) {
|
|
3510
|
+
const staticClosure = await computeCssClosure(
|
|
3511
|
+
state.lastComponentId,
|
|
3512
|
+
state.compositionRegistry,
|
|
3513
|
+
patternDirs,
|
|
3514
|
+
state.classOwnerIndex ?? /* @__PURE__ */ new Map(),
|
|
3515
|
+
state.discoveryResult.discoveryMap
|
|
3516
|
+
);
|
|
3517
|
+
const renderedHtml = await render({ component: state.lastComponentId }).then((r) => r.html, () => "");
|
|
3518
|
+
const closureIds = withRenderedClassOwners(staticClosure, renderedHtml, state.classOwnerIndex ?? /* @__PURE__ */ new Map());
|
|
3519
|
+
orderedHrefs = linkableClosure(closureIds, state.perComponentCss).map((id) => componentCssUrl(id, timestamp));
|
|
3520
|
+
}
|
|
3521
|
+
for (const m of pending) hotreloader.push({ ...m, orderedHrefs });
|
|
3522
|
+
console.log(`[dev-server] CSS updated (recompiled: ${effectiveFiles.map((f) => basename5(f)).join(", ")})`);
|
|
3523
|
+
}
|
|
3524
|
+
async function handleMarkdown(filePath, event) {
|
|
3525
|
+
const abs = resolve8(filePath);
|
|
3526
|
+
const ownerIn = (dr) => {
|
|
3527
|
+
for (const [id, entry] of dr?.discoveryMap ?? []) {
|
|
3528
|
+
if (entry.mdPath && resolve8(entry.mdPath) === abs) return id.split("~")[0];
|
|
3529
|
+
}
|
|
3530
|
+
return void 0;
|
|
3531
|
+
};
|
|
3532
|
+
const rel = relative4(patternsDir, filePath);
|
|
3533
|
+
if (event === "change") state.discoveryResult ??= await buildDiscoveryMap(patternRoots);
|
|
3534
|
+
let owner = ownerIn(state.discoveryResult);
|
|
3535
|
+
if (event === "change") {
|
|
3536
|
+
hotreloader.push({ type: "tree-changed" });
|
|
3537
|
+
console.log(`[dev-server] Docs changed: ${rel} \u2192 tree-changed`);
|
|
3538
|
+
} else {
|
|
3539
|
+
await rebuildAndSignalTree(`Docs ${event === "add" ? "added" : "removed"}: ${rel}`);
|
|
3540
|
+
owner ??= ownerIn(state.discoveryResult);
|
|
3541
|
+
}
|
|
3542
|
+
for (const componentId of /* @__PURE__ */ new Set([owner, state.lastComponentId])) {
|
|
3543
|
+
if (componentId) hotreloader.push({ type: "content-swap", componentId });
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
watcher.on("change", async (filePath) => {
|
|
3547
|
+
const ext = extname3(filePath);
|
|
3548
|
+
if (ext === ".md" && filePath.startsWith(patternsDir)) {
|
|
3549
|
+
await handleMarkdown(filePath, "change");
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
if (ext === ".scss") {
|
|
3553
|
+
if (themesDir && filePath.startsWith(themesDir)) {
|
|
3554
|
+
const themeName = basename5(filePath, ".theme.scss");
|
|
3555
|
+
try {
|
|
3556
|
+
const css = await compileSassFile(filePath);
|
|
3557
|
+
state.themeCss.set(themeName, css);
|
|
3558
|
+
state.themeTokens.set(themeName, await extractThemeTokens(themeName, css, swatchSlots, themesDir));
|
|
3559
|
+
hotreloader.push({ type: "content-swap", componentId: state.lastComponentId });
|
|
3560
|
+
console.log(`[dev-server] Theme CSS updated: ${themeName}`);
|
|
3561
|
+
} catch (e) {
|
|
3562
|
+
hotreloader.push({ type: "error", scope: "build", message: `SCSS error: ${String(e)}` });
|
|
3563
|
+
}
|
|
3564
|
+
} else {
|
|
3565
|
+
await handleComponentScss(filePath);
|
|
3566
|
+
}
|
|
3567
|
+
} else if (ext === ".hbs" || ext === ".json") {
|
|
3568
|
+
const componentId = state.discoveryResult ? resolveComponentForFile(filePath, state.discoveryResult) : null;
|
|
3569
|
+
state.discoveryResult = null;
|
|
3570
|
+
state.compositionRegistry = null;
|
|
3571
|
+
if (!componentId) {
|
|
3572
|
+
console.warn(`[dev-server] Could not resolve component for: ${filePath}`);
|
|
3573
|
+
return;
|
|
3574
|
+
}
|
|
3575
|
+
try {
|
|
3576
|
+
await render({ component: componentId });
|
|
3577
|
+
hotreloader.push({ type: "content-swap", componentId });
|
|
3578
|
+
console.log(`[dev-server] Component changed: ${componentId}`);
|
|
3579
|
+
} catch (e) {
|
|
3580
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3581
|
+
hotreloader.push({ type: "error", scope: "render", componentId, message: msg });
|
|
3582
|
+
console.error(`[dev-server] Render error [${componentId}]:`, msg);
|
|
3583
|
+
}
|
|
3584
|
+
if (state.auditSlices) {
|
|
3585
|
+
state.auditSlices.set(componentId, await auditComponent(componentId, render, void 0));
|
|
3586
|
+
state.liveReauditCount += 1;
|
|
3587
|
+
if (verboseTiming) console.log(`[dev-server:timing] diagnostics: live re-audit ${componentId} (1 component; total ${state.liveReauditCount})`);
|
|
3588
|
+
}
|
|
3589
|
+
} else if (ext === ".ts" && scriptsDir && filePath.startsWith(scriptsDir)) {
|
|
3590
|
+
const scriptId = basename5(filePath, ".ts");
|
|
3591
|
+
state.scriptCache.delete(scriptId);
|
|
3592
|
+
if (state.lastComponentId) {
|
|
3593
|
+
hotreloader.push({ type: "content-swap", componentId: state.lastComponentId });
|
|
3594
|
+
}
|
|
3595
|
+
console.log(`[dev-server] Script changed: ${scriptId} \u2192 srcdoc reload dispatched`);
|
|
3596
|
+
}
|
|
3597
|
+
});
|
|
3598
|
+
async function rebuildAndSignalTree(label) {
|
|
3599
|
+
const dr = await buildDiscoveryMap(patternRoots);
|
|
3600
|
+
state.discoveryResult = dr;
|
|
3601
|
+
state.compositionRegistry = null;
|
|
3602
|
+
state.auditSlices = null;
|
|
3603
|
+
await surfaceCollisions(dr.collisions, cwd, hotreloader);
|
|
3604
|
+
const reg = await buildCompositionRegistry(patternDirs, dr.handleMap, dr.discoveryMap);
|
|
3605
|
+
await surfaceFlattening(dr.discoveryMap, reg, cwd);
|
|
3606
|
+
hotreloader.push({ type: "tree-changed" });
|
|
3607
|
+
console.log(`[dev-server] ${label} \u2192 tree-changed`);
|
|
3608
|
+
}
|
|
3609
|
+
watcher.on("add", async (filePath) => {
|
|
3610
|
+
const ext = extname3(filePath);
|
|
3611
|
+
if (ext === ".scss" && !(themesDir && filePath.startsWith(themesDir))) {
|
|
3612
|
+
await handleComponentScss(filePath);
|
|
3613
|
+
} else if (ext === ".hbs" && filePath.startsWith(patternsDir)) {
|
|
3614
|
+
await rebuildAndSignalTree(`Pattern added: ${relative4(patternsDir, filePath)}`);
|
|
3615
|
+
} else if (ext === ".md" && filePath.startsWith(patternsDir)) {
|
|
3616
|
+
await handleMarkdown(filePath, "add");
|
|
3617
|
+
}
|
|
3618
|
+
});
|
|
3619
|
+
watcher.on("unlink", async (filePath) => {
|
|
3620
|
+
if (extname3(filePath) === ".hbs" && filePath.startsWith(patternsDir)) {
|
|
3621
|
+
await rebuildAndSignalTree(`Pattern removed: ${relative4(patternsDir, filePath)}`);
|
|
3622
|
+
} else if (extname3(filePath) === ".md" && filePath.startsWith(patternsDir)) {
|
|
3623
|
+
await handleMarkdown(filePath, "unlink");
|
|
3624
|
+
}
|
|
3625
|
+
});
|
|
3626
|
+
watcher.on("unlinkDir", async (dirPath) => {
|
|
3627
|
+
if (dirPath.startsWith(patternsDir) && dirPath !== patternsDir) {
|
|
3628
|
+
await rebuildAndSignalTree(`Pattern dir removed: ${relative4(patternsDir, dirPath)}`);
|
|
3629
|
+
}
|
|
3630
|
+
});
|
|
3631
|
+
return watcher;
|
|
3632
|
+
}
|
|
3633
|
+
|
|
3634
|
+
// packages/dev-server/src/export-io.ts
|
|
3635
|
+
import { readFile as readFile16, writeFile as writeFile4, mkdir as mkdir4 } from "node:fs/promises";
|
|
3636
|
+
import { dirname as dirname6, join as join18, relative as relative5, resolve as resolve9 } from "node:path";
|
|
3637
|
+
|
|
3638
|
+
// packages/dev-server/src/export.ts
|
|
3639
|
+
var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3640
|
+
var idFolder = (id) => {
|
|
3641
|
+
const i = id.lastIndexOf("/");
|
|
3642
|
+
return i < 0 ? "" : id.slice(0, i);
|
|
3643
|
+
};
|
|
3644
|
+
var idStem = (id) => {
|
|
3645
|
+
const i = id.lastIndexOf("/");
|
|
3646
|
+
return i < 0 ? id : id.slice(i + 1);
|
|
3647
|
+
};
|
|
3648
|
+
function deriveFamilyNaming(members) {
|
|
3649
|
+
const folders = new Set(members.map((m) => idFolder(m.relPath)));
|
|
3650
|
+
if (folders.size !== 1) return null;
|
|
3651
|
+
const folder = [...folders][0];
|
|
3652
|
+
const stems = members.map((m) => idStem(m.relPath));
|
|
3653
|
+
let lcp = stems[0];
|
|
3654
|
+
for (const s of stems.slice(1)) {
|
|
3655
|
+
let i = 0;
|
|
3656
|
+
while (i < lcp.length && i < s.length && lcp[i] === s[i]) i++;
|
|
3657
|
+
lcp = lcp.slice(0, i);
|
|
3658
|
+
}
|
|
3659
|
+
const dash = lcp.lastIndexOf("-");
|
|
3660
|
+
const baseStem = dash > 0 ? lcp.slice(0, dash) : lcp;
|
|
3661
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
3662
|
+
for (const m of members) {
|
|
3663
|
+
const stem = idStem(m.relPath);
|
|
3664
|
+
const rest = stem === baseStem ? "" : stem.slice(baseStem.length).replace(/^-/, "");
|
|
3665
|
+
tokens.set(m.id, rest);
|
|
3666
|
+
}
|
|
3667
|
+
const folderBase = idStem(folder);
|
|
3668
|
+
const baseId = folder === "" ? baseStem : folderBase === baseStem ? folder : `${folder}/${baseStem}`;
|
|
3669
|
+
return { baseId, tokens };
|
|
3670
|
+
}
|
|
3671
|
+
function analyzeRootClass(hbs, root) {
|
|
3672
|
+
const m = hbs.match(new RegExp(`class="([^"]*\\b${escapeRe(root)}\\b[^"]*)"`));
|
|
3673
|
+
if (!m) return null;
|
|
3674
|
+
const value = m[1];
|
|
3675
|
+
const hbsTail = (value.match(/\{\{[^}]*\}\}/g) ?? []).join("");
|
|
3676
|
+
const staticMods = value.replace(/\{\{[^}]*\}\}/g, " ").split(/\s+/).filter(Boolean).filter((t) => t !== root);
|
|
3677
|
+
return { attr: m[0], staticMods, hbsTail };
|
|
3678
|
+
}
|
|
3679
|
+
function collapseFamily(cluster, members) {
|
|
3680
|
+
const naming = deriveFamilyNaming(members);
|
|
3681
|
+
if (!naming) return { skip: "cross-folder" };
|
|
3682
|
+
const root = cluster.root;
|
|
3683
|
+
const byId = new Map(members.map((m) => [m.id, m.hbs]));
|
|
3684
|
+
const analyses = /* @__PURE__ */ new Map();
|
|
3685
|
+
let tail0 = null;
|
|
3686
|
+
for (const id of cluster.members) {
|
|
3687
|
+
const hbs = byId.get(id) ?? "";
|
|
3688
|
+
const a = analyzeRootClass(hbs, root);
|
|
3689
|
+
if (!a) return { skip: "not-lossless-shape-a" };
|
|
3690
|
+
if (tail0 === null) tail0 = a.hbsTail;
|
|
3691
|
+
else if (a.hbsTail !== tail0) return { skip: "not-lossless-shape-a" };
|
|
3692
|
+
const normalized = hbs.replace(a.attr, `class="${root}${a.hbsTail}"`);
|
|
3693
|
+
analyses.set(id, { staticMods: a.staticMods, hbsTail: a.hbsTail, normalized });
|
|
3694
|
+
}
|
|
3695
|
+
const normSet = new Set([...analyses.values()].map((v) => v.normalized));
|
|
3696
|
+
if (normSet.size !== 1) return { skip: "not-lossless-shape-a" };
|
|
3697
|
+
const base0 = byId.get(cluster.members[0]) ?? "";
|
|
3698
|
+
const a0 = analyzeRootClass(base0, root);
|
|
3699
|
+
const baseHbs = base0.replace(a0.attr, `class="${root}{{ modifier }}${a0.hbsTail}"`);
|
|
3700
|
+
const dataById = new Map(members.map((m) => [m.id, parseData(m.dataJson)]));
|
|
3701
|
+
const baseDefaultId = cluster.members.find((id) => (naming.tokens.get(id) ?? "") === "");
|
|
3702
|
+
const baseData = baseDefaultId ? dataById.get(baseDefaultId) : {};
|
|
3703
|
+
const variants = cluster.members.map((id) => {
|
|
3704
|
+
const mods = analyses.get(id).staticMods;
|
|
3705
|
+
const token = naming.tokens.get(id) ?? "";
|
|
3706
|
+
const delta = token === "" ? {} : topLevelDelta(dataById.get(id), baseData);
|
|
3707
|
+
return {
|
|
3708
|
+
token,
|
|
3709
|
+
sourceId: id,
|
|
3710
|
+
modifier: mods.length ? " " + mods.join(" ") : "",
|
|
3711
|
+
...Object.keys(delta).length ? { data: delta } : {}
|
|
3712
|
+
};
|
|
3713
|
+
});
|
|
3714
|
+
return { family: { baseId: naming.baseId, root, baseHbs, ...Object.keys(baseData).length ? { baseData } : {}, variants } };
|
|
3715
|
+
}
|
|
3716
|
+
function buildRewriteMap(families) {
|
|
3717
|
+
const map = /* @__PURE__ */ new Map();
|
|
3718
|
+
for (const fam of families) {
|
|
3719
|
+
const baseHandle = deriveHandle(fam.baseId);
|
|
3720
|
+
for (const v of fam.variants) {
|
|
3721
|
+
map.set(deriveHandle(v.sourceId), { baseHandle, modifier: v.modifier });
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
return map;
|
|
3725
|
+
}
|
|
3726
|
+
function rewriteIncludes(hbs, map) {
|
|
3727
|
+
let rewrites = 0;
|
|
3728
|
+
const blockRenames = /* @__PURE__ */ new Map();
|
|
3729
|
+
let out = hbs.replace(
|
|
3730
|
+
/(\{\{~?)(#?)>(\s*)("[^"]+"|[^\s}"]+)([\s\S]*?)(~?\}\})/g,
|
|
3731
|
+
(full, open, hash, _ws, rawName, rest, close) => {
|
|
3732
|
+
const bare = rawName.replace(/^"|"$/g, "");
|
|
3733
|
+
const hit = map.get(bare);
|
|
3734
|
+
if (!hit) return full;
|
|
3735
|
+
rewrites++;
|
|
3736
|
+
if (hash === "#") blockRenames.set(bare, hit.baseHandle);
|
|
3737
|
+
const existing = rest.trim();
|
|
3738
|
+
const merged = [existing, `modifier="${hit.modifier}"`].filter(Boolean).join(" ");
|
|
3739
|
+
return `${open}${hash}> ${hit.baseHandle} ${merged}${close}`;
|
|
3740
|
+
}
|
|
3741
|
+
);
|
|
3742
|
+
if (blockRenames.size) {
|
|
3743
|
+
out = out.replace(
|
|
3744
|
+
/(\{\{~?)\/(\s*)("[^"]+"|[^\s}"]+)(\s*~?\}\})/g,
|
|
3745
|
+
(full, open, _ws, rawName, close) => {
|
|
3746
|
+
const bare = rawName.replace(/^"|"$/g, "");
|
|
3747
|
+
const base2 = blockRenames.get(bare);
|
|
3748
|
+
return base2 ? `${open}/${base2}${close}` : full;
|
|
3749
|
+
}
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3752
|
+
return { hbs: out, rewrites };
|
|
3753
|
+
}
|
|
3754
|
+
function parseData(raw) {
|
|
3755
|
+
if (!raw) return {};
|
|
3756
|
+
try {
|
|
3757
|
+
return JSON.parse(raw);
|
|
3758
|
+
} catch {
|
|
3759
|
+
return {};
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
function topLevelDelta(variant, base2) {
|
|
3763
|
+
const out = {};
|
|
3764
|
+
for (const [k, v] of Object.entries(variant)) {
|
|
3765
|
+
if (JSON.stringify(v) !== JSON.stringify(base2[k])) out[k] = v;
|
|
3766
|
+
}
|
|
3767
|
+
return out;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
// packages/dev-server/src/export-io.ts
|
|
3771
|
+
async function planExport(config, cwd) {
|
|
3772
|
+
const patternsDir = resolve9(cwd, config.patternsDir);
|
|
3773
|
+
const discovery = await buildDiscoveryMap(patternsDir);
|
|
3774
|
+
const registry = await buildCompositionRegistry(patternsDir, discovery.handleMap, discovery.discoveryMap);
|
|
3775
|
+
const inputs = [];
|
|
3776
|
+
const hbsById = /* @__PURE__ */ new Map();
|
|
3777
|
+
const relPathById = /* @__PURE__ */ new Map();
|
|
3778
|
+
const dataJsonById = /* @__PURE__ */ new Map();
|
|
3779
|
+
for (const [id, entry] of discovery.discoveryMap) {
|
|
3780
|
+
if (id.includes("~")) continue;
|
|
3781
|
+
if (!entry.hbsPath) continue;
|
|
3782
|
+
try {
|
|
3783
|
+
const hbs = await readFile16(entry.hbsPath, "utf-8");
|
|
3784
|
+
inputs.push({ id, hbs });
|
|
3785
|
+
hbsById.set(id, hbs);
|
|
3786
|
+
relPathById.set(id, relative5(patternsDir, entry.hbsPath).replace(/\.hbs$/, ""));
|
|
3787
|
+
if (entry.jsonPath) {
|
|
3788
|
+
try {
|
|
3789
|
+
dataJsonById.set(id, await readFile16(entry.jsonPath, "utf-8"));
|
|
3790
|
+
} catch {
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
} catch {
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
const hasEdge = (a, b) => (registry.uses.get(a)?.includes(b) ?? false) || (registry.uses.get(b)?.includes(a) ?? false);
|
|
3797
|
+
const clusters = detectFlattening(inputs, { hasEdge });
|
|
3798
|
+
const collapse = [];
|
|
3799
|
+
const threaded = [];
|
|
3800
|
+
const deferred = [];
|
|
3801
|
+
const skipped = [];
|
|
3802
|
+
for (const cluster of clusters) {
|
|
3803
|
+
const hazardSites = {};
|
|
3804
|
+
for (const m of cluster.members) {
|
|
3805
|
+
const sites = registry.usedBy.get(m) ?? [];
|
|
3806
|
+
if (sites.length) hazardSites[m] = sites;
|
|
3807
|
+
}
|
|
3808
|
+
const members = cluster.members.map((id) => ({
|
|
3809
|
+
id,
|
|
3810
|
+
relPath: relPathById.get(id) ?? id,
|
|
3811
|
+
hbs: hbsById.get(id) ?? "",
|
|
3812
|
+
...dataJsonById.has(id) ? { dataJson: dataJsonById.get(id) } : {}
|
|
3813
|
+
}));
|
|
3814
|
+
const result = collapseFamily(cluster, members);
|
|
3815
|
+
if (Object.keys(hazardSites).length) {
|
|
3816
|
+
if (result.family) threaded.push({ family: result.family, hazardSites });
|
|
3817
|
+
else deferred.push({ members: cluster.members, hazardSites });
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
if (result.family) collapse.push(result.family);
|
|
3821
|
+
else skipped.push({ members: cluster.members, reason: result.skip });
|
|
3822
|
+
}
|
|
3823
|
+
return { collapse, threaded, deferred, skipped };
|
|
3824
|
+
}
|
|
3825
|
+
async function writeExport(families, sink) {
|
|
3826
|
+
const files = [];
|
|
3827
|
+
for (const family of families) {
|
|
3828
|
+
const baseRel = `${family.baseId}.hbs`;
|
|
3829
|
+
const basePath = join18(sink, baseRel);
|
|
3830
|
+
await mkdir4(dirname6(basePath), { recursive: true });
|
|
3831
|
+
await writeFile4(basePath, family.baseHbs, "utf-8");
|
|
3832
|
+
files.push(baseRel);
|
|
3833
|
+
if (family.baseData) {
|
|
3834
|
+
const dataRel = `${family.baseId}.json`;
|
|
3835
|
+
await writeFile4(join18(sink, dataRel), JSON.stringify(family.baseData, null, 2) + "\n", "utf-8");
|
|
3836
|
+
files.push(dataRel);
|
|
3837
|
+
}
|
|
3838
|
+
for (const v of family.variants) {
|
|
3839
|
+
if (v.token === "") continue;
|
|
3840
|
+
const overlayRel = `${family.baseId}~${v.token}.json`;
|
|
3841
|
+
const overlay = { modifier: v.modifier, ...v.data ?? {} };
|
|
3842
|
+
await writeFile4(join18(sink, overlayRel), JSON.stringify(overlay, null, 2) + "\n", "utf-8");
|
|
3843
|
+
files.push(overlayRel);
|
|
3844
|
+
}
|
|
3845
|
+
}
|
|
3846
|
+
files.sort();
|
|
3847
|
+
return { files };
|
|
3848
|
+
}
|
|
3849
|
+
async function writeThreadedIncluders(threaded, patternsDir, sink) {
|
|
3850
|
+
if (!threaded.length) return { files: [], rewrites: 0 };
|
|
3851
|
+
const { discoveryMap } = await buildDiscoveryMap(patternsDir);
|
|
3852
|
+
const map = buildRewriteMap(threaded.map((t) => t.family));
|
|
3853
|
+
const includerIds = /* @__PURE__ */ new Set();
|
|
3854
|
+
for (const t of threaded) {
|
|
3855
|
+
for (const sites of Object.values(t.hazardSites)) {
|
|
3856
|
+
for (const s of sites) includerIds.add(s);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
const files = [];
|
|
3860
|
+
let rewrites = 0;
|
|
3861
|
+
for (const id of [...includerIds].sort()) {
|
|
3862
|
+
const hbsPath = discoveryMap.get(id)?.hbsPath;
|
|
3863
|
+
if (!hbsPath) continue;
|
|
3864
|
+
let src = "";
|
|
3865
|
+
try {
|
|
3866
|
+
src = await readFile16(hbsPath, "utf-8");
|
|
3867
|
+
} catch {
|
|
3868
|
+
continue;
|
|
3869
|
+
}
|
|
3870
|
+
const res = rewriteIncludes(src, map);
|
|
3871
|
+
if (res.rewrites === 0) continue;
|
|
3872
|
+
rewrites += res.rewrites;
|
|
3873
|
+
const rel = relative5(patternsDir, hbsPath);
|
|
3874
|
+
const outPath = join18(sink, rel);
|
|
3875
|
+
await mkdir4(dirname6(outPath), { recursive: true });
|
|
3876
|
+
await writeFile4(outPath, res.hbs, "utf-8");
|
|
3877
|
+
files.push(rel);
|
|
3878
|
+
}
|
|
3879
|
+
files.sort();
|
|
3880
|
+
return { files, rewrites };
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// packages/dev-server/src/audit-format.ts
|
|
3884
|
+
function renderAuditMarkdown(r) {
|
|
3885
|
+
const lines = [];
|
|
3886
|
+
lines.push(`# Diagnostics audit${r.subjectSha ? ` \u2014 ${r.subjectSha}` : ""}`);
|
|
3887
|
+
lines.push("");
|
|
3888
|
+
lines.push(`**${r.headline}**`);
|
|
3889
|
+
lines.push("");
|
|
3890
|
+
lines.push(`Render outcomes: ${r.renderOutcomes.ok} ok, ${r.renderOutcomes.blank} blank, ${r.renderOutcomes.error} error.`);
|
|
3891
|
+
lines.push("");
|
|
3892
|
+
lines.push("## Rules by blast radius (components affected; occurrences secondary)");
|
|
3893
|
+
lines.push("");
|
|
3894
|
+
lines.push("| rule | components | occurrences | tag |");
|
|
3895
|
+
lines.push("| --- | ---: | ---: | --- |");
|
|
3896
|
+
for (const rule of r.rulesByBlast) {
|
|
3897
|
+
lines.push(`| \`${rule.engine}:${rule.ruleId}\` | ${rule.components} | ${rule.occurrences} | ${rule.tags.join(", ")} |`);
|
|
3898
|
+
}
|
|
3899
|
+
lines.push("");
|
|
3900
|
+
lines.push("## Per component");
|
|
3901
|
+
lines.push("");
|
|
3902
|
+
for (const c3 of r.perComponent) {
|
|
3903
|
+
lines.push(`### ${c3.id} _(render: ${c3.render})_`);
|
|
3904
|
+
if (c3.occurrences.length === 0) lines.push("- (no defects; render outcome noted above)");
|
|
3905
|
+
for (const o of c3.occurrences) {
|
|
3906
|
+
lines.push(`- \`${o.engine}:${o.ruleId}\` **[${o.tag}]** ${o.message}${o.domPath ? ` \u2014 \`${o.domPath}\`` : ""}`);
|
|
3907
|
+
}
|
|
3908
|
+
lines.push("");
|
|
3909
|
+
}
|
|
3910
|
+
lines.push(SCOPE_BOUNDARY);
|
|
3911
|
+
lines.push("");
|
|
3912
|
+
return lines.join("\n");
|
|
3913
|
+
}
|
|
3914
|
+
function renderAuditText(r) {
|
|
3915
|
+
const lines = [];
|
|
3916
|
+
lines.push(`Diagnostics audit${r.subjectSha ? ` \u2014 ${r.subjectSha}` : ""}`);
|
|
3917
|
+
lines.push("");
|
|
3918
|
+
lines.push(r.headline);
|
|
3919
|
+
lines.push("");
|
|
3920
|
+
lines.push(`Render outcomes: ${r.renderOutcomes.ok} ok, ${r.renderOutcomes.blank} blank, ${r.renderOutcomes.error} error.`);
|
|
3921
|
+
lines.push("");
|
|
3922
|
+
lines.push("Rules by blast radius (components affected; occurrences secondary):");
|
|
3923
|
+
if (r.rulesByBlast.length === 0) lines.push(" (none)");
|
|
3924
|
+
for (const rule of r.rulesByBlast) {
|
|
3925
|
+
lines.push(` ${rule.engine}:${rule.ruleId} \u2014 components ${rule.components}, occurrences ${rule.occurrences} [${rule.tags.join(", ")}]`);
|
|
3926
|
+
}
|
|
3927
|
+
lines.push("");
|
|
3928
|
+
lines.push("Per component:");
|
|
3929
|
+
if (r.perComponent.length === 0) lines.push(" (no components with findings or non-ok render)");
|
|
3930
|
+
for (const c3 of r.perComponent) {
|
|
3931
|
+
lines.push(` ${c3.id} (render: ${c3.render})`);
|
|
3932
|
+
if (c3.occurrences.length === 0) lines.push(" (no defects; render outcome noted above)");
|
|
3933
|
+
for (const o of c3.occurrences) {
|
|
3934
|
+
lines.push(` ${o.engine}:${o.ruleId} [${o.tag}] ${o.message}${o.domPath ? ` \u2014 ${o.domPath}` : ""}`);
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
lines.push("");
|
|
3938
|
+
lines.push(r.scope);
|
|
3939
|
+
lines.push("");
|
|
3940
|
+
return lines.join("\n");
|
|
3941
|
+
}
|
|
3942
|
+
function renderAuditJson(r) {
|
|
3943
|
+
return JSON.stringify(r, null, 2);
|
|
3944
|
+
}
|
|
3945
|
+
function formatAudit(graph, format = "md", opts = {}) {
|
|
3946
|
+
const rollup = buildAuditRollup(graph, opts);
|
|
3947
|
+
return format === "json" ? renderAuditJson(rollup) : format === "text" ? renderAuditText(rollup) : renderAuditMarkdown(rollup);
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
// packages/dev-server/src/index.ts
|
|
3951
|
+
var SILENT_RELOADER = {
|
|
3952
|
+
port: 0,
|
|
3953
|
+
listening: Promise.resolve(),
|
|
3954
|
+
push() {
|
|
3955
|
+
},
|
|
3956
|
+
retain() {
|
|
3957
|
+
},
|
|
3958
|
+
close: async () => {
|
|
3959
|
+
}
|
|
3960
|
+
};
|
|
3961
|
+
var devUiDist = resolveDevUiDist(import.meta.url);
|
|
3962
|
+
async function startDevServer(config, cwd, opts = {}) {
|
|
3963
|
+
const startTime = performance.now();
|
|
3964
|
+
const cfg = resolveServerConfig(config, cwd, opts, loadPatternPackages(config.packages, cwd));
|
|
3965
|
+
const {
|
|
3966
|
+
port: port2,
|
|
3967
|
+
wsPort,
|
|
3968
|
+
patternsDir,
|
|
3969
|
+
stylesDir,
|
|
3970
|
+
patternRoots,
|
|
3971
|
+
styleRoots,
|
|
3972
|
+
themesDir,
|
|
3973
|
+
defaultTheme,
|
|
3974
|
+
swatchSlots
|
|
3975
|
+
} = cfg;
|
|
3976
|
+
for (const p of cfg.packages) {
|
|
3977
|
+
console.log(`[dev-server] pattern package ${p.name}@${p.version}${p.direct ? "" : " (peer)"}`);
|
|
3978
|
+
}
|
|
3979
|
+
await initHighlighter();
|
|
3980
|
+
const helperRegistrars = [];
|
|
3981
|
+
for (const path of cfg.helperPaths) helperRegistrars.push(...await loadHelperRegistrars(path));
|
|
3982
|
+
const globalData = cfg.dataDirs.length ? await loadGlobalData(cfg.dataDirs) : void 0;
|
|
3983
|
+
const render = createRenderer({
|
|
3984
|
+
patternsDir,
|
|
3985
|
+
patternRoots,
|
|
3986
|
+
themesDir: themesDir ?? void 0,
|
|
3987
|
+
defaultTheme,
|
|
3988
|
+
...helperRegistrars && helperRegistrars.length ? { helperRegistrars } : {},
|
|
3989
|
+
...globalData ? { globalData } : {}
|
|
3990
|
+
});
|
|
3991
|
+
const live = opts.live !== false;
|
|
3992
|
+
const hotreloader = live ? createHotReloader(wsPort) : SILENT_RELOADER;
|
|
3993
|
+
const wsPortChosen = opts.wsPort !== void 0 || process.env["PD_WS_PORT"] !== void 0;
|
|
3994
|
+
await hotreloader.listening.catch((e) => {
|
|
3995
|
+
throw describeBindError(e, wsPort, "live reload", { http: port2, wsChosen: wsPortChosen });
|
|
3996
|
+
});
|
|
3997
|
+
const tracker = await createSassDependencyTracker([stylesDir]);
|
|
3998
|
+
console.log("[dev-server] SCSS compiled (dependency tracker ready)");
|
|
3999
|
+
const state = createServerState();
|
|
4000
|
+
if (cfg.themeDirs.length) {
|
|
4001
|
+
for (const { name, dir } of await discoverThemeFiles(cfg.themeDirs)) {
|
|
4002
|
+
try {
|
|
4003
|
+
const css = await compileSassFile(join19(dir, `${name}.theme.scss`));
|
|
4004
|
+
state.themeCss.set(name, css);
|
|
4005
|
+
const slots = config.themes && dir === cfg.themeDirs[0] ? swatchSlots : cfg.packageSwatchSlots;
|
|
4006
|
+
state.themeTokens.set(name, await extractThemeTokens(name, css, slots, dir));
|
|
4007
|
+
} catch (e) {
|
|
4008
|
+
console.warn(`[dev-server] Theme compile error (${name}):`, e);
|
|
4009
|
+
state.themeCss.set(name, "");
|
|
4010
|
+
state.themeTokens.set(name, Object.fromEntries(swatchSlots.map((s) => [s, null])));
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
{
|
|
4015
|
+
const dr = await buildDiscoveryMap(patternRoots);
|
|
4016
|
+
state.discoveryResult = dr;
|
|
4017
|
+
await surfaceCollisions(dr.collisions, cwd, hotreloader);
|
|
4018
|
+
for (const o of dr.overrides) {
|
|
4019
|
+
console.log(`[dev-server] ${o.id} overrides ${o.shadowedSource}'s component of the same handle ("${o.handle}")`);
|
|
4020
|
+
}
|
|
4021
|
+
state.compositionRegistry = await buildCompositionRegistry(patternsDir, dr.handleMap, dr.discoveryMap);
|
|
4022
|
+
await surfaceFlattening(dr.discoveryMap, state.compositionRegistry, cwd);
|
|
4023
|
+
const allComponentIds = baseComponentIds(dr);
|
|
4024
|
+
const styleData = await buildAllStyleData(styleRoots, allComponentIds);
|
|
4025
|
+
state.scssModuleGraph = styleData.scssModuleGraph;
|
|
4026
|
+
state.classOwnerIndex = styleData.classOwnerIndex;
|
|
4027
|
+
state.baseProvenance = styleData.baseProvenance;
|
|
4028
|
+
state.perComponentCss = styleData.componentCssMap;
|
|
4029
|
+
state.baseCss = styleData.baseCss;
|
|
4030
|
+
state.scssGraphDirty = false;
|
|
4031
|
+
}
|
|
4032
|
+
const boot = {
|
|
4033
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4034
|
+
serverBundleMtime: await stat5(fileURLToPath5(import.meta.url)).then((st) => st.mtimeMs).catch(() => null)
|
|
4035
|
+
};
|
|
4036
|
+
const routeCtx = { cfg, state, render, tracker, globalData, devUiDist, boot };
|
|
4037
|
+
const httpServer = createServer((req, res) => {
|
|
4038
|
+
void handleRequest(req, res, routeCtx);
|
|
4039
|
+
});
|
|
4040
|
+
await new Promise((resolve13, reject) => {
|
|
4041
|
+
httpServer.once("error", reject);
|
|
4042
|
+
httpServer.listen(port2, "127.0.0.1", () => {
|
|
4043
|
+
httpServer.off("error", reject);
|
|
4044
|
+
resolve13();
|
|
4045
|
+
});
|
|
4046
|
+
}).catch(async (e) => {
|
|
4047
|
+
await hotreloader.close().catch(() => {
|
|
4048
|
+
});
|
|
4049
|
+
throw describeBindError(e, port2, "workbench", { http: port2, wsChosen: wsPortChosen });
|
|
4050
|
+
});
|
|
4051
|
+
const actualPort = httpServer.address().port;
|
|
4052
|
+
console.log(`[dev-server] Listening at http://127.0.0.1:${actualPort}`);
|
|
4053
|
+
const componentCount = state.discoveryResult ? [...state.discoveryResult.discoveryMap.keys()].filter((id) => !id.includes("~")).length : 0;
|
|
4054
|
+
console.log(`[dev-server] Ready in ${Math.round(performance.now() - startTime)}ms \u2014 ${componentCount} components`);
|
|
4055
|
+
const watcher = live ? startWatching({ cfg, state, render, tracker, hotreloader }) : null;
|
|
4056
|
+
if (watcher) await new Promise((resolve13) => watcher.once("ready", () => resolve13()));
|
|
4057
|
+
return {
|
|
4058
|
+
port: actualPort,
|
|
4059
|
+
async close() {
|
|
4060
|
+
await watcher?.close();
|
|
4061
|
+
await hotreloader.close();
|
|
4062
|
+
await new Promise(
|
|
4063
|
+
(res, rej) => httpServer.close((err) => err ? rej(err) : res())
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
};
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// packages/core/src/index.ts
|
|
4070
|
+
import { build as esbuild } from "esbuild";
|
|
4071
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
4072
|
+
import { readFile as readFile17, writeFile as writeFile5, unlink, mkdir as mkdir5 } from "node:fs/promises";
|
|
4073
|
+
import { join as join20, dirname as dirname7, resolve as resolve10, relative as relative6, isAbsolute } from "node:path";
|
|
4074
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
4075
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4076
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
4077
|
+
async function runDev(root, opts, configPath) {
|
|
4078
|
+
const config = await loadConfig(root, configPath);
|
|
4079
|
+
await startDevServer(config, root, opts);
|
|
4080
|
+
}
|
|
4081
|
+
async function runBuild(root, configPath, options) {
|
|
4082
|
+
const config = await loadConfig(root, configPath);
|
|
4083
|
+
const outDir = resolve10(root, options.out ?? config.build?.outDir ?? config.outDir ?? "dist");
|
|
4084
|
+
const report = await buildStaticSite(config, root, {
|
|
4085
|
+
outDir,
|
|
4086
|
+
...options.base !== void 0 ? { basePath: options.base } : {},
|
|
4087
|
+
...options.audit !== void 0 ? { audit: options.audit } : {},
|
|
4088
|
+
pdVersion: options.pdVersion,
|
|
4089
|
+
log: (line) => console.log(`[pd build] ${line}`)
|
|
4090
|
+
});
|
|
4091
|
+
const mb = (report.bytes / 1024 / 1024).toFixed(1);
|
|
4092
|
+
console.log(`[pd build] ${report.components} components \u2192 ${report.files} files, ${mb} MB in ${report.ms} ms`);
|
|
4093
|
+
console.log(`[pd build] ${report.outDir} (served from ${report.basePath})`);
|
|
4094
|
+
for (const w of report.warnings) console.log(`[pd build] not built: ${w}`);
|
|
4095
|
+
}
|
|
4096
|
+
async function runAudit(root, configPath, format = "md") {
|
|
4097
|
+
const config = await loadConfig(root, configPath);
|
|
4098
|
+
const { graph, reaudited, reused, artifactPath } = await runDiagnosticsAudit(config, root);
|
|
4099
|
+
const report = formatAudit(graph, format);
|
|
4100
|
+
const ext = format === "json" ? "json" : format === "text" ? "txt" : "md";
|
|
4101
|
+
const reportPath = join20(dirname7(artifactPath), `diagnostics-report.${ext}`);
|
|
4102
|
+
await writeFile5(reportPath, report, "utf-8");
|
|
4103
|
+
console.log(report);
|
|
4104
|
+
const affected = new Set(graph.occurrences.map((o) => o.component)).size;
|
|
4105
|
+
const meta = format === "json" ? console.error : console.log;
|
|
4106
|
+
meta(`${format === "json" ? "" : "\n"}[pd audit] re-audited ${reaudited.length}, reused ${reused} (of ${graph.components.length}).`);
|
|
4107
|
+
meta(`[pd audit] serialized graph: ${artifactPath}`);
|
|
4108
|
+
meta(`[pd audit] report: ${reportPath}`);
|
|
4109
|
+
meta(`[pd audit] ${graph.occurrences.length} defects across ${affected}/${graph.components.length} components.`);
|
|
4110
|
+
}
|
|
4111
|
+
function defaultExportDir(root) {
|
|
4112
|
+
const key = createHash3("sha1").update(resolve10(root)).digest("hex").slice(0, 16);
|
|
4113
|
+
return join20(tmpdir2(), "pattern-dump", key, "export");
|
|
4114
|
+
}
|
|
4115
|
+
function assertOutsideRoot(outDir, root) {
|
|
4116
|
+
const rel = relative6(resolve10(root), resolve10(outDir));
|
|
4117
|
+
const inside = rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
4118
|
+
if (inside) {
|
|
4119
|
+
throw new Error(
|
|
4120
|
+
`--out must be outside --root (the root is read-only; export writes the corrected form elsewhere).
|
|
4121
|
+
root: ${resolve10(root)}
|
|
4122
|
+
out: ${resolve10(outDir)}`
|
|
4123
|
+
);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
async function runExport(root, configPath, outDir) {
|
|
4127
|
+
const config = await loadConfig(root, configPath);
|
|
4128
|
+
const patternsDir = resolve10(root, config.patternsDir);
|
|
4129
|
+
const sink = outDir ? resolve10(process.cwd(), outDir) : defaultExportDir(root);
|
|
4130
|
+
assertOutsideRoot(sink, root);
|
|
4131
|
+
const plan = await planExport(config, root);
|
|
4132
|
+
await mkdir5(sink, { recursive: true });
|
|
4133
|
+
const threadedFamilies = plan.threaded.map((t) => t.family);
|
|
4134
|
+
const { files } = await writeExport([...plan.collapse, ...threadedFamilies], sink);
|
|
4135
|
+
const { files: includerFiles, rewrites } = await writeThreadedIncluders(plan.threaded, patternsDir, sink);
|
|
4136
|
+
console.log(`[pd export] patterns: ${patternsDir}`);
|
|
4137
|
+
console.log(`[pd export] sink: ${sink}${outDir ? "" : " (default \u2014 pass --out <dir> for a durable target)"}`);
|
|
4138
|
+
console.log(`[pd export] ${plan.collapse.length} family(ies) collapse (leaf, shape (a)); ${plan.threaded.length} threaded (include-consumed shape (a) \u2014 slice 2); ${plan.deferred.length} deferred (\xA77 hazard, not shape (a) \u2014 slice 3 / shape-(c) track); ${plan.skipped.length} skipped (leaf but not lossless shape (a)).`);
|
|
4139
|
+
console.log(`[pd export] wrote ${files.length} collapsed file(s) + ${includerFiles.length} rewritten includer(s) (${rewrites} include site(s) threaded):`);
|
|
4140
|
+
for (const f of files) console.log(` ${f}`);
|
|
4141
|
+
for (const f of includerFiles) console.log(` ${f} (includer, rewritten)`);
|
|
4142
|
+
for (const family of plan.collapse) {
|
|
4143
|
+
console.log(`
|
|
4144
|
+
[pd export] COLLAPSE ${family.baseId} (root .${family.root}, ${family.variants.length} variants)`);
|
|
4145
|
+
for (const v of family.variants) {
|
|
4146
|
+
console.log(` ~${v.token || "(base-default)"} modifier="${v.modifier}" \u2190 ${v.sourceId}`);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
for (const t of plan.threaded) {
|
|
4150
|
+
const family = t.family;
|
|
4151
|
+
console.log(`
|
|
4152
|
+
[pd export] THREADED ${family.baseId} (root .${family.root}, ${family.variants.length} variants)`);
|
|
4153
|
+
for (const v of family.variants) {
|
|
4154
|
+
console.log(` ~${v.token || "(base-default)"} modifier="${v.modifier}" \u2190 ${v.sourceId}`);
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
const rewriteMap = buildRewriteMap(threadedFamilies);
|
|
4158
|
+
if (rewriteMap.size) {
|
|
4159
|
+
const sitesByHandle = /* @__PURE__ */ new Map();
|
|
4160
|
+
for (const t of plan.threaded) {
|
|
4161
|
+
for (const [memberId, sites] of Object.entries(t.hazardSites)) {
|
|
4162
|
+
const set = sitesByHandle.get(deriveHandle(memberId)) ?? /* @__PURE__ */ new Set();
|
|
4163
|
+
for (const s of sites) set.add(s);
|
|
4164
|
+
sitesByHandle.set(deriveHandle(memberId), set);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
console.log(`
|
|
4168
|
+
[pd export] REWRITE MAP (${rewriteMap.size} flat handle(s) \u2192 collapsed base + threaded modifier):`);
|
|
4169
|
+
for (const [oldHandle, { baseHandle, modifier }] of [...rewriteMap].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
4170
|
+
const sites = [...sitesByHandle.get(oldHandle) ?? []].sort();
|
|
4171
|
+
const sitesNote = sites.length ? ` used-by: [${sites.join(", ")}]` : " (no include site \u2014 free member)";
|
|
4172
|
+
console.log(` {{> ${oldHandle}}} \u2192 {{> ${baseHandle} modifier="${modifier}"}}${sitesNote}`);
|
|
4173
|
+
}
|
|
4174
|
+
}
|
|
4175
|
+
for (const d of plan.deferred) {
|
|
4176
|
+
const sites = Object.entries(d.hazardSites).map(([m, s]) => `${m}\u2192[${s.join(", ")}]`).join("; ");
|
|
4177
|
+
console.log(`
|
|
4178
|
+
[pd export] DEFERRED (${d.members.length} members; \xA77 hazard: ${sites})`);
|
|
4179
|
+
}
|
|
4180
|
+
for (const s of plan.skipped) {
|
|
4181
|
+
console.log(`
|
|
4182
|
+
[pd export] SKIPPED [${s.reason}] (${s.members.length} members: ${s.members.join(", ")})`);
|
|
4183
|
+
}
|
|
4184
|
+
}
|
|
4185
|
+
async function loadConfig(root, configPath) {
|
|
4186
|
+
if (configPath) {
|
|
4187
|
+
if (!existsSync4(configPath)) {
|
|
4188
|
+
throw new Error(`--config file not found: ${configPath}`);
|
|
4189
|
+
}
|
|
4190
|
+
return configPath.endsWith(".json") ? JSON.parse(await readFile17(configPath, "utf-8")) : loadTsConfig(configPath);
|
|
4191
|
+
}
|
|
4192
|
+
const tsPath = join20(root, "pattern-dump.config.ts");
|
|
4193
|
+
const jsonPath = join20(root, "pattern-dump.config.json");
|
|
4194
|
+
if (existsSync4(tsPath)) return loadTsConfig(tsPath);
|
|
4195
|
+
if (existsSync4(jsonPath)) {
|
|
4196
|
+
return JSON.parse(await readFile17(jsonPath, "utf-8"));
|
|
4197
|
+
}
|
|
4198
|
+
throw new Error(
|
|
4199
|
+
`No pattern-dump.config.ts or pattern-dump.config.json found in ${root} (and no --config given)`
|
|
4200
|
+
);
|
|
4201
|
+
}
|
|
4202
|
+
async function loadTsConfig(tsPath) {
|
|
4203
|
+
const tmp = join20(tmpdir2(), `pd-config-${Date.now()}.mjs`);
|
|
4204
|
+
try {
|
|
4205
|
+
await esbuild({
|
|
4206
|
+
entryPoints: [tsPath],
|
|
4207
|
+
bundle: true,
|
|
4208
|
+
format: "esm",
|
|
4209
|
+
platform: "node",
|
|
4210
|
+
// No packages:"external" — the temp file runs from /tmp so we must inline deps.
|
|
4211
|
+
outfile: tmp
|
|
4212
|
+
});
|
|
4213
|
+
const { default: cfg } = await import(pathToFileURL2(tmp).href);
|
|
4214
|
+
return cfg;
|
|
4215
|
+
} finally {
|
|
4216
|
+
await unlink(tmp).catch(() => {
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
// packages/cli/src/index.ts
|
|
4222
|
+
import { spawn } from "node:child_process";
|
|
4223
|
+
import { createInterface } from "node:readline/promises";
|
|
4224
|
+
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
4225
|
+
|
|
4226
|
+
// packages/cli/src/args.ts
|
|
4227
|
+
import { resolve as resolve11 } from "node:path";
|
|
4228
|
+
import { parseArgs } from "node:util";
|
|
4229
|
+
var COMMANDS = ["init", "dev", "audit", "build", "export"];
|
|
4230
|
+
var rootAndConfig = {
|
|
4231
|
+
root: { type: "string" },
|
|
4232
|
+
config: { type: "string" },
|
|
4233
|
+
help: { type: "boolean", short: "h" }
|
|
4234
|
+
};
|
|
4235
|
+
function port(value, flag) {
|
|
4236
|
+
if (value === void 0) return void 0;
|
|
4237
|
+
const n = Number(value);
|
|
4238
|
+
return Number.isInteger(n) && n >= 0 && n <= 65535 ? n : new Error(`${flag} must be a port number (got "${value}")`);
|
|
4239
|
+
}
|
|
4240
|
+
function parseCommand(argv, cwd) {
|
|
4241
|
+
const [cmd, ...rest] = argv;
|
|
4242
|
+
if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
4243
|
+
return { kind: "help", ...rest[0] ? { topic: rest[0] } : {} };
|
|
4244
|
+
}
|
|
4245
|
+
if (!COMMANDS.includes(cmd)) {
|
|
4246
|
+
return { kind: "error", message: `unknown command "${cmd}"` };
|
|
4247
|
+
}
|
|
4248
|
+
try {
|
|
4249
|
+
if (cmd === "init") return parseInit(rest, cwd);
|
|
4250
|
+
if (cmd === "dev") {
|
|
4251
|
+
const { values: values2 } = parseArgs({
|
|
4252
|
+
args: rest,
|
|
4253
|
+
strict: true,
|
|
4254
|
+
allowPositionals: false,
|
|
4255
|
+
options: { ...rootAndConfig, port: { type: "string" }, "ws-port": { type: "string" } }
|
|
4256
|
+
});
|
|
4257
|
+
if (values2.help) return { kind: "help", topic: cmd };
|
|
4258
|
+
const p = port(values2.port, "--port");
|
|
4259
|
+
const ws = port(values2["ws-port"], "--ws-port");
|
|
4260
|
+
if (p instanceof Error) return { kind: "error", message: p.message };
|
|
4261
|
+
if (ws instanceof Error) return { kind: "error", message: ws.message };
|
|
4262
|
+
return {
|
|
4263
|
+
kind: "dev",
|
|
4264
|
+
...base(values2, cwd),
|
|
4265
|
+
...p !== void 0 ? { port: p } : {},
|
|
4266
|
+
...ws !== void 0 ? { wsPort: ws } : {}
|
|
4267
|
+
};
|
|
4268
|
+
}
|
|
4269
|
+
if (cmd === "audit") {
|
|
4270
|
+
const { values: values2 } = parseArgs({
|
|
4271
|
+
args: rest,
|
|
4272
|
+
strict: true,
|
|
4273
|
+
allowPositionals: false,
|
|
4274
|
+
options: { ...rootAndConfig, format: { type: "string" } }
|
|
4275
|
+
});
|
|
4276
|
+
if (values2.help) return { kind: "help", topic: cmd };
|
|
4277
|
+
const format = values2.format ?? "md";
|
|
4278
|
+
if (format !== "md" && format !== "text" && format !== "json") {
|
|
4279
|
+
return { kind: "error", message: `--format must be one of md|text|json (got "${format}")` };
|
|
4280
|
+
}
|
|
4281
|
+
return { kind: "audit", ...base(values2, cwd), format };
|
|
4282
|
+
}
|
|
4283
|
+
if (cmd === "build") {
|
|
4284
|
+
const { values: values2 } = parseArgs({
|
|
4285
|
+
args: rest,
|
|
4286
|
+
strict: true,
|
|
4287
|
+
allowPositionals: false,
|
|
4288
|
+
options: {
|
|
4289
|
+
...rootAndConfig,
|
|
4290
|
+
out: { type: "string" },
|
|
4291
|
+
base: { type: "string" },
|
|
4292
|
+
audit: { type: "boolean" },
|
|
4293
|
+
"no-audit": { type: "boolean" }
|
|
4294
|
+
}
|
|
4295
|
+
});
|
|
4296
|
+
if (values2.help) return { kind: "help", topic: cmd };
|
|
4297
|
+
const audit = toggle(values2, "audit");
|
|
4298
|
+
if (audit instanceof Error) return { kind: "error", message: audit.message };
|
|
4299
|
+
return {
|
|
4300
|
+
kind: "build",
|
|
4301
|
+
...base(values2, cwd),
|
|
4302
|
+
...values2.out !== void 0 ? { out: values2.out } : {},
|
|
4303
|
+
...values2.base !== void 0 ? { base: values2.base } : {},
|
|
4304
|
+
...audit !== void 0 ? { audit } : {}
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
const { values } = parseArgs({
|
|
4308
|
+
args: rest,
|
|
4309
|
+
strict: true,
|
|
4310
|
+
allowPositionals: false,
|
|
4311
|
+
options: { ...rootAndConfig, out: { type: "string" } }
|
|
4312
|
+
});
|
|
4313
|
+
if (values.help) return { kind: "help", topic: cmd };
|
|
4314
|
+
return { kind: "export", ...base(values, cwd), ...values.out !== void 0 ? { out: values.out } : {} };
|
|
4315
|
+
} catch (e) {
|
|
4316
|
+
return { kind: "error", message: e instanceof Error ? e.message : String(e) };
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
function toggle(values, name) {
|
|
4320
|
+
const on = values[name] === true;
|
|
4321
|
+
const off = values[`no-${name}`] === true;
|
|
4322
|
+
if (on && off) return new Error(`--${name} and --no-${name} cannot both be given`);
|
|
4323
|
+
return on ? true : off ? false : void 0;
|
|
4324
|
+
}
|
|
4325
|
+
function parseInit(rest, cwd) {
|
|
4326
|
+
const { values, positionals } = parseArgs({
|
|
4327
|
+
args: rest,
|
|
4328
|
+
strict: true,
|
|
4329
|
+
allowPositionals: true,
|
|
4330
|
+
options: {
|
|
4331
|
+
name: { type: "string" },
|
|
4332
|
+
brand: { type: "string" },
|
|
4333
|
+
examples: { type: "boolean" },
|
|
4334
|
+
"no-examples": { type: "boolean" },
|
|
4335
|
+
install: { type: "boolean" },
|
|
4336
|
+
"no-install": { type: "boolean" },
|
|
4337
|
+
git: { type: "boolean" },
|
|
4338
|
+
"no-git": { type: "boolean" },
|
|
4339
|
+
"pd-source": { type: "string" },
|
|
4340
|
+
yes: { type: "boolean", short: "y" },
|
|
4341
|
+
help: { type: "boolean", short: "h" }
|
|
4342
|
+
}
|
|
4343
|
+
});
|
|
4344
|
+
if (values.help) return { kind: "help", topic: "init" };
|
|
4345
|
+
if (positionals.length > 1) return { kind: "error", message: `init takes one folder (got ${positionals.join(", ")})` };
|
|
4346
|
+
const toggles = { examples: void 0, install: void 0, git: void 0 };
|
|
4347
|
+
for (const name of ["examples", "install", "git"]) {
|
|
4348
|
+
const t = toggle(values, name);
|
|
4349
|
+
if (t instanceof Error) return { kind: "error", message: t.message };
|
|
4350
|
+
toggles[name] = t;
|
|
4351
|
+
}
|
|
4352
|
+
return {
|
|
4353
|
+
kind: "init",
|
|
4354
|
+
target: resolve11(cwd, positionals[0] ?? "."),
|
|
4355
|
+
yes: values.yes === true,
|
|
4356
|
+
...values.name !== void 0 ? { name: values.name } : {},
|
|
4357
|
+
...values.brand !== void 0 ? { brand: values.brand } : {},
|
|
4358
|
+
...toggles.examples !== void 0 ? { examples: toggles.examples } : {},
|
|
4359
|
+
...toggles.install !== void 0 ? { install: toggles.install } : {},
|
|
4360
|
+
...toggles.git !== void 0 ? { git: toggles.git } : {},
|
|
4361
|
+
...values["pd-source"] !== void 0 ? { pdSource: values["pd-source"] } : {}
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
function base(values, cwd) {
|
|
4365
|
+
return {
|
|
4366
|
+
root: values.root ? resolve11(cwd, values.root) : cwd,
|
|
4367
|
+
...values.config ? { configPath: resolve11(cwd, values.config) } : {}
|
|
4368
|
+
};
|
|
4369
|
+
}
|
|
4370
|
+
var USAGE = `Usage: pd <command> [options]
|
|
4371
|
+
|
|
4372
|
+
init [dir] Set up a new Pattern Dump project (default: the current folder)
|
|
4373
|
+
--name <name> Package name (default: the folder name)
|
|
4374
|
+
--brand <title> Title shown in the Nav Rail (default: the name)
|
|
4375
|
+
--no-examples Structure, config and themes only \u2014 no starter components
|
|
4376
|
+
--no-install Skip npm install
|
|
4377
|
+
--git / --no-git Run git init (default: yes, unless already inside a repository)
|
|
4378
|
+
--pd-source <src> Where to install Pattern Dump from: a .tgz file or an npm spec
|
|
4379
|
+
-y, --yes Accept every default without asking
|
|
4380
|
+
|
|
4381
|
+
dev Start the local dev server
|
|
4382
|
+
--root <path> Open a specific project root (default: current directory)
|
|
4383
|
+
--config <path> Use a config file outside the root (paths resolve against --root)
|
|
4384
|
+
--port <n> HTTP port (default 4000, env: PD_PORT)
|
|
4385
|
+
--ws-port <n> WebSocket port (default port+1, env: PD_WS_PORT)
|
|
4386
|
+
|
|
4387
|
+
audit Build the diagnostics graph and emit the report + serialized JSON
|
|
4388
|
+
--root <path> Audit a specific project root
|
|
4389
|
+
--config <path> Use a config file outside the root
|
|
4390
|
+
--format <fmt> Report format: md (default) | text | json (json \u2192 stdout only)
|
|
4391
|
+
|
|
4392
|
+
build Write a static snapshot of the workbench \u2014 plain files for any static host
|
|
4393
|
+
--root <path> Build a specific project root
|
|
4394
|
+
--config <path> Use a config file outside the root
|
|
4395
|
+
--out <dir> Output folder, relative to the root (default: build.outDir, outDir, or dist)
|
|
4396
|
+
--base <path> Where it will be hosted: / (default) or a subpath such as /docs/
|
|
4397
|
+
--no-audit Leave the corpus audit out: it is not run, and the site has no Audit tab
|
|
4398
|
+
(default: build.audit in the config; --audit forces it back in)
|
|
4399
|
+
|
|
4400
|
+
export Write the subject's flattened families back out in corrected form
|
|
4401
|
+
--root <path> Export a specific project root (read-only)
|
|
4402
|
+
--out <dir> Durable output sink (must be outside --root)
|
|
4403
|
+
--config <path> Use a config file outside the root
|
|
4404
|
+
|
|
4405
|
+
help [command] Show this help`;
|
|
4406
|
+
|
|
4407
|
+
// packages/cli/src/init/run.ts
|
|
4408
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
4409
|
+
import { readFile as readFile19 } from "node:fs/promises";
|
|
4410
|
+
import { basename as basename6, dirname as dirname9, join as join22, relative as relative8, resolve as resolve12 } from "node:path";
|
|
4411
|
+
|
|
4412
|
+
// packages/cli/package.json
|
|
4413
|
+
var package_default = {
|
|
4414
|
+
name: "@pattern-dump/cli",
|
|
4415
|
+
version: "0.1.0-alpha.0",
|
|
4416
|
+
private: true,
|
|
4417
|
+
description: "The pd command and the published pattern-dump package.",
|
|
4418
|
+
type: "module",
|
|
4419
|
+
main: "./dist/index.js",
|
|
4420
|
+
types: "./dist/index.d.ts",
|
|
4421
|
+
bin: {
|
|
4422
|
+
pd: "./dist/index.js"
|
|
4423
|
+
},
|
|
4424
|
+
exports: {
|
|
4425
|
+
".": {
|
|
4426
|
+
types: "./dist/index.d.ts",
|
|
4427
|
+
import: "./dist/index.js"
|
|
4428
|
+
}
|
|
4429
|
+
},
|
|
4430
|
+
dependencies: {
|
|
4431
|
+
"@pattern-dump/core": "*"
|
|
4432
|
+
},
|
|
4433
|
+
scripts: {
|
|
4434
|
+
build: "esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --external:@pattern-dump/* --outfile=dist/index.js",
|
|
4435
|
+
test: "vitest run --passWithNoTests",
|
|
4436
|
+
typecheck: "tsc --noEmit",
|
|
4437
|
+
lint: "eslint src"
|
|
4438
|
+
},
|
|
4439
|
+
devDependencies: {
|
|
4440
|
+
"@pattern-dump/contracts": "*",
|
|
4441
|
+
"@pattern-dump/dev-server": "*"
|
|
4442
|
+
}
|
|
4443
|
+
};
|
|
4444
|
+
|
|
4445
|
+
// packages/cli/src/distribution.ts
|
|
4446
|
+
var PD_PACKAGE_NAME = "pattern-dump";
|
|
4447
|
+
var PD_VERSION = package_default.version;
|
|
4448
|
+
|
|
4449
|
+
// packages/cli/src/init/files.ts
|
|
4450
|
+
import { mkdir as mkdir6, readdir as readdir5, readFile as readFile18, writeFile as writeFile6 } from "node:fs/promises";
|
|
4451
|
+
import { dirname as dirname8, join as join21, relative as relative7, sep as sep2 } from "node:path";
|
|
4452
|
+
async function readTemplateDir(dir) {
|
|
4453
|
+
const out = [];
|
|
4454
|
+
async function walk(current) {
|
|
4455
|
+
for (const entry of await readdir5(current, { withFileTypes: true })) {
|
|
4456
|
+
const full = join21(current, entry.name);
|
|
4457
|
+
if (entry.isDirectory()) await walk(full);
|
|
4458
|
+
else out.push({ path: relative7(dir, full).split(sep2).join("/"), content: await readFile18(full, "utf8") });
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4461
|
+
await walk(dir);
|
|
4462
|
+
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
4463
|
+
}
|
|
4464
|
+
async function readTemplates(templatesDir) {
|
|
4465
|
+
const [base2, examples] = await Promise.all([
|
|
4466
|
+
readTemplateDir(join21(templatesDir, "base")),
|
|
4467
|
+
readTemplateDir(join21(templatesDir, "examples"))
|
|
4468
|
+
]);
|
|
4469
|
+
return { base: base2, examples };
|
|
4470
|
+
}
|
|
4471
|
+
async function applyPlan(target, plan) {
|
|
4472
|
+
await mkdir6(target, { recursive: true });
|
|
4473
|
+
for (const file of plan.writes) {
|
|
4474
|
+
const abs = join21(target, file.path);
|
|
4475
|
+
await mkdir6(dirname8(abs), { recursive: true });
|
|
4476
|
+
await writeFile6(abs, file.content, { flag: "wx" });
|
|
4477
|
+
}
|
|
4478
|
+
await writeFile6(join21(target, "package.json"), plan.packageJson.content);
|
|
4479
|
+
for (const dir of ["assets", "src/patterns"]) await mkdir6(join21(target, dir), { recursive: true });
|
|
4480
|
+
}
|
|
4481
|
+
|
|
4482
|
+
// packages/cli/src/init/plan.ts
|
|
4483
|
+
var CONFIG_FILES = ["pattern-dump.config.ts", "pattern-dump.config.json"];
|
|
4484
|
+
var RENAMES = { _gitignore: ".gitignore" };
|
|
4485
|
+
function toPackageName(input) {
|
|
4486
|
+
const slug = input.toLowerCase().replace(/[^a-z0-9._~-]+/g, "-").replace(/^[-._]+|[-._]+$/g, "");
|
|
4487
|
+
return slug || "design-system";
|
|
4488
|
+
}
|
|
4489
|
+
function planInit(templates, answers, existing, packageName) {
|
|
4490
|
+
const existingConfig = CONFIG_FILES.find((f) => existing.exists(f));
|
|
4491
|
+
if (existingConfig) {
|
|
4492
|
+
return { ok: false, reason: `${existingConfig} already exists \u2014 this folder is already a Pattern Dump project` };
|
|
4493
|
+
}
|
|
4494
|
+
const substitute = (text) => text.replaceAll("__PD_PACKAGE__", packageName).replaceAll("__PD_NAME__", answers.name).replaceAll("__PD_BRAND_JSON__", JSON.stringify(answers.brand));
|
|
4495
|
+
const writes = [];
|
|
4496
|
+
const skipped = [];
|
|
4497
|
+
const files = [...templates.base, ...answers.examples ? templates.examples : []];
|
|
4498
|
+
for (const file of files) {
|
|
4499
|
+
const path = renamed(file.path);
|
|
4500
|
+
if (existing.exists(path)) {
|
|
4501
|
+
skipped.push({ path, reason: "already exists \u2014 left untouched" });
|
|
4502
|
+
continue;
|
|
4503
|
+
}
|
|
4504
|
+
writes.push({ path, content: substitute(file.content) });
|
|
4505
|
+
}
|
|
4506
|
+
return { ok: true, plan: { writes, skipped, packageJson: mergePackageJson(existing.packageJson, answers, packageName) } };
|
|
4507
|
+
}
|
|
4508
|
+
function renamed(path) {
|
|
4509
|
+
const parts = path.split("/");
|
|
4510
|
+
const last = parts.pop();
|
|
4511
|
+
return [...parts, RENAMES[last] ?? last].join("/");
|
|
4512
|
+
}
|
|
4513
|
+
var SCRIPTS = {
|
|
4514
|
+
dev: "pd dev",
|
|
4515
|
+
audit: "pd audit",
|
|
4516
|
+
typecheck: "tsc --noEmit",
|
|
4517
|
+
"build:scripts": "tsc"
|
|
4518
|
+
};
|
|
4519
|
+
var TYPESCRIPT_RANGE = "^5.9.0";
|
|
4520
|
+
function mergePackageJson(current, answers, packageName) {
|
|
4521
|
+
const notes = [];
|
|
4522
|
+
const created = current === null;
|
|
4523
|
+
const pkg = created ? { name: toPackageName(answers.name), version: "0.1.0", private: true, type: "module" } : structuredClone(current);
|
|
4524
|
+
const scripts = { ...pkg.scripts };
|
|
4525
|
+
for (const [name, command2] of Object.entries(SCRIPTS)) {
|
|
4526
|
+
if (scripts[name] === void 0) {
|
|
4527
|
+
scripts[name] = command2;
|
|
4528
|
+
} else if (scripts[name] !== command2) {
|
|
4529
|
+
const alt = `pd:${name}`;
|
|
4530
|
+
if (scripts[alt] === void 0) scripts[alt] = command2;
|
|
4531
|
+
notes.push(`script "${name}" already exists \u2014 added "${alt}" instead`);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
pkg.scripts = scripts;
|
|
4535
|
+
const deps = pkg.dependencies;
|
|
4536
|
+
const devDeps = { ...pkg.devDependencies };
|
|
4537
|
+
if (deps?.[packageName] !== void 0) {
|
|
4538
|
+
notes.push(`${packageName} is already a dependency \u2014 left as is`);
|
|
4539
|
+
} else if (devDeps[packageName] !== void 0 && devDeps[packageName] !== answers.dependency) {
|
|
4540
|
+
notes.push(`${packageName} is already a devDependency (${devDeps[packageName]}) \u2014 left as is`);
|
|
4541
|
+
} else {
|
|
4542
|
+
devDeps[packageName] = answers.dependency;
|
|
4543
|
+
}
|
|
4544
|
+
const existingTs = deps?.typescript ?? devDeps.typescript;
|
|
4545
|
+
if (existingTs === void 0) {
|
|
4546
|
+
devDeps.typescript = TYPESCRIPT_RANGE;
|
|
4547
|
+
} else if (existingTs !== TYPESCRIPT_RANGE) {
|
|
4548
|
+
notes.push(`typescript is already a dependency (${existingTs}) \u2014 left as is; the starter scripts expect ${TYPESCRIPT_RANGE}`);
|
|
4549
|
+
}
|
|
4550
|
+
pkg.devDependencies = devDeps;
|
|
4551
|
+
return { content: JSON.stringify(pkg, null, 2) + "\n", created, notes };
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
// packages/cli/src/init/run.ts
|
|
4555
|
+
function dependencySpec(pdSource, cwd) {
|
|
4556
|
+
if (pdSource === void 0) return `^${PD_VERSION}`;
|
|
4557
|
+
const asPath = resolve12(cwd, pdSource);
|
|
4558
|
+
if (existsSync5(asPath)) return `file:${asPath}`;
|
|
4559
|
+
return pdSource.startsWith(`${PD_PACKAGE_NAME}@`) ? pdSource.slice(PD_PACKAGE_NAME.length + 1) : pdSource;
|
|
4560
|
+
}
|
|
4561
|
+
async function insideGitRepo(target, io) {
|
|
4562
|
+
let dir = target;
|
|
4563
|
+
while (!existsSync5(dir) && dirname9(dir) !== dir) dir = dirname9(dir);
|
|
4564
|
+
return await io.run("git", ["rev-parse", "--is-inside-work-tree"], dir) === 0;
|
|
4565
|
+
}
|
|
4566
|
+
async function runInit(cmd, io) {
|
|
4567
|
+
const target = cmd.target;
|
|
4568
|
+
let dependency;
|
|
4569
|
+
if (cmd.pdSource !== void 0) {
|
|
4570
|
+
dependency = dependencySpec(cmd.pdSource, io.cwd);
|
|
4571
|
+
} else if (io.localBuild) {
|
|
4572
|
+
if (!io.localBuild.tarball) {
|
|
4573
|
+
io.log("\u2717 This pd runs from a Pattern Dump source checkout, and there is no packed build to install.");
|
|
4574
|
+
io.log(` Pack one first: (cd ${io.localBuild.repo} && npm run pack:pd)`);
|
|
4575
|
+
io.log(" or pass --pd-source <tarball or npm spec>. Nothing was written.");
|
|
4576
|
+
return 1;
|
|
4577
|
+
}
|
|
4578
|
+
dependency = `file:${io.localBuild.tarball}`;
|
|
4579
|
+
io.log(`\xB7 Installing Pattern Dump from the local build ${io.localBuild.tarball}`);
|
|
4580
|
+
if (io.localBuild.stale) {
|
|
4581
|
+
io.log(` ! It is older than your latest build. For recent changes: (cd ${io.localBuild.repo} && npm run pack:pd)`);
|
|
4582
|
+
}
|
|
4583
|
+
} else {
|
|
4584
|
+
dependency = dependencySpec(void 0, io.cwd);
|
|
4585
|
+
}
|
|
4586
|
+
const pkgPath = join22(target, "package.json");
|
|
4587
|
+
let packageJson = null;
|
|
4588
|
+
if (existsSync5(pkgPath)) {
|
|
4589
|
+
try {
|
|
4590
|
+
packageJson = JSON.parse(await readFile19(pkgPath, "utf8"));
|
|
4591
|
+
} catch (e) {
|
|
4592
|
+
io.log(`\u2717 ${pkgPath} is not valid JSON \u2014 fix it and run pd init again (${e instanceof Error ? e.message : e})`);
|
|
4593
|
+
return 1;
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
const prompt = io.interactive && !cmd.yes;
|
|
4597
|
+
const existingName = typeof packageJson?.name === "string" ? packageJson.name : void 0;
|
|
4598
|
+
const name = cmd.name ?? (prompt ? await io.ask("Project name", existingName ?? toPackageName(basename6(target))) : existingName ?? toPackageName(basename6(target)));
|
|
4599
|
+
const brand = cmd.brand ?? (prompt ? await io.ask("Title in the Nav Rail", name) : name);
|
|
4600
|
+
const examples = cmd.examples ?? (prompt ? await io.confirm("Include starter components", true) : true);
|
|
4601
|
+
const gitDefault = !await insideGitRepo(target, io);
|
|
4602
|
+
const git = cmd.git ?? (prompt && gitDefault ? await io.confirm("Initialise a git repository", true) : gitDefault);
|
|
4603
|
+
const install = cmd.install ?? (prompt ? await io.confirm("Run npm install", true) : true);
|
|
4604
|
+
const answers = { name, brand, examples, dependency };
|
|
4605
|
+
const templates = await readTemplates(io.templatesDir);
|
|
4606
|
+
const result = planInit(templates, answers, { packageJson, exists: (p) => existsSync5(join22(target, p)) }, PD_PACKAGE_NAME);
|
|
4607
|
+
if (!result.ok) {
|
|
4608
|
+
io.log(`\u2717 ${result.reason}`);
|
|
4609
|
+
return 1;
|
|
4610
|
+
}
|
|
4611
|
+
const { plan } = result;
|
|
4612
|
+
await applyPlan(target, plan);
|
|
4613
|
+
const where = relative8(io.cwd, target) || ".";
|
|
4614
|
+
io.log(`\u2713 ${plan.writes.length} files written to ${where}`);
|
|
4615
|
+
io.log(`\u2713 package.json ${plan.packageJson.created ? "created" : "updated"} \u2014 ${PD_PACKAGE_NAME} ${answers.dependency}`);
|
|
4616
|
+
for (const note of plan.packageJson.notes) io.log(` \xB7 ${note}`);
|
|
4617
|
+
for (const s of plan.skipped) io.log(` \xB7 ${s.path}: ${s.reason}`);
|
|
4618
|
+
if (git) {
|
|
4619
|
+
const code = await io.run("git", ["init", "-q"], target);
|
|
4620
|
+
io.log(code === 0 ? "\u2713 git repository initialised" : "\u2717 git init failed \u2014 is git installed? (the project is set up regardless)");
|
|
4621
|
+
}
|
|
4622
|
+
if (install) {
|
|
4623
|
+
io.log("\u2026 npm install");
|
|
4624
|
+
const code = await io.run("npm", ["install"], target);
|
|
4625
|
+
if (code !== 0) {
|
|
4626
|
+
io.log("\u2717 npm install failed. The project files are in place.");
|
|
4627
|
+
if (dependency.startsWith("^")) {
|
|
4628
|
+
io.log(` ${PD_PACKAGE_NAME} may not be published yet. Install a local build into the project:`);
|
|
4629
|
+
io.log(` ${where === "." ? "" : `cd ${where} && `}npm install --save-dev <path to ${PD_PACKAGE_NAME}-${PD_VERSION}.tgz>`);
|
|
4630
|
+
io.log(" (build one in the Pattern Dump repo with: npm run pack:pd)");
|
|
4631
|
+
} else {
|
|
4632
|
+
io.log(` Fix the error above, then run npm install${where === "." ? "" : ` in ${where}`}.`);
|
|
4633
|
+
}
|
|
4634
|
+
return 1;
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
io.log("");
|
|
4638
|
+
io.log("Next:");
|
|
4639
|
+
if (where !== ".") io.log(` cd ${where}`);
|
|
4640
|
+
if (!install) io.log(" npm install");
|
|
4641
|
+
io.log(` npm run ${JSON.parse(plan.packageJson.content).scripts["pd:dev"] ? "pd:dev" : "dev"} # http://localhost:4000`);
|
|
4642
|
+
return 0;
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
// packages/cli/src/init/local-build.ts
|
|
4646
|
+
import { existsSync as existsSync6, statSync } from "node:fs";
|
|
4647
|
+
import { join as join23 } from "node:path";
|
|
4648
|
+
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
4649
|
+
var BUILT = [
|
|
4650
|
+
"packages/cli/dist/index.js",
|
|
4651
|
+
"packages/core/dist/index.js",
|
|
4652
|
+
"packages/dev-server/dist/index.js",
|
|
4653
|
+
"packages/renderer/dist/index.js",
|
|
4654
|
+
"apps/dev-ui/dist/main.js"
|
|
4655
|
+
];
|
|
4656
|
+
var mtime = (path) => {
|
|
4657
|
+
try {
|
|
4658
|
+
return statSync(path).mtimeMs;
|
|
4659
|
+
} catch {
|
|
4660
|
+
return 0;
|
|
4661
|
+
}
|
|
4662
|
+
};
|
|
4663
|
+
function detectLocalBuild(moduleUrl, packageName, version) {
|
|
4664
|
+
const repo = fileURLToPath6(new URL("../../../", moduleUrl)).replace(/[\\/]$/, "");
|
|
4665
|
+
if (!existsSync6(join23(repo, "packages/cli/src/distribution.ts"))) return null;
|
|
4666
|
+
const tarball = join23(repo, ".pd-pack", `${packageName}-${version}.tgz`);
|
|
4667
|
+
if (!existsSync6(tarball)) return { repo, tarball: null, stale: false };
|
|
4668
|
+
const newestBuild = Math.max(...BUILT.map((p) => mtime(join23(repo, p))));
|
|
4669
|
+
return { repo, tarball, stale: newestBuild > mtime(tarball) };
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
// packages/cli/src/index.ts
|
|
4673
|
+
var command = parseCommand(process.argv.slice(2), process.cwd());
|
|
4674
|
+
switch (command.kind) {
|
|
4675
|
+
case "init": {
|
|
4676
|
+
const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
4677
|
+
runInit(command, {
|
|
4678
|
+
cwd: process.cwd(),
|
|
4679
|
+
// templates/ ships beside dist/ — the same relative position in this repo and in the package.
|
|
4680
|
+
templatesDir: fileURLToPath7(new URL("../templates/", import.meta.url)),
|
|
4681
|
+
interactive: rl !== null,
|
|
4682
|
+
localBuild: detectLocalBuild(import.meta.url, PD_PACKAGE_NAME, PD_VERSION),
|
|
4683
|
+
async ask(question, fallback) {
|
|
4684
|
+
const answer = (await rl.question(`${question} (${fallback}): `)).trim();
|
|
4685
|
+
return answer || fallback;
|
|
4686
|
+
},
|
|
4687
|
+
async confirm(question, fallback) {
|
|
4688
|
+
const answer = (await rl.question(`${question} (${fallback ? "Y/n" : "y/N"}): `)).trim().toLowerCase();
|
|
4689
|
+
return answer === "" ? fallback : answer.startsWith("y");
|
|
4690
|
+
},
|
|
4691
|
+
log: (message) => console.log(message),
|
|
4692
|
+
run: (cmd, args, cwd) => new Promise((resolve13) => {
|
|
4693
|
+
const quiet = cmd === "git" && args[0] === "rev-parse";
|
|
4694
|
+
const child = spawn(cmd, args, { cwd, stdio: quiet ? "ignore" : "inherit", shell: process.platform === "win32" });
|
|
4695
|
+
child.on("error", () => resolve13(null));
|
|
4696
|
+
child.on("close", (code) => resolve13(code));
|
|
4697
|
+
})
|
|
4698
|
+
}).then((code) => {
|
|
4699
|
+
rl?.close();
|
|
4700
|
+
process.exitCode = code;
|
|
4701
|
+
}).catch((e) => {
|
|
4702
|
+
rl?.close();
|
|
4703
|
+
console.error("[pd] Error:", e);
|
|
4704
|
+
process.exitCode = 1;
|
|
4705
|
+
});
|
|
4706
|
+
break;
|
|
4707
|
+
}
|
|
4708
|
+
case "dev":
|
|
4709
|
+
runDev(command.root, {
|
|
4710
|
+
...command.port !== void 0 ? { port: command.port } : {},
|
|
4711
|
+
...command.wsPort !== void 0 ? { wsPort: command.wsPort } : {}
|
|
4712
|
+
}, command.configPath).catch((e) => {
|
|
4713
|
+
if (typeof e === "object" && e !== null && e.code === "EADDRINUSE" && e instanceof Error) {
|
|
4714
|
+
console.error(`[pd] ${e.message}`);
|
|
4715
|
+
} else {
|
|
4716
|
+
console.error("[pd] Error:", e);
|
|
4717
|
+
}
|
|
4718
|
+
process.exit(1);
|
|
4719
|
+
});
|
|
4720
|
+
break;
|
|
4721
|
+
case "audit":
|
|
4722
|
+
runAudit(command.root, command.configPath, command.format).then(() => process.stdout.write("", () => process.exit(0))).catch((e) => {
|
|
4723
|
+
console.error("[pd] Error:", e);
|
|
4724
|
+
process.exit(1);
|
|
4725
|
+
});
|
|
4726
|
+
break;
|
|
4727
|
+
case "build":
|
|
4728
|
+
runBuild(command.root, command.configPath, {
|
|
4729
|
+
...command.out !== void 0 ? { out: command.out } : {},
|
|
4730
|
+
...command.base !== void 0 ? { base: command.base } : {},
|
|
4731
|
+
...command.audit !== void 0 ? { audit: command.audit } : {},
|
|
4732
|
+
pdVersion: PD_VERSION
|
|
4733
|
+
}).then(() => process.stdout.write("", () => process.exit(0))).catch((e) => {
|
|
4734
|
+
console.error("[pd] Error:", e instanceof Error ? e.message : e);
|
|
4735
|
+
process.exit(1);
|
|
4736
|
+
});
|
|
4737
|
+
break;
|
|
4738
|
+
case "export":
|
|
4739
|
+
runExport(command.root, command.configPath, command.out).then(() => {
|
|
4740
|
+
process.exitCode = 0;
|
|
4741
|
+
}).catch((e) => {
|
|
4742
|
+
console.error("[pd] Error:", e);
|
|
4743
|
+
process.exitCode = 1;
|
|
4744
|
+
});
|
|
4745
|
+
break;
|
|
4746
|
+
case "help":
|
|
4747
|
+
console.log(USAGE);
|
|
4748
|
+
break;
|
|
4749
|
+
case "error":
|
|
4750
|
+
console.error(`[pd] Error: ${command.message}
|
|
4751
|
+
`);
|
|
4752
|
+
console.error(USAGE);
|
|
4753
|
+
process.exit(1);
|
|
4754
|
+
break;
|
|
4755
|
+
default: {
|
|
4756
|
+
const unhandled = command;
|
|
4757
|
+
void unhandled;
|
|
4758
|
+
}
|
|
4759
|
+
}
|