@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/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 as existsSync2, readFileSync as readFileSync2 } from "fs";
240
- import { join as join2 } from "path";
77
+ import { existsSync, readFileSync } from "fs";
78
+ import { join } from "path";
241
79
  function loadManifest(system) {
242
- const path = join2(systemDir(system), "references", "manifest.json");
243
- if (!existsSync2(path)) return [];
244
- const data = JSON.parse(readFileSync2(path, "utf-8"));
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 join4, basename, relative } from "path";
296
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2 } from "fs";
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 readFileSync3, writeFileSync, existsSync as existsSync3 } from "fs";
424
- import { join as join3 } from "path";
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 join3(systemDir(system), INDEX_FILE);
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 (!existsSync3(path)) return null;
312
+ if (!existsSync2(path)) return null;
475
313
  try {
476
- const data = JSON.parse(readFileSync3(path, "utf-8"));
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 = readdirSync2(found.dir);
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 = join4(found.dir, "description.md");
654
- if (existsSync4(descPath)) {
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(readFileSync4(cssPath, "utf-8").trimEnd());
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(readFileSync4(jsPath, "utf-8").trimEnd());
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 existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "fs";
750
- import { join as join5, extname } from "path";
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 join5(systemDir(system), "references");
590
+ return join4(systemDir(system), "references");
753
591
  }
754
592
  function loadManifest2(system) {
755
- const path = join5(referencesDir(system), "manifest.json");
756
- if (!existsSync5(path)) return null;
757
- return JSON.parse(readFileSync5(path, "utf-8"));
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 (!existsSync5(dir)) return [];
762
- return readdirSync3(dir).filter((f) => f !== "manifest.json").sort();
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 (!existsSync5(dir)) {
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 = join5(dir, fileName);
797
- if (!existsSync5(filePath)) {
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 = readFileSync5(filePath, "utf-8");
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 readFileSync8, existsSync as existsSync8 } from "fs";
812
- import { resolve as resolve2 } from "path";
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 = resolve2(fileArg);
1126
- if (!existsSync8(filePath)) {
676
+ const filePath = resolve(fileArg);
677
+ if (!existsSync5(filePath)) {
1127
678
  error(`File not found: ${filePath}`);
1128
679
  return 1;
1129
680
  }
1130
- markup = readFileSync8(filePath, "utf-8");
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
  };