@sourcescape/ds-cli 0.3.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.
Files changed (2) hide show
  1. package/dist/cli.js +323 -3
  2. package/package.json +5 -3
package/dist/cli.js CHANGED
@@ -696,14 +696,14 @@ async function commandRender(system, fileArg, flags) {
696
696
  console.log(`Preview server running at ${server.url}`);
697
697
  console.log(`Opening in browser... (Ctrl+C to stop)`);
698
698
  openInBrowser(server.url);
699
- await new Promise((resolve2) => {
699
+ await new Promise((resolve3) => {
700
700
  process.on("SIGINT", () => {
701
701
  server.stop();
702
- resolve2();
702
+ resolve3();
703
703
  });
704
704
  process.on("SIGTERM", () => {
705
705
  server.stop();
706
- resolve2();
706
+ resolve3();
707
707
  });
708
708
  });
709
709
  return 0;
@@ -737,6 +737,317 @@ function openInBrowser(target) {
737
737
  exec(`${cmd} "${target}"`);
738
738
  }
739
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
+
740
1051
  // src/cli.ts
741
1052
  function showHelp() {
742
1053
  console.log(`
@@ -754,6 +1065,9 @@ Usage:
754
1065
  design-system <system> references <file> Display reference file
755
1066
  design-system <system> render <file> Render markup with resolved deps
756
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
757
1071
 
758
1072
  Flags:
759
1073
  --html Dump raw HTML to stdout
@@ -796,6 +1110,12 @@ async function main() {
796
1110
  if (rest[0] === "render") {
797
1111
  process.exit(await commandRender(first, rest.slice(1).join(" "), { html: values.html }));
798
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
+ }
799
1119
  const flags = {
800
1120
  html: values.html
801
1121
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sourcescape/ds-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "CLI for browsing design system definitions",
6
6
  "license": "MIT",
@@ -11,9 +11,11 @@
11
11
  }
12
12
  },
13
13
  "bin": {
14
- "design-system": "./dist/cli.js"
14
+ "design-system": "dist/cli.js"
15
15
  },
16
- "files": ["dist"],
16
+ "files": [
17
+ "dist"
18
+ ],
17
19
  "scripts": {
18
20
  "build": "tsup src/cli.ts src/index.ts --format esm --dts --clean",
19
21
  "prepublishOnly": "npm run build"