@sourcescape/ds-cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-U44UTENA.js +492 -0
- package/dist/cli.js +375 -504
- package/dist/index.d.ts +127 -0
- package/dist/index.js +47 -0
- package/package.json +15 -4
package/dist/cli.js
CHANGED
|
@@ -1,191 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildPreviewHtml,
|
|
4
|
+
buildRegistry,
|
|
5
|
+
bundleJs,
|
|
6
|
+
descriptionMtime,
|
|
7
|
+
findComponent,
|
|
8
|
+
findComponentSources,
|
|
9
|
+
getSystemInfo,
|
|
10
|
+
listComponents,
|
|
11
|
+
listSystems,
|
|
12
|
+
readComponentJson,
|
|
13
|
+
readComponentMeta,
|
|
14
|
+
readDescription,
|
|
15
|
+
readFile,
|
|
16
|
+
readFullDescription,
|
|
17
|
+
resolveMarkup,
|
|
18
|
+
startPreviewServer,
|
|
19
|
+
suggestComponent,
|
|
20
|
+
systemDir,
|
|
21
|
+
systemExists
|
|
22
|
+
} from "./chunk-U44UTENA.js";
|
|
2
23
|
|
|
3
24
|
// src/cli.ts
|
|
4
25
|
import { parseArgs } from "util";
|
|
5
26
|
|
|
6
|
-
// src/systems.ts
|
|
7
|
-
import { readFileSync, readdirSync, existsSync, statSync } from "fs";
|
|
8
|
-
import { join, resolve } from "path";
|
|
9
|
-
function getSystemsRoot() {
|
|
10
|
-
const dir = process.env.DESIGN_SYSTEMS_DIR;
|
|
11
|
-
if (!dir) {
|
|
12
|
-
console.error("Error: DESIGN_SYSTEMS_DIR environment variable is required.");
|
|
13
|
-
console.error("Set it to the path containing your design system definitions.");
|
|
14
|
-
process.exit(1);
|
|
15
|
-
}
|
|
16
|
-
return resolve(dir);
|
|
17
|
-
}
|
|
18
|
-
function readComponentJson(componentDir) {
|
|
19
|
-
const jsonPath = join(componentDir, "description.json");
|
|
20
|
-
if (!existsSync(jsonPath)) return null;
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(readFileSync(jsonPath, "utf-8"));
|
|
23
|
-
} catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
function listSystems() {
|
|
28
|
-
return readdirSync(getSystemsRoot(), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
29
|
-
}
|
|
30
|
-
function systemDir(system) {
|
|
31
|
-
return join(getSystemsRoot(), system);
|
|
32
|
-
}
|
|
33
|
-
function systemExists(system) {
|
|
34
|
-
return existsSync(systemDir(system));
|
|
35
|
-
}
|
|
36
|
-
function readFirstHeading(filePath) {
|
|
37
|
-
if (!existsSync(filePath)) return "";
|
|
38
|
-
const content = readFileSync(filePath, "utf-8");
|
|
39
|
-
const match = content.match(/^#\s+(.+)$/m);
|
|
40
|
-
return match ? match[1].trim() : "";
|
|
41
|
-
}
|
|
42
|
-
function readFirstParagraph(filePath) {
|
|
43
|
-
if (!existsSync(filePath)) return "";
|
|
44
|
-
const lines = readFileSync(filePath, "utf-8").split("\n");
|
|
45
|
-
let pastHeading = false;
|
|
46
|
-
for (const line of lines) {
|
|
47
|
-
if (line.startsWith("# ")) {
|
|
48
|
-
pastHeading = true;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
if (pastHeading && line.trim().length > 0 && !line.startsWith("#")) {
|
|
52
|
-
return line.trim();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
function readFile(filePath) {
|
|
58
|
-
return readFileSync(filePath, "utf-8");
|
|
59
|
-
}
|
|
60
|
-
function getSystemInfo(system) {
|
|
61
|
-
const descPath = join(systemDir(system), "DESCRIPTION.md");
|
|
62
|
-
return {
|
|
63
|
-
name: system,
|
|
64
|
-
description: readFirstHeading(descPath)
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
function readDescription(system) {
|
|
68
|
-
const descPath = join(systemDir(system), "DESCRIPTION.md");
|
|
69
|
-
if (!existsSync(descPath)) return "";
|
|
70
|
-
return readFile(descPath);
|
|
71
|
-
}
|
|
72
|
-
function listComponents(system) {
|
|
73
|
-
const compsDir = join(systemDir(system), "components");
|
|
74
|
-
const results = [];
|
|
75
|
-
for (const kind of ["molecules", "cells"]) {
|
|
76
|
-
const kindDir = join(compsDir, kind);
|
|
77
|
-
if (!existsSync(kindDir)) continue;
|
|
78
|
-
const dirs = readdirSync(kindDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
79
|
-
for (const name of dirs) {
|
|
80
|
-
const compDir = join(kindDir, name);
|
|
81
|
-
const json = readComponentJson(compDir);
|
|
82
|
-
if (json) {
|
|
83
|
-
results.push({
|
|
84
|
-
name,
|
|
85
|
-
kind,
|
|
86
|
-
description: json.description,
|
|
87
|
-
tag: json.tag,
|
|
88
|
-
example: json.example
|
|
89
|
-
});
|
|
90
|
-
} else {
|
|
91
|
-
const descPath = join(compDir, "description.md");
|
|
92
|
-
results.push({
|
|
93
|
-
name,
|
|
94
|
-
kind,
|
|
95
|
-
description: readFirstParagraph(descPath)
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return results;
|
|
101
|
-
}
|
|
102
|
-
function findComponent(system, name) {
|
|
103
|
-
const compsDir = join(systemDir(system), "components");
|
|
104
|
-
for (const kind of ["molecules", "cells"]) {
|
|
105
|
-
const dir = join(compsDir, kind, name);
|
|
106
|
-
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
107
|
-
return { kind, dir };
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
function allComponentNames(system) {
|
|
113
|
-
const compsDir = join(systemDir(system), "components");
|
|
114
|
-
const results = [];
|
|
115
|
-
for (const kind of ["molecules", "cells"]) {
|
|
116
|
-
const kindDir = join(compsDir, kind);
|
|
117
|
-
if (!existsSync(kindDir)) continue;
|
|
118
|
-
const dirs = readdirSync(kindDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => ({ name: d.name, kind }));
|
|
119
|
-
results.push(...dirs);
|
|
120
|
-
}
|
|
121
|
-
return results;
|
|
122
|
-
}
|
|
123
|
-
function suggestComponent(system, query) {
|
|
124
|
-
const all = allComponentNames(system);
|
|
125
|
-
const q = query.toLowerCase();
|
|
126
|
-
return all.filter((c) => c.name.includes(q) || q.includes(c.name)).map((c) => `${c.name} (${c.kind})`);
|
|
127
|
-
}
|
|
128
|
-
function readComponentMeta(system, componentName) {
|
|
129
|
-
const found = findComponent(system, componentName);
|
|
130
|
-
if (!found) return { name: componentName, tag: "", description: "" };
|
|
131
|
-
const descPath = join(found.dir, "description.md");
|
|
132
|
-
if (!existsSync(descPath)) return { name: componentName, tag: "", description: "" };
|
|
133
|
-
const lines = readFileSync(descPath, "utf-8").split("\n");
|
|
134
|
-
let name = componentName;
|
|
135
|
-
let tag = "";
|
|
136
|
-
let description = "";
|
|
137
|
-
for (let i = 0; i < lines.length; i++) {
|
|
138
|
-
const line = lines[i];
|
|
139
|
-
if (line.startsWith("# ")) {
|
|
140
|
-
name = line.slice(2).trim();
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (!tag && /^`<.+>`$/.test(line.trim())) {
|
|
144
|
-
tag = line.trim();
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (name && line.trim().length > 0 && !line.startsWith("#") && !line.startsWith("`")) {
|
|
148
|
-
description = line.trim();
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return { name, tag: tag || "*(auto)*", description };
|
|
153
|
-
}
|
|
154
|
-
function readFullDescription(system, name, kind) {
|
|
155
|
-
const compDir = join(systemDir(system), "components", kind, name);
|
|
156
|
-
if (!existsSync(compDir)) return "";
|
|
157
|
-
const mdPath = join(compDir, "description.md");
|
|
158
|
-
if (existsSync(mdPath)) {
|
|
159
|
-
return readFileSync(mdPath, "utf-8");
|
|
160
|
-
}
|
|
161
|
-
const json = readComponentJson(compDir);
|
|
162
|
-
return json?.description ?? "";
|
|
163
|
-
}
|
|
164
|
-
function descriptionMtime(system, name, kind) {
|
|
165
|
-
const compDir = join(systemDir(system), "components", kind, name);
|
|
166
|
-
if (!existsSync(compDir)) return 0;
|
|
167
|
-
const mdPath = join(compDir, "description.md");
|
|
168
|
-
if (existsSync(mdPath)) {
|
|
169
|
-
return statSync(mdPath).mtimeMs;
|
|
170
|
-
}
|
|
171
|
-
const jsonPath = join(compDir, "description.json");
|
|
172
|
-
if (existsSync(jsonPath)) {
|
|
173
|
-
return statSync(jsonPath).mtimeMs;
|
|
174
|
-
}
|
|
175
|
-
return 0;
|
|
176
|
-
}
|
|
177
|
-
function findComponentSources(system, name) {
|
|
178
|
-
const found = findComponent(system, name);
|
|
179
|
-
if (!found) return { css: [], js: [] };
|
|
180
|
-
const css = [];
|
|
181
|
-
const js = [];
|
|
182
|
-
const stylePath = join(found.dir, "style.css");
|
|
183
|
-
const scriptPath = join(found.dir, "component.js");
|
|
184
|
-
if (existsSync(stylePath)) css.push(stylePath);
|
|
185
|
-
if (existsSync(scriptPath)) js.push(scriptPath);
|
|
186
|
-
return { css, js };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
27
|
// src/format.ts
|
|
190
28
|
function heading(text) {
|
|
191
29
|
console.log(`
|
|
@@ -236,12 +74,12 @@ function commandList() {
|
|
|
236
74
|
}
|
|
237
75
|
|
|
238
76
|
// src/commands/show.ts
|
|
239
|
-
import { existsSync
|
|
240
|
-
import { join
|
|
77
|
+
import { existsSync, readFileSync } from "fs";
|
|
78
|
+
import { join } from "path";
|
|
241
79
|
function loadManifest(system) {
|
|
242
|
-
const path =
|
|
243
|
-
if (!
|
|
244
|
-
const data = JSON.parse(
|
|
80
|
+
const path = join(systemDir(system), "references", "manifest.json");
|
|
81
|
+
if (!existsSync(path)) return [];
|
|
82
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
245
83
|
return data.references || [];
|
|
246
84
|
}
|
|
247
85
|
function commandShow(system) {
|
|
@@ -292,8 +130,8 @@ function commandShow(system) {
|
|
|
292
130
|
import { exec } from "child_process";
|
|
293
131
|
|
|
294
132
|
// src/commands/components.ts
|
|
295
|
-
import { join as
|
|
296
|
-
import { existsSync as
|
|
133
|
+
import { join as join3, basename, relative } from "path";
|
|
134
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
297
135
|
|
|
298
136
|
// src/search/tfidf.ts
|
|
299
137
|
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
@@ -420,11 +258,11 @@ function cosineSimilarity(a, b) {
|
|
|
420
258
|
}
|
|
421
259
|
|
|
422
260
|
// src/search/indexer.ts
|
|
423
|
-
import { readFileSync as
|
|
424
|
-
import { join as
|
|
261
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
262
|
+
import { join as join2 } from "path";
|
|
425
263
|
var INDEX_FILE = ".search-index.json";
|
|
426
264
|
function indexPath(system) {
|
|
427
|
-
return
|
|
265
|
+
return join2(systemDir(system), INDEX_FILE);
|
|
428
266
|
}
|
|
429
267
|
function buildSearchIndex(system) {
|
|
430
268
|
const components = listComponents(system);
|
|
@@ -471,9 +309,9 @@ function writeSearchIndex(system, index) {
|
|
|
471
309
|
}
|
|
472
310
|
function loadSearchIndex(system) {
|
|
473
311
|
const path = indexPath(system);
|
|
474
|
-
if (!
|
|
312
|
+
if (!existsSync2(path)) return null;
|
|
475
313
|
try {
|
|
476
|
-
const data = JSON.parse(
|
|
314
|
+
const data = JSON.parse(readFileSync2(path, "utf-8"));
|
|
477
315
|
if (data.version !== 1) return null;
|
|
478
316
|
return data;
|
|
479
317
|
} catch {
|
|
@@ -633,7 +471,7 @@ function showDetails(system, name) {
|
|
|
633
471
|
console.log(` Kind: ${found.kind}`);
|
|
634
472
|
if (json?.tag) console.log(` Tag: ${json.tag}`);
|
|
635
473
|
console.log(` Path: ${relPath}`);
|
|
636
|
-
const files =
|
|
474
|
+
const files = readdirSync(found.dir);
|
|
637
475
|
console.log(` Files: ${files.join(", ")}`);
|
|
638
476
|
blank();
|
|
639
477
|
if (json?.description) {
|
|
@@ -650,8 +488,8 @@ function showDetails(system, name) {
|
|
|
650
488
|
console.log("```");
|
|
651
489
|
blank();
|
|
652
490
|
}
|
|
653
|
-
const descPath =
|
|
654
|
-
if (
|
|
491
|
+
const descPath = join3(found.dir, "description.md");
|
|
492
|
+
if (existsSync3(descPath)) {
|
|
655
493
|
subheading("Description (full)");
|
|
656
494
|
blank();
|
|
657
495
|
console.log(readFile(descPath));
|
|
@@ -662,7 +500,7 @@ function showDetails(system, name) {
|
|
|
662
500
|
subheading(`Source CSS: ${basename(cssPath)}`);
|
|
663
501
|
blank();
|
|
664
502
|
console.log("```css");
|
|
665
|
-
console.log(
|
|
503
|
+
console.log(readFileSync3(cssPath, "utf-8").trimEnd());
|
|
666
504
|
console.log("```");
|
|
667
505
|
blank();
|
|
668
506
|
}
|
|
@@ -672,7 +510,7 @@ function showDetails(system, name) {
|
|
|
672
510
|
subheading(`Source JS: ${basename(jsPath)}`);
|
|
673
511
|
blank();
|
|
674
512
|
console.log("```js");
|
|
675
|
-
console.log(
|
|
513
|
+
console.log(readFileSync3(jsPath, "utf-8").trimEnd());
|
|
676
514
|
console.log("```");
|
|
677
515
|
blank();
|
|
678
516
|
}
|
|
@@ -746,24 +584,24 @@ function rebuildIndex(system) {
|
|
|
746
584
|
}
|
|
747
585
|
|
|
748
586
|
// src/commands/references.ts
|
|
749
|
-
import { existsSync as
|
|
750
|
-
import { join as
|
|
587
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
|
|
588
|
+
import { join as join4, extname } from "path";
|
|
751
589
|
function referencesDir(system) {
|
|
752
|
-
return
|
|
590
|
+
return join4(systemDir(system), "references");
|
|
753
591
|
}
|
|
754
592
|
function loadManifest2(system) {
|
|
755
|
-
const path =
|
|
756
|
-
if (!
|
|
757
|
-
return JSON.parse(
|
|
593
|
+
const path = join4(referencesDir(system), "manifest.json");
|
|
594
|
+
if (!existsSync4(path)) return null;
|
|
595
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
758
596
|
}
|
|
759
597
|
function listReferenceFiles(system) {
|
|
760
598
|
const dir = referencesDir(system);
|
|
761
|
-
if (!
|
|
762
|
-
return
|
|
599
|
+
if (!existsSync4(dir)) return [];
|
|
600
|
+
return readdirSync2(dir).filter((f) => f !== "manifest.json").sort();
|
|
763
601
|
}
|
|
764
602
|
function commandReferences(system, segments) {
|
|
765
603
|
const dir = referencesDir(system);
|
|
766
|
-
if (!
|
|
604
|
+
if (!existsSync4(dir)) {
|
|
767
605
|
error(`No references directory for "${system}"`);
|
|
768
606
|
return 1;
|
|
769
607
|
}
|
|
@@ -793,8 +631,8 @@ function commandReferences(system, segments) {
|
|
|
793
631
|
return 0;
|
|
794
632
|
}
|
|
795
633
|
const fileName = segments.join("/");
|
|
796
|
-
const filePath =
|
|
797
|
-
if (!
|
|
634
|
+
const filePath = join4(dir, fileName);
|
|
635
|
+
if (!existsSync4(filePath)) {
|
|
798
636
|
const files = listReferenceFiles(system);
|
|
799
637
|
const suggestions = files.filter(
|
|
800
638
|
(f) => f.includes(fileName) || fileName.includes(f.replace(extname(f), ""))
|
|
@@ -802,301 +640,14 @@ function commandReferences(system, segments) {
|
|
|
802
640
|
error(`Reference "${fileName}" not found`, suggestions);
|
|
803
641
|
return 1;
|
|
804
642
|
}
|
|
805
|
-
const content =
|
|
643
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
806
644
|
process.stdout.write(content);
|
|
807
645
|
return 0;
|
|
808
646
|
}
|
|
809
647
|
|
|
810
648
|
// src/commands/render.ts
|
|
811
|
-
import { readFileSync as
|
|
812
|
-
import { resolve
|
|
813
|
-
|
|
814
|
-
// src/resolve-deps.ts
|
|
815
|
-
import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
|
|
816
|
-
import { join as join7, relative as relative2 } from "path";
|
|
817
|
-
|
|
818
|
-
// src/tokens.ts
|
|
819
|
-
import { readFileSync as readFileSync6, readdirSync as readdirSync4 } from "fs";
|
|
820
|
-
import { join as join6, basename as basename2 } from "path";
|
|
821
|
-
function loadTokens(system) {
|
|
822
|
-
const dir = join6(systemDir(system), "tokens");
|
|
823
|
-
const files = readdirSync4(dir).filter((f) => f.endsWith(".json")).sort();
|
|
824
|
-
return files.map((f) => ({
|
|
825
|
-
name: basename2(f, ".json"),
|
|
826
|
-
data: JSON.parse(readFileSync6(join6(dir, f), "utf-8"))
|
|
827
|
-
}));
|
|
828
|
-
}
|
|
829
|
-
function walkTokens(obj, path = []) {
|
|
830
|
-
const leaves = [];
|
|
831
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
832
|
-
if (key.startsWith("$")) continue;
|
|
833
|
-
const child = val;
|
|
834
|
-
if (child.$type !== void 0 && child.$value !== void 0) {
|
|
835
|
-
leaves.push({ path: [...path, key], type: child.$type, value: child.$value });
|
|
836
|
-
} else if (typeof child === "object" && child !== null) {
|
|
837
|
-
leaves.push(...walkTokens(child, [...path, key]));
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return leaves;
|
|
841
|
-
}
|
|
842
|
-
function toCssVarName(path) {
|
|
843
|
-
return `--${path.join("-")}`;
|
|
844
|
-
}
|
|
845
|
-
function formatValue(type, value) {
|
|
846
|
-
switch (type) {
|
|
847
|
-
case "color":
|
|
848
|
-
case "dimension":
|
|
849
|
-
case "gradient":
|
|
850
|
-
case "duration":
|
|
851
|
-
return String(value);
|
|
852
|
-
case "fontFamily": {
|
|
853
|
-
const families = value;
|
|
854
|
-
const generics = ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui"];
|
|
855
|
-
return families.map((f) => {
|
|
856
|
-
if (generics.includes(f) || !/\s/.test(f)) return f;
|
|
857
|
-
return f.includes("'") ? `"${f}"` : `'${f}'`;
|
|
858
|
-
}).join(", ");
|
|
859
|
-
}
|
|
860
|
-
// Composite types — skip
|
|
861
|
-
case "typography":
|
|
862
|
-
case "object":
|
|
863
|
-
case "cubicBezier":
|
|
864
|
-
return null;
|
|
865
|
-
default:
|
|
866
|
-
return null;
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
function generateTokensCss(system) {
|
|
870
|
-
const tokenFiles = loadTokens(system);
|
|
871
|
-
const groups = [];
|
|
872
|
-
for (const { name, data } of tokenFiles) {
|
|
873
|
-
const leaves = walkTokens(data);
|
|
874
|
-
const vars = [];
|
|
875
|
-
for (const leaf of leaves) {
|
|
876
|
-
const formatted = formatValue(leaf.type, leaf.value);
|
|
877
|
-
if (formatted !== null) {
|
|
878
|
-
vars.push(` ${toCssVarName(leaf.path)}: ${formatted};`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
if (vars.length > 0) {
|
|
882
|
-
groups.push({ name, vars });
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
const lines = ["@layer tokens {", " :root {"];
|
|
886
|
-
for (let i = 0; i < groups.length; i++) {
|
|
887
|
-
const { name, vars } = groups[i];
|
|
888
|
-
if (i > 0) lines.push("");
|
|
889
|
-
lines.push(` /* ${name}.json */`);
|
|
890
|
-
lines.push(...vars);
|
|
891
|
-
}
|
|
892
|
-
lines.push(" }", "}");
|
|
893
|
-
return lines.join("\n") + "\n";
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
// src/resolve-deps.ts
|
|
897
|
-
function buildRegistry(system) {
|
|
898
|
-
const components = listComponents(system);
|
|
899
|
-
const entries = [];
|
|
900
|
-
const tagMap = /* @__PURE__ */ new Map();
|
|
901
|
-
for (const comp of components) {
|
|
902
|
-
const found = findComponent(system, comp.name);
|
|
903
|
-
if (!found) continue;
|
|
904
|
-
const scriptPath = join7(found.dir, "component.js");
|
|
905
|
-
const stylePath = join7(found.dir, "style.css");
|
|
906
|
-
const hasJs = existsSync6(scriptPath);
|
|
907
|
-
const hasCss = existsSync6(stylePath);
|
|
908
|
-
const tags = [];
|
|
909
|
-
const dynamicTags = [];
|
|
910
|
-
if (hasJs) {
|
|
911
|
-
const js = readFileSync7(scriptPath, "utf-8");
|
|
912
|
-
for (const m of js.matchAll(/customElements\.define\(\s*['"]([^'"]+)['"]/g)) {
|
|
913
|
-
tags.push(m[1]);
|
|
914
|
-
}
|
|
915
|
-
for (const m of js.matchAll(/document\.createElement\(\s*['"]([a-z][a-z0-9]*-[a-z0-9-]*)['"]\s*\)/g)) {
|
|
916
|
-
if (!tags.includes(m[1])) dynamicTags.push(m[1]);
|
|
917
|
-
}
|
|
918
|
-
for (const m of js.matchAll(/<([a-z][a-z0-9]*-[a-z0-9-]*)[>\s/'"]/g)) {
|
|
919
|
-
const tag = m[1];
|
|
920
|
-
if (!tags.includes(tag) && !dynamicTags.includes(tag)) {
|
|
921
|
-
dynamicTags.push(tag);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
if (hasCss) {
|
|
926
|
-
const css = readFileSync7(stylePath, "utf-8");
|
|
927
|
-
for (const m of css.matchAll(/(?:^|[\s,}>+~])([a-z][a-z0-9]*-[a-z0-9-]*)(?=[\s,{[:.>+~]|$)/gm)) {
|
|
928
|
-
const tag = m[1];
|
|
929
|
-
if (!tags.includes(tag)) tags.push(tag);
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
const dirName = comp.name;
|
|
933
|
-
if (dirName.includes("-") && !tags.includes(dirName)) {
|
|
934
|
-
tags.push(dirName);
|
|
935
|
-
}
|
|
936
|
-
const prdName = `prd-${dirName}`;
|
|
937
|
-
if (!tags.includes(prdName)) {
|
|
938
|
-
tags.push(prdName);
|
|
939
|
-
}
|
|
940
|
-
const entry = {
|
|
941
|
-
name: comp.name,
|
|
942
|
-
kind: comp.kind,
|
|
943
|
-
dir: found.dir,
|
|
944
|
-
tags,
|
|
945
|
-
dynamicTags,
|
|
946
|
-
hasCss,
|
|
947
|
-
hasJs
|
|
948
|
-
};
|
|
949
|
-
entries.push(entry);
|
|
950
|
-
for (const tag of tags) {
|
|
951
|
-
if (!tagMap.has(tag)) tagMap.set(tag, entry);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
return { tagMap, entries };
|
|
955
|
-
}
|
|
956
|
-
function resolveMarkup(registry, markup) {
|
|
957
|
-
const markupTags = /* @__PURE__ */ new Set();
|
|
958
|
-
for (const m of markup.matchAll(/<([a-z][a-z0-9]*-[a-z0-9-]*)/g)) {
|
|
959
|
-
markupTags.add(m[1]);
|
|
960
|
-
}
|
|
961
|
-
const matched = /* @__PURE__ */ new Set();
|
|
962
|
-
const unmatchedTags = [];
|
|
963
|
-
const resolved = /* @__PURE__ */ new Set();
|
|
964
|
-
const queue = [...markupTags];
|
|
965
|
-
while (queue.length > 0) {
|
|
966
|
-
const tag = queue.pop();
|
|
967
|
-
if (resolved.has(tag)) continue;
|
|
968
|
-
resolved.add(tag);
|
|
969
|
-
const entry = registry.tagMap.get(tag);
|
|
970
|
-
if (entry) {
|
|
971
|
-
if (!matched.has(entry)) {
|
|
972
|
-
matched.add(entry);
|
|
973
|
-
for (const dt of entry.dynamicTags) {
|
|
974
|
-
if (!resolved.has(dt)) queue.push(dt);
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
} else {
|
|
978
|
-
unmatchedTags.push(tag);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
const cssFiles = [];
|
|
982
|
-
const jsFiles = [];
|
|
983
|
-
for (const entry of matched) {
|
|
984
|
-
if (entry.hasCss) cssFiles.push(join7(entry.dir, "style.css"));
|
|
985
|
-
if (entry.hasJs) jsFiles.push(join7(entry.dir, "component.js"));
|
|
986
|
-
}
|
|
987
|
-
return { cssFiles, jsFiles, unmatchedTags };
|
|
988
|
-
}
|
|
989
|
-
async function bundleJs(system) {
|
|
990
|
-
const entrypoint = join7(systemDir(system), "src", "index.js");
|
|
991
|
-
if (!existsSync6(entrypoint)) return null;
|
|
992
|
-
const result = await Bun.build({
|
|
993
|
-
entrypoints: [entrypoint],
|
|
994
|
-
bundle: true
|
|
995
|
-
});
|
|
996
|
-
if (!result.success) {
|
|
997
|
-
console.warn("Bundle failed:", result.logs);
|
|
998
|
-
return null;
|
|
999
|
-
}
|
|
1000
|
-
return await result.outputs[0].text();
|
|
1001
|
-
}
|
|
1002
|
-
function buildPreviewHtml(system, markup, deps, bundledJs) {
|
|
1003
|
-
const sysDir = systemDir(system);
|
|
1004
|
-
const blocks = ["@layer tokens, base, components;"];
|
|
1005
|
-
const tokensCss = generateTokensCss(system);
|
|
1006
|
-
blocks.push("/* tokens (generated) */");
|
|
1007
|
-
blocks.push(tokensCss);
|
|
1008
|
-
const baseCss = join7(sysDir, "src", "_shared", "_base.css");
|
|
1009
|
-
if (existsSync6(baseCss)) {
|
|
1010
|
-
blocks.push(`/* ${relative2(sysDir, baseCss)} */`);
|
|
1011
|
-
blocks.push(readFileSync7(baseCss, "utf-8"));
|
|
1012
|
-
}
|
|
1013
|
-
for (const p of deps.cssFiles) {
|
|
1014
|
-
blocks.push(`/* ${relative2(sysDir, p)} */`);
|
|
1015
|
-
blocks.push(readFileSync7(p, "utf-8"));
|
|
1016
|
-
}
|
|
1017
|
-
const inlinedCss = blocks.join("\n");
|
|
1018
|
-
const scriptTags = bundledJs ? ` <script>
|
|
1019
|
-
${bundledJs}
|
|
1020
|
-
</script>` : deps.jsFiles.map((p) => ` <script type="module" src="/${relative2(sysDir, p)}"></script>`).join("\n");
|
|
1021
|
-
return `<!DOCTYPE html>
|
|
1022
|
-
<html lang="en">
|
|
1023
|
-
<head>
|
|
1024
|
-
<meta charset="UTF-8">
|
|
1025
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1026
|
-
<style>
|
|
1027
|
-
${inlinedCss}
|
|
1028
|
-
</style>
|
|
1029
|
-
</head>
|
|
1030
|
-
<body>
|
|
1031
|
-
${markup}
|
|
1032
|
-
${scriptTags}
|
|
1033
|
-
</body>
|
|
1034
|
-
</html>`;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// src/preview-server.ts
|
|
1038
|
-
import { createServer } from "http";
|
|
1039
|
-
import { join as join8, extname as extname2 } from "path";
|
|
1040
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1041
|
-
import { existsSync as existsSync7 } from "fs";
|
|
1042
|
-
var MIME_TYPES = {
|
|
1043
|
-
".html": "text/html; charset=utf-8",
|
|
1044
|
-
".css": "text/css; charset=utf-8",
|
|
1045
|
-
".js": "application/javascript; charset=utf-8",
|
|
1046
|
-
".json": "application/json; charset=utf-8",
|
|
1047
|
-
".svg": "image/svg+xml",
|
|
1048
|
-
".png": "image/png",
|
|
1049
|
-
".jpg": "image/jpeg",
|
|
1050
|
-
".jpeg": "image/jpeg",
|
|
1051
|
-
".gif": "image/gif",
|
|
1052
|
-
".woff": "font/woff",
|
|
1053
|
-
".woff2": "font/woff2",
|
|
1054
|
-
".ttf": "font/ttf",
|
|
1055
|
-
".otf": "font/otf",
|
|
1056
|
-
".eot": "application/vnd.ms-fontobject"
|
|
1057
|
-
};
|
|
1058
|
-
function startPreviewServer(systemDir2, opts) {
|
|
1059
|
-
return new Promise((resolve3, reject) => {
|
|
1060
|
-
const server = createServer(async (req, res) => {
|
|
1061
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1062
|
-
if (url.pathname === "/" && opts.html) {
|
|
1063
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1064
|
-
res.end(opts.html);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
const filePath = join8(systemDir2, decodeURIComponent(url.pathname));
|
|
1068
|
-
if (!existsSync7(filePath)) {
|
|
1069
|
-
res.writeHead(404);
|
|
1070
|
-
res.end("Not found");
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
try {
|
|
1074
|
-
const data = await readFile2(filePath);
|
|
1075
|
-
const ext = extname2(filePath).toLowerCase();
|
|
1076
|
-
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1077
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
1078
|
-
res.end(data);
|
|
1079
|
-
} catch {
|
|
1080
|
-
res.writeHead(500);
|
|
1081
|
-
res.end("Internal server error");
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
server.listen(0, () => {
|
|
1085
|
-
const addr = server.address();
|
|
1086
|
-
if (!addr || typeof addr === "string") {
|
|
1087
|
-
reject(new Error("Failed to get server address"));
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
resolve3({
|
|
1091
|
-
url: `http://localhost:${addr.port}`,
|
|
1092
|
-
stop: () => server.close()
|
|
1093
|
-
});
|
|
1094
|
-
});
|
|
1095
|
-
server.on("error", reject);
|
|
1096
|
-
});
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
// src/commands/render.ts
|
|
649
|
+
import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
|
|
650
|
+
import { resolve } from "path";
|
|
1100
651
|
async function readStdin() {
|
|
1101
652
|
const chunks = [];
|
|
1102
653
|
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
@@ -1122,12 +673,12 @@ async function commandRender(system, fileArg, flags) {
|
|
|
1122
673
|
if (fileArg === "-") {
|
|
1123
674
|
markup = await readStdin();
|
|
1124
675
|
} else {
|
|
1125
|
-
const filePath =
|
|
1126
|
-
if (!
|
|
676
|
+
const filePath = resolve(fileArg);
|
|
677
|
+
if (!existsSync5(filePath)) {
|
|
1127
678
|
error(`File not found: ${filePath}`);
|
|
1128
679
|
return 1;
|
|
1129
680
|
}
|
|
1130
|
-
markup =
|
|
681
|
+
markup = readFileSync5(filePath, "utf-8");
|
|
1131
682
|
}
|
|
1132
683
|
const registry = buildRegistry(system);
|
|
1133
684
|
const deps = resolveMarkup(registry, markup);
|
|
@@ -1186,6 +737,317 @@ function openInBrowser(target) {
|
|
|
1186
737
|
exec(`${cmd} "${target}"`);
|
|
1187
738
|
}
|
|
1188
739
|
|
|
740
|
+
// src/commands/health.ts
|
|
741
|
+
import { readFileSync as readFileSync6, existsSync as existsSync6, readdirSync as readdirSync3 } from "fs";
|
|
742
|
+
import { join as join5 } from "path";
|
|
743
|
+
function pass(label) {
|
|
744
|
+
return { label, passed: true };
|
|
745
|
+
}
|
|
746
|
+
function fail(label, detail) {
|
|
747
|
+
return { label, passed: false, detail };
|
|
748
|
+
}
|
|
749
|
+
function commandHealth(system) {
|
|
750
|
+
if (!systemExists(system)) {
|
|
751
|
+
const suggestions = listSystems().filter(
|
|
752
|
+
(s) => s.includes(system) || system.includes(s)
|
|
753
|
+
);
|
|
754
|
+
error(`Design system "${system}" not found`, suggestions);
|
|
755
|
+
return 1;
|
|
756
|
+
}
|
|
757
|
+
const sysDir = systemDir(system);
|
|
758
|
+
const results = [];
|
|
759
|
+
heading("Folder \u2194 Tag consistency");
|
|
760
|
+
for (const kind of ["molecules", "cells"]) {
|
|
761
|
+
const kindDir = join5(sysDir, "components", kind);
|
|
762
|
+
if (!existsSync6(kindDir)) continue;
|
|
763
|
+
for (const d of readdirSync3(kindDir, { withFileTypes: true })) {
|
|
764
|
+
if (!d.isDirectory()) continue;
|
|
765
|
+
const compDir = join5(kindDir, d.name);
|
|
766
|
+
const json = readComponentJson(compDir);
|
|
767
|
+
if (!json?.tag) {
|
|
768
|
+
results.push(fail(
|
|
769
|
+
`${kind}/${d.name}: tag`,
|
|
770
|
+
"missing tag in description.json"
|
|
771
|
+
));
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const tagName = json.tag.replace(/^<|>$/g, "");
|
|
775
|
+
if (tagName !== d.name) {
|
|
776
|
+
results.push(fail(
|
|
777
|
+
`${kind}/${d.name}: tag`,
|
|
778
|
+
`folder "${d.name}" \u2260 tag "${tagName}"`
|
|
779
|
+
));
|
|
780
|
+
} else {
|
|
781
|
+
results.push(pass(`${kind}/${d.name}: tag`));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
heading("Required files");
|
|
786
|
+
for (const kind of ["molecules", "cells"]) {
|
|
787
|
+
const kindDir = join5(sysDir, "components", kind);
|
|
788
|
+
if (!existsSync6(kindDir)) continue;
|
|
789
|
+
for (const d of readdirSync3(kindDir, { withFileTypes: true })) {
|
|
790
|
+
if (!d.isDirectory()) continue;
|
|
791
|
+
const compDir = join5(kindDir, d.name);
|
|
792
|
+
if (!existsSync6(join5(compDir, "description.json"))) {
|
|
793
|
+
results.push(fail(`${kind}/${d.name}: description.json`, "missing"));
|
|
794
|
+
} else {
|
|
795
|
+
results.push(pass(`${kind}/${d.name}: description.json`));
|
|
796
|
+
}
|
|
797
|
+
if (!existsSync6(join5(compDir, "style.css"))) {
|
|
798
|
+
results.push(fail(`${kind}/${d.name}: style.css`, "missing"));
|
|
799
|
+
} else {
|
|
800
|
+
results.push(pass(`${kind}/${d.name}: style.css`));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
heading("description.json completeness");
|
|
805
|
+
for (const kind of ["molecules", "cells"]) {
|
|
806
|
+
const kindDir = join5(sysDir, "components", kind);
|
|
807
|
+
if (!existsSync6(kindDir)) continue;
|
|
808
|
+
for (const d of readdirSync3(kindDir, { withFileTypes: true })) {
|
|
809
|
+
if (!d.isDirectory()) continue;
|
|
810
|
+
const compDir = join5(kindDir, d.name);
|
|
811
|
+
const json = readComponentJson(compDir);
|
|
812
|
+
if (!json) continue;
|
|
813
|
+
for (const field of ["description", "tag", "example"]) {
|
|
814
|
+
const val = json[field];
|
|
815
|
+
if (!val || typeof val === "string" && val.trim() === "") {
|
|
816
|
+
results.push(fail(
|
|
817
|
+
`${kind}/${d.name}: ${field}`,
|
|
818
|
+
`empty or missing "${field}" field`
|
|
819
|
+
));
|
|
820
|
+
} else {
|
|
821
|
+
results.push(pass(`${kind}/${d.name}: ${field}`));
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
heading("index.js imports");
|
|
827
|
+
const indexPath2 = join5(sysDir, "src", "index.js");
|
|
828
|
+
if (!existsSync6(indexPath2)) {
|
|
829
|
+
results.push(fail("src/index.js", "file not found"));
|
|
830
|
+
} else {
|
|
831
|
+
const indexSrc = readFileSync6(indexPath2, "utf-8");
|
|
832
|
+
const importPaths = [...indexSrc.matchAll(/import\s+['"]([^'"]+)['"]/g)].map(
|
|
833
|
+
(m) => m[1]
|
|
834
|
+
);
|
|
835
|
+
for (const imp of importPaths) {
|
|
836
|
+
const resolved = join5(sysDir, "src", imp);
|
|
837
|
+
if (!existsSync6(resolved)) {
|
|
838
|
+
results.push(fail(`import: ${imp}`, "file not found"));
|
|
839
|
+
} else {
|
|
840
|
+
results.push(pass(`import: ${imp}`));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const kind of ["molecules", "cells"]) {
|
|
844
|
+
const kindDir = join5(sysDir, "components", kind);
|
|
845
|
+
if (!existsSync6(kindDir)) continue;
|
|
846
|
+
for (const d of readdirSync3(kindDir, { withFileTypes: true })) {
|
|
847
|
+
if (!d.isDirectory()) continue;
|
|
848
|
+
const jsPath = join5(kindDir, d.name, "component.js");
|
|
849
|
+
if (!existsSync6(jsPath)) continue;
|
|
850
|
+
const expectedFragment = `components/${kind}/${d.name}/component.js`;
|
|
851
|
+
const imported = importPaths.some((p) => p.includes(expectedFragment));
|
|
852
|
+
if (!imported) {
|
|
853
|
+
results.push(fail(
|
|
854
|
+
`${kind}/${d.name}: import`,
|
|
855
|
+
`component.js exists but not imported in index.js`
|
|
856
|
+
));
|
|
857
|
+
} else {
|
|
858
|
+
results.push(pass(`${kind}/${d.name}: import`));
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
heading("Token files");
|
|
864
|
+
const tokensDir = join5(sysDir, "tokens");
|
|
865
|
+
for (const required of ["colors.json", "effects.json", "layout.json"]) {
|
|
866
|
+
if (!existsSync6(join5(tokensDir, required))) {
|
|
867
|
+
results.push(fail(`tokens/${required}`, "missing"));
|
|
868
|
+
} else {
|
|
869
|
+
results.push(pass(`tokens/${required}`));
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
heading("Assets");
|
|
873
|
+
const fontsDir = join5(sysDir, "assets", "fonts");
|
|
874
|
+
if (!existsSync6(fontsDir)) {
|
|
875
|
+
results.push(fail("assets/fonts", "directory not found"));
|
|
876
|
+
} else {
|
|
877
|
+
const fontFamilies = readdirSync3(fontsDir, { withFileTypes: true }).filter(
|
|
878
|
+
(d) => d.isDirectory()
|
|
879
|
+
);
|
|
880
|
+
if (fontFamilies.length === 0) {
|
|
881
|
+
results.push(fail("assets/fonts", "no font family directories"));
|
|
882
|
+
} else {
|
|
883
|
+
let hasWoff2 = false;
|
|
884
|
+
for (const fam of fontFamilies) {
|
|
885
|
+
const files = readdirSync3(join5(fontsDir, fam.name));
|
|
886
|
+
if (files.some((f) => f.endsWith(".woff2"))) hasWoff2 = true;
|
|
887
|
+
}
|
|
888
|
+
if (!hasWoff2) {
|
|
889
|
+
results.push(fail("assets/fonts", "no .woff2 files found"));
|
|
890
|
+
} else {
|
|
891
|
+
results.push(pass("assets/fonts"));
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const passed = results.filter((r) => r.passed).length;
|
|
896
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
897
|
+
if (failed > 0) {
|
|
898
|
+
heading("Failures");
|
|
899
|
+
for (const r of results) {
|
|
900
|
+
if (!r.passed) {
|
|
901
|
+
console.log(` FAIL ${r.label} \u2014 ${r.detail}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
console.log();
|
|
906
|
+
console.log(
|
|
907
|
+
`${passed} passed, ${failed} failed`
|
|
908
|
+
);
|
|
909
|
+
return failed > 0 ? 1 : 0;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// src/commands/check.ts
|
|
913
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
914
|
+
import { resolve as resolve2 } from "path";
|
|
915
|
+
function pass2(label) {
|
|
916
|
+
return { label, passed: true };
|
|
917
|
+
}
|
|
918
|
+
function fail2(label, detail) {
|
|
919
|
+
return { label, passed: false, detail };
|
|
920
|
+
}
|
|
921
|
+
async function readStdin2() {
|
|
922
|
+
const chunks = [];
|
|
923
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
924
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
925
|
+
}
|
|
926
|
+
function extractCustomElements(markup) {
|
|
927
|
+
const results = [];
|
|
928
|
+
const lines = markup.split("\n");
|
|
929
|
+
for (let i = 0; i < lines.length; i++) {
|
|
930
|
+
const lineText = lines[i];
|
|
931
|
+
for (const m of lineText.matchAll(/<([a-z][a-z0-9]*-[a-z0-9-]*)([^>]*)/g)) {
|
|
932
|
+
results.push({
|
|
933
|
+
tag: m[1],
|
|
934
|
+
line: i + 1,
|
|
935
|
+
attrs: m[2]
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return results;
|
|
940
|
+
}
|
|
941
|
+
function checkAttributes(registry, elements) {
|
|
942
|
+
const results = [];
|
|
943
|
+
for (const el of elements) {
|
|
944
|
+
const entry = registry.tagMap.get(el.tag);
|
|
945
|
+
if (!entry) continue;
|
|
946
|
+
for (const m of el.attrs.matchAll(/(\w+)=""/g)) {
|
|
947
|
+
results.push(fail2(
|
|
948
|
+
`line ${el.line}: <${el.tag}>`,
|
|
949
|
+
`empty attribute "${m[1]}"`
|
|
950
|
+
));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return results;
|
|
954
|
+
}
|
|
955
|
+
async function commandCheck(system, fileArg) {
|
|
956
|
+
if (!systemExists(system)) {
|
|
957
|
+
const suggestions = listSystems().filter(
|
|
958
|
+
(s) => s.includes(system) || system.includes(s)
|
|
959
|
+
);
|
|
960
|
+
error(`Design system "${system}" not found`, suggestions);
|
|
961
|
+
return 1;
|
|
962
|
+
}
|
|
963
|
+
if (!fileArg) {
|
|
964
|
+
error("Usage: ds <system> check <file|->", [
|
|
965
|
+
"ds fw-prd check page.xml",
|
|
966
|
+
"cat page.xml | ds fw-prd check -"
|
|
967
|
+
]);
|
|
968
|
+
return 1;
|
|
969
|
+
}
|
|
970
|
+
let markup;
|
|
971
|
+
let fileName;
|
|
972
|
+
if (fileArg === "-") {
|
|
973
|
+
markup = await readStdin2();
|
|
974
|
+
fileName = "<stdin>";
|
|
975
|
+
} else {
|
|
976
|
+
const filePath = resolve2(fileArg);
|
|
977
|
+
if (!existsSync7(filePath)) {
|
|
978
|
+
error(`File not found: ${filePath}`);
|
|
979
|
+
return 1;
|
|
980
|
+
}
|
|
981
|
+
markup = readFileSync7(filePath, "utf-8");
|
|
982
|
+
fileName = fileArg;
|
|
983
|
+
}
|
|
984
|
+
const results = [];
|
|
985
|
+
heading(`Checking ${fileName}`);
|
|
986
|
+
const registry = buildRegistry(system);
|
|
987
|
+
const deps = resolveMarkup(registry, markup);
|
|
988
|
+
const elements = extractCustomElements(markup);
|
|
989
|
+
const uniqueTags = new Set(elements.map((e) => e.tag));
|
|
990
|
+
console.log(` ${uniqueTags.size} unique custom elements found`);
|
|
991
|
+
console.log(` ${elements.length} total usages
|
|
992
|
+
`);
|
|
993
|
+
heading("Tag resolution");
|
|
994
|
+
for (const tag of uniqueTags) {
|
|
995
|
+
if (registry.tagMap.has(tag)) {
|
|
996
|
+
const entry = registry.tagMap.get(tag);
|
|
997
|
+
results.push(pass2(`<${tag}> \u2192 ${entry.kind}/${entry.name}`));
|
|
998
|
+
} else {
|
|
999
|
+
const lines = elements.filter((e) => e.tag === tag).map((e) => e.line);
|
|
1000
|
+
results.push(fail2(
|
|
1001
|
+
`<${tag}>`,
|
|
1002
|
+
`unknown element (lines: ${lines.join(", ")})`
|
|
1003
|
+
));
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
heading("Dependencies");
|
|
1007
|
+
const matchedCount = uniqueTags.size - deps.unmatchedTags.length;
|
|
1008
|
+
console.log(` ${deps.cssFiles.length} CSS files needed`);
|
|
1009
|
+
console.log(` ${deps.jsFiles.length} JS files needed`);
|
|
1010
|
+
for (const f of [...deps.cssFiles, ...deps.jsFiles]) {
|
|
1011
|
+
if (!existsSync7(f)) {
|
|
1012
|
+
results.push(fail2(`dependency: ${f}`, "file not found"));
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
heading("Attributes");
|
|
1016
|
+
const attrResults = checkAttributes(registry, elements);
|
|
1017
|
+
results.push(...attrResults);
|
|
1018
|
+
if (attrResults.length === 0) {
|
|
1019
|
+
console.log(" No attribute issues found");
|
|
1020
|
+
}
|
|
1021
|
+
heading("Structure");
|
|
1022
|
+
for (const tag of uniqueTags) {
|
|
1023
|
+
const opens = (markup.match(new RegExp(`<${tag}[\\s>]`, "g")) || []).length;
|
|
1024
|
+
const closes = (markup.match(new RegExp(`</${tag}>`, "g")) || []).length;
|
|
1025
|
+
const selfClosing = (markup.match(new RegExp(`<${tag}[^>]*/\\s*>`, "g")) || []).length;
|
|
1026
|
+
const expectedCloses = opens - selfClosing;
|
|
1027
|
+
if (expectedCloses > 0 && closes < expectedCloses) {
|
|
1028
|
+
results.push(fail2(
|
|
1029
|
+
`<${tag}>`,
|
|
1030
|
+
`${opens} opens, ${closes} closes (${expectedCloses - closes} unclosed)`
|
|
1031
|
+
));
|
|
1032
|
+
} else {
|
|
1033
|
+
results.push(pass2(`<${tag}>: balanced`));
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const passed = results.filter((r) => r.passed).length;
|
|
1037
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
1038
|
+
if (failed > 0) {
|
|
1039
|
+
heading("Failures");
|
|
1040
|
+
for (const r of results) {
|
|
1041
|
+
if (!r.passed) {
|
|
1042
|
+
console.log(` FAIL ${r.label} \u2014 ${r.detail}`);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
console.log();
|
|
1047
|
+
console.log(`${passed} passed, ${failed} failed`);
|
|
1048
|
+
return failed > 0 ? 1 : 0;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1189
1051
|
// src/cli.ts
|
|
1190
1052
|
function showHelp() {
|
|
1191
1053
|
console.log(`
|
|
@@ -1203,6 +1065,9 @@ Usage:
|
|
|
1203
1065
|
design-system <system> references <file> Display reference file
|
|
1204
1066
|
design-system <system> render <file> Render markup with resolved deps
|
|
1205
1067
|
design-system <system> render - Render from stdin
|
|
1068
|
+
design-system <system> health Check design system consistency
|
|
1069
|
+
design-system <system> check <file> Validate markup against design system
|
|
1070
|
+
design-system <system> check - Validate from stdin
|
|
1206
1071
|
|
|
1207
1072
|
Flags:
|
|
1208
1073
|
--html Dump raw HTML to stdout
|
|
@@ -1245,6 +1110,12 @@ async function main() {
|
|
|
1245
1110
|
if (rest[0] === "render") {
|
|
1246
1111
|
process.exit(await commandRender(first, rest.slice(1).join(" "), { html: values.html }));
|
|
1247
1112
|
}
|
|
1113
|
+
if (rest[0] === "health") {
|
|
1114
|
+
process.exit(commandHealth(first));
|
|
1115
|
+
}
|
|
1116
|
+
if (rest[0] === "check") {
|
|
1117
|
+
process.exit(await commandCheck(first, rest.slice(1).join(" ")));
|
|
1118
|
+
}
|
|
1248
1119
|
const flags = {
|
|
1249
1120
|
html: values.html
|
|
1250
1121
|
};
|