@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.
- package/dist/cli.js +323 -3
- 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((
|
|
699
|
+
await new Promise((resolve3) => {
|
|
700
700
|
process.on("SIGINT", () => {
|
|
701
701
|
server.stop();
|
|
702
|
-
|
|
702
|
+
resolve3();
|
|
703
703
|
});
|
|
704
704
|
process.on("SIGTERM", () => {
|
|
705
705
|
server.stop();
|
|
706
|
-
|
|
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
|
+
"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": "
|
|
14
|
+
"design-system": "dist/cli.js"
|
|
15
15
|
},
|
|
16
|
-
"files": [
|
|
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"
|