onsystem 0.8.0 → 0.9.1
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 +20 -2
- package/dist/cli.js +122 -21
- package/dist/{config-kYGiD5oW.d.ts → config-B98w8bcg.d.ts} +5 -0
- package/dist/{design-system-Dmcv7eHR.js → design-system-DZm2aP1z.js} +1105 -189
- package/dist/eslint-worker.d.ts +1 -1
- package/dist/eslint-worker.js +2 -2
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +2 -2
- package/dist/{stdio-MTS6QAgo.js → stdio-BJvjEnKW.js} +338 -87
- package/dist/{workspace-CM1RyqZA.js → workspace-Dqu3nkYM.js} +1 -1
- package/package.json +6 -4
|
@@ -5,10 +5,10 @@ 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 postcss from "postcss";
|
|
8
9
|
import { clampChroma, colorsNamed, converter, differenceEuclidean, formatHex, parse, wcagContrast } from "culori";
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
10
11
|
import fsp from "node:fs/promises";
|
|
11
|
-
import postcss from "postcss";
|
|
12
12
|
import { LineCounter, isMap, isScalar, parseDocument, stringify } from "yaml";
|
|
13
13
|
import { stemmer } from "stemmer";
|
|
14
14
|
//#region src/extract/program.ts
|
|
@@ -142,7 +142,7 @@ function slashGlob(pattern) {
|
|
|
142
142
|
* `.mjs` and `.cjs` forms, in any case. CSS, Markdown or JSON parsed as TSX gives hundreds of
|
|
143
143
|
* syntax errors (209 for a shadcn/ui `app/globals.css`).
|
|
144
144
|
*/
|
|
145
|
-
const SCRIPT_FILE$
|
|
145
|
+
const SCRIPT_FILE$2 = /\.(?:[jt]sx?|[cm][jt]s)$/i;
|
|
146
146
|
/** Path relative to `root`, always with forward slashes. */
|
|
147
147
|
function relativePath(root, file) {
|
|
148
148
|
return toPosix(path.relative(root, file));
|
|
@@ -514,6 +514,59 @@ function isFile$1(file) {
|
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
516
|
//#endregion
|
|
517
|
+
//#region src/tokens/imports.ts
|
|
518
|
+
/** Enough stylesheets for a theme split over files, without reading a whole design system. */
|
|
519
|
+
const MAX_IMPORTED = 50;
|
|
520
|
+
/**
|
|
521
|
+
* The specifiers a stylesheet `@import`s: `@import "./theme.css" layer(base)`,
|
|
522
|
+
* `@import url(theme.css)`, `@import "@acme/theme"`. Empty for CSS that does not parse.
|
|
523
|
+
*/
|
|
524
|
+
function cssImports(css) {
|
|
525
|
+
let root;
|
|
526
|
+
try {
|
|
527
|
+
root = postcss.parse(css);
|
|
528
|
+
} catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
const out = [];
|
|
532
|
+
for (const node of root.nodes) {
|
|
533
|
+
if (node.type !== "atrule" || node.name !== "import") continue;
|
|
534
|
+
const match = /^\s*(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^\s"')]+)\s*\))/.exec(node.params);
|
|
535
|
+
const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
536
|
+
if (specifier) out.push(specifier);
|
|
537
|
+
}
|
|
538
|
+
return out;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* The stylesheets `files` import, and the ones those import in turn, each once, in the order
|
|
542
|
+
* the cascade ranks them, for a reader that keeps the first declaration of a variable. CSS
|
|
543
|
+
* puts an imported stylesheet where its `@import` is, so a later import overrides an earlier
|
|
544
|
+
* one, and the importing stylesheet's own rules override both: `@import "./base.css";
|
|
545
|
+
* @import "./brand.css";` lists brand.css, then what brand.css imports, then base.css. The
|
|
546
|
+
* files themselves are not repeated, and come before all of these.
|
|
547
|
+
*/
|
|
548
|
+
function importedStylesheets(files, resolve) {
|
|
549
|
+
const seen = new Set(files);
|
|
550
|
+
const out = [];
|
|
551
|
+
const visit = (file) => {
|
|
552
|
+
let text;
|
|
553
|
+
try {
|
|
554
|
+
text = fs.readFileSync(file, "utf8");
|
|
555
|
+
} catch {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
for (const specifier of cssImports(text).reverse()) {
|
|
559
|
+
const target = resolve(specifier, file);
|
|
560
|
+
if (!target || seen.has(target) || out.length >= MAX_IMPORTED) continue;
|
|
561
|
+
seen.add(target);
|
|
562
|
+
out.push(target);
|
|
563
|
+
visit(target);
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
for (const file of files) visit(file);
|
|
567
|
+
return out;
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
517
570
|
//#region src/util/strings.ts
|
|
518
571
|
/** `alert-dialog` / `alert_dialog` / `alertDialog` → `AlertDialog`. */
|
|
519
572
|
function pascalCase(input) {
|
|
@@ -579,7 +632,7 @@ function unique(items) {
|
|
|
579
632
|
const ignore = () => void 0;
|
|
580
633
|
const COMPONENT_FILE = /\.[jt]sx$/;
|
|
581
634
|
/** A module a barrel can lead to: TypeScript or JavaScript, not a declaration file. */
|
|
582
|
-
const SCRIPT_FILE = /(?<!\.d)\.[cm]?[jt]sx?$/;
|
|
635
|
+
const SCRIPT_FILE$1 = /(?<!\.d)\.[cm]?[jt]sx?$/;
|
|
583
636
|
const STYLESHEETS = [
|
|
584
637
|
"src/styles/globals.css",
|
|
585
638
|
"src/globals.css",
|
|
@@ -588,6 +641,18 @@ const STYLESHEETS = [
|
|
|
588
641
|
"src/index.css"
|
|
589
642
|
];
|
|
590
643
|
/**
|
|
644
|
+
* An app's own stylesheets, where zero config looks for them without components.json (the
|
|
645
|
+
* stylesheets of `DEFAULT_TOKENS` in config.ts).
|
|
646
|
+
*/
|
|
647
|
+
const APP_STYLESHEETS = [
|
|
648
|
+
"app/globals.css",
|
|
649
|
+
"src/app/globals.css",
|
|
650
|
+
"styles/globals.css",
|
|
651
|
+
"src/styles/globals.css",
|
|
652
|
+
"src/index.css",
|
|
653
|
+
"app/app.css"
|
|
654
|
+
];
|
|
655
|
+
/**
|
|
591
656
|
* Finds the design system without a config:
|
|
592
657
|
*
|
|
593
658
|
* 1. `components.json` (shadcn/ui): the `ui` alias, resolved through tsconfig
|
|
@@ -660,6 +725,15 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
660
725
|
const names = unique([...declared ? [declared.name] : [], ...dependencies.filter(isDesignSystemName)]);
|
|
661
726
|
if (!names.length) step("workspace design-system packages", false, `no dependency is named like one (@acme/ui, acme-ui, @acme/design-system) among ${plural(dependencies.length, "dependency", "dependencies")}`);
|
|
662
727
|
let imported;
|
|
728
|
+
let declarations;
|
|
729
|
+
/** What the trace adds for an installed package whose sources are not read. */
|
|
730
|
+
const declarationsNote = (name, dir, pkg) => {
|
|
731
|
+
declarations ??= new Set(declarationsPackages(root, packages).map((p) => p.name));
|
|
732
|
+
if (declarations.has(name)) return "; its components are read from its type declarations instead";
|
|
733
|
+
if (!declaresTypes(dir, pkg)) return "";
|
|
734
|
+
const scope = scopeOf$1(name);
|
|
735
|
+
return `; its type declarations are read when it declares its onsystem config${scope ? `, when the app is in its npm scope (${scope}/…)` : ""} or when the app's config sets "importPath" to "${name}"`;
|
|
736
|
+
};
|
|
663
737
|
for (const name of names) {
|
|
664
738
|
const own = name === declared?.name ? declared : void 0;
|
|
665
739
|
const workspaceDir = own ? void 0 : packages.find(name);
|
|
@@ -687,7 +761,10 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
687
761
|
prefixes: [name]
|
|
688
762
|
});
|
|
689
763
|
step(candidate, true, `${relativePath(root, dir)}, ${countFiles(root, result)}`);
|
|
690
|
-
} else
|
|
764
|
+
} else {
|
|
765
|
+
const reason = result ? "its components match no file" : unique(reasons).join("; ") || "nothing to read";
|
|
766
|
+
step(candidate, false, kind === "installed" ? `${reason}${declarationsNote(name, dir, pkg)}` : reason);
|
|
767
|
+
}
|
|
691
768
|
}
|
|
692
769
|
const registries = shadcn && project && fromShadcn && !withDefaults ? fromRegistries(root, shadcn, fromShadcn, found, project, step) : [];
|
|
693
770
|
if (found.length) {
|
|
@@ -880,15 +957,28 @@ function fromComponentsJson(root, dir, json, project, packages, note = ignore) {
|
|
|
880
957
|
}
|
|
881
958
|
const tailwind = asRecord(json.tailwind);
|
|
882
959
|
const css = typeof tailwind.css === "string" && tailwind.css ? path.resolve(dir, tailwind.css) : void 0;
|
|
960
|
+
const named = css && fs.existsSync(css) ? [css] : [];
|
|
883
961
|
return {
|
|
884
962
|
components: [`${escapePath(relativePath(root, uiDir) || ".")}/**/*.{tsx,jsx}`],
|
|
885
|
-
tokens:
|
|
963
|
+
tokens: (dir === root ? withAppStylesheets(root, named) : named).map((f) => escapePath(relativePath(root, f))),
|
|
886
964
|
imports,
|
|
887
965
|
dirs: [uiDir],
|
|
888
966
|
source: `${ui} → ${relativePath(root, uiDir) || "."}`,
|
|
889
967
|
...ownerPackage(root, uiDir, ui)
|
|
890
968
|
};
|
|
891
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* The stylesheets an app's tokens come from when its components.json names a stylesheet in
|
|
972
|
+
* another package, as cal.com's names coss/ui's: the app's own stylesheets first, which hold
|
|
973
|
+
* its own tokens, then the named one, unless they import it, so that the cascade places it.
|
|
974
|
+
*/
|
|
975
|
+
function withAppStylesheets(root, named) {
|
|
976
|
+
if (named.every((file) => isInside(root, file))) return named;
|
|
977
|
+
const own = APP_STYLESHEETS.map((f) => path.join(root, f)).filter(isFile);
|
|
978
|
+
if (!own.length) return named;
|
|
979
|
+
const imported = new Set(importedStylesheets(own, stylesheetResolver(root)));
|
|
980
|
+
return [...own, ...named.filter((file) => !imported.has(file))];
|
|
981
|
+
}
|
|
892
982
|
/** How a shadcn registry is named in components.json: `@` and a name, so none leads out of the folder. */
|
|
893
983
|
const REGISTRY_NAMESPACE = /^@[a-z0-9][\w-]*$/i;
|
|
894
984
|
/**
|
|
@@ -1044,7 +1134,7 @@ function fromPackage(root, dir, pkg, note = ignore, installed = false) {
|
|
|
1044
1134
|
for (const mapping of mappings) {
|
|
1045
1135
|
const file = path.resolve(root, mapping.target);
|
|
1046
1136
|
if (installed && isCompiledExport(dir, file)) continue;
|
|
1047
|
-
if (mapping.target.includes("*") || !SCRIPT_FILE.test(file) || !isFile(file)) {
|
|
1137
|
+
if (mapping.target.includes("*") || !SCRIPT_FILE$1.test(file) || !isFile(file)) {
|
|
1048
1138
|
add(mapping, COMPONENT_FILE.test(mapping.target));
|
|
1049
1139
|
continue;
|
|
1050
1140
|
}
|
|
@@ -1334,7 +1424,7 @@ function presetResolver(root) {
|
|
|
1334
1424
|
if (file || specifier.startsWith(".")) return file;
|
|
1335
1425
|
const name = packageName(specifier);
|
|
1336
1426
|
const dir = name ? packages.find(name) : void 0;
|
|
1337
|
-
return name && dir ? exportedFile(dir, `.${specifier.slice(name.length)}`, (f) => SCRIPT_FILE.test(f)) : void 0;
|
|
1427
|
+
return name && dir ? exportedFile(dir, `.${specifier.slice(name.length)}`, (f) => SCRIPT_FILE$1.test(f)) : void 0;
|
|
1338
1428
|
};
|
|
1339
1429
|
}
|
|
1340
1430
|
/**
|
|
@@ -1342,15 +1432,21 @@ function presetResolver(root) {
|
|
|
1342
1432
|
* (`./theme.css`, or `theme.css` as plain CSS reads it), or a stylesheet of a workspace
|
|
1343
1433
|
* package (`@acme/theme/colors.css`, or `@acme/theme` through its `exports` or its `style`
|
|
1344
1434
|
* field). An installed package resolves only when zero config reads it as a design system
|
|
1345
|
-
* ({@link installedDesignSystem})
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1435
|
+
* ({@link installedDesignSystem}) or reads its type declarations ({@link declarationsPackages}:
|
|
1436
|
+
* by its config, its scope or the config's `importPath`), and a stylesheet in it imports others
|
|
1437
|
+
* of that package alone. In an installed package, a stylesheet in a build folder
|
|
1438
|
+
* (`dist/index.css`) is compiled, and resolves to nothing. Other installed packages
|
|
1439
|
+
* (`tailwindcss`) and URLs resolve to nothing.
|
|
1348
1440
|
*/
|
|
1349
|
-
function stylesheetResolver(root) {
|
|
1441
|
+
function stylesheetResolver(root, { importPath } = {}) {
|
|
1350
1442
|
const packages = new PackageFinder(root);
|
|
1351
1443
|
const designSystems = /* @__PURE__ */ new Map();
|
|
1444
|
+
let declarations;
|
|
1352
1445
|
const designSystem = (name) => {
|
|
1353
|
-
if (!designSystems.has(name))
|
|
1446
|
+
if (!designSystems.has(name)) {
|
|
1447
|
+
declarations ??= new Set(declarationsPackages(root, packages, { importPath }).map((p) => p.name));
|
|
1448
|
+
designSystems.set(name, installedDesignSystem(root, name, packages) ?? (declarations.has(name) ? packages.installed(name) : void 0));
|
|
1449
|
+
}
|
|
1354
1450
|
return designSystems.get(name);
|
|
1355
1451
|
};
|
|
1356
1452
|
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));
|
|
@@ -1566,6 +1662,87 @@ function packageImports(root, dir) {
|
|
|
1566
1662
|
return pkg ? fromPackage(root, dir, pkg)?.imports ?? [] : [];
|
|
1567
1663
|
}
|
|
1568
1664
|
/**
|
|
1665
|
+
* The dependencies of `root` (in `dependencies` or `devDependencies`) whose components are read
|
|
1666
|
+
* from their type declarations: design-system packages installed from a registry that name
|
|
1667
|
+
* `.d.ts` files, and whose component sources the root does not read (`options.components`
|
|
1668
|
+
* matches no file in them), as when they publish only compiled code. A design-system package
|
|
1669
|
+
* is one that declares its onsystem config (set up with `init`); one named like a design system
|
|
1670
|
+
* in the root's own npm scope (`@acme/ui` for `@acme/web`); or the dependency the root's config
|
|
1671
|
+
* names as its `importPath`, whatever its name or scope (`@dub/ui` in an unscoped app). A kit
|
|
1672
|
+
* from another scope named like one (`@sanity/ui`, `@react-email/components`) is not the app's
|
|
1673
|
+
* design system unless the config says so, and is not read. Workspace packages are read from
|
|
1674
|
+
* their sources. None at a workspace root, whose projects each find their own.
|
|
1675
|
+
*/
|
|
1676
|
+
function declarationsPackages(root, packages = new PackageFinder(root), options = {}) {
|
|
1677
|
+
const own = readJson$1(path.join(root, "package.json"));
|
|
1678
|
+
if (!own || isWorkspaceRoot(root)) return [];
|
|
1679
|
+
const scope = scopeOf$1(own.name);
|
|
1680
|
+
const found = [];
|
|
1681
|
+
let sources;
|
|
1682
|
+
for (const name of Object.keys({
|
|
1683
|
+
...asRecord(own.dependencies),
|
|
1684
|
+
...asRecord(own.devDependencies)
|
|
1685
|
+
})) {
|
|
1686
|
+
const located = packages.locate(name);
|
|
1687
|
+
if (!located) continue;
|
|
1688
|
+
const declaration = located.json["onsystem"] === void 0 ? void 0 : declaredConfig(packages.place(located.dir).dir, located.json);
|
|
1689
|
+
const by = declaration !== void 0 && "file" in declaration ? "config" : scope !== void 0 && scopeOf$1(name) === scope && isDesignSystemName(name) ? "scope" : name === options.importPath ? "importPath" : void 0;
|
|
1690
|
+
if (!by) continue;
|
|
1691
|
+
const installed = packages.installed(name);
|
|
1692
|
+
const pkg = installed ? cachedManifest(path.join(installed, "package.json")) : void 0;
|
|
1693
|
+
if (!installed || !pkg || !declaresTypes(installed, pkg)) continue;
|
|
1694
|
+
sources ??= componentSources(root, options.components ?? []);
|
|
1695
|
+
if (sources.some((file) => isInside(realPath(installed), file))) continue;
|
|
1696
|
+
const version = typeof pkg.version === "string" ? pkg.version : void 0;
|
|
1697
|
+
found.push({
|
|
1698
|
+
name,
|
|
1699
|
+
dir: path.resolve(located.dir),
|
|
1700
|
+
version,
|
|
1701
|
+
by
|
|
1702
|
+
});
|
|
1703
|
+
}
|
|
1704
|
+
return found;
|
|
1705
|
+
}
|
|
1706
|
+
/** `@acme` for `@acme/web`; undefined for an unscoped name. */
|
|
1707
|
+
function scopeOf$1(name) {
|
|
1708
|
+
return typeof name === "string" && /^@[^/]+\/./.test(name) ? name.split("/")[0] : void 0;
|
|
1709
|
+
}
|
|
1710
|
+
/** Whether a package names type declarations: `types`, a `types` export condition, or `index.d.ts`. */
|
|
1711
|
+
function declaresTypes(dir, pkg) {
|
|
1712
|
+
if (typeof pkg.types === "string" || typeof pkg.typings === "string") return true;
|
|
1713
|
+
const hasTypes = (value) => Array.isArray(value) ? value.some(hasTypes) : value !== null && typeof value === "object" && Object.entries(value).some(([key, nested]) => key === "types" || hasTypes(nested));
|
|
1714
|
+
return hasTypes(pkg.exports) || isFile(path.join(dir, "index.d.ts"));
|
|
1715
|
+
}
|
|
1716
|
+
/** Files that hold no component to read: declarations, tests and stories, as `exclude` has them. */
|
|
1717
|
+
const NOT_COMPONENT_SOURCES = [
|
|
1718
|
+
"**/*.d.ts",
|
|
1719
|
+
"**/*.{test,spec,stories}.{ts,tsx,js,jsx}",
|
|
1720
|
+
"**/__{tests,stories,mocks}__/**"
|
|
1721
|
+
];
|
|
1722
|
+
/**
|
|
1723
|
+
* The real paths of the component files in `node_modules` that `patterns` (relative to `root`)
|
|
1724
|
+
* match: the sources of an installed package that the root reads, through a design-system
|
|
1725
|
+
* package's config or zero config. Tests and stories are not components.
|
|
1726
|
+
*/
|
|
1727
|
+
function componentSources(root, patterns) {
|
|
1728
|
+
const named = patterns.filter((p) => p.includes("node_modules"));
|
|
1729
|
+
if (!named.length) return [];
|
|
1730
|
+
return globSync(named, {
|
|
1731
|
+
cwd: root,
|
|
1732
|
+
absolute: true,
|
|
1733
|
+
expandDirectories: false,
|
|
1734
|
+
ignore: NOT_COMPONENT_SOURCES
|
|
1735
|
+
}).map(realPath);
|
|
1736
|
+
}
|
|
1737
|
+
/** A path with its links resolved, or as it is when it cannot be. */
|
|
1738
|
+
function realPath(file) {
|
|
1739
|
+
try {
|
|
1740
|
+
return fs.realpathSync(file);
|
|
1741
|
+
} catch {
|
|
1742
|
+
return path.resolve(file);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
/**
|
|
1569
1746
|
* Dependencies' package.json files, parsed, by path: read again only when one's size or mtime
|
|
1570
1747
|
* changes, so a config loaded again (the ESLint plugin's and the MCP server's reloads) does not
|
|
1571
1748
|
* read every dependency again.
|
|
@@ -1910,7 +2087,7 @@ const KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
1910
2087
|
* `/ alpha` allowed): how Tailwind v3 projects, and shadcn/ui before v4, store
|
|
1911
2088
|
* colors, to be used as `hsl(var(--border))`.
|
|
1912
2089
|
*/
|
|
1913
|
-
const BARE_HSL = /^(
|
|
2090
|
+
const BARE_HSL = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg)?(?:\s*,\s*|\s+)(\d+(?:\.\d+)?|\.\d+)%(?:\s*,\s*|\s+)(\d+(?:\.\d+)?|\.\d+)%(?:\s*\/\s*((?:\d+(?:\.\d+)?|\.\d+)%?))?$/;
|
|
1914
2091
|
function isBareHsl(value) {
|
|
1915
2092
|
return BARE_HSL.test(value.trim());
|
|
1916
2093
|
}
|
|
@@ -1920,7 +2097,7 @@ function isBareHsl(value) {
|
|
|
1920
2097
|
* Three numbers alone are no sure color, so they count as one only in a token already
|
|
1921
2098
|
* known to be a color: one the Tailwind config reads with `rgb()`.
|
|
1922
2099
|
*/
|
|
1923
|
-
const BARE_RGB = /^(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*\/\s*(
|
|
2100
|
+
const BARE_RGB = /^(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*,\s*|\s+)(\d{1,3})(?:\s*\/\s*((?:\d+(?:\.\d+)?|\.\d+)%?))?$/;
|
|
1924
2101
|
function isBareRgb(value) {
|
|
1925
2102
|
const match = BARE_RGB.exec(value.trim());
|
|
1926
2103
|
return match !== null && match.slice(1, 4).every((channel) => Number(channel) <= 255);
|
|
@@ -2432,9 +2609,23 @@ const UNREAD_LIBRARIES = [
|
|
|
2432
2609
|
function libraryAdapter(name) {
|
|
2433
2610
|
return LIBRARIES.find((library) => library.package === name);
|
|
2434
2611
|
}
|
|
2435
|
-
/**
|
|
2612
|
+
/** What a design-system package read from its type declarations is called in messages. */
|
|
2613
|
+
const PACKAGE_TITLE = "Design-system package";
|
|
2614
|
+
/** A library's name for messages, read or not: `MUI`, `Chakra UI`, `Design-system package`. */
|
|
2436
2615
|
function libraryTitle(name) {
|
|
2437
|
-
return [...LIBRARIES, ...UNREAD_LIBRARIES].find((l) => l.package === name)?.title ??
|
|
2616
|
+
return [...LIBRARIES, ...UNREAD_LIBRARIES].find((l) => l.package === name)?.title ?? PACKAGE_TITLE;
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* The adapter for a design-system package read from its type declarations (`@acme/ui`): its
|
|
2620
|
+
* components and their props, with no theme to read, and the app's augmentations of its modules.
|
|
2621
|
+
*/
|
|
2622
|
+
function packageAdapter(name) {
|
|
2623
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2624
|
+
return {
|
|
2625
|
+
package: name,
|
|
2626
|
+
title: PACKAGE_TITLE,
|
|
2627
|
+
augments: new RegExp(`declare\\s+module\\s+['"]${escaped}(?:/[\\w./-]*)?['"]`)
|
|
2628
|
+
};
|
|
2438
2629
|
}
|
|
2439
2630
|
/** Why a library in `UNREAD_LIBRARIES` is not read: `a dependency onsystem does not read yet (…)`. */
|
|
2440
2631
|
function notReadYet() {
|
|
@@ -2446,8 +2637,15 @@ function notReadYet() {
|
|
|
2446
2637
|
* folder above it, as a monorepo hoists it. Nothing in the package is run. `ids` is the config's
|
|
2447
2638
|
* `libraries`: undefined reads every library onsystem supports, false none, a list those named;
|
|
2448
2639
|
* the dependencies it leaves out are `off`. Those in `UNREAD_LIBRARIES` are `unread`.
|
|
2449
|
-
|
|
2450
|
-
|
|
2640
|
+
*
|
|
2641
|
+
* The design-system packages installed without their component sources that the app depends
|
|
2642
|
+
* on ({@link declarationsPackages}) come after them, read from their type declarations unless
|
|
2643
|
+
* `libraries` is false. A list names libraries, not packages, so it leaves them in. `options`
|
|
2644
|
+
* is what the config says of them: its `importPath`, which makes the dependency it names one,
|
|
2645
|
+
* and the component globs that read a package from its sources instead. A library onsystem
|
|
2646
|
+
* knows is never read as one, whatever `importPath` says.
|
|
2647
|
+
*/
|
|
2648
|
+
function detectLibraries(root, ids, options = {}) {
|
|
2451
2649
|
const pkg = readJson(path.join(root, "package.json"));
|
|
2452
2650
|
if (!pkg) return {
|
|
2453
2651
|
read: [],
|
|
@@ -2476,9 +2674,16 @@ function detectLibraries(root, ids) {
|
|
|
2476
2674
|
source: sourceOf(library.package)
|
|
2477
2675
|
}));
|
|
2478
2676
|
const wanted = (id) => ids === void 0 || ids !== false && ids.includes(id);
|
|
2677
|
+
const known = new Set([...LIBRARIES, ...UNREAD_LIBRARIES].map((library) => library.package));
|
|
2678
|
+
const packages = declarationsPackages(root, void 0, options).filter((p) => !known.has(p.name)).map((p) => ({
|
|
2679
|
+
package: p.name,
|
|
2680
|
+
dir: p.dir,
|
|
2681
|
+
version: p.version,
|
|
2682
|
+
designSystem: true
|
|
2683
|
+
}));
|
|
2479
2684
|
return {
|
|
2480
|
-
read: found.filter((f) => wanted(f.id)).map((f) => f.source),
|
|
2481
|
-
off: found.filter((f) => !wanted(f.id)).map((f) => f.source),
|
|
2685
|
+
read: [...found.filter((f) => wanted(f.id)).map((f) => f.source), ...ids === false ? [] : packages],
|
|
2686
|
+
off: [...found.filter((f) => !wanted(f.id)).map((f) => f.source), ...ids === false ? packages : []],
|
|
2482
2687
|
unread: UNREAD_LIBRARIES.filter((library) => dependencies.has(library.package)).map((library) => sourceOf(library.package))
|
|
2483
2688
|
};
|
|
2484
2689
|
}
|
|
@@ -2630,7 +2835,7 @@ function classText(node) {
|
|
|
2630
2835
|
visit(node);
|
|
2631
2836
|
return parts.join(" ").replace(/\s+/g, " ").trim();
|
|
2632
2837
|
}
|
|
2633
|
-
const LENGTH = /^(-?(?:\d
|
|
2838
|
+
const LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem|em)?$/i;
|
|
2634
2839
|
/** `"0.5rem"` → 8, `"12px"` → 12, `"4"` → 4. Returns undefined for anything else. */
|
|
2635
2840
|
function lengthToPx(value) {
|
|
2636
2841
|
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
@@ -2658,13 +2863,27 @@ function evaluateLength(value, resolve, depth = 0) {
|
|
|
2658
2863
|
function expandVars(value, resolve, depth) {
|
|
2659
2864
|
let out = "";
|
|
2660
2865
|
let last = 0;
|
|
2661
|
-
for (
|
|
2662
|
-
|
|
2866
|
+
for (let at = value.indexOf("var(", last); at !== -1; at = value.indexOf("var(", last)) {
|
|
2867
|
+
let level = 0;
|
|
2868
|
+
let comma = -1;
|
|
2869
|
+
let end = -1;
|
|
2870
|
+
for (let i = at + 3; i < value.length; i++) {
|
|
2871
|
+
const c = value[i];
|
|
2872
|
+
if (c === "(") level++;
|
|
2873
|
+
else if (c === ")" && --level === 0) {
|
|
2874
|
+
end = i;
|
|
2875
|
+
break;
|
|
2876
|
+
} else if (c === "," && level === 1 && comma === -1) comma = i;
|
|
2877
|
+
}
|
|
2878
|
+
if (end === -1) return void 0;
|
|
2879
|
+
const name = value.slice(at + 4, comma === -1 ? end : comma).trim();
|
|
2880
|
+
if (!/^--[\w-]+$/.test(name)) return void 0;
|
|
2881
|
+
const fallback = comma === -1 ? void 0 : value.slice(comma + 1, end);
|
|
2663
2882
|
const resolved = resolve(name) ?? fallback;
|
|
2664
2883
|
const px = resolved === void 0 ? void 0 : evaluateLength(resolved, resolve, depth + 1);
|
|
2665
2884
|
if (px === void 0) return void 0;
|
|
2666
|
-
out += `${value.slice(last,
|
|
2667
|
-
last =
|
|
2885
|
+
out += `${value.slice(last, at)}${px}px`;
|
|
2886
|
+
last = end + 1;
|
|
2668
2887
|
}
|
|
2669
2888
|
return out + value.slice(last);
|
|
2670
2889
|
}
|
|
@@ -2790,6 +3009,10 @@ function utilities(namespace, key) {
|
|
|
2790
3009
|
//#region src/tokens/tailwind-config.ts
|
|
2791
3010
|
/** A preset chain is a few files deep; anything beyond is not a Tailwind config. */
|
|
2792
3011
|
const MAX_FILES = 12;
|
|
3012
|
+
/** A config or preset module, which is read for its theme. */
|
|
3013
|
+
const SCRIPT_FILE = /\.[cm]?[jt]s$/;
|
|
3014
|
+
/** Tailwind's own modules, such as `tailwindcss/defaultTheme`, which hold its defaults. */
|
|
3015
|
+
const TAILWIND_MODULE = /^tailwindcss(?:\/|$)/;
|
|
2793
3016
|
/**
|
|
2794
3017
|
* Reads which custom property backs which color class from a Tailwind v3
|
|
2795
3018
|
* config, without running it: every `colors` object in the file
|
|
@@ -2819,7 +3042,7 @@ function readTailwindColors(file, resolve) {
|
|
|
2819
3042
|
}
|
|
2820
3043
|
const specifier = importedSpecifier(node);
|
|
2821
3044
|
const target = specifier === void 0 ? void 0 : resolve(specifier, current);
|
|
2822
|
-
if (target &&
|
|
3045
|
+
if (target && SCRIPT_FILE.test(target)) queue.push(target);
|
|
2823
3046
|
ts.forEachChild(node, visit);
|
|
2824
3047
|
};
|
|
2825
3048
|
visit(source);
|
|
@@ -2851,13 +3074,47 @@ function collect(object, keys, colors, rgb) {
|
|
|
2851
3074
|
}
|
|
2852
3075
|
}
|
|
2853
3076
|
/**
|
|
3077
|
+
* Tailwind v3's default `borderRadius` scale, which `theme.extend.borderRadius` adds to and
|
|
3078
|
+
* `theme.borderRadius` replaces. `none` (0) is left out, as the rule skips zero, and `full`
|
|
3079
|
+
* is the token index's own `rounded-full`.
|
|
3080
|
+
*/
|
|
3081
|
+
const TAILWIND_V3_RADIUS = {
|
|
3082
|
+
sm: "0.125rem",
|
|
3083
|
+
DEFAULT: "0.25rem",
|
|
3084
|
+
md: "0.375rem",
|
|
3085
|
+
lg: "0.5rem",
|
|
3086
|
+
xl: "0.75rem",
|
|
3087
|
+
"2xl": "1rem",
|
|
3088
|
+
"3xl": "1.5rem"
|
|
3089
|
+
};
|
|
3090
|
+
/** borderRadius entries by key, and which of them are Tailwind's default scale rather than the config's. */
|
|
3091
|
+
var RadiusEntries = class extends Map {
|
|
3092
|
+
defaults = /* @__PURE__ */ new Set();
|
|
3093
|
+
};
|
|
3094
|
+
/**
|
|
2854
3095
|
* Reads `borderRadius` entries from a Tailwind v3 config without running it:
|
|
2855
3096
|
* every `borderRadius` object in the file, then in the project modules it
|
|
2856
|
-
* imports, such as a shared preset. The config's own entries win.
|
|
3097
|
+
* imports, such as a shared preset. The config's own entries win. Tailwind's
|
|
3098
|
+
* default scale follows, for the keys none of them sets, unless the first
|
|
3099
|
+
* `theme.borderRadius` outside `extend` replaces it without spreading it back
|
|
3100
|
+
* in (`...defaultTheme.borderRadius`), a `theme` is a whole module or spreads
|
|
3101
|
+
* one (`theme: require("./theme")`, `theme: { ...require("./theme") }`), other
|
|
3102
|
+
* than Tailwind's own, `presets: []` drops Tailwind's default config
|
|
3103
|
+
* and no `theme.borderRadius` spreads the scale back in, or the scale comes from
|
|
3104
|
+
* somewhere this cannot read. That is a module that is no project file (one
|
|
3105
|
+
* installed from npm, other than Tailwind's own) that the config takes as a
|
|
3106
|
+
* preset, a theme, or the config itself (`module.exports = require("x")`,
|
|
3107
|
+
* `{ ...require("x"), content }`), or spreads into a theme, its `extend` or a
|
|
3108
|
+
* `borderRadius`; a `presets` that is no array; or a `theme.borderRadius` or
|
|
3109
|
+
* `theme.extend.borderRadius` that is no object (`borderRadius: radius`,
|
|
3110
|
+
* `borderRadius: ({ theme }) => …`) or spreads anything but Tailwind's scale.
|
|
2857
3111
|
*/
|
|
2858
3112
|
function readTailwindRadii(file, resolve) {
|
|
2859
|
-
const radii =
|
|
3113
|
+
const radii = new RadiusEntries();
|
|
2860
3114
|
const files = [];
|
|
3115
|
+
let keepsDefaults;
|
|
3116
|
+
let unread = false;
|
|
3117
|
+
let noDefaultConfig = false;
|
|
2861
3118
|
const queue = [file];
|
|
2862
3119
|
while (queue.length && files.length < MAX_FILES) {
|
|
2863
3120
|
const current = queue.shift() ?? "";
|
|
@@ -2870,18 +3127,58 @@ function readTailwindRadii(file, resolve) {
|
|
|
2870
3127
|
}
|
|
2871
3128
|
files.push(current);
|
|
2872
3129
|
const source = ts.createSourceFile(current, text, ts.ScriptTarget.Latest, true, scriptKind(current));
|
|
3130
|
+
const bindings = importBindings(source);
|
|
3131
|
+
const external = (specifier) => {
|
|
3132
|
+
if (specifier === void 0 || TAILWIND_MODULE.test(specifier)) return false;
|
|
3133
|
+
const target = resolve(specifier, current);
|
|
3134
|
+
return !(target && SCRIPT_FILE.test(target));
|
|
3135
|
+
};
|
|
3136
|
+
const fromPackage = (value) => external(moduleOf(value, bindings));
|
|
3137
|
+
const fromTailwind = (value) => TAILWIND_MODULE.test(moduleOf(value, bindings) ?? "");
|
|
2873
3138
|
const visit = (node) => {
|
|
2874
|
-
if (ts.isPropertyAssignment(node) && propertyName(node.name) === "borderRadius"
|
|
2875
|
-
|
|
2876
|
-
|
|
3139
|
+
if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && propertyName(node.name) === "borderRadius") {
|
|
3140
|
+
const value = ts.isPropertyAssignment(node) ? node.initializer : void 0;
|
|
3141
|
+
const holder = themeSection(node.parent);
|
|
3142
|
+
if (value && ts.isObjectLiteralExpression(value)) {
|
|
3143
|
+
collectRadius(value, radii);
|
|
3144
|
+
let spreads = false;
|
|
3145
|
+
for (const property of value.properties) {
|
|
3146
|
+
if (!ts.isSpreadAssignment(property)) continue;
|
|
3147
|
+
if (fromTailwind(property.expression)) spreads = true;
|
|
3148
|
+
else unread = true;
|
|
3149
|
+
}
|
|
3150
|
+
if (holder === "theme") keepsDefaults ??= spreads;
|
|
3151
|
+
return;
|
|
3152
|
+
}
|
|
3153
|
+
if (holder === "theme") keepsDefaults ??= false;
|
|
3154
|
+
else if (holder === "extend") unread = true;
|
|
2877
3155
|
}
|
|
3156
|
+
if ((ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) && propertyName(node.name) === "presets") {
|
|
3157
|
+
const value = ts.isPropertyAssignment(node) ? node.initializer : void 0;
|
|
3158
|
+
if (value && ts.isArrayLiteralExpression(value)) {
|
|
3159
|
+
if (!value.elements.length) noDefaultConfig = true;
|
|
3160
|
+
for (const element of value.elements) if (fromPackage(ts.isSpreadElement(element) ? element.expression : element)) unread = true;
|
|
3161
|
+
} else unread = true;
|
|
3162
|
+
}
|
|
3163
|
+
if ((ts.isShorthandPropertyAssignment(node) || ts.isPropertyAssignment(node) && !ts.isObjectLiteralExpression(node.initializer)) && propertyName(node.name) === "theme" && !ts.isPropertyAssignment(node.parent.parent)) {
|
|
3164
|
+
keepsDefaults ??= false;
|
|
3165
|
+
if (fromPackage(ts.isPropertyAssignment(node) ? node.initializer : node.name)) unread = true;
|
|
3166
|
+
}
|
|
3167
|
+
if (ts.isSpreadAssignment(node) && fromPackage(node.expression) && (themeSection(node.parent) !== void 0 || !ts.isPropertyAssignment(node.parent.parent) && !themeValueOnly(node.parent, source))) unread = true;
|
|
3168
|
+
if (ts.isSpreadAssignment(node) && themeSection(node.parent) === "theme" && !fromTailwind(node.expression)) keepsDefaults ??= false;
|
|
3169
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && isModuleExports(node.left) && fromPackage(node.right) || ts.isExportAssignment(node) && fromPackage(node.expression) || ts.isExportDeclaration(node) && !node.isTypeOnly && node.exportClause !== void 0 && ts.isNamedExports(node.exportClause) && node.exportClause.elements.some((element) => element.name.text === "default") && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier) && external(node.moduleSpecifier.text)) unread = true;
|
|
2878
3170
|
const specifier = importedSpecifier(node);
|
|
2879
3171
|
const target = specifier === void 0 ? void 0 : resolve(specifier, current);
|
|
2880
|
-
if (target &&
|
|
3172
|
+
if (target && SCRIPT_FILE.test(target)) queue.push(target);
|
|
2881
3173
|
ts.forEachChild(node, visit);
|
|
2882
3174
|
};
|
|
2883
3175
|
visit(source);
|
|
2884
3176
|
}
|
|
3177
|
+
if ((noDefaultConfig ? keepsDefaults === true : keepsDefaults !== false) && !unread) for (const [key, value] of Object.entries(TAILWIND_V3_RADIUS)) {
|
|
3178
|
+
if (radii.has(key)) continue;
|
|
3179
|
+
radii.set(key, value);
|
|
3180
|
+
radii.defaults.add(key);
|
|
3181
|
+
}
|
|
2885
3182
|
return {
|
|
2886
3183
|
radii,
|
|
2887
3184
|
files
|
|
@@ -2901,6 +3198,10 @@ function collectRadius(object, radii) {
|
|
|
2901
3198
|
radii.set(key, raw);
|
|
2902
3199
|
}
|
|
2903
3200
|
}
|
|
3201
|
+
/** `module.exports`, as in `module.exports = require("@acme/tailwind-config")`. */
|
|
3202
|
+
function isModuleExports(node) {
|
|
3203
|
+
return ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
|
|
3204
|
+
}
|
|
2904
3205
|
/**
|
|
2905
3206
|
* Gives radius tokens their Tailwind v3 class names: each config entry is
|
|
2906
3207
|
* evaluated (its `var()` against radius token values, so
|
|
@@ -2909,15 +3210,29 @@ function collectRadius(object, radii) {
|
|
|
2909
3210
|
* token add a synthetic one, so the scale step exists as a candidate
|
|
2910
3211
|
* (mirrors the spacing-steps synthesis); without it the literal would keep
|
|
2911
3212
|
* matching the nearest unrelated token. Config entries win over presets.
|
|
3213
|
+
*
|
|
3214
|
+
* The steps of Tailwind's default scale ({@link RadiusEntries.defaults}) only add
|
|
3215
|
+
* a token for a size that has none, named after nothing the project names:
|
|
3216
|
+
* `rounded-xl` is 12px in a shadcn/ui v3 project, whose config extends the scale
|
|
3217
|
+
* with `lg`, `md` and `sm`. They add none when a config step has no size (its
|
|
3218
|
+
* `var(--radius)` is in no stylesheet read), as they would make a scale with
|
|
3219
|
+
* holes, nor when the tokens are a Tailwind v4 theme, whose own scale is read from
|
|
3220
|
+
* its stylesheet: Tailwind v4's defaults, or a token of an `@theme` variable outside
|
|
3221
|
+
* the colors (only v4 reads `@theme`; a v3 config names colors too).
|
|
2912
3222
|
*/
|
|
2913
3223
|
function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
|
|
2914
3224
|
const byVar = /* @__PURE__ */ new Map();
|
|
2915
3225
|
for (const token of tokens) if (token.category === "radius" && token.cssVar) byVar.set(token.cssVar, token.value);
|
|
2916
3226
|
const resolve = (name) => byVar.get(name);
|
|
3227
|
+
const defaults = radii instanceof RadiusEntries ? radii.defaults : /* @__PURE__ */ new Set();
|
|
3228
|
+
const v4 = tokens.some((t) => t.origin === "tailwind-default" || t.category !== "color" && t.tailwind !== void 0);
|
|
2917
3229
|
const byPx = /* @__PURE__ */ new Map();
|
|
3230
|
+
let unsized = false;
|
|
2918
3231
|
for (const [key, raw] of radii) {
|
|
3232
|
+
if (defaults.has(key)) continue;
|
|
2919
3233
|
const px = evaluateLength(raw, resolve);
|
|
2920
|
-
if (px
|
|
3234
|
+
if (px === void 0) unsized = true;
|
|
3235
|
+
else if (!byPx.has(px)) byPx.set(px, key);
|
|
2921
3236
|
}
|
|
2922
3237
|
const havePx = /* @__PURE__ */ new Set();
|
|
2923
3238
|
for (const token of tokens) {
|
|
@@ -2949,6 +3264,123 @@ function applyTailwindRadii(tokens, radii, configFile = "tailwind.config") {
|
|
|
2949
3264
|
tokens.push(token);
|
|
2950
3265
|
havePx.add(px);
|
|
2951
3266
|
}
|
|
3267
|
+
if (unsized || v4) return;
|
|
3268
|
+
const names = new Set(tokens.map((t) => t.name));
|
|
3269
|
+
for (const key of defaults) {
|
|
3270
|
+
const value = radii.get(key) ?? "";
|
|
3271
|
+
const px = evaluateLength(value, resolve);
|
|
3272
|
+
const name = `radius-${key}`;
|
|
3273
|
+
if (px === void 0 || havePx.has(px) || names.has(name)) continue;
|
|
3274
|
+
const token = {
|
|
3275
|
+
name,
|
|
3276
|
+
category: "radius",
|
|
3277
|
+
value,
|
|
3278
|
+
tailwind: key,
|
|
3279
|
+
usage: usageFor({
|
|
3280
|
+
category: "radius",
|
|
3281
|
+
tailwind: key
|
|
3282
|
+
}, "--radius"),
|
|
3283
|
+
description: "Tailwind's default radius scale.",
|
|
3284
|
+
origin: "tailwind-default",
|
|
3285
|
+
source: {
|
|
3286
|
+
file: configFile,
|
|
3287
|
+
line: 1
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
tokens.push(token);
|
|
3291
|
+
havePx.add(px);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* Whether an object is the value of a variable that its file uses only as a theme value of a
|
|
3296
|
+
* key other than `extend` and `borderRadius`, such as `const brand = { ...blue, ...blueDark }`
|
|
3297
|
+
* with `theme: { extend: { colors: brand } }`. What it spreads is then no config, theme or
|
|
3298
|
+
* radius scale. A variable that is exported, or used in any other way, may be one.
|
|
3299
|
+
*/
|
|
3300
|
+
function themeValueOnly(object, source) {
|
|
3301
|
+
let value = object;
|
|
3302
|
+
while (ts.isParenthesizedExpression(value.parent) || ts.isAsExpression(value.parent) || ts.isSatisfiesExpression(value.parent)) value = value.parent;
|
|
3303
|
+
const declaration = value.parent;
|
|
3304
|
+
if (!ts.isVariableDeclaration(declaration) || declaration.initializer !== value || !ts.isIdentifier(declaration.name) || !ts.isVariableStatement(declaration.parent.parent) || declaration.parent.parent.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)) return false;
|
|
3305
|
+
const name = declaration.name;
|
|
3306
|
+
let only = true;
|
|
3307
|
+
const visit = (node) => {
|
|
3308
|
+
if (!only) return;
|
|
3309
|
+
const parent = node.parent;
|
|
3310
|
+
const key = parent !== void 0 && (ts.isPropertyAssignment(parent) || ts.isPropertyAccessExpression(parent)) && parent.name === node;
|
|
3311
|
+
if (ts.isIdentifier(node) && node !== name && node.text === name.text && !key) only = isThemeValue(node);
|
|
3312
|
+
ts.forEachChild(node, visit);
|
|
3313
|
+
};
|
|
3314
|
+
visit(source);
|
|
3315
|
+
return only;
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Whether a name is a theme value, under a key other than `extend` and `borderRadius`:
|
|
3319
|
+
* `colors: brand`, or `colors: { brand }`.
|
|
3320
|
+
*/
|
|
3321
|
+
function isThemeValue(reference) {
|
|
3322
|
+
const parent = reference.parent;
|
|
3323
|
+
let property;
|
|
3324
|
+
if (ts.isPropertyAssignment(parent) && parent.initializer === reference) property = parent;
|
|
3325
|
+
else if (ts.isShorthandPropertyAssignment(parent)) property = parent;
|
|
3326
|
+
else return false;
|
|
3327
|
+
for (;;) {
|
|
3328
|
+
const key = propertyName(property.name);
|
|
3329
|
+
const object = property.parent;
|
|
3330
|
+
if (themeSection(object) !== void 0) return key !== void 0 && key !== "extend" && key !== "borderRadius";
|
|
3331
|
+
const holder = object.parent;
|
|
3332
|
+
if (!ts.isPropertyAssignment(holder) || holder.initializer !== object) return false;
|
|
3333
|
+
property = holder;
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
/** `theme` or `extend` for a property of `theme: {…}` or of `theme.extend: {…}`; else undefined. */
|
|
3337
|
+
function themeSection(object) {
|
|
3338
|
+
const holder = object.parent;
|
|
3339
|
+
if (!ts.isObjectLiteralExpression(object) || !ts.isPropertyAssignment(holder)) return void 0;
|
|
3340
|
+
const key = propertyName(holder.name);
|
|
3341
|
+
if (key === "theme") return "theme";
|
|
3342
|
+
return key === "extend" && themeSection(holder.parent) === "theme" ? "extend" : void 0;
|
|
3343
|
+
}
|
|
3344
|
+
/**
|
|
3345
|
+
* The names a module binds to what it imports or requires, by specifier: `acme` for
|
|
3346
|
+
* `import acme from "x"`, `import { acme } from "x"`, `const acme = require("x")` or
|
|
3347
|
+
* `const { default: acme } = require("x")`.
|
|
3348
|
+
*/
|
|
3349
|
+
function importBindings(source) {
|
|
3350
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
3351
|
+
const bind = (name, specifier) => {
|
|
3352
|
+
if (ts.isIdentifier(name)) {
|
|
3353
|
+
bindings.set(name.text, specifier);
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
for (const element of name.elements) if (!ts.isOmittedExpression(element)) bind(element.name, specifier);
|
|
3357
|
+
};
|
|
3358
|
+
for (const statement of source.statements) if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
3359
|
+
const specifier = statement.moduleSpecifier.text;
|
|
3360
|
+
const clause = statement.importClause;
|
|
3361
|
+
if (clause?.name) bind(clause.name, specifier);
|
|
3362
|
+
const named = clause?.namedBindings;
|
|
3363
|
+
if (named && ts.isNamespaceImport(named)) bind(named.name, specifier);
|
|
3364
|
+
else if (named) for (const element of named.elements) bind(element.name, specifier);
|
|
3365
|
+
} else if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
|
|
3366
|
+
const specifier = declaration.initializer && moduleOf(declaration.initializer, bindings);
|
|
3367
|
+
if (specifier !== void 0) bind(declaration.name, specifier);
|
|
3368
|
+
}
|
|
3369
|
+
return bindings;
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* The module an expression takes its value from: `"x"` for `require("x")`, `import("x")`, a
|
|
3373
|
+
* name bound to either ({@link importBindings}), a call of any of these
|
|
3374
|
+
* (`createPreset({…})`, `require("x")()`) or a property of one (`require("x").default`).
|
|
3375
|
+
*/
|
|
3376
|
+
function moduleOf(node, bindings) {
|
|
3377
|
+
let current = node;
|
|
3378
|
+
for (;;) if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isNonNullExpression(current) || ts.isAwaitExpression(current) || ts.isPropertyAccessExpression(current)) current = current.expression;
|
|
3379
|
+
else if (ts.isCallExpression(current)) {
|
|
3380
|
+
const specifier = importedSpecifier(current);
|
|
3381
|
+
if (specifier !== void 0) return specifier;
|
|
3382
|
+
current = current.expression;
|
|
3383
|
+
} else return ts.isIdentifier(current) ? bindings.get(current.text) : void 0;
|
|
2952
3384
|
}
|
|
2953
3385
|
/** The specifier of `require("x")`, `import x from "x"`, `export { default } from "x"` or `import("x")`. */
|
|
2954
3386
|
function importedSpecifier(node) {
|
|
@@ -3297,7 +3729,11 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3297
3729
|
const absolute = path.resolve(root, slashGlob(file));
|
|
3298
3730
|
if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
|
|
3299
3731
|
}
|
|
3300
|
-
const
|
|
3732
|
+
const components = (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob);
|
|
3733
|
+
const libraries = detectLibraries(root, config.libraries, {
|
|
3734
|
+
importPath: config.importPath,
|
|
3735
|
+
components
|
|
3736
|
+
});
|
|
3301
3737
|
const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens !== void 0 ? configTokens.map((t) => typeof t === "string" ? { path: fromBase(t) } : {
|
|
3302
3738
|
...t,
|
|
3303
3739
|
path: fromBase(t.path)
|
|
@@ -3316,7 +3752,7 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3316
3752
|
return {
|
|
3317
3753
|
root,
|
|
3318
3754
|
configFile,
|
|
3319
|
-
components
|
|
3755
|
+
components,
|
|
3320
3756
|
exclude: declared && config.exclude ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE, ...exclude])] : exclude,
|
|
3321
3757
|
...declared && config.exclude ? { designSystemExclude: exclude } : {},
|
|
3322
3758
|
tokens: tokenSources.map((t) => ({
|
|
@@ -4481,9 +4917,9 @@ function dedent(code) {
|
|
|
4481
4917
|
const MAX_LISTED_INHERITED = 40;
|
|
4482
4918
|
/**
|
|
4483
4919
|
* Reads a published library's components from its type declarations with the TypeScript
|
|
4484
|
-
* checker: every value its entry exports under a capitalised name whose type takes props
|
|
4485
|
-
* (function and class components, `forwardRef` and MUI's `OverridableComponent`), with
|
|
4486
|
-
* props of each call signature, their string-literal values (custom ones the app declares by
|
|
4920
|
+
* checker: every value its entry exports under a capitalised name whose type takes props and
|
|
4921
|
+
* renders (function and class components, `forwardRef` and MUI's `OverridableComponent`), with
|
|
4922
|
+
* the props of each call signature, their string-literal values (custom ones the app declares by
|
|
4487
4923
|
* module augmentation included), JSDoc, the element it renders, and the module that
|
|
4488
4924
|
* default-exports it. A signature generic over the element to render (MUI's `component`) marks
|
|
4489
4925
|
* that prop as polymorphic instead of adding its props.
|
|
@@ -4505,16 +4941,27 @@ function extractLibrary(options) {
|
|
|
4505
4941
|
return empty(`${adapter.package}: ${dir} could not be read`);
|
|
4506
4942
|
}
|
|
4507
4943
|
const resolve = (specifier) => ts.resolveModuleName(specifier, path.join(root, "__onsystem__.tsx"), options.project.options, ts.sys).resolvedModule?.resolvedFileName;
|
|
4508
|
-
const
|
|
4509
|
-
|
|
4944
|
+
const main = resolve(adapter.package) ?? path.join(real, typesEntry(real));
|
|
4945
|
+
const entries = isDeclarations(main) ? [{
|
|
4946
|
+
specifier: adapter.package,
|
|
4947
|
+
file: main
|
|
4948
|
+
}] : subpathEntries(real, adapter.package, resolve);
|
|
4949
|
+
if (!entries.length) return empty(`${adapter.package}: no type declarations at ${relativePath(root, main)}`);
|
|
4510
4950
|
const program = ts.createProgram({
|
|
4511
|
-
rootNames: [
|
|
4512
|
-
options: options.project.options
|
|
4951
|
+
rootNames: [...entries.map((e) => e.file), ...options.augmentations],
|
|
4952
|
+
options: options.project.options,
|
|
4953
|
+
host: declarationsHost(options.project.options)
|
|
4513
4954
|
});
|
|
4514
4955
|
const checker = program.getTypeChecker();
|
|
4515
|
-
const
|
|
4516
|
-
|
|
4517
|
-
|
|
4956
|
+
const modules = entries.flatMap((e) => {
|
|
4957
|
+
const file = program.getSourceFile(e.file);
|
|
4958
|
+
const symbol = file ? checker.getSymbolAtLocation(file) : void 0;
|
|
4959
|
+
return symbol ? [{
|
|
4960
|
+
...e,
|
|
4961
|
+
symbol
|
|
4962
|
+
}] : [];
|
|
4963
|
+
});
|
|
4964
|
+
if (!modules.length) return empty(`${adapter.package}: ${entries[0]?.file ?? main} has no exports`);
|
|
4518
4965
|
const shown = (file) => {
|
|
4519
4966
|
const absolute = path.resolve(file);
|
|
4520
4967
|
return relativePath(root, isInside(real, absolute) ? path.join(dir, path.relative(real, absolute)) : absolute);
|
|
@@ -4523,38 +4970,45 @@ function extractLibrary(options) {
|
|
|
4523
4970
|
checker,
|
|
4524
4971
|
adapter,
|
|
4525
4972
|
propSets: /* @__PURE__ */ new Map(),
|
|
4526
|
-
scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package
|
|
4973
|
+
scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package,
|
|
4974
|
+
icons: /* @__PURE__ */ new Set()
|
|
4527
4975
|
};
|
|
4528
4976
|
const exportNames = /* @__PURE__ */ new Set();
|
|
4529
4977
|
const bySymbol = /* @__PURE__ */ new Map();
|
|
4530
4978
|
const components = [];
|
|
4531
|
-
for (const
|
|
4532
|
-
const
|
|
4533
|
-
const
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4979
|
+
for (const module of modules) {
|
|
4980
|
+
const subpath = module.specifier !== adapter.package;
|
|
4981
|
+
for (const exported of checker.getExportsOfModule(module.symbol)) {
|
|
4982
|
+
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
|
|
4983
|
+
if (!(symbol.flags & ts.SymbolFlags.Value)) continue;
|
|
4984
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
4985
|
+
const isDefault = exported.getName() === "default";
|
|
4986
|
+
const name = isDefault && subpath && declaration ? ts.getNameOfDeclaration(declaration)?.getText() ?? "default" : exported.getName();
|
|
4987
|
+
if (isDefault && !/^[A-Z]/.test(name)) continue;
|
|
4988
|
+
exportNames.add(name);
|
|
4989
|
+
if (!/^[A-Z]/.test(name)) continue;
|
|
4990
|
+
const known = bySymbol.get(symbol);
|
|
4991
|
+
if (known) {
|
|
4992
|
+
if (known.name !== name && !known.aliases.includes(name)) known.aliases.push(name);
|
|
4993
|
+
continue;
|
|
4994
|
+
}
|
|
4995
|
+
if (!declaration) continue;
|
|
4996
|
+
const component = buildComponent(context, name, symbol);
|
|
4997
|
+
if (!component) continue;
|
|
4998
|
+
const file = declaration.getSourceFile();
|
|
4999
|
+
const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
|
|
5000
|
+
component.source = {
|
|
5001
|
+
file: shown(file.fileName),
|
|
5002
|
+
line
|
|
5003
|
+
};
|
|
5004
|
+
if (subpath) component.importPath = module.specifier;
|
|
5005
|
+
const defaultImport = isDefault ? module.specifier : defaultModule(program, checker, resolve, adapter.package, file, symbol);
|
|
5006
|
+
if (defaultImport) component.defaultImport = defaultImport;
|
|
5007
|
+
bySymbol.set(symbol, component);
|
|
5008
|
+
components.push(component);
|
|
4541
5009
|
}
|
|
4542
|
-
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
4543
|
-
if (!declaration) continue;
|
|
4544
|
-
const component = buildComponent(context, name, symbol);
|
|
4545
|
-
if (!component) continue;
|
|
4546
|
-
const file = declaration.getSourceFile();
|
|
4547
|
-
const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
|
|
4548
|
-
component.source = {
|
|
4549
|
-
file: shown(file.fileName),
|
|
4550
|
-
line
|
|
4551
|
-
};
|
|
4552
|
-
const defaultImport = defaultModule(program, checker, resolve, adapter.package, file, symbol);
|
|
4553
|
-
if (defaultImport) component.defaultImport = defaultImport;
|
|
4554
|
-
bySymbol.set(symbol, component);
|
|
4555
|
-
components.push(component);
|
|
4556
5010
|
}
|
|
4557
|
-
linkSlots(components);
|
|
5011
|
+
linkSlots(components, context.icons);
|
|
4558
5012
|
return {
|
|
4559
5013
|
components,
|
|
4560
5014
|
propSets: Object.fromEntries(context.propSets),
|
|
@@ -4563,6 +5017,29 @@ function extractLibrary(options) {
|
|
|
4563
5017
|
dependencies: unique(projectFiles(program))
|
|
4564
5018
|
};
|
|
4565
5019
|
}
|
|
5020
|
+
/**
|
|
5021
|
+
* A compiler host that resolves what the app's own resolution leaves unresolved in a package's
|
|
5022
|
+
* declarations by path, as Node.js's old resolution does. A bundler emits declarations that
|
|
5023
|
+
* import a dependency's file by its path (`class-variance-authority/dist/types`, for the
|
|
5024
|
+
* `ClassProp` behind every `cva()` variant), which that dependency's `exports` do not name: with
|
|
5025
|
+
* `"moduleResolution": "bundler"` the import is `any`, and a component's variants and props go
|
|
5026
|
+
* with it. The app's own files resolve as they do for tsc.
|
|
5027
|
+
*/
|
|
5028
|
+
function declarationsHost(options) {
|
|
5029
|
+
const host = ts.createCompilerHost(options);
|
|
5030
|
+
const cache = ts.createModuleResolutionCache(host.getCurrentDirectory(), (file) => host.getCanonicalFileName(file), options);
|
|
5031
|
+
const byPath = {
|
|
5032
|
+
...options,
|
|
5033
|
+
moduleResolution: ts.ModuleResolutionKind.Node10
|
|
5034
|
+
};
|
|
5035
|
+
host.resolveModuleNameLiterals = (literals, containingFile, redirected, compilerOptions, file) => literals.map((literal) => {
|
|
5036
|
+
const resolved = ts.resolveModuleName(literal.text, containingFile, compilerOptions, host, cache, redirected, ts.getModeForUsageLocation(file, literal, compilerOptions));
|
|
5037
|
+
if (resolved.resolvedModule || !/[\\/]node_modules[\\/].*\.d\.[cm]?ts$/.test(containingFile)) return resolved;
|
|
5038
|
+
const fallback = ts.resolveModuleName(literal.text, containingFile, byPath, host);
|
|
5039
|
+
return fallback.resolvedModule && /\.d\.[cm]?ts$/.test(fallback.resolvedModule.resolvedFileName) ? fallback : resolved;
|
|
5040
|
+
});
|
|
5041
|
+
return host;
|
|
5042
|
+
}
|
|
4566
5043
|
/** The declaration entry of an installed package: its `types`, else `index.d.ts`. */
|
|
4567
5044
|
function typesEntry(dir) {
|
|
4568
5045
|
try {
|
|
@@ -4572,9 +5049,106 @@ function typesEntry(dir) {
|
|
|
4572
5049
|
} catch {}
|
|
4573
5050
|
return "index.d.ts";
|
|
4574
5051
|
}
|
|
4575
|
-
/**
|
|
4576
|
-
function
|
|
4577
|
-
return
|
|
5052
|
+
/** A type declarations file on disk: `.d.ts`, `.d.mts` or `.d.cts`. */
|
|
5053
|
+
function isDeclarations(file) {
|
|
5054
|
+
return /\.d\.[cm]?ts$/.test(file) && fs.existsSync(file);
|
|
5055
|
+
}
|
|
5056
|
+
/** Packages exporting more subpaths than this are read from the first ones, sorted. */
|
|
5057
|
+
const MAX_SUBPATHS = 500;
|
|
5058
|
+
/**
|
|
5059
|
+
* The declarations of each subpath a package without an entry of its own exports (`./button`,
|
|
5060
|
+
* `./card`, or the files a `./*` pattern maps), with the specifier apps import it by
|
|
5061
|
+
* (`@acme/ui/button`): as the app's own imports resolve it, else through the `types` condition
|
|
5062
|
+
* its `exports` give it. Subpaths that name no declarations (`./theme.css`) are left out.
|
|
5063
|
+
*/
|
|
5064
|
+
function subpathEntries(dir, name, resolve) {
|
|
5065
|
+
let exports;
|
|
5066
|
+
try {
|
|
5067
|
+
exports = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).exports;
|
|
5068
|
+
} catch {
|
|
5069
|
+
return [];
|
|
5070
|
+
}
|
|
5071
|
+
if (!exports || typeof exports !== "object" || Array.isArray(exports)) return [];
|
|
5072
|
+
const found = /* @__PURE__ */ new Map();
|
|
5073
|
+
const add = (subpath, target) => {
|
|
5074
|
+
const specifier = `${name}/${subpath}`;
|
|
5075
|
+
const file = resolve(specifier) ?? (target ? path.resolve(dir, target) : void 0);
|
|
5076
|
+
if (file && isInside(dir, path.resolve(file)) && isDeclarations(file)) found.set(specifier, file);
|
|
5077
|
+
};
|
|
5078
|
+
for (const [key, value] of Object.entries(exports)) {
|
|
5079
|
+
if (!key.startsWith("./") || key === "./package.json") continue;
|
|
5080
|
+
const target = typesTarget(value);
|
|
5081
|
+
const star = key.indexOf("*");
|
|
5082
|
+
if (star === -1) {
|
|
5083
|
+
add(key.slice(2), target);
|
|
5084
|
+
continue;
|
|
5085
|
+
}
|
|
5086
|
+
const at = target?.indexOf("*") ?? -1;
|
|
5087
|
+
if (!target || at === -1) continue;
|
|
5088
|
+
const prefix = path.posix.normalize(target.slice(0, at)).replace(/^\.\//, "");
|
|
5089
|
+
const suffix = target.slice(at + 1);
|
|
5090
|
+
const folder = prefix.slice(0, prefix.lastIndexOf("/") + 1);
|
|
5091
|
+
const files = globSync(`${escapePath(folder)}**/*${escapePath(suffix)}`, {
|
|
5092
|
+
cwd: dir,
|
|
5093
|
+
ignore: ["**/node_modules/**"],
|
|
5094
|
+
expandDirectories: false
|
|
5095
|
+
});
|
|
5096
|
+
for (const file of files.sort()) {
|
|
5097
|
+
if (!file.startsWith(prefix) || !file.endsWith(suffix)) continue;
|
|
5098
|
+
const middle = file.slice(prefix.length, file.length - suffix.length);
|
|
5099
|
+
add(`${key.slice(2, star)}${middle}${key.slice(star + 1)}`, path.join(dir, file));
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
return [...found].sort(([a], [b]) => a.localeCompare(b)).slice(0, MAX_SUBPATHS).map(([specifier, file]) => ({
|
|
5103
|
+
specifier,
|
|
5104
|
+
file
|
|
5105
|
+
}));
|
|
5106
|
+
}
|
|
5107
|
+
/** The first `types` target an `exports` value gives, through nested conditions. */
|
|
5108
|
+
function typesTarget(value) {
|
|
5109
|
+
if (Array.isArray(value)) {
|
|
5110
|
+
for (const item of value) {
|
|
5111
|
+
const found = typesTarget(item);
|
|
5112
|
+
if (found) return found;
|
|
5113
|
+
}
|
|
5114
|
+
return;
|
|
5115
|
+
}
|
|
5116
|
+
if (!value || typeof value !== "object") return void 0;
|
|
5117
|
+
for (const [condition, nested] of Object.entries(value)) {
|
|
5118
|
+
if (condition === "types" && typeof nested === "string") return nested;
|
|
5119
|
+
const found = typesTarget(nested);
|
|
5120
|
+
if (found) return found;
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
/**
|
|
5124
|
+
* Call and construct signatures that take props and render, across a union
|
|
5125
|
+
* (`React.ComponentType`): a call that returns an element, and a class whose instances have
|
|
5126
|
+
* `render`. A capitalised function that returns anything else (`Format(value): string`) is no
|
|
5127
|
+
* component.
|
|
5128
|
+
*/
|
|
5129
|
+
function propSignatures(checker, type) {
|
|
5130
|
+
return (type.isUnion() ? type.types : [type]).flatMap((member) => [...member.getCallSignatures().filter((s) => rendersElement(checker, checker.getReturnTypeOfSignature(s))), ...member.getConstructSignatures().filter((s) => checker.getPropertyOfType(checker.getReturnTypeOfSignature(s), "render") !== void 0)]).filter((signature) => signature.getParameters().length >= 1);
|
|
5131
|
+
}
|
|
5132
|
+
/**
|
|
5133
|
+
* Whether a call's return type is what a component returns: a React element (`JSX.Element`,
|
|
5134
|
+
* `ReactElement`), a `ReactNode` that can be one, a promise of one (a server component), or
|
|
5135
|
+
* nothing (`null`). `any`, where React's types do not resolve, counts.
|
|
5136
|
+
*/
|
|
5137
|
+
function rendersElement(checker, type) {
|
|
5138
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Null)) return true;
|
|
5139
|
+
const members = (type.isUnion() ? type.types : [type]).filter((member) => !(member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)));
|
|
5140
|
+
if (!members.length) return true;
|
|
5141
|
+
return members.some((member) => {
|
|
5142
|
+
if (member.flags & ts.TypeFlags.Any) return true;
|
|
5143
|
+
if (checker.getPropertyOfType(member, "props") !== void 0 && checker.getPropertyOfType(member, "type") !== void 0) return true;
|
|
5144
|
+
const awaited = checker.getPropertyOfType(member, "then") ? checker.getAwaitedType(member) : void 0;
|
|
5145
|
+
return awaited !== void 0 && awaited !== member && rendersElement(checker, awaited);
|
|
5146
|
+
});
|
|
5147
|
+
}
|
|
5148
|
+
/** A conditional type the checker cannot resolve yet, or a union or intersection with one. */
|
|
5149
|
+
function hasConditional(type) {
|
|
5150
|
+
if (type.flags & ts.TypeFlags.Conditional) return true;
|
|
5151
|
+
return type.isUnionOrIntersection() && type.types.some((member) => hasConditional(member));
|
|
4578
5152
|
}
|
|
4579
5153
|
/** A React context (`ButtonGroupContext`): callable in React 19's types, but not a component to offer. */
|
|
4580
5154
|
function isContext(checker, type) {
|
|
@@ -4603,7 +5177,7 @@ function buildComponent(context, name, symbol) {
|
|
|
4603
5177
|
const { checker, adapter } = context;
|
|
4604
5178
|
const type = checker.getTypeOfSymbol(symbol);
|
|
4605
5179
|
if (isContext(checker, type)) return void 0;
|
|
4606
|
-
const signatures = propSignatures(type);
|
|
5180
|
+
const signatures = propSignatures(checker, type);
|
|
4607
5181
|
if (!signatures.length) return void 0;
|
|
4608
5182
|
let polymorphic;
|
|
4609
5183
|
const plain = [];
|
|
@@ -4624,6 +5198,7 @@ function buildComponent(context, name, symbol) {
|
|
|
4624
5198
|
openProps = true;
|
|
4625
5199
|
continue;
|
|
4626
5200
|
}
|
|
5201
|
+
if (signature.getTypeParameters()?.length && hasConditional(propsType)) openProps = true;
|
|
4627
5202
|
for (const part of propsType.isUnion() ? propsType.types : [propsType]) {
|
|
4628
5203
|
members++;
|
|
4629
5204
|
if (checker.getIndexInfosOfType(part).length) openProps = true;
|
|
@@ -4650,7 +5225,8 @@ function buildComponent(context, name, symbol) {
|
|
|
4650
5225
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
4651
5226
|
for (const [propName, { origin }] of entries) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], propName]);
|
|
4652
5227
|
const summarised = new Set([...byOrigin].filter(([origin, names]) => !own(origin) && (origin === "react" || names.length > MAX_LISTED_INHERITED)).map(([origin]) => origin));
|
|
4653
|
-
const
|
|
5228
|
+
const rendered = elementText(checker, used.at(-1));
|
|
5229
|
+
const element = renderedElement(rendered);
|
|
4654
5230
|
const props = [];
|
|
4655
5231
|
const variants = [];
|
|
4656
5232
|
for (const [propName, entry] of entries) {
|
|
@@ -4704,6 +5280,7 @@ function buildComponent(context, name, symbol) {
|
|
|
4704
5280
|
};
|
|
4705
5281
|
if (polymorphic) info.polymorphic = polymorphic.name;
|
|
4706
5282
|
if (element) info.element = element;
|
|
5283
|
+
if (rendered && /\bSVG\w*Element\b/.test(rendered)) context.icons.add(info);
|
|
4707
5284
|
const jsdoc = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
4708
5285
|
const description = jsdoc ? adapter.describe?.(jsdoc, name) ?? jsdoc : void 0;
|
|
4709
5286
|
if (description) info.description = description;
|
|
@@ -4805,15 +5382,23 @@ function literalValues$1(checker, type, depth = 0) {
|
|
|
4805
5382
|
}
|
|
4806
5383
|
return values.length ? unique(values) : void 0;
|
|
4807
5384
|
}
|
|
4808
|
-
/** The native element
|
|
4809
|
-
function renderedElement(
|
|
5385
|
+
/** The native element an {@link elementText} names: `button` for `Ref<HTMLButtonElement>`. */
|
|
5386
|
+
function renderedElement(text) {
|
|
5387
|
+
const element = text ? /\bHTML(\w+?)Element\b/.exec(text)?.[1] : void 0;
|
|
5388
|
+
return element ? ELEMENT_INTERFACES[element] : void 0;
|
|
5389
|
+
}
|
|
5390
|
+
/**
|
|
5391
|
+
* What names the native element behind a signature, as text: the type of the `ref` its props
|
|
5392
|
+
* take, or without one, of the DOM event handler they take (`HTMLAttributes<HTMLSpanElement>`
|
|
5393
|
+
* gives `onFocus: FocusEventHandler<HTMLSpanElement>`), as a function component's props declare
|
|
5394
|
+
* it. An icon's names an SVG element (`FocusEventHandler<SVGSVGElement>`).
|
|
5395
|
+
*/
|
|
5396
|
+
function elementText(checker, signature) {
|
|
4810
5397
|
const param = signature?.getParameters()[0];
|
|
4811
5398
|
if (!param) return void 0;
|
|
4812
|
-
const
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
const element = /\bHTML(\w+?)Element\b/.exec(text)?.[1];
|
|
4816
|
-
return element ? ELEMENT_INTERFACES[element] : void 0;
|
|
5399
|
+
const props = checker.getTypeOfSymbol(param);
|
|
5400
|
+
const typed = checker.getPropertyOfType(props, "ref") ?? checker.getPropertyOfType(props, "onFocus");
|
|
5401
|
+
return typed ? checker.typeToString(checker.getTypeOfSymbol(typed)) : void 0;
|
|
4817
5402
|
}
|
|
4818
5403
|
/**
|
|
4819
5404
|
* The folder module that default-exports a component (`@mui/material/Button` for
|
|
@@ -4831,16 +5416,36 @@ function defaultModule(program, checker, resolve, name, file, symbol) {
|
|
|
4831
5416
|
}
|
|
4832
5417
|
/**
|
|
4833
5418
|
* Styled slots a library exports next to the component they belong to (`SliderThumb` in
|
|
4834
|
-
* `Slider.d.ts`
|
|
4835
|
-
|
|
4836
|
-
|
|
5419
|
+
* `Slider.d.ts`, `CardHeader` and `CardContent` next to `Card` in a bundle's one `index.d.ts`)
|
|
5420
|
+
* become its parts, so lists show the component and its parts under it. A name that only goes
|
|
5421
|
+
* on in lower case (`Tabs` next to `Tab`) is another component, and so is a lone container
|
|
5422
|
+
* (`CheckboxGroup` next to `Checkbox`), as for source components. Icons are no parts and have
|
|
5423
|
+
* none (`UserCheck` next to `User`). In a file that declares several components of their own,
|
|
5424
|
+
* as a bundle does, a component with one such name next to it is no family
|
|
5425
|
+
* (`ButtonTooltip` next to `Button`): only two or more are its parts.
|
|
5426
|
+
*/
|
|
5427
|
+
function linkSlots(components, icons) {
|
|
4837
5428
|
const byFile = /* @__PURE__ */ new Map();
|
|
4838
|
-
for (const component of components)
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
5429
|
+
for (const component of components) {
|
|
5430
|
+
if (icons.has(component)) continue;
|
|
5431
|
+
byFile.set(component.source.file, [...byFile.get(component.source.file) ?? [], component]);
|
|
5432
|
+
}
|
|
5433
|
+
for (const group of byFile.values()) {
|
|
5434
|
+
const parts = /* @__PURE__ */ new Map();
|
|
5435
|
+
for (const component of group) {
|
|
5436
|
+
const owner = group.filter((other) => other !== component && component.name.length > other.name.length && component.name.startsWith(other.name) && /^[A-Z]/.test(component.name.charAt(other.name.length))).sort((a, b) => b.name.length - a.name.length)[0];
|
|
5437
|
+
if (owner) parts.set(owner, [...parts.get(owner) ?? [], component]);
|
|
5438
|
+
}
|
|
5439
|
+
const owned = new Set([...parts.values()].flat());
|
|
5440
|
+
const bundle = group.filter((component) => !owned.has(component)).length > 1;
|
|
5441
|
+
for (const [owner, family] of parts) {
|
|
5442
|
+
if (family.every((part) => /^(?:Group|List)$/.test(part.name.slice(owner.name.length)))) continue;
|
|
5443
|
+
if (bundle && family.length < 2) continue;
|
|
5444
|
+
for (const part of family) {
|
|
5445
|
+
part.parent = owner.name;
|
|
5446
|
+
owner.subcomponents.push(part.name);
|
|
5447
|
+
}
|
|
5448
|
+
}
|
|
4844
5449
|
}
|
|
4845
5450
|
}
|
|
4846
5451
|
//#endregion
|
|
@@ -4861,7 +5466,8 @@ const NOT_SOURCES = [
|
|
|
4861
5466
|
/**
|
|
4862
5467
|
* The components and tokens of the libraries `config` found (`config.libraries`): each one's
|
|
4863
5468
|
* components from its type declarations, read together with the app's files that augment them,
|
|
4864
|
-
* and the tokens of the theme the app creates for it.
|
|
5469
|
+
* and the tokens of the theme the app creates for it. A design-system package installed without
|
|
5470
|
+
* its sources is read the same way, with no theme.
|
|
4865
5471
|
*/
|
|
4866
5472
|
async function loadLibraries(config) {
|
|
4867
5473
|
const loaded = {
|
|
@@ -4873,7 +5479,7 @@ async function loadLibraries(config) {
|
|
|
4873
5479
|
dependencies: []
|
|
4874
5480
|
};
|
|
4875
5481
|
const libraries = (config.libraries ?? []).flatMap((source) => {
|
|
4876
|
-
const adapter = libraryAdapter(source.package);
|
|
5482
|
+
const adapter = libraryAdapter(source.package) ?? (source.designSystem ? packageAdapter(source.package) : void 0);
|
|
4877
5483
|
return adapter ? [{
|
|
4878
5484
|
source,
|
|
4879
5485
|
adapter
|
|
@@ -5892,7 +6498,7 @@ function cssSheetTokens(sheets) {
|
|
|
5892
6498
|
const category = isSpacingBase ? "spacing" : namespace?.[1] ?? "other";
|
|
5893
6499
|
const tailwindKey = isSpacingBase ? "" : variable.name.slice(namespace?.[0].length ?? 0);
|
|
5894
6500
|
const tailwindNamespace = isSpacingBase ? "--spacing" : namespace?.[0].slice(0, -1) ?? "";
|
|
5895
|
-
const alias =
|
|
6501
|
+
const alias = themeAlias(variable.value, base, lookup);
|
|
5896
6502
|
const target = alias ? base.get(alias) : void 0;
|
|
5897
6503
|
if (alias && target) {
|
|
5898
6504
|
consumed.add(alias);
|
|
@@ -5913,7 +6519,7 @@ function cssSheetTokens(sheets) {
|
|
|
5913
6519
|
add(finalize({
|
|
5914
6520
|
name: variable.name.slice(2),
|
|
5915
6521
|
category,
|
|
5916
|
-
value: variable.value,
|
|
6522
|
+
value: resolveValue(variable.value, lookup),
|
|
5917
6523
|
modes: modeValues(variable.name, modes, lookup),
|
|
5918
6524
|
cssVar: variable.inline ? void 0 : variable.name,
|
|
5919
6525
|
tailwind: tailwindKey,
|
|
@@ -6044,6 +6650,22 @@ function categorize$1(name, value) {
|
|
|
6044
6650
|
function varAlias(value) {
|
|
6045
6651
|
return /^var\(\s*(--[\w-]+)\s*\)$/.exec(value.trim())?.[1];
|
|
6046
6652
|
}
|
|
6653
|
+
/**
|
|
6654
|
+
* The base variable an `@theme` variable stands for: `--x` for `var(--x)`, and for
|
|
6655
|
+
* `hsl(var(--x))` or `rgb(var(--x))` when `--x` holds bare HSL or RGB channels, as
|
|
6656
|
+
* shadcn/ui's Tailwind v4 codemod leaves a v3 theme (`--color-primary: hsl(var(--primary))`).
|
|
6657
|
+
* The token is then `--x`, whose usage already wraps the channels (`hsl(var(--primary))`).
|
|
6658
|
+
*/
|
|
6659
|
+
function themeAlias(value, base, lookup) {
|
|
6660
|
+
const plain = varAlias(value);
|
|
6661
|
+
if (plain) return plain;
|
|
6662
|
+
const match = /^(hsl|rgb)a?\(\s*var\(\s*(--[\w-]+)\s*\)\s*(?:\/\s*<alpha-value>\s*)?\)$/.exec(value.trim());
|
|
6663
|
+
const name = match?.[2];
|
|
6664
|
+
const target = name ? base.get(name) : void 0;
|
|
6665
|
+
if (!match || !name || !target) return void 0;
|
|
6666
|
+
const channels = resolveValue(target.value, lookup);
|
|
6667
|
+
return (match[1] === "hsl" ? isBareHsl(channels) : isBareRgb(channels)) ? name : void 0;
|
|
6668
|
+
}
|
|
6047
6669
|
/** Follows plain `var(--x)` aliases so the token shows its concrete value. */
|
|
6048
6670
|
function resolveValue(value, lookup, depth = 0) {
|
|
6049
6671
|
const alias = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value)?.[1];
|
|
@@ -6665,24 +7287,33 @@ const CORE_FAMILIES = /* @__PURE__ */ new Set([
|
|
|
6665
7287
|
"overlay",
|
|
6666
7288
|
"link"
|
|
6667
7289
|
]);
|
|
6668
|
-
/** Name segments of status colors: what happened, rather than how it looks. */
|
|
6669
|
-
const
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
7290
|
+
/** Name segments of status colors, by the status they show: what happened, rather than how it looks. */
|
|
7291
|
+
const STATUS_KINDS = {
|
|
7292
|
+
destructive: [
|
|
7293
|
+
"destructive",
|
|
7294
|
+
"danger",
|
|
7295
|
+
"error",
|
|
7296
|
+
"critical",
|
|
7297
|
+
"negative",
|
|
7298
|
+
"invalid"
|
|
7299
|
+
],
|
|
7300
|
+
warning: [
|
|
7301
|
+
"warning",
|
|
7302
|
+
"warn",
|
|
7303
|
+
"caution"
|
|
7304
|
+
],
|
|
7305
|
+
success: [
|
|
7306
|
+
"success",
|
|
7307
|
+
"positive",
|
|
7308
|
+
"valid"
|
|
7309
|
+
],
|
|
7310
|
+
info: [
|
|
7311
|
+
"info",
|
|
7312
|
+
"information",
|
|
7313
|
+
"notice"
|
|
7314
|
+
]
|
|
7315
|
+
};
|
|
7316
|
+
const STATUS_WORDS = new Set(Object.values(STATUS_KINDS).flat());
|
|
6686
7317
|
/** The family a color token belongs to: the first segment of its name (`sidebar`, `sh`, `rainbow`). */
|
|
6687
7318
|
function tokenFamily(token) {
|
|
6688
7319
|
return tokenKey(token).split("-")[0] ?? "";
|
|
@@ -6725,6 +7356,19 @@ function scopesIn(families, ...texts) {
|
|
|
6725
7356
|
function isStatusToken(token) {
|
|
6726
7357
|
return roleKey(token).split("-").some((segment) => STATUS_WORDS.has(segment));
|
|
6727
7358
|
}
|
|
7359
|
+
/**
|
|
7360
|
+
* The statuses a theme has a color for: `success` for `--success`, Dub's `content-success` or a
|
|
7361
|
+
* DTCG `color.feedback.positive`, `destructive` for shadcn/ui's `--destructive` or `error`.
|
|
7362
|
+
*/
|
|
7363
|
+
function statusKinds(tokens) {
|
|
7364
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
7365
|
+
for (const token of tokens) {
|
|
7366
|
+
if (token.category !== "color") continue;
|
|
7367
|
+
const segments = roleKey(token).split("-");
|
|
7368
|
+
for (const [kind, words] of Object.entries(STATUS_KINDS)) if (segments.some((segment) => words.includes(segment))) kinds.add(kind);
|
|
7369
|
+
}
|
|
7370
|
+
return kinds;
|
|
7371
|
+
}
|
|
6728
7372
|
/** `primary` for `--color-primary`, `text-muted` for the DTCG token `color.text.muted`. */
|
|
6729
7373
|
function tokenKey(token) {
|
|
6730
7374
|
return (token.tailwind || token.name).toLowerCase().replace(/[.\s_]+/g, "-").replace(/^colou?rs?-/, "");
|
|
@@ -6887,7 +7531,7 @@ function toDtcg(tokens) {
|
|
|
6887
7531
|
const paths = assignPaths(tokens, warnings);
|
|
6888
7532
|
const lookup = varLookup(tokens);
|
|
6889
7533
|
const byName = new Map(tokens.map((t) => [t.name, t]));
|
|
6890
|
-
const tree =
|
|
7534
|
+
const tree = Object.create(null);
|
|
6891
7535
|
let count = 0;
|
|
6892
7536
|
for (const category of TOKEN_CATEGORIES) for (const token of tokens) {
|
|
6893
7537
|
const path = token.category === category ? paths.get(token) : void 0;
|
|
@@ -6969,7 +7613,7 @@ function insert(tree, path, node) {
|
|
|
6969
7613
|
const next = group[key];
|
|
6970
7614
|
if (isObject$1(next)) group = next;
|
|
6971
7615
|
else {
|
|
6972
|
-
const created =
|
|
7616
|
+
const created = Object.create(null);
|
|
6973
7617
|
group[key] = created;
|
|
6974
7618
|
group = created;
|
|
6975
7619
|
}
|
|
@@ -7201,7 +7845,7 @@ function colorObject(css) {
|
|
|
7201
7845
|
else if (spec.space === "srgb" && components.every((c) => typeof c === "number" && Math.abs(c * 255 - Math.round(c * 255)) < .001)) out.hex = formatHex(color);
|
|
7202
7846
|
return out;
|
|
7203
7847
|
}
|
|
7204
|
-
const PLAIN_LENGTH = /^(-?(?:\d
|
|
7848
|
+
const PLAIN_LENGTH = /^(-?(?:\d+(?:\.\d*)?|\.\d+))(px|rem)$/;
|
|
7205
7849
|
/** `0.5rem` → `{ value: 0.5, unit: "rem" }`; a `calc()` of tokens → its px value. */
|
|
7206
7850
|
function dimensionValue(value, lookup) {
|
|
7207
7851
|
const plain = plainDimension(value);
|
|
@@ -7231,8 +7875,8 @@ function plainDimension(value) {
|
|
|
7231
7875
|
unit: match[2] ?? "px"
|
|
7232
7876
|
} : void 0;
|
|
7233
7877
|
}
|
|
7234
|
-
const COMPOSITE = /^[\w-]+\s
|
|
7235
|
-
const NUMBER = /^-?(?:\d
|
|
7878
|
+
const COMPOSITE = /^[\w-]+\s*:[^;]+(?:;\s*[\w-]+\s*:[^;]+)*;?$/;
|
|
7879
|
+
const NUMBER = /^-?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
7236
7880
|
/** What a typography token sets, from its name and Tailwind class (`font-sans`, `text-sm`). */
|
|
7237
7881
|
function typographyKind(token) {
|
|
7238
7882
|
const value = token.value.trim();
|
|
@@ -7492,7 +8136,7 @@ function frontMatterValue(token, key, lookup) {
|
|
|
7492
8136
|
}
|
|
7493
8137
|
}
|
|
7494
8138
|
/** A DESIGN.md dimension: px, em or rem. */
|
|
7495
|
-
const DESIGN_DIMENSION = /^-?(?:\d
|
|
8139
|
+
const DESIGN_DIMENSION = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:px|rem|em)$/;
|
|
7496
8140
|
/**
|
|
7497
8141
|
* A typography token as a DESIGN.md typography level: `{ fontFamily: "Inter" }` for
|
|
7498
8142
|
* `--font-sans`, every property of a composite. A `var()` is no font name (`next/font` sets
|
|
@@ -9557,6 +10201,35 @@ function labelledByParent(context, element) {
|
|
|
9557
10201
|
return !ts.isJsxElement(value) && !ts.isJsxSelfClosingElement(value) && !ts.isJsxFragment(value);
|
|
9558
10202
|
}
|
|
9559
10203
|
/**
|
|
10204
|
+
* What an `id` or `htmlFor` holds, to compare the two: a string as itself (`"dark-mode"`, `{"x"}`),
|
|
10205
|
+
* an expression as written (`{id}`, `` {`${id}-switch`} ``) together with the function it is in,
|
|
10206
|
+
* since two components in one file can each have their own `id`. Undefined when it holds nothing.
|
|
10207
|
+
*/
|
|
10208
|
+
function idValue(context, attribute) {
|
|
10209
|
+
const init = attribute?.initializer;
|
|
10210
|
+
if (!init) return void 0;
|
|
10211
|
+
const value = ts.isJsxExpression(init) ? init.expression : init;
|
|
10212
|
+
if (!value) return void 0;
|
|
10213
|
+
if (ts.isStringLiteralLike(value)) return value.text.trim() ? `"${value.text}"` : void 0;
|
|
10214
|
+
const scope = ts.findAncestor(value, ts.isFunctionLike);
|
|
10215
|
+
return `${String(scope?.pos ?? -1)}:${value.getText(context.sourceFile)}`;
|
|
10216
|
+
}
|
|
10217
|
+
/**
|
|
10218
|
+
* Whether a label with content names the button through its `id`: `<label htmlFor={id}>Dark
|
|
10219
|
+
* mode</label>` next to `<button id={id} role="switch">`, or the design system's `<Label>`. A
|
|
10220
|
+
* button is a labelable element, so the label's text is its accessible name.
|
|
10221
|
+
*/
|
|
10222
|
+
function labelledByFor(context, element) {
|
|
10223
|
+
const id = idValue(context, findAttribute(element, "id"));
|
|
10224
|
+
if (id === void 0) return false;
|
|
10225
|
+
return context.analysis.elements.some((label) => {
|
|
10226
|
+
if (label === element) return false;
|
|
10227
|
+
if (idValue(context, findAttribute(label, "htmlFor") ?? findAttribute(label, "for")) !== id) return false;
|
|
10228
|
+
const content = classify(label.children, context, []);
|
|
10229
|
+
return content === "text" || content === "unknown";
|
|
10230
|
+
});
|
|
10231
|
+
}
|
|
10232
|
+
/**
|
|
9560
10233
|
* The element whose `render` prop this one is, as in Base UI's
|
|
9561
10234
|
* `<Dialog.Close render={<Button size="icon" />}><XIcon /></Dialog.Close>`:
|
|
9562
10235
|
* the rendered button takes the host's children and attributes. Also through
|
|
@@ -9662,6 +10335,7 @@ const iconButtonAccessibleName = {
|
|
|
9662
10335
|
if (host && (isButton(context, host) || host.hasSpread)) continue;
|
|
9663
10336
|
if (hasLabel(element.attributes) || host && hasLabel(host.attributes)) continue;
|
|
9664
10337
|
if (labelledByParent(context, element)) continue;
|
|
10338
|
+
if (labelledByFor(context, element) || host && labelledByFor(context, host)) continue;
|
|
9665
10339
|
const icons = [];
|
|
9666
10340
|
const children = host ? [...host.children, ...element.children] : element.children;
|
|
9667
10341
|
const content = host && !children.length ? "unknown" : classify(children, context, icons);
|
|
@@ -9946,8 +10620,63 @@ const noHardcodedColor = {
|
|
|
9946
10620
|
}
|
|
9947
10621
|
}
|
|
9948
10622
|
}
|
|
10623
|
+
reportChartTokens(context, allow);
|
|
9949
10624
|
}
|
|
9950
10625
|
};
|
|
10626
|
+
/** Packages that draw charts: Recharts, Chart.js, Nivo, visx, Victory, Tremor, MUI X Charts, D3. */
|
|
10627
|
+
const CHART_LIBRARY = /chart|^@nivo\/|^@visx\/|^victory|^@tremor\/|^d3(?:-|$)/i;
|
|
10628
|
+
/** Roles of an element that is a drawing: a chart drawn by hand is an image. */
|
|
10629
|
+
const DRAWING_ROLE = /^(?:img|figure|graphics-[a-z]+)$/;
|
|
10630
|
+
const CHART_TOKEN = "is a chart color, and this is not a chart: it changes with the chart palette, and it does not say what a status or brand color says. For a status color, add a token for it to the theme, such as `--success`; otherwise use the semantic token that fits.";
|
|
10631
|
+
/**
|
|
10632
|
+
* Warns on a chart token outside a chart: `text-chart-2` on a green "Active" label in a theme
|
|
10633
|
+
* with no success color. The class is a token, so the color rule would call it clean, but it
|
|
10634
|
+
* says nothing a status or brand color says, and it changes with the chart palette (a gray or a
|
|
10635
|
+
* blue in some shadcn/ui themes). A file that draws a chart is in scope throughout, legend dots
|
|
10636
|
+
* and all: one that imports a chart library, renders a tag such as `<ChartContainer>` or
|
|
10637
|
+
* `<BarChart>`, declares a name such as `chartData`, or is named for charts. So is an element
|
|
10638
|
+
* in a drawing (`<svg>`, `role="img"`), as a chart drawn by hand is. When the design system's
|
|
10639
|
+
* own components use chart tokens outside a chart, they are a palette it shares: nothing is
|
|
10640
|
+
* reported.
|
|
10641
|
+
*/
|
|
10642
|
+
function reportChartTokens(context, allow) {
|
|
10643
|
+
const { tokens } = context.target;
|
|
10644
|
+
if (!tokens.scopedFamilies.has("chart")) return;
|
|
10645
|
+
const chart = /* @__PURE__ */ new Set(["chart"]);
|
|
10646
|
+
if ([...tokens.familyFiles.get("chart") ?? []].some((file) => !scopesIn(chart, file).size)) return;
|
|
10647
|
+
const { elements, imports, declared } = context.analysis;
|
|
10648
|
+
if (scopesIn(chart, ...[
|
|
10649
|
+
context.file,
|
|
10650
|
+
...elements.map((e) => e.tag),
|
|
10651
|
+
...declared
|
|
10652
|
+
]).size) return;
|
|
10653
|
+
if ([...imports.values()].some((binding) => CHART_LIBRARY.test(binding.source))) return;
|
|
10654
|
+
for (const classString of context.analysis.classStrings) {
|
|
10655
|
+
const { element } = classString;
|
|
10656
|
+
if (element && inDrawing(element)) continue;
|
|
10657
|
+
for (const cls of splitClasses(classString.text, classString.start)) {
|
|
10658
|
+
const key = COLOR_UTILITY.exec(parseUtility(cls.value).base)?.[1];
|
|
10659
|
+
if (!key?.startsWith("chart-") || !tokens.colorKeys.has(key)) continue;
|
|
10660
|
+
if (allow.has(cls.value.toLowerCase())) continue;
|
|
10661
|
+
context.report({
|
|
10662
|
+
start: cls.start,
|
|
10663
|
+
end: cls.end,
|
|
10664
|
+
message: `\`${cls.value}\` ${CHART_TOKEN}`,
|
|
10665
|
+
severity: "warning"
|
|
10666
|
+
});
|
|
10667
|
+
}
|
|
10668
|
+
}
|
|
10669
|
+
}
|
|
10670
|
+
/** Whether the element is, or is in, a drawing: an `<svg>`, a `<figure>` or a `role="img"`. */
|
|
10671
|
+
function inDrawing(element) {
|
|
10672
|
+
return ts.findAncestor(element.node, (node) => {
|
|
10673
|
+
if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) return false;
|
|
10674
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
10675
|
+
const tag = opening.tagName.getText(element.node.getSourceFile());
|
|
10676
|
+
if (tag === "svg" || tag === "figure") return true;
|
|
10677
|
+
return opening.attributes.properties.some((attribute) => ts.isJsxAttribute(attribute) && attributeName(attribute) === "role" && literalValues(attribute).some((value) => DRAWING_ROLE.test(value.text)));
|
|
10678
|
+
}) !== void 0;
|
|
10679
|
+
}
|
|
9951
10680
|
function reportLiteralColors(context, literal, element, options) {
|
|
9952
10681
|
const start = literal.getStart(context.sourceFile) + 1;
|
|
9953
10682
|
const text = context.text.slice(start, literal.end - 1);
|
|
@@ -10984,28 +11713,36 @@ function readBaseline(file) {
|
|
|
10984
11713
|
rules: parsed.rules ? new Set(parsed.rules) : void 0
|
|
10985
11714
|
};
|
|
10986
11715
|
}
|
|
11716
|
+
/** Whether `diagnostic` is on one of the `lines`, such as those a pull request changed. */
|
|
11717
|
+
function onLines(lines, diagnostic) {
|
|
11718
|
+
return lines?.some(([start, end]) => start <= diagnostic.endLine && end >= diagnostic.line) ?? false;
|
|
11719
|
+
}
|
|
10987
11720
|
/**
|
|
10988
11721
|
* Removes the findings the baseline accepts for `file`. Within a file and
|
|
10989
11722
|
* rule, each entry accepts that many findings with the same source text, in
|
|
10990
|
-
* order; any beyond the count are new.
|
|
10991
|
-
*
|
|
11723
|
+
* order; any beyond the count are new. With `changed`, the lines a pull request
|
|
11724
|
+
* changed, findings on other lines are accepted first: a copy of a baselined
|
|
11725
|
+
* line inserted above it is the new one, not the line that was there. Entries
|
|
11726
|
+
* of `disabled` rules are not counted as fixed: the rule is off, not satisfied.
|
|
10992
11727
|
*/
|
|
10993
|
-
function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set()) {
|
|
11728
|
+
function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set(), changed) {
|
|
10994
11729
|
const remaining = /* @__PURE__ */ new Map();
|
|
10995
11730
|
for (const [rule, sources] of baseline.get(file) ?? []) {
|
|
10996
11731
|
if (disabled.has(rule)) continue;
|
|
10997
11732
|
for (const [source, count] of sources) remaining.set(`${rule}\0${source}`, count);
|
|
10998
11733
|
}
|
|
10999
|
-
const
|
|
11000
|
-
|
|
11001
|
-
|
|
11734
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
11735
|
+
for (const later of [false, true]) for (const diagnostic of result.diagnostics) {
|
|
11736
|
+
if (onLines(changed, diagnostic) !== later) continue;
|
|
11002
11737
|
const id = `${diagnostic.ruleId}\0${findingKey(diagnostic)}`;
|
|
11003
11738
|
const left = remaining.get(id) ?? 0;
|
|
11004
11739
|
if (left > 0) {
|
|
11005
11740
|
remaining.set(id, left - 1);
|
|
11006
|
-
|
|
11007
|
-
}
|
|
11741
|
+
accepted.add(diagnostic);
|
|
11742
|
+
}
|
|
11008
11743
|
}
|
|
11744
|
+
const diagnostics = result.diagnostics.filter((d) => !accepted.has(d));
|
|
11745
|
+
const baselined = accepted.size;
|
|
11009
11746
|
let fixed = 0;
|
|
11010
11747
|
for (const count of remaining.values()) fixed += count;
|
|
11011
11748
|
return {
|
|
@@ -11020,15 +11757,116 @@ function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Se
|
|
|
11020
11757
|
};
|
|
11021
11758
|
}
|
|
11022
11759
|
/**
|
|
11760
|
+
* Whether the key `file`, relative to the real `root`, still names a file by that spelling. On a
|
|
11761
|
+
* disk that ignores case (macOS, Windows), `Billing.tsx` still opens after a rename to
|
|
11762
|
+
* `billing.tsx`, but `check` keys the file by its new spelling, so the old key is gone, as on
|
|
11763
|
+
* Linux.
|
|
11764
|
+
*/
|
|
11765
|
+
function existsAs(root, file) {
|
|
11766
|
+
try {
|
|
11767
|
+
return relativePath(root, fs.realpathSync.native(path.resolve(root, file))).normalize("NFC") === file;
|
|
11768
|
+
} catch {
|
|
11769
|
+
return false;
|
|
11770
|
+
}
|
|
11771
|
+
}
|
|
11772
|
+
/**
|
|
11773
|
+
* The files with baseline entries that no longer exist under `root`, deleted or renamed, among
|
|
11774
|
+
* those `include` takes. `checked` are files a run read, which exist.
|
|
11775
|
+
*/
|
|
11776
|
+
function goneFiles(entries, root, checked, include = () => true) {
|
|
11777
|
+
return [...entries.keys()].filter((file) => !checked.has(file) && include(file) && !existsAs(root, file));
|
|
11778
|
+
}
|
|
11779
|
+
/**
|
|
11780
|
+
* Renamed and moved files (`git mv`, a folder reorganised): each file of `unrecorded` (file → its
|
|
11781
|
+
* findings), which has no entries of its own, paired with a file of `gone` whose entries match at
|
|
11782
|
+
* least half of its findings and half of those entries match its findings, as git counts a
|
|
11783
|
+
* rename from 50% similarity. Each file of `gone` pairs with one file at most, so a copy of a
|
|
11784
|
+
* file is new however many copies there are. Entries of rules `disabled` for the new file are
|
|
11785
|
+
* left out, as `applyBaseline` leaves them out. Returns new file → old file.
|
|
11786
|
+
*
|
|
11787
|
+
* Best match first: the file with most matching findings that are not `fresh`, then the one with
|
|
11788
|
+
* most matching findings, then the pair with fewest findings and entries the other does not
|
|
11789
|
+
* match. With `fresh`, the findings on the lines a pull request changed, a file the pull request
|
|
11790
|
+
* did not touch keeps the entries it took over in an earlier move, and a copy the pull request
|
|
11791
|
+
* adds is the new one, as `applyBaseline` spends entries on the other lines first.
|
|
11792
|
+
* Without it, as in a plain `check`, ties go to a file that kept the old one's name (a folder
|
|
11793
|
+
* moved), then to one in the old one's folder (a file renamed), then to path order.
|
|
11794
|
+
*/
|
|
11795
|
+
function findMoves(entries, unrecorded, gone, disabled = () => /* @__PURE__ */ new Set(), fresh = () => false) {
|
|
11796
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
11797
|
+
for (const file of gone) for (const [rule, sources] of entries.get(file) ?? []) for (const [source, count] of sources) {
|
|
11798
|
+
const id = `${rule}\0${source}`;
|
|
11799
|
+
const list = recorded.get(id) ?? [];
|
|
11800
|
+
list.push({
|
|
11801
|
+
file,
|
|
11802
|
+
count
|
|
11803
|
+
});
|
|
11804
|
+
recorded.set(id, list);
|
|
11805
|
+
}
|
|
11806
|
+
const pairs = [];
|
|
11807
|
+
for (const [to, diagnostics] of unrecorded) {
|
|
11808
|
+
if (entries.has(to) || !diagnostics.length) continue;
|
|
11809
|
+
const found = /* @__PURE__ */ new Map();
|
|
11810
|
+
for (const diagnostic of diagnostics) {
|
|
11811
|
+
const id = `${diagnostic.ruleId}\0${findingKey(diagnostic)}`;
|
|
11812
|
+
const n = found.get(id) ?? {
|
|
11813
|
+
all: 0,
|
|
11814
|
+
old: 0
|
|
11815
|
+
};
|
|
11816
|
+
n.all += 1;
|
|
11817
|
+
if (!fresh(to, diagnostic)) n.old += 1;
|
|
11818
|
+
found.set(id, n);
|
|
11819
|
+
}
|
|
11820
|
+
const matched = /* @__PURE__ */ new Map();
|
|
11821
|
+
for (const [id, { all, old }] of found) for (const { file, count } of recorded.get(id) ?? []) {
|
|
11822
|
+
const m = matched.get(file) ?? {
|
|
11823
|
+
matched: 0,
|
|
11824
|
+
kept: 0
|
|
11825
|
+
};
|
|
11826
|
+
m.matched += Math.min(all, count);
|
|
11827
|
+
m.kept += Math.min(old, count);
|
|
11828
|
+
matched.set(file, m);
|
|
11829
|
+
}
|
|
11830
|
+
const off = disabled(to);
|
|
11831
|
+
for (const [from, { matched: n, kept }] of matched) {
|
|
11832
|
+
let size = 0;
|
|
11833
|
+
for (const [rule, sources] of entries.get(from) ?? []) {
|
|
11834
|
+
if (off.has(rule)) continue;
|
|
11835
|
+
for (const count of sources.values()) size += count;
|
|
11836
|
+
}
|
|
11837
|
+
const unmatched = diagnostics.length + size - 2 * n;
|
|
11838
|
+
if (n * 2 >= diagnostics.length && n * 2 >= size) pairs.push({
|
|
11839
|
+
to,
|
|
11840
|
+
from,
|
|
11841
|
+
matched: n,
|
|
11842
|
+
kept,
|
|
11843
|
+
unmatched
|
|
11844
|
+
});
|
|
11845
|
+
}
|
|
11846
|
+
}
|
|
11847
|
+
const sameName = (pair) => path.posix.basename(pair.to) === path.posix.basename(pair.from) ? 0 : 1;
|
|
11848
|
+
const sameFolder = (pair) => path.posix.dirname(pair.to) === path.posix.dirname(pair.from) ? 0 : 1;
|
|
11849
|
+
const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
11850
|
+
pairs.sort((a, b) => b.kept - a.kept || b.matched - a.matched || a.unmatched - b.unmatched || sameName(a) - sameName(b) || sameFolder(a) - sameFolder(b) || order(a.to, b.to) || order(a.from, b.from));
|
|
11851
|
+
const moves = /* @__PURE__ */ new Map();
|
|
11852
|
+
const taken = /* @__PURE__ */ new Set();
|
|
11853
|
+
for (const { to, from } of pairs) {
|
|
11854
|
+
if (moves.has(to) || taken.has(from)) continue;
|
|
11855
|
+
moves.set(to, from);
|
|
11856
|
+
taken.add(from);
|
|
11857
|
+
}
|
|
11858
|
+
return moves;
|
|
11859
|
+
}
|
|
11860
|
+
/**
|
|
11023
11861
|
* The baseline after a run over `checked` (file → its findings): those files
|
|
11024
11862
|
* get exactly their current findings, plus their previous entries for
|
|
11025
11863
|
* `disabled` rules; other files keep their entries while they still exist
|
|
11026
|
-
* under `root
|
|
11864
|
+
* under `root` by the same spelling.
|
|
11027
11865
|
*/
|
|
11028
11866
|
function updateBaseline(previous, checked, root, disabled = /* @__PURE__ */ new Set()) {
|
|
11029
11867
|
const disabledIn = typeof disabled === "function" ? disabled : () => disabled;
|
|
11030
11868
|
const next = /* @__PURE__ */ new Map();
|
|
11031
|
-
for (const [file, rules] of previous ?? []) if (!checked.has(file) &&
|
|
11869
|
+
for (const [file, rules] of previous ?? []) if (!checked.has(file) && existsAs(root, file)) next.set(file, rules);
|
|
11032
11870
|
for (const [file, diagnostics] of checked) {
|
|
11033
11871
|
const rules = /* @__PURE__ */ new Map();
|
|
11034
11872
|
const off = disabledIn(file);
|
|
@@ -11063,14 +11901,21 @@ function rulesRun(previous, next, checked, ran, wholeRoot) {
|
|
|
11063
11901
|
* The baseline without the entries a run over `checked` (file → its findings) shows no longer
|
|
11064
11902
|
* occur: a checked file keeps at most as many of each finding as it still has, plus its entries
|
|
11065
11903
|
* for `disabled` rules, and files the run did not check keep theirs while they still exist under
|
|
11066
|
-
* `root
|
|
11904
|
+
* `root` by the same spelling. Nothing is added, so a new finding stays new, unlike with
|
|
11905
|
+
* `updateBaseline`. A file of `moves` (new file → old file, from `findMoves`) takes the old
|
|
11906
|
+
* file's entries as its own.
|
|
11067
11907
|
*/
|
|
11068
|
-
function pruneBaseline(previous, checked, root, disabled) {
|
|
11908
|
+
function pruneBaseline(previous, checked, root, disabled, moves = /* @__PURE__ */ new Map()) {
|
|
11909
|
+
const files = new Map(previous);
|
|
11910
|
+
for (const [to, from] of moves) {
|
|
11911
|
+
const rules = previous.get(from);
|
|
11912
|
+
if (rules && !previous.has(to)) files.set(to, rules);
|
|
11913
|
+
}
|
|
11069
11914
|
const next = /* @__PURE__ */ new Map();
|
|
11070
|
-
for (const [file, rules] of
|
|
11915
|
+
for (const [file, rules] of files) {
|
|
11071
11916
|
const diagnostics = checked.get(file);
|
|
11072
11917
|
if (!diagnostics) {
|
|
11073
|
-
if (
|
|
11918
|
+
if (existsAs(root, file)) next.set(file, rules);
|
|
11074
11919
|
continue;
|
|
11075
11920
|
}
|
|
11076
11921
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -11153,7 +11998,7 @@ function isObject(value) {
|
|
|
11153
11998
|
var name = "onsystem";
|
|
11154
11999
|
//#endregion
|
|
11155
12000
|
//#region src/version.ts
|
|
11156
|
-
const VERSION = "0.
|
|
12001
|
+
const VERSION = "0.9.1";
|
|
11157
12002
|
const NAME = name;
|
|
11158
12003
|
//#endregion
|
|
11159
12004
|
//#region src/lint/sarif.ts
|
|
@@ -11837,6 +12682,89 @@ const WEIGHTS = {
|
|
|
11837
12682
|
props: 1,
|
|
11838
12683
|
examples: .5
|
|
11839
12684
|
};
|
|
12685
|
+
/**
|
|
12686
|
+
* What the components of shadcn/ui's registry are for, by the name its file exports them
|
|
12687
|
+
* under (`Badge` in `badge.tsx`). Its files have no JSDoc, so without these a stock shadcn/ui
|
|
12688
|
+
* app lists every component as undescribed and search finds them by name alone.
|
|
12689
|
+
*/
|
|
12690
|
+
const SHADCN_DESCRIPTIONS = {
|
|
12691
|
+
Accordion: "Stacked sections whose headings expand and collapse their content.",
|
|
12692
|
+
Alert: "A callout that draws attention to a message, with an optional title and icon.",
|
|
12693
|
+
AlertDialog: "A modal dialog that asks the user to confirm an action, such as a deletion, before it happens.",
|
|
12694
|
+
AspectRatio: "Keeps its content, such as an image or a video, at a fixed width-to-height ratio.",
|
|
12695
|
+
Avatar: "A user's picture, with a fallback such as initials while it loads or when it is missing.",
|
|
12696
|
+
Badge: "A small label for a status, a count or a category.",
|
|
12697
|
+
Breadcrumb: "The path to the current page, as a trail of links.",
|
|
12698
|
+
Button: "Triggers an action, such as submitting a form or opening a dialog.",
|
|
12699
|
+
ButtonGroup: "Joins related buttons into one bar.",
|
|
12700
|
+
Calendar: "A month grid to pick a date or a range of dates.",
|
|
12701
|
+
Card: "A bordered surface that groups related content, with a header, content and footer.",
|
|
12702
|
+
Carousel: "Shows a row of slides one at a time, with previous and next buttons.",
|
|
12703
|
+
Checkbox: "A box the user ticks to turn an option on or off.",
|
|
12704
|
+
Collapsible: "Shows and hides a panel of content from a trigger.",
|
|
12705
|
+
Combobox: "A text input with a list of options that narrows as the user types.",
|
|
12706
|
+
Command: "A searchable list of commands or options, as in a command palette.",
|
|
12707
|
+
ContextMenu: "A menu of actions that opens on right-click.",
|
|
12708
|
+
Dialog: "A modal window over the page, for a task or a message that needs the user first.",
|
|
12709
|
+
Drawer: "A panel that slides in from an edge of the screen, often from the bottom on phones.",
|
|
12710
|
+
DropdownMenu: "A menu of actions or options that opens from a button.",
|
|
12711
|
+
Empty: "An empty state: what to show when there is nothing yet, with an action to start.",
|
|
12712
|
+
Field: "A form field: its label, control, description and error message together.",
|
|
12713
|
+
HoverCard: "A card of preview content that opens while the pointer rests on a link.",
|
|
12714
|
+
Input: "A single-line text field.",
|
|
12715
|
+
InputGroup: "An input with icons, text or buttons inside its border.",
|
|
12716
|
+
InputOTP: "A row of one-character slots for a one-time code.",
|
|
12717
|
+
Item: "A row of content for lists: media, a title, a description and actions.",
|
|
12718
|
+
Kbd: "Shows a keyboard key or shortcut.",
|
|
12719
|
+
Label: "The text label of a form control.",
|
|
12720
|
+
Menubar: "A horizontal bar of menus, as in a desktop app.",
|
|
12721
|
+
NativeSelect: "A styled native <select> element.",
|
|
12722
|
+
NavigationMenu: "A site's main navigation links, with panels that open from them.",
|
|
12723
|
+
Pagination: "Links to move between pages of results, with previous and next.",
|
|
12724
|
+
Popover: "Rich content in a floating panel that opens from a button.",
|
|
12725
|
+
Progress: "A bar that shows how far a task has got.",
|
|
12726
|
+
RadioGroup: "A set of options of which the user picks exactly one.",
|
|
12727
|
+
ScrollArea: "A scrollable region with styled scrollbars.",
|
|
12728
|
+
Select: "Lets the user pick one value from a list that opens from a button.",
|
|
12729
|
+
Separator: "A thin line between content, horizontal or vertical.",
|
|
12730
|
+
Sheet: "A panel that slides in over the page from an edge of the screen, for secondary content.",
|
|
12731
|
+
Sidebar: "The app's side navigation: a collapsible panel of grouped menu items.",
|
|
12732
|
+
Skeleton: "A placeholder shape shown while content loads.",
|
|
12733
|
+
Slider: "Picks a number, or a range, by dragging along a track.",
|
|
12734
|
+
Spinner: "An animated indicator that something is loading.",
|
|
12735
|
+
Switch: "A toggle that turns a setting on or off at once.",
|
|
12736
|
+
Table: "Rows and columns of data.",
|
|
12737
|
+
Tabs: "Switches between panels of content, one visible at a time.",
|
|
12738
|
+
Textarea: "A multi-line text field.",
|
|
12739
|
+
Toggle: "A button that stays pressed or not, such as Bold in a toolbar.",
|
|
12740
|
+
ToggleGroup: "A set of toggle buttons, of which one or several can be on.",
|
|
12741
|
+
Tooltip: "A short hint that appears while the pointer rests on, or focus is in, an element."
|
|
12742
|
+
};
|
|
12743
|
+
/**
|
|
12744
|
+
* What shadcn/ui's component of this name is for, when `component` is laid out as `npx shadcn
|
|
12745
|
+
* add` writes it: exported under the registry's name from the file named for it, in a `ui`
|
|
12746
|
+
* folder (`Badge` from `components/ui/badge.tsx`, components.json's default `ui` alias). A
|
|
12747
|
+
* component of the same name elsewhere (`Sheet` in a design system's `src/sheet.tsx`) is not
|
|
12748
|
+
* shadcn/ui's, and may be something else entirely.
|
|
12749
|
+
*/
|
|
12750
|
+
function shadcnPurpose(component) {
|
|
12751
|
+
if (component.library || component.parent) return void 0;
|
|
12752
|
+
if (!Object.hasOwn(SHADCN_DESCRIPTIONS, component.name)) return void 0;
|
|
12753
|
+
const [folder, file = ""] = component.source.file.split(/[\\/]/).slice(-2);
|
|
12754
|
+
if (folder !== "ui") return void 0;
|
|
12755
|
+
const kebab = splitWords(component.name).join("-").toLowerCase();
|
|
12756
|
+
return file.replace(/\.[jt]sx?$/, "") === kebab ? SHADCN_DESCRIPTIONS[component.name] : void 0;
|
|
12757
|
+
}
|
|
12758
|
+
/**
|
|
12759
|
+
* The component's own description or, for a shadcn/ui component without one (`Badge` in
|
|
12760
|
+
* `components/ui/badge.tsx`, no JSDoc), what shadcn/ui's component of that name is for, said
|
|
12761
|
+
* as shadcn/ui's: `shadcn/ui's Badge: A small label for a status, a count or a category.`
|
|
12762
|
+
*/
|
|
12763
|
+
function descriptionOf(component) {
|
|
12764
|
+
if (component.description) return component.description;
|
|
12765
|
+
const purpose = shadcnPurpose(component);
|
|
12766
|
+
return purpose && `shadcn/ui's ${component.name}: ${purpose}`;
|
|
12767
|
+
}
|
|
11840
12768
|
function buildSearchIndex(components) {
|
|
11841
12769
|
const index = new Bm25Index();
|
|
11842
12770
|
for (const component of components) index.add(component, [
|
|
@@ -11845,7 +12773,7 @@ function buildSearchIndex(components) {
|
|
|
11845
12773
|
...component.aliases,
|
|
11846
12774
|
component.parent ?? ""
|
|
11847
12775
|
].join(" ")), WEIGHTS.name],
|
|
11848
|
-
[tokenize(component.description ?? ""), WEIGHTS.description],
|
|
12776
|
+
[tokenize(component.description ?? shadcnPurpose(component) ?? ""), WEIGHTS.description],
|
|
11849
12777
|
[tokenize(component.variants.flatMap((v) => [v.name, ...v.values]).join(" ")), WEIGHTS.variants],
|
|
11850
12778
|
[tokenize((component.docs?.sections ?? []).map((s) => `${s.heading} ${s.body}`).join(" ")), WEIGHTS.docs],
|
|
11851
12779
|
[tokenize(component.props.map((p) => `${p.name} ${p.description ?? ""}`).join(" ")), WEIGHTS.props],
|
|
@@ -11853,61 +12781,49 @@ function buildSearchIndex(components) {
|
|
|
11853
12781
|
]);
|
|
11854
12782
|
return index;
|
|
11855
12783
|
}
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
const MAX_IMPORTED = 50;
|
|
11867
|
-
/**
|
|
11868
|
-
* The specifiers a stylesheet `@import`s: `@import "./theme.css" layer(base)`,
|
|
11869
|
-
* `@import url(theme.css)`, `@import "@acme/theme"`. Empty for CSS that does not parse.
|
|
11870
|
-
*/
|
|
11871
|
-
function cssImports(css) {
|
|
11872
|
-
let root;
|
|
11873
|
-
try {
|
|
11874
|
-
root = postcss.parse(css);
|
|
11875
|
-
} catch {
|
|
11876
|
-
return [];
|
|
11877
|
-
}
|
|
11878
|
-
const out = [];
|
|
11879
|
-
for (const node of root.nodes) {
|
|
11880
|
-
if (node.type !== "atrule" || node.name !== "import") continue;
|
|
11881
|
-
const match = /^\s*(?:url\(\s*)?(?:"([^"]+)"|'([^']+)'|([^\s"')]+)\s*\))/.exec(node.params);
|
|
11882
|
-
const specifier = match?.[1] ?? match?.[2] ?? match?.[3];
|
|
11883
|
-
if (specifier) out.push(specifier);
|
|
12784
|
+
/** The query's words, each with the terms that find it: `modal` is found by modal and dialog. */
|
|
12785
|
+
function queryWords(query) {
|
|
12786
|
+
const words = /* @__PURE__ */ new Map();
|
|
12787
|
+
for (const word of splitWords(query)) {
|
|
12788
|
+
const stems = tokenize(word);
|
|
12789
|
+
const key = stems.join(" ");
|
|
12790
|
+
if (stems.length && !words.has(key)) words.set(key, {
|
|
12791
|
+
stems,
|
|
12792
|
+
terms: [...expandQuery(word).keys()]
|
|
12793
|
+
});
|
|
11884
12794
|
}
|
|
11885
|
-
return
|
|
12795
|
+
return [...words.values()];
|
|
11886
12796
|
}
|
|
11887
12797
|
/**
|
|
11888
|
-
*
|
|
11889
|
-
*
|
|
12798
|
+
* Components for `query`, best first. A hit matches clearly when a word of the query is in
|
|
12799
|
+
* its name (`button` for Button) or when it has more than half of the query's words, each
|
|
12800
|
+
* itself or through a synonym (`modal` through `dialog`). The hits that do not are left out
|
|
12801
|
+
* when any does, and otherwise come back marked `weak`: "a dropdown menu to pick a country",
|
|
12802
|
+
* in a design system with no select, finds only a card's action slot that mentions a menu,
|
|
12803
|
+
* which is not an answer to give as the best match.
|
|
11890
12804
|
*/
|
|
11891
|
-
function
|
|
11892
|
-
const
|
|
11893
|
-
const
|
|
11894
|
-
|
|
11895
|
-
|
|
11896
|
-
|
|
11897
|
-
|
|
11898
|
-
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
|
|
11906
|
-
|
|
11907
|
-
|
|
11908
|
-
|
|
11909
|
-
|
|
11910
|
-
|
|
12805
|
+
function searchComponents(index, query, limit = 5) {
|
|
12806
|
+
const words = queryWords(query);
|
|
12807
|
+
const hits = index.search(expandQuery(query), index.size).map((hit) => {
|
|
12808
|
+
const matched = new Set(hit.terms);
|
|
12809
|
+
const covered = words.filter((word) => word.terms.some((term) => matched.has(term))).length;
|
|
12810
|
+
const names = new Set(tokenize([hit.item.name, ...hit.item.aliases].join(" ")));
|
|
12811
|
+
const clear = covered * 2 > words.length || words.some((word) => word.stems.some((s) => names.has(s)));
|
|
12812
|
+
return {
|
|
12813
|
+
found: {
|
|
12814
|
+
component: hit.item,
|
|
12815
|
+
score: Math.round(hit.score * 100) / 100,
|
|
12816
|
+
matched: hit.terms
|
|
12817
|
+
},
|
|
12818
|
+
clear
|
|
12819
|
+
};
|
|
12820
|
+
});
|
|
12821
|
+
const clear = hits.filter((hit) => hit.clear);
|
|
12822
|
+
if (clear.length) return clear.slice(0, limit).map((hit) => hit.found);
|
|
12823
|
+
return hits.slice(0, limit).map((hit) => ({
|
|
12824
|
+
...hit.found,
|
|
12825
|
+
weak: true
|
|
12826
|
+
}));
|
|
11911
12827
|
}
|
|
11912
12828
|
//#endregion
|
|
11913
12829
|
//#region src/util/log.ts
|
|
@@ -12255,7 +13171,7 @@ async function resolveFiles(config) {
|
|
|
12255
13171
|
* `@dub/tailwind-config` and its `themes.css`), with what they import. May repeat a file.
|
|
12256
13172
|
*/
|
|
12257
13173
|
function linkedStylesheets(config, found) {
|
|
12258
|
-
const resolve = stylesheetResolver(config.root);
|
|
13174
|
+
const resolve = stylesheetResolver(config.root, { importPath: config.importPath });
|
|
12259
13175
|
const preset = config.tailwindConfig ? presetStylesheets(readTailwindColors(config.tailwindConfig, presetResolver(config.root)), config.root) : [];
|
|
12260
13176
|
return [
|
|
12261
13177
|
...importedStylesheets(found.filter((f) => f.endsWith(".css")), resolve),
|
|
@@ -12595,4 +13511,4 @@ var DesignSystemHost = class {
|
|
|
12595
13511
|
}
|
|
12596
13512
|
};
|
|
12597
13513
|
//#endregion
|
|
12598
|
-
export {
|
|
13514
|
+
export { LEGACY_CONFIG_FILES as $, unrecordedRules as A, relativePath as At, statusKinds as B, countBaseline as C, closest as Ct, pruneBaseline as D, SCRIPT_FILE$2 as Dt, onLines as E, unique as Et, loadTokens as F, parseDoc as G, parseDesignMd as H, mergeTokens as I, ConfigError as J, extractComponents as K, toDesignMd as L, writeBaseline as M, defaultProjectConfig as Mt, RULES as N, readProjectConfig as Nt, readBaseline as O, isInside as Ot, TokenIndex as P, DEFAULT_TEST_FOLDERS as Q, toDtcg as R, applyBaseline as S, workspacePatterns as St, goneFiles as T, truncate as Tt, parseCssTokens as U, parseDtcgTokens as V, attachDocs as W, DEFAULT_SEVERITY as X, DEFAULT_DOCS as Y, DEFAULT_TEST_EXCLUDE as Z, formatDiagnostics as _, detectProject as _t, fingerprint as a, loadConfig as at, BASELINE_FILE as b, isWorkspaceRoot as bt, silentLogger as c, formatPx as ct, descriptionOf as d, LIBRARY_IDS as dt, RULE_IDS as et, searchComponents as f, libraryTitle as ft, escapeData as g, declaredConfig as gt, uncheckedNotice as h, PackageFinder as ht, componentFiles as i, hasOwnConfig as it, updateBaseline as j, toPosix as jt, rulesRun as k, matchesGlob as kt, stderrLogger as l, findVariantDefinitions as lt, checkSource as m, PACKAGE_FIELD as mt, DesignSystemHost as n, defineConfig as nt, loadDesignSystem as o, loadConfigWith as ot, applyFixes as p, notReadYet as pt, CONFIG_FILES as q, buildModel as r, excludedBy as rt, tokenFiles as s, rulesFor as st, DesignSystem as t, configJsonSchema as tt, buildSearchIndex as u, LIBRARIES as ut, NAME as v, findDesignSystemPackage as vt, findMoves as w, plural$1 as wt, LEGACY_BASELINE_FILE as x, workspacePackageDirs as xt, VERSION as y, isDesignSystemName as yt, TOKEN_CATEGORIES as z };
|