onsystem 0.7.0 → 0.8.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 +1 -1
- package/dist/cli.js +397 -70
- package/dist/{config-1XSTYI44.d.ts → config-kYGiD5oW.d.ts} +7 -0
- package/dist/{design-system-C4bhqLLw.js → design-system-Dmcv7eHR.js} +1086 -76
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +3 -5
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +2 -2
- package/dist/{stdio-BojAQ6JP.js → stdio-MTS6QAgo.js} +30 -7
- package/dist/{workspace-Z0tZf_Dv.js → workspace-CM1RyqZA.js} +1 -1
- package/package.json +1 -1
- package/schema.json +1 -1
|
@@ -5,7 +5,7 @@ import * as z from "zod";
|
|
|
5
5
|
import { escapePath, glob, globSync } from "tinyglobby";
|
|
6
6
|
import ts from "typescript";
|
|
7
7
|
import os from "node:os";
|
|
8
|
-
import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse } from "culori";
|
|
8
|
+
import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import fsp from "node:fs/promises";
|
|
11
11
|
import postcss from "postcss";
|
|
@@ -137,6 +137,12 @@ function slashGlob(pattern) {
|
|
|
137
137
|
if (!pattern.includes("/")) return pattern.replaceAll("\\", "/");
|
|
138
138
|
return pattern.replace(/\\(?![()[\]{}*?!+@])/g, "/");
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* The files `check` and `check_ui` read: `.tsx`, `.jsx`, `.ts`, `.js` and the `.mts`, `.cts`,
|
|
142
|
+
* `.mjs` and `.cjs` forms, in any case. CSS, Markdown or JSON parsed as TSX gives hundreds of
|
|
143
|
+
* syntax errors (209 for a shadcn/ui `app/globals.css`).
|
|
144
|
+
*/
|
|
145
|
+
const SCRIPT_FILE$1 = /\.(?:[jt]sx?|[cm][jt]s)$/i;
|
|
140
146
|
/** Path relative to `root`, always with forward slashes. */
|
|
141
147
|
function relativePath(root, file) {
|
|
142
148
|
return toPosix(path.relative(root, file));
|
|
@@ -191,6 +197,16 @@ function matchesGlob(file, pattern) {
|
|
|
191
197
|
return regex.test(file.replace(/^\.\//, ""));
|
|
192
198
|
}
|
|
193
199
|
const globRegExps = /* @__PURE__ */ new Map();
|
|
200
|
+
/**
|
|
201
|
+
* Whether a root-relative path is one a glob matches, or is in a folder it matches, as a
|
|
202
|
+
* glob's `ignore` leaves a matching folder out: `app/legacy`, `app/legacy/`, `./app/legacy` and
|
|
203
|
+
* `app/legacy/**` all cover `app/legacy/old.tsx`, and `**\/legacy` covers every legacy folder.
|
|
204
|
+
* How `exclude` patterns are matched, wherever they apply.
|
|
205
|
+
*/
|
|
206
|
+
function coveredByGlob(file, pattern) {
|
|
207
|
+
const glob = pattern.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
208
|
+
return matchesGlob(file, glob) || matchesGlob(file, `${glob}/**`);
|
|
209
|
+
}
|
|
194
210
|
function globSource(pattern) {
|
|
195
211
|
let out = "";
|
|
196
212
|
for (let i = 0; i < pattern.length; i++) {
|
|
@@ -582,7 +598,9 @@ const STYLESHEETS = [
|
|
|
582
598
|
* workspace sources, read the same way or through their own `components.json`,
|
|
583
599
|
* or, for a package without `exports` that apps import by path
|
|
584
600
|
* (`@acme/ui/primitives/button`), from the files the root's code imports. They
|
|
585
|
-
* are added to what `components.json` found, not skipped for it.
|
|
601
|
+
* are added to what `components.json` found, not skipped for it. One installed
|
|
602
|
+
* from a registry is read through its `exports` alone, when they point at the
|
|
603
|
+
* component sources it publishes, not from `dist/` back to `src/`.
|
|
586
604
|
* 4. A flat folder of components that wrap a primitives library, the way React
|
|
587
605
|
* Aria's Tailwind starter ships them (`src/Button.tsx`, `src/Checkbox.tsx`).
|
|
588
606
|
*
|
|
@@ -590,6 +608,10 @@ const STYLESHEETS = [
|
|
|
590
608
|
* (`designSystems`). A candidate whose globs match no file is skipped. Returns undefined
|
|
591
609
|
* when nothing applies, so the shadcn defaults stay in force.
|
|
592
610
|
*
|
|
611
|
+
* Next to the ui folder `components.json` names, the folders `npx shadcn add` copies other
|
|
612
|
+
* registries' code into (its `registries`: `@signoff-ui` → `components/signoff-ui`) are
|
|
613
|
+
* read-only design systems, after all the others ({@link fromRegistries}).
|
|
614
|
+
*
|
|
593
615
|
* `declared` is the dependency whose config the root takes on ({@link findDesignSystemPackage}):
|
|
594
616
|
* it is read first among the dependencies in step 3, whatever its name, and also when it is
|
|
595
617
|
* installed from a registry rather than a workspace package.
|
|
@@ -602,8 +624,9 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
602
624
|
});
|
|
603
625
|
const found = [];
|
|
604
626
|
const shadcn = readJson$1(path.join(root, "components.json"));
|
|
627
|
+
const project = shadcn ? readProjectConfig(root, tsconfig) : void 0;
|
|
605
628
|
let why = "no ui or components alias";
|
|
606
|
-
const fromShadcn = shadcn ? fromComponentsJson(root, root, shadcn,
|
|
629
|
+
const fromShadcn = shadcn && project ? fromComponentsJson(root, root, shadcn, project, packages, (r) => {
|
|
607
630
|
why = r;
|
|
608
631
|
}) : void 0;
|
|
609
632
|
if (!shadcn) step("components.json", false, fs.existsSync(path.join(root, "components.json")) ? "not valid JSON" : "not found");
|
|
@@ -639,21 +662,23 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
639
662
|
let imported;
|
|
640
663
|
for (const name of names) {
|
|
641
664
|
const own = name === declared?.name ? declared : void 0;
|
|
642
|
-
const
|
|
665
|
+
const workspaceDir = own ? void 0 : packages.find(name);
|
|
666
|
+
const installedDir = own || workspaceDir ? void 0 : packages.installed(name);
|
|
667
|
+
const kind = own?.installed ?? installedDir !== void 0 ? "installed" : "workspace";
|
|
643
668
|
const candidate = `${kind} package ${name}`;
|
|
644
669
|
if (found.some((f) => f.package === name)) {
|
|
645
670
|
step(candidate, false, "not added twice: components.json points into it");
|
|
646
671
|
continue;
|
|
647
672
|
}
|
|
648
|
-
const dir = own
|
|
673
|
+
const dir = own?.dir ?? workspaceDir ?? installedDir;
|
|
649
674
|
const pkg = dir ? readJson$1(path.join(dir, "package.json")) : void 0;
|
|
650
675
|
if (!dir || !pkg) {
|
|
651
|
-
step(candidate, false, "not a workspace package
|
|
676
|
+
step(candidate, false, "not a workspace package, and not installed");
|
|
652
677
|
continue;
|
|
653
678
|
}
|
|
654
679
|
const reasons = [];
|
|
655
680
|
const note = (r) => reasons.push(r);
|
|
656
|
-
const result = fromPackage(root, dir, pkg, note) ?? fromPackageComponentsJson(root, dir, pkg, packages, note) ?? fromImports(root, dir, name, (imported ??= importedSubpaths(root, names)).get(name), note);
|
|
681
|
+
const result = installedDir ? fromPackage(root, dir, pkg, note, true) : fromPackage(root, dir, pkg, note) ?? fromPackageComponentsJson(root, dir, pkg, packages, note) ?? fromImports(root, dir, name, (imported ??= importedSubpaths(root, names)).get(name), note);
|
|
657
682
|
if (result && matchesAny(root, result)) {
|
|
658
683
|
found.push({
|
|
659
684
|
...result,
|
|
@@ -664,11 +689,28 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
664
689
|
step(candidate, true, `${relativePath(root, dir)}, ${countFiles(root, result)}`);
|
|
665
690
|
} else step(candidate, false, result ? "its components match no file" : unique(reasons).join("; ") || "nothing to read");
|
|
666
691
|
}
|
|
692
|
+
const registries = shadcn && project && fromShadcn && !withDefaults ? fromRegistries(root, shadcn, fromShadcn, found, project, step) : [];
|
|
667
693
|
if (found.length) {
|
|
668
694
|
step("a flat src/ of components", false, "not read: a design system was found");
|
|
695
|
+
const named = (source) => registries.length ? `${source}; from shadcn registries, read-only: ${registries.map((r) => r.source).join(", ")}` : source;
|
|
696
|
+
const read = registries.map((f) => ({
|
|
697
|
+
source: f.source,
|
|
698
|
+
components: f.components,
|
|
699
|
+
imports: 0,
|
|
700
|
+
registry: f.registry
|
|
701
|
+
}));
|
|
669
702
|
if (found.length === 1) {
|
|
670
703
|
const [only] = found;
|
|
671
|
-
|
|
704
|
+
const source = only?.source ?? "";
|
|
705
|
+
if (!only || !read.length) return detection(root, found, withDefaults, source);
|
|
706
|
+
return {
|
|
707
|
+
...detection(root, [...found, ...registries], withDefaults, named(source)),
|
|
708
|
+
designSystems: [{
|
|
709
|
+
source,
|
|
710
|
+
components: only.components,
|
|
711
|
+
imports: 0
|
|
712
|
+
}, ...read]
|
|
713
|
+
};
|
|
672
714
|
}
|
|
673
715
|
const counts = importCounts(root, found.map((f) => f.prefixes ?? []));
|
|
674
716
|
const ranked = found.map((f, i) => ({
|
|
@@ -679,12 +721,12 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
679
721
|
const [primary, ...rest] = ranked;
|
|
680
722
|
const describe = (r) => `${r.f.source} (${r.imports.toLocaleString("en-US")} ${r.imports === 1 ? "import" : "imports"})`;
|
|
681
723
|
return {
|
|
682
|
-
...detection(root, ranked.map((r) => r.f), withDefaults, `primary: ${primary ? describe(primary) : ""}; also: ${rest.map(describe).join(", ")}`),
|
|
683
|
-
designSystems: ranked.map((r) => ({
|
|
724
|
+
...detection(root, [...ranked.map((r) => r.f), ...registries], withDefaults, named(`primary: ${primary ? describe(primary) : ""}; also: ${rest.map(describe).join(", ")}`)),
|
|
725
|
+
designSystems: [...ranked.map((r) => ({
|
|
684
726
|
source: r.f.source,
|
|
685
727
|
components: r.f.components,
|
|
686
728
|
imports: r.imports
|
|
687
|
-
}))
|
|
729
|
+
})), ...read]
|
|
688
730
|
};
|
|
689
731
|
}
|
|
690
732
|
let flatWhy = "no package.json";
|
|
@@ -847,6 +889,101 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
|
|
|
847
889
|
...ownerPackage(root, uiDir, ui)
|
|
848
890
|
};
|
|
849
891
|
}
|
|
892
|
+
/** How a shadcn registry is named in components.json: `@` and a name, so none leads out of the folder. */
|
|
893
|
+
const REGISTRY_NAMESPACE = /^@[a-z0-9][\w-]*$/i;
|
|
894
|
+
/**
|
|
895
|
+
* The registries `components.json` names (`"registries": { "@signoff-ui": … }`), sorted, and
|
|
896
|
+
* where `npx shadcn add` copies their code: the components alias, through tsconfig `paths`, when
|
|
897
|
+
* it names a folder in the project. `why` says why there is none.
|
|
898
|
+
*/
|
|
899
|
+
function registryBase(root, json, project) {
|
|
900
|
+
const namespaces = Object.keys(asRecord(json.registries)).filter((n) => REGISTRY_NAMESPACE.test(n)).sort();
|
|
901
|
+
const alias = asRecord(json.aliases).components;
|
|
902
|
+
if (typeof alias !== "string") return {
|
|
903
|
+
namespaces,
|
|
904
|
+
why: "no components alias"
|
|
905
|
+
};
|
|
906
|
+
const base = namespaces.length ? resolveThroughPaths(alias, project) : void 0;
|
|
907
|
+
if (!base || !isInside(root, base)) return {
|
|
908
|
+
namespaces,
|
|
909
|
+
why: `the components alias ${alias} names no folder in the project through tsconfig paths`
|
|
910
|
+
};
|
|
911
|
+
return {
|
|
912
|
+
namespaces,
|
|
913
|
+
base,
|
|
914
|
+
alias
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
/** The folder of a registry's code: the namespace without its `@`, under the components folder. */
|
|
918
|
+
function registryFolder(root, base, namespace) {
|
|
919
|
+
const dir = relativePath(root, path.join(base, namespace.slice(1)));
|
|
920
|
+
return {
|
|
921
|
+
namespace,
|
|
922
|
+
dir,
|
|
923
|
+
glob: `${escapePath(dir)}/**/*.{tsx,jsx}`
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* The folders `npx shadcn add` copies the code of the registries `components.json` names into,
|
|
928
|
+
* whether they hold files yet or not, outside the ui folder ({@link fromRegistries}). A config
|
|
929
|
+
* that lists them in `components` reads them as zero config does: read-only, after the rest.
|
|
930
|
+
*/
|
|
931
|
+
function registryFolders(root, tsconfig) {
|
|
932
|
+
const json = readJson$1(path.join(root, "components.json"));
|
|
933
|
+
if (!json || !Object.keys(asRecord(json.registries)).length) return [];
|
|
934
|
+
const project = readProjectConfig(root, tsconfig);
|
|
935
|
+
const { namespaces, base } = registryBase(root, json, project);
|
|
936
|
+
if (!base) return [];
|
|
937
|
+
const ui = asRecord(json.aliases).ui;
|
|
938
|
+
const uiDir = typeof ui === "string" ? resolveThroughPaths(ui, project) : void 0;
|
|
939
|
+
return namespaces.filter((namespace) => !uiDir || !isInside(uiDir, path.join(base, namespace.slice(1)))).map((namespace) => registryFolder(root, base, namespace));
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* The folders `npx shadcn add` copies the code of the registries `components.json` names into
|
|
943
|
+
* (`@/components` and `@signoff-ui` → `components/signoff-ui`). Each one that holds a .tsx or
|
|
944
|
+
* .jsx file is a read-only design system: the app's code is checked against its components, and
|
|
945
|
+
* its own files are skipped as the ui folder's are. Not when a design system found is imported
|
|
946
|
+
* through a package's `exports` (`@workspace/ui`): a config that names that package would give
|
|
947
|
+
* registry code its name too. A folder inside the ui folder is already part of it. Code a
|
|
948
|
+
* registry copies loose into the components folder (React Bits) is not set apart.
|
|
949
|
+
*/
|
|
950
|
+
function fromRegistries(root, json, ui, others, project, step) {
|
|
951
|
+
const { namespaces, base, alias, why } = registryBase(root, json, project);
|
|
952
|
+
if (!namespaces.length) return [];
|
|
953
|
+
const no = (reason) => {
|
|
954
|
+
for (const namespace of namespaces) step(`registry ${namespace}`, false, reason);
|
|
955
|
+
return [];
|
|
956
|
+
};
|
|
957
|
+
const exported = others.find((f) => f.imports.length);
|
|
958
|
+
if (exported) return no(`not read: ${exported.package ?? "a package"} is imported through its exports, and the registry's code is not`);
|
|
959
|
+
if (!base || alias === void 0) return no(why ?? "no components alias");
|
|
960
|
+
const found = [];
|
|
961
|
+
for (const namespace of namespaces) {
|
|
962
|
+
const candidate = `registry ${namespace}`;
|
|
963
|
+
const { dir, glob } = registryFolder(root, base, namespace);
|
|
964
|
+
const absolute = path.join(root, dir);
|
|
965
|
+
if (ui.dirs.some((d) => isInside(d, absolute))) {
|
|
966
|
+
step(candidate, false, `${dir} is in the ui folder`);
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
const files = isDirectory$1(absolute) ? componentGlob(root, [glob]).length : 0;
|
|
970
|
+
if (!files) {
|
|
971
|
+
step(candidate, false, `${dir} ${isDirectory$1(absolute) ? "holds no .tsx or .jsx file yet" : "does not exist yet"}`);
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
step(candidate, true, `${dir}, ${plural(files, "component file")}, read-only`);
|
|
975
|
+
found.push({
|
|
976
|
+
components: [glob],
|
|
977
|
+
tokens: [],
|
|
978
|
+
imports: [],
|
|
979
|
+
dirs: [],
|
|
980
|
+
source: `${dir} (${namespace}, ${plural(files, "file")})`,
|
|
981
|
+
prefixes: [`${alias}/${namespace.slice(1)}`],
|
|
982
|
+
registry: namespace
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
return found;
|
|
986
|
+
}
|
|
850
987
|
/**
|
|
851
988
|
* The workspace package a `components.json` folder belongs to, when it is not the root
|
|
852
989
|
* itself (`@coss/ui/components`, through tsconfig `paths` or the package), and the
|
|
@@ -889,22 +1026,29 @@ function fromPackageComponentsJson(root, dir, pkg, packages, note = ignore) {
|
|
|
889
1026
|
* A package whose `exports` point at component source files is a design system. An export
|
|
890
1027
|
* that leads to a barrel (`./components/button` → `components/button/index.ts`, or `.` →
|
|
891
1028
|
* `src/index.tsx` behind `dist/`) stands for the files the barrel re-exports, each
|
|
892
|
-
* suggested with the export's specifier.
|
|
893
|
-
|
|
894
|
-
|
|
1029
|
+
* suggested with the export's specifier. An `installed` package's exports are read as an app
|
|
1030
|
+
* resolves them ({@link APP_CONDITIONS}), not from `dist/` back to `src/`: a widget kit
|
|
1031
|
+
* publishes `src/` beside `dist/` for its source maps, or points a `source` condition at it for
|
|
1032
|
+
* its own repository, and it is not the app's design system. Its compiled files ({@link isBuildOutput})
|
|
1033
|
+
* are not read at all: no barrel, component or stylesheet in them. Nor is an export that points
|
|
1034
|
+
* at JavaScript outside a build folder ({@link isCompiledExport}): its re-exports are not followed.
|
|
1035
|
+
*/
|
|
1036
|
+
function fromPackage(root, dir, pkg, note = ignore, installed = false) {
|
|
895
1037
|
const imports = [];
|
|
896
1038
|
const components = [];
|
|
897
1039
|
const add = (mapping, component) => {
|
|
898
1040
|
imports.push(mapping);
|
|
899
1041
|
if (component && !components.some((m) => m.target === mapping.target)) components.push(mapping);
|
|
900
1042
|
};
|
|
901
|
-
|
|
1043
|
+
const mappings = exportMappings(root, dir, pkg, installed);
|
|
1044
|
+
for (const mapping of mappings) {
|
|
902
1045
|
const file = path.resolve(root, mapping.target);
|
|
1046
|
+
if (installed && isCompiledExport(dir, file)) continue;
|
|
903
1047
|
if (mapping.target.includes("*") || !SCRIPT_FILE.test(file) || !isFile(file)) {
|
|
904
1048
|
add(mapping, COMPONENT_FILE.test(mapping.target));
|
|
905
1049
|
continue;
|
|
906
1050
|
}
|
|
907
|
-
for (const reached of exportedFiles(file, dir)) if (COMPONENT_FILE.test(reached)) add({
|
|
1051
|
+
for (const reached of exportedFiles(file, dir)) if (COMPONENT_FILE.test(reached) && !(installed && isBuildOutput(dir, reached))) add({
|
|
908
1052
|
specifier: mapping.specifier,
|
|
909
1053
|
target: relativePath(root, reached)
|
|
910
1054
|
}, true);
|
|
@@ -912,8 +1056,9 @@ function fromPackage(root, dir, pkg, note = ignore) {
|
|
|
912
1056
|
const files = components.filter((m) => !m.target.includes("*"));
|
|
913
1057
|
const patterns = components.filter((m) => m.target.includes("*"));
|
|
914
1058
|
if (files.length < 3 && !patterns.length) {
|
|
915
|
-
const built =
|
|
916
|
-
|
|
1059
|
+
const built = mappings.filter((m) => /(?:^|\/)(?:dist|build|lib|out|esm|cjs)\//.test(m.target) || installed && isCompiledExport(dir, path.resolve(root, m.target)));
|
|
1060
|
+
const output = built.slice(0, 2).map((m) => m.target).join(", ");
|
|
1061
|
+
note(pkg.exports === void 0 && typeof pkg.main !== "string" ? "no exports" : built.length ? installed ? `its exports point at build output (${output}), not at component sources, and only sources are read` : `its exports point at build output with no source found (${output})` : `its exports lead to ${plural(files.length, "component file")}; a design system exports at least 3`);
|
|
917
1062
|
return;
|
|
918
1063
|
}
|
|
919
1064
|
const stylesheets = imports.filter((m) => m.target.endsWith(".css") && !m.target.includes("*")).map((m) => path.resolve(root, m.target)).filter((f) => fs.existsSync(f));
|
|
@@ -1031,10 +1176,11 @@ function importedSubpaths(root, names) {
|
|
|
1031
1176
|
}
|
|
1032
1177
|
/**
|
|
1033
1178
|
* `exports` subpaths with a source target, as specifiers relative to `root`. A target
|
|
1034
|
-
* built to `dist/` maps back to its source (`src/`, through the build config's entries)
|
|
1035
|
-
*
|
|
1179
|
+
* built to `dist/` maps back to its source (`src/`, through the build config's entries),
|
|
1180
|
+
* unless `asWritten`: then each target is the one an app that installs the package resolves
|
|
1181
|
+
* ({@link APP_CONDITIONS}), as it is. Without an `exports` map, `main` stands for `.`.
|
|
1036
1182
|
*/
|
|
1037
|
-
function exportMappings(root, dir, pkg) {
|
|
1183
|
+
function exportMappings(root, dir, pkg, asWritten = false) {
|
|
1038
1184
|
const name = typeof pkg.name === "string" ? pkg.name : void 0;
|
|
1039
1185
|
if (!name) return [];
|
|
1040
1186
|
const exports = pkg.exports ?? (typeof pkg.main === "string" && /^\.?\/?(?:dist|build|lib)\//.test(pkg.main) ? pkg.main : void 0);
|
|
@@ -1042,9 +1188,9 @@ function exportMappings(root, dir, pkg) {
|
|
|
1042
1188
|
let build;
|
|
1043
1189
|
const mappings = [];
|
|
1044
1190
|
for (const [key, value] of entries) {
|
|
1045
|
-
const target = exportTarget(value);
|
|
1191
|
+
const target = exportTarget(value, asWritten ? APP_CONDITIONS : void 0);
|
|
1046
1192
|
if (!target) continue;
|
|
1047
|
-
const source = target.includes("*") ? sourcePattern(dir, target) : sourceTarget(dir, key, target, build ??= buildEntries(dir));
|
|
1193
|
+
const source = asWritten ? void 0 : target.includes("*") ? sourcePattern(dir, target) : sourceTarget(dir, key, target, build ??= buildEntries(dir));
|
|
1048
1194
|
mappings.push({
|
|
1049
1195
|
specifier: key === "." ? name : `${name}/${key.replace(/^\.\//, "")}`,
|
|
1050
1196
|
target: relativePath(root, path.resolve(dir, source ?? target))
|
|
@@ -1052,6 +1198,23 @@ function exportMappings(root, dir, pkg) {
|
|
|
1052
1198
|
}
|
|
1053
1199
|
return mappings;
|
|
1054
1200
|
}
|
|
1201
|
+
/**
|
|
1202
|
+
* A compiled file in package `dir`'s build folder: JavaScript, JSX or CSS (`dist/index.mjs`,
|
|
1203
|
+
* `lib/button.jsx`, `dist/index.css`), or a pattern of them (`dist/*.jsx`). TypeScript there
|
|
1204
|
+
* (`lib/utils.ts`) is source.
|
|
1205
|
+
*/
|
|
1206
|
+
function isBuildOutput(dir, file) {
|
|
1207
|
+
return /^(?:dist|build|lib|out|esm|cjs)\/.*\.(?:[cm]?jsx?|css)$/.test(toPosix(path.relative(dir, file)));
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* An `exports` target of installed package `dir` that is compiled: its build output, or
|
|
1211
|
+
* JavaScript in any folder (`./index.js`, `./es/index.mjs`, `./build-module/index.js`), which a
|
|
1212
|
+
* package published from TypeScript or JSX sources ships compiled. Its re-exports are not
|
|
1213
|
+
* followed, so a package that ships a `.js` barrel in front of `.jsx` sources is not read.
|
|
1214
|
+
*/
|
|
1215
|
+
function isCompiledExport(dir, file) {
|
|
1216
|
+
return isBuildOutput(dir, file) || /\.[cm]?js$/.test(file);
|
|
1217
|
+
}
|
|
1055
1218
|
/** `./dist/*.mjs` → `src/*.tsx` (or `.ts`) when files match it there; anything else as it is. */
|
|
1056
1219
|
function sourcePattern(dir, target) {
|
|
1057
1220
|
const relative = target.replace(/^\.\//, "");
|
|
@@ -1067,19 +1230,40 @@ function sourcePattern(dir, target) {
|
|
|
1067
1230
|
}
|
|
1068
1231
|
return relative;
|
|
1069
1232
|
}
|
|
1070
|
-
/**
|
|
1071
|
-
|
|
1233
|
+
/**
|
|
1234
|
+
* The `exports` conditions an app resolves an installed package through: Node.js's own
|
|
1235
|
+
* (`import`, `require`, `module-sync`, `node`, `default`), and the `module`, `browser` and
|
|
1236
|
+
* `style` that bundlers and Tailwind CSS add. Others, such as `source`, `expo-source` and
|
|
1237
|
+
* `development`, lead the package's own repository to its sources, and an app that installs it
|
|
1238
|
+
* builds the target after them: `@expo/ui`'s `"default": "./build/universal/index.js"`, not its
|
|
1239
|
+
* `"expo-source": "./src/universal/index.ts"` before it.
|
|
1240
|
+
*/
|
|
1241
|
+
const APP_CONDITIONS = /* @__PURE__ */ new Set([
|
|
1242
|
+
"import",
|
|
1243
|
+
"require",
|
|
1244
|
+
"module-sync",
|
|
1245
|
+
"node",
|
|
1246
|
+
"module",
|
|
1247
|
+
"browser",
|
|
1248
|
+
"style",
|
|
1249
|
+
"default"
|
|
1250
|
+
]);
|
|
1251
|
+
/**
|
|
1252
|
+
* The source file an export condition map points at: the first string target that is not a
|
|
1253
|
+
* declaration file, through any condition but `types`, or through `conditions` alone.
|
|
1254
|
+
*/
|
|
1255
|
+
function exportTarget(value, conditions) {
|
|
1072
1256
|
if (typeof value === "string") return value.endsWith(".d.ts") ? void 0 : value;
|
|
1073
1257
|
if (Array.isArray(value)) {
|
|
1074
1258
|
for (const item of value) {
|
|
1075
|
-
const target = exportTarget(item);
|
|
1259
|
+
const target = exportTarget(item, conditions);
|
|
1076
1260
|
if (target) return target;
|
|
1077
1261
|
}
|
|
1078
1262
|
return;
|
|
1079
1263
|
}
|
|
1080
1264
|
if (value && typeof value === "object") for (const [condition, nested] of Object.entries(value)) {
|
|
1081
|
-
if (condition === "types") continue;
|
|
1082
|
-
const target = exportTarget(nested);
|
|
1265
|
+
if (condition === "types" || conditions && !conditions.has(condition)) continue;
|
|
1266
|
+
const target = exportTarget(nested, conditions);
|
|
1083
1267
|
if (target) return target;
|
|
1084
1268
|
}
|
|
1085
1269
|
}
|
|
@@ -1157,29 +1341,87 @@ function presetResolver(root) {
|
|
|
1157
1341
|
* Resolves a stylesheet's `@import` to a project file: a path next to the stylesheet
|
|
1158
1342
|
* (`./theme.css`, or `theme.css` as plain CSS reads it), or a stylesheet of a workspace
|
|
1159
1343
|
* package (`@acme/theme/colors.css`, or `@acme/theme` through its `exports` or its `style`
|
|
1160
|
-
* field).
|
|
1344
|
+
* field). An installed package resolves only when zero config reads it as a design system
|
|
1345
|
+
* ({@link installedDesignSystem}), and a stylesheet in it imports others of that package alone.
|
|
1346
|
+
* In an installed package, a stylesheet in a build folder (`dist/index.css`) is compiled, and
|
|
1347
|
+
* resolves to nothing. Other installed packages (`tailwindcss`) and URLs resolve to nothing.
|
|
1161
1348
|
*/
|
|
1162
1349
|
function stylesheetResolver(root) {
|
|
1163
1350
|
const packages = new PackageFinder(root);
|
|
1164
|
-
const
|
|
1351
|
+
const designSystems = /* @__PURE__ */ new Map();
|
|
1352
|
+
const designSystem = (name) => {
|
|
1353
|
+
if (!designSystems.has(name)) designSystems.set(name, installedDesignSystem(root, name, packages));
|
|
1354
|
+
return designSystems.get(name);
|
|
1355
|
+
};
|
|
1356
|
+
const stylesheet = (file, within) => [file, `${file}.css`].find((f) => f.endsWith(".css") && (within === void 0 || isInside(within, f) && !isBuildOutput(within, f)) && !/(?:^|[\\/])node_modules[\\/]/.test(within === void 0 ? f : path.relative(within, f)) && isFile(f));
|
|
1165
1357
|
return (specifier, fromFile) => {
|
|
1166
1358
|
if (/^(?:[a-z][\w+.-]*:|\/)/i.test(specifier)) return void 0;
|
|
1167
|
-
const relative = stylesheet(path.resolve(path.dirname(fromFile), specifier));
|
|
1359
|
+
const relative = stylesheet(path.resolve(path.dirname(fromFile), specifier), installedPackageOf(fromFile));
|
|
1168
1360
|
if (relative || specifier.startsWith(".")) return relative;
|
|
1169
1361
|
const name = packageName(specifier);
|
|
1170
|
-
const
|
|
1362
|
+
const workspace = name ? packages.find(name) : void 0;
|
|
1363
|
+
const installed = name && !workspace ? designSystem(name) : void 0;
|
|
1364
|
+
const dir = workspace ?? installed;
|
|
1171
1365
|
if (!name || !dir) return void 0;
|
|
1172
1366
|
const subpath = specifier.slice(name.length + 1);
|
|
1367
|
+
const key = subpath ? `./${subpath}` : ".";
|
|
1368
|
+
const exported = installed ? installedStylesheet(installed, key) : exportedFile(dir, key, (f) => f.endsWith(".css"));
|
|
1369
|
+
if (exported !== void 0) return exported ?? void 0;
|
|
1173
1370
|
const style = readJson$1(path.join(dir, "package.json"))?.style;
|
|
1174
|
-
return
|
|
1371
|
+
return subpath ? stylesheet(path.join(dir, subpath), installed) : stylesheet(path.resolve(dir, typeof style === "string" ? style : "index.css"), installed);
|
|
1175
1372
|
};
|
|
1176
1373
|
}
|
|
1177
1374
|
/**
|
|
1375
|
+
* The stylesheet an app resolves `subpath` (`./globals.css`) of installed package `dir` to,
|
|
1376
|
+
* as the package's exported stylesheets are found ({@link exportTarget}): the most specific
|
|
1377
|
+
* `exports` entry that maps it decides, through its first stylesheet target under
|
|
1378
|
+
* {@link APP_CONDITIONS}, and a later condition is not tried. `null` when that entry leads to no
|
|
1379
|
+
* stylesheet, or to one in a build folder (`{ "style": "./dist/x.css", "default": "./src/x.css" }`
|
|
1380
|
+
* gives apps the compiled `dist/x.css`); undefined when no entry maps the subpath.
|
|
1381
|
+
*/
|
|
1382
|
+
function installedStylesheet(dir, subpath) {
|
|
1383
|
+
const [match] = exportMatches(dir, subpath);
|
|
1384
|
+
if (!match) return void 0;
|
|
1385
|
+
const target = exportTargets(match.value, APP_CONDITIONS).map((t) => t.replaceAll("*", match.middle)).find((t) => t.endsWith(".css"));
|
|
1386
|
+
const file = target === void 0 ? void 0 : path.resolve(dir, target);
|
|
1387
|
+
return file && isInside(dir, file) && !isBuildOutput(dir, file) && isFile(file) ? file : null;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* The folder of `name` installed from a registry, when zero config reads it as a design system:
|
|
1391
|
+
* named like one, with `exports` that point at the component sources it publishes.
|
|
1392
|
+
*/
|
|
1393
|
+
function installedDesignSystem(root, name, packages) {
|
|
1394
|
+
if (!isDesignSystemName(name)) return void 0;
|
|
1395
|
+
const dir = packages.installed(name);
|
|
1396
|
+
const pkg = dir ? readJson$1(path.join(dir, "package.json")) : void 0;
|
|
1397
|
+
const found = dir && pkg ? fromPackage(root, dir, pkg, ignore, true) : void 0;
|
|
1398
|
+
return found && matchesAny(root, found) ? dir : void 0;
|
|
1399
|
+
}
|
|
1400
|
+
/** The installed package a file is in: `…/node_modules/@acme/ui` for `…/node_modules/@acme/ui/src/a.css`. */
|
|
1401
|
+
function installedPackageOf(file) {
|
|
1402
|
+
const parts = path.resolve(file).split(path.sep);
|
|
1403
|
+
const at = parts.lastIndexOf("node_modules");
|
|
1404
|
+
const first = parts[at + 1] ?? ".";
|
|
1405
|
+
if (at === -1 || first.startsWith(".")) return void 0;
|
|
1406
|
+
const end = at + (first.startsWith("@") ? 3 : 2);
|
|
1407
|
+
return end < parts.length ? parts.slice(0, end).join(path.sep) : void 0;
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1178
1410
|
* The file a package's `exports` map `subpath` (`.`, `./theme`) to, through any condition
|
|
1179
1411
|
* (`style`, `import`, `default`) and `*` patterns: the first target on disk, inside the
|
|
1180
1412
|
* package, that `accept` takes.
|
|
1181
1413
|
*/
|
|
1182
1414
|
function exportedFile(dir, subpath, accept) {
|
|
1415
|
+
for (const { middle, value } of exportMatches(dir, subpath)) for (const target of exportTargets(value)) {
|
|
1416
|
+
const file = path.resolve(dir, target.replaceAll("*", middle));
|
|
1417
|
+
if (isInside(dir, file) && accept(file) && isFile(file)) return file;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* The entries of package `dir`'s `exports` that map `subpath`, with what their `*` stands for:
|
|
1422
|
+
* an exact subpath first, then the most specific pattern, as Node.js picks them.
|
|
1423
|
+
*/
|
|
1424
|
+
function exportMatches(dir, subpath) {
|
|
1183
1425
|
const exports = readJson$1(path.join(dir, "package.json"))?.exports;
|
|
1184
1426
|
const entries = exports && typeof exports === "object" && !Array.isArray(exports) ? Object.keys(exports).every((k) => k.startsWith(".")) ? Object.entries(exports) : [[".", exports]] : exports === void 0 ? [] : [[".", exports]];
|
|
1185
1427
|
const matches = [];
|
|
@@ -1204,17 +1446,13 @@ function exportedFile(dir, subpath, accept) {
|
|
|
1204
1446
|
});
|
|
1205
1447
|
}
|
|
1206
1448
|
}
|
|
1207
|
-
matches.sort((a, b) => b.rank - a.rank);
|
|
1208
|
-
for (const { middle, value } of matches) for (const target of exportTargets(value)) {
|
|
1209
|
-
const file = path.resolve(dir, target.replaceAll("*", middle));
|
|
1210
|
-
if (isInside(dir, file) && accept(file) && isFile(file)) return file;
|
|
1211
|
-
}
|
|
1449
|
+
return matches.sort((a, b) => b.rank - a.rank);
|
|
1212
1450
|
}
|
|
1213
|
-
/** Every target of an export condition map, in order, but its types. */
|
|
1214
|
-
function exportTargets(value) {
|
|
1451
|
+
/** Every target of an export condition map, in order, but its types, or those of `conditions` alone. */
|
|
1452
|
+
function exportTargets(value, conditions) {
|
|
1215
1453
|
if (typeof value === "string") return [value];
|
|
1216
|
-
if (Array.isArray(value)) return value.flatMap(exportTargets);
|
|
1217
|
-
if (value && typeof value === "object") return Object.entries(value).flatMap(([condition, nested]) => condition === "types" ? [] : exportTargets(nested));
|
|
1454
|
+
if (Array.isArray(value)) return value.flatMap((item) => exportTargets(item, conditions));
|
|
1455
|
+
if (value && typeof value === "object") return Object.entries(value).flatMap(([condition, nested]) => condition === "types" || conditions && !conditions.has(condition) ? [] : exportTargets(nested, conditions));
|
|
1218
1456
|
return [];
|
|
1219
1457
|
}
|
|
1220
1458
|
/**
|
|
@@ -1389,14 +1627,8 @@ var PackageFinder = class {
|
|
|
1389
1627
|
* checkout that is not installed (only then is the workspace scanned). With its package.json.
|
|
1390
1628
|
*/
|
|
1391
1629
|
locate(name) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
const json = cachedManifest(path.join(dir, "package.json"));
|
|
1395
|
-
if (json) return {
|
|
1396
|
-
dir,
|
|
1397
|
-
json
|
|
1398
|
-
};
|
|
1399
|
-
}
|
|
1630
|
+
const found = this.inNodeModules(name);
|
|
1631
|
+
if (found) return found;
|
|
1400
1632
|
this.linkable ??= scanWorkspace(this.root, false);
|
|
1401
1633
|
const dir = this.linkable.get(name);
|
|
1402
1634
|
const json = dir ? cachedManifest(path.join(dir, "package.json")) : void 0;
|
|
@@ -1421,6 +1653,30 @@ var PackageFinder = class {
|
|
|
1421
1653
|
boundary: installed ? dir : this.boundaryOf(dir)
|
|
1422
1654
|
};
|
|
1423
1655
|
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Where `name` is installed from a registry, rather than linked to a workspace package: its
|
|
1658
|
+
* real folder, where pnpm puts its own dependencies beside it, expressed from the root. When
|
|
1659
|
+
* that is outside the root and its workspace (pnpm's store elsewhere on the machine), the
|
|
1660
|
+
* folder in `node_modules` it was found by, the only way the loader reads it.
|
|
1661
|
+
*/
|
|
1662
|
+
installed(name) {
|
|
1663
|
+
const located = this.inNodeModules(name);
|
|
1664
|
+
if (!located) return void 0;
|
|
1665
|
+
const placed = this.place(located.dir);
|
|
1666
|
+
if (!placed.installed) return void 0;
|
|
1667
|
+
return isInside(this.root, placed.dir) || this.boundaryOf(placed.dir) !== placed.dir ? placed.dir : path.resolve(located.dir);
|
|
1668
|
+
}
|
|
1669
|
+
/** `name` in the nearest `node_modules` at or above the root that has it, with its package.json. */
|
|
1670
|
+
inNodeModules(name) {
|
|
1671
|
+
for (const nodeModules of this.nodeModulesDirs()) {
|
|
1672
|
+
const dir = path.join(nodeModules, ...name.split("/"));
|
|
1673
|
+
const json = cachedManifest(path.join(dir, "package.json"));
|
|
1674
|
+
if (json) return {
|
|
1675
|
+
dir,
|
|
1676
|
+
json
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1424
1680
|
/** The `node_modules` folders at and above the root, nearest first, looked up once. */
|
|
1425
1681
|
nodeModulesDirs() {
|
|
1426
1682
|
if (!this.nodeModules) {
|
|
@@ -2881,7 +3137,7 @@ const librariesSchema = z.union([z.literal(false), z.array(z.enum(LIBRARY_IDS))]
|
|
|
2881
3137
|
const configShape = {
|
|
2882
3138
|
$schema: z.string().optional(),
|
|
2883
3139
|
components: globsSchema.optional().describe("Globs for component source files, relative to the root."),
|
|
2884
|
-
exclude: z.array(z.string()).optional().describe("Globs to
|
|
3140
|
+
exclude: z.array(z.string()).optional().describe("Globs for files to leave out, relative to the root: `check` in folders and globs, the ESLint plugin, the Claude Code hook and check_ui leave them out, and finding components skips them. A folder (`app/legacy`) covers everything in it. Adds to the defaults (node_modules, .d.ts files, tests and stories); a pattern that starts with `!` takes out the default it names, such as `!**/*.d.ts`."),
|
|
2885
3141
|
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)."),
|
|
2886
3142
|
docs: globsSchema.optional().describe("Globs for per-component Markdown/MDX docs."),
|
|
2887
3143
|
tsconfig: z.string().optional().describe("tsconfig used to resolve types and path aliases."),
|
|
@@ -3001,6 +3257,7 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3001
3257
|
const excludeFromBase = (e) => {
|
|
3002
3258
|
if (!inherited || e.startsWith("**/")) return e;
|
|
3003
3259
|
if (declared) return fromBase(e);
|
|
3260
|
+
if (e === toProject) return "**";
|
|
3004
3261
|
return e.startsWith(`${toProject}/`) ? e.slice(toProject.length + 1) : void 0;
|
|
3005
3262
|
};
|
|
3006
3263
|
const outside = [];
|
|
@@ -3048,7 +3305,14 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3048
3305
|
path: p,
|
|
3049
3306
|
fallback: true
|
|
3050
3307
|
}))];
|
|
3051
|
-
const
|
|
3308
|
+
const tidy = (e) => slashGlob(e).replace(/^(?:\.\/)+/, "").replace(/(.)\/+$/, "$1");
|
|
3309
|
+
const listed = config.exclude ?? [];
|
|
3310
|
+
const dropped = new Set(listed.filter((e) => e.startsWith("!")).map((e) => tidy(e.slice(1))));
|
|
3311
|
+
for (const pattern of dropped) {
|
|
3312
|
+
if (DEFAULT_EXCLUDE.includes(pattern)) continue;
|
|
3313
|
+
warnings.push(`"!${pattern}" in "exclude" takes out none of the defaults, which are ${DEFAULT_EXCLUDE.map((d) => `"${d}"`).join(", ")}: a pattern that starts with "!" names one of them.`);
|
|
3314
|
+
}
|
|
3315
|
+
const exclude = [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE.filter((d) => !dropped.has(d)), ...listed.filter((e) => !e.startsWith("!")).map(tidy)])].map(excludeFromBase).filter((e) => e !== void 0);
|
|
3052
3316
|
return {
|
|
3053
3317
|
root,
|
|
3054
3318
|
configFile,
|
|
@@ -3130,6 +3394,20 @@ function resolveRule(setting) {
|
|
|
3130
3394
|
options: setting[1] ?? {}
|
|
3131
3395
|
};
|
|
3132
3396
|
}
|
|
3397
|
+
/** The default test and story patterns, which `--include-tests` lifts. */
|
|
3398
|
+
function isTestPattern(pattern) {
|
|
3399
|
+
return pattern === "**/*.{test,spec,stories}.{ts,tsx,js,jsx}" || pattern === "**/__{tests,stories,mocks}__/**";
|
|
3400
|
+
}
|
|
3401
|
+
/**
|
|
3402
|
+
* The `exclude` pattern that leaves `file` (relative to the root) out, or undefined when none
|
|
3403
|
+
* does. The one test `check` (in folders and globs, and for files named outright with
|
|
3404
|
+
* `--skip-excluded`), the ESLint plugin, the Claude Code hook and check_ui all apply, so they
|
|
3405
|
+
* leave out the same files. A pattern covers the files it matches and everything in a folder it
|
|
3406
|
+
* matches. With `includeTests`, the default test and story patterns do not count.
|
|
3407
|
+
*/
|
|
3408
|
+
function excludedBy(config, file, options = {}) {
|
|
3409
|
+
return config.exclude.find((pattern) => !(options.includeTests && isTestPattern(pattern)) && coveredByGlob(file, pattern));
|
|
3410
|
+
}
|
|
3133
3411
|
/**
|
|
3134
3412
|
* The rules for one file: the config's `rules`, then every override whose
|
|
3135
3413
|
* `files` match the path (relative to the root), in order.
|
|
@@ -4703,6 +4981,17 @@ function walk$1(root, enter) {
|
|
|
4703
4981
|
}
|
|
4704
4982
|
}
|
|
4705
4983
|
const VARIANT_FUNCTIONS = /^(cva|tv)$/;
|
|
4984
|
+
/** How many names deep a class value is followed: `className={tone}` with `const tone = base`. */
|
|
4985
|
+
const MAX_FOLLOW = 4;
|
|
4986
|
+
/** The node around `node`, or undefined at the file. */
|
|
4987
|
+
function parentScope(node) {
|
|
4988
|
+
return ts.isSourceFile(node) ? void 0 : node.parent;
|
|
4989
|
+
}
|
|
4990
|
+
/** Whether a declared name (`tone`, or one in `{ tone, size }`) binds `name`. */
|
|
4991
|
+
function binds(declared, name) {
|
|
4992
|
+
if (ts.isIdentifier(declared)) return declared.text === name;
|
|
4993
|
+
return declared.elements.some((e) => !ts.isOmittedExpression(e) && binds(e.name, name));
|
|
4994
|
+
}
|
|
4706
4995
|
function analyze(sourceFile) {
|
|
4707
4996
|
const analysis = {
|
|
4708
4997
|
imports: /* @__PURE__ */ new Map(),
|
|
@@ -4751,10 +5040,100 @@ function analyze(sourceFile) {
|
|
|
4751
5040
|
for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
|
|
4752
5041
|
}
|
|
4753
5042
|
};
|
|
5043
|
+
/**
|
|
5044
|
+
* The `const` value or function a name refers to where it is used, declared in this file and
|
|
5045
|
+
* in scope there: undefined for a parameter, an import, a `let` or anything else that can
|
|
5046
|
+
* hold another value.
|
|
5047
|
+
*/
|
|
5048
|
+
const declarationOf = (use) => {
|
|
5049
|
+
const name = use.text;
|
|
5050
|
+
for (let node = use.parent; node; node = parentScope(node)) {
|
|
5051
|
+
if (ts.isFunctionLike(node) && node.parameters.some((p) => binds(p.name, name))) return;
|
|
5052
|
+
if (ts.isCatchClause(node) && node.variableDeclaration) {
|
|
5053
|
+
if (binds(node.variableDeclaration.name, name)) return;
|
|
5054
|
+
}
|
|
5055
|
+
if ((ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isForInStatement(node)) && node.initializer && ts.isVariableDeclarationList(node.initializer) && node.initializer.declarations.some((d) => binds(d.name, name))) return;
|
|
5056
|
+
const statements = ts.isBlock(node) || ts.isSourceFile(node) || ts.isModuleBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node) ? node.statements : void 0;
|
|
5057
|
+
for (const statement of statements ?? []) {
|
|
5058
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name) return statement;
|
|
5059
|
+
if ((ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement)) && statement.name?.text === name) return;
|
|
5060
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
5061
|
+
const list = statement.declarationList;
|
|
5062
|
+
const declaration = list.declarations.find((d) => binds(d.name, name));
|
|
5063
|
+
if (!declaration) continue;
|
|
5064
|
+
return (list.flags & ts.NodeFlags.Const) !== 0 && ts.isIdentifier(declaration.name) ? declaration.initializer : void 0;
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
};
|
|
5068
|
+
/** A same-file `const` value: `"bg-green-50"`, a condition, an object map. */
|
|
5069
|
+
const constValue = (use) => {
|
|
5070
|
+
const declaration = declarationOf(use);
|
|
5071
|
+
return declaration && !ts.isFunctionDeclaration(declaration) ? declaration : void 0;
|
|
5072
|
+
};
|
|
5073
|
+
/** The object literals a value can be: a `const` map, or a map in one (`styles.badge`). */
|
|
5074
|
+
const objectsOf = (expr, depth) => {
|
|
5075
|
+
if (depth > MAX_FOLLOW) return [];
|
|
5076
|
+
return valueBranches(expr).flatMap((leaf) => {
|
|
5077
|
+
if (ts.isObjectLiteralExpression(leaf)) return [leaf];
|
|
5078
|
+
if (ts.isIdentifier(leaf)) {
|
|
5079
|
+
const value = constValue(leaf);
|
|
5080
|
+
return value ? objectsOf(value, depth + 1) : [];
|
|
5081
|
+
}
|
|
5082
|
+
if (ts.isPropertyAccessExpression(leaf) || ts.isElementAccessExpression(leaf)) return entriesOf(leaf, depth).flatMap((value) => objectsOf(value, depth + 1));
|
|
5083
|
+
return [];
|
|
5084
|
+
});
|
|
5085
|
+
};
|
|
5086
|
+
/** What a key reads from a same-file map: `styles.badge`, or every value of `styles[status]`. */
|
|
5087
|
+
const entriesOf = (access, depth) => {
|
|
5088
|
+
const key = ts.isPropertyAccessExpression(access) ? access.name.text : ts.isStringLiteralLike(access.argumentExpression) || ts.isNumericLiteral(access.argumentExpression) ? access.argumentExpression.text : void 0;
|
|
5089
|
+
return objectsOf(access.expression, depth + 1).flatMap((object) => object.properties.flatMap((prop) => ts.isPropertyAssignment(prop) && (key === void 0 || propertyName(prop.name) === key) ? [prop.initializer] : []));
|
|
5090
|
+
};
|
|
5091
|
+
/** What a call to a function of this file returns: `toneOf(status)`. */
|
|
5092
|
+
const returnsOf = (call) => {
|
|
5093
|
+
if (!ts.isIdentifier(call.expression)) return [];
|
|
5094
|
+
const declaration = declarationOf(call.expression);
|
|
5095
|
+
if (declaration && ts.isFunctionDeclaration(declaration)) return returnedExpressions(declaration);
|
|
5096
|
+
const fn = declaration && asFunction(declaration);
|
|
5097
|
+
return fn ? returnedExpressions(fn) : [];
|
|
5098
|
+
};
|
|
5099
|
+
const followed = /* @__PURE__ */ new Set();
|
|
5100
|
+
const follow = (value, element, depth) => {
|
|
5101
|
+
if (followed.has(value)) return;
|
|
5102
|
+
followed.add(value);
|
|
5103
|
+
classValue(value, element, depth + 1);
|
|
5104
|
+
};
|
|
5105
|
+
/**
|
|
5106
|
+
* The classes a class value holds where it is not written out: a `const` (`className={tone}`,
|
|
5107
|
+
* `cn("p-4", extra)`), a key of a `const` map (`statusStyles[status]`), what a function of
|
|
5108
|
+
* this file returns (`toneOf(status)`), and the parts of a template or a concatenation,
|
|
5109
|
+
* through conditions. Strings written out in it are collected as well, once.
|
|
5110
|
+
*/
|
|
5111
|
+
const classValue = (expr, element, depth) => {
|
|
5112
|
+
if (depth > MAX_FOLLOW) return;
|
|
5113
|
+
for (const leaf of valueBranches(expr)) if (ts.isStringLiteralLike(leaf) || ts.isObjectLiteralExpression(leaf)) collectStrings(leaf, element);
|
|
5114
|
+
else if (ts.isTemplateExpression(leaf)) {
|
|
5115
|
+
collectStrings(leaf, element);
|
|
5116
|
+
for (const span of leaf.templateSpans) classValue(span.expression, element, depth);
|
|
5117
|
+
} else if (ts.isBinaryExpression(leaf) && leaf.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
5118
|
+
classValue(leaf.left, element, depth);
|
|
5119
|
+
classValue(leaf.right, element, depth);
|
|
5120
|
+
} else if (ts.isArrayLiteralExpression(leaf)) for (const item of leaf.elements) classValue(ts.isSpreadElement(item) ? item.expression : item, element, depth);
|
|
5121
|
+
else if (ts.isIdentifier(leaf)) {
|
|
5122
|
+
const value = constValue(leaf);
|
|
5123
|
+
if (value) follow(value, element, depth);
|
|
5124
|
+
} else if (ts.isPropertyAccessExpression(leaf) || ts.isElementAccessExpression(leaf)) for (const value of entriesOf(leaf, depth)) follow(value, element, depth);
|
|
5125
|
+
else if (ts.isCallExpression(leaf)) {
|
|
5126
|
+
const callee = leaf.expression.getText(sourceFile);
|
|
5127
|
+
if (CLASS_FUNCTIONS.test(callee)) {
|
|
5128
|
+
if (depth === 0 && !VARIANT_FUNCTIONS.test(callee)) for (const arg of leaf.arguments) classValue(arg, element, depth);
|
|
5129
|
+
} else for (const value of returnsOf(leaf)) follow(value, element, depth);
|
|
5130
|
+
}
|
|
5131
|
+
};
|
|
4754
5132
|
/** A class value, or an object of them: tv slots, and variant options that style slots. */
|
|
4755
5133
|
const collectClassValues = (node) => {
|
|
4756
5134
|
if (!ts.isObjectLiteralExpression(node)) {
|
|
4757
5135
|
collectStrings(node);
|
|
5136
|
+
classValue(node, void 0, 0);
|
|
4758
5137
|
return;
|
|
4759
5138
|
}
|
|
4760
5139
|
for (const prop of node.properties) if (ts.isPropertyAssignment(prop)) collectClassValues(prop.initializer);
|
|
@@ -4819,7 +5198,10 @@ function analyze(sourceFile) {
|
|
|
4819
5198
|
const name = attribute.name.getText(sourceFile);
|
|
4820
5199
|
const init = attribute.initializer;
|
|
4821
5200
|
if (!init) continue;
|
|
4822
|
-
if (name === "className" || name === "class")
|
|
5201
|
+
if (name === "className" || name === "class") {
|
|
5202
|
+
collectStrings(init, element);
|
|
5203
|
+
if (ts.isJsxExpression(init) && init.expression) classValue(init.expression, element, 0);
|
|
5204
|
+
}
|
|
4823
5205
|
if (name === "style" && ts.isJsxExpression(init) && init.expression) for (const object of styleObjects(init.expression)) analysis.styles.push({
|
|
4824
5206
|
element,
|
|
4825
5207
|
object
|
|
@@ -4830,7 +5212,10 @@ function analyze(sourceFile) {
|
|
|
4830
5212
|
const variants = VARIANT_FUNCTIONS.test(node.expression.getText(sourceFile));
|
|
4831
5213
|
node.arguments.forEach((arg) => {
|
|
4832
5214
|
if (variants && ts.isObjectLiteralExpression(arg)) collectVariantConfig(arg);
|
|
4833
|
-
else
|
|
5215
|
+
else {
|
|
5216
|
+
collectStrings(arg);
|
|
5217
|
+
classValue(arg, void 0, 0);
|
|
5218
|
+
}
|
|
4834
5219
|
});
|
|
4835
5220
|
}
|
|
4836
5221
|
return true;
|
|
@@ -4901,6 +5286,7 @@ function valueBranches(expr) {
|
|
|
4901
5286
|
* style functions (`style={({ isPressed }) => ({ ... })}`) are read through this.
|
|
4902
5287
|
*/
|
|
4903
5288
|
function returnedExpressions(fn) {
|
|
5289
|
+
if (!fn.body) return [];
|
|
4904
5290
|
if (!ts.isBlock(fn.body)) return [fn.body];
|
|
4905
5291
|
const out = [];
|
|
4906
5292
|
const stack = [fn.body];
|
|
@@ -5364,6 +5750,11 @@ const TAILWIND_PALETTE = {
|
|
|
5364
5750
|
950: "oklch(14.7% 0.004 49.3)"
|
|
5365
5751
|
}
|
|
5366
5752
|
};
|
|
5753
|
+
/** Tailwind's colors without shades: `white` and `black`. */
|
|
5754
|
+
const TAILWIND_KEYWORD_COLORS = {
|
|
5755
|
+
black: "#000",
|
|
5756
|
+
white: "#fff"
|
|
5757
|
+
};
|
|
5367
5758
|
//#endregion
|
|
5368
5759
|
//#region src/tokens/css.ts
|
|
5369
5760
|
/** Tailwind v4 theme namespaces → token category. Longest prefixes first. */
|
|
@@ -8215,6 +8606,7 @@ const preferDesignSystemComponent = {
|
|
|
8215
8606
|
}
|
|
8216
8607
|
const component = context.target.elements.get(key);
|
|
8217
8608
|
if (!component) continue;
|
|
8609
|
+
if (fromRegistry(component, context) && context.target.config.elements[key] === void 0) continue;
|
|
8218
8610
|
if (hiddenFromUser(element)) continue;
|
|
8219
8611
|
if (styledByParent(element, context)) continue;
|
|
8220
8612
|
if (isBare(element)) continue;
|
|
@@ -8232,6 +8624,14 @@ const preferDesignSystemComponent = {
|
|
|
8232
8624
|
}
|
|
8233
8625
|
}
|
|
8234
8626
|
};
|
|
8627
|
+
/**
|
|
8628
|
+
* Whether `component` is in the folder of a shadcn registry that components.json names
|
|
8629
|
+
* (`components/signoff-ui`): a read-only design system, ranked after the others.
|
|
8630
|
+
*/
|
|
8631
|
+
function fromRegistry(component, context) {
|
|
8632
|
+
const file = component.source.file;
|
|
8633
|
+
return (context.target.config.designSystems ?? []).some((source) => source.registry !== void 0 && source.components.some((g) => matchesGlob(file, g)));
|
|
8634
|
+
}
|
|
8235
8635
|
const definitionsByFile = /* @__PURE__ */ new WeakMap();
|
|
8236
8636
|
/** Whether the element's `className` calls a `cva(...)` or `tv(...)` definition declared in this file. */
|
|
8237
8637
|
function styledByOwnVariants(element, context) {
|
|
@@ -9205,14 +9605,58 @@ function isHidden(element) {
|
|
|
9205
9605
|
return opening !== void 0 && hiddenBy(opening.attributes);
|
|
9206
9606
|
}) !== void 0;
|
|
9207
9607
|
}
|
|
9608
|
+
/**
|
|
9609
|
+
* A link a design system renders through `asChild`: `<a>`, a component that renders one, or a
|
|
9610
|
+
* router's `Link` (`next/link`, `NavLink`) with its `href` or `to`, not an icon named `ExternalLink`.
|
|
9611
|
+
*/
|
|
9612
|
+
function isLink(context, element) {
|
|
9613
|
+
const resolution = context.resolve(element);
|
|
9614
|
+
if (resolution.kind === "intrinsic") return resolution.tag === "a";
|
|
9615
|
+
if (resolution.kind === "component" && resolution.component.element === "a") return true;
|
|
9616
|
+
return /Link$/.test(element.tag) && Boolean(findAttribute(element, "href") ?? findAttribute(element, "to"));
|
|
9617
|
+
}
|
|
9618
|
+
/**
|
|
9619
|
+
* `<Button asChild size="icon"><Link href="/users/1/edit"><Pencil /></Link></Button>`: Radix's
|
|
9620
|
+
* Slot renders the link with the button's props, so the link is the button, and an icon alone
|
|
9621
|
+
* in it names nothing. A label on either counts (`aria-label`, `title`, visually hidden text in
|
|
9622
|
+
* the link). A child that is a button itself is checked on its own; any other child (a `<span>`
|
|
9623
|
+
* under a tooltip's trigger) is not a control.
|
|
9624
|
+
*/
|
|
9625
|
+
function checkAsChildLink(context, button) {
|
|
9626
|
+
const children = button.children.filter((c) => !ts.isJsxText(c) || c.text.trim());
|
|
9627
|
+
const [child] = children;
|
|
9628
|
+
if (children.length !== 1 || !child) return;
|
|
9629
|
+
const link = context.analysis.elements.find((e) => e.node === child);
|
|
9630
|
+
if (!link || link.hasSpread || !isLink(context, link)) return;
|
|
9631
|
+
if (hasLabel(button.attributes) || hasLabel(link.attributes)) return;
|
|
9632
|
+
if (labelledByParent(context, button)) return;
|
|
9633
|
+
const icons = [];
|
|
9634
|
+
const content = classify(link.children, context, icons);
|
|
9635
|
+
const sizeIcon = attributeLiterals(button, "size").some((v) => v.text.includes("icon"));
|
|
9636
|
+
if (content !== "icon" && !(content === "empty" && sizeIcon)) return;
|
|
9637
|
+
const { label, fixed } = iconLabel(context, link, button, icons[0]);
|
|
9638
|
+
const attribute = `aria-label="${label ?? "…"}"`;
|
|
9639
|
+
context.report({
|
|
9640
|
+
...tagRange(context, link),
|
|
9641
|
+
message: content === "icon" ? `Icon-only <${link.tag}> in <${button.tag} asChild> has no accessible name. Add ${attribute} describing the action, or visually hidden text.` : `<${link.tag}> in <${button.tag} asChild> has no content and no accessible name. Add ${attribute}.`,
|
|
9642
|
+
suggestion: attribute,
|
|
9643
|
+
fix: fixed ? [{
|
|
9644
|
+
range: [link.tagName.end, link.tagName.end],
|
|
9645
|
+
text: ` ${attribute}`
|
|
9646
|
+
}] : void 0
|
|
9647
|
+
});
|
|
9648
|
+
}
|
|
9208
9649
|
const iconButtonAccessibleName = {
|
|
9209
9650
|
id: "icon-button-accessible-name",
|
|
9210
9651
|
description: "Buttons whose only content is an icon need an aria-label (or visually hidden text).",
|
|
9211
9652
|
run(context) {
|
|
9212
9653
|
for (const element of context.analysis.elements) {
|
|
9213
9654
|
const resolution = context.resolve(element);
|
|
9214
|
-
if (!isButton(context, element) || element.hasSpread) continue;
|
|
9215
|
-
if (findAttribute(element, "asChild")
|
|
9655
|
+
if (!isButton(context, element) || element.hasSpread || isHidden(element)) continue;
|
|
9656
|
+
if (findAttribute(element, "asChild")) {
|
|
9657
|
+
checkAsChildLink(context, element);
|
|
9658
|
+
continue;
|
|
9659
|
+
}
|
|
9216
9660
|
const host = renderHost(context, element);
|
|
9217
9661
|
if (host === "object") continue;
|
|
9218
9662
|
if (host && (isButton(context, host) || host.hasSpread)) continue;
|
|
@@ -9239,6 +9683,8 @@ const iconButtonAccessibleName = {
|
|
|
9239
9683
|
};
|
|
9240
9684
|
//#endregion
|
|
9241
9685
|
//#region src/lint/rules/hardcoded.ts
|
|
9686
|
+
/** `var(--color-green-50)`: a shade of Tailwind's palette through its CSS variable. */
|
|
9687
|
+
const PALETTE_VARIABLE = /^var\(\s*--color-([a-z]+-(?:50|[1-9]00|950))\s*\)$/;
|
|
9242
9688
|
/** Luminance values, not design colors: `white`/`black` (and hex twins). */
|
|
9243
9689
|
const MASK_LUMINANCE = /* @__PURE__ */ new Set([
|
|
9244
9690
|
"white",
|
|
@@ -9304,25 +9750,51 @@ const noHardcodedColor = {
|
|
|
9304
9750
|
const key = classString.element ?? classString;
|
|
9305
9751
|
const list = lists.get(key) ?? [];
|
|
9306
9752
|
lists.set(key, list);
|
|
9753
|
+
const element = classString.element;
|
|
9307
9754
|
for (const token of splitClasses(classString.text, classString.start)) {
|
|
9308
9755
|
const utility = parseUtility(token.value);
|
|
9309
9756
|
const listed = {
|
|
9310
9757
|
value: token.value,
|
|
9311
|
-
prefix: tokenColorPrefix(utility, tokens.colorKeys)
|
|
9758
|
+
prefix: tokenColorPrefix(utility, tokens.colorKeys),
|
|
9759
|
+
element
|
|
9312
9760
|
};
|
|
9313
9761
|
list.push(listed);
|
|
9314
|
-
|
|
9315
|
-
const element = classString.element;
|
|
9762
|
+
const allowed = allow.has(token.value.toLowerCase());
|
|
9316
9763
|
if (utility.arbitrary !== void 0 && utility.prefix && COLOR_PREFIXES.includes(utility.prefix)) {
|
|
9317
9764
|
const value = utility.arbitrary.replace(/^color:/, "").trim();
|
|
9765
|
+
const shade = PALETTE_VARIABLE.exec(value)?.[1];
|
|
9766
|
+
const viaVariable = shade ? paletteColor(`${utility.prefix}-${shade}`) : void 0;
|
|
9767
|
+
if (viaVariable) {
|
|
9768
|
+
const color = tokens.colorKeys.has(viaVariable.key) ? void 0 : parseColor(viaVariable.value);
|
|
9769
|
+
if (!color) continue;
|
|
9770
|
+
listed.color = color;
|
|
9771
|
+
listed.prefix = viaVariable.prefix;
|
|
9772
|
+
listed.tinted = !viaVariable.gray;
|
|
9773
|
+
if (allowed) continue;
|
|
9774
|
+
listed.report = colorReport(context, {
|
|
9775
|
+
start: token.start,
|
|
9776
|
+
end: token.end,
|
|
9777
|
+
what: `\`${token.value}\` is Tailwind's default palette (\`${viaVariable.key}\`), not a design-system color`,
|
|
9778
|
+
color,
|
|
9779
|
+
tinted: !viaVariable.gray,
|
|
9780
|
+
palette: viaVariable.key,
|
|
9781
|
+
prefix: viaVariable.prefix,
|
|
9782
|
+
place: "class",
|
|
9783
|
+
utility,
|
|
9784
|
+
element,
|
|
9785
|
+
classes: classString.text
|
|
9786
|
+
});
|
|
9787
|
+
continue;
|
|
9788
|
+
}
|
|
9318
9789
|
const whole = parseColor(value);
|
|
9319
9790
|
const literal = whole ? {
|
|
9320
9791
|
text: value,
|
|
9321
9792
|
color: whole
|
|
9322
9793
|
} : findColorLiterals(value)[0];
|
|
9323
|
-
if (!literal
|
|
9794
|
+
if (!literal) continue;
|
|
9324
9795
|
listed.color = literal.color;
|
|
9325
9796
|
listed.prefix = whole ? utility.prefix : void 0;
|
|
9797
|
+
if (allowed || allow.has(literal.text.toLowerCase())) continue;
|
|
9326
9798
|
listed.report = colorReport(context, {
|
|
9327
9799
|
start: token.start,
|
|
9328
9800
|
end: token.end,
|
|
@@ -9344,9 +9816,10 @@ const noHardcodedColor = {
|
|
|
9344
9816
|
text: value,
|
|
9345
9817
|
color: whole
|
|
9346
9818
|
} : findColorLiterals(value)[0];
|
|
9347
|
-
if (!literal
|
|
9819
|
+
if (!literal) continue;
|
|
9348
9820
|
listed.color = literal.color;
|
|
9349
9821
|
listed.prefix = whole ? property.prefix : void 0;
|
|
9822
|
+
if (allowed || allow.has(literal.text.toLowerCase())) continue;
|
|
9350
9823
|
listed.report = colorReport(context, {
|
|
9351
9824
|
start: token.start,
|
|
9352
9825
|
end: token.end,
|
|
@@ -9366,6 +9839,8 @@ const noHardcodedColor = {
|
|
|
9366
9839
|
if (!color) continue;
|
|
9367
9840
|
listed.color = color;
|
|
9368
9841
|
listed.prefix = palette.prefix;
|
|
9842
|
+
listed.tinted = !palette.gray;
|
|
9843
|
+
if (allowed) continue;
|
|
9369
9844
|
listed.report = colorReport(context, {
|
|
9370
9845
|
start: token.start,
|
|
9371
9846
|
end: token.end,
|
|
@@ -9379,13 +9854,64 @@ const noHardcodedColor = {
|
|
|
9379
9854
|
element,
|
|
9380
9855
|
classes: classString.text
|
|
9381
9856
|
});
|
|
9857
|
+
continue;
|
|
9858
|
+
}
|
|
9859
|
+
const keyword = keywordColor(utility, tokens.colorKeys);
|
|
9860
|
+
const tint = keyword ? void 0 : keywordColor(utility, tokens.colorKeys, true);
|
|
9861
|
+
if (tint && colorRole(tint.prefix) === "text" && !(element && isInsideMask(element))) {
|
|
9862
|
+
listed.color = tint.color;
|
|
9863
|
+
listed.prefix = tint.prefix;
|
|
9864
|
+
}
|
|
9865
|
+
if (keyword && !(element && isInsideMask(element))) {
|
|
9866
|
+
listed.color = keyword.color;
|
|
9867
|
+
listed.prefix = keyword.prefix;
|
|
9868
|
+
if (allowed) continue;
|
|
9869
|
+
listed.keyword = {
|
|
9870
|
+
start: token.start,
|
|
9871
|
+
end: token.end,
|
|
9872
|
+
what: `\`${token.value}\` is a fixed color, not a design-system color`,
|
|
9873
|
+
color: keyword.color,
|
|
9874
|
+
tinted: false,
|
|
9875
|
+
keyword: true,
|
|
9876
|
+
prefix: keyword.prefix,
|
|
9877
|
+
place: "class",
|
|
9878
|
+
utility,
|
|
9879
|
+
element,
|
|
9880
|
+
classes: classString.text
|
|
9881
|
+
};
|
|
9382
9882
|
}
|
|
9383
9883
|
}
|
|
9384
9884
|
}
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9885
|
+
const surfaces = new Surfaces(context, lists);
|
|
9886
|
+
const texts = [];
|
|
9887
|
+
const fixedTexts = [];
|
|
9888
|
+
for (const list of lists.values()) for (const listed of list) {
|
|
9889
|
+
const text = colorRole(listed.prefix) === "text" && listed.color !== void 0;
|
|
9890
|
+
if (listed.keyword) {
|
|
9891
|
+
if (text) {
|
|
9892
|
+
const placed = surfaces.place(list, listed);
|
|
9893
|
+
texts.push({
|
|
9894
|
+
text: listed,
|
|
9895
|
+
list
|
|
9896
|
+
});
|
|
9897
|
+
if (placed.contrasts) continue;
|
|
9898
|
+
}
|
|
9899
|
+
listed.report = colorReport(context, listed.keyword);
|
|
9900
|
+
} else if (text) (listed.report?.fix ? fixedTexts : texts).push({
|
|
9901
|
+
text: listed,
|
|
9902
|
+
list
|
|
9903
|
+
});
|
|
9904
|
+
}
|
|
9905
|
+
for (const { text, list } of texts) surfaces.holdUnderText(text, list);
|
|
9906
|
+
for (const list of lists.values()) keepStatesApart(list);
|
|
9907
|
+
const stays = new Set(texts.map(({ text }) => text));
|
|
9908
|
+
for (const { text, list } of fixedTexts) {
|
|
9909
|
+
if (text.report?.fix) continue;
|
|
9910
|
+
stays.add(text);
|
|
9911
|
+
surfaces.holdUnderText(text, list);
|
|
9388
9912
|
}
|
|
9913
|
+
surfaces.holdUnderInherited(stays);
|
|
9914
|
+
for (const list of lists.values()) for (const { report } of list) if (report) context.report(report);
|
|
9389
9915
|
for (const { object, element } of context.analysis.styles) for (const prop of object.properties) {
|
|
9390
9916
|
if (!ts.isPropertyAssignment(prop)) continue;
|
|
9391
9917
|
const name = propertyName(prop.name);
|
|
@@ -9474,6 +10000,8 @@ function colorReport(context, input) {
|
|
|
9474
10000
|
const opacityGap = formatOpacityGap(input.color, value, `${token.name}${inMode}`);
|
|
9475
10001
|
const tinted = input.tinted ?? isTinted(input.color);
|
|
9476
10002
|
const noHue = !close && reason === "hue" && tinted;
|
|
10003
|
+
const keywordText = input.keyword === true && role === "text";
|
|
10004
|
+
const keptBack = input.keyword && close && edit && !held && !status ? keywordText ? "White or black text depends on the surface under it, so this is not applied automatically: use the foreground token of that surface." : nearest.distance >= .02 ? "Only a token of exactly this color is applied automatically." : void 0 : void 0;
|
|
9477
10005
|
let message;
|
|
9478
10006
|
if (close) {
|
|
9479
10007
|
message = `${input.what}. ${nearest.distance < .02 ? "Matches token" : "Nearest token"} ${found}`;
|
|
@@ -9483,6 +10011,7 @@ function colorReport(context, input) {
|
|
|
9483
10011
|
if (opacityGap) message += ` ${opacityGap}`;
|
|
9484
10012
|
if (status) message += ` ${status}`;
|
|
9485
10013
|
else if (held) message += ` ${held}`;
|
|
10014
|
+
else if (keptBack) message += ` ${keptBack}`;
|
|
9486
10015
|
} else if (noHue) message = `${input.what}. ${NO_TOKEN_HAS_HUE}`;
|
|
9487
10016
|
else message = `${input.what}. ${noMatch(reason, role, found, hueOff)}${meantForAnother(otherRole, input.color, mode)}${opacityGap ? ` ${opacityGap}` : ""} Pick the semantic token that fits.`;
|
|
9488
10017
|
const owner = !variant && input.prefix && input.element ? variantProp(context, input.element, input.prefix) : void 0;
|
|
@@ -9492,11 +10021,11 @@ function colorReport(context, input) {
|
|
|
9492
10021
|
end: input.end,
|
|
9493
10022
|
message,
|
|
9494
10023
|
suggestion: close ? variant ?? replacement : void 0,
|
|
9495
|
-
fix: close && edit && !held && !status ? [{
|
|
10024
|
+
fix: close && edit && !held && !status && !keptBack ? [{
|
|
9496
10025
|
range: [input.start, input.end],
|
|
9497
10026
|
text: edit
|
|
9498
10027
|
}] : void 0,
|
|
9499
|
-
severity: noHue ? "warning" : void 0
|
|
10028
|
+
severity: noHue || keywordText ? "warning" : void 0
|
|
9500
10029
|
};
|
|
9501
10030
|
}
|
|
9502
10031
|
/**
|
|
@@ -9555,6 +10084,390 @@ function tokenColorPrefix(utility, colorKeys) {
|
|
|
9555
10084
|
if (!key || !(colorKeys.has(key) || KEYWORD_COLORS.has(key))) return void 0;
|
|
9556
10085
|
return utility.base.slice(0, -key.length - 1);
|
|
9557
10086
|
}
|
|
10087
|
+
/** `bg-white`, `border-black`: Tailwind's colors without a shade, as a color utility sets them. */
|
|
10088
|
+
const KEYWORD_CLASS = new RegExp(`^(${COLOR_PREFIXES.join("|")})-(${Object.keys(TAILWIND_KEYWORD_COLORS).join("|")})$`);
|
|
10089
|
+
/**
|
|
10090
|
+
* The white or black an opaque class sets (`bg-white`, `text-black`). With an opacity modifier
|
|
10091
|
+
* (`bg-black/50`, `border-white/10`) it is a tint of whatever is under it, a scrim or a hairline
|
|
10092
|
+
* that suits both modes, so it is left alone: with `tint`, only such a class is read. A theme
|
|
10093
|
+
* that defines `--color-white` (after `--color-*: initial`) makes `bg-white` one of its tokens.
|
|
10094
|
+
*/
|
|
10095
|
+
function keywordColor(utility, colorKeys, tint = false) {
|
|
10096
|
+
if (Boolean(utility.modifier) !== tint || utility.arbitrary !== void 0) return void 0;
|
|
10097
|
+
const [, prefix, name] = KEYWORD_CLASS.exec(utility.base) ?? [];
|
|
10098
|
+
if (!prefix || !name || colorKeys.has(name)) return void 0;
|
|
10099
|
+
const color = parseColor(TAILWIND_KEYWORD_COLORS[name] ?? "");
|
|
10100
|
+
return color ? {
|
|
10101
|
+
prefix,
|
|
10102
|
+
color
|
|
10103
|
+
} : void 0;
|
|
10104
|
+
}
|
|
10105
|
+
/** The node around `node`, or undefined at the file. */
|
|
10106
|
+
function parentOf(node) {
|
|
10107
|
+
return ts.isSourceFile(node) ? void 0 : node.parent;
|
|
10108
|
+
}
|
|
10109
|
+
/** Background classes that set no background of their own: what is under them shows through. */
|
|
10110
|
+
const NO_BACKGROUND = /^bg-(?:transparent|current|inherit|none)$/;
|
|
10111
|
+
/** Style properties that set an element's background, and values that set none. */
|
|
10112
|
+
const STYLE_BACKGROUNDS = /* @__PURE__ */ new Set([
|
|
10113
|
+
"background",
|
|
10114
|
+
"backgroundColor",
|
|
10115
|
+
"backgroundImage"
|
|
10116
|
+
]);
|
|
10117
|
+
const NO_STYLE_BACKGROUND = /^(?:transparent|none|inherit|initial|unset|revert|currentcolor)$/i;
|
|
10118
|
+
/** `var(--primary)`: a custom property, which can be a token's. */
|
|
10119
|
+
const CSS_VARIABLE = /^var\(\s*(--[\w-]+)\s*\)$/;
|
|
10120
|
+
/**
|
|
10121
|
+
* How far apart in lightness (OKLCH L) white or black text and its surface must be for the
|
|
10122
|
+
* surface to be the text's own: white on `bg-indigo-600` or `bg-orange-500` (0.705), not on
|
|
10123
|
+
* `bg-amber-400` or `bg-muted`; black on `bg-yellow-300`, not on `bg-indigo-600`. A token is
|
|
10124
|
+
* judged in each mode the text shows in: shadcn/ui's `primary` is near white in dark mode, so
|
|
10125
|
+
* white text on `bg-primary` is not its own there.
|
|
10126
|
+
*/
|
|
10127
|
+
const SURFACE_CONTRAST = .25;
|
|
10128
|
+
/** Whether text of this color stands out from a surface of this color. */
|
|
10129
|
+
function standsOut(surface, text) {
|
|
10130
|
+
return Math.abs(surface.l - text.l) >= SURFACE_CONTRAST;
|
|
10131
|
+
}
|
|
10132
|
+
/**
|
|
10133
|
+
* WCAG's least contrast for large text and the parts of a control. Lightness alone says too
|
|
10134
|
+
* little near black: `text-neutral-600` is 0.44 lighter than black and has 2.7:1 on it, and
|
|
10135
|
+
* `text-neutral-800` 1.6:1.
|
|
10136
|
+
*/
|
|
10137
|
+
const TEXT_CONTRAST = 3;
|
|
10138
|
+
/** How hardcoded text reads on a surface: far enough apart in lightness, and in WCAG contrast. */
|
|
10139
|
+
function legibility(surface, text) {
|
|
10140
|
+
return {
|
|
10141
|
+
apart: standsOut(surface, text),
|
|
10142
|
+
contrast: wcagContrast(surface, text) >= TEXT_CONTRAST
|
|
10143
|
+
};
|
|
10144
|
+
}
|
|
10145
|
+
/** The color a text, fill or stroke class hardcodes, or undefined when it sets none. */
|
|
10146
|
+
function paintOf(text) {
|
|
10147
|
+
if (colorRole(text.prefix) !== "text" || !text.color) return void 0;
|
|
10148
|
+
return {
|
|
10149
|
+
color: text.color,
|
|
10150
|
+
variants: parseUtility(text.value).variants,
|
|
10151
|
+
element: text.element
|
|
10152
|
+
};
|
|
10153
|
+
}
|
|
10154
|
+
/**
|
|
10155
|
+
* Where white or black text sits, which says whether the text is the surface's contrast color
|
|
10156
|
+
* (shadcn/ui's `bg-destructive text-white`) or the theme's: the background its own element sets,
|
|
10157
|
+
* in `style` or in its class list, its component's variants set (`<Button>` with `bg-*`
|
|
10158
|
+
* variants), or the nearest element around it that sets one. Any other hardcoded text is placed
|
|
10159
|
+
* the same way, to hold the fix of a background it would not stand out from.
|
|
10160
|
+
*/
|
|
10161
|
+
var Surfaces = class {
|
|
10162
|
+
context;
|
|
10163
|
+
lists;
|
|
10164
|
+
byNode = /* @__PURE__ */ new Map();
|
|
10165
|
+
tokenColors = /* @__PURE__ */ new Map();
|
|
10166
|
+
byVar = /* @__PURE__ */ new Map();
|
|
10167
|
+
/** Backgrounds an element sets in `style`: each value's color, by mode. */
|
|
10168
|
+
styled = /* @__PURE__ */ new Map();
|
|
10169
|
+
held = /* @__PURE__ */ new Set();
|
|
10170
|
+
placed = /* @__PURE__ */ new Map();
|
|
10171
|
+
constructor(context, lists) {
|
|
10172
|
+
this.context = context;
|
|
10173
|
+
this.lists = lists;
|
|
10174
|
+
for (const element of context.analysis.elements) this.byNode.set(element.node, element);
|
|
10175
|
+
for (const candidate of context.target.tokens.colors) {
|
|
10176
|
+
const { tailwind, cssVar } = candidate.token;
|
|
10177
|
+
if (tailwind) this.tokenColors.set(tailwind, candidate);
|
|
10178
|
+
if (cssVar) this.byVar.set(cssVar, candidate);
|
|
10179
|
+
}
|
|
10180
|
+
for (const { element, object } of context.analysis.styles) for (const prop of object.properties) {
|
|
10181
|
+
if (!ts.isPropertyAssignment(prop) && !ts.isShorthandPropertyAssignment(prop)) continue;
|
|
10182
|
+
const name = propertyName(prop.name);
|
|
10183
|
+
if (name === void 0 || !STYLE_BACKGROUNDS.has(name)) continue;
|
|
10184
|
+
const values = ts.isPropertyAssignment(prop) ? valueBranches(prop.initializer) : [prop.name];
|
|
10185
|
+
const colors = this.styled.get(element.node) ?? [];
|
|
10186
|
+
for (const value of values) {
|
|
10187
|
+
const read = this.styleColor(value);
|
|
10188
|
+
if (read) colors.push(read);
|
|
10189
|
+
}
|
|
10190
|
+
if (colors.length) this.styled.set(element.node, colors);
|
|
10191
|
+
}
|
|
10192
|
+
}
|
|
10193
|
+
/**
|
|
10194
|
+
* Where the text sits, in each mode it shows in: a `dark:` class in dark mode only, another
|
|
10195
|
+
* one in its base mode and in dark mode, unless its class list sets its dark color itself
|
|
10196
|
+
* (`text-black dark:text-white`). It is the surface's own only if it stands out in each.
|
|
10197
|
+
*/
|
|
10198
|
+
place(list, text) {
|
|
10199
|
+
let placed = this.placed.get(text);
|
|
10200
|
+
if (!placed) {
|
|
10201
|
+
const paint = paintOf(text);
|
|
10202
|
+
placed = paint ? this.placeInModes(list, text, paint) : { contrasts: true };
|
|
10203
|
+
this.placed.set(text, placed);
|
|
10204
|
+
}
|
|
10205
|
+
return placed;
|
|
10206
|
+
}
|
|
10207
|
+
placeInModes(list, text, paint) {
|
|
10208
|
+
const { variants } = paint;
|
|
10209
|
+
const modes = variants.includes("dark") ? ["dark"] : hasDarkCounterpart(list, text) ? [void 0] : [void 0, "dark"];
|
|
10210
|
+
let under;
|
|
10211
|
+
for (const mode of modes) {
|
|
10212
|
+
const state = mode === "dark" && !variants.includes("dark") ? [...variants, "dark"] : variants;
|
|
10213
|
+
const placed = this.placeIn(list, paint, state, mode);
|
|
10214
|
+
if (!placed.contrasts) return placed;
|
|
10215
|
+
under ??= placed.under;
|
|
10216
|
+
}
|
|
10217
|
+
return {
|
|
10218
|
+
contrasts: true,
|
|
10219
|
+
under
|
|
10220
|
+
};
|
|
10221
|
+
}
|
|
10222
|
+
/**
|
|
10223
|
+
* Takes back the fix of a hardcoded background when hardcoded text on it stays as it is and may
|
|
10224
|
+
* no longer stand out: `bg-black text-white` or `bg-zinc-900 text-white` would become
|
|
10225
|
+
* `bg-primary text-white`, white on near white where `primary` is light (shadcn/ui's dark
|
|
10226
|
+
* mode), and `bg-white text-neutral-800` would become `bg-background text-neutral-800`, dark
|
|
10227
|
+
* gray on near black. The text is on the backgrounds of its own class list, in their states too
|
|
10228
|
+
* (`hover:bg-black`), and on the surface `place` finds for it.
|
|
10229
|
+
*/
|
|
10230
|
+
holdUnderText(text, list) {
|
|
10231
|
+
this.holdUnder(text, list, list);
|
|
10232
|
+
const { under } = this.place(list, text);
|
|
10233
|
+
if (under && under !== list) this.holdUnder(text, list, under);
|
|
10234
|
+
}
|
|
10235
|
+
/**
|
|
10236
|
+
* Takes back the fixes of backgrounds whose text color comes from around them, as text color is
|
|
10237
|
+
* inherited: in `<div className="text-neutral-800"><p className="bg-white">` the paragraph is
|
|
10238
|
+
* dark gray on white. An element in between that sets a text color, or a component, which can
|
|
10239
|
+
* set one of its own, ends it. An element with nothing in it (a skeleton's `<span />`) shows no
|
|
10240
|
+
* text.
|
|
10241
|
+
*/
|
|
10242
|
+
holdUnderInherited(stays) {
|
|
10243
|
+
for (const list of this.lists.values()) {
|
|
10244
|
+
const element = list[0]?.element;
|
|
10245
|
+
if (!element || !element.children.some((c) => !(ts.isJsxText(c) && c.containsOnlyTriviaWhiteSpaces)) || !list.some((listed) => listed.prefix === "bg" && listed.report?.fix)) continue;
|
|
10246
|
+
for (let node = element.node; node; node = parentOf(node)) {
|
|
10247
|
+
const around = this.byNode.get(node);
|
|
10248
|
+
if (!around) continue;
|
|
10249
|
+
if (this.context.resolve(around).kind !== "intrinsic") break;
|
|
10250
|
+
const aroundList = this.lists.get(around) ?? [];
|
|
10251
|
+
const texts = aroundList.filter((listed) => listed.prefix === "text" && !parseUtility(listed.value).variants.some(stylesAnother));
|
|
10252
|
+
if (!texts.length) continue;
|
|
10253
|
+
if (around !== element) {
|
|
10254
|
+
for (const text of texts) if (stays.has(text)) this.holdUnder(text, aroundList, list);
|
|
10255
|
+
}
|
|
10256
|
+
break;
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
}
|
|
10260
|
+
/**
|
|
10261
|
+
* Takes back the fixes of the backgrounds in `under` that the text would not stand out from.
|
|
10262
|
+
* The text loses a token in a mode where it has less than it had on the hardcoded background:
|
|
10263
|
+
* 0.25 of lightness apart, or 3:1 contrast. A token without other modes may still have a dark
|
|
10264
|
+
* one onsystem does not read (`.dark:not(.light)`), so it counts as changing there. The fix
|
|
10265
|
+
* stays when the token keeps that in every mode the text shows in, or the class lists set the
|
|
10266
|
+
* dark-mode background or text (`dark:bg-zinc-900`, `dark:text-white`).
|
|
10267
|
+
*/
|
|
10268
|
+
holdUnder(text, list, under) {
|
|
10269
|
+
const paint = paintOf(text);
|
|
10270
|
+
if (!paint) return;
|
|
10271
|
+
const textIsDark = paint.variants.includes("dark");
|
|
10272
|
+
const textInDark = textIsDark || !hasDarkCounterpart(list, text);
|
|
10273
|
+
for (const surface of under) {
|
|
10274
|
+
const { report, color: surfaceColor } = surface;
|
|
10275
|
+
const fix = report?.fix?.[0];
|
|
10276
|
+
if (!report || !fix || !surfaceColor || surface.prefix !== "bg" || this.held.has(surface)) continue;
|
|
10277
|
+
const surfaceUtility = parseUtility(surface.value);
|
|
10278
|
+
if (surfaceUtility.variants.includes("dark")) continue;
|
|
10279
|
+
const before = legibility(surfaceColor, paint.color);
|
|
10280
|
+
if (!before.apart && !before.contrast) continue;
|
|
10281
|
+
const token = this.fixedTo(fix.text);
|
|
10282
|
+
if (!token) continue;
|
|
10283
|
+
const surfaceInDark = !hasDarkCounterpart(under, surface);
|
|
10284
|
+
const modes = Object.entries(token.modes ?? {});
|
|
10285
|
+
const known = modes.length > 0;
|
|
10286
|
+
if (!known) modes.push(["dark", void 0]);
|
|
10287
|
+
const lost = modes.filter(([mode, value]) => {
|
|
10288
|
+
if (!(mode === "dark" ? textInDark && surfaceInDark : !textIsDark)) return false;
|
|
10289
|
+
if (value === void 0) return true;
|
|
10290
|
+
const after = legibility(value, paint.color);
|
|
10291
|
+
return before.apart && !after.apart || before.contrast && !after.contrast;
|
|
10292
|
+
}).map(([mode]) => mode);
|
|
10293
|
+
if (!lost.length) continue;
|
|
10294
|
+
this.held.add(surface);
|
|
10295
|
+
const textUtility = parseUtility(text.value);
|
|
10296
|
+
const shade = KEYWORD_CLASS.exec(textUtility.base)?.[2] ?? "as it is";
|
|
10297
|
+
const name = token.token.name;
|
|
10298
|
+
const why = !known ? `and ${name} may be another color in a dark mode` : `which would not stand out from ${name} in ${lost.includes("dark") ? "dark mode" : `the ${lost[0] ?? ""} mode`}`;
|
|
10299
|
+
const variants = [...textUtility.variants];
|
|
10300
|
+
if (under === list) {
|
|
10301
|
+
for (const variant of surfaceUtility.variants) if (!variants.includes(variant)) variants.push(variant);
|
|
10302
|
+
}
|
|
10303
|
+
const pair = text.tinted ?? isTinted(paint.color) ? void 0 : this.foregroundOf(token, {
|
|
10304
|
+
...textUtility,
|
|
10305
|
+
variants
|
|
10306
|
+
}, text.prefix);
|
|
10307
|
+
report.message += ` \`${text.value}\` on it stays ${shade}, ${why}, so this is not applied automatically: change the text with it${pair ? `, to \`${pair}\`` : ""}.`;
|
|
10308
|
+
report.fix = void 0;
|
|
10309
|
+
}
|
|
10310
|
+
}
|
|
10311
|
+
/** The token a background's fix puts in: `bg-primary`, or `bg-[var(--chart-bg)]`. */
|
|
10312
|
+
fixedTo(fixed) {
|
|
10313
|
+
const utility = parseUtility(fixed);
|
|
10314
|
+
const variable = utility.arbitrary === void 0 ? void 0 : CSS_VARIABLE.exec(utility.arbitrary.trim())?.[1];
|
|
10315
|
+
if (variable) return this.byVar.get(variable);
|
|
10316
|
+
return this.tokenColors.get(utility.base.slice(3));
|
|
10317
|
+
}
|
|
10318
|
+
/** The text class of the token made for text on `surface`: `text-foreground` on `background`. */
|
|
10319
|
+
foregroundOf(surface, text, prefix) {
|
|
10320
|
+
const key = surface.token.tailwind;
|
|
10321
|
+
if (!key || !prefix) return void 0;
|
|
10322
|
+
const pair = key === "background" ? "foreground" : `${key}-foreground`;
|
|
10323
|
+
return this.tokenColors.has(pair) ? withBase(text, `${prefix}-${pair}`) : void 0;
|
|
10324
|
+
}
|
|
10325
|
+
/**
|
|
10326
|
+
* Where the text sits in one mode, and the class list that says so. Under a tint it does not
|
|
10327
|
+
* stand out from (`bg-white/10` under white text), what is under the tint decides; with nothing
|
|
10328
|
+
* under it in the file, the page's `background` in that mode does.
|
|
10329
|
+
*/
|
|
10330
|
+
placeIn(list, text, state, mode) {
|
|
10331
|
+
const { color, element } = text;
|
|
10332
|
+
/** Whether the text is under a tint it does not stand out from, which shows what is under it. */
|
|
10333
|
+
const seen = { through: false };
|
|
10334
|
+
/** What an element's `style`, then its class list, says: undefined to look further out. */
|
|
10335
|
+
const at = (node, classes, component) => {
|
|
10336
|
+
const styled = node ? this.styleVerdict(node, color, mode) : void 0;
|
|
10337
|
+
if (styled === "through") seen.through = true;
|
|
10338
|
+
if (styled !== void 0) return styled === "through" ? void 0 : { contrasts: styled };
|
|
10339
|
+
const says = this.verdict(classes, color, state, mode);
|
|
10340
|
+
if (says === "through") seen.through = true;
|
|
10341
|
+
else if (says !== void 0) return {
|
|
10342
|
+
contrasts: says,
|
|
10343
|
+
under: classes
|
|
10344
|
+
};
|
|
10345
|
+
if (component && variantProp(this.context, component, "bg")) return { contrasts: true };
|
|
10346
|
+
};
|
|
10347
|
+
const own = at(element?.node, list, element);
|
|
10348
|
+
if (own) return own;
|
|
10349
|
+
for (let node = element?.node.parent; node; node = parentOf(node)) {
|
|
10350
|
+
const around = this.byNode.get(node);
|
|
10351
|
+
if (!around) continue;
|
|
10352
|
+
const placed = at(node, this.lists.get(around) ?? [], around);
|
|
10353
|
+
if (placed) return placed;
|
|
10354
|
+
}
|
|
10355
|
+
if (!seen.through) return { contrasts: false };
|
|
10356
|
+
const page = this.tokenColors.get("background");
|
|
10357
|
+
return { contrasts: !page || standsOut(valueIn(page, mode), color) };
|
|
10358
|
+
}
|
|
10359
|
+
/**
|
|
10360
|
+
* What a class list's background says for the text, or undefined when it sets none. Any
|
|
10361
|
+
* background the text stands out from is its own: a list holds the classes of every branch
|
|
10362
|
+
* of a `cn()` object, which never apply together. Then a tint it does not stand out from,
|
|
10363
|
+
* whose surface is under it: in dark mode, `bg-neutral-50 dark:bg-white/10` is a tint over the
|
|
10364
|
+
* page.
|
|
10365
|
+
*/
|
|
10366
|
+
verdict(list, text, state, mode) {
|
|
10367
|
+
let verdict;
|
|
10368
|
+
for (const { value } of list) {
|
|
10369
|
+
const says = this.excuses(parseUtility(value), text, state, mode);
|
|
10370
|
+
if (says === true) return true;
|
|
10371
|
+
if (says === "through" || verdict === void 0) verdict = says ?? verdict;
|
|
10372
|
+
}
|
|
10373
|
+
return verdict;
|
|
10374
|
+
}
|
|
10375
|
+
/**
|
|
10376
|
+
* What a background set in `style` says for the text: an inline style is over the classes.
|
|
10377
|
+
* A color known only at run time (a swatch) or an image is a surface of its own.
|
|
10378
|
+
*/
|
|
10379
|
+
styleVerdict(node, text, mode) {
|
|
10380
|
+
const colors = this.styled.get(node);
|
|
10381
|
+
if (!colors) return void 0;
|
|
10382
|
+
let verdict = "through";
|
|
10383
|
+
for (const read of colors) {
|
|
10384
|
+
const color = read(mode);
|
|
10385
|
+
if (color === "unknown" || standsOut(color, text)) verdict = true;
|
|
10386
|
+
else if ((color.alpha ?? 1) >= 1) return false;
|
|
10387
|
+
}
|
|
10388
|
+
return verdict;
|
|
10389
|
+
}
|
|
10390
|
+
/** How a style value colors the background: undefined when it sets none. */
|
|
10391
|
+
styleColor(value) {
|
|
10392
|
+
if (value.kind === ts.SyntaxKind.NullKeyword) return void 0;
|
|
10393
|
+
if (ts.isIdentifier(value) && value.text === "undefined") return void 0;
|
|
10394
|
+
if (!ts.isStringLiteralLike(value)) return () => "unknown";
|
|
10395
|
+
const text = value.text.trim();
|
|
10396
|
+
if (!text || NO_STYLE_BACKGROUND.test(text)) return void 0;
|
|
10397
|
+
const variable = CSS_VARIABLE.exec(text)?.[1];
|
|
10398
|
+
const token = variable ? this.byVar.get(variable) : void 0;
|
|
10399
|
+
if (token) return (mode) => valueIn(token, mode);
|
|
10400
|
+
const color = parseColor(text);
|
|
10401
|
+
return () => color ?? "unknown";
|
|
10402
|
+
}
|
|
10403
|
+
/**
|
|
10404
|
+
* Whether a class sets a surface the text contrasts with: a gradient, an image, or a color far
|
|
10405
|
+
* enough from the text's in lightness. An opaque `bg-white` or `bg-black` is the page's own
|
|
10406
|
+
* color, as hardcoded as the text. A tint (`bg-black/60`) is a surface where it stands out;
|
|
10407
|
+
* `bg-white/10` under white text shows what is under it (`through`). Undefined for a class
|
|
10408
|
+
* that sets no background, or sets one only in a state the text is not in: `hover:bg-red-600`
|
|
10409
|
+
* is the surface of `hover:text-white`, not of `text-white`.
|
|
10410
|
+
*/
|
|
10411
|
+
excuses(utility, text, state, mode) {
|
|
10412
|
+
const { base, modifier } = utility;
|
|
10413
|
+
if (!utility.variants.every((variant) => state.includes(variant))) return void 0;
|
|
10414
|
+
if (/^(?:from|via)-/.test(base)) return true;
|
|
10415
|
+
if (!base.startsWith("bg-") || NO_BACKGROUND.test(base)) return void 0;
|
|
10416
|
+
if (keywordColor(utility, this.context.target.tokens.colorKeys)) return false;
|
|
10417
|
+
const color = this.backgroundColor(utility, mode);
|
|
10418
|
+
if (color === "unknown") return true;
|
|
10419
|
+
const contrasts = standsOut(color, text);
|
|
10420
|
+
return modifier && !contrasts ? "through" : contrasts;
|
|
10421
|
+
}
|
|
10422
|
+
/** The color a `bg-*` class sets in a mode: a token's, the palette's, or written in it. */
|
|
10423
|
+
backgroundColor(utility, mode) {
|
|
10424
|
+
const key = utility.base.slice(3);
|
|
10425
|
+
const token = this.tokenColors.get(key);
|
|
10426
|
+
if (token) return valueIn(token, mode);
|
|
10427
|
+
const palette = paletteColor(utility.base);
|
|
10428
|
+
if (palette) return parseColor(palette.value) ?? "unknown";
|
|
10429
|
+
const keyword = TAILWIND_KEYWORD_COLORS[key];
|
|
10430
|
+
if (keyword) return parseColor(keyword) ?? "unknown";
|
|
10431
|
+
if (utility.arbitrary === void 0) return "unknown";
|
|
10432
|
+
const value = utility.arbitrary.replace(/^color:/, "").trim();
|
|
10433
|
+
const shade = PALETTE_VARIABLE.exec(value)?.[1];
|
|
10434
|
+
const variable = shade ? void 0 : CSS_VARIABLE.exec(value)?.[1];
|
|
10435
|
+
const viaVariable = variable ? this.byVar.get(variable) : void 0;
|
|
10436
|
+
if (viaVariable) return valueIn(viaVariable, mode);
|
|
10437
|
+
const color = shade ? paletteColor(`bg-${shade}`)?.value : value;
|
|
10438
|
+
return color && parseColor(color) || "unknown";
|
|
10439
|
+
}
|
|
10440
|
+
};
|
|
10441
|
+
/**
|
|
10442
|
+
* Whether the class list sets the color of `listed` for dark mode too: `text-black dark:text-white`,
|
|
10443
|
+
* `bg-white dark:bg-zinc-900`. Only a class that adds `dark:` and nothing else to its variants,
|
|
10444
|
+
* whatever its color: `dark:bg-darkgray-50` names a color of a config onsystem may not read.
|
|
10445
|
+
*/
|
|
10446
|
+
function hasDarkCounterpart(list, listed) {
|
|
10447
|
+
const { prefix } = listed;
|
|
10448
|
+
if (prefix === void 0) return false;
|
|
10449
|
+
const { variants } = parseUtility(listed.value);
|
|
10450
|
+
if (variants.includes("dark")) return false;
|
|
10451
|
+
return list.some((other) => {
|
|
10452
|
+
if (other === listed) return false;
|
|
10453
|
+
const theirs = parseUtility(other.value);
|
|
10454
|
+
if (other.prefix !== prefix && !(other.prefix === void 0 && setsColor(theirs, prefix))) return false;
|
|
10455
|
+
return theirs.variants.includes("dark") && theirs.variants.every((variant) => variant === "dark" || variants.includes(variant)) && variants.every((variant) => theirs.variants.includes(variant));
|
|
10456
|
+
});
|
|
10457
|
+
}
|
|
10458
|
+
/** Utilities of a color prefix that set no color: `bg-cover`, `text-sm`, `stroke-2`. */
|
|
10459
|
+
const NOT_A_COLOR = {
|
|
10460
|
+
bg: /^bg-(?:auto|cover|contain|fixed|local|scroll|repeat.*|no-repeat|center|(?:top|bottom|left|right)(?:-.*)?|clip-.*|origin-.*|blend-.*|gradient-.*|linear.*|radial.*|conic.*|none|size-.*|position-.*|\[url\(.*)$/,
|
|
10461
|
+
text: /^text-(?:xs|sm|base|lg|\d?xl|left|center|right|justify|start|end|wrap|nowrap|balance|pretty|ellipsis|clip|\[\d.*)$/,
|
|
10462
|
+
fill: /^fill-none$/,
|
|
10463
|
+
stroke: /^stroke-(?:none|\d.*|\[\d.*)$/
|
|
10464
|
+
};
|
|
10465
|
+
/** Whether a class of no known color sets the color of `prefix`: `dark:bg-darkgray-50`. */
|
|
10466
|
+
function setsColor(utility, prefix) {
|
|
10467
|
+
if (!utility.base.startsWith(`${prefix}-`) || COLOR_UTILITY.exec(utility.base) === null) return false;
|
|
10468
|
+
if (utility.prefix !== void 0 && utility.prefix !== prefix) return false;
|
|
10469
|
+
return !NOT_A_COLOR[prefix]?.test(utility.base);
|
|
10470
|
+
}
|
|
9558
10471
|
/**
|
|
9559
10472
|
* Takes back a fix that would make a class under a condition (`hover:`, `focus-visible:`, `md:`)
|
|
9560
10473
|
* the same color as the class without it: `bg-[#2563eb] hover:bg-[#1d4ed8]` would become
|
|
@@ -9823,13 +10736,14 @@ function lengthRule(id, description, spec) {
|
|
|
9823
10736
|
...utility,
|
|
9824
10737
|
negative
|
|
9825
10738
|
}, replacement.cls);
|
|
9826
|
-
const
|
|
10739
|
+
const relative = !replacement.pill && FONT_RELATIVE.test(utility.arbitrary.trim());
|
|
10740
|
+
const message = relative ? `\`${token.value}\` is font-relative: ${utility.arbitrary.trim()} is ${formatPx(px)} only at a 16px font. Nearest step: \`${fixed}\` (${formatPx(replacement.px)}), which does not grow with the font, so it is not applied automatically.` : replacement.exact ? `\`${token.value}\` is ${formatPx(px)}, which is on the ${spec.category} scale: use \`${fixed}\`.` : replacement.pill ? `\`${token.value}\` (${formatPx(px)}) is far above the radius scale, so it reads as fully rounded: use \`${fixed}\`.` : `Hardcoded ${spec.category} \`${token.value}\` (${formatPx(px)}) is off the scale. Nearest: \`${fixed}\` (${formatPx(replacement.px)})` + (replacement.far ? ", too far off to replace automatically." : ".");
|
|
9827
10741
|
context.report({
|
|
9828
10742
|
start: token.start,
|
|
9829
10743
|
end: token.end,
|
|
9830
10744
|
message,
|
|
9831
10745
|
suggestion: fixed,
|
|
9832
|
-
fix: replacement.far ? void 0 : [{
|
|
10746
|
+
fix: replacement.far || relative ? void 0 : [{
|
|
9833
10747
|
range: [token.start, token.end],
|
|
9834
10748
|
text: fixed
|
|
9835
10749
|
}]
|
|
@@ -9858,12 +10772,13 @@ function lengthRule(id, description, spec) {
|
|
|
9858
10772
|
const shown = number !== void 0 ? `${name}: ${raw}` : `${name}: "${raw}"`;
|
|
9859
10773
|
const size = replacement?.pill ? "fully rounded" : formatPx(replacement?.px ?? 0);
|
|
9860
10774
|
const advice = replacement ? replacement.tailwind ? `Use \`${cls}\` (${size}) in className instead of an inline style.` : `Use \`${cssVar ?? cls}\` (${size}).` : `Use ${spec.category} tokens instead.`;
|
|
10775
|
+
const relative = number === void 0 && !replacement?.pill && values.some((v) => FONT_RELATIVE.test(v));
|
|
9861
10776
|
context.report({
|
|
9862
10777
|
start: init.getStart(context.sourceFile),
|
|
9863
10778
|
end: init.end,
|
|
9864
|
-
message: `Hardcoded ${spec.category} \`${shown}\` in style. ${advice}`,
|
|
10779
|
+
message: relative ? `Hardcoded ${spec.category} \`${shown}\` in style, font-relative: ${formatPx(px)} only at a 16px font. ${advice} No step of the scale grows with the font, so it is not applied automatically.` : `Hardcoded ${spec.category} \`${shown}\` in style. ${advice}`,
|
|
9865
10780
|
suggestion: replacement ? replacement.tailwind ? cls : cssVar : void 0,
|
|
9866
|
-
fix: replacement && !replacement.tailwind && cssVar && replacement.exact ? [{
|
|
10781
|
+
fix: replacement && !replacement.tailwind && cssVar && replacement.exact && !relative ? [{
|
|
9867
10782
|
range: [init.getStart(context.sourceFile), init.end],
|
|
9868
10783
|
text: `"${cssVar}"`
|
|
9869
10784
|
}] : void 0
|
|
@@ -9880,6 +10795,8 @@ function lengthRule(id, description, spec) {
|
|
|
9880
10795
|
function unnamed(replacement) {
|
|
9881
10796
|
return replacement.exact && !replacement.tailwind && !replacement.cssVar;
|
|
9882
10797
|
}
|
|
10798
|
+
/** A length in `em`, relative to the element's font size (`0.5em`, not `0.5rem`). */
|
|
10799
|
+
const FONT_RELATIVE = /^-?(?:\d+\.?\d*|\.\d+)em$/i;
|
|
9883
10800
|
/** The nearest step is "far" when it is off by more than half the value and more than 4px. */
|
|
9884
10801
|
const FAR_RATIO = .5;
|
|
9885
10802
|
const FAR_PX = 4;
|
|
@@ -10236,7 +11153,7 @@ function isObject(value) {
|
|
|
10236
11153
|
var name = "onsystem";
|
|
10237
11154
|
//#endregion
|
|
10238
11155
|
//#region src/version.ts
|
|
10239
|
-
const VERSION = "0.
|
|
11156
|
+
const VERSION = "0.8.0";
|
|
10240
11157
|
const NAME = name;
|
|
10241
11158
|
//#endregion
|
|
10242
11159
|
//#region src/lint/sarif.ts
|
|
@@ -10349,6 +11266,10 @@ function fingerprint$1(uri, identity, occurrence) {
|
|
|
10349
11266
|
}
|
|
10350
11267
|
//#endregion
|
|
10351
11268
|
//#region src/lint/format.ts
|
|
11269
|
+
/** From this many findings, `pretty` ends with a summary: more than a screen to scroll back through. */
|
|
11270
|
+
const SUMMARY_FROM = 50;
|
|
11271
|
+
/** The palette classes the summary lists. */
|
|
11272
|
+
const TOP_CLASSES = 10;
|
|
10352
11273
|
const color = (code) => (text, enabled) => enabled ? `\u001b[${code}m${text}\u001b[39m` : text;
|
|
10353
11274
|
const red = color(31);
|
|
10354
11275
|
const yellow = color(33);
|
|
@@ -10392,6 +11313,7 @@ function formatDiagnostics(results, format, options = {}) {
|
|
|
10392
11313
|
lines.push("");
|
|
10393
11314
|
}
|
|
10394
11315
|
const total = errors + warnings;
|
|
11316
|
+
if (total >= SUMMARY_FROM) lines.push(...summarize(results), "");
|
|
10395
11317
|
const notes = [options.baselined === void 0 ? "" : `${options.baselined.toLocaleString("en-US")} in the baseline`, ...options.notes ?? []].filter(Boolean);
|
|
10396
11318
|
const aside = notes.length ? ` (${notes.join("; ")})` : "";
|
|
10397
11319
|
const flagged = results.filter((r) => r.diagnostics.length).length;
|
|
@@ -10400,6 +11322,41 @@ function formatDiagnostics(results, format, options = {}) {
|
|
|
10400
11322
|
if (fixedHint) lines.push(dim(fixedHint, useColor));
|
|
10401
11323
|
return lines.join("\n");
|
|
10402
11324
|
}
|
|
11325
|
+
/**
|
|
11326
|
+
* What a long run's findings add up to: the count per rule, as `init`'s dry run gives it, and
|
|
11327
|
+
* the Tailwind palette classes found most, each with the token to use. On a palette-heavy app
|
|
11328
|
+
* (Dub's web app, over 14,000 lines of findings) that reads as a plan for moving to the tokens.
|
|
11329
|
+
*/
|
|
11330
|
+
function summarize(results) {
|
|
11331
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
11332
|
+
const pairs = /* @__PURE__ */ new Map();
|
|
11333
|
+
const paletteFiles = /* @__PURE__ */ new Set();
|
|
11334
|
+
let palette = 0;
|
|
11335
|
+
for (const result of results) for (const d of result.diagnostics) {
|
|
11336
|
+
byRule.set(d.ruleId, (byRule.get(d.ruleId) ?? 0) + 1);
|
|
11337
|
+
if (d.ruleId !== "no-hardcoded-color") continue;
|
|
11338
|
+
const utility = parseUtility(d.source);
|
|
11339
|
+
if (!paletteColor(utility.base)) continue;
|
|
11340
|
+
palette++;
|
|
11341
|
+
paletteFiles.add(result.file);
|
|
11342
|
+
const to = (d.fix?.length === 1 ? d.fix[0]?.text : void 0) ?? (d.suggestion && /^[^\s"'=]+$/.test(d.suggestion) ? withBase(utility, d.suggestion) : d.suggestion);
|
|
11343
|
+
const nearest = d.message.includes(". Nearest token ") ? " (nearest token)" : "";
|
|
11344
|
+
const text = `${d.source} → ${to ? `${to}${nearest}` : "no close token"}`;
|
|
11345
|
+
const pair = pairs.get(text) ?? {
|
|
11346
|
+
text,
|
|
11347
|
+
count: 0
|
|
11348
|
+
};
|
|
11349
|
+
pair.count++;
|
|
11350
|
+
pairs.set(text, pair);
|
|
11351
|
+
}
|
|
11352
|
+
const lines = [`By rule: ${[...byRule].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([rule, n]) => `${rule} ${n.toLocaleString("en-US")}`).join(", ")}.`];
|
|
11353
|
+
const top = [...pairs.values()].sort((a, b) => b.count - a.count || a.text.localeCompare(b.text)).slice(0, TOP_CLASSES);
|
|
11354
|
+
if (top.length) {
|
|
11355
|
+
const width = Math.max(...top.map((pair) => pair.count.toLocaleString("en-US").length));
|
|
11356
|
+
lines.push(`Tailwind palette classes: ${palette.toLocaleString("en-US")} in ${plural$1(paletteFiles.size, "file")}. The most common, with the token to use:`, ...top.map((pair) => ` ${pair.count.toLocaleString("en-US").padStart(width)}× ${pair.text}`));
|
|
11357
|
+
}
|
|
11358
|
+
return lines;
|
|
11359
|
+
}
|
|
10403
11360
|
/** Escapes a workflow command's message, so `%` and line breaks reach GitHub as written. */
|
|
10404
11361
|
function escapeData(value) {
|
|
10405
11362
|
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
@@ -10974,18 +11931,46 @@ var DesignSystem = class {
|
|
|
10974
11931
|
searchIndex;
|
|
10975
11932
|
/**
|
|
10976
11933
|
* The components the tools offer: all of them, except a library's (MUI's) where the project
|
|
10977
|
-
* has its own of that name, a wrapper the team means to be used
|
|
10978
|
-
*
|
|
11934
|
+
* has its own of that name, a wrapper the team means to be used, and the helpers in a
|
|
11935
|
+
* registry folder's `lib/` (signoff-ui's `Img` and icons). The rules still check code that
|
|
11936
|
+
* imports either directly against them.
|
|
10979
11937
|
*/
|
|
10980
11938
|
offered;
|
|
11939
|
+
/** The design systems the rules rank, with the registry folders a config lists. */
|
|
11940
|
+
sources;
|
|
10981
11941
|
constructor(model, config) {
|
|
10982
11942
|
this.model = model;
|
|
10983
11943
|
this.config = config;
|
|
10984
|
-
this.
|
|
11944
|
+
this.sources = rankedSources(config);
|
|
11945
|
+
this.lint = new LintTarget(model, this.sources === config.designSystems ? config : {
|
|
11946
|
+
...config,
|
|
11947
|
+
designSystems: this.sources
|
|
11948
|
+
}, new ModuleResolver(config));
|
|
10985
11949
|
const own = new Set(model.components.filter((c) => !c.library).flatMap((c) => [c.name, ...c.aliases]));
|
|
10986
|
-
this.offered = model.components.filter((c) => !c.library || !(own.has(c.name) || c.parent !== void 0 && own.has(c.parent)));
|
|
11950
|
+
this.offered = model.components.filter((c) => (!c.library || !(own.has(c.name) || c.parent !== void 0 && own.has(c.parent))) && !this.registryHelper(c));
|
|
10987
11951
|
this.searchIndex = buildSearchIndex(this.offered);
|
|
10988
11952
|
}
|
|
11953
|
+
/**
|
|
11954
|
+
* The shadcn registry a component was copied from (`@signoff-ui`), when its folder is one
|
|
11955
|
+
* components.json names: a read-only design system, whose code is the registry's.
|
|
11956
|
+
*/
|
|
11957
|
+
registryOf(component) {
|
|
11958
|
+
if (component.library) return void 0;
|
|
11959
|
+
const file = component.source.file;
|
|
11960
|
+
for (const source of this.sources ?? []) {
|
|
11961
|
+
if (!source.registry) continue;
|
|
11962
|
+
const folder = source.components.find((g) => matchesGlob(file, g))?.replace(/\/\*\*\/\*\.\{tsx,jsx\}$/, "").replace(/\\(.)/g, "$1");
|
|
11963
|
+
if (folder !== void 0) return {
|
|
11964
|
+
registry: source.registry,
|
|
11965
|
+
folder
|
|
11966
|
+
};
|
|
11967
|
+
}
|
|
11968
|
+
}
|
|
11969
|
+
/** A component in a registry folder's `lib/`: an internal of its items, not one of them. */
|
|
11970
|
+
registryHelper(component) {
|
|
11971
|
+
const from = this.registryOf(component);
|
|
11972
|
+
return from !== void 0 && component.source.file.startsWith(`${from.folder}/lib/`);
|
|
11973
|
+
}
|
|
10989
11974
|
get root() {
|
|
10990
11975
|
return this.model.root;
|
|
10991
11976
|
}
|
|
@@ -11071,6 +12056,31 @@ var DesignSystem = class {
|
|
|
11071
12056
|
}
|
|
11072
12057
|
};
|
|
11073
12058
|
const CONFIGURATION_DOCS = "https://onsystem.vercel.app/docs/configuration";
|
|
12059
|
+
/**
|
|
12060
|
+
* The design systems the rules rank, first to last: zero config's, or, for a config that lists
|
|
12061
|
+
* the folders `npx shadcn add` copies the registries of components.json into (as `init` writes
|
|
12062
|
+
* them), its other components first and each such folder after them, read-only, as zero config
|
|
12063
|
+
* reads them.
|
|
12064
|
+
*/
|
|
12065
|
+
function rankedSources(config) {
|
|
12066
|
+
if (config.designSystems || !config.configured) return config.designSystems;
|
|
12067
|
+
const listed = registryFolders(config.root, config.tsconfig).filter((f) => config.components.some((g) => matchesGlob(`${f.dir}/index.tsx`, g)));
|
|
12068
|
+
if (!listed.length) return void 0;
|
|
12069
|
+
const inRegistry = (glob) => {
|
|
12070
|
+
const base = globBase(glob);
|
|
12071
|
+
return listed.some((f) => base === f.dir || base.startsWith(`${f.dir}/`));
|
|
12072
|
+
};
|
|
12073
|
+
return [{
|
|
12074
|
+
source: "components",
|
|
12075
|
+
components: config.components.filter((g) => !inRegistry(g)),
|
|
12076
|
+
imports: 0
|
|
12077
|
+
}, ...listed.map((f) => ({
|
|
12078
|
+
source: `${f.dir} (${f.namespace})`,
|
|
12079
|
+
components: [f.glob],
|
|
12080
|
+
imports: 0,
|
|
12081
|
+
registry: f.namespace
|
|
12082
|
+
}))];
|
|
12083
|
+
}
|
|
11074
12084
|
/** `border-ring` → token `ring`, for color utilities a token's usage list does not spell out. */
|
|
11075
12085
|
function matchUtility(base, tokens) {
|
|
11076
12086
|
const key = COLOR_UTILITY.exec(base)?.[1];
|
|
@@ -11585,4 +12595,4 @@ var DesignSystemHost = class {
|
|
|
11585
12595
|
}
|
|
11586
12596
|
};
|
|
11587
12597
|
//#endregion
|
|
11588
|
-
export {
|
|
12598
|
+
export { hasOwnConfig as $, TokenIndex as A, parseDoc as B, pruneBaseline as C, isInside as Ct, updateBaseline as D, defaultProjectConfig as Dt, unrecordedRules as E, toPosix as Et, TOKEN_CATEGORIES as F, DEFAULT_SEVERITY as G, CONFIG_FILES as H, parseDtcgTokens as I, LEGACY_CONFIG_FILES as J, DEFAULT_TEST_EXCLUDE as K, parseDesignMd as L, mergeTokens as M, toDesignMd as N, writeBaseline as O, readProjectConfig as Ot, toDtcg as P, excludedBy as Q, parseCssTokens as R, countBaseline as S, SCRIPT_FILE$1 as St, rulesRun as T, relativePath as Tt, ConfigError as U, extractComponents as V, DEFAULT_DOCS as W, configJsonSchema as X, RULE_IDS as Y, defineConfig as Z, NAME as _, workspacePatterns as _t, fingerprint as a, LIBRARIES as at, LEGACY_BASELINE_FILE as b, truncate as bt, silentLogger as c, notReadYet as ct, searchComponents as d, declaredConfig as dt, loadConfig as et, applyFixes as f, detectProject as ft, formatDiagnostics as g, workspacePackageDirs as gt, escapeData as h, isWorkspaceRoot as ht, componentFiles as i, findVariantDefinitions as it, loadTokens as j, RULES as k, stderrLogger as l, PACKAGE_FIELD as lt, uncheckedNotice as m, isDesignSystemName as mt, DesignSystemHost as n, rulesFor as nt, loadDesignSystem as o, LIBRARY_IDS as ot, checkSource as p, findDesignSystemPackage as pt, DEFAULT_TEST_FOLDERS as q, buildModel as r, formatPx as rt, tokenFiles as s, libraryTitle as st, DesignSystem as t, loadConfigWith as tt, buildSearchIndex as u, PackageFinder as ut, VERSION as v, closest as vt, readBaseline as w, matchesGlob as wt, applyBaseline as x, unique as xt, BASELINE_FILE as y, plural$1 as yt, attachDocs as z };
|