onsystem 0.5.0 → 0.6.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/README.md +21 -4
- package/dist/cli.js +302 -33
- package/dist/{types-DJtccu3A.d.ts → config-lbfW91qB.d.ts} +263 -167
- package/dist/{design-system-CnEv5sIL.js → design-system-A3pRX3Ka.js} +2861 -414
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +9 -2
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +3 -3
- package/dist/{stdio-DjfkiDMV.js → stdio-CEChNFLR.js} +21 -24
- package/dist/{workspace-DzWQ4L8R.js → workspace-CHvUwgz9.js} +44 -10
- package/package.json +3 -2
- package/schema.json +19 -1
|
@@ -4,11 +4,12 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import * as z from "zod";
|
|
5
5
|
import { escapePath, glob, globSync } from "tinyglobby";
|
|
6
6
|
import ts from "typescript";
|
|
7
|
-
import { clampChroma, colorsNamed, converter, differenceEuclidean, parse } from "culori";
|
|
7
|
+
import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse } from "culori";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import
|
|
9
|
+
import fsp from "node:fs/promises";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import postcss from "postcss";
|
|
12
|
+
import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
|
|
12
13
|
import { stemmer } from "stemmer";
|
|
13
14
|
//#region src/extract/program.ts
|
|
14
15
|
const DEFAULT_OPTIONS = {
|
|
@@ -32,7 +33,8 @@ function readProjectConfig(root, tsconfig) {
|
|
|
32
33
|
options: { ...DEFAULT_OPTIONS },
|
|
33
34
|
pathsBase: root,
|
|
34
35
|
paths: {},
|
|
35
|
-
configFiles: []
|
|
36
|
+
configFiles: [],
|
|
37
|
+
fileNames: []
|
|
36
38
|
};
|
|
37
39
|
const configFiles = [configFile];
|
|
38
40
|
const read = ts.readConfigFile(configFile, (file) => ts.sys.readFile(file));
|
|
@@ -41,7 +43,8 @@ function readProjectConfig(root, tsconfig) {
|
|
|
41
43
|
pathsBase: root,
|
|
42
44
|
paths: {},
|
|
43
45
|
configFile,
|
|
44
|
-
configFiles
|
|
46
|
+
configFiles,
|
|
47
|
+
fileNames: []
|
|
45
48
|
};
|
|
46
49
|
const host = {
|
|
47
50
|
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
|
|
@@ -69,7 +72,8 @@ function readProjectConfig(root, tsconfig) {
|
|
|
69
72
|
pathsBase: options.baseUrl ?? (typeof options.pathsBasePath === "string" ? options.pathsBasePath : path.dirname(configFile)),
|
|
70
73
|
paths: options.paths ?? {},
|
|
71
74
|
configFile,
|
|
72
|
-
configFiles
|
|
75
|
+
configFiles,
|
|
76
|
+
fileNames: parsed.fileNames
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
function createProgram(files, config, oldProgram) {
|
|
@@ -166,7 +170,7 @@ function globSource(pattern) {
|
|
|
166
170
|
out += "\\{";
|
|
167
171
|
continue;
|
|
168
172
|
}
|
|
169
|
-
const options = splitTopLevel$
|
|
173
|
+
const options = splitTopLevel$2(pattern.slice(i + 1, end));
|
|
170
174
|
out += `(?:${options.map(globSource).join("|")})`;
|
|
171
175
|
i = end;
|
|
172
176
|
} else out += escapeRegExp(c);
|
|
@@ -180,7 +184,7 @@ function closingBrace(pattern, open) {
|
|
|
180
184
|
else if (pattern[i] === "}" && --depth === 0) return i;
|
|
181
185
|
return -1;
|
|
182
186
|
}
|
|
183
|
-
function splitTopLevel$
|
|
187
|
+
function splitTopLevel$2(text) {
|
|
184
188
|
const parts = [];
|
|
185
189
|
let depth = 0;
|
|
186
190
|
let current = "";
|
|
@@ -235,13 +239,17 @@ const SOURCE_EXTENSIONS = [
|
|
|
235
239
|
const BUILD_DIR = /^(?:dist|build|lib|out|esm|cjs|es|module|types)\//;
|
|
236
240
|
/** `index.mjs`, `index.d.ts`, `button.cjs`: an output file, by its extension. */
|
|
237
241
|
const OUTPUT_EXTENSION = /(?:\.d)?\.[cm]?[jt]sx?$/;
|
|
238
|
-
/**
|
|
239
|
-
|
|
242
|
+
/**
|
|
243
|
+
* `base` as a file, with an extension added, or as a directory's index, never in an installed
|
|
244
|
+
* package: in `node_modules`, or with `within` (a package's own folder, which may be installed
|
|
245
|
+
* itself), in a `node_modules` below it.
|
|
246
|
+
*/
|
|
247
|
+
function moduleFile(base, extensions = MODULE_EXTENSIONS, within) {
|
|
240
248
|
return [
|
|
241
249
|
base,
|
|
242
250
|
...extensions.map((ext) => `${base}${ext}`),
|
|
243
251
|
...extensions.map((ext) => path.join(base, `index${ext}`))
|
|
244
|
-
].find((file) => isFile$1(file) && !/[\\/]node_modules[\\/]/.test(file));
|
|
252
|
+
].find((file) => isFile$1(file) && !/(?:^|[\\/])node_modules[\\/]/.test(within ? path.relative(within, file) : file));
|
|
245
253
|
}
|
|
246
254
|
const BUILD_CONFIGS = [
|
|
247
255
|
"tsup",
|
|
@@ -361,7 +369,7 @@ function sourceTarget(dir, key, target, entries) {
|
|
|
361
369
|
];
|
|
362
370
|
for (const candidate of candidates) {
|
|
363
371
|
if (candidate === void 0) continue;
|
|
364
|
-
const file = moduleFile(path.join(dir, candidate), SOURCE_EXTENSIONS);
|
|
372
|
+
const file = moduleFile(path.join(dir, candidate), SOURCE_EXTENSIONS, dir);
|
|
365
373
|
if (file && isInside(dir, file)) return toPosix(path.relative(dir, file));
|
|
366
374
|
}
|
|
367
375
|
}
|
|
@@ -389,7 +397,7 @@ function exportedFiles(file, packageDir) {
|
|
|
389
397
|
if (!barrel) found.push(current);
|
|
390
398
|
for (const specifier of from) {
|
|
391
399
|
if (!specifier.startsWith(".")) continue;
|
|
392
|
-
const next = moduleFile(path.resolve(path.dirname(current), specifier));
|
|
400
|
+
const next = moduleFile(path.resolve(path.dirname(current), specifier), void 0, packageDir);
|
|
393
401
|
if (next && isInside(packageDir, next)) visit(next, depth + 1);
|
|
394
402
|
}
|
|
395
403
|
};
|
|
@@ -533,16 +541,19 @@ const STYLESHEETS = [
|
|
|
533
541
|
* With more than one design system, the one the root's code imports most comes first
|
|
534
542
|
* (`designSystems`). A candidate whose globs match no file is skipped. Returns undefined
|
|
535
543
|
* when nothing applies, so the shadcn defaults stay in force.
|
|
544
|
+
*
|
|
545
|
+
* `declared` is the dependency whose config the root takes on ({@link findDesignSystemPackage}):
|
|
546
|
+
* it is read first among the dependencies in step 3, whatever its name, and also when it is
|
|
547
|
+
* installed from a registry rather than a workspace package.
|
|
536
548
|
*/
|
|
537
|
-
function detectProject(root, tsconfig, trace) {
|
|
549
|
+
function detectProject(root, tsconfig, trace, declared, packages = new PackageFinder(root)) {
|
|
538
550
|
const step = (candidate, accepted, reason) => trace?.push({
|
|
539
551
|
candidate,
|
|
540
552
|
accepted,
|
|
541
553
|
reason
|
|
542
554
|
});
|
|
543
|
-
const packages = new PackageFinder(root);
|
|
544
555
|
const found = [];
|
|
545
|
-
const shadcn = readJson(path.join(root, "components.json"));
|
|
556
|
+
const shadcn = readJson$1(path.join(root, "components.json"));
|
|
546
557
|
let why = "no ui or components alias";
|
|
547
558
|
const fromShadcn = shadcn ? fromComponentsJson(root, root, shadcn, readProjectConfig(root, tsconfig), packages, (r) => {
|
|
548
559
|
why = r;
|
|
@@ -558,7 +569,7 @@ function detectProject(root, tsconfig, trace) {
|
|
|
558
569
|
step("components.json", true, `ui: ${fromShadcn.source}, ${countFiles(root, fromShadcn)}`);
|
|
559
570
|
}
|
|
560
571
|
const withDefaults = !found.length;
|
|
561
|
-
const own = readJson(path.join(root, "package.json"));
|
|
572
|
+
const own = readJson$1(path.join(root, "package.json"));
|
|
562
573
|
const ownName = typeof own?.name === "string" ? own.name : "the root";
|
|
563
574
|
if (!found.length) {
|
|
564
575
|
let reason = "no package.json";
|
|
@@ -575,17 +586,19 @@ function detectProject(root, tsconfig, trace) {
|
|
|
575
586
|
...asRecord(own?.dependencies),
|
|
576
587
|
...asRecord(own?.devDependencies)
|
|
577
588
|
});
|
|
578
|
-
const names = dependencies.filter(isDesignSystemName);
|
|
589
|
+
const names = unique([...declared ? [declared.name] : [], ...dependencies.filter(isDesignSystemName)]);
|
|
579
590
|
if (!names.length) step("workspace design-system packages", false, `no dependency is named like one (@acme/ui, acme-ui, @acme/design-system) among ${plural(dependencies.length, "dependency", "dependencies")}`);
|
|
580
591
|
let imported;
|
|
581
592
|
for (const name of names) {
|
|
582
|
-
const
|
|
593
|
+
const own = name === declared?.name ? declared : void 0;
|
|
594
|
+
const kind = own?.installed ? "installed" : "workspace";
|
|
595
|
+
const candidate = `${kind} package ${name}`;
|
|
583
596
|
if (found.some((f) => f.package === name)) {
|
|
584
597
|
step(candidate, false, "not added twice: components.json points into it");
|
|
585
598
|
continue;
|
|
586
599
|
}
|
|
587
|
-
const dir = packages.find(name);
|
|
588
|
-
const pkg = dir ? readJson(path.join(dir, "package.json")) : void 0;
|
|
600
|
+
const dir = own ? own.dir : packages.find(name);
|
|
601
|
+
const pkg = dir ? readJson$1(path.join(dir, "package.json")) : void 0;
|
|
589
602
|
if (!dir || !pkg) {
|
|
590
603
|
step(candidate, false, "not a workspace package: an installed package is compiled, and only sources are read");
|
|
591
604
|
continue;
|
|
@@ -596,7 +609,7 @@ function detectProject(root, tsconfig, trace) {
|
|
|
596
609
|
if (result && matchesAny(root, result)) {
|
|
597
610
|
found.push({
|
|
598
611
|
...result,
|
|
599
|
-
source:
|
|
612
|
+
source: `${kind} package ${result.source}`,
|
|
600
613
|
package: name,
|
|
601
614
|
prefixes: [name]
|
|
602
615
|
});
|
|
@@ -638,12 +651,23 @@ function detectProject(root, tsconfig, trace) {
|
|
|
638
651
|
}
|
|
639
652
|
/** `12 component files`, for the trace. */
|
|
640
653
|
function countFiles(root, found) {
|
|
641
|
-
|
|
654
|
+
return plural(componentGlob(root, found.components).length, "component file");
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* The files `patterns` match, outside `node_modules` unless a pattern points into it: the
|
|
658
|
+
* sources of a design-system package installed there.
|
|
659
|
+
*/
|
|
660
|
+
function componentGlob(root, patterns) {
|
|
661
|
+
const named = patterns.filter((p) => p.includes("node_modules"));
|
|
662
|
+
const rest = patterns.filter((p) => !p.includes("node_modules"));
|
|
663
|
+
const options = {
|
|
642
664
|
cwd: root,
|
|
643
|
-
ignore: ["**/node_modules/**"],
|
|
644
665
|
expandDirectories: false
|
|
645
|
-
}
|
|
646
|
-
return
|
|
666
|
+
};
|
|
667
|
+
return [...rest.length ? globSync(rest, {
|
|
668
|
+
...options,
|
|
669
|
+
ignore: ["**/node_modules/**"]
|
|
670
|
+
}) : [], ...named.length ? globSync(named, options) : []];
|
|
647
671
|
}
|
|
648
672
|
function plural(n, one, many = `${one}s`) {
|
|
649
673
|
return `${n.toLocaleString("en-US")} ${n === 1 ? one : many}`;
|
|
@@ -737,11 +761,7 @@ function detection(root, found, withDefaults, source) {
|
|
|
737
761
|
};
|
|
738
762
|
}
|
|
739
763
|
function matchesAny(root, found) {
|
|
740
|
-
return
|
|
741
|
-
cwd: root,
|
|
742
|
-
ignore: ["**/node_modules/**"],
|
|
743
|
-
expandDirectories: false
|
|
744
|
-
}).length > 0;
|
|
764
|
+
return componentGlob(root, found.components).length > 0;
|
|
745
765
|
}
|
|
746
766
|
/** `components.json` read from `dir` (the root, or a workspace package's directory). */
|
|
747
767
|
function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
|
|
@@ -756,7 +776,7 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
|
|
|
756
776
|
if (!uiDir) {
|
|
757
777
|
const name = packageName(ui);
|
|
758
778
|
const pkgDir = name ? packages.find(name) : void 0;
|
|
759
|
-
const pkg = pkgDir ? readJson(path.join(pkgDir, "package.json")) : void 0;
|
|
779
|
+
const pkg = pkgDir ? readJson$1(path.join(pkgDir, "package.json")) : void 0;
|
|
760
780
|
if (!name || !pkgDir || !pkg) {
|
|
761
781
|
note(`the ui alias ${ui} resolves through neither tsconfig paths nor a workspace package`);
|
|
762
782
|
return;
|
|
@@ -786,7 +806,7 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
|
|
|
786
806
|
*/
|
|
787
807
|
function ownerPackage(root, dir, alias) {
|
|
788
808
|
for (let current = dir;; current = path.dirname(current)) {
|
|
789
|
-
const name = readJson(path.join(current, "package.json"))?.name;
|
|
809
|
+
const name = readJson$1(path.join(current, "package.json"))?.name;
|
|
790
810
|
if (typeof name === "string") return path.resolve(current) === path.resolve(root) ? { prefixes: [alias] } : {
|
|
791
811
|
package: name,
|
|
792
812
|
prefixes: [alias, name]
|
|
@@ -800,7 +820,7 @@ function ownerPackage(root, dir, alias) {
|
|
|
800
820
|
* the package name, not through the package's internal `@/` alias.
|
|
801
821
|
*/
|
|
802
822
|
function fromPackageComponentsJson(root, dir, pkg, packages, note = ignore) {
|
|
803
|
-
const json = readJson(path.join(dir, "components.json"));
|
|
823
|
+
const json = readJson$1(path.join(dir, "components.json"));
|
|
804
824
|
if (!json) return void 0;
|
|
805
825
|
const found = fromComponentsJson(root, dir, json, readProjectConfig(dir), packages, (r) => {
|
|
806
826
|
note(`its components.json: ${r}`);
|
|
@@ -876,7 +896,7 @@ function fromImports(root, dir, name, subpaths, note = ignore) {
|
|
|
876
896
|
const stylesheets = [];
|
|
877
897
|
const seen = /* @__PURE__ */ new Set();
|
|
878
898
|
for (const subpath of [...subpaths ?? []].sort()) {
|
|
879
|
-
const file = moduleFile(path.join(dir, subpath));
|
|
899
|
+
const file = moduleFile(path.join(dir, subpath), void 0, dir);
|
|
880
900
|
if (!file || seen.has(file)) continue;
|
|
881
901
|
seen.add(file);
|
|
882
902
|
const specifier = `${name}/${subpath}`;
|
|
@@ -1065,7 +1085,7 @@ function moduleResolver(root) {
|
|
|
1065
1085
|
if (!name || !dir) return void 0;
|
|
1066
1086
|
const subpath = specifier.slice(name.length + 1);
|
|
1067
1087
|
if (subpath) return moduleFile(path.join(dir, subpath));
|
|
1068
|
-
const main = readJson(path.join(dir, "package.json"))?.main;
|
|
1088
|
+
const main = readJson$1(path.join(dir, "package.json"))?.main;
|
|
1069
1089
|
return moduleFile(path.resolve(dir, typeof main === "string" ? main : "index"));
|
|
1070
1090
|
};
|
|
1071
1091
|
}
|
|
@@ -1086,7 +1106,7 @@ function workspaceModule(root) {
|
|
|
1086
1106
|
if (!name || !dir) return void 0;
|
|
1087
1107
|
const source = moduleFile(path.join(dir, "src", specifier.slice(name.length + 1) || "index"));
|
|
1088
1108
|
if (source) return { file: source };
|
|
1089
|
-
const exports = readJson(path.join(dir, "package.json"))?.exports;
|
|
1109
|
+
const exports = readJson$1(path.join(dir, "package.json"))?.exports;
|
|
1090
1110
|
if (!exports || typeof exports !== "object" || Array.isArray(exports)) return void 0;
|
|
1091
1111
|
const subpath = `.${specifier.slice(name.length)}`;
|
|
1092
1112
|
return Object.keys(exports).some((key) => {
|
|
@@ -1096,18 +1116,130 @@ function workspaceModule(root) {
|
|
|
1096
1116
|
}) ? {} : void 0;
|
|
1097
1117
|
};
|
|
1098
1118
|
}
|
|
1119
|
+
/** The package.json field a design-system package names its onsystem config with. */
|
|
1120
|
+
const PACKAGE_FIELD = "onsystem";
|
|
1121
|
+
/**
|
|
1122
|
+
* The config the package.json in `dir` declares: its `onsystem` field, a JSON file in the
|
|
1123
|
+
* package (`"./onsystem.config.json"`). Undefined without the field; a problem when the field
|
|
1124
|
+
* names something that cannot be read. Only JSON: a config in code would run a dependency's code.
|
|
1125
|
+
*/
|
|
1126
|
+
function declaredConfig(dir, pkg = readJson$1(path.join(dir, "package.json"))) {
|
|
1127
|
+
const value = pkg?.[PACKAGE_FIELD];
|
|
1128
|
+
if (value === void 0) return void 0;
|
|
1129
|
+
const field = `"${PACKAGE_FIELD}" in ${path.join(dir, "package.json")}`;
|
|
1130
|
+
if (typeof value !== "string" || !value.endsWith(".json")) return { problem: `${field} is ${JSON.stringify(value)}: it names a JSON config file in the package, such as "./onsystem.config.json".` };
|
|
1131
|
+
const file = path.resolve(dir, value);
|
|
1132
|
+
if (!isInside(dir, file) || file === path.resolve(dir)) return { problem: `${field} names ${value}, which is outside the package.` };
|
|
1133
|
+
if (!isFile(file)) return { problem: `${field} names ${value}, which does not exist.` };
|
|
1134
|
+
return { file };
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* The dependency of `root` (in `dependencies` or `devDependencies`) whose package.json declares
|
|
1138
|
+
* its onsystem config, as a workspace package or installed from a registry: the design system
|
|
1139
|
+
* the root uses, set up in its own package. With several, the one the root's code imports
|
|
1140
|
+
* most, then the first listed; `others` names the rest. A declaration that cannot be read is a
|
|
1141
|
+
* problem in a workspace package, which its team owns, and is skipped in an installed one, so a
|
|
1142
|
+
* published package cannot stop the apps that use it. Undefined at a workspace root, whose
|
|
1143
|
+
* projects each find their own, and without a package.json.
|
|
1144
|
+
*
|
|
1145
|
+
* Only the root's own dependencies are looked at, each where Node.js resolves it
|
|
1146
|
+
* ({@link PackageFinder.locate}). `packages` is the load's finder, shared with detection.
|
|
1147
|
+
*/
|
|
1148
|
+
function findDesignSystemPackage(root, packages = new PackageFinder(root)) {
|
|
1149
|
+
const own = readJson$1(path.join(root, "package.json"));
|
|
1150
|
+
if (!own || isWorkspaceRoot(root)) return void 0;
|
|
1151
|
+
const names = Object.keys({
|
|
1152
|
+
...asRecord(own.dependencies),
|
|
1153
|
+
...asRecord(own.devDependencies)
|
|
1154
|
+
});
|
|
1155
|
+
const declaring = [];
|
|
1156
|
+
const problems = [];
|
|
1157
|
+
const skipped = [];
|
|
1158
|
+
for (const name of names) {
|
|
1159
|
+
const located = packages.locate(name);
|
|
1160
|
+
if (located?.json["onsystem"] === void 0) continue;
|
|
1161
|
+
const resolved = packages.place(located.dir);
|
|
1162
|
+
const declared = declaredConfig(resolved.dir, located.json);
|
|
1163
|
+
if (!declared) continue;
|
|
1164
|
+
if ("problem" in declared) (resolved.installed ? skipped : problems).push({
|
|
1165
|
+
name,
|
|
1166
|
+
problem: declared.problem
|
|
1167
|
+
});
|
|
1168
|
+
else declaring.push({
|
|
1169
|
+
name,
|
|
1170
|
+
dir: resolved.dir,
|
|
1171
|
+
config: declared.file,
|
|
1172
|
+
installed: resolved.installed,
|
|
1173
|
+
boundary: resolved.boundary
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
let ranked = declaring;
|
|
1177
|
+
if (declaring.length > 1) {
|
|
1178
|
+
const counts = importCounts(root, declaring.map((p) => [p.name]));
|
|
1179
|
+
ranked = declaring.map((p, i) => ({
|
|
1180
|
+
p,
|
|
1181
|
+
imports: counts[i] ?? 0,
|
|
1182
|
+
i
|
|
1183
|
+
})).sort((a, b) => b.imports - a.imports || a.i - b.i).map((r) => r.p);
|
|
1184
|
+
}
|
|
1185
|
+
const [found, ...others] = ranked;
|
|
1186
|
+
return {
|
|
1187
|
+
found,
|
|
1188
|
+
others: others.map((p) => p.name),
|
|
1189
|
+
problems,
|
|
1190
|
+
skipped,
|
|
1191
|
+
dependencies: names.length
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Import specifiers for the component files a package's `exports` lead to, through barrels and
|
|
1196
|
+
* from `dist/` back to the sources: what apps import its components by.
|
|
1197
|
+
*/
|
|
1198
|
+
function packageImports(root, dir) {
|
|
1199
|
+
const pkg = readJson$1(path.join(dir, "package.json"));
|
|
1200
|
+
return pkg ? fromPackage(root, dir, pkg)?.imports ?? [] : [];
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Dependencies' package.json files, parsed, by path: read again only when one's size or mtime
|
|
1204
|
+
* changes, so a config loaded again (the ESLint plugin's and the MCP server's reloads) does not
|
|
1205
|
+
* read every dependency again.
|
|
1206
|
+
*/
|
|
1207
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
1208
|
+
function cachedManifest(file) {
|
|
1209
|
+
const stamp = fileStamp(file);
|
|
1210
|
+
const cached = manifests.get(file);
|
|
1211
|
+
if (cached?.stamp === stamp) return cached.json;
|
|
1212
|
+
const json = readJson$1(file);
|
|
1213
|
+
manifests.set(file, {
|
|
1214
|
+
stamp,
|
|
1215
|
+
json
|
|
1216
|
+
});
|
|
1217
|
+
return json;
|
|
1218
|
+
}
|
|
1219
|
+
/** A file's or folder's size and mtime, or `-` when it is missing. */
|
|
1220
|
+
function fileStamp(file) {
|
|
1221
|
+
const stats = fs.statSync(file, { throwIfNoEntry: false });
|
|
1222
|
+
return stats ? `${String(stats.mtimeMs)}:${String(stats.size)}` : "-";
|
|
1223
|
+
}
|
|
1099
1224
|
/** `@acme/ui`, `@acme/ui-kit`, `@acme/design-system`, `acme-ui`; not `@acme/email-components`. */
|
|
1100
1225
|
function isDesignSystemName(name) {
|
|
1101
1226
|
const base = name.slice(name.lastIndexOf("/") + 1);
|
|
1102
1227
|
return /^(?:ui|ui-kit|uikit|ui-react|react-ui|design-system|ds|components|primitives)$/.test(base) || /-(?:ui|ui-kit|design-system)$/.test(base);
|
|
1103
1228
|
}
|
|
1104
1229
|
/**
|
|
1105
|
-
* Locates
|
|
1106
|
-
*
|
|
1230
|
+
* Locates packages by name for one load: a project's config, its design-system package and
|
|
1231
|
+
* its detection share one. Workspace packages are found through `node_modules` links up to the
|
|
1232
|
+
* workspace root, then by the workspace's package globs ({@link find}); any dependency as
|
|
1233
|
+
* Node.js resolves it ({@link locate}).
|
|
1107
1234
|
*/
|
|
1108
1235
|
var PackageFinder = class {
|
|
1109
1236
|
root;
|
|
1110
1237
|
byName;
|
|
1238
|
+
/** The packages the package manager links: those its workspace globs list, without Nx's. */
|
|
1239
|
+
linkable;
|
|
1240
|
+
nodeModules;
|
|
1241
|
+
realRoot;
|
|
1242
|
+
workspace;
|
|
1111
1243
|
constructor(root) {
|
|
1112
1244
|
this.root = root;
|
|
1113
1245
|
}
|
|
@@ -1123,6 +1255,61 @@ var PackageFinder = class {
|
|
|
1123
1255
|
this.byName ??= scanWorkspace(this.root);
|
|
1124
1256
|
return this.byName.get(name);
|
|
1125
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* Where `name` resolves for code in the root, as Node.js finds it: in the nearest
|
|
1260
|
+
* `node_modules` at or above it that has it; else the workspace package of that name, in a
|
|
1261
|
+
* checkout that is not installed (only then is the workspace scanned). With its package.json.
|
|
1262
|
+
*/
|
|
1263
|
+
locate(name) {
|
|
1264
|
+
for (const nodeModules of this.nodeModulesDirs()) {
|
|
1265
|
+
const dir = path.join(nodeModules, ...name.split("/"));
|
|
1266
|
+
const json = cachedManifest(path.join(dir, "package.json"));
|
|
1267
|
+
if (json) return {
|
|
1268
|
+
dir,
|
|
1269
|
+
json
|
|
1270
|
+
};
|
|
1271
|
+
}
|
|
1272
|
+
this.linkable ??= scanWorkspace(this.root, false);
|
|
1273
|
+
const dir = this.linkable.get(name);
|
|
1274
|
+
const json = dir ? cachedManifest(path.join(dir, "package.json")) : void 0;
|
|
1275
|
+
return dir && json ? {
|
|
1276
|
+
dir,
|
|
1277
|
+
json
|
|
1278
|
+
} : void 0;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* A located package at its real folder, expressed from the root as given: `installed` when
|
|
1282
|
+
* that is in a `node_modules`, rather than a workspace package linked there, and the
|
|
1283
|
+
* `boundary` its config's paths must stay in.
|
|
1284
|
+
*/
|
|
1285
|
+
place(located) {
|
|
1286
|
+
this.realRoot ??= fs.realpathSync(this.root);
|
|
1287
|
+
const real = path.relative(this.realRoot, fs.realpathSync(located));
|
|
1288
|
+
const dir = path.resolve(this.root, real);
|
|
1289
|
+
const installed = /(?:^|[\\/])node_modules[\\/]/.test(real);
|
|
1290
|
+
return {
|
|
1291
|
+
dir,
|
|
1292
|
+
installed,
|
|
1293
|
+
boundary: installed ? dir : this.boundaryOf(dir)
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
/** The `node_modules` folders at and above the root, nearest first, looked up once. */
|
|
1297
|
+
nodeModulesDirs() {
|
|
1298
|
+
if (!this.nodeModules) {
|
|
1299
|
+
this.nodeModules = [];
|
|
1300
|
+
for (let dir = path.resolve(this.root);; dir = path.dirname(dir)) {
|
|
1301
|
+
const candidate = path.join(dir, "node_modules");
|
|
1302
|
+
if (isDirectory$1(candidate)) this.nodeModules.push(candidate);
|
|
1303
|
+
if (dir === path.dirname(dir)) break;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return this.nodeModules;
|
|
1307
|
+
}
|
|
1308
|
+
/** The workspace root when `dir` is in it, else `dir` itself. */
|
|
1309
|
+
boundaryOf(dir) {
|
|
1310
|
+
if (this.workspace === void 0) this.workspace = workspaceRootOf(this.root) ?? null;
|
|
1311
|
+
return this.workspace !== null && isInside(this.workspace, dir) ? this.workspace : dir;
|
|
1312
|
+
}
|
|
1126
1313
|
};
|
|
1127
1314
|
/**
|
|
1128
1315
|
* A monorepo root: a pnpm-workspace.yaml, `workspaces` in package.json (npm, Yarn, Bun),
|
|
@@ -1132,28 +1319,65 @@ var PackageFinder = class {
|
|
|
1132
1319
|
function isWorkspaceRoot(dir) {
|
|
1133
1320
|
if (fs.existsSync(path.join(dir, "pnpm-workspace.yaml"))) return true;
|
|
1134
1321
|
if (fs.existsSync(path.join(dir, "nx.json"))) return true;
|
|
1135
|
-
const pkg = readJson(path.join(dir, "package.json"));
|
|
1322
|
+
const pkg = readJson$1(path.join(dir, "package.json"));
|
|
1136
1323
|
if (pkg !== void 0 && pkg.workspaces !== void 0) return true;
|
|
1137
|
-
return Array.isArray(readJson(path.join(dir, "lerna.json"))?.packages);
|
|
1324
|
+
return Array.isArray(readJson$1(path.join(dir, "lerna.json"))?.packages);
|
|
1138
1325
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
function
|
|
1141
|
-
const packages = /* @__PURE__ */ new Map();
|
|
1142
|
-
let workspaceRoot;
|
|
1326
|
+
/** The nearest workspace root at or above `start`. */
|
|
1327
|
+
function workspaceRootOf(start) {
|
|
1143
1328
|
for (let dir = start;; dir = path.dirname(dir)) {
|
|
1144
|
-
if (isWorkspaceRoot(dir))
|
|
1145
|
-
|
|
1146
|
-
break;
|
|
1147
|
-
}
|
|
1148
|
-
if (dir === path.dirname(dir)) break;
|
|
1329
|
+
if (isWorkspaceRoot(dir)) return dir;
|
|
1330
|
+
if (dir === path.dirname(dir)) return void 0;
|
|
1149
1331
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1332
|
+
}
|
|
1333
|
+
/** What a workspace root declares its packages in, and Nx's marker. */
|
|
1334
|
+
const WORKSPACE_MANIFESTS = [
|
|
1335
|
+
"pnpm-workspace.yaml",
|
|
1336
|
+
"package.json",
|
|
1337
|
+
"lerna.json",
|
|
1338
|
+
"nx.json"
|
|
1339
|
+
];
|
|
1340
|
+
/**
|
|
1341
|
+
* Workspace maps by workspace root (with Nx's projects or not), kept for the next load in the
|
|
1342
|
+
* same process (a monorepo root's projects, the ESLint plugin's and the MCP server's reloads)
|
|
1343
|
+
* while the files they were read from keep their size and mtime: the root's manifests, each
|
|
1344
|
+
* package's package.json, the folders the packages are in and the folders the package globs
|
|
1345
|
+
* start from. A package added to one of those folders, or a manifest edited, makes the next
|
|
1346
|
+
* load read the workspace again.
|
|
1347
|
+
*/
|
|
1348
|
+
const workspaceMaps = /* @__PURE__ */ new Map();
|
|
1349
|
+
/**
|
|
1350
|
+
* Package name → directory for every package the workspace declares, with Nx's projects
|
|
1351
|
+
* unless `nx` is false.
|
|
1352
|
+
*/
|
|
1353
|
+
function scanWorkspace(start, nx = true) {
|
|
1354
|
+
const workspaceRoot = workspaceRootOf(start);
|
|
1355
|
+
if (!workspaceRoot) return /* @__PURE__ */ new Map();
|
|
1356
|
+
const key = `${workspaceRoot}\0${String(nx)}`;
|
|
1357
|
+
const cached = workspaceMaps.get(key);
|
|
1358
|
+
if (cached && stampOf(cached.watched) === cached.stamp) return cached.packages;
|
|
1359
|
+
const before = [...WORKSPACE_MANIFESTS.map((name) => path.join(workspaceRoot, name)), ...workspacePatterns(workspaceRoot).include.map((p) => path.resolve(workspaceRoot, globBase(`${p}/package.json`)))];
|
|
1360
|
+
const stamps = new Map(before.map((file) => [file, fileStamp(file)]));
|
|
1361
|
+
const packages = /* @__PURE__ */ new Map();
|
|
1362
|
+
const dirs = workspacePackageDirs(workspaceRoot, nx);
|
|
1363
|
+
for (const dir of dirs) {
|
|
1364
|
+
const manifest = path.join(dir, "package.json");
|
|
1365
|
+
stamps.set(manifest, fileStamp(manifest));
|
|
1366
|
+
if (!stamps.has(path.dirname(dir))) stamps.set(path.dirname(dir), fileStamp(path.dirname(dir)));
|
|
1367
|
+
const name = readJson$1(manifest)?.name;
|
|
1153
1368
|
if (typeof name === "string" && !packages.has(name)) packages.set(name, dir);
|
|
1154
1369
|
}
|
|
1370
|
+
const watched = [...stamps.keys()];
|
|
1371
|
+
workspaceMaps.set(key, {
|
|
1372
|
+
stamp: watched.map((file) => stamps.get(file)).join("|"),
|
|
1373
|
+
watched,
|
|
1374
|
+
packages
|
|
1375
|
+
});
|
|
1155
1376
|
return packages;
|
|
1156
1377
|
}
|
|
1378
|
+
function stampOf(files) {
|
|
1379
|
+
return files.map(fileStamp).join("|");
|
|
1380
|
+
}
|
|
1157
1381
|
/**
|
|
1158
1382
|
* `packages` from pnpm-workspace.yaml, `workspaces` from package.json (npm, Yarn, Bun), or
|
|
1159
1383
|
* `packages` from lerna.json, in that order of precedence.
|
|
@@ -1166,10 +1390,10 @@ function workspacePatterns(dir) {
|
|
|
1166
1390
|
});
|
|
1167
1391
|
const yaml = path.join(dir, "pnpm-workspace.yaml");
|
|
1168
1392
|
if (fs.existsSync(yaml)) return split(pnpmPackages(fs.readFileSync(yaml, "utf8")), "pnpm-workspace.yaml");
|
|
1169
|
-
const workspaces = readJson(path.join(dir, "package.json"))?.workspaces;
|
|
1393
|
+
const workspaces = readJson$1(path.join(dir, "package.json"))?.workspaces;
|
|
1170
1394
|
const list = Array.isArray(workspaces) ? workspaces : asRecord(workspaces).packages;
|
|
1171
1395
|
if (Array.isArray(list)) return split(list.filter((p) => typeof p === "string"), "package.json workspaces");
|
|
1172
|
-
const lerna = readJson(path.join(dir, "lerna.json"))?.packages;
|
|
1396
|
+
const lerna = readJson$1(path.join(dir, "lerna.json"))?.packages;
|
|
1173
1397
|
if (Array.isArray(lerna)) return split(lerna.filter((p) => typeof p === "string"), "lerna.json");
|
|
1174
1398
|
return {
|
|
1175
1399
|
include: [],
|
|
@@ -1184,7 +1408,7 @@ const NOT_PACKAGES = ["**/node_modules/**", "**/.git/**"];
|
|
|
1184
1408
|
* package globs match that hold a package.json, and with an nx.json, every folder with an
|
|
1185
1409
|
* Nx project.json.
|
|
1186
1410
|
*/
|
|
1187
|
-
function workspacePackageDirs(root) {
|
|
1411
|
+
function workspacePackageDirs(root, nx = true) {
|
|
1188
1412
|
const { include, exclude } = workspacePatterns(root);
|
|
1189
1413
|
const ignore = [...NOT_PACKAGES, ...exclude];
|
|
1190
1414
|
const dirs = include.length ? globSync(include.map((p) => `${p}/package.json`), {
|
|
@@ -1192,7 +1416,7 @@ function workspacePackageDirs(root) {
|
|
|
1192
1416
|
absolute: true,
|
|
1193
1417
|
ignore
|
|
1194
1418
|
}).map((manifest) => path.dirname(manifest)) : [];
|
|
1195
|
-
if (fs.existsSync(path.join(root, "nx.json"))) {
|
|
1419
|
+
if (nx && fs.existsSync(path.join(root, "nx.json"))) {
|
|
1196
1420
|
const projects = globSync("**/project.json", {
|
|
1197
1421
|
cwd: root,
|
|
1198
1422
|
absolute: true,
|
|
@@ -1229,7 +1453,7 @@ function pnpmPackages(text) {
|
|
|
1229
1453
|
}
|
|
1230
1454
|
return items.filter(Boolean);
|
|
1231
1455
|
}
|
|
1232
|
-
function readJson(file) {
|
|
1456
|
+
function readJson$1(file) {
|
|
1233
1457
|
try {
|
|
1234
1458
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1235
1459
|
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
@@ -1255,6 +1479,532 @@ function isDirectory$1(dir) {
|
|
|
1255
1479
|
}
|
|
1256
1480
|
}
|
|
1257
1481
|
//#endregion
|
|
1482
|
+
//#region src/tokens/color.ts
|
|
1483
|
+
const toOklch$1 = converter("oklch");
|
|
1484
|
+
/**
|
|
1485
|
+
* Euclidean distance in OKLCH with hue handled as an angle, which is ΔE in
|
|
1486
|
+
* OKLab: perceptually uniform, so "nearest token" means nearest to the eye.
|
|
1487
|
+
*/
|
|
1488
|
+
const okDistance = differenceEuclidean("oklch");
|
|
1489
|
+
const KEYWORDS = /* @__PURE__ */ new Set([
|
|
1490
|
+
"transparent",
|
|
1491
|
+
"currentcolor",
|
|
1492
|
+
"inherit",
|
|
1493
|
+
"initial",
|
|
1494
|
+
"unset",
|
|
1495
|
+
"revert",
|
|
1496
|
+
"revert-layer",
|
|
1497
|
+
"none"
|
|
1498
|
+
]);
|
|
1499
|
+
/**
|
|
1500
|
+
* HSL channels without the function, `214.3 31.8% 91.4%` (commas, `deg` and
|
|
1501
|
+
* `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
|
|
1502
|
+
* colors, to be used as `hsl(var(--border))`.
|
|
1503
|
+
*/
|
|
1504
|
+
const BARE_HSL = /^(-?\d*\.?\d+)(deg)?(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*\/\s*(\d*\.?\d+%?))?$/;
|
|
1505
|
+
function isBareHsl(value) {
|
|
1506
|
+
return BARE_HSL.test(value.trim());
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Parses any CSS color (hex, rgb(), hsl(), oklch(), named, ...) into OKLCH.
|
|
1510
|
+
* With `bareHsl`, for token values, HSL channels count too: in a stylesheet
|
|
1511
|
+
* they can only be a color, while in a class or style they are not one.
|
|
1512
|
+
*/
|
|
1513
|
+
function parseColor(value, options = {}) {
|
|
1514
|
+
const text = value.trim();
|
|
1515
|
+
if (!text || KEYWORDS.has(text.toLowerCase()) || text.includes("var(")) return void 0;
|
|
1516
|
+
let parsed = parse(text);
|
|
1517
|
+
const hsl = !parsed && options.bareHsl ? BARE_HSL.exec(text) : null;
|
|
1518
|
+
if (hsl) {
|
|
1519
|
+
const [, h, deg = "", s, l, alpha] = hsl;
|
|
1520
|
+
parsed = parse(`hsl(${h}${deg} ${s}% ${l}%${alpha ? ` / ${alpha}` : ""})`);
|
|
1521
|
+
}
|
|
1522
|
+
return parsed ? toOklch$1(parsed) : void 0;
|
|
1523
|
+
}
|
|
1524
|
+
function colorDistance(a, b) {
|
|
1525
|
+
return okDistance(a, b);
|
|
1526
|
+
}
|
|
1527
|
+
function isNamedColor(value) {
|
|
1528
|
+
return Object.hasOwn(colorsNamed, value.toLowerCase());
|
|
1529
|
+
}
|
|
1530
|
+
/** Color literals inside arbitrary CSS text: hex (not `url(#id)` references) and color functions. */
|
|
1531
|
+
const COLOR_LITERAL = /(?<!url\(\s*["']?)#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})(?![0-9a-zA-Z_-])|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([^()]*(?:\([^()]*\)[^()]*)*\)/g;
|
|
1532
|
+
/** Finds hex and functional color literals in `text` (named colors are not matched). */
|
|
1533
|
+
function findColorLiterals(text) {
|
|
1534
|
+
const matches = [];
|
|
1535
|
+
for (const match of text.matchAll(COLOR_LITERAL)) {
|
|
1536
|
+
const color = parseColor(match[0]);
|
|
1537
|
+
if (color) matches.push({
|
|
1538
|
+
text: match[0],
|
|
1539
|
+
index: match.index,
|
|
1540
|
+
color
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
return matches;
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Chroma from which a color reads as a hue at any lightness. Tailwind's grays,
|
|
1547
|
+
* slate to taupe, stay below 0.046; its colors pass 0.05 by shade 200.
|
|
1548
|
+
*/
|
|
1549
|
+
const TINT_CHROMA = .05;
|
|
1550
|
+
/**
|
|
1551
|
+
* Pale colors have little chroma to give, so below TINT_CHROMA a light color's
|
|
1552
|
+
* hue is told by its chroma relative to the most sRGB allows at that lightness
|
|
1553
|
+
* and hue. `#fef2f2` (red-50) has 0.013 of a possible 0.014; Tailwind's light
|
|
1554
|
+
* grays reach at most 0.44 of theirs (slate-100), its palest hues start at
|
|
1555
|
+
* 0.64 (teal-50). Near black the measure says little (slate-950 and shadcn's
|
|
1556
|
+
* v3 dark background would read as blue), so it applies from TINT_LIGHTNESS up.
|
|
1557
|
+
*/
|
|
1558
|
+
const TINT_RELATIVE = .55;
|
|
1559
|
+
const TINT_LIGHTNESS = .7;
|
|
1560
|
+
/** Below this chroma the hue angle is rounding noise. */
|
|
1561
|
+
const MIN_CHROMA = .005;
|
|
1562
|
+
const maxChromas = /* @__PURE__ */ new Map();
|
|
1563
|
+
/** The highest chroma sRGB can show at this lightness and hue. */
|
|
1564
|
+
function maxChroma(l, h) {
|
|
1565
|
+
const key = `${l.toFixed(3)}:${h.toFixed(1)}`;
|
|
1566
|
+
let max = maxChromas.get(key);
|
|
1567
|
+
if (max === void 0) {
|
|
1568
|
+
max = clampChroma({
|
|
1569
|
+
mode: "oklch",
|
|
1570
|
+
l: Math.min(1, Math.max(0, l)),
|
|
1571
|
+
c: .4,
|
|
1572
|
+
h
|
|
1573
|
+
}, "oklch").c;
|
|
1574
|
+
maxChromas.set(key, max);
|
|
1575
|
+
}
|
|
1576
|
+
return max;
|
|
1577
|
+
}
|
|
1578
|
+
/** Whether a color reads as a hue rather than a gray, however pale or dark it is. */
|
|
1579
|
+
function isTinted(color) {
|
|
1580
|
+
if (color.c >= TINT_CHROMA) return true;
|
|
1581
|
+
if (color.c < MIN_CHROMA || color.h === void 0 || color.l < TINT_LIGHTNESS) return false;
|
|
1582
|
+
const max = maxChroma(color.l, color.h);
|
|
1583
|
+
return max > 0 && color.c / max >= TINT_RELATIVE;
|
|
1584
|
+
}
|
|
1585
|
+
/** The angle between two hues, 0–180°; 0 when either is a gray. */
|
|
1586
|
+
function hueDistance(a, b) {
|
|
1587
|
+
if (a.h === void 0 || b.h === void 0) return 0;
|
|
1588
|
+
const diff = Math.abs(a.h - b.h) % 360;
|
|
1589
|
+
return Math.min(diff, 360 - diff);
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Whether `token` can stand in for `color` without changing what it is: both
|
|
1593
|
+
* grays, or both hues within `tolerance`. ΔE alone does not say it: a pale
|
|
1594
|
+
* yellow is 0.07 from a light gray. `tinted` overrides the test for the source,
|
|
1595
|
+
* so a pale `bg-sky-50` counts as blue whatever its chroma.
|
|
1596
|
+
*/
|
|
1597
|
+
function sameHue(color, token, tinted = isTinted(color), tolerance = 15) {
|
|
1598
|
+
if (tinted !== isTinted(token)) return false;
|
|
1599
|
+
return !tinted || hueDistance(color, token) <= tolerance;
|
|
1600
|
+
}
|
|
1601
|
+
/** Formats a ΔE value the way it reads best in a diagnostic. */
|
|
1602
|
+
function formatDeltaE(deltaE) {
|
|
1603
|
+
return deltaE < 5e-4 ? "exact match" : `ΔE ${deltaE.toFixed(3)}`;
|
|
1604
|
+
}
|
|
1605
|
+
//#endregion
|
|
1606
|
+
//#region src/libraries/mui.ts
|
|
1607
|
+
/** The palette groups MUI's components take as a `color` value (`<Button color="primary">`). */
|
|
1608
|
+
const ROLES = /* @__PURE__ */ new Set([
|
|
1609
|
+
"primary",
|
|
1610
|
+
"secondary",
|
|
1611
|
+
"error",
|
|
1612
|
+
"warning",
|
|
1613
|
+
"info",
|
|
1614
|
+
"success"
|
|
1615
|
+
]);
|
|
1616
|
+
/** Palette keys that are settings, not colors. */
|
|
1617
|
+
const SETTINGS = /* @__PURE__ */ new Set([
|
|
1618
|
+
"mode",
|
|
1619
|
+
"contrastThreshold",
|
|
1620
|
+
"tonalOffset",
|
|
1621
|
+
"augmentColor",
|
|
1622
|
+
"getContrastText"
|
|
1623
|
+
]);
|
|
1624
|
+
/** Functions that build an MUI theme from an options object. */
|
|
1625
|
+
const THEME_CALLS = /^(?:createTheme|extendTheme|experimental_extendTheme|createMuiTheme)$/;
|
|
1626
|
+
/**
|
|
1627
|
+
* MUI (`@mui/material`): components from its type declarations, and tokens from the theme the
|
|
1628
|
+
* app creates with `createTheme`, where its values are written out.
|
|
1629
|
+
*/
|
|
1630
|
+
const mui = {
|
|
1631
|
+
id: "mui",
|
|
1632
|
+
package: "@mui/material",
|
|
1633
|
+
title: "MUI",
|
|
1634
|
+
augments: /declare\s+module\s+['"]@mui\/(?:material|system|types)(?:\/[\w./-]*)?['"]/,
|
|
1635
|
+
describe: describeMui,
|
|
1636
|
+
annotate(component) {
|
|
1637
|
+
if (component.name === "Tooltip") component.labelsChild = {
|
|
1638
|
+
prop: "title",
|
|
1639
|
+
unless: "describeChild"
|
|
1640
|
+
};
|
|
1641
|
+
},
|
|
1642
|
+
theme: {
|
|
1643
|
+
matches: /\b(?:createTheme|extendTheme|experimental_extendTheme|createMuiTheme)\s*\(|\bmain\s*:|\bvariants\s*:/,
|
|
1644
|
+
read: readThemes
|
|
1645
|
+
}
|
|
1646
|
+
};
|
|
1647
|
+
/** The keys of a palette color group: `teal: { light, main, dark, contrastText }`. */
|
|
1648
|
+
const SHADES = /^(?:light|main|dark|contrastText|lighter|darker|A?\d{2,3})$/;
|
|
1649
|
+
/**
|
|
1650
|
+
* MUI's JSDoc lists demos and API pages rather than describing the component: keep any prose,
|
|
1651
|
+
* then the demo titles and the API page, which also feed search.
|
|
1652
|
+
*/
|
|
1653
|
+
function describeMui(jsdoc) {
|
|
1654
|
+
const [prose = "", rest = ""] = jsdoc.split(/^Demos:\s*$/m);
|
|
1655
|
+
const demos = [...(rest.split(/^API:\s*$/m)[0] ?? "").matchAll(/^- \[([^\]]+)\]\(/gm)].map((m) => m[1]);
|
|
1656
|
+
const api = /\[[^\]]+ API\]\((https:[^)\s]+)\)/.exec(rest)?.[1];
|
|
1657
|
+
const parts = [
|
|
1658
|
+
prose.trim(),
|
|
1659
|
+
demos.length ? `Demos: ${demos.join(", ")}.` : "",
|
|
1660
|
+
api ? `API: ${api}` : ""
|
|
1661
|
+
].filter(Boolean);
|
|
1662
|
+
return parts.length ? parts.join("\n\n") : void 0;
|
|
1663
|
+
}
|
|
1664
|
+
/** Tests, stories, mocks and fixtures: themes there are not the app's. */
|
|
1665
|
+
const NOT_THE_APP = /(?:^|\/)[^/]*(?:test|spec|stor(?:y|ies)|mock|fixture)[^/]*(?:\/|$)/i;
|
|
1666
|
+
/**
|
|
1667
|
+
* Tokens from the options objects passed to `createTheme` (or `extendTheme`) in `files`:
|
|
1668
|
+
* palette colors (`primary.main`, `background.paper`, `divider`) with their dark values from
|
|
1669
|
+
* `colorSchemes.dark`, and the spacing unit, border radius and font family. Values written as
|
|
1670
|
+
* literals are read, through constants of the same file or imported from another of the app's
|
|
1671
|
+
* files by a relative path; anything computed is left out. Nothing runs. Tests, stories and
|
|
1672
|
+
* their helpers are skipped: a theme there is not the app's.
|
|
1673
|
+
*/
|
|
1674
|
+
function readThemes(files, root) {
|
|
1675
|
+
const tokens = /* @__PURE__ */ new Map();
|
|
1676
|
+
const values = [];
|
|
1677
|
+
const scopes = new Scopes(root);
|
|
1678
|
+
for (const { file, text } of files) {
|
|
1679
|
+
const rel = relativePath(root, file);
|
|
1680
|
+
if (NOT_THE_APP.test(rel)) continue;
|
|
1681
|
+
const scope = scopes.of(file, text);
|
|
1682
|
+
if (!scope) continue;
|
|
1683
|
+
const visit = (node) => {
|
|
1684
|
+
if (ts.isCallExpression(node) && THEME_CALLS.test(calleeName(node.expression) ?? "")) {
|
|
1685
|
+
const [first] = node.arguments;
|
|
1686
|
+
const literal = object(first ? scopes.evaluate(first, scope, 0) : void 0);
|
|
1687
|
+
if (literal) {
|
|
1688
|
+
const line = scope.source.getLineAndCharacterOfPosition(node.getStart(scope.source)).line + 1;
|
|
1689
|
+
for (const token of themeTokens(literal, {
|
|
1690
|
+
file: rel,
|
|
1691
|
+
line
|
|
1692
|
+
})) if (!tokens.has(token.name)) tokens.set(token.name, token);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
if (ts.isPropertyAssignment(node) && ts.isObjectLiteralExpression(node.initializer)) values.push(...addedValues(node.name, node.initializer));
|
|
1696
|
+
ts.forEachChild(node, visit);
|
|
1697
|
+
};
|
|
1698
|
+
visit(scope.source);
|
|
1699
|
+
}
|
|
1700
|
+
return {
|
|
1701
|
+
tokens: [...tokens.values()],
|
|
1702
|
+
warnings: [],
|
|
1703
|
+
values,
|
|
1704
|
+
files: scopes.read()
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Values a theme adds, whether or not its options can be read: a palette group
|
|
1709
|
+
* (`teal: { main: …, dark: … }`) makes `teal` a color of every component's `color`, and a
|
|
1710
|
+
* component's `variants: [{ props: { variant: "dashed" } }]` (under `MuiButton`) a value of
|
|
1711
|
+
* that prop. Over-reading only allows more; it never reports anything.
|
|
1712
|
+
*/
|
|
1713
|
+
function addedValues(name, value) {
|
|
1714
|
+
const key = keyOf(name);
|
|
1715
|
+
if (key === void 0) return [];
|
|
1716
|
+
const keys = value.properties.map((p) => p.name ? keyOf(p.name) : void 0);
|
|
1717
|
+
if (keys.includes("main") && keys.every((k) => k !== void 0 && SHADES.test(k))) return [{
|
|
1718
|
+
prop: "color",
|
|
1719
|
+
value: key,
|
|
1720
|
+
alongside: "primary"
|
|
1721
|
+
}];
|
|
1722
|
+
const component = /^Mui([A-Z]\w*)$/.exec(key)?.[1];
|
|
1723
|
+
if (!component) return [];
|
|
1724
|
+
const found = [];
|
|
1725
|
+
const visit = (node) => {
|
|
1726
|
+
if (ts.isPropertyAssignment(node) && keyOf(node.name) === "props" && ts.isObjectLiteralExpression(node.initializer)) {
|
|
1727
|
+
for (const prop of node.initializer.properties) {
|
|
1728
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isStringLiteralLike(prop.initializer)) continue;
|
|
1729
|
+
const propName = keyOf(prop.name);
|
|
1730
|
+
if (propName) found.push({
|
|
1731
|
+
component,
|
|
1732
|
+
prop: propName,
|
|
1733
|
+
value: prop.initializer.text
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
ts.forEachChild(node, visit);
|
|
1739
|
+
};
|
|
1740
|
+
visit(value);
|
|
1741
|
+
return found;
|
|
1742
|
+
}
|
|
1743
|
+
function calleeName(expression) {
|
|
1744
|
+
if (ts.isIdentifier(expression)) return expression.text;
|
|
1745
|
+
if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
|
|
1746
|
+
}
|
|
1747
|
+
const SCRIPT_EXTENSIONS = [
|
|
1748
|
+
".ts",
|
|
1749
|
+
".tsx",
|
|
1750
|
+
".js",
|
|
1751
|
+
".jsx",
|
|
1752
|
+
".mjs",
|
|
1753
|
+
".cjs",
|
|
1754
|
+
".mts",
|
|
1755
|
+
".cts"
|
|
1756
|
+
];
|
|
1757
|
+
/** The app's files a theme reads from, each parsed once. */
|
|
1758
|
+
var Scopes = class {
|
|
1759
|
+
root;
|
|
1760
|
+
scopes = /* @__PURE__ */ new Map();
|
|
1761
|
+
constructor(root) {
|
|
1762
|
+
this.root = root;
|
|
1763
|
+
}
|
|
1764
|
+
/** The files read beyond those passed in: the modules a theme imports. */
|
|
1765
|
+
read() {
|
|
1766
|
+
return [...this.scopes.keys()];
|
|
1767
|
+
}
|
|
1768
|
+
of(file, text) {
|
|
1769
|
+
const known = this.scopes.get(file);
|
|
1770
|
+
if (known !== void 0) return known ?? void 0;
|
|
1771
|
+
let scope;
|
|
1772
|
+
try {
|
|
1773
|
+
scope = scopeOf(file, ts.createSourceFile(file, text ?? fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, scriptKind$1(file)));
|
|
1774
|
+
} catch {
|
|
1775
|
+
scope = null;
|
|
1776
|
+
}
|
|
1777
|
+
this.scopes.set(file, scope);
|
|
1778
|
+
return scope ?? void 0;
|
|
1779
|
+
}
|
|
1780
|
+
/** A literal value, following constants and relative imports; undefined for anything computed. */
|
|
1781
|
+
evaluate(node, scope, depth) {
|
|
1782
|
+
if (depth > 24) return void 0;
|
|
1783
|
+
const next = (n, s = scope) => this.evaluate(n, s, depth + 1);
|
|
1784
|
+
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node)) return next(node.expression);
|
|
1785
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
1786
|
+
if (ts.isNumericLiteral(node)) return Number(node.text);
|
|
1787
|
+
if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
|
|
1788
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
1789
|
+
if (ts.isIdentifier(node)) return this.binding(node.text, scope, depth);
|
|
1790
|
+
if (ts.isPropertyAccessExpression(node)) return member(next(node.expression), node.name.text);
|
|
1791
|
+
if (ts.isElementAccessExpression(node)) {
|
|
1792
|
+
const key = next(node.argumentExpression);
|
|
1793
|
+
return typeof key === "string" || typeof key === "number" ? member(next(node.expression), String(key)) : void 0;
|
|
1794
|
+
}
|
|
1795
|
+
if (ts.isArrayLiteralExpression(node)) return node.elements.map((element) => next(element));
|
|
1796
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
1797
|
+
const result = {};
|
|
1798
|
+
for (const property of node.properties) if (ts.isPropertyAssignment(property)) {
|
|
1799
|
+
const key = keyOf(property.name);
|
|
1800
|
+
if (key !== void 0) result[key] = next(property.initializer);
|
|
1801
|
+
} else if (ts.isShorthandPropertyAssignment(property)) result[property.name.text] = next(property.name);
|
|
1802
|
+
else if (ts.isSpreadAssignment(property)) {
|
|
1803
|
+
const spread = object(next(property.expression));
|
|
1804
|
+
if (spread) Object.assign(result, spread);
|
|
1805
|
+
}
|
|
1806
|
+
return result;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/** The value of a name in a scope: one of its constants, or what it imports by that name. */
|
|
1810
|
+
binding(name, scope, depth) {
|
|
1811
|
+
const constant = scope.constants.get(name);
|
|
1812
|
+
if (constant) return this.evaluate(constant, scope, depth + 1);
|
|
1813
|
+
const imported = scope.imports.get(name);
|
|
1814
|
+
if (!imported) return void 0;
|
|
1815
|
+
const target = this.resolve(imported.specifier, scope.file);
|
|
1816
|
+
const module = target ? this.of(target) : void 0;
|
|
1817
|
+
const local = module?.exports.get(imported.name);
|
|
1818
|
+
return module && local ? this.binding(local, module, depth + 1) : void 0;
|
|
1819
|
+
}
|
|
1820
|
+
/** A relative import's file, among the app's own files. */
|
|
1821
|
+
resolve(specifier, from) {
|
|
1822
|
+
if (!specifier.startsWith(".")) return void 0;
|
|
1823
|
+
const base = path.resolve(path.dirname(from), specifier);
|
|
1824
|
+
if (!isInside(this.root, base) || /[\\/]node_modules[\\/]/.test(base)) return void 0;
|
|
1825
|
+
const stem = base.replace(/\.[cm]?js$/, "");
|
|
1826
|
+
for (const candidate of [
|
|
1827
|
+
base,
|
|
1828
|
+
...SCRIPT_EXTENSIONS.map((ext) => `${stem}${ext}`),
|
|
1829
|
+
...SCRIPT_EXTENSIONS.map((ext) => path.join(base, `index${ext}`))
|
|
1830
|
+
]) if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
function scriptKind$1(file) {
|
|
1834
|
+
if (/\.tsx$/.test(file)) return ts.ScriptKind.TSX;
|
|
1835
|
+
if (/\.jsx$/.test(file)) return ts.ScriptKind.JSX;
|
|
1836
|
+
if (/\.[cm]?js$/.test(file)) return ts.ScriptKind.JS;
|
|
1837
|
+
return ts.ScriptKind.TS;
|
|
1838
|
+
}
|
|
1839
|
+
function scopeOf(file, source) {
|
|
1840
|
+
const scope = {
|
|
1841
|
+
file,
|
|
1842
|
+
source,
|
|
1843
|
+
constants: /* @__PURE__ */ new Map(),
|
|
1844
|
+
imports: /* @__PURE__ */ new Map(),
|
|
1845
|
+
exports: /* @__PURE__ */ new Map()
|
|
1846
|
+
};
|
|
1847
|
+
for (const statement of source.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
1848
|
+
const specifier = statement.moduleSpecifier.text;
|
|
1849
|
+
const clause = statement.importClause;
|
|
1850
|
+
if (clause?.name) scope.imports.set(clause.name.text, {
|
|
1851
|
+
specifier,
|
|
1852
|
+
name: "default"
|
|
1853
|
+
});
|
|
1854
|
+
const bindings = clause?.namedBindings;
|
|
1855
|
+
if (bindings && ts.isNamedImports(bindings)) for (const element of bindings.elements) scope.imports.set(element.name.text, {
|
|
1856
|
+
specifier,
|
|
1857
|
+
name: (element.propertyName ?? element.name).text
|
|
1858
|
+
});
|
|
1859
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
1860
|
+
const list = statement.declarationList;
|
|
1861
|
+
if (!(list.flags & ts.NodeFlags.Const)) continue;
|
|
1862
|
+
const exported = statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
1863
|
+
for (const declaration of list.declarations) {
|
|
1864
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
|
|
1865
|
+
scope.constants.set(declaration.name.text, declaration.initializer);
|
|
1866
|
+
if (exported) scope.exports.set(declaration.name.text, declaration.name.text);
|
|
1867
|
+
}
|
|
1868
|
+
} else if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier) {
|
|
1869
|
+
const clause = statement.exportClause;
|
|
1870
|
+
if (clause && ts.isNamedExports(clause)) for (const element of clause.elements) scope.exports.set(element.name.text, (element.propertyName ?? element.name).text);
|
|
1871
|
+
} else if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
1872
|
+
if (ts.isIdentifier(statement.expression)) scope.exports.set("default", statement.expression.text);
|
|
1873
|
+
else {
|
|
1874
|
+
scope.constants.set("default", statement.expression);
|
|
1875
|
+
scope.exports.set("default", "default");
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return scope;
|
|
1879
|
+
}
|
|
1880
|
+
/** A property of an object read so far (`neonBlue[400]`, `palette.primary`). */
|
|
1881
|
+
function member(value, key) {
|
|
1882
|
+
if (Array.isArray(value)) return /^\d+$/.test(key) ? value[Number(key)] : void 0;
|
|
1883
|
+
return object(value)?.[key];
|
|
1884
|
+
}
|
|
1885
|
+
function keyOf(name) {
|
|
1886
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
1887
|
+
}
|
|
1888
|
+
function object(value) {
|
|
1889
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1890
|
+
}
|
|
1891
|
+
/** The colors of a palette, by path: `primary.main`, `background.paper`, `divider`. */
|
|
1892
|
+
function paletteColors(palette) {
|
|
1893
|
+
const colors = /* @__PURE__ */ new Map();
|
|
1894
|
+
for (const [key, value] of Object.entries(palette ?? {})) {
|
|
1895
|
+
if (SETTINGS.has(key)) continue;
|
|
1896
|
+
if (typeof value === "string") {
|
|
1897
|
+
if (parseColor(value)) colors.set(key, value);
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
for (const [shade, color] of Object.entries(object(value) ?? {})) if (typeof color === "string" && parseColor(color)) colors.set(`${key}.${shade}`, color);
|
|
1901
|
+
}
|
|
1902
|
+
return colors;
|
|
1903
|
+
}
|
|
1904
|
+
function themeTokens(options, source) {
|
|
1905
|
+
const tokens = [];
|
|
1906
|
+
const schemes = object(options.colorSchemes);
|
|
1907
|
+
const light = paletteColors(object(object(schemes?.light)?.palette) ?? object(options.palette));
|
|
1908
|
+
const dark = paletteColors(object(object(schemes?.dark)?.palette));
|
|
1909
|
+
const variables = options.cssVariables !== void 0 && options.cssVariables !== false;
|
|
1910
|
+
const add = (name, category, value, usage) => tokens.push({
|
|
1911
|
+
name,
|
|
1912
|
+
category,
|
|
1913
|
+
value,
|
|
1914
|
+
usage,
|
|
1915
|
+
source,
|
|
1916
|
+
description: "From the MUI theme."
|
|
1917
|
+
});
|
|
1918
|
+
for (const name of /* @__PURE__ */ new Set([...light.keys(), ...dark.keys()])) {
|
|
1919
|
+
const value = light.get(name) ?? dark.get(name) ?? "";
|
|
1920
|
+
const [group = name, shade] = name.split(".");
|
|
1921
|
+
const key = group === "background" ? "bgcolor" : group === "divider" ? "borderColor" : "color";
|
|
1922
|
+
add(name, "color", value, [
|
|
1923
|
+
...ROLES.has(group) && shade === "main" ? [`color="${group}"`] : [],
|
|
1924
|
+
`sx={{ ${key}: '${name}' }}`,
|
|
1925
|
+
`theme.palette.${name}`
|
|
1926
|
+
]);
|
|
1927
|
+
const token = tokens.at(-1);
|
|
1928
|
+
if (!token) continue;
|
|
1929
|
+
if (variables) token.cssVar = `--mui-palette-${name.replace(/\./g, "-")}`;
|
|
1930
|
+
const darkValue = dark.get(name);
|
|
1931
|
+
if (darkValue !== void 0 && light.has(name) && darkValue !== value) token.modes = { dark: darkValue };
|
|
1932
|
+
}
|
|
1933
|
+
const spacing = options.spacing;
|
|
1934
|
+
if (typeof spacing === "number" || typeof spacing === "string") add("spacing", "spacing", typeof spacing === "number" ? `${spacing}px` : spacing, ["theme.spacing(1)", "sx={{ p: 1 }}"]);
|
|
1935
|
+
const radius = object(options.shape)?.borderRadius;
|
|
1936
|
+
if (typeof radius === "number" || typeof radius === "string") add("shape.borderRadius", "radius", typeof radius === "number" ? `${radius}px` : radius, ["theme.shape.borderRadius", "sx={{ borderRadius: 1 }}"]);
|
|
1937
|
+
const family = object(options.typography)?.fontFamily;
|
|
1938
|
+
if (typeof family === "string") add("typography.fontFamily", "typography", family, ["theme.typography.fontFamily"]);
|
|
1939
|
+
for (const token of tokens) token.origin = "library";
|
|
1940
|
+
return tokens;
|
|
1941
|
+
}
|
|
1942
|
+
//#endregion
|
|
1943
|
+
//#region src/libraries/index.ts
|
|
1944
|
+
/** The ids of the libraries onsystem can read, for the config's `libraries` list. */
|
|
1945
|
+
const LIBRARY_IDS = ["mui"];
|
|
1946
|
+
/** The libraries onsystem can read, by package. */
|
|
1947
|
+
const LIBRARIES = [mui];
|
|
1948
|
+
function libraryAdapter(name) {
|
|
1949
|
+
return LIBRARIES.find((library) => library.package === name);
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* The libraries `root`'s package.json depends on (dependencies, devDependencies or
|
|
1953
|
+
* peerDependencies), each found where Node.js would find it: in `node_modules` at the root or a
|
|
1954
|
+
* folder above it, as a monorepo hoists it. Nothing in the package is run. `ids` is the config's
|
|
1955
|
+
* `libraries`: undefined reads every library onsystem supports, false none, a list those named;
|
|
1956
|
+
* the dependencies it leaves out are `off`.
|
|
1957
|
+
*/
|
|
1958
|
+
function detectLibraries(root, ids) {
|
|
1959
|
+
const pkg = readJson(path.join(root, "package.json"));
|
|
1960
|
+
if (!pkg) return {
|
|
1961
|
+
read: [],
|
|
1962
|
+
off: []
|
|
1963
|
+
};
|
|
1964
|
+
const dependencies = new Set([
|
|
1965
|
+
"dependencies",
|
|
1966
|
+
"devDependencies",
|
|
1967
|
+
"peerDependencies"
|
|
1968
|
+
].flatMap((field) => {
|
|
1969
|
+
const value = pkg[field];
|
|
1970
|
+
return value && typeof value === "object" ? Object.keys(value) : [];
|
|
1971
|
+
}));
|
|
1972
|
+
const found = LIBRARIES.filter((library) => dependencies.has(library.package)).map((library) => {
|
|
1973
|
+
const dir = installedPackage(root, library.package);
|
|
1974
|
+
const version = dir ? readJson(path.join(dir, "package.json"))?.version : void 0;
|
|
1975
|
+
const source = {
|
|
1976
|
+
package: library.package,
|
|
1977
|
+
dir,
|
|
1978
|
+
version: typeof version === "string" ? version : void 0
|
|
1979
|
+
};
|
|
1980
|
+
return {
|
|
1981
|
+
id: library.id,
|
|
1982
|
+
source
|
|
1983
|
+
};
|
|
1984
|
+
});
|
|
1985
|
+
const wanted = (id) => ids === void 0 || ids !== false && ids.includes(id);
|
|
1986
|
+
return {
|
|
1987
|
+
read: found.filter((f) => wanted(f.id)).map((f) => f.source),
|
|
1988
|
+
off: found.filter((f) => !wanted(f.id)).map((f) => f.source)
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
/** The folder `name` is installed in for code in `root`, looking up through `node_modules` folders. */
|
|
1992
|
+
function installedPackage(root, name) {
|
|
1993
|
+
for (let dir = path.resolve(root);; dir = path.dirname(dir)) {
|
|
1994
|
+
const candidate = path.join(dir, "node_modules", ...name.split("/"));
|
|
1995
|
+
if (fs.existsSync(path.join(candidate, "package.json"))) return candidate;
|
|
1996
|
+
if (dir === path.dirname(dir)) return void 0;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
function readJson(file) {
|
|
2000
|
+
try {
|
|
2001
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
2002
|
+
return value && typeof value === "object" ? value : void 0;
|
|
2003
|
+
} catch {
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
//#endregion
|
|
1258
2008
|
//#region src/extract/cva.ts
|
|
1259
2009
|
const FACTORIES = /* @__PURE__ */ new Set(["cva", "tv"]);
|
|
1260
2010
|
/** Finds every `const x = cva(base, { variants, defaultVariants })` in a file. */
|
|
@@ -1386,129 +2136,102 @@ function classText(node) {
|
|
|
1386
2136
|
visit(node);
|
|
1387
2137
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
1388
2138
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
const KEYWORDS = /* @__PURE__ */ new Set([
|
|
1398
|
-
"transparent",
|
|
1399
|
-
"currentcolor",
|
|
1400
|
-
"inherit",
|
|
1401
|
-
"initial",
|
|
1402
|
-
"unset",
|
|
1403
|
-
"revert",
|
|
1404
|
-
"revert-layer",
|
|
1405
|
-
"none"
|
|
1406
|
-
]);
|
|
1407
|
-
/**
|
|
1408
|
-
* HSL channels without the function, `214.3 31.8% 91.4%` (commas, `deg` and
|
|
1409
|
-
* `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
|
|
1410
|
-
* colors, to be used as `hsl(var(--border))`.
|
|
1411
|
-
*/
|
|
1412
|
-
const BARE_HSL = /^(-?\d*\.?\d+)(deg)?(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*,\s*|\s+)(\d*\.?\d+)%(?:\s*\/\s*(\d*\.?\d+%?))?$/;
|
|
1413
|
-
function isBareHsl(value) {
|
|
1414
|
-
return BARE_HSL.test(value.trim());
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* Parses any CSS color (hex, rgb(), hsl(), oklch(), named, ...) into OKLCH.
|
|
1418
|
-
* With `bareHsl`, for token values, HSL channels count too: in a stylesheet
|
|
1419
|
-
* they can only be a color, while in a class or style they are not one.
|
|
1420
|
-
*/
|
|
1421
|
-
function parseColor(value, options = {}) {
|
|
1422
|
-
const text = value.trim();
|
|
1423
|
-
if (!text || KEYWORDS.has(text.toLowerCase()) || text.includes("var(")) return void 0;
|
|
1424
|
-
let parsed = parse(text);
|
|
1425
|
-
const hsl = !parsed && options.bareHsl ? BARE_HSL.exec(text) : null;
|
|
1426
|
-
if (hsl) {
|
|
1427
|
-
const [, h, deg = "", s, l, alpha] = hsl;
|
|
1428
|
-
parsed = parse(`hsl(${h}${deg} ${s}% ${l}%${alpha ? ` / ${alpha}` : ""})`);
|
|
1429
|
-
}
|
|
1430
|
-
return parsed ? toOklch(parsed) : void 0;
|
|
1431
|
-
}
|
|
1432
|
-
function colorDistance(a, b) {
|
|
1433
|
-
return okDistance(a, b);
|
|
1434
|
-
}
|
|
1435
|
-
function isNamedColor(value) {
|
|
1436
|
-
return Object.hasOwn(colorsNamed, value.toLowerCase());
|
|
2139
|
+
const LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem|em)?$/i;
|
|
2140
|
+
/** `"0.5rem"` → 8, `"12px"` → 12, `"4"` → 4. Returns undefined for anything else. */
|
|
2141
|
+
function lengthToPx(value) {
|
|
2142
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
2143
|
+
const match = LENGTH.exec(value.trim());
|
|
2144
|
+
if (!match) return void 0;
|
|
2145
|
+
const n = Number(match[1]);
|
|
2146
|
+
return (match[2] ?? "px").toLowerCase() === "px" ? n : n * 16;
|
|
1437
2147
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
/** Finds hex and functional color literals in `text` (named colors are not matched). */
|
|
1441
|
-
function findColorLiterals(text) {
|
|
1442
|
-
const matches = [];
|
|
1443
|
-
for (const match of text.matchAll(COLOR_LITERAL)) {
|
|
1444
|
-
const color = parseColor(match[0]);
|
|
1445
|
-
if (color) matches.push({
|
|
1446
|
-
text: match[0],
|
|
1447
|
-
index: match.index,
|
|
1448
|
-
color
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
return matches;
|
|
2148
|
+
function formatPx(px) {
|
|
2149
|
+
return `${Number(px.toFixed(2))}px`;
|
|
1452
2150
|
}
|
|
1453
2151
|
/**
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*/
|
|
1457
|
-
const TINT_CHROMA = .05;
|
|
1458
|
-
/**
|
|
1459
|
-
* Pale colors have little chroma to give, so below TINT_CHROMA a light color's
|
|
1460
|
-
* hue is told by its chroma relative to the most sRGB allows at that lightness
|
|
1461
|
-
* and hue. `#fef2f2` (red-50) has 0.013 of a possible 0.014; Tailwind's light
|
|
1462
|
-
* grays reach at most 0.44 of theirs (slate-100), its palest hues start at
|
|
1463
|
-
* 0.64 (teal-50). Near black the measure says little (slate-950 and shadcn's
|
|
1464
|
-
* v3 dark background would read as blue), so it applies from TINT_LIGHTNESS up.
|
|
2152
|
+
* Evaluates a length that may use `var()` and `calc()` to px:
|
|
2153
|
+
* `calc(var(--radius) - 4px)` with `--radius: 0.625rem` → 6.
|
|
1465
2154
|
*/
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
let max = maxChromas.get(key);
|
|
1475
|
-
if (max === void 0) {
|
|
1476
|
-
max = clampChroma({
|
|
1477
|
-
mode: "oklch",
|
|
1478
|
-
l: Math.min(1, Math.max(0, l)),
|
|
1479
|
-
c: .4,
|
|
1480
|
-
h
|
|
1481
|
-
}, "oklch").c;
|
|
1482
|
-
maxChromas.set(key, max);
|
|
1483
|
-
}
|
|
1484
|
-
return max;
|
|
1485
|
-
}
|
|
1486
|
-
/** Whether a color reads as a hue rather than a gray, however pale or dark it is. */
|
|
1487
|
-
function isTinted(color) {
|
|
1488
|
-
if (color.c >= TINT_CHROMA) return true;
|
|
1489
|
-
if (color.c < MIN_CHROMA || color.h === void 0 || color.l < TINT_LIGHTNESS) return false;
|
|
1490
|
-
const max = maxChroma(color.l, color.h);
|
|
1491
|
-
return max > 0 && color.c / max >= TINT_RELATIVE;
|
|
2155
|
+
function evaluateLength(value, resolve, depth = 0) {
|
|
2156
|
+
if (depth > 16) return void 0;
|
|
2157
|
+
const expanded = expandVars(value.trim(), resolve, depth);
|
|
2158
|
+
if (expanded === void 0) return void 0;
|
|
2159
|
+
const direct = lengthToPx(expanded);
|
|
2160
|
+
if (direct !== void 0) return direct;
|
|
2161
|
+
const calc = /^calc\((.*)\)$/is.exec(expanded);
|
|
2162
|
+
return calc?.[1] !== void 0 ? evaluateCalc(calc[1]) : void 0;
|
|
1492
2163
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
2164
|
+
function expandVars(value, resolve, depth) {
|
|
2165
|
+
let out = "";
|
|
2166
|
+
let last = 0;
|
|
2167
|
+
for (const match of value.matchAll(/var\(\s*(--[\w-]+)\s*(?:,\s*([^)]+))?\)/g)) {
|
|
2168
|
+
const [whole, name = "", fallback] = match;
|
|
2169
|
+
const resolved = resolve(name) ?? fallback;
|
|
2170
|
+
const px = resolved === void 0 ? void 0 : evaluateLength(resolved, resolve, depth + 1);
|
|
2171
|
+
if (px === void 0) return void 0;
|
|
2172
|
+
out += `${value.slice(last, match.index)}${px}px`;
|
|
2173
|
+
last = match.index + whole.length;
|
|
2174
|
+
}
|
|
2175
|
+
return out + value.slice(last);
|
|
1498
2176
|
}
|
|
2177
|
+
const CALC_TOKEN = /-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)?|[()+\-*/]|calc/gi;
|
|
1499
2178
|
/**
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
2179
|
+
* Tiny recursive-descent evaluator for `+ - * /` over px/rem/em/unitless
|
|
2180
|
+
* numbers. Anything else (`%`, `vw`, `ch`, functions) has no fixed px value,
|
|
2181
|
+
* so the whole expression is undefined.
|
|
2182
|
+
*/
|
|
2183
|
+
function evaluateCalc(expression) {
|
|
2184
|
+
if (expression.replace(CALC_TOKEN, "").trim()) return void 0;
|
|
2185
|
+
const tokens = expression.match(CALC_TOKEN);
|
|
2186
|
+
if (!tokens) return void 0;
|
|
2187
|
+
let i = 0;
|
|
2188
|
+
const primary = () => {
|
|
2189
|
+
const token = tokens[i++];
|
|
2190
|
+
if (token === void 0) return void 0;
|
|
2191
|
+
if (token.toLowerCase() === "calc") return primary();
|
|
2192
|
+
if (token === "(") {
|
|
2193
|
+
const v = sum();
|
|
2194
|
+
i++;
|
|
2195
|
+
return v;
|
|
2196
|
+
}
|
|
2197
|
+
const px = lengthToPx(token);
|
|
2198
|
+
if (px === void 0) return void 0;
|
|
2199
|
+
return {
|
|
2200
|
+
n: px,
|
|
2201
|
+
length: /[a-z]$/i.test(token)
|
|
2202
|
+
};
|
|
2203
|
+
};
|
|
2204
|
+
const product = () => {
|
|
2205
|
+
let left = primary();
|
|
2206
|
+
while (left && (tokens[i] === "*" || tokens[i] === "/")) {
|
|
2207
|
+
const op = tokens[i++];
|
|
2208
|
+
const right = primary();
|
|
2209
|
+
if (!right) return void 0;
|
|
2210
|
+
left = op === "*" ? {
|
|
2211
|
+
n: left.n * right.n,
|
|
2212
|
+
length: left.length || right.length
|
|
2213
|
+
} : {
|
|
2214
|
+
n: left.n / right.n,
|
|
2215
|
+
length: left.length
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
return left;
|
|
2219
|
+
};
|
|
2220
|
+
const sum = () => {
|
|
2221
|
+
let left = product();
|
|
2222
|
+
while (left && (tokens[i] === "+" || tokens[i] === "-")) {
|
|
2223
|
+
const op = tokens[i++];
|
|
2224
|
+
const right = product();
|
|
2225
|
+
if (!right) return void 0;
|
|
2226
|
+
left = {
|
|
2227
|
+
n: op === "+" ? left.n + right.n : left.n - right.n,
|
|
2228
|
+
length: true
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
return left;
|
|
2232
|
+
};
|
|
2233
|
+
const result = sum();
|
|
2234
|
+
return result && Number.isFinite(result.n) ? result.n : void 0;
|
|
1512
2235
|
}
|
|
1513
2236
|
//#endregion
|
|
1514
2237
|
//#region src/tokens/usage.ts
|
|
@@ -1553,7 +2276,7 @@ function utilities(namespace, key) {
|
|
|
1553
2276
|
"gap-2",
|
|
1554
2277
|
"m-1"
|
|
1555
2278
|
];
|
|
1556
|
-
case "--radius": return [`rounded-${key}`];
|
|
2279
|
+
case "--radius": return [key === "DEFAULT" ? "rounded" : `rounded-${key}`];
|
|
1557
2280
|
case "--font": return [`font-${key}`];
|
|
1558
2281
|
case "--font-weight": return [`font-${key}`];
|
|
1559
2282
|
case "--text": return [`text-${key}`];
|
|
@@ -1626,6 +2349,106 @@ function collect(object, keys, colors) {
|
|
|
1626
2349
|
if (variable && name && !colors.has(variable)) colors.set(variable, name);
|
|
1627
2350
|
}
|
|
1628
2351
|
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Reads `borderRadius` entries from a Tailwind v3 config without running it:
|
|
2354
|
+
* every `borderRadius` object in the file, then in the project modules it
|
|
2355
|
+
* imports, such as a shared preset. The config's own entries win.
|
|
2356
|
+
*/
|
|
2357
|
+
function readTailwindRadii(file, resolve) {
|
|
2358
|
+
const radii = /* @__PURE__ */ new Map();
|
|
2359
|
+
const files = [];
|
|
2360
|
+
const queue = [file];
|
|
2361
|
+
while (queue.length && files.length < MAX_FILES) {
|
|
2362
|
+
const current = queue.shift() ?? "";
|
|
2363
|
+
if (files.includes(current)) continue;
|
|
2364
|
+
let text;
|
|
2365
|
+
try {
|
|
2366
|
+
text = fs.readFileSync(current, "utf8");
|
|
2367
|
+
} catch {
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
files.push(current);
|
|
2371
|
+
const source = ts.createSourceFile(current, text, ts.ScriptTarget.Latest, true, scriptKind(current));
|
|
2372
|
+
const visit = (node) => {
|
|
2373
|
+
if (ts.isPropertyAssignment(node) && propertyName(node.name) === "borderRadius" && ts.isObjectLiteralExpression(node.initializer)) {
|
|
2374
|
+
collectRadius(node.initializer, radii);
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
const specifier = importedSpecifier(node);
|
|
2378
|
+
const target = specifier === void 0 ? void 0 : resolve(specifier, current);
|
|
2379
|
+
if (target && /\.[cm]?[jt]s$/.test(target)) queue.push(target);
|
|
2380
|
+
ts.forEachChild(node, visit);
|
|
2381
|
+
};
|
|
2382
|
+
visit(source);
|
|
2383
|
+
}
|
|
2384
|
+
return {
|
|
2385
|
+
radii,
|
|
2386
|
+
files
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
/** `sm: "calc(var(--radius) - 4px)"` → `sm` → raw string; first seen wins. */
|
|
2390
|
+
function collectRadius(object, radii) {
|
|
2391
|
+
for (const property of object.properties) {
|
|
2392
|
+
if (!ts.isPropertyAssignment(property)) continue;
|
|
2393
|
+
const key = propertyName(property.name);
|
|
2394
|
+
if (key === void 0) continue;
|
|
2395
|
+
const value = property.initializer;
|
|
2396
|
+
let raw;
|
|
2397
|
+
if (ts.isStringLiteralLike(value)) raw = value.text;
|
|
2398
|
+
else if (ts.isNumericLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) raw = value.text;
|
|
2399
|
+
if (raw === void 0 || radii.has(key)) continue;
|
|
2400
|
+
radii.set(key, raw);
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
/**
|
|
2404
|
+
* Gives radius tokens their Tailwind v3 class names: each config entry is
|
|
2405
|
+
* evaluated (its `var()` against radius token values, so
|
|
2406
|
+
* `calc(var(--radius) - 4px)` resolves with the theme), then radius tokens
|
|
2407
|
+
* whose px matches exactly take the entry key. Entries with no matching
|
|
2408
|
+
* token add a synthetic one, so the scale step exists as a candidate
|
|
2409
|
+
* (mirrors the spacing-steps synthesis); without it the literal would keep
|
|
2410
|
+
* matching the nearest unrelated token. Config entries win over presets.
|
|
2411
|
+
*/
|
|
2412
|
+
function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
|
|
2413
|
+
const byVar = /* @__PURE__ */ new Map();
|
|
2414
|
+
for (const token of tokens) if (token.category === "radius" && token.cssVar) byVar.set(token.cssVar, token.value);
|
|
2415
|
+
const resolve = (name) => byVar.get(name);
|
|
2416
|
+
const byPx = /* @__PURE__ */ new Map();
|
|
2417
|
+
for (const [key, raw] of radii) {
|
|
2418
|
+
const px = evaluateLength(raw, resolve);
|
|
2419
|
+
if (px !== void 0 && !byPx.has(px)) byPx.set(px, key);
|
|
2420
|
+
}
|
|
2421
|
+
const havePx = /* @__PURE__ */ new Set();
|
|
2422
|
+
for (const token of tokens) {
|
|
2423
|
+
if (token.category !== "radius") continue;
|
|
2424
|
+
const px = evaluateLength(token.value, resolve);
|
|
2425
|
+
if (px === void 0) continue;
|
|
2426
|
+
havePx.add(px);
|
|
2427
|
+
const key = byPx.get(px);
|
|
2428
|
+
if (key === void 0) continue;
|
|
2429
|
+
token.tailwind = key;
|
|
2430
|
+
token.usage = usageFor(token, "--radius");
|
|
2431
|
+
}
|
|
2432
|
+
for (const [px, key] of byPx) {
|
|
2433
|
+
if (havePx.has(px)) continue;
|
|
2434
|
+
const token = {
|
|
2435
|
+
name: `radius-${key}`,
|
|
2436
|
+
category: "radius",
|
|
2437
|
+
value: `${px}px`,
|
|
2438
|
+
tailwind: key,
|
|
2439
|
+
usage: usageFor({
|
|
2440
|
+
category: "radius",
|
|
2441
|
+
tailwind: key
|
|
2442
|
+
}, "--radius"),
|
|
2443
|
+
source: {
|
|
2444
|
+
file: configFile,
|
|
2445
|
+
line: 1
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
tokens.push(token);
|
|
2449
|
+
havePx.add(px);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
1629
2452
|
/** The specifier of `require("x")`, `import x from "x"` or `import("x")`. */
|
|
1630
2453
|
function importedSpecifier(node) {
|
|
1631
2454
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) return node.moduleSpecifier.text;
|
|
@@ -1754,15 +2577,25 @@ const tokenSourceSchema = z.union([z.string(), z.object({
|
|
|
1754
2577
|
path: z.string().describe("File or glob."),
|
|
1755
2578
|
prefix: z.string().optional().describe("DTCG only: custom property prefix, e.g. \"acme\" for --acme-color-primary.")
|
|
1756
2579
|
}).strict()]);
|
|
2580
|
+
/** `false`, or a list of library ids; an unknown id is named with the closest known one. */
|
|
2581
|
+
const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))], { error: (issue) => {
|
|
2582
|
+
const unknown = Array.isArray(issue.input) ? issue.input.find((id) => !LIBRARY_IDS.includes(id)) : void 0;
|
|
2583
|
+
if (typeof unknown === "string") {
|
|
2584
|
+
const guess = closest(unknown, LIBRARY_IDS, .5);
|
|
2585
|
+
return `Unknown library "${unknown}". ${guess ? `Did you mean "${guess}"?` : `Expected one of: ${LIBRARY_IDS.join(", ")}.`}`;
|
|
2586
|
+
}
|
|
2587
|
+
return `"libraries" is false, to read no library, or a list of the ones to read: ${JSON.stringify(LIBRARY_IDS)}.`;
|
|
2588
|
+
} });
|
|
1757
2589
|
const configShape = {
|
|
1758
2590
|
$schema: z.string().optional(),
|
|
1759
2591
|
components: globsSchema.optional().describe("Globs for component source files, relative to the root."),
|
|
1760
2592
|
exclude: z.array(z.string()).optional().describe("Globs to ignore when finding components and when `check` expands folders and globs (stories, tests). Replaces the defaults; node_modules is skipped either way unless a pattern names it."),
|
|
1761
|
-
tokens: z.union([tokenSourceSchema, z.array(tokenSourceSchema)]).optional().describe("Token files: W3C DTCG JSON (*.json)
|
|
2593
|
+
tokens: z.union([tokenSourceSchema, z.array(tokenSourceSchema)]).optional().describe("Token files: W3C DTCG JSON (*.json), CSS custom properties (*.css) or a DESIGN.md's front matter (*.md)."),
|
|
1762
2594
|
docs: globsSchema.optional().describe("Globs for per-component Markdown/MDX docs."),
|
|
1763
2595
|
tsconfig: z.string().optional().describe("tsconfig used to resolve types and path aliases."),
|
|
1764
2596
|
importPath: z.string().optional().describe("Package name components are imported from, e.g. \"@acme/ui\", or a pattern for packages imported by path: \"@acme/ui/{path}\" is @acme/ui/primitives/button for primitives/button.tsx in the package. Default: inferred from package exports and tsconfig paths."),
|
|
1765
2597
|
elements: z.record(z.string(), z.string()).optional().describe("Extra native element → component mappings, e.g. { \"a\": \"Link\" }."),
|
|
2598
|
+
libraries: librariesSchema.optional().describe(`Published component libraries to read from their type declarations when the project depends on them. Omitted: every one onsystem reads (${LIBRARY_IDS.join(", ")}). false: none. A list: only those, e.g. ["mui"].`),
|
|
1766
2599
|
includeDesignSystem: z.boolean().optional().describe("check: lint the design system's own component files too. Off by default: they implement the scale and primitives the rules enforce."),
|
|
1767
2600
|
rules: rulesSchema.optional().describe("Rule severities and options: \"off\" | \"warn\" | \"error\" | [severity, { allow }]."),
|
|
1768
2601
|
overrides: z.array(overrideSchema).optional().describe("Rules for some files: [{ \"files\": [\"app/legacy/**\"], \"rules\": { \"no-hardcoded-color\": \"warn\" } }]. Later entries win.")
|
|
@@ -1805,6 +2638,11 @@ const DEFAULT_TOKENS = [
|
|
|
1805
2638
|
"app/app.css",
|
|
1806
2639
|
"**/*.tokens.json"
|
|
1807
2640
|
];
|
|
2641
|
+
/**
|
|
2642
|
+
* Zero config reads these only when none of the token files above exist: the tokens in a
|
|
2643
|
+
* DESIGN.md's front matter, which describes a design system to agents.
|
|
2644
|
+
*/
|
|
2645
|
+
const FALLBACK_TOKENS = ["DESIGN.md"];
|
|
1808
2646
|
const DEFAULT_DOCS = [
|
|
1809
2647
|
"docs/components/**/*.{md,mdx}",
|
|
1810
2648
|
"components/ui/**/*.{md,mdx}",
|
|
@@ -1824,18 +2662,35 @@ var ConfigError = class extends Error {
|
|
|
1824
2662
|
name = "ConfigError";
|
|
1825
2663
|
};
|
|
1826
2664
|
async function loadConfig(options = {}) {
|
|
2665
|
+
return loadConfigWith(options);
|
|
2666
|
+
}
|
|
2667
|
+
/** {@link loadConfig}, with what the caller has already found in the folder searched. */
|
|
2668
|
+
async function loadConfigWith(options, context = {}) {
|
|
1827
2669
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
1828
2670
|
const searchDir = options.root ? path.resolve(cwd, options.root) : cwd;
|
|
1829
2671
|
const deprecations = [];
|
|
2672
|
+
const warnings = [];
|
|
2673
|
+
let packages = context.packages?.root === searchDir ? context.packages : void 0;
|
|
2674
|
+
const finder = (dir) => packages?.root === dir ? packages : packages = new PackageFinder(dir);
|
|
2675
|
+
let lookup = context.lookup;
|
|
1830
2676
|
let configFile;
|
|
2677
|
+
/** The design-system package whose config this project takes on, having none of its own. */
|
|
2678
|
+
let declared;
|
|
1831
2679
|
if (options.config) configFile = path.resolve(cwd, options.config);
|
|
1832
2680
|
else {
|
|
1833
2681
|
const find = (names) => names.map((name) => path.join(searchDir, name)).find((file) => fs.existsSync(file));
|
|
1834
2682
|
configFile = find(CONFIG_FILES);
|
|
1835
2683
|
const legacy = find(LEGACY_CONFIG_FILES);
|
|
1836
2684
|
if (legacy) deprecations.push(legacyConfigNotice(legacy, configFile));
|
|
1837
|
-
configFile ??= legacy;
|
|
2685
|
+
configFile ??= legacy ?? ownDeclaredConfig(searchDir);
|
|
2686
|
+
if (!configFile) {
|
|
2687
|
+
lookup ??= findDesignSystemPackage(searchDir, finder(searchDir));
|
|
2688
|
+
if (lookup?.problems.length) throw declarationError(lookup, searchDir);
|
|
2689
|
+
declared = lookup?.found;
|
|
2690
|
+
configFile = declared?.config;
|
|
2691
|
+
}
|
|
1838
2692
|
}
|
|
2693
|
+
for (const { name, problem } of lookup?.skipped ?? []) warnings.push(`${name}'s onsystem config was skipped: ${problem} ${name} is installed in node_modules, so this project is read as if it declared none.`);
|
|
1839
2694
|
let raw = {};
|
|
1840
2695
|
if (configFile) {
|
|
1841
2696
|
if (!fs.existsSync(configFile)) throw new ConfigError(`Config file not found: ${configFile}`);
|
|
@@ -1845,16 +2700,27 @@ async function loadConfig(options = {}) {
|
|
|
1845
2700
|
const parsed = configSchema.safeParse(raw);
|
|
1846
2701
|
if (!parsed.success) throw new ConfigError(`Invalid config in ${configFile ?? "(defaults)"}:\n${z.prettifyError(parsed.error)}`);
|
|
1847
2702
|
const config = parsed.data;
|
|
1848
|
-
const root = options.root ? path.resolve(cwd, options.root) : configFile ? path.dirname(configFile) :
|
|
2703
|
+
const root = options.root ? path.resolve(cwd, options.root) : options.config && configFile ? path.dirname(configFile) : searchDir;
|
|
1849
2704
|
if (!isDirectory(root)) throw new ConfigError(`Project root not found: ${root}`);
|
|
1850
|
-
const base = options.base ? path.resolve(cwd, options.base) : root;
|
|
2705
|
+
const base = declared ? declared.dir : options.base ? path.resolve(cwd, options.base) : root;
|
|
1851
2706
|
const inherited = base !== root;
|
|
1852
2707
|
const fromBase = (p) => inherited ? toPosix(path.relative(root, path.resolve(base, slashGlob(p)))) || "." : p;
|
|
1853
2708
|
const toProject = toPosix(path.relative(base, root));
|
|
1854
2709
|
const excludeFromBase = (e) => {
|
|
1855
2710
|
if (!inherited || e.startsWith("**/")) return e;
|
|
2711
|
+
if (declared) return fromBase(e);
|
|
1856
2712
|
return e.startsWith(`${toProject}/`) ? e.slice(toProject.length + 1) : void 0;
|
|
1857
2713
|
};
|
|
2714
|
+
const outside = [];
|
|
2715
|
+
const within = (field) => (p) => {
|
|
2716
|
+
if (!declared || staysIn(declared.dir, declared.boundary, slashGlob(p))) return true;
|
|
2717
|
+
outside.push(`${field} ${p}`);
|
|
2718
|
+
return false;
|
|
2719
|
+
};
|
|
2720
|
+
const configComponents = config.components === void 0 ? void 0 : toArray(config.components).filter(within("components"));
|
|
2721
|
+
const configTokens = config.tokens === void 0 ? void 0 : toArray(config.tokens).filter((t) => within("tokens")(typeof t === "string" ? t : t.path));
|
|
2722
|
+
const configDocs = config.docs === void 0 ? void 0 : toArray(config.docs).filter(within("docs"));
|
|
2723
|
+
if (declared && outside.length) warnings.push(`${declared.name}'s onsystem config names paths outside ${declared.installed ? "the package" : "the workspace"}, which were ignored: ${outside.join(", ")}.`);
|
|
1858
2724
|
const rules = {};
|
|
1859
2725
|
for (const id of RULE_IDS) {
|
|
1860
2726
|
const setting = config.rules?.[id];
|
|
@@ -1863,18 +2729,18 @@ async function loadConfig(options = {}) {
|
|
|
1863
2729
|
options: {}
|
|
1864
2730
|
} : resolveRule(setting);
|
|
1865
2731
|
}
|
|
1866
|
-
const overrides = (config.overrides ?? []).map((override) => ({
|
|
2732
|
+
const overrides = (declared ? [] : config.overrides ?? []).map((override) => ({
|
|
1867
2733
|
files: toArray(override.files).map(slashGlob),
|
|
1868
2734
|
rules: Object.fromEntries(Object.entries(override.rules).map(([id, setting]) => [id, resolveRule(setting)]))
|
|
1869
2735
|
}));
|
|
1870
|
-
const tsconfig = config.tsconfig === void 0 ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
|
|
2736
|
+
const tsconfig = config.tsconfig === void 0 || declared ? void 0 : fromBase(config.tsconfig.replaceAll("\\", "/"));
|
|
1871
2737
|
if (tsconfig !== void 0 && !fs.existsSync(path.resolve(root, tsconfig))) throw new ConfigError(`tsconfig not found: ${path.resolve(root, tsconfig)} (from "tsconfig" in ${configFile ?? "the config"})`);
|
|
1872
2738
|
const componentsSet = Boolean(options.components?.length) || config.components !== void 0;
|
|
1873
|
-
const detection = componentsSet ? void 0 : detectProject(root, tsconfig);
|
|
2739
|
+
const detection = componentsSet ? void 0 : detectProject(root, tsconfig, void 0, declared, finder(root));
|
|
1874
2740
|
const explicitTokens = options.tokens?.length ? {
|
|
1875
2741
|
paths: options.tokens,
|
|
1876
2742
|
from: "--tokens"
|
|
1877
|
-
} : config.tokens !== void 0 ? {
|
|
2743
|
+
} : config.tokens !== void 0 && !declared ? {
|
|
1878
2744
|
paths: toArray(config.tokens).map((t) => fromBase(typeof t === "string" ? t : t.path)),
|
|
1879
2745
|
from: `"tokens" in ${configFile ?? "the config"}`
|
|
1880
2746
|
} : void 0;
|
|
@@ -1882,35 +2748,77 @@ async function loadConfig(options = {}) {
|
|
|
1882
2748
|
const absolute = path.resolve(root, slashGlob(file));
|
|
1883
2749
|
if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
|
|
1884
2750
|
}
|
|
1885
|
-
const
|
|
2751
|
+
const libraries = detectLibraries(root, config.libraries);
|
|
2752
|
+
const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens !== void 0 ? configTokens.map((t) => typeof t === "string" ? { path: fromBase(t) } : {
|
|
1886
2753
|
...t,
|
|
1887
2754
|
path: fromBase(t.path)
|
|
1888
|
-
}) : (detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p }))
|
|
2755
|
+
}) : [...(detection?.tokens.length ? [...detection.tokens, ...DEFAULT_TOKENS.filter((t) => t.endsWith(".json"))] : DEFAULT_TOKENS).map((p) => ({ path: p })), ...FALLBACK_TOKENS.map((p) => ({
|
|
2756
|
+
path: p,
|
|
2757
|
+
fallback: true
|
|
2758
|
+
}))];
|
|
2759
|
+
const exclude = (config.exclude ?? DEFAULT_EXCLUDE).map(slashGlob).map(excludeFromBase).filter((e) => e !== void 0);
|
|
1889
2760
|
return {
|
|
1890
2761
|
root,
|
|
1891
2762
|
configFile,
|
|
1892
|
-
components: (options.components?.length ? options.components :
|
|
1893
|
-
exclude:
|
|
2763
|
+
components: (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob),
|
|
2764
|
+
exclude: declared && config.exclude ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE, ...exclude])] : exclude,
|
|
2765
|
+
...declared && config.exclude ? { designSystemExclude: exclude } : {},
|
|
1894
2766
|
tokens: tokenSources.map((t) => ({
|
|
1895
2767
|
...t,
|
|
1896
2768
|
path: slashGlob(t.path)
|
|
1897
2769
|
})),
|
|
1898
|
-
docs: (options.docs?.length ? options.docs :
|
|
2770
|
+
docs: (options.docs?.length ? options.docs : configDocs === void 0 ? [.../* @__PURE__ */ new Set([...DEFAULT_DOCS, ...detection?.docs ?? []])] : configDocs.map(fromBase)).map(slashGlob),
|
|
1899
2771
|
tsconfig,
|
|
1900
2772
|
importPath: config.importPath,
|
|
1901
|
-
imports: detection?.imports ?? [],
|
|
2773
|
+
imports: detection?.imports ?? (declared && componentsSet ? packageImports(root, declared.dir) : []),
|
|
1902
2774
|
detected: detection?.source,
|
|
1903
2775
|
...detection?.designSystems ? { designSystems: detection.designSystems } : {},
|
|
1904
2776
|
tailwindConfig: findTailwindConfig(root),
|
|
2777
|
+
libraries: libraries.read,
|
|
2778
|
+
libraryIds: config.libraries,
|
|
2779
|
+
librariesOff: libraries.off,
|
|
1905
2780
|
elements: config.elements ?? {},
|
|
1906
|
-
includeDesignSystem: options.includeDesignSystem ?? config.includeDesignSystem ?? false,
|
|
2781
|
+
includeDesignSystem: options.includeDesignSystem ?? (declared ? void 0 : config.includeDesignSystem) ?? false,
|
|
1907
2782
|
rules,
|
|
1908
2783
|
overrides,
|
|
1909
|
-
...inherited ? { base } : {},
|
|
2784
|
+
...inherited && !declared ? { base } : {},
|
|
2785
|
+
...declared ? { designSystemPackage: declared } : {},
|
|
1910
2786
|
configured: componentsSet,
|
|
1911
|
-
...deprecations.length ? { deprecations } : {}
|
|
2787
|
+
...deprecations.length ? { deprecations } : {},
|
|
2788
|
+
...warnings.length ? { warnings } : {}
|
|
1912
2789
|
};
|
|
1913
2790
|
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Whether `dir` has a config of its own: a config file under its current or old name, or one its
|
|
2793
|
+
* package.json names in its `onsystem` field. Without one, a project takes on its design-system
|
|
2794
|
+
* package's config, else the workspace root's.
|
|
2795
|
+
*/
|
|
2796
|
+
function hasOwnConfig(dir) {
|
|
2797
|
+
if ([...CONFIG_FILES, ...LEGACY_CONFIG_FILES].some((name) => fs.existsSync(path.join(dir, name)))) return true;
|
|
2798
|
+
return declaredConfig(dir) !== void 0;
|
|
2799
|
+
}
|
|
2800
|
+
/** The config `dir`'s own package.json names in its `onsystem` field, as a design-system package does. */
|
|
2801
|
+
function ownDeclaredConfig(dir) {
|
|
2802
|
+
const declared = declaredConfig(dir);
|
|
2803
|
+
if (declared && "problem" in declared) throw new ConfigError(declared.problem);
|
|
2804
|
+
return declared?.file;
|
|
2805
|
+
}
|
|
2806
|
+
/** The error for workspace packages whose declaration cannot be read: their team can fix them. */
|
|
2807
|
+
function declarationError(lookup, dir) {
|
|
2808
|
+
return new ConfigError(`${lookup.problems.map((p) => `${p.name}: ${p.problem}`).join("\n")}\nFix the "${PACKAGE_FIELD}" field in that package, or give ${dir} a config of its own (onsystem.config.json), which is read instead.`);
|
|
2809
|
+
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Whether a path or glob in a dependency's config, relative to its folder `dir`, stays in
|
|
2812
|
+
* `boundary`: its static part resolves inside it, and no `..` hides among its wildcards and
|
|
2813
|
+
* braces (`{src,../..}/**`), which could reach anywhere.
|
|
2814
|
+
*/
|
|
2815
|
+
function staysIn(dir, boundary, pattern) {
|
|
2816
|
+
const base = globBase(pattern);
|
|
2817
|
+
const parts = pattern.split("/");
|
|
2818
|
+
const fixed = base === "." ? parts[0] === "." ? 1 : 0 : base.split("/").length;
|
|
2819
|
+
if (parts.slice(fixed).some((part) => part.includes(".."))) return false;
|
|
2820
|
+
return isInside(boundary, path.resolve(dir, base));
|
|
2821
|
+
}
|
|
1914
2822
|
/** What to say about a config file under its old name, with or without a new one next to it. */
|
|
1915
2823
|
function legacyConfigNotice(legacy, current) {
|
|
1916
2824
|
const name = path.basename(legacy);
|
|
@@ -2016,7 +2924,7 @@ function extractComponents(options) {
|
|
|
2016
2924
|
if (seen.has(c.declaration)) return false;
|
|
2017
2925
|
seen.add(c.declaration);
|
|
2018
2926
|
return true;
|
|
2019
|
-
}).map((candidate) => buildComponent(context, candidate));
|
|
2927
|
+
}).map((candidate) => buildComponent$1(context, candidate));
|
|
2020
2928
|
linkComposition(fileComponents);
|
|
2021
2929
|
components.push(...fileComponents);
|
|
2022
2930
|
}
|
|
@@ -2202,7 +3110,7 @@ function declarationName(declaration) {
|
|
|
2202
3110
|
if ((ts.isFunctionDeclaration(declaration) || ts.isClassDeclaration(declaration)) && declaration.name) return declaration.name.text;
|
|
2203
3111
|
if (ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name)) return declaration.name.text;
|
|
2204
3112
|
}
|
|
2205
|
-
function buildComponent(context, candidate) {
|
|
3113
|
+
function buildComponent$1(context, candidate) {
|
|
2206
3114
|
const { checker, sourceFile } = context;
|
|
2207
3115
|
const param = candidate.fn?.parameters[0];
|
|
2208
3116
|
const typeNode = candidate.propsTypeNode ?? param?.type;
|
|
@@ -2382,11 +3290,11 @@ function extractProps(context, candidate, typeNode, param) {
|
|
|
2382
3290
|
}
|
|
2383
3291
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
2384
3292
|
for (const [name, { origin }] of own) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], name]);
|
|
2385
|
-
const summarised = new Set([...byOrigin].filter(([origin, names]) => origin === "react" || origin !== "own" && names.length > MAX_LISTED_INHERITED).map(([origin]) => origin));
|
|
3293
|
+
const summarised = new Set([...byOrigin].filter(([origin, names]) => origin === "react" || origin !== "own" && names.length > MAX_LISTED_INHERITED$1).map(([origin]) => origin));
|
|
2386
3294
|
const props = [];
|
|
2387
3295
|
for (const [name, { symbol, requiredIn, origin }] of own) {
|
|
2388
3296
|
if (summarised.has(origin)) continue;
|
|
2389
|
-
props.push(describeProp(context, name, symbol, location, requiredIn === types.length, origin));
|
|
3297
|
+
props.push(describeProp$1(context, name, symbol, location, requiredIn === types.length, origin));
|
|
2390
3298
|
}
|
|
2391
3299
|
const inherits = [];
|
|
2392
3300
|
for (const origin of summarised) {
|
|
@@ -2410,7 +3318,7 @@ function extractProps(context, candidate, typeNode, param) {
|
|
|
2410
3318
|
};
|
|
2411
3319
|
}
|
|
2412
3320
|
/** Packages contributing more props than this are summarised instead of listed. */
|
|
2413
|
-
const MAX_LISTED_INHERITED = 40;
|
|
3321
|
+
const MAX_LISTED_INHERITED$1 = 40;
|
|
2414
3322
|
/** Flattens intersections and local aliases into the types they are built from. */
|
|
2415
3323
|
function constituents(context, typeNode, depth = 0) {
|
|
2416
3324
|
const { checker } = context;
|
|
@@ -2466,7 +3374,7 @@ function originOf(symbol) {
|
|
|
2466
3374
|
if (pkg === "@types/react" || pkg === "typescript") return "react";
|
|
2467
3375
|
return pkg;
|
|
2468
3376
|
}
|
|
2469
|
-
function describeProp(context, name, symbol, location, required, origin) {
|
|
3377
|
+
function describeProp$1(context, name, symbol, location, required, origin) {
|
|
2470
3378
|
const { checker } = context;
|
|
2471
3379
|
const declared = checker.getTypeOfSymbolAtLocation(symbol, location);
|
|
2472
3380
|
const type = checker.getNonNullableType(declared);
|
|
@@ -2476,7 +3384,7 @@ function describeProp(context, name, symbol, location, required, origin) {
|
|
|
2476
3384
|
required,
|
|
2477
3385
|
kind: "prop"
|
|
2478
3386
|
};
|
|
2479
|
-
const values = literalValues$
|
|
3387
|
+
const values = literalValues$2(type);
|
|
2480
3388
|
if (values) prop.values = values;
|
|
2481
3389
|
const description = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
2482
3390
|
if (description) prop.description = description;
|
|
@@ -2499,7 +3407,7 @@ function unwrapParens(text) {
|
|
|
2499
3407
|
}
|
|
2500
3408
|
return text.slice(1, -1);
|
|
2501
3409
|
}
|
|
2502
|
-
function literalValues$
|
|
3410
|
+
function literalValues$2(type) {
|
|
2503
3411
|
const members = type.isUnion() ? type.types : [type];
|
|
2504
3412
|
if (!members.every((t) => t.isStringLiteral())) return void 0;
|
|
2505
3413
|
return members.map((t) => t.value);
|
|
@@ -2639,10 +3547,10 @@ function inferElement(context, candidate, typeNode) {
|
|
|
2639
3547
|
const ref = candidate.refTypeNode?.getText();
|
|
2640
3548
|
const refElement = ref ? /^HTML(\w+)Element$/.exec(ref)?.[1] : void 0;
|
|
2641
3549
|
if (refElement) return ELEMENT_INTERFACES[refElement];
|
|
2642
|
-
return candidate.fn ? renderedElement(candidate.fn) : void 0;
|
|
3550
|
+
return candidate.fn ? renderedElement$1(candidate.fn) : void 0;
|
|
2643
3551
|
}
|
|
2644
3552
|
/** The intrinsic element returned by the render function, following `const Comp = asChild ? Slot : "button"`. */
|
|
2645
|
-
function renderedElement(fn) {
|
|
3553
|
+
function renderedElement$1(fn) {
|
|
2646
3554
|
let found;
|
|
2647
3555
|
const stringAssignments = /* @__PURE__ */ new Map();
|
|
2648
3556
|
const visit = (node) => {
|
|
@@ -2927,10 +3835,491 @@ function unquote(value) {
|
|
|
2927
3835
|
function stringValue(value) {
|
|
2928
3836
|
return typeof value === "string" && value ? value : void 0;
|
|
2929
3837
|
}
|
|
2930
|
-
function dedent(code) {
|
|
2931
|
-
const lines = code.replace(/^\n+|\s+$/g, "").split("\n");
|
|
2932
|
-
const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => /^\s*/.exec(l)?.[0].length ?? 0));
|
|
2933
|
-
return lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
|
|
3838
|
+
function dedent(code) {
|
|
3839
|
+
const lines = code.replace(/^\n+|\s+$/g, "").split("\n");
|
|
3840
|
+
const indent = Math.min(...lines.filter((l) => l.trim()).map((l) => /^\s*/.exec(l)?.[0].length ?? 0));
|
|
3841
|
+
return lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
|
|
3842
|
+
}
|
|
3843
|
+
//#endregion
|
|
3844
|
+
//#region src/libraries/declarations.ts
|
|
3845
|
+
/** Packages contributing more props than this are summarised, as in source extraction. */
|
|
3846
|
+
const MAX_LISTED_INHERITED = 40;
|
|
3847
|
+
/**
|
|
3848
|
+
* Reads a published library's components from its type declarations with the TypeScript
|
|
3849
|
+
* checker: every value its entry exports under a capitalised name whose type takes props
|
|
3850
|
+
* (function and class components, `forwardRef` and MUI's `OverridableComponent`), with the
|
|
3851
|
+
* props of each call signature, their string-literal values (custom ones the app declares by
|
|
3852
|
+
* module augmentation included), JSDoc, the element it renders, and the module that
|
|
3853
|
+
* default-exports it. A signature generic over the element to render (MUI's `component`) marks
|
|
3854
|
+
* that prop as polymorphic instead of adding its props.
|
|
3855
|
+
*/
|
|
3856
|
+
function extractLibrary(options) {
|
|
3857
|
+
const { root, adapter, dir } = options;
|
|
3858
|
+
const warnings = [];
|
|
3859
|
+
const empty = (warning) => ({
|
|
3860
|
+
components: [],
|
|
3861
|
+
propSets: {},
|
|
3862
|
+
exports: [],
|
|
3863
|
+
warnings: [warning],
|
|
3864
|
+
dependencies: []
|
|
3865
|
+
});
|
|
3866
|
+
let real;
|
|
3867
|
+
try {
|
|
3868
|
+
real = fs.realpathSync.native(dir);
|
|
3869
|
+
} catch {
|
|
3870
|
+
return empty(`${adapter.package}: ${dir} could not be read`);
|
|
3871
|
+
}
|
|
3872
|
+
const resolve = (specifier) => ts.resolveModuleName(specifier, path.join(root, "__onsystem__.tsx"), options.project.options, ts.sys).resolvedModule?.resolvedFileName;
|
|
3873
|
+
const entry = resolve(adapter.package) ?? path.join(real, typesEntry(real));
|
|
3874
|
+
if (!fs.existsSync(entry) || !/\.d\.[cm]?ts$/.test(entry)) return empty(`${adapter.package}: no type declarations at ${relativePath(root, entry)}`);
|
|
3875
|
+
const program = ts.createProgram({
|
|
3876
|
+
rootNames: [entry, ...options.augmentations],
|
|
3877
|
+
options: options.project.options
|
|
3878
|
+
});
|
|
3879
|
+
const checker = program.getTypeChecker();
|
|
3880
|
+
const entryFile = program.getSourceFile(entry);
|
|
3881
|
+
const moduleSymbol = entryFile ? checker.getSymbolAtLocation(entryFile) : void 0;
|
|
3882
|
+
if (!moduleSymbol) return empty(`${adapter.package}: ${entry} has no exports`);
|
|
3883
|
+
const shown = (file) => {
|
|
3884
|
+
const absolute = path.resolve(file);
|
|
3885
|
+
return relativePath(root, isInside(real, absolute) ? path.join(dir, path.relative(real, absolute)) : absolute);
|
|
3886
|
+
};
|
|
3887
|
+
const context = {
|
|
3888
|
+
checker,
|
|
3889
|
+
adapter,
|
|
3890
|
+
propSets: /* @__PURE__ */ new Map(),
|
|
3891
|
+
scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package
|
|
3892
|
+
};
|
|
3893
|
+
const exportNames = /* @__PURE__ */ new Set();
|
|
3894
|
+
const bySymbol = /* @__PURE__ */ new Map();
|
|
3895
|
+
const components = [];
|
|
3896
|
+
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
|
|
3897
|
+
const name = exported.getName();
|
|
3898
|
+
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
|
|
3899
|
+
if (!(symbol.flags & ts.SymbolFlags.Value)) continue;
|
|
3900
|
+
exportNames.add(name);
|
|
3901
|
+
if (!/^[A-Z]/.test(name)) continue;
|
|
3902
|
+
const known = bySymbol.get(symbol);
|
|
3903
|
+
if (known) {
|
|
3904
|
+
known.aliases.push(name);
|
|
3905
|
+
continue;
|
|
3906
|
+
}
|
|
3907
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
3908
|
+
if (!declaration) continue;
|
|
3909
|
+
const component = buildComponent(context, name, symbol);
|
|
3910
|
+
if (!component) continue;
|
|
3911
|
+
const file = declaration.getSourceFile();
|
|
3912
|
+
const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
|
|
3913
|
+
component.source = {
|
|
3914
|
+
file: shown(file.fileName),
|
|
3915
|
+
line
|
|
3916
|
+
};
|
|
3917
|
+
const defaultImport = defaultModule(program, checker, resolve, adapter.package, file, symbol);
|
|
3918
|
+
if (defaultImport) component.defaultImport = defaultImport;
|
|
3919
|
+
bySymbol.set(symbol, component);
|
|
3920
|
+
components.push(component);
|
|
3921
|
+
}
|
|
3922
|
+
linkSlots(components);
|
|
3923
|
+
return {
|
|
3924
|
+
components,
|
|
3925
|
+
propSets: Object.fromEntries(context.propSets),
|
|
3926
|
+
exports: [...exportNames].sort(),
|
|
3927
|
+
warnings,
|
|
3928
|
+
dependencies: unique(projectFiles(program))
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
/** The declaration entry of an installed package: its `types`, else `index.d.ts`. */
|
|
3932
|
+
function typesEntry(dir) {
|
|
3933
|
+
try {
|
|
3934
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
3935
|
+
const types = typeof pkg.types === "string" ? pkg.types : pkg.typings;
|
|
3936
|
+
if (typeof types === "string" && types.endsWith(".d.ts")) return types;
|
|
3937
|
+
} catch {}
|
|
3938
|
+
return "index.d.ts";
|
|
3939
|
+
}
|
|
3940
|
+
/** Call and construct signatures that take props, across a union (`React.ComponentType`). */
|
|
3941
|
+
function propSignatures(type) {
|
|
3942
|
+
return (type.isUnion() ? type.types : [type]).flatMap((member) => [...member.getCallSignatures(), ...member.getConstructSignatures()]).filter((signature) => signature.getParameters().length >= 1);
|
|
3943
|
+
}
|
|
3944
|
+
/** A React context (`ButtonGroupContext`): callable in React 19's types, but not a component to offer. */
|
|
3945
|
+
function isContext(checker, type) {
|
|
3946
|
+
return checker.getPropertyOfType(type, "Provider") !== void 0 && checker.getPropertyOfType(type, "Consumer") !== void 0;
|
|
3947
|
+
}
|
|
3948
|
+
/**
|
|
3949
|
+
* The prop a generic signature renders in the component's place (`component: RootComponent`
|
|
3950
|
+
* where `RootComponent extends React.ElementType`), which then takes that element's props.
|
|
3951
|
+
*/
|
|
3952
|
+
function polymorphicProp(checker, signature) {
|
|
3953
|
+
const parameters = signature.getTypeParameters();
|
|
3954
|
+
if (!parameters?.length) return void 0;
|
|
3955
|
+
const props = signature.getParameters()[0];
|
|
3956
|
+
if (!props) return void 0;
|
|
3957
|
+
for (const prop of checker.getPropertiesOfType(checker.getTypeOfSymbol(props))) {
|
|
3958
|
+
const type = checker.getNonNullableType(checker.getTypeOfSymbol(prop));
|
|
3959
|
+
if (!(type.flags & ts.TypeFlags.TypeParameter)) continue;
|
|
3960
|
+
const declaration = type.symbol.declarations?.find(ts.isTypeParameterDeclaration);
|
|
3961
|
+
if (parameters.includes(type) && declaration?.constraint && /\bElementType\b/.test(declaration.constraint.getText())) return {
|
|
3962
|
+
name: prop.getName(),
|
|
3963
|
+
symbol: prop
|
|
3964
|
+
};
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
function buildComponent(context, name, symbol) {
|
|
3968
|
+
const { checker, adapter } = context;
|
|
3969
|
+
const type = checker.getTypeOfSymbol(symbol);
|
|
3970
|
+
if (isContext(checker, type)) return void 0;
|
|
3971
|
+
const signatures = propSignatures(type);
|
|
3972
|
+
if (!signatures.length) return void 0;
|
|
3973
|
+
let polymorphic;
|
|
3974
|
+
const plain = [];
|
|
3975
|
+
for (const signature of signatures) {
|
|
3976
|
+
const prop = polymorphicProp(checker, signature);
|
|
3977
|
+
if (prop) polymorphic ??= prop;
|
|
3978
|
+
else plain.push(signature);
|
|
3979
|
+
}
|
|
3980
|
+
const used = plain.length ? plain : signatures;
|
|
3981
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3982
|
+
let members = 0;
|
|
3983
|
+
let openProps = false;
|
|
3984
|
+
for (const signature of used) {
|
|
3985
|
+
const param = signature.getParameters()[0];
|
|
3986
|
+
if (!param) continue;
|
|
3987
|
+
const propsType = checker.getTypeOfSymbol(param);
|
|
3988
|
+
if (propsType.flags & ts.TypeFlags.Any) {
|
|
3989
|
+
openProps = true;
|
|
3990
|
+
continue;
|
|
3991
|
+
}
|
|
3992
|
+
for (const part of propsType.isUnion() ? propsType.types : [propsType]) {
|
|
3993
|
+
members++;
|
|
3994
|
+
if (checker.getIndexInfosOfType(part).length) openProps = true;
|
|
3995
|
+
for (const prop of checker.getPropertiesOfType(part)) {
|
|
3996
|
+
const propName = prop.getName();
|
|
3997
|
+
const entry = entries.get(propName) ?? {
|
|
3998
|
+
symbols: [],
|
|
3999
|
+
required: 0,
|
|
4000
|
+
origin: originOf(prop)
|
|
4001
|
+
};
|
|
4002
|
+
entry.symbols.push(prop);
|
|
4003
|
+
if (!(prop.flags & ts.SymbolFlags.Optional)) entry.required++;
|
|
4004
|
+
entries.set(propName, entry);
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
if (!members && !openProps) return void 0;
|
|
4009
|
+
if (polymorphic && !entries.has(polymorphic.name)) entries.set(polymorphic.name, {
|
|
4010
|
+
symbols: [polymorphic.symbol],
|
|
4011
|
+
required: 0,
|
|
4012
|
+
origin: originOf(polymorphic.symbol)
|
|
4013
|
+
});
|
|
4014
|
+
const own = (origin) => origin === adapter.package || origin.startsWith(context.scope) || origin === "own";
|
|
4015
|
+
const byOrigin = /* @__PURE__ */ new Map();
|
|
4016
|
+
for (const [propName, { origin }] of entries) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], propName]);
|
|
4017
|
+
const summarised = new Set([...byOrigin].filter(([origin, names]) => !own(origin) && (origin === "react" || names.length > MAX_LISTED_INHERITED)).map(([origin]) => origin));
|
|
4018
|
+
const element = renderedElement(checker, used.at(-1));
|
|
4019
|
+
const props = [];
|
|
4020
|
+
const variants = [];
|
|
4021
|
+
for (const [propName, entry] of entries) {
|
|
4022
|
+
if (summarised.has(entry.origin)) continue;
|
|
4023
|
+
const prop = describeProp(checker, propName, entry, entry.required === members);
|
|
4024
|
+
if (propName === polymorphic?.name) prop.type = "React.ElementType";
|
|
4025
|
+
if (!own(entry.origin)) prop.description = [prop.description, `From ${entry.origin}.`].filter(Boolean).join(" ");
|
|
4026
|
+
if (prop.values && prop.values.length > 1) {
|
|
4027
|
+
prop.kind = "variant";
|
|
4028
|
+
const fallback = prop.default?.replace(/^(['"])(.*)\1$/, "$2");
|
|
4029
|
+
variants.push({
|
|
4030
|
+
name: propName,
|
|
4031
|
+
values: [...prop.values],
|
|
4032
|
+
...fallback !== void 0 && prop.values.includes(fallback) ? { default: fallback } : {},
|
|
4033
|
+
classes: {}
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
4036
|
+
props.push(prop);
|
|
4037
|
+
}
|
|
4038
|
+
props.sort((a, b) => Number(b.kind === "variant") - Number(a.kind === "variant"));
|
|
4039
|
+
const inherits = [];
|
|
4040
|
+
for (const origin of summarised) {
|
|
4041
|
+
const names = (byOrigin.get(origin) ?? []).sort();
|
|
4042
|
+
const deprecated = names.filter((propName) => entries.get(propName)?.symbols[0]?.getJsDocTags(checker).some((tag) => tag.name === "deprecated"));
|
|
4043
|
+
inherits.push({
|
|
4044
|
+
from: origin === "react" ? element ? `React.ComponentPropsWithRef<"${element}">` : "React DOM attributes" : origin,
|
|
4045
|
+
count: names.length,
|
|
4046
|
+
set: registerPropSet(context.propSets, names),
|
|
4047
|
+
...deprecated.length ? { deprecated } : {}
|
|
4048
|
+
});
|
|
4049
|
+
}
|
|
4050
|
+
const info = {
|
|
4051
|
+
name,
|
|
4052
|
+
aliases: [],
|
|
4053
|
+
importPath: adapter.package,
|
|
4054
|
+
exportName: name,
|
|
4055
|
+
source: {
|
|
4056
|
+
file: "",
|
|
4057
|
+
line: 0
|
|
4058
|
+
},
|
|
4059
|
+
subcomponents: [],
|
|
4060
|
+
props,
|
|
4061
|
+
inherits,
|
|
4062
|
+
openProps,
|
|
4063
|
+
variants,
|
|
4064
|
+
compoundVariants: [],
|
|
4065
|
+
examples: [],
|
|
4066
|
+
classNames: [],
|
|
4067
|
+
cssVars: [],
|
|
4068
|
+
library: adapter.package
|
|
4069
|
+
};
|
|
4070
|
+
if (polymorphic) info.polymorphic = polymorphic.name;
|
|
4071
|
+
if (element) info.element = element;
|
|
4072
|
+
const jsdoc = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
4073
|
+
const description = jsdoc ? adapter.describe?.(jsdoc, name) ?? jsdoc : void 0;
|
|
4074
|
+
if (description) info.description = description;
|
|
4075
|
+
for (const tag of symbol.getJsDocTags(checker)) {
|
|
4076
|
+
if (tag.name !== "deprecated") continue;
|
|
4077
|
+
info.deprecated = ts.displayPartsToString(tag.text).trim() || true;
|
|
4078
|
+
}
|
|
4079
|
+
adapter.annotate?.(info);
|
|
4080
|
+
return info;
|
|
4081
|
+
}
|
|
4082
|
+
function describeProp(checker, name, entry, required) {
|
|
4083
|
+
const [first] = entry.symbols;
|
|
4084
|
+
const declared = first ? checker.getTypeOfSymbol(first) : void 0;
|
|
4085
|
+
const printed = declared ? checker.typeToString(declared, void 0, ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope).replace(/^(?:(?:undefined|null) \| )+|(?: \| (?:undefined|null))+$/g, "") : "unknown";
|
|
4086
|
+
let values = [];
|
|
4087
|
+
for (const symbol of entry.symbols) {
|
|
4088
|
+
const found = literalValues$1(checker, checker.getTypeOfSymbol(symbol));
|
|
4089
|
+
if (!found) {
|
|
4090
|
+
values = void 0;
|
|
4091
|
+
break;
|
|
4092
|
+
}
|
|
4093
|
+
values.push(...found);
|
|
4094
|
+
}
|
|
4095
|
+
const ordered = values?.length ? declarationOrder(unique(values), entry.symbols, checker) : void 0;
|
|
4096
|
+
const prop = {
|
|
4097
|
+
name,
|
|
4098
|
+
type: ordered ? ordered.map((v) => JSON.stringify(v)).join(" | ") : truncate(unwrapParens(printed) || "unknown", 200),
|
|
4099
|
+
required,
|
|
4100
|
+
kind: "prop"
|
|
4101
|
+
};
|
|
4102
|
+
if (ordered) prop.values = ordered;
|
|
4103
|
+
const documented = entry.symbols.find((s) => s.getDocumentationComment(checker).length);
|
|
4104
|
+
const description = documented ? ts.displayPartsToString(documented.getDocumentationComment(checker)).trim() : "";
|
|
4105
|
+
if (description) prop.description = description;
|
|
4106
|
+
for (const tag of (documented ?? first)?.getJsDocTags(checker) ?? []) {
|
|
4107
|
+
const text = ts.displayPartsToString(tag.text).trim();
|
|
4108
|
+
if ((tag.name === "default" || tag.name === "defaultValue") && text) prop.default = text;
|
|
4109
|
+
if (tag.name === "deprecated") prop.deprecated = text || true;
|
|
4110
|
+
}
|
|
4111
|
+
return prop;
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* Values in the order the declaration writes them (`'small' | 'medium' | 'large'`), which the
|
|
4115
|
+
* checker does not keep; values it does not write (an app's augmentation adds them) go last.
|
|
4116
|
+
*/
|
|
4117
|
+
function declarationOrder(values, symbols, checker) {
|
|
4118
|
+
const text = symbols.map((s) => {
|
|
4119
|
+
const declaration = s.valueDeclaration ?? s.declarations?.[0];
|
|
4120
|
+
return declaration && (ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration)) && declaration.type ? typeText(checker, declaration.type, 0) : "";
|
|
4121
|
+
}).join("\n");
|
|
4122
|
+
const at = (value) => {
|
|
4123
|
+
const index = Math.max(text.indexOf(`'${value}'`), text.indexOf(`"${value}"`));
|
|
4124
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
4125
|
+
};
|
|
4126
|
+
return values.map((value, i) => ({
|
|
4127
|
+
value,
|
|
4128
|
+
at: at(value),
|
|
4129
|
+
i
|
|
4130
|
+
})).sort((a, b) => a.at - b.at || a.i - b.i).map((entry) => entry.value);
|
|
4131
|
+
}
|
|
4132
|
+
/**
|
|
4133
|
+
* A type as written, with the type parameters and aliases it names written out too
|
|
4134
|
+
* (`Variant` → `Variant extends TextFieldVariants` → `'outlined' | 'standard' | 'filled'`).
|
|
4135
|
+
*/
|
|
4136
|
+
function typeText(checker, node, depth) {
|
|
4137
|
+
let text = node.getText();
|
|
4138
|
+
if (depth > 2) return text;
|
|
4139
|
+
const visit = (child) => {
|
|
4140
|
+
if (ts.isTypeReferenceNode(child)) {
|
|
4141
|
+
const symbol = checker.getSymbolAtLocation(child.typeName);
|
|
4142
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
4143
|
+
const referenced = ts.isTypeParameterDeclaration(declaration) ? declaration.constraint : ts.isTypeAliasDeclaration(declaration) ? declaration.type : void 0;
|
|
4144
|
+
if (referenced) text += `\n${typeText(checker, referenced, depth + 1)}`;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
ts.forEachChild(child, visit);
|
|
4148
|
+
};
|
|
4149
|
+
visit(node);
|
|
4150
|
+
return text;
|
|
4151
|
+
}
|
|
4152
|
+
/**
|
|
4153
|
+
* The string literals a type allows, without `undefined` and `null`: a type parameter's
|
|
4154
|
+
* constraint (`variant?: Variant` where `Variant extends "outlined" | "filled"`). Undefined
|
|
4155
|
+
* when any member is not a string literal (`string & {}` leaves the set open).
|
|
4156
|
+
*/
|
|
4157
|
+
function literalValues$1(checker, type, depth = 0) {
|
|
4158
|
+
if (depth > 4) return void 0;
|
|
4159
|
+
const values = [];
|
|
4160
|
+
for (const member of type.isUnion() ? type.types : [type]) {
|
|
4161
|
+
if (member.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) continue;
|
|
4162
|
+
if (member.isStringLiteral()) {
|
|
4163
|
+
values.push(member.value);
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
const constraint = member.flags & ts.TypeFlags.TypeParameter ? checker.getBaseConstraintOfType(member) : void 0;
|
|
4167
|
+
const inner = constraint && constraint !== member ? literalValues$1(checker, constraint, depth + 1) : void 0;
|
|
4168
|
+
if (!inner) return void 0;
|
|
4169
|
+
values.push(...inner);
|
|
4170
|
+
}
|
|
4171
|
+
return values.length ? unique(values) : void 0;
|
|
4172
|
+
}
|
|
4173
|
+
/** The native element behind a signature, from the type of the `ref` its props take. */
|
|
4174
|
+
function renderedElement(checker, signature) {
|
|
4175
|
+
const param = signature?.getParameters()[0];
|
|
4176
|
+
if (!param) return void 0;
|
|
4177
|
+
const ref = checker.getPropertyOfType(checker.getTypeOfSymbol(param), "ref");
|
|
4178
|
+
if (!ref) return void 0;
|
|
4179
|
+
const text = checker.typeToString(checker.getTypeOfSymbol(ref));
|
|
4180
|
+
const element = /\bHTML(\w+?)Element\b/.exec(text)?.[1];
|
|
4181
|
+
return element ? ELEMENT_INTERFACES[element] : void 0;
|
|
4182
|
+
}
|
|
4183
|
+
/**
|
|
4184
|
+
* The folder module that default-exports a component (`@mui/material/Button` for
|
|
4185
|
+
* `Button/Button.d.ts`), when the package has one for it.
|
|
4186
|
+
*/
|
|
4187
|
+
function defaultModule(program, checker, resolve, name, file, symbol) {
|
|
4188
|
+
const folder = path.basename(path.dirname(file.fileName));
|
|
4189
|
+
if (!/^[A-Za-z_][\w-]*$/.test(folder)) return void 0;
|
|
4190
|
+
const resolved = resolve(`${name}/${folder}`);
|
|
4191
|
+
const index = resolved ? program.getSourceFile(resolved) : void 0;
|
|
4192
|
+
const module = index ? checker.getSymbolAtLocation(index) : void 0;
|
|
4193
|
+
const exported = module ? checker.getExportsOfModule(module).find((e) => e.getName() === "default") : void 0;
|
|
4194
|
+
if (!exported) return void 0;
|
|
4195
|
+
return (exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported) === symbol ? `${name}/${folder}` : void 0;
|
|
4196
|
+
}
|
|
4197
|
+
/**
|
|
4198
|
+
* Styled slots a library exports next to the component they belong to (`SliderThumb` in
|
|
4199
|
+
* `Slider.d.ts`) become its parts, so lists show the component and its parts under it.
|
|
4200
|
+
*/
|
|
4201
|
+
function linkSlots(components) {
|
|
4202
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
4203
|
+
for (const component of components) byFile.set(component.source.file, [...byFile.get(component.source.file) ?? [], component]);
|
|
4204
|
+
for (const group of byFile.values()) for (const component of group) {
|
|
4205
|
+
const owner = group.filter((other) => other !== component && component.name.length > other.name.length && component.name.startsWith(other.name)).sort((a, b) => b.name.length - a.name.length)[0];
|
|
4206
|
+
if (!owner) continue;
|
|
4207
|
+
component.parent = owner.name;
|
|
4208
|
+
owner.subcomponents.push(component.name);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
//#endregion
|
|
4212
|
+
//#region src/libraries/load.ts
|
|
4213
|
+
/** App sources read for module augmentations and themes, at most. */
|
|
4214
|
+
const MAX_SOURCES = 5e3;
|
|
4215
|
+
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
|
|
4216
|
+
/** Without a tsconfig, where an app's sources are not. */
|
|
4217
|
+
const NOT_SOURCES = [
|
|
4218
|
+
"**/node_modules/**",
|
|
4219
|
+
"**/dist/**",
|
|
4220
|
+
"**/build/**",
|
|
4221
|
+
"**/out/**",
|
|
4222
|
+
"**/.next/**",
|
|
4223
|
+
"**/coverage/**",
|
|
4224
|
+
"**/public/**"
|
|
4225
|
+
];
|
|
4226
|
+
/**
|
|
4227
|
+
* The components and tokens of the libraries `config` found (`config.libraries`): each one's
|
|
4228
|
+
* components from its type declarations, read together with the app's files that augment them,
|
|
4229
|
+
* and the tokens of the theme the app creates for it.
|
|
4230
|
+
*/
|
|
4231
|
+
async function loadLibraries(config) {
|
|
4232
|
+
const loaded = {
|
|
4233
|
+
components: [],
|
|
4234
|
+
propSets: {},
|
|
4235
|
+
exports: [],
|
|
4236
|
+
tokens: [],
|
|
4237
|
+
warnings: [],
|
|
4238
|
+
dependencies: []
|
|
4239
|
+
};
|
|
4240
|
+
const libraries = (config.libraries ?? []).flatMap((source) => {
|
|
4241
|
+
const adapter = libraryAdapter(source.package);
|
|
4242
|
+
return adapter ? [{
|
|
4243
|
+
source,
|
|
4244
|
+
adapter
|
|
4245
|
+
}] : [];
|
|
4246
|
+
});
|
|
4247
|
+
if (!libraries.length) return loaded;
|
|
4248
|
+
const project = readProjectConfig(config.root, config.tsconfig);
|
|
4249
|
+
const patterns = libraries.flatMap(({ adapter }) => [adapter.augments, ...adapter.theme ? [adapter.theme.matches] : []]);
|
|
4250
|
+
const sources = await appSources(config.root, project, patterns);
|
|
4251
|
+
for (const { source, adapter } of libraries) {
|
|
4252
|
+
if (!source.dir) {
|
|
4253
|
+
loaded.warnings.push(`${adapter.title} (${source.package}) is a dependency but is not installed, so its components are not known: install the dependencies.`);
|
|
4254
|
+
continue;
|
|
4255
|
+
}
|
|
4256
|
+
const augmentations = sources.filter((s) => adapter.augments.test(s.text)).map((s) => s.file);
|
|
4257
|
+
const extracted = extractLibrary({
|
|
4258
|
+
root: config.root,
|
|
4259
|
+
adapter,
|
|
4260
|
+
dir: source.dir,
|
|
4261
|
+
project,
|
|
4262
|
+
augmentations
|
|
4263
|
+
});
|
|
4264
|
+
if (adapter.theme) {
|
|
4265
|
+
const { matches } = adapter.theme;
|
|
4266
|
+
const themes = sources.filter((s) => matches.test(s.text));
|
|
4267
|
+
const read = adapter.theme.read(themes, config.root);
|
|
4268
|
+
addValues(extracted.components, read.values);
|
|
4269
|
+
loaded.tokens.push(...read.tokens);
|
|
4270
|
+
loaded.warnings.push(...read.warnings);
|
|
4271
|
+
loaded.dependencies.push(...themes.map((s) => s.file), ...read.files ?? []);
|
|
4272
|
+
}
|
|
4273
|
+
loaded.components.push(...extracted.components);
|
|
4274
|
+
Object.assign(loaded.propSets, extracted.propSets);
|
|
4275
|
+
loaded.exports.push(...extracted.exports);
|
|
4276
|
+
loaded.warnings.push(...extracted.warnings);
|
|
4277
|
+
loaded.dependencies.push(...extracted.dependencies, ...augmentations);
|
|
4278
|
+
}
|
|
4279
|
+
loaded.exports = unique(loaded.exports).sort();
|
|
4280
|
+
loaded.dependencies = unique(loaded.dependencies);
|
|
4281
|
+
return loaded;
|
|
4282
|
+
}
|
|
4283
|
+
/** Adds the values the app's theme gives props (`teal`, `dashed`) to the props that take values. */
|
|
4284
|
+
function addValues(components, values) {
|
|
4285
|
+
for (const added of values) for (const component of components) {
|
|
4286
|
+
if (added.component !== void 0 && component.name !== added.component) continue;
|
|
4287
|
+
const prop = component.props.find((p) => p.name === added.prop);
|
|
4288
|
+
if (!prop?.values || prop.values.includes(added.value)) continue;
|
|
4289
|
+
if (added.alongside !== void 0 && !prop.values.includes(added.alongside)) continue;
|
|
4290
|
+
prop.values.push(added.value);
|
|
4291
|
+
prop.type = prop.values.map((v) => JSON.stringify(v)).join(" | ");
|
|
4292
|
+
const variant = component.variants.find((v) => v.name === added.prop);
|
|
4293
|
+
if (variant && !variant.values.includes(added.value)) variant.values.push(added.value);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
/**
|
|
4297
|
+
* The app's source files whose text one of `patterns` matches, with that text: the files its
|
|
4298
|
+
* tsconfig includes, or without one the scripts under the root, at most `MAX_SOURCES`.
|
|
4299
|
+
*/
|
|
4300
|
+
async function appSources(root, project, patterns) {
|
|
4301
|
+
const included = project.fileNames.filter((file) => SOURCE_FILE.test(file) && !/[\\/]node_modules[\\/]/.test(file));
|
|
4302
|
+
const files = (included.length ? included : await glob("**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}", {
|
|
4303
|
+
cwd: root,
|
|
4304
|
+
absolute: true,
|
|
4305
|
+
ignore: NOT_SOURCES
|
|
4306
|
+
})).sort().slice(0, MAX_SOURCES);
|
|
4307
|
+
const found = [];
|
|
4308
|
+
for (let i = 0; i < files.length; i += 64) {
|
|
4309
|
+
const batch = await Promise.all(files.slice(i, i + 64).map(async (file) => {
|
|
4310
|
+
try {
|
|
4311
|
+
const text = await fsp.readFile(file, "utf8");
|
|
4312
|
+
return patterns.some((pattern) => pattern.test(text)) ? {
|
|
4313
|
+
file,
|
|
4314
|
+
text
|
|
4315
|
+
} : void 0;
|
|
4316
|
+
} catch {
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
}));
|
|
4320
|
+
for (const entry of batch) if (entry) found.push(entry);
|
|
4321
|
+
}
|
|
4322
|
+
return found;
|
|
2934
4323
|
}
|
|
2935
4324
|
//#endregion
|
|
2936
4325
|
//#region src/lint/analyze.ts
|
|
@@ -3196,6 +4585,8 @@ function resolveElement(element, analysis, target, file) {
|
|
|
3196
4585
|
if (binding?.imported === "*" && rest.length) {
|
|
3197
4586
|
if (!target.isDesignSystemImport(binding.source, file)) return { kind: "external" };
|
|
3198
4587
|
const name = rest.join(".");
|
|
4588
|
+
const library = target.libraryOf(binding.source);
|
|
4589
|
+
if (library) return fromLibrary(target, library, name, binding.source, rest.length > 1);
|
|
3199
4590
|
const component = target.components.get(name);
|
|
3200
4591
|
if (component) return {
|
|
3201
4592
|
kind: "component",
|
|
@@ -3211,7 +4602,14 @@ function resolveElement(element, analysis, target, file) {
|
|
|
3211
4602
|
}
|
|
3212
4603
|
let owner;
|
|
3213
4604
|
if (binding && analysis.declared.has(head)) return { kind: "external" };
|
|
3214
|
-
|
|
4605
|
+
const library = binding ? target.libraryOf(binding.source) : void 0;
|
|
4606
|
+
if (binding && library) {
|
|
4607
|
+
const imported = binding.imported === "default" ? target.defaultExport(binding.source, file)?.name : binding.imported;
|
|
4608
|
+
if (imported === void 0) return { kind: "external" };
|
|
4609
|
+
const resolved = fromLibrary(target, library, imported, binding.source, rest.length > 0);
|
|
4610
|
+
if (resolved.kind !== "component") return resolved;
|
|
4611
|
+
owner = resolved.component;
|
|
4612
|
+
} else if (binding) {
|
|
3215
4613
|
if (!target.isDesignSystemImport(binding.source, file)) return { kind: "external" };
|
|
3216
4614
|
const imported = binding.imported === "default" ? target.defaultExport(binding.source, file)?.name ?? head : binding.imported;
|
|
3217
4615
|
owner = target.componentFor(imported, binding.source, file);
|
|
@@ -3246,6 +4644,26 @@ function resolveElement(element, analysis, target, file) {
|
|
|
3246
4644
|
member: rest.join(".")
|
|
3247
4645
|
};
|
|
3248
4646
|
}
|
|
4647
|
+
/**
|
|
4648
|
+
* `name` from a library (`@mui/material`, or a module of it such as `@mui/material/Button`): its
|
|
4649
|
+
* own component of that name, never a wrapper the project has. A name the package exports that
|
|
4650
|
+
* is no component (a context, a styled slot it does not offer), or one imported from one of its
|
|
4651
|
+
* modules rather than the package itself, is not judged; one the package does not export at all
|
|
4652
|
+
* is invented.
|
|
4653
|
+
*/
|
|
4654
|
+
function fromLibrary(target, library, name, source, member) {
|
|
4655
|
+
const component = target.libraryComponent(name, library);
|
|
4656
|
+
if (component) return {
|
|
4657
|
+
kind: "component",
|
|
4658
|
+
component
|
|
4659
|
+
};
|
|
4660
|
+
if (source !== library || target.exports.has(name) || member) return { kind: "external" };
|
|
4661
|
+
return {
|
|
4662
|
+
kind: "missing-export",
|
|
4663
|
+
name,
|
|
4664
|
+
source
|
|
4665
|
+
};
|
|
4666
|
+
}
|
|
3249
4667
|
//#endregion
|
|
3250
4668
|
//#region src/lint/tailwind-palette.ts
|
|
3251
4669
|
/** Tailwind's default color palette: name → shade → OKLCH value. */
|
|
@@ -3606,6 +5024,23 @@ const NAMESPACES = [
|
|
|
3606
5024
|
["--font-", "typography"],
|
|
3607
5025
|
["--text-", "typography"]
|
|
3608
5026
|
];
|
|
5027
|
+
/**
|
|
5028
|
+
* A Tailwind v4 theme variable split into its namespace and key: `--color-primary` →
|
|
5029
|
+
* `--color` and `primary`, `--spacing` → `--spacing` and `''` (the spacing unit).
|
|
5030
|
+
*/
|
|
5031
|
+
function themeVariable(name) {
|
|
5032
|
+
if (name === "--spacing") return {
|
|
5033
|
+
namespace: "--spacing",
|
|
5034
|
+
key: "",
|
|
5035
|
+
category: "spacing"
|
|
5036
|
+
};
|
|
5037
|
+
const found = NAMESPACES.find(([prefix]) => name.startsWith(prefix) && name.length > prefix.length);
|
|
5038
|
+
return found && {
|
|
5039
|
+
namespace: found[0].slice(0, -1),
|
|
5040
|
+
key: name.slice(found[0].length),
|
|
5041
|
+
category: found[1]
|
|
5042
|
+
};
|
|
5043
|
+
}
|
|
3609
5044
|
const TAILWIND_DEFAULT_SPACING = "0.25rem";
|
|
3610
5045
|
/** Tailwind v4's default `--radius-*` scale; a theme overrides keys one by one. */
|
|
3611
5046
|
const TAILWIND_DEFAULT_RADIUS = {
|
|
@@ -3695,6 +5130,12 @@ function cssSheetTokens(sheets) {
|
|
|
3695
5130
|
const lookup = (name) => base.get(name)?.value ?? theme.get(name)?.value ?? tailwindColor(name);
|
|
3696
5131
|
const tokens = [];
|
|
3697
5132
|
const consumed = /* @__PURE__ */ new Set();
|
|
5133
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
5134
|
+
const add = (token, raw) => {
|
|
5135
|
+
tokens.push(token);
|
|
5136
|
+
const alias = varAlias(raw);
|
|
5137
|
+
if (alias) aliases.set(token, alias);
|
|
5138
|
+
};
|
|
3698
5139
|
for (const variable of theme.values()) {
|
|
3699
5140
|
const namespace = NAMESPACES.find(([prefix]) => variable.name.startsWith(prefix));
|
|
3700
5141
|
const isSpacingBase = variable.name === "--spacing";
|
|
@@ -3707,7 +5148,7 @@ function cssSheetTokens(sheets) {
|
|
|
3707
5148
|
const target = alias ? base.get(alias) : void 0;
|
|
3708
5149
|
if (alias && target) {
|
|
3709
5150
|
consumed.add(alias);
|
|
3710
|
-
|
|
5151
|
+
add(finalize({
|
|
3711
5152
|
name: alias.slice(2),
|
|
3712
5153
|
category,
|
|
3713
5154
|
value: resolveValue(target.value, lookup),
|
|
@@ -3718,10 +5159,10 @@ function cssSheetTokens(sheets) {
|
|
|
3718
5159
|
description: target.comment ?? variable.comment,
|
|
3719
5160
|
line: target.line,
|
|
3720
5161
|
file: target.file
|
|
3721
|
-
}));
|
|
5162
|
+
}), target.value);
|
|
3722
5163
|
continue;
|
|
3723
5164
|
}
|
|
3724
|
-
|
|
5165
|
+
add(finalize({
|
|
3725
5166
|
name: variable.name.slice(2),
|
|
3726
5167
|
category,
|
|
3727
5168
|
value: variable.value,
|
|
@@ -3732,11 +5173,11 @@ function cssSheetTokens(sheets) {
|
|
|
3732
5173
|
description: variable.comment,
|
|
3733
5174
|
line: variable.line,
|
|
3734
5175
|
file: variable.file
|
|
3735
|
-
}));
|
|
5176
|
+
}), variable.value);
|
|
3736
5177
|
}
|
|
3737
5178
|
for (const variable of base.values()) {
|
|
3738
5179
|
if (consumed.has(variable.name)) continue;
|
|
3739
|
-
|
|
5180
|
+
add(finalize({
|
|
3740
5181
|
name: variable.name.slice(2),
|
|
3741
5182
|
category: categorize$1(variable.name, resolveValue(variable.value, lookup)),
|
|
3742
5183
|
value: resolveValue(variable.value, lookup),
|
|
@@ -3745,7 +5186,12 @@ function cssSheetTokens(sheets) {
|
|
|
3745
5186
|
description: variable.comment,
|
|
3746
5187
|
line: variable.line,
|
|
3747
5188
|
file: variable.file
|
|
3748
|
-
}));
|
|
5189
|
+
}), variable.value);
|
|
5190
|
+
}
|
|
5191
|
+
const byVar = new Map(tokens.map((t) => [t.cssVar ?? `--${t.name}`, t]));
|
|
5192
|
+
for (const [token, name] of aliases) {
|
|
5193
|
+
const target = byVar.get(name);
|
|
5194
|
+
if (target && target !== token) token.aliasOf = target.name;
|
|
3749
5195
|
}
|
|
3750
5196
|
const cleared = (namespace) => resets.has("*") || resets.has(namespace);
|
|
3751
5197
|
if (tailwind && !cleared("spacing") && !theme.has("--spacing") && !base.has("--spacing")) tokens.push({
|
|
@@ -3846,6 +5292,10 @@ function categorize$1(name, value) {
|
|
|
3846
5292
|
if (parseColor(value, { bareHsl: true })) return "color";
|
|
3847
5293
|
return "other";
|
|
3848
5294
|
}
|
|
5295
|
+
/** `--x` for a value that is exactly `var(--x)`. */
|
|
5296
|
+
function varAlias(value) {
|
|
5297
|
+
return /^var\(\s*(--[\w-]+)\s*\)$/.exec(value.trim())?.[1];
|
|
5298
|
+
}
|
|
3849
5299
|
/** Follows plain `var(--x)` aliases so the token shows its concrete value. */
|
|
3850
5300
|
function resolveValue(value, lookup, depth = 0) {
|
|
3851
5301
|
const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value)?.[1];
|
|
@@ -3924,7 +5374,150 @@ function trails(comment, before) {
|
|
|
3924
5374
|
return line !== void 0 && comment.source?.start?.line === line;
|
|
3925
5375
|
}
|
|
3926
5376
|
//#endregion
|
|
5377
|
+
//#region src/tokens/design-md.ts
|
|
5378
|
+
/**
|
|
5379
|
+
* Reads the tokens in a DESIGN.md's YAML front matter (https://github.com/google-labs-code/design.md):
|
|
5380
|
+
* `colors`, `typography`, `rounded` and `spacing`. Tokens are named by their path
|
|
5381
|
+
* (`colors.primary`), which is how `{colors.primary}` references them. `components` holds
|
|
5382
|
+
* per-component styling rather than tokens to choose from, and the Markdown body is prose, so
|
|
5383
|
+
* neither is read. A DESIGN.md says nothing about how code reads its tokens, so they have no
|
|
5384
|
+
* custom property or Tailwind class: a finding names the nearest token without a fix.
|
|
5385
|
+
*/
|
|
5386
|
+
/** Front matter section → category, in the spec's order. */
|
|
5387
|
+
const SECTIONS$1 = [
|
|
5388
|
+
["colors", "color"],
|
|
5389
|
+
["typography", "typography"],
|
|
5390
|
+
["rounded", "radius"],
|
|
5391
|
+
["spacing", "spacing"]
|
|
5392
|
+
];
|
|
5393
|
+
/** A DESIGN.md dimension: a number with px, em or rem, or a bare 0. */
|
|
5394
|
+
const DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$|^0$/;
|
|
5395
|
+
function parseDesignMd(text, file) {
|
|
5396
|
+
const front = frontMatter(text);
|
|
5397
|
+
if (front === void 0) return {
|
|
5398
|
+
tokens: [],
|
|
5399
|
+
warnings: [`${file}: no YAML front matter (between --- lines at the top)`]
|
|
5400
|
+
};
|
|
5401
|
+
const lineCounter = new LineCounter();
|
|
5402
|
+
const doc = parseDocument(front.yaml, { lineCounter });
|
|
5403
|
+
const [error] = doc.errors;
|
|
5404
|
+
if (error) return {
|
|
5405
|
+
tokens: [],
|
|
5406
|
+
warnings: [`${file}: invalid YAML front matter (${error.message.split("\n")[0] ?? ""})`]
|
|
5407
|
+
};
|
|
5408
|
+
if (!isMap(doc.contents)) return {
|
|
5409
|
+
tokens: [],
|
|
5410
|
+
warnings: []
|
|
5411
|
+
};
|
|
5412
|
+
const warnings = [];
|
|
5413
|
+
const raw = [];
|
|
5414
|
+
const lineAt = (offset) => offset === void 0 ? 1 : lineCounter.linePos(offset).line + front.line;
|
|
5415
|
+
const collect = (map, path, category) => {
|
|
5416
|
+
for (const pair of map.items) {
|
|
5417
|
+
const key = isScalar(pair.key) ? scalar(pair.key.value) : void 0;
|
|
5418
|
+
if (key === void 0) continue;
|
|
5419
|
+
const at = [...path, key];
|
|
5420
|
+
const line = lineAt(isScalar(pair.key) ? pair.key.range?.[0] : void 0);
|
|
5421
|
+
const node = pair.value;
|
|
5422
|
+
if (isMap(node)) {
|
|
5423
|
+
if (category === "typography") {
|
|
5424
|
+
const value = Object.fromEntries(node.items.flatMap((p) => {
|
|
5425
|
+
const property = isScalar(p.key) ? scalar(p.key.value) : void 0;
|
|
5426
|
+
const value = isScalar(p.value) ? scalar(p.value.value) : void 0;
|
|
5427
|
+
return property === void 0 || value === void 0 ? [] : [[property, value]];
|
|
5428
|
+
}));
|
|
5429
|
+
raw.push({
|
|
5430
|
+
path: at,
|
|
5431
|
+
category,
|
|
5432
|
+
value,
|
|
5433
|
+
line
|
|
5434
|
+
});
|
|
5435
|
+
} else collect(node, at, category);
|
|
5436
|
+
continue;
|
|
5437
|
+
}
|
|
5438
|
+
const value = isScalar(node) ? scalar(node.value) : void 0;
|
|
5439
|
+
if (value === void 0) {
|
|
5440
|
+
warnings.push(`${file}: ${at.join(".")} has no value${category === "color" ? " (a # starts a YAML comment: quote hex colors)" : ""}`);
|
|
5441
|
+
continue;
|
|
5442
|
+
}
|
|
5443
|
+
const length = category === "spacing" || category === "radius";
|
|
5444
|
+
raw.push({
|
|
5445
|
+
path: at,
|
|
5446
|
+
category: length && !DIMENSION.test(value) && !isReference(value) ? "other" : category,
|
|
5447
|
+
value,
|
|
5448
|
+
line
|
|
5449
|
+
});
|
|
5450
|
+
}
|
|
5451
|
+
};
|
|
5452
|
+
for (const [section, category] of SECTIONS$1) {
|
|
5453
|
+
const node = doc.contents.get(section, true);
|
|
5454
|
+
if (isMap(node)) collect(node, [section], category);
|
|
5455
|
+
}
|
|
5456
|
+
const byPath = new Map(raw.map((t) => [t.path.join("."), t]));
|
|
5457
|
+
const resolve = (value, seen) => {
|
|
5458
|
+
const alias = reference(value);
|
|
5459
|
+
if (alias === void 0) return value;
|
|
5460
|
+
const target = byPath.get(alias);
|
|
5461
|
+
if (!target || typeof target.value !== "string") {
|
|
5462
|
+
warnings.push(`${file}: unresolved reference {${alias}}`);
|
|
5463
|
+
return value;
|
|
5464
|
+
}
|
|
5465
|
+
if (seen.has(alias)) {
|
|
5466
|
+
warnings.push(`${file}: circular reference {${alias}}`);
|
|
5467
|
+
return value;
|
|
5468
|
+
}
|
|
5469
|
+
return resolve(target.value, /* @__PURE__ */ new Set([...seen, alias]));
|
|
5470
|
+
};
|
|
5471
|
+
return {
|
|
5472
|
+
tokens: raw.map((t) => {
|
|
5473
|
+
const name = t.path.join(".");
|
|
5474
|
+
const value = typeof t.value === "string" ? resolve(t.value, /* @__PURE__ */ new Set([name])) : Object.entries(t.value).map(([k, v]) => `${k}: ${resolve(v, /* @__PURE__ */ new Set([name]))}`).join("; ");
|
|
5475
|
+
const token = {
|
|
5476
|
+
name,
|
|
5477
|
+
category: t.category,
|
|
5478
|
+
value,
|
|
5479
|
+
usage: [],
|
|
5480
|
+
source: {
|
|
5481
|
+
file,
|
|
5482
|
+
line: t.line
|
|
5483
|
+
}
|
|
5484
|
+
};
|
|
5485
|
+
const alias = typeof t.value === "string" ? reference(t.value) : void 0;
|
|
5486
|
+
if (alias && byPath.has(alias)) token.aliasOf = alias;
|
|
5487
|
+
return token;
|
|
5488
|
+
}),
|
|
5489
|
+
warnings
|
|
5490
|
+
};
|
|
5491
|
+
}
|
|
5492
|
+
/** The YAML between a first line of exactly `---` and the next such line, and the line it starts on. */
|
|
5493
|
+
function frontMatter(text) {
|
|
5494
|
+
const lines = text.replace(/^\uFEFF/, "").split(/\r?\n/);
|
|
5495
|
+
if (lines[0]?.trimEnd() !== "---") return void 0;
|
|
5496
|
+
const end = lines.findIndex((line, i) => i > 0 && line.trimEnd() === "---");
|
|
5497
|
+
if (end === -1) return void 0;
|
|
5498
|
+
return {
|
|
5499
|
+
yaml: lines.slice(1, end).join("\n"),
|
|
5500
|
+
line: 1
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
/** A YAML scalar as text: strings, numbers and booleans; null (an empty value) is none. */
|
|
5504
|
+
function scalar(value) {
|
|
5505
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : void 0;
|
|
5506
|
+
}
|
|
5507
|
+
function reference(value) {
|
|
5508
|
+
return /^\{([^{}]+)\}$/.exec(value.trim())?.[1];
|
|
5509
|
+
}
|
|
5510
|
+
function isReference(value) {
|
|
5511
|
+
return reference(value) !== void 0;
|
|
5512
|
+
}
|
|
5513
|
+
//#endregion
|
|
3927
5514
|
//#region src/tokens/dtcg.ts
|
|
5515
|
+
/**
|
|
5516
|
+
* The `$extensions` key under which onsystem keeps what DTCG has no field for: the token's
|
|
5517
|
+
* own name, the custom property code uses, the Tailwind theme variable and the values in
|
|
5518
|
+
* other modes.
|
|
5519
|
+
*/
|
|
5520
|
+
const ONSYSTEM_EXTENSION = "com.github.dgesteves.onsystem";
|
|
3928
5521
|
function parseDtcgTokens(text, file, options = {}) {
|
|
3929
5522
|
const warnings = [];
|
|
3930
5523
|
let json;
|
|
@@ -3936,13 +5529,16 @@ function parseDtcgTokens(text, file, options = {}) {
|
|
|
3936
5529
|
warnings: [`${file}: invalid JSON (${error.message})`]
|
|
3937
5530
|
};
|
|
3938
5531
|
}
|
|
3939
|
-
if (!isObject$
|
|
5532
|
+
if (!isObject$2(json)) return {
|
|
3940
5533
|
tokens: [],
|
|
3941
5534
|
warnings: [`${file}: expected a JSON object`]
|
|
3942
5535
|
};
|
|
3943
5536
|
const lines = lineIndex(text);
|
|
5537
|
+
const origins = /* @__PURE__ */ new WeakMap();
|
|
5538
|
+
recordPaths(json, [], origins);
|
|
5539
|
+
const tree = extendGroups(json, (message) => warnings.push(`${file}: ${message}`));
|
|
3944
5540
|
const raw = [];
|
|
3945
|
-
walk(
|
|
5541
|
+
walk(tree, [], void 0, raw, (node, path) => lines(origins.get(node) ?? path));
|
|
3946
5542
|
const byPath = new Map(raw.map((t) => [t.path.join("."), t]));
|
|
3947
5543
|
const resolve = (value, seen) => {
|
|
3948
5544
|
if (typeof value === "string") {
|
|
@@ -3962,7 +5558,7 @@ function parseDtcgTokens(text, file, options = {}) {
|
|
|
3962
5558
|
return value;
|
|
3963
5559
|
}
|
|
3964
5560
|
if (Array.isArray(value)) return value.map((v) => resolve(v, seen));
|
|
3965
|
-
if (isObject$
|
|
5561
|
+
if (isObject$2(value)) {
|
|
3966
5562
|
const ref = value.$ref;
|
|
3967
5563
|
if (typeof ref === "string" && Object.keys(value).length === 1) {
|
|
3968
5564
|
const pointer = ref.replace(/^#\//, "").split("/").filter((p) => p !== "$value");
|
|
@@ -3982,61 +5578,189 @@ function parseDtcgTokens(text, file, options = {}) {
|
|
|
3982
5578
|
const target = byPath.get(alias);
|
|
3983
5579
|
return target ? typeOf(target, seen) : void 0;
|
|
3984
5580
|
};
|
|
5581
|
+
const nameOf = /* @__PURE__ */ new Map();
|
|
5582
|
+
const taken = new Set(raw.filter((t) => !t.own?.name).map((t) => t.path.join(".")));
|
|
5583
|
+
for (const t of raw) {
|
|
5584
|
+
const path = t.path.join(".");
|
|
5585
|
+
const own = t.own?.name;
|
|
5586
|
+
const name = own !== void 0 && !taken.has(own) ? own : path;
|
|
5587
|
+
taken.add(name);
|
|
5588
|
+
nameOf.set(path, name);
|
|
5589
|
+
}
|
|
3985
5590
|
return {
|
|
3986
5591
|
tokens: raw.map((t) => {
|
|
3987
|
-
const
|
|
3988
|
-
const
|
|
5592
|
+
const path = t.path.join(".");
|
|
5593
|
+
const name = nameOf.get(path) ?? path;
|
|
5594
|
+
const alias = aliasTarget(t.value);
|
|
5595
|
+
const aliasOf = alias === void 0 ? void 0 : nameOf.get(alias) ?? alias;
|
|
3989
5596
|
const type = typeOf(t);
|
|
3990
|
-
const
|
|
5597
|
+
const format = (value) => {
|
|
5598
|
+
const resolved = resolve(value, /* @__PURE__ */ new Set([path]));
|
|
5599
|
+
return t.own?.channels === "hsl" && hslChannels(resolved) || formatValue(resolved, type);
|
|
5600
|
+
};
|
|
5601
|
+
const segments = [options.prefix, ...t.path.filter((p) => p !== "$root")].filter(Boolean);
|
|
5602
|
+
const generated = segments.length ? `--${segments.join("-").replace(/[^\w-]+/g, "-")}` : void 0;
|
|
5603
|
+
const theme = t.own?.tailwind ? themeVariable(t.own.tailwind) : void 0;
|
|
3991
5604
|
const token = {
|
|
3992
5605
|
name,
|
|
3993
5606
|
category: categorize(type, t.path),
|
|
3994
|
-
value,
|
|
3995
|
-
cssVar: `--${[options.prefix, ...t.path].filter(Boolean).join("-").replace(/[^\w-]+/g, "-")}`,
|
|
5607
|
+
value: format(t.value),
|
|
3996
5608
|
usage: [],
|
|
3997
5609
|
source: {
|
|
3998
5610
|
file,
|
|
3999
5611
|
line: t.line
|
|
4000
5612
|
}
|
|
4001
5613
|
};
|
|
5614
|
+
const cssVar = t.own ? t.own.cssVar : generated;
|
|
5615
|
+
if (cssVar) token.cssVar = cssVar;
|
|
5616
|
+
if (theme) token.tailwind = theme.key;
|
|
4002
5617
|
if (aliasOf) token.aliasOf = aliasOf;
|
|
4003
5618
|
if (t.description) token.description = t.description;
|
|
4004
5619
|
if (t.deprecated) token.deprecated = t.deprecated;
|
|
4005
|
-
if (t.modes) token.modes = Object.fromEntries(Object.entries(t.modes).map(([mode, v]) => [mode,
|
|
4006
|
-
token.usage = usageFor(token);
|
|
5620
|
+
if (t.modes) token.modes = Object.fromEntries(Object.entries(t.modes).map(([mode, v]) => [mode, format(v)]));
|
|
5621
|
+
token.usage = usageFor(token, theme?.namespace);
|
|
4007
5622
|
return token;
|
|
4008
5623
|
}),
|
|
4009
5624
|
warnings
|
|
4010
5625
|
};
|
|
4011
5626
|
}
|
|
4012
|
-
function walk(node, path, inheritedType, out,
|
|
5627
|
+
function walk(node, path, inheritedType, out, lineOf) {
|
|
4013
5628
|
const type = typeof node.$type === "string" ? node.$type : inheritedType;
|
|
4014
5629
|
if ("$value" in node) {
|
|
4015
5630
|
const token = {
|
|
4016
5631
|
path,
|
|
4017
5632
|
value: node.$value ?? null,
|
|
4018
|
-
line:
|
|
5633
|
+
line: lineOf(node, path)
|
|
4019
5634
|
};
|
|
4020
5635
|
if (type) token.type = type;
|
|
4021
5636
|
if (typeof node.$description === "string") token.description = node.$description;
|
|
4022
5637
|
if (node.$deprecated === true) token.deprecated = true;
|
|
4023
5638
|
else if (typeof node.$deprecated === "string") token.deprecated = node.$deprecated;
|
|
4024
|
-
const
|
|
5639
|
+
const extension = onsystemExtension(node.$extensions);
|
|
5640
|
+
const modes = extension?.modes ?? modesOf(node.$extensions);
|
|
4025
5641
|
if (modes) token.modes = modes;
|
|
5642
|
+
if (extension) {
|
|
5643
|
+
const { modes: _modes, ...own } = extension;
|
|
5644
|
+
token.own = own;
|
|
5645
|
+
}
|
|
4026
5646
|
out.push(token);
|
|
4027
5647
|
return;
|
|
4028
5648
|
}
|
|
4029
5649
|
for (const [key, child] of Object.entries(node)) {
|
|
4030
|
-
if (key.startsWith("$") || !isObject$
|
|
4031
|
-
walk(child, [...path, key], type, out,
|
|
5650
|
+
if (key.startsWith("$") && key !== "$root" || !isObject$2(child)) continue;
|
|
5651
|
+
walk(child, [...path, key], type, out, lineOf);
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
/** Where each object of the file is, by path. */
|
|
5655
|
+
function recordPaths(node, path, into) {
|
|
5656
|
+
into.set(node, path);
|
|
5657
|
+
if ("$value" in node) return;
|
|
5658
|
+
for (const [key, child] of Object.entries(node)) {
|
|
5659
|
+
if (key.startsWith("$") && key !== "$root" || !isObject$2(child)) continue;
|
|
5660
|
+
recordPaths(child, [...path, key], into);
|
|
5661
|
+
}
|
|
5662
|
+
}
|
|
5663
|
+
/**
|
|
5664
|
+
* Resolves group extension: a group with `$extends` (or JSON Schema's `$ref`, its equivalent)
|
|
5665
|
+
* takes on every token and property of the group it names, and its own win where both define
|
|
5666
|
+
* one, group by group (a token is replaced whole). A `$ref` on an object without `$value`
|
|
5667
|
+
* that points at a token makes it an alias of that token. Unresolvable and circular
|
|
5668
|
+
* references, and `$extends` that names a token, are reported and left out.
|
|
5669
|
+
*/
|
|
5670
|
+
function extendGroups(root, warn) {
|
|
5671
|
+
const done = /* @__PURE__ */ new Map();
|
|
5672
|
+
const active = /* @__PURE__ */ new Set();
|
|
5673
|
+
const expand = (node, path) => {
|
|
5674
|
+
if ("$value" in node) return node;
|
|
5675
|
+
const key = JSON.stringify(path);
|
|
5676
|
+
const cached = done.get(key);
|
|
5677
|
+
if (cached) return cached;
|
|
5678
|
+
active.add(key);
|
|
5679
|
+
const extendsRef = typeof node.$extends === "string" ? node.$extends : void 0;
|
|
5680
|
+
const ref = extendsRef ?? (typeof node.$ref === "string" ? node.$ref : void 0);
|
|
5681
|
+
const label = extendsRef === void 0 ? "$ref" : "$extends";
|
|
5682
|
+
let out = {};
|
|
5683
|
+
let result;
|
|
5684
|
+
if (ref !== void 0) {
|
|
5685
|
+
const target = referencePath(ref);
|
|
5686
|
+
const found = target && lookup(root, target);
|
|
5687
|
+
if (!target || !found) warn(`unresolved ${label} ${ref}`);
|
|
5688
|
+
else if ("$value" in found) {
|
|
5689
|
+
if (extendsRef !== void 0) warn(`$extends ${ref} names a token, not a group`);
|
|
5690
|
+
else {
|
|
5691
|
+
const { $ref: _ref, ...rest } = node;
|
|
5692
|
+
result = {
|
|
5693
|
+
...rest,
|
|
5694
|
+
$value: `{${target.join(".")}}`
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5697
|
+
} else if (active.has(JSON.stringify(target))) warn(`circular ${label} ${ref}`);
|
|
5698
|
+
else out = { ...expand(found, target) };
|
|
5699
|
+
}
|
|
5700
|
+
if (!result) {
|
|
5701
|
+
for (const [k, v] of Object.entries(node)) {
|
|
5702
|
+
if (k === "$extends" || k === "$ref" && extendsRef === void 0) continue;
|
|
5703
|
+
if (isObject$2(v) && (!k.startsWith("$") || k === "$root")) {
|
|
5704
|
+
const child = expand(v, [...path, k]);
|
|
5705
|
+
const inherited = out[k];
|
|
5706
|
+
out[k] = isGroup(inherited) && isGroup(child) ? mergeGroups(inherited, child) : child;
|
|
5707
|
+
} else out[k] = v;
|
|
5708
|
+
}
|
|
5709
|
+
result = out;
|
|
5710
|
+
}
|
|
5711
|
+
active.delete(key);
|
|
5712
|
+
done.set(key, result);
|
|
5713
|
+
return result;
|
|
5714
|
+
};
|
|
5715
|
+
return expand(root, []);
|
|
5716
|
+
}
|
|
5717
|
+
/** `base` with `own` over it: tokens and properties replaced whole, groups merged. */
|
|
5718
|
+
function mergeGroups(base, own) {
|
|
5719
|
+
const out = { ...base };
|
|
5720
|
+
for (const [key, value] of Object.entries(own)) {
|
|
5721
|
+
const inherited = out[key];
|
|
5722
|
+
out[key] = (!key.startsWith("$") || key === "$root") && isGroup(inherited) && isGroup(value) ? mergeGroups(inherited, value) : value;
|
|
4032
5723
|
}
|
|
5724
|
+
return out;
|
|
5725
|
+
}
|
|
5726
|
+
/** `{button}` or `#/button` → `["button"]`; a pointer's trailing `$value` is the token itself. */
|
|
5727
|
+
function referencePath(ref) {
|
|
5728
|
+
const curly = /^\{([^{}]+)\}$/.exec(ref.trim())?.[1];
|
|
5729
|
+
if (curly) return curly.split(".");
|
|
5730
|
+
if (!ref.startsWith("#/")) return void 0;
|
|
5731
|
+
const parts = ref.slice(2).split("/").map((p) => p.replaceAll("~1", "/").replaceAll("~0", "~"));
|
|
5732
|
+
if (parts.at(-1) === "$value") parts.pop();
|
|
5733
|
+
return parts.length ? parts : void 0;
|
|
5734
|
+
}
|
|
5735
|
+
function lookup(root, path) {
|
|
5736
|
+
let node = root;
|
|
5737
|
+
for (const key of path) {
|
|
5738
|
+
if (!isObject$2(node)) return void 0;
|
|
5739
|
+
node = node[key];
|
|
5740
|
+
}
|
|
5741
|
+
return isObject$2(node) ? node : void 0;
|
|
5742
|
+
}
|
|
5743
|
+
function isGroup(value) {
|
|
5744
|
+
return isObject$2(value) && !("$value" in value);
|
|
5745
|
+
}
|
|
5746
|
+
/** The fields onsystem reads from its own `$extensions` entry, when they are well formed. */
|
|
5747
|
+
function onsystemExtension(extensions) {
|
|
5748
|
+
const own = isObject$2(extensions) ? extensions[ONSYSTEM_EXTENSION] : void 0;
|
|
5749
|
+
if (!isObject$2(own)) return void 0;
|
|
5750
|
+
const out = {};
|
|
5751
|
+
if (typeof own.name === "string" && /^[^\s{}]+$/.test(own.name)) out.name = own.name;
|
|
5752
|
+
if (typeof own.cssVar === "string" && /^--[\w-]+$/.test(own.cssVar)) out.cssVar = own.cssVar;
|
|
5753
|
+
if (typeof own.tailwind === "string" && themeVariable(own.tailwind)) out.tailwind = own.tailwind;
|
|
5754
|
+
if (own.channels === "hsl") out.channels = "hsl";
|
|
5755
|
+
if (isObject$2(own.modes)) out.modes = own.modes;
|
|
5756
|
+
return out;
|
|
4033
5757
|
}
|
|
4034
5758
|
/** Modes are not standardised yet; accept the common `$extensions.mode(s)` shape. */
|
|
4035
5759
|
function modesOf(extensions) {
|
|
4036
|
-
if (!isObject$
|
|
5760
|
+
if (!isObject$2(extensions)) return void 0;
|
|
4037
5761
|
for (const key of ["modes", "mode"]) {
|
|
4038
5762
|
const value = extensions[key];
|
|
4039
|
-
if (isObject$
|
|
5763
|
+
if (isObject$2(value)) return value;
|
|
4040
5764
|
}
|
|
4041
5765
|
}
|
|
4042
5766
|
function categorize(type, path) {
|
|
@@ -4060,6 +5784,13 @@ function categorize(type, path) {
|
|
|
4060
5784
|
return "other";
|
|
4061
5785
|
}
|
|
4062
5786
|
}
|
|
5787
|
+
/** `214.3 31.8% 91.4%`: an HSL color as the bare channels a Tailwind v3 custom property holds. */
|
|
5788
|
+
function hslChannels(value) {
|
|
5789
|
+
if (!isObject$2(value) || value.colorSpace !== "hsl" || !Array.isArray(value.components)) return;
|
|
5790
|
+
const [h, s, l] = value.components.map((c) => formatValue(c, void 0));
|
|
5791
|
+
if (h === void 0 || s === void 0 || l === void 0) return void 0;
|
|
5792
|
+
return `${h} ${s}% ${l}%${typeof value.alpha === "number" && value.alpha < 1 ? ` / ${value.alpha}` : ""}`;
|
|
5793
|
+
}
|
|
4063
5794
|
function formatValue(value, type) {
|
|
4064
5795
|
if (typeof value === "string") return value;
|
|
4065
5796
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
@@ -4074,6 +5805,7 @@ function formatValue(value, type) {
|
|
|
4074
5805
|
if (type === "shadow") {
|
|
4075
5806
|
const s = value;
|
|
4076
5807
|
return [
|
|
5808
|
+
s.inset === true ? "inset" : void 0,
|
|
4077
5809
|
s.offsetX,
|
|
4078
5810
|
s.offsetY,
|
|
4079
5811
|
s.blur,
|
|
@@ -4083,7 +5815,7 @@ function formatValue(value, type) {
|
|
|
4083
5815
|
}
|
|
4084
5816
|
return Object.entries(value).map(([k, v]) => `${k}: ${formatValue(v, void 0)}`).join("; ");
|
|
4085
5817
|
}
|
|
4086
|
-
const COLOR_SPACES = {
|
|
5818
|
+
const COLOR_SPACES$1 = {
|
|
4087
5819
|
srgb: ([r, g, b]) => `rgb(${pct255(r)} ${pct255(g)} ${pct255(b)}`,
|
|
4088
5820
|
hsl: ([h, s, l]) => `hsl(${h} ${s}% ${l}%`,
|
|
4089
5821
|
hwb: ([h, w, b]) => `hwb(${h} ${w}% ${b}%`,
|
|
@@ -4097,14 +5829,14 @@ function formatColor(value) {
|
|
|
4097
5829
|
if (typeof value.hex === "string" && alpha === void 0) return value.hex;
|
|
4098
5830
|
const space = typeof value.colorSpace === "string" ? value.colorSpace : "srgb";
|
|
4099
5831
|
const components = Array.isArray(value.components) ? value.components.map((c) => formatValue(c, void 0)) : [];
|
|
4100
|
-
const make = COLOR_SPACES[space];
|
|
5832
|
+
const make = COLOR_SPACES$1[space];
|
|
4101
5833
|
return `${make ? make(components) : `color(${space} ${components.join(" ")}`}${alpha === void 0 ? "" : ` / ${alpha}`})`;
|
|
4102
5834
|
}
|
|
4103
5835
|
function pct255(component) {
|
|
4104
5836
|
if (component === void 0 || component === "none") return "none";
|
|
4105
5837
|
return String(Math.round(Number(component) * 255));
|
|
4106
5838
|
}
|
|
4107
|
-
function isObject$
|
|
5839
|
+
function isObject$2(value) {
|
|
4108
5840
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4109
5841
|
}
|
|
4110
5842
|
/**
|
|
@@ -4212,7 +5944,7 @@ function scopedFamilies(tokens, filesUsing) {
|
|
|
4212
5944
|
const family = tokenFamily(token);
|
|
4213
5945
|
if (!family || scoped.has(family)) continue;
|
|
4214
5946
|
if (NAMED_SCOPES.has(family)) scoped.add(family);
|
|
4215
|
-
else if (filesUsing && !CORE_FAMILIES.has(family) && !STATUS_WORDS.has(family) && (filesUsing.get(family)?.size ?? 0) <= 1) scoped.add(family);
|
|
5947
|
+
else if (filesUsing && token.usage.length > 0 && !CORE_FAMILIES.has(family) && !STATUS_WORDS.has(family) && (filesUsing.get(family)?.size ?? 0) <= 1) scoped.add(family);
|
|
4216
5948
|
}
|
|
4217
5949
|
return scoped;
|
|
4218
5950
|
}
|
|
@@ -4270,165 +6002,774 @@ const KIND_WORDS = [
|
|
|
4270
6002
|
]]
|
|
4271
6003
|
];
|
|
4272
6004
|
/**
|
|
4273
|
-
* The name that says what a token is for: a sidebar's own palette mirrors the
|
|
4274
|
-
* core one, so `sidebar-accent` is an `accent` and `sidebar-foreground` a
|
|
4275
|
-
* `foreground`. shadcn/ui's sidebar surface is `--sidebar` (v4) or
|
|
4276
|
-
* `--sidebar-background` (v3).
|
|
6005
|
+
* The name that says what a token is for: a sidebar's own palette mirrors the
|
|
6006
|
+
* core one, so `sidebar-accent` is an `accent` and `sidebar-foreground` a
|
|
6007
|
+
* `foreground`. shadcn/ui's sidebar surface is `--sidebar` (v4) or
|
|
6008
|
+
* `--sidebar-background` (v3).
|
|
6009
|
+
*/
|
|
6010
|
+
function roleKey(token) {
|
|
6011
|
+
const key = tokenKey(token);
|
|
6012
|
+
return key === "sidebar" ? "background" : key.replace(/^sidebar-/, "");
|
|
6013
|
+
}
|
|
6014
|
+
/** What a token is for, from its name: shadcn/ui's conventions and common DTCG names. */
|
|
6015
|
+
function kindOf(key) {
|
|
6016
|
+
const segments = key.split("-");
|
|
6017
|
+
for (const [kind, words] of KIND_WORDS) if (segments.some((s) => words.includes(s))) return kind;
|
|
6018
|
+
return "other";
|
|
6019
|
+
}
|
|
6020
|
+
/** Tokens made for each role, in the order to prefer them when their values tie. */
|
|
6021
|
+
const PREFERRED = {
|
|
6022
|
+
text: ["foreground", "muted-foreground"],
|
|
6023
|
+
surface: [
|
|
6024
|
+
"background",
|
|
6025
|
+
"muted",
|
|
6026
|
+
"secondary",
|
|
6027
|
+
"accent",
|
|
6028
|
+
"card",
|
|
6029
|
+
"popover"
|
|
6030
|
+
],
|
|
6031
|
+
line: [
|
|
6032
|
+
"border",
|
|
6033
|
+
"input",
|
|
6034
|
+
"ring"
|
|
6035
|
+
]
|
|
6036
|
+
};
|
|
6037
|
+
/** Plain text colors, as opposed to the text that goes on one surface (`card-foreground`). */
|
|
6038
|
+
const GENERIC_TEXT = /^(?:muted-foreground|(?:foreground|text|fg)(?:-(?:default|base|primary|secondary|muted|subtle))?)$/;
|
|
6039
|
+
function roleFit(token, role) {
|
|
6040
|
+
if (!role) return {
|
|
6041
|
+
tier: 1,
|
|
6042
|
+
rank: 0
|
|
6043
|
+
};
|
|
6044
|
+
const key = roleKey(token);
|
|
6045
|
+
const index = PREFERRED[role].indexOf(key);
|
|
6046
|
+
const rank = index === -1 ? PREFERRED[role].length : index;
|
|
6047
|
+
const kind = kindOf(key);
|
|
6048
|
+
switch (role) {
|
|
6049
|
+
case "text":
|
|
6050
|
+
if (kind === "foreground") return {
|
|
6051
|
+
tier: GENERIC_TEXT.test(key) ? 0 : 1,
|
|
6052
|
+
rank
|
|
6053
|
+
};
|
|
6054
|
+
return {
|
|
6055
|
+
tier: kind === "other" ? 1 : 2,
|
|
6056
|
+
rank
|
|
6057
|
+
};
|
|
6058
|
+
case "surface": return {
|
|
6059
|
+
tier: kind === "surface" ? 0 : kind === "foreground" ? 2 : 1,
|
|
6060
|
+
rank
|
|
6061
|
+
};
|
|
6062
|
+
case "line": return {
|
|
6063
|
+
tier: kind === "line" ? 0 : kind === "other" ? 1 : 2,
|
|
6064
|
+
rank
|
|
6065
|
+
};
|
|
6066
|
+
}
|
|
6067
|
+
}
|
|
6068
|
+
//#endregion
|
|
6069
|
+
//#region src/types.ts
|
|
6070
|
+
/**
|
|
6071
|
+
* The design-system model the server exposes. Everything here is plain JSON so
|
|
6072
|
+
* it can be cached on disk and returned as MCP structured content.
|
|
6073
|
+
*/
|
|
6074
|
+
const TOKEN_CATEGORIES = [
|
|
6075
|
+
"color",
|
|
6076
|
+
"spacing",
|
|
6077
|
+
"radius",
|
|
6078
|
+
"typography",
|
|
6079
|
+
"shadow",
|
|
6080
|
+
"other"
|
|
6081
|
+
];
|
|
6082
|
+
//#endregion
|
|
6083
|
+
//#region src/tokens/export.ts
|
|
6084
|
+
/** Group names a DTCG or DESIGN.md token path may already start with, per category. */
|
|
6085
|
+
const GROUP_NAMES = {
|
|
6086
|
+
color: [
|
|
6087
|
+
"color",
|
|
6088
|
+
"colors",
|
|
6089
|
+
"colour",
|
|
6090
|
+
"colours"
|
|
6091
|
+
],
|
|
6092
|
+
spacing: ["spacing", "space"],
|
|
6093
|
+
radius: [
|
|
6094
|
+
"radius",
|
|
6095
|
+
"radii",
|
|
6096
|
+
"rounded"
|
|
6097
|
+
],
|
|
6098
|
+
typography: [
|
|
6099
|
+
"typography",
|
|
6100
|
+
"font",
|
|
6101
|
+
"fonts"
|
|
6102
|
+
],
|
|
6103
|
+
shadow: [
|
|
6104
|
+
"shadow",
|
|
6105
|
+
"shadows",
|
|
6106
|
+
"elevation"
|
|
6107
|
+
],
|
|
6108
|
+
other: []
|
|
6109
|
+
};
|
|
6110
|
+
/**
|
|
6111
|
+
* The tokens as a DTCG 2025.10 file: a group per category, colors as color objects in their
|
|
6112
|
+
* own color space, lengths as `{ value, unit }`, aliases as references, and what DTCG has no
|
|
6113
|
+
* field for (the token's name, the custom property, the Tailwind theme variable, dark and
|
|
6114
|
+
* other modes) under `$extensions["com.github.dgesteves.onsystem"]`.
|
|
6115
|
+
*/
|
|
6116
|
+
function toDtcg(tokens) {
|
|
6117
|
+
const warnings = [];
|
|
6118
|
+
const paths = assignPaths(tokens, warnings);
|
|
6119
|
+
const lookup = varLookup(tokens);
|
|
6120
|
+
const byName = new Map(tokens.map((t) => [t.name, t]));
|
|
6121
|
+
const tree = {};
|
|
6122
|
+
let count = 0;
|
|
6123
|
+
for (const category of TOKEN_CATEGORIES) for (const token of tokens) {
|
|
6124
|
+
const path = token.category === category ? paths.get(token) : void 0;
|
|
6125
|
+
if (!path) continue;
|
|
6126
|
+
const own = typed(token.value, token, lookup);
|
|
6127
|
+
const target = token.aliasOf ? byName.get(token.aliasOf) : void 0;
|
|
6128
|
+
const targetPath = target ? paths.get(target) : void 0;
|
|
6129
|
+
const node = {};
|
|
6130
|
+
const type = target && targetPath ? typed(target.value, target, lookup).type : own.type;
|
|
6131
|
+
if (type) node.$type = type;
|
|
6132
|
+
node.$value = targetPath ? `{${targetPath.join(".")}}` : own.value;
|
|
6133
|
+
if (token.description) node.$description = token.description;
|
|
6134
|
+
if (token.deprecated) node.$deprecated = token.deprecated;
|
|
6135
|
+
const extension = {};
|
|
6136
|
+
if (token.name !== path.join(".")) extension.name = token.name;
|
|
6137
|
+
if (token.cssVar) extension.cssVar = token.cssVar;
|
|
6138
|
+
const tailwind = tailwindVariable(token);
|
|
6139
|
+
if (tailwind) extension.tailwind = tailwind;
|
|
6140
|
+
if (own.channels) extension.channels = "hsl";
|
|
6141
|
+
if (token.modes && Object.keys(token.modes).length) extension.modes = Object.fromEntries(Object.entries(token.modes).map(([mode, value]) => [mode, typed(value, token, lookup).value]));
|
|
6142
|
+
if (Object.keys(extension).length) node.$extensions = { [ONSYSTEM_EXTENSION]: extension };
|
|
6143
|
+
insert(tree, path, node);
|
|
6144
|
+
count++;
|
|
6145
|
+
}
|
|
6146
|
+
return {
|
|
6147
|
+
text: `${JSON.stringify(tree, null, 2).replace(/\[\n\s*([^[\]{}]*?)\n\s*\]/g, (_, items) => `[${items.split(/,\n\s*/).join(", ")}]`)}\n`,
|
|
6148
|
+
count,
|
|
6149
|
+
warnings
|
|
6150
|
+
};
|
|
6151
|
+
}
|
|
6152
|
+
/**
|
|
6153
|
+
* Each token's path: its category, then its name without that category (`primary` →
|
|
6154
|
+
* `color.primary`, `radius-md` → `radius.md`, `color.brand.500` as it is). The spacing unit,
|
|
6155
|
+
* named like its group, is the group's `$root`; so is a token whose path is also a group.
|
|
6156
|
+
*/
|
|
6157
|
+
function assignPaths(tokens, warnings) {
|
|
6158
|
+
const out = /* @__PURE__ */ new Map();
|
|
6159
|
+
const taken = /* @__PURE__ */ new Set();
|
|
6160
|
+
const key = (path) => JSON.stringify(path);
|
|
6161
|
+
for (const token of tokens) {
|
|
6162
|
+
const path = [[token.category, ...leaf(token)], [token.category, ...token.name.split(".")]].find((p) => !taken.has(key(p)));
|
|
6163
|
+
if (!path) {
|
|
6164
|
+
warnings.push(`${token.name} is left out: another token has the same path`);
|
|
6165
|
+
continue;
|
|
6166
|
+
}
|
|
6167
|
+
taken.add(key(path));
|
|
6168
|
+
out.set(token, path);
|
|
6169
|
+
}
|
|
6170
|
+
const groups = /* @__PURE__ */ new Set();
|
|
6171
|
+
for (const path of out.values()) for (let i = 1; i < path.length; i++) groups.add(key(path.slice(0, i)));
|
|
6172
|
+
for (const [token, path] of out) {
|
|
6173
|
+
if (!groups.has(key(path))) continue;
|
|
6174
|
+
const root = [...path, "$root"];
|
|
6175
|
+
if (taken.has(key(root))) {
|
|
6176
|
+
warnings.push(`${token.name} is left out: another token has the same path`);
|
|
6177
|
+
out.delete(token);
|
|
6178
|
+
continue;
|
|
6179
|
+
}
|
|
6180
|
+
taken.add(key(root));
|
|
6181
|
+
out.set(token, root);
|
|
6182
|
+
}
|
|
6183
|
+
return out;
|
|
6184
|
+
}
|
|
6185
|
+
/** A token's path within its category group. */
|
|
6186
|
+
function leaf(token) {
|
|
6187
|
+
const group = token.category;
|
|
6188
|
+
if (token.name.includes(".")) {
|
|
6189
|
+
const parts = token.name.split(".");
|
|
6190
|
+
const [first = ""] = parts;
|
|
6191
|
+
return parts.length > 1 && GROUP_NAMES[group].includes(first.toLowerCase()) ? parts.slice(1) : parts;
|
|
6192
|
+
}
|
|
6193
|
+
if (token.name.startsWith(`${group}-`)) return [token.name.slice(group.length + 1)];
|
|
6194
|
+
if (token.name !== group) return [token.name];
|
|
6195
|
+
return [token.tailwind || "$root"];
|
|
6196
|
+
}
|
|
6197
|
+
function insert(tree, path, node) {
|
|
6198
|
+
let group = tree;
|
|
6199
|
+
for (const key of path.slice(0, -1)) {
|
|
6200
|
+
const next = group[key];
|
|
6201
|
+
if (isObject$1(next)) group = next;
|
|
6202
|
+
else {
|
|
6203
|
+
const created = {};
|
|
6204
|
+
group[key] = created;
|
|
6205
|
+
group = created;
|
|
6206
|
+
}
|
|
6207
|
+
}
|
|
6208
|
+
group[path.at(-1) ?? ""] = node;
|
|
6209
|
+
}
|
|
6210
|
+
/**
|
|
6211
|
+
* The Tailwind theme variable a token is exposed through: `--color-primary` for `bg-primary`,
|
|
6212
|
+
* `--spacing` for the spacing unit. Tailwind v3 colors come from its config, and read the same.
|
|
6213
|
+
*/
|
|
6214
|
+
function tailwindVariable(token) {
|
|
6215
|
+
const key = token.tailwind;
|
|
6216
|
+
if (key === void 0) return void 0;
|
|
6217
|
+
const utility = token.usage.find((u) => !u.includes("(")) ?? "";
|
|
6218
|
+
switch (token.category) {
|
|
6219
|
+
case "color": return `--color-${key}`;
|
|
6220
|
+
case "spacing": return key ? `--spacing-${key}` : "--spacing";
|
|
6221
|
+
case "radius": return `--radius-${key}`;
|
|
6222
|
+
case "shadow": return `--${/^(inset-shadow|drop-shadow|text-shadow|shadow)-/.exec(utility)?.[1] ?? "shadow"}-${key}`;
|
|
6223
|
+
case "typography": {
|
|
6224
|
+
const kind = typographyKind(token);
|
|
6225
|
+
if (kind === "fontWeight") return `--font-weight-${key}`;
|
|
6226
|
+
if (kind === "fontFamily") return `--font-${key}`;
|
|
6227
|
+
if (kind === "lineHeight") return `--leading-${key}`;
|
|
6228
|
+
if (kind === "letterSpacing") return `--tracking-${key}`;
|
|
6229
|
+
if (kind === "fontSize") return `--text-${key}`;
|
|
6230
|
+
return;
|
|
6231
|
+
}
|
|
6232
|
+
default: return;
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
/** Custom property → value, for lengths that read another token (`calc(var(--radius) - 4px)`). */
|
|
6236
|
+
function varLookup(tokens) {
|
|
6237
|
+
const values = /* @__PURE__ */ new Map();
|
|
6238
|
+
for (const token of tokens) values.set(token.cssVar ?? `--${token.name}`, token.value);
|
|
6239
|
+
return (name) => values.get(name);
|
|
6240
|
+
}
|
|
6241
|
+
/**
|
|
6242
|
+
* A CSS value in DTCG form. Values DTCG has no type for (`color-mix()`, `var()` without a
|
|
6243
|
+
* token, lengths in `em` or `%`) stay strings without `$type`, which onsystem reads back as is.
|
|
6244
|
+
*/
|
|
6245
|
+
function typed(value, token, lookup) {
|
|
6246
|
+
switch (token.category) {
|
|
6247
|
+
case "color": return colorValue(value);
|
|
6248
|
+
case "spacing":
|
|
6249
|
+
case "radius": return dimensionValue(value, lookup) ?? { value };
|
|
6250
|
+
case "typography": return typographyValue(value, token) ?? { value };
|
|
6251
|
+
case "shadow": {
|
|
6252
|
+
const shadow = shadowValue(value);
|
|
6253
|
+
return shadow ? {
|
|
6254
|
+
type: "shadow",
|
|
6255
|
+
value: shadow
|
|
6256
|
+
} : { value };
|
|
6257
|
+
}
|
|
6258
|
+
default: return { value };
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
function colorValue(value) {
|
|
6262
|
+
if (!parseColor(value, { bareHsl: true })) return { value };
|
|
6263
|
+
const bare = isBareHsl(value);
|
|
6264
|
+
const color = colorObject(bare ? `hsl(${value.replace(/,/g, " ")})` : value);
|
|
6265
|
+
if (!color) return { value };
|
|
6266
|
+
return bare ? {
|
|
6267
|
+
type: "color",
|
|
6268
|
+
value: color,
|
|
6269
|
+
channels: true
|
|
6270
|
+
} : {
|
|
6271
|
+
type: "color",
|
|
6272
|
+
value: color
|
|
6273
|
+
};
|
|
6274
|
+
}
|
|
6275
|
+
/** culori mode → DTCG color space, its channels, and the factor from culori's range to DTCG's. */
|
|
6276
|
+
const COLOR_SPACES = {
|
|
6277
|
+
rgb: {
|
|
6278
|
+
space: "srgb",
|
|
6279
|
+
channels: [
|
|
6280
|
+
"r",
|
|
6281
|
+
"g",
|
|
6282
|
+
"b"
|
|
6283
|
+
]
|
|
6284
|
+
},
|
|
6285
|
+
lrgb: {
|
|
6286
|
+
space: "srgb-linear",
|
|
6287
|
+
channels: [
|
|
6288
|
+
"r",
|
|
6289
|
+
"g",
|
|
6290
|
+
"b"
|
|
6291
|
+
]
|
|
6292
|
+
},
|
|
6293
|
+
hsl: {
|
|
6294
|
+
space: "hsl",
|
|
6295
|
+
channels: [
|
|
6296
|
+
"h",
|
|
6297
|
+
"s",
|
|
6298
|
+
"l"
|
|
6299
|
+
],
|
|
6300
|
+
scale: [
|
|
6301
|
+
1,
|
|
6302
|
+
100,
|
|
6303
|
+
100
|
|
6304
|
+
]
|
|
6305
|
+
},
|
|
6306
|
+
hwb: {
|
|
6307
|
+
space: "hwb",
|
|
6308
|
+
channels: [
|
|
6309
|
+
"h",
|
|
6310
|
+
"w",
|
|
6311
|
+
"b"
|
|
6312
|
+
],
|
|
6313
|
+
scale: [
|
|
6314
|
+
1,
|
|
6315
|
+
100,
|
|
6316
|
+
100
|
|
6317
|
+
]
|
|
6318
|
+
},
|
|
6319
|
+
lab: {
|
|
6320
|
+
space: "lab",
|
|
6321
|
+
channels: [
|
|
6322
|
+
"l",
|
|
6323
|
+
"a",
|
|
6324
|
+
"b"
|
|
6325
|
+
]
|
|
6326
|
+
},
|
|
6327
|
+
lch: {
|
|
6328
|
+
space: "lch",
|
|
6329
|
+
channels: [
|
|
6330
|
+
"l",
|
|
6331
|
+
"c",
|
|
6332
|
+
"h"
|
|
6333
|
+
]
|
|
6334
|
+
},
|
|
6335
|
+
oklab: {
|
|
6336
|
+
space: "oklab",
|
|
6337
|
+
channels: [
|
|
6338
|
+
"l",
|
|
6339
|
+
"a",
|
|
6340
|
+
"b"
|
|
6341
|
+
]
|
|
6342
|
+
},
|
|
6343
|
+
oklch: {
|
|
6344
|
+
space: "oklch",
|
|
6345
|
+
channels: [
|
|
6346
|
+
"l",
|
|
6347
|
+
"c",
|
|
6348
|
+
"h"
|
|
6349
|
+
]
|
|
6350
|
+
},
|
|
6351
|
+
p3: {
|
|
6352
|
+
space: "display-p3",
|
|
6353
|
+
channels: [
|
|
6354
|
+
"r",
|
|
6355
|
+
"g",
|
|
6356
|
+
"b"
|
|
6357
|
+
]
|
|
6358
|
+
},
|
|
6359
|
+
a98: {
|
|
6360
|
+
space: "a98-rgb",
|
|
6361
|
+
channels: [
|
|
6362
|
+
"r",
|
|
6363
|
+
"g",
|
|
6364
|
+
"b"
|
|
6365
|
+
]
|
|
6366
|
+
},
|
|
6367
|
+
prophoto: {
|
|
6368
|
+
space: "prophoto-rgb",
|
|
6369
|
+
channels: [
|
|
6370
|
+
"r",
|
|
6371
|
+
"g",
|
|
6372
|
+
"b"
|
|
6373
|
+
]
|
|
6374
|
+
},
|
|
6375
|
+
rec2020: {
|
|
6376
|
+
space: "rec2020",
|
|
6377
|
+
channels: [
|
|
6378
|
+
"r",
|
|
6379
|
+
"g",
|
|
6380
|
+
"b"
|
|
6381
|
+
]
|
|
6382
|
+
},
|
|
6383
|
+
xyz65: {
|
|
6384
|
+
space: "xyz-d65",
|
|
6385
|
+
channels: [
|
|
6386
|
+
"x",
|
|
6387
|
+
"y",
|
|
6388
|
+
"z"
|
|
6389
|
+
]
|
|
6390
|
+
},
|
|
6391
|
+
xyz50: {
|
|
6392
|
+
space: "xyz-d50",
|
|
6393
|
+
channels: [
|
|
6394
|
+
"x",
|
|
6395
|
+
"y",
|
|
6396
|
+
"z"
|
|
6397
|
+
]
|
|
6398
|
+
}
|
|
6399
|
+
};
|
|
6400
|
+
const toOklch = converter("oklch");
|
|
6401
|
+
/**
|
|
6402
|
+
* A CSS color as a DTCG color object, in the color space it is written in (others in OKLCH).
|
|
6403
|
+
* An 8-bit sRGB color also gets its `hex`.
|
|
4277
6404
|
*/
|
|
4278
|
-
function
|
|
4279
|
-
const
|
|
4280
|
-
|
|
6405
|
+
function colorObject(css) {
|
|
6406
|
+
const parsed = parse(css);
|
|
6407
|
+
if (!parsed) return void 0;
|
|
6408
|
+
const known = COLOR_SPACES[parsed.mode];
|
|
6409
|
+
const color = known ? parsed : toOklch(parsed);
|
|
6410
|
+
const spec = known ?? {
|
|
6411
|
+
space: "oklch",
|
|
6412
|
+
channels: [
|
|
6413
|
+
"l",
|
|
6414
|
+
"c",
|
|
6415
|
+
"h"
|
|
6416
|
+
]
|
|
6417
|
+
};
|
|
6418
|
+
const channels = color;
|
|
6419
|
+
const components = spec.channels.map((channel, i) => {
|
|
6420
|
+
const v = channels[channel];
|
|
6421
|
+
return v === void 0 || Number.isNaN(v) ? "none" : round(v * (spec.scale?.[i] ?? 1));
|
|
6422
|
+
});
|
|
6423
|
+
const out = {
|
|
6424
|
+
colorSpace: spec.space,
|
|
6425
|
+
components
|
|
6426
|
+
};
|
|
6427
|
+
const alpha = color.alpha;
|
|
6428
|
+
if (alpha !== void 0 && alpha < 1) out.alpha = round(alpha);
|
|
6429
|
+
else if (spec.space === "srgb" && components.every((c) => typeof c === "number" && Math.abs(c * 255 - Math.round(c * 255)) < .001)) out.hex = formatHex(color);
|
|
6430
|
+
return out;
|
|
4281
6431
|
}
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
return
|
|
6432
|
+
const PLAIN_LENGTH = /^(-?(?:\d+\.?\d*|\.\d+))(px|rem)$/;
|
|
6433
|
+
/** `0.5rem` → `{ value: 0.5, unit: "rem" }`; a `calc()` of tokens → its px value. */
|
|
6434
|
+
function dimensionValue(value, lookup) {
|
|
6435
|
+
const plain = plainDimension(value);
|
|
6436
|
+
if (plain) return {
|
|
6437
|
+
type: "dimension",
|
|
6438
|
+
value: plain
|
|
6439
|
+
};
|
|
6440
|
+
const px = evaluateLength(value, lookup);
|
|
6441
|
+
if (px === void 0 || !Number.isFinite(px)) return void 0;
|
|
6442
|
+
return {
|
|
6443
|
+
type: "dimension",
|
|
6444
|
+
value: {
|
|
6445
|
+
value: round(px),
|
|
6446
|
+
unit: "px"
|
|
6447
|
+
}
|
|
6448
|
+
};
|
|
4287
6449
|
}
|
|
4288
|
-
|
|
4289
|
-
const
|
|
4290
|
-
text
|
|
4291
|
-
|
|
4292
|
-
"
|
|
4293
|
-
"muted",
|
|
4294
|
-
"secondary",
|
|
4295
|
-
"accent",
|
|
4296
|
-
"card",
|
|
4297
|
-
"popover"
|
|
4298
|
-
],
|
|
4299
|
-
line: [
|
|
4300
|
-
"border",
|
|
4301
|
-
"input",
|
|
4302
|
-
"ring"
|
|
4303
|
-
]
|
|
4304
|
-
};
|
|
4305
|
-
/** Plain text colors, as opposed to the text that goes on one surface (`card-foreground`). */
|
|
4306
|
-
const GENERIC_TEXT = /^(?:muted-foreground|(?:foreground|text|fg)(?:-(?:default|base|primary|secondary|muted|subtle))?)$/;
|
|
4307
|
-
function roleFit(token, role) {
|
|
4308
|
-
if (!role) return {
|
|
4309
|
-
tier: 1,
|
|
4310
|
-
rank: 0
|
|
6450
|
+
function plainDimension(value) {
|
|
6451
|
+
const text = value.trim();
|
|
6452
|
+
if (text === "0") return {
|
|
6453
|
+
value: 0,
|
|
6454
|
+
unit: "px"
|
|
4311
6455
|
};
|
|
4312
|
-
const
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
6456
|
+
const match = PLAIN_LENGTH.exec(text);
|
|
6457
|
+
return match ? {
|
|
6458
|
+
value: Number(match[1]),
|
|
6459
|
+
unit: match[2] ?? "px"
|
|
6460
|
+
} : void 0;
|
|
6461
|
+
}
|
|
6462
|
+
const COMPOSITE = /^[\w-]+\s*:\s*[^;]+(?:;\s*[\w-]+\s*:\s*[^;]+)*;?$/;
|
|
6463
|
+
const NUMBER = /^-?(?:\d+\.?\d*|\.\d+)$/;
|
|
6464
|
+
/** What a typography token sets, from its name and Tailwind class (`font-sans`, `text-sm`). */
|
|
6465
|
+
function typographyKind(token) {
|
|
6466
|
+
const value = token.value.trim();
|
|
6467
|
+
if (COMPOSITE.test(value)) return "composite";
|
|
6468
|
+
const utility = token.usage.find((u) => !u.includes("(")) ?? "";
|
|
6469
|
+
const name = `${token.name} ${utility}`.toLowerCase();
|
|
6470
|
+
if (/weight/.test(name) || utility.startsWith("font-") && /^\d+$/.test(value)) return "fontWeight";
|
|
6471
|
+
if (/leading|line-?height/.test(name)) return "lineHeight";
|
|
6472
|
+
if (/tracking|letter-?spacing/.test(name)) return "letterSpacing";
|
|
6473
|
+
if (/(?:^|[\s.-])text-|size/.test(name)) return "fontSize";
|
|
6474
|
+
if (/font|family/.test(name)) return "fontFamily";
|
|
6475
|
+
}
|
|
6476
|
+
function typographyValue(value, token) {
|
|
6477
|
+
const text = value.trim();
|
|
6478
|
+
switch (typographyKind(token)) {
|
|
6479
|
+
case "fontFamily": return {
|
|
6480
|
+
type: "fontFamily",
|
|
6481
|
+
value: fontFamilies(text)
|
|
6482
|
+
};
|
|
6483
|
+
case "fontWeight": {
|
|
6484
|
+
const n = weight(text);
|
|
6485
|
+
return n === void 0 ? void 0 : {
|
|
6486
|
+
type: "fontWeight",
|
|
6487
|
+
value: n
|
|
4321
6488
|
};
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
6489
|
+
}
|
|
6490
|
+
case "lineHeight": {
|
|
6491
|
+
if (NUMBER.test(text)) return {
|
|
6492
|
+
type: "number",
|
|
6493
|
+
value: Number(text)
|
|
4325
6494
|
};
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
6495
|
+
const length = plainDimension(text);
|
|
6496
|
+
return length && {
|
|
6497
|
+
type: "dimension",
|
|
6498
|
+
value: length
|
|
6499
|
+
};
|
|
6500
|
+
}
|
|
6501
|
+
case "fontSize":
|
|
6502
|
+
case "letterSpacing": {
|
|
6503
|
+
const length = plainDimension(text);
|
|
6504
|
+
return length && {
|
|
6505
|
+
type: "dimension",
|
|
6506
|
+
value: length
|
|
6507
|
+
};
|
|
6508
|
+
}
|
|
6509
|
+
case "composite": return {
|
|
6510
|
+
type: "typography",
|
|
6511
|
+
value: composite(text)
|
|
4333
6512
|
};
|
|
6513
|
+
default: return;
|
|
4334
6514
|
}
|
|
4335
6515
|
}
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
if (
|
|
4342
|
-
|
|
4343
|
-
return
|
|
6516
|
+
/** `fontFamily: Inter; fontSize: 16px` → `{ fontFamily: "Inter", fontSize: { value: 16, unit: "px" } }`. */
|
|
6517
|
+
function composite(text) {
|
|
6518
|
+
const out = {};
|
|
6519
|
+
for (const [key, value] of pairs(text)) if (key === "fontFamily") out[key] = fontFamilies(value);
|
|
6520
|
+
else if (key === "fontWeight") out[key] = weight(value) ?? value;
|
|
6521
|
+
else if (key === "lineHeight" && NUMBER.test(value)) out[key] = Number(value);
|
|
6522
|
+
else out[key] = plainDimension(value) ?? value;
|
|
6523
|
+
return out;
|
|
4344
6524
|
}
|
|
4345
|
-
|
|
4346
|
-
|
|
6525
|
+
/** `fontFamily: Inter; fontSize: 16px` → `[["fontFamily", "Inter"], ["fontSize", "16px"]]`. */
|
|
6526
|
+
function pairs(text) {
|
|
6527
|
+
return text.split(";").flatMap((part) => {
|
|
6528
|
+
const colon = part.indexOf(":");
|
|
6529
|
+
const key = part.slice(0, colon).trim();
|
|
6530
|
+
const value = part.slice(colon + 1).trim();
|
|
6531
|
+
return colon > 0 && key && value ? [[key, value]] : [];
|
|
6532
|
+
});
|
|
4347
6533
|
}
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
6534
|
+
function weight(value) {
|
|
6535
|
+
const n = Number(value);
|
|
6536
|
+
return /^\d+$/.test(value) && n >= 1 && n <= 1e3 ? n : void 0;
|
|
6537
|
+
}
|
|
6538
|
+
/** `"Geist", ui-sans-serif` → `["Geist", "ui-sans-serif"]`; one family, or a `var()`, stays a string. */
|
|
6539
|
+
function fontFamilies(value) {
|
|
6540
|
+
if (value.includes("var(")) return value;
|
|
6541
|
+
const families = splitTopLevel$1(value, ",").map((f) => f.trim().replace(/^(["'])(.*)\1$/, "$2")).filter(Boolean);
|
|
6542
|
+
return families.length === 1 ? families[0] ?? value : families;
|
|
6543
|
+
}
|
|
6544
|
+
/** `0 1px 2px 0 rgb(0 0 0 / 0.05)` → a DTCG shadow object; several layers → an array of them. */
|
|
6545
|
+
function shadowValue(value) {
|
|
6546
|
+
const layers = [];
|
|
6547
|
+
for (const layer of splitTopLevel$1(value, ",")) {
|
|
6548
|
+
let inset = false;
|
|
6549
|
+
let color;
|
|
6550
|
+
const lengths = [];
|
|
6551
|
+
for (const part of splitTopLevel$1(layer.trim(), " ")) {
|
|
6552
|
+
if (!part) continue;
|
|
6553
|
+
const length = plainDimension(part);
|
|
6554
|
+
if (part === "inset") inset = true;
|
|
6555
|
+
else if (length) lengths.push(length);
|
|
6556
|
+
else if (!color && parseColor(part)) color = colorObject(part);
|
|
6557
|
+
else return void 0;
|
|
6558
|
+
}
|
|
6559
|
+
const [offsetX, offsetY, blur = zero(), spread = zero()] = lengths;
|
|
6560
|
+
if (!color || !offsetX || !offsetY || lengths.length > 4) return void 0;
|
|
6561
|
+
layers.push({
|
|
6562
|
+
color,
|
|
6563
|
+
offsetX,
|
|
6564
|
+
offsetY,
|
|
6565
|
+
blur,
|
|
6566
|
+
spread,
|
|
6567
|
+
...inset ? { inset: true } : {}
|
|
6568
|
+
});
|
|
6569
|
+
}
|
|
6570
|
+
if (!layers.length) return void 0;
|
|
6571
|
+
return layers.length === 1 ? layers[0] ?? null : layers;
|
|
4360
6572
|
}
|
|
4361
|
-
function
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
6573
|
+
function zero() {
|
|
6574
|
+
return {
|
|
6575
|
+
value: 0,
|
|
6576
|
+
unit: "px"
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6579
|
+
/** Splits on `separator` outside parentheses and quotes. */
|
|
6580
|
+
function splitTopLevel$1(text, separator) {
|
|
6581
|
+
const parts = [];
|
|
6582
|
+
let depth = 0;
|
|
6583
|
+
let quote;
|
|
6584
|
+
let start = 0;
|
|
6585
|
+
for (let i = 0; i < text.length; i++) {
|
|
6586
|
+
const ch = text[i];
|
|
6587
|
+
if (quote) {
|
|
6588
|
+
if (ch === quote) quote = void 0;
|
|
6589
|
+
} else if (ch === "\"" || ch === "'") quote = ch;
|
|
6590
|
+
else if (ch === "(") depth++;
|
|
6591
|
+
else if (ch === ")") depth--;
|
|
6592
|
+
else if (depth === 0 && (separator === " " ? /\s/.test(ch ?? "") : ch === separator)) {
|
|
6593
|
+
parts.push(text.slice(start, i));
|
|
6594
|
+
start = i + 1;
|
|
6595
|
+
}
|
|
4371
6596
|
}
|
|
4372
|
-
|
|
6597
|
+
parts.push(text.slice(start));
|
|
6598
|
+
return parts;
|
|
4373
6599
|
}
|
|
4374
|
-
|
|
6600
|
+
function round(n) {
|
|
6601
|
+
return Number(n.toFixed(6));
|
|
6602
|
+
}
|
|
6603
|
+
function isObject$1(value) {
|
|
6604
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6605
|
+
}
|
|
6606
|
+
/** DESIGN.md sections in the spec's order, each with the front matter key it fills. */
|
|
6607
|
+
const SECTIONS = [
|
|
6608
|
+
{
|
|
6609
|
+
category: "color",
|
|
6610
|
+
heading: "Colors",
|
|
6611
|
+
key: "colors",
|
|
6612
|
+
intro: "Use these colors instead of hex, rgb() or Tailwind palette values."
|
|
6613
|
+
},
|
|
6614
|
+
{
|
|
6615
|
+
category: "typography",
|
|
6616
|
+
heading: "Typography",
|
|
6617
|
+
key: "typography",
|
|
6618
|
+
intro: "The font families, sizes and weights of the design system."
|
|
6619
|
+
},
|
|
6620
|
+
{
|
|
6621
|
+
category: "spacing",
|
|
6622
|
+
heading: "Layout",
|
|
6623
|
+
key: "spacing",
|
|
6624
|
+
intro: "Use these spacing steps instead of arbitrary lengths."
|
|
6625
|
+
},
|
|
6626
|
+
{
|
|
6627
|
+
category: "shadow",
|
|
6628
|
+
heading: "Elevation & Depth",
|
|
6629
|
+
intro: "The shadows of the design system."
|
|
6630
|
+
},
|
|
6631
|
+
{
|
|
6632
|
+
category: "radius",
|
|
6633
|
+
heading: "Shapes",
|
|
6634
|
+
key: "rounded",
|
|
6635
|
+
intro: "Use these corner radii instead of arbitrary values."
|
|
6636
|
+
},
|
|
6637
|
+
{
|
|
6638
|
+
category: "other",
|
|
6639
|
+
heading: "Other tokens",
|
|
6640
|
+
intro: "Tokens that are not colors, typography, spacing, shadows or radii."
|
|
6641
|
+
}
|
|
6642
|
+
];
|
|
6643
|
+
/** The front matter keys, in the order of the spec's schema. */
|
|
6644
|
+
const FRONT_MATTER_ORDER = [
|
|
6645
|
+
"colors",
|
|
6646
|
+
"typography",
|
|
6647
|
+
"rounded",
|
|
6648
|
+
"spacing"
|
|
6649
|
+
];
|
|
4375
6650
|
/**
|
|
4376
|
-
*
|
|
4377
|
-
*
|
|
4378
|
-
*
|
|
6651
|
+
* The tokens as a DESIGN.md: front matter with the colors, typography, rounded corners and
|
|
6652
|
+
* spacing in the spec's schema, then a heading per category with a table of its tokens, their
|
|
6653
|
+
* values in each mode and how code uses them.
|
|
4379
6654
|
*/
|
|
4380
|
-
function
|
|
4381
|
-
|
|
4382
|
-
const
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
if (token === void 0) return void 0;
|
|
4388
|
-
if (token.toLowerCase() === "calc") return primary();
|
|
4389
|
-
if (token === "(") {
|
|
4390
|
-
const v = sum();
|
|
4391
|
-
i++;
|
|
4392
|
-
return v;
|
|
4393
|
-
}
|
|
4394
|
-
const px = lengthToPx(token);
|
|
4395
|
-
if (px === void 0) return void 0;
|
|
4396
|
-
return {
|
|
4397
|
-
n: px,
|
|
4398
|
-
length: /[a-z]$/i.test(token)
|
|
4399
|
-
};
|
|
6655
|
+
function toDesignMd(tokens, options) {
|
|
6656
|
+
const warnings = [];
|
|
6657
|
+
const lookup = varLookup(tokens);
|
|
6658
|
+
const keys = designKeys(tokens);
|
|
6659
|
+
const front = {
|
|
6660
|
+
version: "alpha",
|
|
6661
|
+
name: options.name
|
|
4400
6662
|
};
|
|
4401
|
-
const
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
6663
|
+
const sections = {};
|
|
6664
|
+
const body = [`# ${options.name}`];
|
|
6665
|
+
const byName = new Map(tokens.map((t) => [t.name, t]));
|
|
6666
|
+
for (const section of SECTIONS) {
|
|
6667
|
+
const list = tokens.filter((t) => t.category === section.category);
|
|
6668
|
+
if (!list.length) continue;
|
|
6669
|
+
if (section.key) {
|
|
6670
|
+
const values = {};
|
|
6671
|
+
for (const token of list) {
|
|
6672
|
+
const key = keys.get(token);
|
|
6673
|
+
if (key === void 0) continue;
|
|
6674
|
+
const target = token.aliasOf ? byName.get(token.aliasOf) : void 0;
|
|
6675
|
+
const targetKey = target?.category === token.category && frontMatterValue(target, section.key, lookup) !== void 0 ? keys.get(target) : void 0;
|
|
6676
|
+
const value = targetKey === void 0 ? frontMatterValue(token, section.key, lookup) : `{${section.key}.${targetKey}}`;
|
|
6677
|
+
if (value === void 0) continue;
|
|
6678
|
+
if (key in values) {
|
|
6679
|
+
warnings.push(`${token.name} is left out of the front matter: another token has its key`);
|
|
6680
|
+
continue;
|
|
6681
|
+
}
|
|
6682
|
+
values[key] = value;
|
|
6683
|
+
}
|
|
6684
|
+
if (Object.keys(values).length) sections[section.key] = values;
|
|
4414
6685
|
}
|
|
4415
|
-
|
|
6686
|
+
body.push("", `## ${section.heading}`, "", section.intro, "", ...table(list, keys));
|
|
6687
|
+
}
|
|
6688
|
+
for (const key of FRONT_MATTER_ORDER) if (sections[key]) front[key] = sections[key];
|
|
6689
|
+
return {
|
|
6690
|
+
text: `---\n${stringify(front, { lineWidth: 0 })}---\n\n${body.join("\n")}\n`,
|
|
6691
|
+
count: tokens.length,
|
|
6692
|
+
warnings
|
|
4416
6693
|
};
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
6694
|
+
}
|
|
6695
|
+
/** Each token's key in its section: its DTCG path without the group, joined with `-`. */
|
|
6696
|
+
function designKeys(tokens) {
|
|
6697
|
+
const out = /* @__PURE__ */ new Map();
|
|
6698
|
+
for (const token of tokens) {
|
|
6699
|
+
const parts = leaf(token).filter((p) => p !== "$root");
|
|
6700
|
+
out.set(token, parts.join("-") || "base");
|
|
6701
|
+
}
|
|
6702
|
+
return out;
|
|
6703
|
+
}
|
|
6704
|
+
function frontMatterValue(token, key, lookup) {
|
|
6705
|
+
const value = token.value.trim();
|
|
6706
|
+
switch (key) {
|
|
6707
|
+
case "colors":
|
|
6708
|
+
if (!parseColor(value, { bareHsl: true })) return void 0;
|
|
6709
|
+
return isBareHsl(value) ? `hsl(${value})` : value;
|
|
6710
|
+
case "rounded":
|
|
6711
|
+
case "spacing": {
|
|
6712
|
+
if (DESIGN_DIMENSION.test(value) || value === "0") return value;
|
|
6713
|
+
const px = evaluateLength(value, lookup);
|
|
6714
|
+
return px === void 0 || !Number.isFinite(px) ? void 0 : `${round(px)}px`;
|
|
4427
6715
|
}
|
|
4428
|
-
return
|
|
4429
|
-
}
|
|
4430
|
-
|
|
4431
|
-
|
|
6716
|
+
case "typography": return designTypography(token);
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6719
|
+
/** A DESIGN.md dimension: px, em or rem. */
|
|
6720
|
+
const DESIGN_DIMENSION = /^-?(?:\d+\.?\d*|\.\d+)(?:px|rem|em)$/;
|
|
6721
|
+
/**
|
|
6722
|
+
* A typography token as a DESIGN.md typography level: `{ fontFamily: "Inter" }` for
|
|
6723
|
+
* `--font-sans`, every property of a composite. A `var()` is no font name (`next/font` sets
|
|
6724
|
+
* it at runtime), and a value the schema has no form for is left out.
|
|
6725
|
+
*/
|
|
6726
|
+
function designTypography(token) {
|
|
6727
|
+
const value = token.value.trim();
|
|
6728
|
+
const kind = typographyKind(token);
|
|
6729
|
+
const entries = kind === "composite" ? pairs(value) : kind ? [[kind, value]] : [];
|
|
6730
|
+
const out = {};
|
|
6731
|
+
for (const [property, v] of entries) if (property === "fontFamily") {
|
|
6732
|
+
if (!v.includes("var(")) out[property] = v;
|
|
6733
|
+
} else if (property === "fontWeight") {
|
|
6734
|
+
const n = weight(v);
|
|
6735
|
+
if (n !== void 0) out[property] = n;
|
|
6736
|
+
} else if (property === "lineHeight") {
|
|
6737
|
+
if (NUMBER.test(v)) out[property] = Number(v);
|
|
6738
|
+
else if (DESIGN_DIMENSION.test(v)) out[property] = v;
|
|
6739
|
+
} else if (property === "fontSize" || property === "letterSpacing") {
|
|
6740
|
+
if (DESIGN_DIMENSION.test(v)) out[property] = v;
|
|
6741
|
+
} else out[property] = v;
|
|
6742
|
+
return Object.keys(out).length ? out : void 0;
|
|
6743
|
+
}
|
|
6744
|
+
/** A Markdown table of tokens: key, value, a column per mode, how code uses it, and the description. */
|
|
6745
|
+
function table(tokens, keys) {
|
|
6746
|
+
const modes = [...new Set(tokens.flatMap((t) => Object.keys(t.modes ?? {})))];
|
|
6747
|
+
const described = tokens.some((t) => t.description);
|
|
6748
|
+
const header = [
|
|
6749
|
+
"Token",
|
|
6750
|
+
"Value",
|
|
6751
|
+
...modes.map((m) => m.charAt(0).toUpperCase() + m.slice(1)),
|
|
6752
|
+
"Use",
|
|
6753
|
+
...described ? ["Description"] : []
|
|
6754
|
+
];
|
|
6755
|
+
const code = (text) => text ? `\`${text}\`` : "";
|
|
6756
|
+
const rows = tokens.map((token) => [
|
|
6757
|
+
code(keys.get(token) ?? token.name),
|
|
6758
|
+
code(token.value),
|
|
6759
|
+
...modes.map((m) => code(token.modes?.[m])),
|
|
6760
|
+
use(token),
|
|
6761
|
+
...described ? [token.description ?? ""] : []
|
|
6762
|
+
]);
|
|
6763
|
+
const line = (cells) => `| ${cells.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`;
|
|
6764
|
+
return [
|
|
6765
|
+
line(header),
|
|
6766
|
+
line(header.map(() => "---")),
|
|
6767
|
+
...rows.map(line)
|
|
6768
|
+
];
|
|
6769
|
+
}
|
|
6770
|
+
/** `bg-primary`, `var(--primary)`: the first utility class and the CSS reference. */
|
|
6771
|
+
function use(token) {
|
|
6772
|
+
return [token.usage.find((u) => !u.includes("(")), cssReference(token)].filter((u) => Boolean(u)).map((u) => `\`${u}\``).join(", ");
|
|
4432
6773
|
}
|
|
4433
6774
|
//#endregion
|
|
4434
6775
|
//#region src/tokens/index.ts
|
|
@@ -4441,7 +6782,7 @@ async function loadTokens(root, sources) {
|
|
|
4441
6782
|
const rel = relativePath(root, source.file);
|
|
4442
6783
|
let text;
|
|
4443
6784
|
try {
|
|
4444
|
-
text = await
|
|
6785
|
+
text = await fsp.readFile(source.file, "utf8");
|
|
4445
6786
|
} catch {
|
|
4446
6787
|
warnings.push(`${rel}: token file not found`);
|
|
4447
6788
|
continue;
|
|
@@ -4453,6 +6794,10 @@ async function loadTokens(root, sources) {
|
|
|
4453
6794
|
const result = parseDtcgTokens(text, rel, options);
|
|
4454
6795
|
all.push(...result.tokens);
|
|
4455
6796
|
warnings.push(...result.warnings);
|
|
6797
|
+
} else if (ext === ".md" || ext === ".markdown") {
|
|
6798
|
+
const result = parseDesignMd(text, rel);
|
|
6799
|
+
all.push(...result.tokens);
|
|
6800
|
+
warnings.push(...result.warnings);
|
|
4456
6801
|
} else {
|
|
4457
6802
|
sheets.push(readCssSheet(text, rel));
|
|
4458
6803
|
sheetsAt ??= all.length;
|
|
@@ -4561,7 +6906,7 @@ var TokenIndex = class {
|
|
|
4561
6906
|
...colorModes(token)
|
|
4562
6907
|
});
|
|
4563
6908
|
if (token.tailwind) this.colorKeys.add(token.tailwind);
|
|
4564
|
-
} else if (token.category === "spacing" || token.category === "radius") {
|
|
6909
|
+
} else if ((token.category === "spacing" || token.category === "radius") && token.origin !== "library") {
|
|
4565
6910
|
const px = evaluateLength(token.value, resolve);
|
|
4566
6911
|
if (px === void 0) continue;
|
|
4567
6912
|
if (token.category === "spacing" && token.tailwind === "") {
|
|
@@ -5142,6 +7487,8 @@ var LintTarget = class {
|
|
|
5142
7487
|
moduleDirs = /* @__PURE__ */ new Set();
|
|
5143
7488
|
/** The files extraction read, when the model says. */
|
|
5144
7489
|
sources;
|
|
7490
|
+
/** Published libraries the model read from type declarations: `@mui/material`. */
|
|
7491
|
+
libraries;
|
|
5145
7492
|
/**
|
|
5146
7493
|
* `resolver` finds the module an import points at and what it exports
|
|
5147
7494
|
* (`@calcom/ui/components/icon` → `components/icon/index.ts`), so a name from
|
|
@@ -5153,13 +7500,14 @@ var LintTarget = class {
|
|
|
5153
7500
|
this.config = config;
|
|
5154
7501
|
this.resolver = resolver;
|
|
5155
7502
|
const rank = designSystemRank(config.designSystems);
|
|
7503
|
+
this.libraries = new Set(model.components.flatMap((c) => c.library ? [c.library] : []));
|
|
5156
7504
|
const byRank = [...model.components].sort((a, b) => rank(b) - rank(a));
|
|
5157
7505
|
for (const component of byRank) for (const name of [component.name, ...component.aliases]) {
|
|
5158
7506
|
this.components.set(name, component);
|
|
5159
7507
|
this.named.set(name, [...this.named.get(name) ?? [], component]);
|
|
5160
7508
|
}
|
|
5161
7509
|
this.tokens = new TokenIndex(model.tokens, { filesUsing: model.components.length ? familyUsage(model) : void 0 });
|
|
5162
|
-
this.componentFiles = new Set(model.components.map((c) => c.source.file));
|
|
7510
|
+
this.componentFiles = new Set(model.components.filter((c) => !c.library).map((c) => c.source.file));
|
|
5163
7511
|
this.exports = new Set(model.exports);
|
|
5164
7512
|
for (const component of model.components) {
|
|
5165
7513
|
if (component.parent) continue;
|
|
@@ -5181,7 +7529,7 @@ var LintTarget = class {
|
|
|
5181
7529
|
}
|
|
5182
7530
|
this.importPaths = new Set(model.components.map((c) => c.importPath));
|
|
5183
7531
|
const pattern = config.importPath?.indexOf("{path}") ?? -1;
|
|
5184
|
-
this.importPrefixes = config.importPath ? [pattern === -1 ? `${config.importPath}/` : config.importPath.slice(0, pattern)] :
|
|
7532
|
+
this.importPrefixes = unique([...config.importPath ? [pattern === -1 ? `${config.importPath}/` : config.importPath.slice(0, pattern)] : [...this.importPaths].filter((p) => !p.startsWith(".")).map((p) => isPackageName(p) ? `${p}/` : p.slice(0, p.lastIndexOf("/") + 1)).filter(Boolean), ...[...this.libraries].map((library) => `${library}/`)]);
|
|
5185
7533
|
for (const file of this.componentFiles) {
|
|
5186
7534
|
for (const module of modulePaths(file)) this.componentModules.add(module);
|
|
5187
7535
|
this.moduleDirs.add(path.posix.dirname(file));
|
|
@@ -5306,6 +7654,8 @@ var LintTarget = class {
|
|
|
5306
7654
|
/** The component a module exports as `default`, for `import Anything from "..."`. */
|
|
5307
7655
|
defaultExport(specifier, fromFile = "snippet.tsx") {
|
|
5308
7656
|
const resolved = specifier.startsWith(".") ? this.resolveRelative(specifier, fromFile) : void 0;
|
|
7657
|
+
const fromLibrary = this.model.components.filter((c) => c.defaultImport === specifier);
|
|
7658
|
+
if (fromLibrary.length === 1) return fromLibrary[0];
|
|
5309
7659
|
const matches = this.model.components.filter((c) => c.exportName === "default" && (resolved === void 0 ? (!this.config.importPath || this.config.importPath.includes("{path}")) && c.importPath === specifier : modulePaths(c.source.file).includes(resolved)));
|
|
5310
7660
|
return matches.length === 1 ? matches[0] : void 0;
|
|
5311
7661
|
}
|
|
@@ -5313,6 +7663,14 @@ var LintTarget = class {
|
|
|
5313
7663
|
const from = path.isAbsolute(fromFile) ? path.relative(this.model.root, fromFile) : fromFile;
|
|
5314
7664
|
return path.posix.join(path.posix.dirname(toPosix(from)), stripExtension(specifier));
|
|
5315
7665
|
}
|
|
7666
|
+
/** The library an import specifier points into (`@mui/material/Button`), if any. */
|
|
7667
|
+
libraryOf(specifier) {
|
|
7668
|
+
for (const library of this.libraries) if (specifier === library || specifier.startsWith(`${library}/`)) return library;
|
|
7669
|
+
}
|
|
7670
|
+
/** The library's own component of that name: `Button` from `@mui/material`, not a wrapper. */
|
|
7671
|
+
libraryComponent(name, library) {
|
|
7672
|
+
return (this.named.get(name) ?? []).find((c) => c.library === library);
|
|
7673
|
+
}
|
|
5316
7674
|
/** Component names for "did you mean" suggestions. */
|
|
5317
7675
|
names() {
|
|
5318
7676
|
return [...this.components.keys()];
|
|
@@ -5350,12 +7708,14 @@ function familyUsage(model) {
|
|
|
5350
7708
|
}
|
|
5351
7709
|
/**
|
|
5352
7710
|
* A component's place among the design systems an app uses: 0 for the most imported, and
|
|
5353
|
-
* so on; with one design system, 0 for all.
|
|
7711
|
+
* so on; with one design system, 0 for all. A library's components (MUI) come after them all.
|
|
5354
7712
|
*/
|
|
5355
7713
|
function designSystemRank(sources) {
|
|
5356
|
-
|
|
7714
|
+
const last = (sources?.length ?? 1) + 1;
|
|
7715
|
+
if (!sources || sources.length < 2) return (component) => component.library ? last : 0;
|
|
5357
7716
|
const ranks = /* @__PURE__ */ new Map();
|
|
5358
7717
|
return (component) => {
|
|
7718
|
+
if (component.library) return last;
|
|
5359
7719
|
const file = component.source.file;
|
|
5360
7720
|
let rank = ranks.get(file);
|
|
5361
7721
|
if (rank === void 0) {
|
|
@@ -5694,6 +8054,7 @@ const noUnknownProp = {
|
|
|
5694
8054
|
const resolution = context.resolve(element);
|
|
5695
8055
|
if (resolution.kind !== "component" || resolution.component.openProps) continue;
|
|
5696
8056
|
const component = resolution.component;
|
|
8057
|
+
if (component.polymorphic && findAttribute(element, component.polymorphic)) continue;
|
|
5697
8058
|
const own = component.props.map((p) => p.name);
|
|
5698
8059
|
const known = new Set(own);
|
|
5699
8060
|
for (const inherited of component.inherits) for (const name of propSets[inherited.set] ?? []) known.add(name);
|
|
@@ -6015,6 +8376,38 @@ function hasLabel(attributes) {
|
|
|
6015
8376
|
});
|
|
6016
8377
|
}
|
|
6017
8378
|
/**
|
|
8379
|
+
* Whether the element's parent is a component that gives its child an accessible name, as MUI's
|
|
8380
|
+
* `<Tooltip title="Delete">` does with a string title (not a JSX one, and not with
|
|
8381
|
+
* `describeChild`, which makes it a description).
|
|
8382
|
+
*/
|
|
8383
|
+
function labelledByParent(context, element) {
|
|
8384
|
+
let node = element.node.parent;
|
|
8385
|
+
while (ts.isParenthesizedExpression(node) || ts.isJsxExpression(node) || ts.isConditionalExpression(node) && node.condition !== element.node || ts.isBinaryExpression(node) && [
|
|
8386
|
+
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
8387
|
+
ts.SyntaxKind.BarBarToken,
|
|
8388
|
+
ts.SyntaxKind.QuestionQuestionToken
|
|
8389
|
+
].includes(node.operatorToken.kind)) node = node.parent;
|
|
8390
|
+
if (!ts.isJsxElement(node)) return false;
|
|
8391
|
+
const parent = context.analysis.elements.find((e) => e.node === node);
|
|
8392
|
+
if (!parent) return false;
|
|
8393
|
+
const resolution = context.resolve(parent);
|
|
8394
|
+
const labels = resolution.kind === "component" ? resolution.component.labelsChild : void 0;
|
|
8395
|
+
if (!labels) return false;
|
|
8396
|
+
const unless = labels.unless ? findAttribute(parent, labels.unless) : void 0;
|
|
8397
|
+
if (unless) {
|
|
8398
|
+
const init = unless.initializer;
|
|
8399
|
+
if ((init && ts.isJsxExpression(init) ? init.expression : init)?.kind !== ts.SyntaxKind.FalseKeyword) return false;
|
|
8400
|
+
}
|
|
8401
|
+
const label = findAttribute(parent, labels.prop);
|
|
8402
|
+
if (!label?.initializer) return false;
|
|
8403
|
+
if (ts.isStringLiteral(label.initializer)) return label.initializer.text.trim() !== "";
|
|
8404
|
+
const value = ts.isJsxExpression(label.initializer) ? label.initializer.expression : void 0;
|
|
8405
|
+
if (!value) return false;
|
|
8406
|
+
const literals = literalValues(label);
|
|
8407
|
+
if (literals.length) return literals.some((l) => l.text.trim() !== "");
|
|
8408
|
+
return !ts.isJsxElement(value) && !ts.isJsxSelfClosingElement(value) && !ts.isJsxFragment(value);
|
|
8409
|
+
}
|
|
8410
|
+
/**
|
|
6018
8411
|
* The element whose `render` prop this one is, as in Base UI's
|
|
6019
8412
|
* `<Dialog.Close render={<Button size="icon" />}><XIcon /></Dialog.Close>`:
|
|
6020
8413
|
* the rendered button takes the host's children and attributes. Also through
|
|
@@ -6075,6 +8468,7 @@ const iconButtonAccessibleName = {
|
|
|
6075
8468
|
if (host === "object") continue;
|
|
6076
8469
|
if (host && (isButton(context, host) || host.hasSpread)) continue;
|
|
6077
8470
|
if (hasLabel(element.attributes) || host && hasLabel(host.attributes)) continue;
|
|
8471
|
+
if (labelledByParent(context, element)) continue;
|
|
6078
8472
|
const icons = [];
|
|
6079
8473
|
const children = host ? [...host.children, ...element.children] : element.children;
|
|
6080
8474
|
const content = host && !children.length ? "unknown" : classify(children, context, icons);
|
|
@@ -6259,6 +8653,7 @@ function reportColor(context, input) {
|
|
|
6259
8653
|
replacement = cls ? `className="${cls}"` : cssVar;
|
|
6260
8654
|
advice = cls ? `\`${cls}\` in className` : void 0;
|
|
6261
8655
|
}
|
|
8656
|
+
if (!cls && !cssVar && token.usage.length) advice = token.usage.map((usage) => `\`${usage}\``).join(" or ");
|
|
6262
8657
|
const same = sameValue.length ? `, same value as ${sameValue.map((t) => t.name).join(", ")}` : "";
|
|
6263
8658
|
const inMode = mode && token.modes?.[mode] !== void 0 ? ` in ${mode} mode` : "";
|
|
6264
8659
|
const found = `${token.name}${inMode} (${formatDeltaE(nearest.distance)}${same})`;
|
|
@@ -6362,7 +8757,7 @@ function lengthRule(id, description, spec) {
|
|
|
6362
8757
|
const negative = utility.negative !== px < 0;
|
|
6363
8758
|
if (negative && !NEGATIVE_PREFIXES.has(utility.prefix)) continue;
|
|
6364
8759
|
const replacement = lengthReplacement(context, spec.category, px, utility.prefix);
|
|
6365
|
-
if (!replacement) continue;
|
|
8760
|
+
if (!replacement || unnamed(replacement)) continue;
|
|
6366
8761
|
const fixed = withBase({
|
|
6367
8762
|
...utility,
|
|
6368
8763
|
negative
|
|
@@ -6395,6 +8790,7 @@ function lengthRule(id, description, spec) {
|
|
|
6395
8790
|
const negative = px < 0;
|
|
6396
8791
|
if (negative && !NEGATIVE_PREFIXES.has(prefix)) continue;
|
|
6397
8792
|
const replacement = values.length === 1 ? lengthReplacement(context, spec.category, px, prefix) : void 0;
|
|
8793
|
+
if (replacement && unnamed(replacement)) continue;
|
|
6398
8794
|
const cls = replacement && `${negative ? "-" : ""}${replacement.cls}`;
|
|
6399
8795
|
const cssVar = replacement?.cssVar && (negative ? `calc(${replacement.cssVar} * -1)` : replacement.cssVar);
|
|
6400
8796
|
const shown = number !== void 0 ? `${name}: ${raw}` : `${name}: "${raw}"`;
|
|
@@ -6415,6 +8811,13 @@ function lengthRule(id, description, spec) {
|
|
|
6415
8811
|
}
|
|
6416
8812
|
};
|
|
6417
8813
|
}
|
|
8814
|
+
/**
|
|
8815
|
+
* A value on the scale whose token code cannot name, with neither a Tailwind class nor a
|
|
8816
|
+
* custom property (a DESIGN.md's `spacing.md`): there is nothing better to write.
|
|
8817
|
+
*/
|
|
8818
|
+
function unnamed(replacement) {
|
|
8819
|
+
return replacement.exact && !replacement.tailwind && !replacement.cssVar;
|
|
8820
|
+
}
|
|
6418
8821
|
/** The nearest step is "far" when it is off by more than half the value and more than 4px. */
|
|
6419
8822
|
const FAR_RATIO = .5;
|
|
6420
8823
|
const FAR_PX = 4;
|
|
@@ -6693,7 +9096,7 @@ function isObject(value) {
|
|
|
6693
9096
|
var name = "onsystem";
|
|
6694
9097
|
//#endregion
|
|
6695
9098
|
//#region src/version.ts
|
|
6696
|
-
const VERSION = "0.
|
|
9099
|
+
const VERSION = "0.6.0";
|
|
6697
9100
|
const NAME = name;
|
|
6698
9101
|
//#endregion
|
|
6699
9102
|
//#region src/lint/sarif.ts
|
|
@@ -7379,17 +9782,25 @@ var DesignSystem = class {
|
|
|
7379
9782
|
config;
|
|
7380
9783
|
lint;
|
|
7381
9784
|
searchIndex;
|
|
9785
|
+
/**
|
|
9786
|
+
* The components the tools offer: all of them, except a library's (MUI's) where the project
|
|
9787
|
+
* has its own of that name, a wrapper the team means to be used. The rules still check code
|
|
9788
|
+
* that imports the library's directly against the library's.
|
|
9789
|
+
*/
|
|
9790
|
+
offered;
|
|
7382
9791
|
constructor(model, config) {
|
|
7383
9792
|
this.model = model;
|
|
7384
9793
|
this.config = config;
|
|
7385
9794
|
this.lint = new LintTarget(model, config, new ModuleResolver(config));
|
|
7386
|
-
|
|
9795
|
+
const own = new Set(model.components.filter((c) => !c.library).flatMap((c) => [c.name, ...c.aliases]));
|
|
9796
|
+
this.offered = model.components.filter((c) => !c.library || !(own.has(c.name) || c.parent !== void 0 && own.has(c.parent)));
|
|
9797
|
+
this.searchIndex = buildSearchIndex(this.offered);
|
|
7387
9798
|
}
|
|
7388
9799
|
get root() {
|
|
7389
9800
|
return this.model.root;
|
|
7390
9801
|
}
|
|
7391
9802
|
get components() {
|
|
7392
|
-
return this.
|
|
9803
|
+
return this.offered;
|
|
7393
9804
|
}
|
|
7394
9805
|
get tokens() {
|
|
7395
9806
|
return this.model.tokens;
|
|
@@ -7399,7 +9810,7 @@ var DesignSystem = class {
|
|
|
7399
9810
|
}
|
|
7400
9811
|
/** Top-level components, i.e. not parts of another component. */
|
|
7401
9812
|
roots() {
|
|
7402
|
-
return this.
|
|
9813
|
+
return this.offered.filter((c) => !c.parent);
|
|
7403
9814
|
}
|
|
7404
9815
|
/** Looks a component up by name, alias, `Card.Header`/`CardHeader` or case-insensitively. */
|
|
7405
9816
|
getComponent(name) {
|
|
@@ -7407,7 +9818,7 @@ var DesignSystem = class {
|
|
|
7407
9818
|
if (exact) return exact;
|
|
7408
9819
|
const key = (n) => n.replace(/\./g, "").toLowerCase();
|
|
7409
9820
|
const wanted = key(name);
|
|
7410
|
-
return this.
|
|
9821
|
+
return this.offered.find((c) => key(c.name) === wanted || c.aliases.some((a) => key(a) === wanted));
|
|
7411
9822
|
}
|
|
7412
9823
|
suggestComponent(name) {
|
|
7413
9824
|
return closest(name, this.lint.names(), .5);
|
|
@@ -7438,6 +9849,16 @@ var DesignSystem = class {
|
|
|
7438
9849
|
return [...found.values()];
|
|
7439
9850
|
}
|
|
7440
9851
|
/**
|
|
9852
|
+
* Whether `check --require-design-system` (and the GitHub Action, where it is on by default)
|
|
9853
|
+
* finds a design system to check against: components and color tokens, or components from a
|
|
9854
|
+
* published library (MUI). A library is the design system whatever its theme: when the theme's
|
|
9855
|
+
* colors cannot be read (a palette from Sass variables), the check still runs and `notice()`
|
|
9856
|
+
* says that no-hardcoded-color did not.
|
|
9857
|
+
*/
|
|
9858
|
+
hasDesignSystem() {
|
|
9859
|
+
return !this.notice() || this.model.components.some((c) => c.library !== void 0);
|
|
9860
|
+
}
|
|
9861
|
+
/**
|
|
7441
9862
|
* What the rules could not check, when it matters: without components the
|
|
7442
9863
|
* component rules find nothing, and without color tokens neither does
|
|
7443
9864
|
* no-hardcoded-color, so a clean result says little. Undefined when both exist.
|
|
@@ -7476,17 +9897,18 @@ async function find(config, patterns) {
|
|
|
7476
9897
|
absolute: true,
|
|
7477
9898
|
dot: false
|
|
7478
9899
|
};
|
|
9900
|
+
const exclude = config.designSystemExclude ?? config.exclude;
|
|
7479
9901
|
const named = patterns.filter((p) => p.includes("node_modules"));
|
|
7480
9902
|
const outside = patterns.filter((p) => !p.includes("node_modules") && p.startsWith("../"));
|
|
7481
9903
|
const rest = patterns.filter((p) => !p.includes("node_modules") && !p.startsWith("../"));
|
|
7482
9904
|
return unique((await Promise.all([
|
|
7483
9905
|
rest.length ? glob(rest, {
|
|
7484
9906
|
...options,
|
|
7485
|
-
ignore: unique(["**/node_modules/**", ...
|
|
9907
|
+
ignore: unique(["**/node_modules/**", ...exclude])
|
|
7486
9908
|
}) : [],
|
|
7487
9909
|
named.length ? glob(named, {
|
|
7488
9910
|
...options,
|
|
7489
|
-
ignore:
|
|
9911
|
+
ignore: exclude.filter((e) => !/^(?:\*\*\/)?node_modules(?:\/\*\*)?\/?$/.test(e))
|
|
7490
9912
|
}) : [],
|
|
7491
9913
|
...outside.map((pattern) => {
|
|
7492
9914
|
const base = globBase(pattern);
|
|
@@ -7496,8 +9918,8 @@ async function find(config, patterns) {
|
|
|
7496
9918
|
cwd: path.resolve(config.root, base),
|
|
7497
9919
|
ignore: unique([
|
|
7498
9920
|
"**/node_modules/**",
|
|
7499
|
-
...
|
|
7500
|
-
...
|
|
9921
|
+
...exclude.filter((e) => e.startsWith("**/")),
|
|
9922
|
+
...exclude.filter((e) => e.startsWith(prefix)).map((e) => e.slice(prefix.length))
|
|
7501
9923
|
])
|
|
7502
9924
|
});
|
|
7503
9925
|
})
|
|
@@ -7516,14 +9938,18 @@ async function resolveFiles(config) {
|
|
|
7516
9938
|
const docs = await find(config, config.docs);
|
|
7517
9939
|
const tokens = [];
|
|
7518
9940
|
const seen = /* @__PURE__ */ new Set();
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
9941
|
+
const add = async (sources) => {
|
|
9942
|
+
for (const source of sources) for (const file of await find(config, [source.path])) {
|
|
9943
|
+
if (seen.has(file)) continue;
|
|
9944
|
+
seen.add(file);
|
|
9945
|
+
tokens.push({
|
|
9946
|
+
file,
|
|
9947
|
+
prefix: source.prefix
|
|
9948
|
+
});
|
|
9949
|
+
}
|
|
9950
|
+
};
|
|
9951
|
+
await add(config.tokens.filter((t) => !t.fallback));
|
|
9952
|
+
if (!tokens.length) await add(config.tokens.filter((t) => t.fallback));
|
|
7527
9953
|
return {
|
|
7528
9954
|
components,
|
|
7529
9955
|
tokens,
|
|
@@ -7541,7 +9967,7 @@ var FileStats = class {
|
|
|
7541
9967
|
return Promise.all(files.map((file) => {
|
|
7542
9968
|
let stat = this.stats.get(file);
|
|
7543
9969
|
if (!stat) {
|
|
7544
|
-
stat =
|
|
9970
|
+
stat = fsp.stat(file).then((s) => `${relativePath(this.root, file)}:${s.size}:${Math.round(s.mtimeMs)}`, () => `${file}:missing`);
|
|
7545
9971
|
this.stats.set(file, stat);
|
|
7546
9972
|
}
|
|
7547
9973
|
return stat;
|
|
@@ -7597,7 +10023,7 @@ async function buildModel(config, options = {}) {
|
|
|
7597
10023
|
const cachePath = cacheFile(config.root);
|
|
7598
10024
|
let cached;
|
|
7599
10025
|
if (useCache && !options.oldProgram) try {
|
|
7600
|
-
cached = JSON.parse(await
|
|
10026
|
+
cached = JSON.parse(await fsp.readFile(cachePath, "utf8"));
|
|
7601
10027
|
} catch {}
|
|
7602
10028
|
const known = Array.isArray(cached?.dependencies) ? cached.dependencies : options.oldProgram ? projectFiles(options.oldProgram) : [];
|
|
7603
10029
|
const stats = new FileStats(config.root);
|
|
@@ -7622,24 +10048,36 @@ async function buildModel(config, options = {}) {
|
|
|
7622
10048
|
const docs = [];
|
|
7623
10049
|
const warnings = [...extracted.warnings];
|
|
7624
10050
|
for (const file of files.docs) try {
|
|
7625
|
-
docs.push(parseDoc(await
|
|
10051
|
+
docs.push(parseDoc(await fsp.readFile(file, "utf8"), relativePath(config.root, file)));
|
|
7626
10052
|
} catch (error) {
|
|
7627
10053
|
warnings.push(`${relativePath(config.root, file)}: ${error.message}`);
|
|
7628
10054
|
}
|
|
7629
10055
|
warnings.push(...attachDocs(extracted.components, docs));
|
|
7630
10056
|
const tokens = await loadTokens(config.root, files.tokens);
|
|
7631
10057
|
warnings.push(...tokens.warnings);
|
|
10058
|
+
const libraries = await loadLibraries(config);
|
|
10059
|
+
warnings.push(...libraries.warnings);
|
|
10060
|
+
tokens.tokens.push(...libraries.tokens);
|
|
7632
10061
|
const tailwind = config.tailwindConfig ? readTailwindColors(config.tailwindConfig, moduleResolver(config.root)) : void 0;
|
|
7633
10062
|
if (tailwind) applyTailwindColors(tokens.tokens, tailwind.colors);
|
|
7634
|
-
|
|
7635
|
-
if (
|
|
10063
|
+
let radiiFiles = [];
|
|
10064
|
+
if (config.tailwindConfig) {
|
|
10065
|
+
const radii = readTailwindRadii(config.tailwindConfig, moduleResolver(config.root));
|
|
10066
|
+
applyTailwindRadii(tokens.tokens, radii.radii, config.tailwindConfig);
|
|
10067
|
+
radiiFiles = radii.files;
|
|
10068
|
+
}
|
|
10069
|
+
if (!files.components.length && !libraries.components.length) warnings.push(`No component files matched ${config.components.join(", ")} under ${config.root}`);
|
|
10070
|
+
if (!files.tokens.length && !libraries.tokens.length) warnings.push("No token files found; token rules are disabled.");
|
|
7636
10071
|
const model = {
|
|
7637
10072
|
version: 1,
|
|
7638
10073
|
root: config.root,
|
|
7639
|
-
components: extracted.components,
|
|
10074
|
+
components: [...extracted.components, ...libraries.components],
|
|
7640
10075
|
tokens: tokens.tokens,
|
|
7641
|
-
propSets:
|
|
7642
|
-
|
|
10076
|
+
propSets: {
|
|
10077
|
+
...extracted.propSets,
|
|
10078
|
+
...libraries.propSets
|
|
10079
|
+
},
|
|
10080
|
+
exports: unique([...extracted.exports, ...libraries.exports]).sort(),
|
|
7643
10081
|
warnings,
|
|
7644
10082
|
sources: files.components.map((file) => relativePath(config.root, file)),
|
|
7645
10083
|
stats: {
|
|
@@ -7652,11 +10090,16 @@ async function buildModel(config, options = {}) {
|
|
|
7652
10090
|
fromCache: false
|
|
7653
10091
|
}
|
|
7654
10092
|
};
|
|
7655
|
-
const dependencies = unique([
|
|
10093
|
+
const dependencies = unique([
|
|
10094
|
+
...extracted.dependencies,
|
|
10095
|
+
...tailwind?.files ?? [],
|
|
10096
|
+
...radiiFiles,
|
|
10097
|
+
...libraries.dependencies
|
|
10098
|
+
]);
|
|
7656
10099
|
const key = await cacheKey(config, files, dependencies, stats);
|
|
7657
10100
|
if (useCache) try {
|
|
7658
|
-
await
|
|
7659
|
-
await
|
|
10101
|
+
await fsp.mkdir(path.dirname(cachePath), { recursive: true });
|
|
10102
|
+
await fsp.writeFile(cachePath, JSON.stringify({
|
|
7660
10103
|
key,
|
|
7661
10104
|
dependencies,
|
|
7662
10105
|
model
|
|
@@ -7743,9 +10186,10 @@ var DesignSystemHost = class {
|
|
|
7743
10186
|
* `extends` target is picked up.
|
|
7744
10187
|
*/
|
|
7745
10188
|
watchConfigFiles() {
|
|
7746
|
-
const { root, configFile, tsconfig } = this.config;
|
|
10189
|
+
const { root, configFile, tsconfig, designSystemPackage } = this.config;
|
|
7747
10190
|
const files = [
|
|
7748
10191
|
configFile,
|
|
10192
|
+
designSystemPackage && path.join(designSystemPackage.dir, "package.json"),
|
|
7749
10193
|
...DETECTION_FILES.map((f) => path.join(root, f)),
|
|
7750
10194
|
path.resolve(root, tsconfig ?? "tsconfig.json"),
|
|
7751
10195
|
...readProjectConfig(root, tsconfig).configFiles.filter(isProjectFile)
|
|
@@ -7762,8 +10206,8 @@ var DesignSystemHost = class {
|
|
|
7762
10206
|
}
|
|
7763
10207
|
}
|
|
7764
10208
|
changed(file) {
|
|
7765
|
-
const { root, configFile, detected } = this.config;
|
|
7766
|
-
if (file === void 0 || file === configFile || path.dirname(file) === root && DETECTION_FILES.includes(path.basename(file)) || detected !== void 0 && this.watchedFiles.has(file)) this.configChanged = true;
|
|
10209
|
+
const { root, configFile, detected, designSystemPackage } = this.config;
|
|
10210
|
+
if (file === void 0 || file === configFile || designSystemPackage !== void 0 && this.watchedFiles.has(file) || path.dirname(file) === root && DETECTION_FILES.includes(path.basename(file)) || detected !== void 0 && this.watchedFiles.has(file)) this.configChanged = true;
|
|
7767
10211
|
this.schedule();
|
|
7768
10212
|
}
|
|
7769
10213
|
close() {
|
|
@@ -7799,10 +10243,10 @@ var DesignSystemHost = class {
|
|
|
7799
10243
|
}
|
|
7800
10244
|
/** Re-reads the config, re-watches what it points at and rebuilds. */
|
|
7801
10245
|
async reloadConfig() {
|
|
7802
|
-
const { root, configFile } = this.config;
|
|
10246
|
+
const { root, configFile, designSystemPackage, warnings } = this.config;
|
|
7803
10247
|
const load = this.options.loadConfig ?? (() => loadConfig({
|
|
7804
10248
|
root,
|
|
7805
|
-
config: configFile
|
|
10249
|
+
config: designSystemPackage ? void 0 : configFile
|
|
7806
10250
|
}));
|
|
7807
10251
|
try {
|
|
7808
10252
|
this.config = await load();
|
|
@@ -7810,6 +10254,7 @@ var DesignSystemHost = class {
|
|
|
7810
10254
|
(this.options.logger ?? silentLogger).error(`config reload failed: ${error.message}`);
|
|
7811
10255
|
return;
|
|
7812
10256
|
}
|
|
10257
|
+
for (const text of this.config.warnings ?? []) if (!warnings?.includes(text)) (this.options.logger ?? silentLogger).warn(text);
|
|
7813
10258
|
if (this.watchers.length) {
|
|
7814
10259
|
this.close();
|
|
7815
10260
|
this.watch();
|
|
@@ -7831,11 +10276,13 @@ var DesignSystemHost = class {
|
|
|
7831
10276
|
this.dependencies = dependencies ?? [];
|
|
7832
10277
|
if (this.watchers.length) this.watchConfigFiles();
|
|
7833
10278
|
const { stats } = model;
|
|
7834
|
-
|
|
10279
|
+
const roots = ds.roots().length;
|
|
10280
|
+
const parts = ds.components.length - roots;
|
|
10281
|
+
logger.info(`${incremental ? "reloaded" : "loaded"} ${plural$1(roots, "component")}${parts ? ` (+${plural$1(parts, "part")})` : ""}, ${plural$1(model.tokens.length, "token")} from ${relativePath(process.cwd(), config.root) || "."} in ${stats.durationMs}ms${stats.fromCache ? " (cache)" : ""}`);
|
|
7835
10282
|
for (const warning of model.warnings) logger.warn(warning);
|
|
7836
10283
|
if (incremental) for (const listener of this.listeners) listener(ds);
|
|
7837
10284
|
return ds;
|
|
7838
10285
|
}
|
|
7839
10286
|
};
|
|
7840
10287
|
//#endregion
|
|
7841
|
-
export {
|
|
10288
|
+
export { findVariantDefinitions as $, toDtcg as A, DEFAULT_DOCS as B, updateBaseline as C, loadTokens as D, TokenIndex as E, attachDocs as F, RULE_IDS as G, DEFAULT_TEST_EXCLUDE as H, parseDoc as I, hasOwnConfig as J, configJsonSchema as K, extractComponents as L, parseDtcgTokens as M, parseDesignMd as N, mergeTokens as O, parseCssTokens as P, formatPx as Q, CONFIG_FILES as R, readBaseline as S, RULES as T, DEFAULT_TEST_FOLDERS as U, DEFAULT_SEVERITY as V, LEGACY_CONFIG_FILES as W, loadConfigWith as X, loadConfig as Y, rulesFor as Z, VERSION as _, relativePath as _t, fingerprint as a, detectProject as at, applyBaseline as b, silentLogger as c, isWorkspaceRoot as ct, searchComponents as d, closest as dt, LIBRARY_IDS as et, applyFixes as f, plural$1 as ft, NAME as g, matchesGlob as gt, formatDiagnostics as h, isInside as ht, componentFiles as i, declaredConfig as it, TOKEN_CATEGORIES as j, toDesignMd as k, stderrLogger as l, workspacePackageDirs as lt, uncheckedNotice as m, unique as mt, DesignSystemHost as n, PACKAGE_FIELD as nt, loadDesignSystem as o, findDesignSystemPackage as ot, checkSource as p, truncate as pt, defineConfig as q, buildModel as r, PackageFinder as rt, tokenFiles as s, isDesignSystemName as st, DesignSystem as t, libraryAdapter as tt, buildSearchIndex as u, workspacePatterns as ut, BASELINE_FILE as v, toPosix as vt, writeBaseline as w, countBaseline as x, LEGACY_BASELINE_FILE as y, ConfigError as z };
|