onsystem 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/dist/cli.js +119 -20
- package/dist/{config-kYGiD5oW.d.ts → config-B98w8bcg.d.ts} +5 -0
- package/dist/{design-system-Dmcv7eHR.js → design-system-Bykei5W0.js} +757 -113
- 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-B7u6dqLd.js} +324 -86
- package/dist/{workspace-CM1RyqZA.js → workspace-Dp1q8qZ8.js} +1 -1
- package/package.json +6 -4
|
@@ -660,6 +660,15 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
660
660
|
const names = unique([...declared ? [declared.name] : [], ...dependencies.filter(isDesignSystemName)]);
|
|
661
661
|
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
662
|
let imported;
|
|
663
|
+
let declarations;
|
|
664
|
+
/** What the trace adds for an installed package whose sources are not read. */
|
|
665
|
+
const declarationsNote = (name, dir, pkg) => {
|
|
666
|
+
declarations ??= new Set(declarationsPackages(root, packages).map((p) => p.name));
|
|
667
|
+
if (declarations.has(name)) return "; its components are read from its type declarations instead";
|
|
668
|
+
if (!declaresTypes(dir, pkg)) return "";
|
|
669
|
+
const scope = scopeOf$1(name);
|
|
670
|
+
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}"`;
|
|
671
|
+
};
|
|
663
672
|
for (const name of names) {
|
|
664
673
|
const own = name === declared?.name ? declared : void 0;
|
|
665
674
|
const workspaceDir = own ? void 0 : packages.find(name);
|
|
@@ -687,7 +696,10 @@ function detectProject(root, tsconfig, trace, declared, packages = new PackageFi
|
|
|
687
696
|
prefixes: [name]
|
|
688
697
|
});
|
|
689
698
|
step(candidate, true, `${relativePath(root, dir)}, ${countFiles(root, result)}`);
|
|
690
|
-
} else
|
|
699
|
+
} else {
|
|
700
|
+
const reason = result ? "its components match no file" : unique(reasons).join("; ") || "nothing to read";
|
|
701
|
+
step(candidate, false, kind === "installed" ? `${reason}${declarationsNote(name, dir, pkg)}` : reason);
|
|
702
|
+
}
|
|
691
703
|
}
|
|
692
704
|
const registries = shadcn && project && fromShadcn && !withDefaults ? fromRegistries(root, shadcn, fromShadcn, found, project, step) : [];
|
|
693
705
|
if (found.length) {
|
|
@@ -1342,15 +1354,21 @@ function presetResolver(root) {
|
|
|
1342
1354
|
* (`./theme.css`, or `theme.css` as plain CSS reads it), or a stylesheet of a workspace
|
|
1343
1355
|
* package (`@acme/theme/colors.css`, or `@acme/theme` through its `exports` or its `style`
|
|
1344
1356
|
* field). An installed package resolves only when zero config reads it as a design system
|
|
1345
|
-
* ({@link installedDesignSystem})
|
|
1346
|
-
*
|
|
1347
|
-
*
|
|
1357
|
+
* ({@link installedDesignSystem}) or reads its type declarations ({@link declarationsPackages}:
|
|
1358
|
+
* by its config, its scope or the config's `importPath`), and a stylesheet in it imports others
|
|
1359
|
+
* of that package alone. In an installed package, a stylesheet in a build folder
|
|
1360
|
+
* (`dist/index.css`) is compiled, and resolves to nothing. Other installed packages
|
|
1361
|
+
* (`tailwindcss`) and URLs resolve to nothing.
|
|
1348
1362
|
*/
|
|
1349
|
-
function stylesheetResolver(root) {
|
|
1363
|
+
function stylesheetResolver(root, { importPath } = {}) {
|
|
1350
1364
|
const packages = new PackageFinder(root);
|
|
1351
1365
|
const designSystems = /* @__PURE__ */ new Map();
|
|
1366
|
+
let declarations;
|
|
1352
1367
|
const designSystem = (name) => {
|
|
1353
|
-
if (!designSystems.has(name))
|
|
1368
|
+
if (!designSystems.has(name)) {
|
|
1369
|
+
declarations ??= new Set(declarationsPackages(root, packages, { importPath }).map((p) => p.name));
|
|
1370
|
+
designSystems.set(name, installedDesignSystem(root, name, packages) ?? (declarations.has(name) ? packages.installed(name) : void 0));
|
|
1371
|
+
}
|
|
1354
1372
|
return designSystems.get(name);
|
|
1355
1373
|
};
|
|
1356
1374
|
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 +1584,87 @@ function packageImports(root, dir) {
|
|
|
1566
1584
|
return pkg ? fromPackage(root, dir, pkg)?.imports ?? [] : [];
|
|
1567
1585
|
}
|
|
1568
1586
|
/**
|
|
1587
|
+
* The dependencies of `root` (in `dependencies` or `devDependencies`) whose components are read
|
|
1588
|
+
* from their type declarations: design-system packages installed from a registry that name
|
|
1589
|
+
* `.d.ts` files, and whose component sources the root does not read (`options.components`
|
|
1590
|
+
* matches no file in them), as when they publish only compiled code. A design-system package
|
|
1591
|
+
* is one that declares its onsystem config (set up with `init`); one named like a design system
|
|
1592
|
+
* in the root's own npm scope (`@acme/ui` for `@acme/web`); or the dependency the root's config
|
|
1593
|
+
* names as its `importPath`, whatever its name or scope (`@dub/ui` in an unscoped app). A kit
|
|
1594
|
+
* from another scope named like one (`@sanity/ui`, `@react-email/components`) is not the app's
|
|
1595
|
+
* design system unless the config says so, and is not read. Workspace packages are read from
|
|
1596
|
+
* their sources. None at a workspace root, whose projects each find their own.
|
|
1597
|
+
*/
|
|
1598
|
+
function declarationsPackages(root, packages = new PackageFinder(root), options = {}) {
|
|
1599
|
+
const own = readJson$1(path.join(root, "package.json"));
|
|
1600
|
+
if (!own || isWorkspaceRoot(root)) return [];
|
|
1601
|
+
const scope = scopeOf$1(own.name);
|
|
1602
|
+
const found = [];
|
|
1603
|
+
let sources;
|
|
1604
|
+
for (const name of Object.keys({
|
|
1605
|
+
...asRecord(own.dependencies),
|
|
1606
|
+
...asRecord(own.devDependencies)
|
|
1607
|
+
})) {
|
|
1608
|
+
const located = packages.locate(name);
|
|
1609
|
+
if (!located) continue;
|
|
1610
|
+
const declaration = located.json["onsystem"] === void 0 ? void 0 : declaredConfig(packages.place(located.dir).dir, located.json);
|
|
1611
|
+
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;
|
|
1612
|
+
if (!by) continue;
|
|
1613
|
+
const installed = packages.installed(name);
|
|
1614
|
+
const pkg = installed ? cachedManifest(path.join(installed, "package.json")) : void 0;
|
|
1615
|
+
if (!installed || !pkg || !declaresTypes(installed, pkg)) continue;
|
|
1616
|
+
sources ??= componentSources(root, options.components ?? []);
|
|
1617
|
+
if (sources.some((file) => isInside(realPath(installed), file))) continue;
|
|
1618
|
+
const version = typeof pkg.version === "string" ? pkg.version : void 0;
|
|
1619
|
+
found.push({
|
|
1620
|
+
name,
|
|
1621
|
+
dir: path.resolve(located.dir),
|
|
1622
|
+
version,
|
|
1623
|
+
by
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
return found;
|
|
1627
|
+
}
|
|
1628
|
+
/** `@acme` for `@acme/web`; undefined for an unscoped name. */
|
|
1629
|
+
function scopeOf$1(name) {
|
|
1630
|
+
return typeof name === "string" && /^@[^/]+\/./.test(name) ? name.split("/")[0] : void 0;
|
|
1631
|
+
}
|
|
1632
|
+
/** Whether a package names type declarations: `types`, a `types` export condition, or `index.d.ts`. */
|
|
1633
|
+
function declaresTypes(dir, pkg) {
|
|
1634
|
+
if (typeof pkg.types === "string" || typeof pkg.typings === "string") return true;
|
|
1635
|
+
const hasTypes = (value) => Array.isArray(value) ? value.some(hasTypes) : value !== null && typeof value === "object" && Object.entries(value).some(([key, nested]) => key === "types" || hasTypes(nested));
|
|
1636
|
+
return hasTypes(pkg.exports) || isFile(path.join(dir, "index.d.ts"));
|
|
1637
|
+
}
|
|
1638
|
+
/** Files that hold no component to read: declarations, tests and stories, as `exclude` has them. */
|
|
1639
|
+
const NOT_COMPONENT_SOURCES = [
|
|
1640
|
+
"**/*.d.ts",
|
|
1641
|
+
"**/*.{test,spec,stories}.{ts,tsx,js,jsx}",
|
|
1642
|
+
"**/__{tests,stories,mocks}__/**"
|
|
1643
|
+
];
|
|
1644
|
+
/**
|
|
1645
|
+
* The real paths of the component files in `node_modules` that `patterns` (relative to `root`)
|
|
1646
|
+
* match: the sources of an installed package that the root reads, through a design-system
|
|
1647
|
+
* package's config or zero config. Tests and stories are not components.
|
|
1648
|
+
*/
|
|
1649
|
+
function componentSources(root, patterns) {
|
|
1650
|
+
const named = patterns.filter((p) => p.includes("node_modules"));
|
|
1651
|
+
if (!named.length) return [];
|
|
1652
|
+
return globSync(named, {
|
|
1653
|
+
cwd: root,
|
|
1654
|
+
absolute: true,
|
|
1655
|
+
expandDirectories: false,
|
|
1656
|
+
ignore: NOT_COMPONENT_SOURCES
|
|
1657
|
+
}).map(realPath);
|
|
1658
|
+
}
|
|
1659
|
+
/** A path with its links resolved, or as it is when it cannot be. */
|
|
1660
|
+
function realPath(file) {
|
|
1661
|
+
try {
|
|
1662
|
+
return fs.realpathSync(file);
|
|
1663
|
+
} catch {
|
|
1664
|
+
return path.resolve(file);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1569
1668
|
* Dependencies' package.json files, parsed, by path: read again only when one's size or mtime
|
|
1570
1669
|
* changes, so a config loaded again (the ESLint plugin's and the MCP server's reloads) does not
|
|
1571
1670
|
* read every dependency again.
|
|
@@ -2432,9 +2531,23 @@ const UNREAD_LIBRARIES = [
|
|
|
2432
2531
|
function libraryAdapter(name) {
|
|
2433
2532
|
return LIBRARIES.find((library) => library.package === name);
|
|
2434
2533
|
}
|
|
2435
|
-
/**
|
|
2534
|
+
/** What a design-system package read from its type declarations is called in messages. */
|
|
2535
|
+
const PACKAGE_TITLE = "Design-system package";
|
|
2536
|
+
/** A library's name for messages, read or not: `MUI`, `Chakra UI`, `Design-system package`. */
|
|
2436
2537
|
function libraryTitle(name) {
|
|
2437
|
-
return [...LIBRARIES, ...UNREAD_LIBRARIES].find((l) => l.package === name)?.title ??
|
|
2538
|
+
return [...LIBRARIES, ...UNREAD_LIBRARIES].find((l) => l.package === name)?.title ?? PACKAGE_TITLE;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* The adapter for a design-system package read from its type declarations (`@acme/ui`): its
|
|
2542
|
+
* components and their props, with no theme to read, and the app's augmentations of its modules.
|
|
2543
|
+
*/
|
|
2544
|
+
function packageAdapter(name) {
|
|
2545
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2546
|
+
return {
|
|
2547
|
+
package: name,
|
|
2548
|
+
title: PACKAGE_TITLE,
|
|
2549
|
+
augments: new RegExp(`declare\\s+module\\s+['"]${escaped}(?:/[\\w./-]*)?['"]`)
|
|
2550
|
+
};
|
|
2438
2551
|
}
|
|
2439
2552
|
/** Why a library in `UNREAD_LIBRARIES` is not read: `a dependency onsystem does not read yet (…)`. */
|
|
2440
2553
|
function notReadYet() {
|
|
@@ -2446,8 +2559,15 @@ function notReadYet() {
|
|
|
2446
2559
|
* folder above it, as a monorepo hoists it. Nothing in the package is run. `ids` is the config's
|
|
2447
2560
|
* `libraries`: undefined reads every library onsystem supports, false none, a list those named;
|
|
2448
2561
|
* the dependencies it leaves out are `off`. Those in `UNREAD_LIBRARIES` are `unread`.
|
|
2449
|
-
|
|
2450
|
-
|
|
2562
|
+
*
|
|
2563
|
+
* The design-system packages installed without their component sources that the app depends
|
|
2564
|
+
* on ({@link declarationsPackages}) come after them, read from their type declarations unless
|
|
2565
|
+
* `libraries` is false. A list names libraries, not packages, so it leaves them in. `options`
|
|
2566
|
+
* is what the config says of them: its `importPath`, which makes the dependency it names one,
|
|
2567
|
+
* and the component globs that read a package from its sources instead. A library onsystem
|
|
2568
|
+
* knows is never read as one, whatever `importPath` says.
|
|
2569
|
+
*/
|
|
2570
|
+
function detectLibraries(root, ids, options = {}) {
|
|
2451
2571
|
const pkg = readJson(path.join(root, "package.json"));
|
|
2452
2572
|
if (!pkg) return {
|
|
2453
2573
|
read: [],
|
|
@@ -2476,9 +2596,16 @@ function detectLibraries(root, ids) {
|
|
|
2476
2596
|
source: sourceOf(library.package)
|
|
2477
2597
|
}));
|
|
2478
2598
|
const wanted = (id) => ids === void 0 || ids !== false && ids.includes(id);
|
|
2599
|
+
const known = new Set([...LIBRARIES, ...UNREAD_LIBRARIES].map((library) => library.package));
|
|
2600
|
+
const packages = declarationsPackages(root, void 0, options).filter((p) => !known.has(p.name)).map((p) => ({
|
|
2601
|
+
package: p.name,
|
|
2602
|
+
dir: p.dir,
|
|
2603
|
+
version: p.version,
|
|
2604
|
+
designSystem: true
|
|
2605
|
+
}));
|
|
2479
2606
|
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),
|
|
2607
|
+
read: [...found.filter((f) => wanted(f.id)).map((f) => f.source), ...ids === false ? [] : packages],
|
|
2608
|
+
off: [...found.filter((f) => !wanted(f.id)).map((f) => f.source), ...ids === false ? packages : []],
|
|
2482
2609
|
unread: UNREAD_LIBRARIES.filter((library) => dependencies.has(library.package)).map((library) => sourceOf(library.package))
|
|
2483
2610
|
};
|
|
2484
2611
|
}
|
|
@@ -3297,7 +3424,11 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3297
3424
|
const absolute = path.resolve(root, slashGlob(file));
|
|
3298
3425
|
if (!/[*?[\]{}()!]/.test(file) && !fs.existsSync(absolute)) throw new ConfigError(`Token file not found: ${absolute} (from ${explicitTokens?.from})`);
|
|
3299
3426
|
}
|
|
3300
|
-
const
|
|
3427
|
+
const components = (options.components?.length ? options.components : configComponents !== void 0 ? configComponents.map(fromBase) : detection ? [...detection.withDefaults ? DEFAULT_COMPONENTS : [], ...detection.components] : DEFAULT_COMPONENTS).map(slashGlob);
|
|
3428
|
+
const libraries = detectLibraries(root, config.libraries, {
|
|
3429
|
+
importPath: config.importPath,
|
|
3430
|
+
components
|
|
3431
|
+
});
|
|
3301
3432
|
const tokenSources = options.tokens?.length ? options.tokens.map((p) => ({ path: p })) : configTokens !== void 0 ? configTokens.map((t) => typeof t === "string" ? { path: fromBase(t) } : {
|
|
3302
3433
|
...t,
|
|
3303
3434
|
path: fromBase(t.path)
|
|
@@ -3316,7 +3447,7 @@ async function loadConfigWith(options, context = {}) {
|
|
|
3316
3447
|
return {
|
|
3317
3448
|
root,
|
|
3318
3449
|
configFile,
|
|
3319
|
-
components
|
|
3450
|
+
components,
|
|
3320
3451
|
exclude: declared && config.exclude ? [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDE, ...exclude])] : exclude,
|
|
3321
3452
|
...declared && config.exclude ? { designSystemExclude: exclude } : {},
|
|
3322
3453
|
tokens: tokenSources.map((t) => ({
|
|
@@ -4481,9 +4612,9 @@ function dedent(code) {
|
|
|
4481
4612
|
const MAX_LISTED_INHERITED = 40;
|
|
4482
4613
|
/**
|
|
4483
4614
|
* 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
|
|
4615
|
+
* checker: every value its entry exports under a capitalised name whose type takes props and
|
|
4616
|
+
* renders (function and class components, `forwardRef` and MUI's `OverridableComponent`), with
|
|
4617
|
+
* the props of each call signature, their string-literal values (custom ones the app declares by
|
|
4487
4618
|
* module augmentation included), JSDoc, the element it renders, and the module that
|
|
4488
4619
|
* default-exports it. A signature generic over the element to render (MUI's `component`) marks
|
|
4489
4620
|
* that prop as polymorphic instead of adding its props.
|
|
@@ -4505,16 +4636,27 @@ function extractLibrary(options) {
|
|
|
4505
4636
|
return empty(`${adapter.package}: ${dir} could not be read`);
|
|
4506
4637
|
}
|
|
4507
4638
|
const resolve = (specifier) => ts.resolveModuleName(specifier, path.join(root, "__onsystem__.tsx"), options.project.options, ts.sys).resolvedModule?.resolvedFileName;
|
|
4508
|
-
const
|
|
4509
|
-
|
|
4639
|
+
const main = resolve(adapter.package) ?? path.join(real, typesEntry(real));
|
|
4640
|
+
const entries = isDeclarations(main) ? [{
|
|
4641
|
+
specifier: adapter.package,
|
|
4642
|
+
file: main
|
|
4643
|
+
}] : subpathEntries(real, adapter.package, resolve);
|
|
4644
|
+
if (!entries.length) return empty(`${adapter.package}: no type declarations at ${relativePath(root, main)}`);
|
|
4510
4645
|
const program = ts.createProgram({
|
|
4511
|
-
rootNames: [
|
|
4512
|
-
options: options.project.options
|
|
4646
|
+
rootNames: [...entries.map((e) => e.file), ...options.augmentations],
|
|
4647
|
+
options: options.project.options,
|
|
4648
|
+
host: declarationsHost(options.project.options)
|
|
4513
4649
|
});
|
|
4514
4650
|
const checker = program.getTypeChecker();
|
|
4515
|
-
const
|
|
4516
|
-
|
|
4517
|
-
|
|
4651
|
+
const modules = entries.flatMap((e) => {
|
|
4652
|
+
const file = program.getSourceFile(e.file);
|
|
4653
|
+
const symbol = file ? checker.getSymbolAtLocation(file) : void 0;
|
|
4654
|
+
return symbol ? [{
|
|
4655
|
+
...e,
|
|
4656
|
+
symbol
|
|
4657
|
+
}] : [];
|
|
4658
|
+
});
|
|
4659
|
+
if (!modules.length) return empty(`${adapter.package}: ${entries[0]?.file ?? main} has no exports`);
|
|
4518
4660
|
const shown = (file) => {
|
|
4519
4661
|
const absolute = path.resolve(file);
|
|
4520
4662
|
return relativePath(root, isInside(real, absolute) ? path.join(dir, path.relative(real, absolute)) : absolute);
|
|
@@ -4523,38 +4665,45 @@ function extractLibrary(options) {
|
|
|
4523
4665
|
checker,
|
|
4524
4666
|
adapter,
|
|
4525
4667
|
propSets: /* @__PURE__ */ new Map(),
|
|
4526
|
-
scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package
|
|
4668
|
+
scope: adapter.package.startsWith("@") ? `${adapter.package.split("/")[0] ?? ""}/` : adapter.package,
|
|
4669
|
+
icons: /* @__PURE__ */ new Set()
|
|
4527
4670
|
};
|
|
4528
4671
|
const exportNames = /* @__PURE__ */ new Set();
|
|
4529
4672
|
const bySymbol = /* @__PURE__ */ new Map();
|
|
4530
4673
|
const components = [];
|
|
4531
|
-
for (const
|
|
4532
|
-
const
|
|
4533
|
-
const
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4674
|
+
for (const module of modules) {
|
|
4675
|
+
const subpath = module.specifier !== adapter.package;
|
|
4676
|
+
for (const exported of checker.getExportsOfModule(module.symbol)) {
|
|
4677
|
+
const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
|
|
4678
|
+
if (!(symbol.flags & ts.SymbolFlags.Value)) continue;
|
|
4679
|
+
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
4680
|
+
const isDefault = exported.getName() === "default";
|
|
4681
|
+
const name = isDefault && subpath && declaration ? ts.getNameOfDeclaration(declaration)?.getText() ?? "default" : exported.getName();
|
|
4682
|
+
if (isDefault && !/^[A-Z]/.test(name)) continue;
|
|
4683
|
+
exportNames.add(name);
|
|
4684
|
+
if (!/^[A-Z]/.test(name)) continue;
|
|
4685
|
+
const known = bySymbol.get(symbol);
|
|
4686
|
+
if (known) {
|
|
4687
|
+
if (known.name !== name && !known.aliases.includes(name)) known.aliases.push(name);
|
|
4688
|
+
continue;
|
|
4689
|
+
}
|
|
4690
|
+
if (!declaration) continue;
|
|
4691
|
+
const component = buildComponent(context, name, symbol);
|
|
4692
|
+
if (!component) continue;
|
|
4693
|
+
const file = declaration.getSourceFile();
|
|
4694
|
+
const line = file.getLineAndCharacterOfPosition(declaration.getStart(file)).line + 1;
|
|
4695
|
+
component.source = {
|
|
4696
|
+
file: shown(file.fileName),
|
|
4697
|
+
line
|
|
4698
|
+
};
|
|
4699
|
+
if (subpath) component.importPath = module.specifier;
|
|
4700
|
+
const defaultImport = isDefault ? module.specifier : defaultModule(program, checker, resolve, adapter.package, file, symbol);
|
|
4701
|
+
if (defaultImport) component.defaultImport = defaultImport;
|
|
4702
|
+
bySymbol.set(symbol, component);
|
|
4703
|
+
components.push(component);
|
|
4541
4704
|
}
|
|
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
4705
|
}
|
|
4557
|
-
linkSlots(components);
|
|
4706
|
+
linkSlots(components, context.icons);
|
|
4558
4707
|
return {
|
|
4559
4708
|
components,
|
|
4560
4709
|
propSets: Object.fromEntries(context.propSets),
|
|
@@ -4563,6 +4712,29 @@ function extractLibrary(options) {
|
|
|
4563
4712
|
dependencies: unique(projectFiles(program))
|
|
4564
4713
|
};
|
|
4565
4714
|
}
|
|
4715
|
+
/**
|
|
4716
|
+
* A compiler host that resolves what the app's own resolution leaves unresolved in a package's
|
|
4717
|
+
* declarations by path, as Node.js's old resolution does. A bundler emits declarations that
|
|
4718
|
+
* import a dependency's file by its path (`class-variance-authority/dist/types`, for the
|
|
4719
|
+
* `ClassProp` behind every `cva()` variant), which that dependency's `exports` do not name: with
|
|
4720
|
+
* `"moduleResolution": "bundler"` the import is `any`, and a component's variants and props go
|
|
4721
|
+
* with it. The app's own files resolve as they do for tsc.
|
|
4722
|
+
*/
|
|
4723
|
+
function declarationsHost(options) {
|
|
4724
|
+
const host = ts.createCompilerHost(options);
|
|
4725
|
+
const cache = ts.createModuleResolutionCache(host.getCurrentDirectory(), (file) => host.getCanonicalFileName(file), options);
|
|
4726
|
+
const byPath = {
|
|
4727
|
+
...options,
|
|
4728
|
+
moduleResolution: ts.ModuleResolutionKind.Node10
|
|
4729
|
+
};
|
|
4730
|
+
host.resolveModuleNameLiterals = (literals, containingFile, redirected, compilerOptions, file) => literals.map((literal) => {
|
|
4731
|
+
const resolved = ts.resolveModuleName(literal.text, containingFile, compilerOptions, host, cache, redirected, ts.getModeForUsageLocation(file, literal, compilerOptions));
|
|
4732
|
+
if (resolved.resolvedModule || !/[\\/]node_modules[\\/].*\.d\.[cm]?ts$/.test(containingFile)) return resolved;
|
|
4733
|
+
const fallback = ts.resolveModuleName(literal.text, containingFile, byPath, host);
|
|
4734
|
+
return fallback.resolvedModule && /\.d\.[cm]?ts$/.test(fallback.resolvedModule.resolvedFileName) ? fallback : resolved;
|
|
4735
|
+
});
|
|
4736
|
+
return host;
|
|
4737
|
+
}
|
|
4566
4738
|
/** The declaration entry of an installed package: its `types`, else `index.d.ts`. */
|
|
4567
4739
|
function typesEntry(dir) {
|
|
4568
4740
|
try {
|
|
@@ -4572,9 +4744,106 @@ function typesEntry(dir) {
|
|
|
4572
4744
|
} catch {}
|
|
4573
4745
|
return "index.d.ts";
|
|
4574
4746
|
}
|
|
4575
|
-
/**
|
|
4576
|
-
function
|
|
4577
|
-
return
|
|
4747
|
+
/** A type declarations file on disk: `.d.ts`, `.d.mts` or `.d.cts`. */
|
|
4748
|
+
function isDeclarations(file) {
|
|
4749
|
+
return /\.d\.[cm]?ts$/.test(file) && fs.existsSync(file);
|
|
4750
|
+
}
|
|
4751
|
+
/** Packages exporting more subpaths than this are read from the first ones, sorted. */
|
|
4752
|
+
const MAX_SUBPATHS = 500;
|
|
4753
|
+
/**
|
|
4754
|
+
* The declarations of each subpath a package without an entry of its own exports (`./button`,
|
|
4755
|
+
* `./card`, or the files a `./*` pattern maps), with the specifier apps import it by
|
|
4756
|
+
* (`@acme/ui/button`): as the app's own imports resolve it, else through the `types` condition
|
|
4757
|
+
* its `exports` give it. Subpaths that name no declarations (`./theme.css`) are left out.
|
|
4758
|
+
*/
|
|
4759
|
+
function subpathEntries(dir, name, resolve) {
|
|
4760
|
+
let exports;
|
|
4761
|
+
try {
|
|
4762
|
+
exports = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8")).exports;
|
|
4763
|
+
} catch {
|
|
4764
|
+
return [];
|
|
4765
|
+
}
|
|
4766
|
+
if (!exports || typeof exports !== "object" || Array.isArray(exports)) return [];
|
|
4767
|
+
const found = /* @__PURE__ */ new Map();
|
|
4768
|
+
const add = (subpath, target) => {
|
|
4769
|
+
const specifier = `${name}/${subpath}`;
|
|
4770
|
+
const file = resolve(specifier) ?? (target ? path.resolve(dir, target) : void 0);
|
|
4771
|
+
if (file && isInside(dir, path.resolve(file)) && isDeclarations(file)) found.set(specifier, file);
|
|
4772
|
+
};
|
|
4773
|
+
for (const [key, value] of Object.entries(exports)) {
|
|
4774
|
+
if (!key.startsWith("./") || key === "./package.json") continue;
|
|
4775
|
+
const target = typesTarget(value);
|
|
4776
|
+
const star = key.indexOf("*");
|
|
4777
|
+
if (star === -1) {
|
|
4778
|
+
add(key.slice(2), target);
|
|
4779
|
+
continue;
|
|
4780
|
+
}
|
|
4781
|
+
const at = target?.indexOf("*") ?? -1;
|
|
4782
|
+
if (!target || at === -1) continue;
|
|
4783
|
+
const prefix = path.posix.normalize(target.slice(0, at)).replace(/^\.\//, "");
|
|
4784
|
+
const suffix = target.slice(at + 1);
|
|
4785
|
+
const folder = prefix.slice(0, prefix.lastIndexOf("/") + 1);
|
|
4786
|
+
const files = globSync(`${escapePath(folder)}**/*${escapePath(suffix)}`, {
|
|
4787
|
+
cwd: dir,
|
|
4788
|
+
ignore: ["**/node_modules/**"],
|
|
4789
|
+
expandDirectories: false
|
|
4790
|
+
});
|
|
4791
|
+
for (const file of files.sort()) {
|
|
4792
|
+
if (!file.startsWith(prefix) || !file.endsWith(suffix)) continue;
|
|
4793
|
+
const middle = file.slice(prefix.length, file.length - suffix.length);
|
|
4794
|
+
add(`${key.slice(2, star)}${middle}${key.slice(star + 1)}`, path.join(dir, file));
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
return [...found].sort(([a], [b]) => a.localeCompare(b)).slice(0, MAX_SUBPATHS).map(([specifier, file]) => ({
|
|
4798
|
+
specifier,
|
|
4799
|
+
file
|
|
4800
|
+
}));
|
|
4801
|
+
}
|
|
4802
|
+
/** The first `types` target an `exports` value gives, through nested conditions. */
|
|
4803
|
+
function typesTarget(value) {
|
|
4804
|
+
if (Array.isArray(value)) {
|
|
4805
|
+
for (const item of value) {
|
|
4806
|
+
const found = typesTarget(item);
|
|
4807
|
+
if (found) return found;
|
|
4808
|
+
}
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
if (!value || typeof value !== "object") return void 0;
|
|
4812
|
+
for (const [condition, nested] of Object.entries(value)) {
|
|
4813
|
+
if (condition === "types" && typeof nested === "string") return nested;
|
|
4814
|
+
const found = typesTarget(nested);
|
|
4815
|
+
if (found) return found;
|
|
4816
|
+
}
|
|
4817
|
+
}
|
|
4818
|
+
/**
|
|
4819
|
+
* Call and construct signatures that take props and render, across a union
|
|
4820
|
+
* (`React.ComponentType`): a call that returns an element, and a class whose instances have
|
|
4821
|
+
* `render`. A capitalised function that returns anything else (`Format(value): string`) is no
|
|
4822
|
+
* component.
|
|
4823
|
+
*/
|
|
4824
|
+
function propSignatures(checker, type) {
|
|
4825
|
+
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);
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* Whether a call's return type is what a component returns: a React element (`JSX.Element`,
|
|
4829
|
+
* `ReactElement`), a `ReactNode` that can be one, a promise of one (a server component), or
|
|
4830
|
+
* nothing (`null`). `any`, where React's types do not resolve, counts.
|
|
4831
|
+
*/
|
|
4832
|
+
function rendersElement(checker, type) {
|
|
4833
|
+
if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Null)) return true;
|
|
4834
|
+
const members = (type.isUnion() ? type.types : [type]).filter((member) => !(member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)));
|
|
4835
|
+
if (!members.length) return true;
|
|
4836
|
+
return members.some((member) => {
|
|
4837
|
+
if (member.flags & ts.TypeFlags.Any) return true;
|
|
4838
|
+
if (checker.getPropertyOfType(member, "props") !== void 0 && checker.getPropertyOfType(member, "type") !== void 0) return true;
|
|
4839
|
+
const awaited = checker.getPropertyOfType(member, "then") ? checker.getAwaitedType(member) : void 0;
|
|
4840
|
+
return awaited !== void 0 && awaited !== member && rendersElement(checker, awaited);
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
4843
|
+
/** A conditional type the checker cannot resolve yet, or a union or intersection with one. */
|
|
4844
|
+
function hasConditional(type) {
|
|
4845
|
+
if (type.flags & ts.TypeFlags.Conditional) return true;
|
|
4846
|
+
return type.isUnionOrIntersection() && type.types.some((member) => hasConditional(member));
|
|
4578
4847
|
}
|
|
4579
4848
|
/** A React context (`ButtonGroupContext`): callable in React 19's types, but not a component to offer. */
|
|
4580
4849
|
function isContext(checker, type) {
|
|
@@ -4603,7 +4872,7 @@ function buildComponent(context, name, symbol) {
|
|
|
4603
4872
|
const { checker, adapter } = context;
|
|
4604
4873
|
const type = checker.getTypeOfSymbol(symbol);
|
|
4605
4874
|
if (isContext(checker, type)) return void 0;
|
|
4606
|
-
const signatures = propSignatures(type);
|
|
4875
|
+
const signatures = propSignatures(checker, type);
|
|
4607
4876
|
if (!signatures.length) return void 0;
|
|
4608
4877
|
let polymorphic;
|
|
4609
4878
|
const plain = [];
|
|
@@ -4624,6 +4893,7 @@ function buildComponent(context, name, symbol) {
|
|
|
4624
4893
|
openProps = true;
|
|
4625
4894
|
continue;
|
|
4626
4895
|
}
|
|
4896
|
+
if (signature.getTypeParameters()?.length && hasConditional(propsType)) openProps = true;
|
|
4627
4897
|
for (const part of propsType.isUnion() ? propsType.types : [propsType]) {
|
|
4628
4898
|
members++;
|
|
4629
4899
|
if (checker.getIndexInfosOfType(part).length) openProps = true;
|
|
@@ -4650,7 +4920,8 @@ function buildComponent(context, name, symbol) {
|
|
|
4650
4920
|
const byOrigin = /* @__PURE__ */ new Map();
|
|
4651
4921
|
for (const [propName, { origin }] of entries) byOrigin.set(origin, [...byOrigin.get(origin) ?? [], propName]);
|
|
4652
4922
|
const summarised = new Set([...byOrigin].filter(([origin, names]) => !own(origin) && (origin === "react" || names.length > MAX_LISTED_INHERITED)).map(([origin]) => origin));
|
|
4653
|
-
const
|
|
4923
|
+
const rendered = elementText(checker, used.at(-1));
|
|
4924
|
+
const element = renderedElement(rendered);
|
|
4654
4925
|
const props = [];
|
|
4655
4926
|
const variants = [];
|
|
4656
4927
|
for (const [propName, entry] of entries) {
|
|
@@ -4704,6 +4975,7 @@ function buildComponent(context, name, symbol) {
|
|
|
4704
4975
|
};
|
|
4705
4976
|
if (polymorphic) info.polymorphic = polymorphic.name;
|
|
4706
4977
|
if (element) info.element = element;
|
|
4978
|
+
if (rendered && /\bSVG\w*Element\b/.test(rendered)) context.icons.add(info);
|
|
4707
4979
|
const jsdoc = ts.displayPartsToString(symbol.getDocumentationComment(checker)).trim();
|
|
4708
4980
|
const description = jsdoc ? adapter.describe?.(jsdoc, name) ?? jsdoc : void 0;
|
|
4709
4981
|
if (description) info.description = description;
|
|
@@ -4805,15 +5077,23 @@ function literalValues$1(checker, type, depth = 0) {
|
|
|
4805
5077
|
}
|
|
4806
5078
|
return values.length ? unique(values) : void 0;
|
|
4807
5079
|
}
|
|
4808
|
-
/** The native element
|
|
4809
|
-
function renderedElement(
|
|
5080
|
+
/** The native element an {@link elementText} names: `button` for `Ref<HTMLButtonElement>`. */
|
|
5081
|
+
function renderedElement(text) {
|
|
5082
|
+
const element = text ? /\bHTML(\w+?)Element\b/.exec(text)?.[1] : void 0;
|
|
5083
|
+
return element ? ELEMENT_INTERFACES[element] : void 0;
|
|
5084
|
+
}
|
|
5085
|
+
/**
|
|
5086
|
+
* What names the native element behind a signature, as text: the type of the `ref` its props
|
|
5087
|
+
* take, or without one, of the DOM event handler they take (`HTMLAttributes<HTMLSpanElement>`
|
|
5088
|
+
* gives `onFocus: FocusEventHandler<HTMLSpanElement>`), as a function component's props declare
|
|
5089
|
+
* it. An icon's names an SVG element (`FocusEventHandler<SVGSVGElement>`).
|
|
5090
|
+
*/
|
|
5091
|
+
function elementText(checker, signature) {
|
|
4810
5092
|
const param = signature?.getParameters()[0];
|
|
4811
5093
|
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;
|
|
5094
|
+
const props = checker.getTypeOfSymbol(param);
|
|
5095
|
+
const typed = checker.getPropertyOfType(props, "ref") ?? checker.getPropertyOfType(props, "onFocus");
|
|
5096
|
+
return typed ? checker.typeToString(checker.getTypeOfSymbol(typed)) : void 0;
|
|
4817
5097
|
}
|
|
4818
5098
|
/**
|
|
4819
5099
|
* The folder module that default-exports a component (`@mui/material/Button` for
|
|
@@ -4831,16 +5111,36 @@ function defaultModule(program, checker, resolve, name, file, symbol) {
|
|
|
4831
5111
|
}
|
|
4832
5112
|
/**
|
|
4833
5113
|
* Styled slots a library exports next to the component they belong to (`SliderThumb` in
|
|
4834
|
-
* `Slider.d.ts`
|
|
4835
|
-
|
|
4836
|
-
|
|
5114
|
+
* `Slider.d.ts`, `CardHeader` and `CardContent` next to `Card` in a bundle's one `index.d.ts`)
|
|
5115
|
+
* become its parts, so lists show the component and its parts under it. A name that only goes
|
|
5116
|
+
* on in lower case (`Tabs` next to `Tab`) is another component, and so is a lone container
|
|
5117
|
+
* (`CheckboxGroup` next to `Checkbox`), as for source components. Icons are no parts and have
|
|
5118
|
+
* none (`UserCheck` next to `User`). In a file that declares several components of their own,
|
|
5119
|
+
* as a bundle does, a component with one such name next to it is no family
|
|
5120
|
+
* (`ButtonTooltip` next to `Button`): only two or more are its parts.
|
|
5121
|
+
*/
|
|
5122
|
+
function linkSlots(components, icons) {
|
|
4837
5123
|
const byFile = /* @__PURE__ */ new Map();
|
|
4838
|
-
for (const component of components)
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
5124
|
+
for (const component of components) {
|
|
5125
|
+
if (icons.has(component)) continue;
|
|
5126
|
+
byFile.set(component.source.file, [...byFile.get(component.source.file) ?? [], component]);
|
|
5127
|
+
}
|
|
5128
|
+
for (const group of byFile.values()) {
|
|
5129
|
+
const parts = /* @__PURE__ */ new Map();
|
|
5130
|
+
for (const component of group) {
|
|
5131
|
+
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];
|
|
5132
|
+
if (owner) parts.set(owner, [...parts.get(owner) ?? [], component]);
|
|
5133
|
+
}
|
|
5134
|
+
const owned = new Set([...parts.values()].flat());
|
|
5135
|
+
const bundle = group.filter((component) => !owned.has(component)).length > 1;
|
|
5136
|
+
for (const [owner, family] of parts) {
|
|
5137
|
+
if (family.every((part) => /^(?:Group|List)$/.test(part.name.slice(owner.name.length)))) continue;
|
|
5138
|
+
if (bundle && family.length < 2) continue;
|
|
5139
|
+
for (const part of family) {
|
|
5140
|
+
part.parent = owner.name;
|
|
5141
|
+
owner.subcomponents.push(part.name);
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
4844
5144
|
}
|
|
4845
5145
|
}
|
|
4846
5146
|
//#endregion
|
|
@@ -4861,7 +5161,8 @@ const NOT_SOURCES = [
|
|
|
4861
5161
|
/**
|
|
4862
5162
|
* The components and tokens of the libraries `config` found (`config.libraries`): each one's
|
|
4863
5163
|
* 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.
|
|
5164
|
+
* and the tokens of the theme the app creates for it. A design-system package installed without
|
|
5165
|
+
* its sources is read the same way, with no theme.
|
|
4865
5166
|
*/
|
|
4866
5167
|
async function loadLibraries(config) {
|
|
4867
5168
|
const loaded = {
|
|
@@ -4873,7 +5174,7 @@ async function loadLibraries(config) {
|
|
|
4873
5174
|
dependencies: []
|
|
4874
5175
|
};
|
|
4875
5176
|
const libraries = (config.libraries ?? []).flatMap((source) => {
|
|
4876
|
-
const adapter = libraryAdapter(source.package);
|
|
5177
|
+
const adapter = libraryAdapter(source.package) ?? (source.designSystem ? packageAdapter(source.package) : void 0);
|
|
4877
5178
|
return adapter ? [{
|
|
4878
5179
|
source,
|
|
4879
5180
|
adapter
|
|
@@ -6665,24 +6966,33 @@ const CORE_FAMILIES = /* @__PURE__ */ new Set([
|
|
|
6665
6966
|
"overlay",
|
|
6666
6967
|
"link"
|
|
6667
6968
|
]);
|
|
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
|
-
|
|
6969
|
+
/** Name segments of status colors, by the status they show: what happened, rather than how it looks. */
|
|
6970
|
+
const STATUS_KINDS = {
|
|
6971
|
+
destructive: [
|
|
6972
|
+
"destructive",
|
|
6973
|
+
"danger",
|
|
6974
|
+
"error",
|
|
6975
|
+
"critical",
|
|
6976
|
+
"negative",
|
|
6977
|
+
"invalid"
|
|
6978
|
+
],
|
|
6979
|
+
warning: [
|
|
6980
|
+
"warning",
|
|
6981
|
+
"warn",
|
|
6982
|
+
"caution"
|
|
6983
|
+
],
|
|
6984
|
+
success: [
|
|
6985
|
+
"success",
|
|
6986
|
+
"positive",
|
|
6987
|
+
"valid"
|
|
6988
|
+
],
|
|
6989
|
+
info: [
|
|
6990
|
+
"info",
|
|
6991
|
+
"information",
|
|
6992
|
+
"notice"
|
|
6993
|
+
]
|
|
6994
|
+
};
|
|
6995
|
+
const STATUS_WORDS = new Set(Object.values(STATUS_KINDS).flat());
|
|
6686
6996
|
/** The family a color token belongs to: the first segment of its name (`sidebar`, `sh`, `rainbow`). */
|
|
6687
6997
|
function tokenFamily(token) {
|
|
6688
6998
|
return tokenKey(token).split("-")[0] ?? "";
|
|
@@ -6725,6 +7035,19 @@ function scopesIn(families, ...texts) {
|
|
|
6725
7035
|
function isStatusToken(token) {
|
|
6726
7036
|
return roleKey(token).split("-").some((segment) => STATUS_WORDS.has(segment));
|
|
6727
7037
|
}
|
|
7038
|
+
/**
|
|
7039
|
+
* The statuses a theme has a color for: `success` for `--success`, Dub's `content-success` or a
|
|
7040
|
+
* DTCG `color.feedback.positive`, `destructive` for shadcn/ui's `--destructive` or `error`.
|
|
7041
|
+
*/
|
|
7042
|
+
function statusKinds(tokens) {
|
|
7043
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
7044
|
+
for (const token of tokens) {
|
|
7045
|
+
if (token.category !== "color") continue;
|
|
7046
|
+
const segments = roleKey(token).split("-");
|
|
7047
|
+
for (const [kind, words] of Object.entries(STATUS_KINDS)) if (segments.some((segment) => words.includes(segment))) kinds.add(kind);
|
|
7048
|
+
}
|
|
7049
|
+
return kinds;
|
|
7050
|
+
}
|
|
6728
7051
|
/** `primary` for `--color-primary`, `text-muted` for the DTCG token `color.text.muted`. */
|
|
6729
7052
|
function tokenKey(token) {
|
|
6730
7053
|
return (token.tailwind || token.name).toLowerCase().replace(/[.\s_]+/g, "-").replace(/^colou?rs?-/, "");
|
|
@@ -9557,6 +9880,35 @@ function labelledByParent(context, element) {
|
|
|
9557
9880
|
return !ts.isJsxElement(value) && !ts.isJsxSelfClosingElement(value) && !ts.isJsxFragment(value);
|
|
9558
9881
|
}
|
|
9559
9882
|
/**
|
|
9883
|
+
* What an `id` or `htmlFor` holds, to compare the two: a string as itself (`"dark-mode"`, `{"x"}`),
|
|
9884
|
+
* an expression as written (`{id}`, `` {`${id}-switch`} ``) together with the function it is in,
|
|
9885
|
+
* since two components in one file can each have their own `id`. Undefined when it holds nothing.
|
|
9886
|
+
*/
|
|
9887
|
+
function idValue(context, attribute) {
|
|
9888
|
+
const init = attribute?.initializer;
|
|
9889
|
+
if (!init) return void 0;
|
|
9890
|
+
const value = ts.isJsxExpression(init) ? init.expression : init;
|
|
9891
|
+
if (!value) return void 0;
|
|
9892
|
+
if (ts.isStringLiteralLike(value)) return value.text.trim() ? `"${value.text}"` : void 0;
|
|
9893
|
+
const scope = ts.findAncestor(value, ts.isFunctionLike);
|
|
9894
|
+
return `${String(scope?.pos ?? -1)}:${value.getText(context.sourceFile)}`;
|
|
9895
|
+
}
|
|
9896
|
+
/**
|
|
9897
|
+
* Whether a label with content names the button through its `id`: `<label htmlFor={id}>Dark
|
|
9898
|
+
* mode</label>` next to `<button id={id} role="switch">`, or the design system's `<Label>`. A
|
|
9899
|
+
* button is a labelable element, so the label's text is its accessible name.
|
|
9900
|
+
*/
|
|
9901
|
+
function labelledByFor(context, element) {
|
|
9902
|
+
const id = idValue(context, findAttribute(element, "id"));
|
|
9903
|
+
if (id === void 0) return false;
|
|
9904
|
+
return context.analysis.elements.some((label) => {
|
|
9905
|
+
if (label === element) return false;
|
|
9906
|
+
if (idValue(context, findAttribute(label, "htmlFor") ?? findAttribute(label, "for")) !== id) return false;
|
|
9907
|
+
const content = classify(label.children, context, []);
|
|
9908
|
+
return content === "text" || content === "unknown";
|
|
9909
|
+
});
|
|
9910
|
+
}
|
|
9911
|
+
/**
|
|
9560
9912
|
* The element whose `render` prop this one is, as in Base UI's
|
|
9561
9913
|
* `<Dialog.Close render={<Button size="icon" />}><XIcon /></Dialog.Close>`:
|
|
9562
9914
|
* the rendered button takes the host's children and attributes. Also through
|
|
@@ -9662,6 +10014,7 @@ const iconButtonAccessibleName = {
|
|
|
9662
10014
|
if (host && (isButton(context, host) || host.hasSpread)) continue;
|
|
9663
10015
|
if (hasLabel(element.attributes) || host && hasLabel(host.attributes)) continue;
|
|
9664
10016
|
if (labelledByParent(context, element)) continue;
|
|
10017
|
+
if (labelledByFor(context, element) || host && labelledByFor(context, host)) continue;
|
|
9665
10018
|
const icons = [];
|
|
9666
10019
|
const children = host ? [...host.children, ...element.children] : element.children;
|
|
9667
10020
|
const content = host && !children.length ? "unknown" : classify(children, context, icons);
|
|
@@ -9946,8 +10299,63 @@ const noHardcodedColor = {
|
|
|
9946
10299
|
}
|
|
9947
10300
|
}
|
|
9948
10301
|
}
|
|
10302
|
+
reportChartTokens(context, allow);
|
|
9949
10303
|
}
|
|
9950
10304
|
};
|
|
10305
|
+
/** Packages that draw charts: Recharts, Chart.js, Nivo, visx, Victory, Tremor, MUI X Charts, D3. */
|
|
10306
|
+
const CHART_LIBRARY = /chart|^@nivo\/|^@visx\/|^victory|^@tremor\/|^d3(?:-|$)/i;
|
|
10307
|
+
/** Roles of an element that is a drawing: a chart drawn by hand is an image. */
|
|
10308
|
+
const DRAWING_ROLE = /^(?:img|figure|graphics-[a-z]+)$/;
|
|
10309
|
+
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.";
|
|
10310
|
+
/**
|
|
10311
|
+
* Warns on a chart token outside a chart: `text-chart-2` on a green "Active" label in a theme
|
|
10312
|
+
* with no success color. The class is a token, so the color rule would call it clean, but it
|
|
10313
|
+
* says nothing a status or brand color says, and it changes with the chart palette (a gray or a
|
|
10314
|
+
* blue in some shadcn/ui themes). A file that draws a chart is in scope throughout, legend dots
|
|
10315
|
+
* and all: one that imports a chart library, renders a tag such as `<ChartContainer>` or
|
|
10316
|
+
* `<BarChart>`, declares a name such as `chartData`, or is named for charts. So is an element
|
|
10317
|
+
* in a drawing (`<svg>`, `role="img"`), as a chart drawn by hand is. When the design system's
|
|
10318
|
+
* own components use chart tokens outside a chart, they are a palette it shares: nothing is
|
|
10319
|
+
* reported.
|
|
10320
|
+
*/
|
|
10321
|
+
function reportChartTokens(context, allow) {
|
|
10322
|
+
const { tokens } = context.target;
|
|
10323
|
+
if (!tokens.scopedFamilies.has("chart")) return;
|
|
10324
|
+
const chart = /* @__PURE__ */ new Set(["chart"]);
|
|
10325
|
+
if ([...tokens.familyFiles.get("chart") ?? []].some((file) => !scopesIn(chart, file).size)) return;
|
|
10326
|
+
const { elements, imports, declared } = context.analysis;
|
|
10327
|
+
if (scopesIn(chart, ...[
|
|
10328
|
+
context.file,
|
|
10329
|
+
...elements.map((e) => e.tag),
|
|
10330
|
+
...declared
|
|
10331
|
+
]).size) return;
|
|
10332
|
+
if ([...imports.values()].some((binding) => CHART_LIBRARY.test(binding.source))) return;
|
|
10333
|
+
for (const classString of context.analysis.classStrings) {
|
|
10334
|
+
const { element } = classString;
|
|
10335
|
+
if (element && inDrawing(element)) continue;
|
|
10336
|
+
for (const cls of splitClasses(classString.text, classString.start)) {
|
|
10337
|
+
const key = COLOR_UTILITY.exec(parseUtility(cls.value).base)?.[1];
|
|
10338
|
+
if (!key?.startsWith("chart-") || !tokens.colorKeys.has(key)) continue;
|
|
10339
|
+
if (allow.has(cls.value.toLowerCase())) continue;
|
|
10340
|
+
context.report({
|
|
10341
|
+
start: cls.start,
|
|
10342
|
+
end: cls.end,
|
|
10343
|
+
message: `\`${cls.value}\` ${CHART_TOKEN}`,
|
|
10344
|
+
severity: "warning"
|
|
10345
|
+
});
|
|
10346
|
+
}
|
|
10347
|
+
}
|
|
10348
|
+
}
|
|
10349
|
+
/** Whether the element is, or is in, a drawing: an `<svg>`, a `<figure>` or a `role="img"`. */
|
|
10350
|
+
function inDrawing(element) {
|
|
10351
|
+
return ts.findAncestor(element.node, (node) => {
|
|
10352
|
+
if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) return false;
|
|
10353
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
10354
|
+
const tag = opening.tagName.getText(element.node.getSourceFile());
|
|
10355
|
+
if (tag === "svg" || tag === "figure") return true;
|
|
10356
|
+
return opening.attributes.properties.some((attribute) => ts.isJsxAttribute(attribute) && attributeName(attribute) === "role" && literalValues(attribute).some((value) => DRAWING_ROLE.test(value.text)));
|
|
10357
|
+
}) !== void 0;
|
|
10358
|
+
}
|
|
9951
10359
|
function reportLiteralColors(context, literal, element, options) {
|
|
9952
10360
|
const start = literal.getStart(context.sourceFile) + 1;
|
|
9953
10361
|
const text = context.text.slice(start, literal.end - 1);
|
|
@@ -10984,28 +11392,36 @@ function readBaseline(file) {
|
|
|
10984
11392
|
rules: parsed.rules ? new Set(parsed.rules) : void 0
|
|
10985
11393
|
};
|
|
10986
11394
|
}
|
|
11395
|
+
/** Whether `diagnostic` is on one of the `lines`, such as those a pull request changed. */
|
|
11396
|
+
function onLines(lines, diagnostic) {
|
|
11397
|
+
return lines?.some(([start, end]) => start <= diagnostic.endLine && end >= diagnostic.line) ?? false;
|
|
11398
|
+
}
|
|
10987
11399
|
/**
|
|
10988
11400
|
* Removes the findings the baseline accepts for `file`. Within a file and
|
|
10989
11401
|
* rule, each entry accepts that many findings with the same source text, in
|
|
10990
|
-
* order; any beyond the count are new.
|
|
10991
|
-
*
|
|
11402
|
+
* order; any beyond the count are new. With `changed`, the lines a pull request
|
|
11403
|
+
* changed, findings on other lines are accepted first: a copy of a baselined
|
|
11404
|
+
* line inserted above it is the new one, not the line that was there. Entries
|
|
11405
|
+
* of `disabled` rules are not counted as fixed: the rule is off, not satisfied.
|
|
10992
11406
|
*/
|
|
10993
|
-
function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set()) {
|
|
11407
|
+
function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Set(), changed) {
|
|
10994
11408
|
const remaining = /* @__PURE__ */ new Map();
|
|
10995
11409
|
for (const [rule, sources] of baseline.get(file) ?? []) {
|
|
10996
11410
|
if (disabled.has(rule)) continue;
|
|
10997
11411
|
for (const [source, count] of sources) remaining.set(`${rule}\0${source}`, count);
|
|
10998
11412
|
}
|
|
10999
|
-
const
|
|
11000
|
-
|
|
11001
|
-
|
|
11413
|
+
const accepted = /* @__PURE__ */ new Set();
|
|
11414
|
+
for (const later of [false, true]) for (const diagnostic of result.diagnostics) {
|
|
11415
|
+
if (onLines(changed, diagnostic) !== later) continue;
|
|
11002
11416
|
const id = `${diagnostic.ruleId}\0${findingKey(diagnostic)}`;
|
|
11003
11417
|
const left = remaining.get(id) ?? 0;
|
|
11004
11418
|
if (left > 0) {
|
|
11005
11419
|
remaining.set(id, left - 1);
|
|
11006
|
-
|
|
11007
|
-
}
|
|
11420
|
+
accepted.add(diagnostic);
|
|
11421
|
+
}
|
|
11008
11422
|
}
|
|
11423
|
+
const diagnostics = result.diagnostics.filter((d) => !accepted.has(d));
|
|
11424
|
+
const baselined = accepted.size;
|
|
11009
11425
|
let fixed = 0;
|
|
11010
11426
|
for (const count of remaining.values()) fixed += count;
|
|
11011
11427
|
return {
|
|
@@ -11020,15 +11436,116 @@ function applyBaseline(baseline, file, result, disabled = /* @__PURE__ */ new Se
|
|
|
11020
11436
|
};
|
|
11021
11437
|
}
|
|
11022
11438
|
/**
|
|
11439
|
+
* Whether the key `file`, relative to the real `root`, still names a file by that spelling. On a
|
|
11440
|
+
* disk that ignores case (macOS, Windows), `Billing.tsx` still opens after a rename to
|
|
11441
|
+
* `billing.tsx`, but `check` keys the file by its new spelling, so the old key is gone, as on
|
|
11442
|
+
* Linux.
|
|
11443
|
+
*/
|
|
11444
|
+
function existsAs(root, file) {
|
|
11445
|
+
try {
|
|
11446
|
+
return relativePath(root, fs.realpathSync.native(path.resolve(root, file))).normalize("NFC") === file;
|
|
11447
|
+
} catch {
|
|
11448
|
+
return false;
|
|
11449
|
+
}
|
|
11450
|
+
}
|
|
11451
|
+
/**
|
|
11452
|
+
* The files with baseline entries that no longer exist under `root`, deleted or renamed, among
|
|
11453
|
+
* those `include` takes. `checked` are files a run read, which exist.
|
|
11454
|
+
*/
|
|
11455
|
+
function goneFiles(entries, root, checked, include = () => true) {
|
|
11456
|
+
return [...entries.keys()].filter((file) => !checked.has(file) && include(file) && !existsAs(root, file));
|
|
11457
|
+
}
|
|
11458
|
+
/**
|
|
11459
|
+
* Renamed and moved files (`git mv`, a folder reorganised): each file of `unrecorded` (file → its
|
|
11460
|
+
* findings), which has no entries of its own, paired with a file of `gone` whose entries match at
|
|
11461
|
+
* least half of its findings and half of those entries match its findings, as git counts a
|
|
11462
|
+
* rename from 50% similarity. Each file of `gone` pairs with one file at most, so a copy of a
|
|
11463
|
+
* file is new however many copies there are. Entries of rules `disabled` for the new file are
|
|
11464
|
+
* left out, as `applyBaseline` leaves them out. Returns new file → old file.
|
|
11465
|
+
*
|
|
11466
|
+
* Best match first: the file with most matching findings that are not `fresh`, then the one with
|
|
11467
|
+
* most matching findings, then the pair with fewest findings and entries the other does not
|
|
11468
|
+
* match. With `fresh`, the findings on the lines a pull request changed, a file the pull request
|
|
11469
|
+
* did not touch keeps the entries it took over in an earlier move, and a copy the pull request
|
|
11470
|
+
* adds is the new one, as `applyBaseline` spends entries on the other lines first.
|
|
11471
|
+
* Without it, as in a plain `check`, ties go to a file that kept the old one's name (a folder
|
|
11472
|
+
* moved), then to one in the old one's folder (a file renamed), then to path order.
|
|
11473
|
+
*/
|
|
11474
|
+
function findMoves(entries, unrecorded, gone, disabled = () => /* @__PURE__ */ new Set(), fresh = () => false) {
|
|
11475
|
+
const recorded = /* @__PURE__ */ new Map();
|
|
11476
|
+
for (const file of gone) for (const [rule, sources] of entries.get(file) ?? []) for (const [source, count] of sources) {
|
|
11477
|
+
const id = `${rule}\0${source}`;
|
|
11478
|
+
const list = recorded.get(id) ?? [];
|
|
11479
|
+
list.push({
|
|
11480
|
+
file,
|
|
11481
|
+
count
|
|
11482
|
+
});
|
|
11483
|
+
recorded.set(id, list);
|
|
11484
|
+
}
|
|
11485
|
+
const pairs = [];
|
|
11486
|
+
for (const [to, diagnostics] of unrecorded) {
|
|
11487
|
+
if (entries.has(to) || !diagnostics.length) continue;
|
|
11488
|
+
const found = /* @__PURE__ */ new Map();
|
|
11489
|
+
for (const diagnostic of diagnostics) {
|
|
11490
|
+
const id = `${diagnostic.ruleId}\0${findingKey(diagnostic)}`;
|
|
11491
|
+
const n = found.get(id) ?? {
|
|
11492
|
+
all: 0,
|
|
11493
|
+
old: 0
|
|
11494
|
+
};
|
|
11495
|
+
n.all += 1;
|
|
11496
|
+
if (!fresh(to, diagnostic)) n.old += 1;
|
|
11497
|
+
found.set(id, n);
|
|
11498
|
+
}
|
|
11499
|
+
const matched = /* @__PURE__ */ new Map();
|
|
11500
|
+
for (const [id, { all, old }] of found) for (const { file, count } of recorded.get(id) ?? []) {
|
|
11501
|
+
const m = matched.get(file) ?? {
|
|
11502
|
+
matched: 0,
|
|
11503
|
+
kept: 0
|
|
11504
|
+
};
|
|
11505
|
+
m.matched += Math.min(all, count);
|
|
11506
|
+
m.kept += Math.min(old, count);
|
|
11507
|
+
matched.set(file, m);
|
|
11508
|
+
}
|
|
11509
|
+
const off = disabled(to);
|
|
11510
|
+
for (const [from, { matched: n, kept }] of matched) {
|
|
11511
|
+
let size = 0;
|
|
11512
|
+
for (const [rule, sources] of entries.get(from) ?? []) {
|
|
11513
|
+
if (off.has(rule)) continue;
|
|
11514
|
+
for (const count of sources.values()) size += count;
|
|
11515
|
+
}
|
|
11516
|
+
const unmatched = diagnostics.length + size - 2 * n;
|
|
11517
|
+
if (n * 2 >= diagnostics.length && n * 2 >= size) pairs.push({
|
|
11518
|
+
to,
|
|
11519
|
+
from,
|
|
11520
|
+
matched: n,
|
|
11521
|
+
kept,
|
|
11522
|
+
unmatched
|
|
11523
|
+
});
|
|
11524
|
+
}
|
|
11525
|
+
}
|
|
11526
|
+
const sameName = (pair) => path.posix.basename(pair.to) === path.posix.basename(pair.from) ? 0 : 1;
|
|
11527
|
+
const sameFolder = (pair) => path.posix.dirname(pair.to) === path.posix.dirname(pair.from) ? 0 : 1;
|
|
11528
|
+
const order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
11529
|
+
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));
|
|
11530
|
+
const moves = /* @__PURE__ */ new Map();
|
|
11531
|
+
const taken = /* @__PURE__ */ new Set();
|
|
11532
|
+
for (const { to, from } of pairs) {
|
|
11533
|
+
if (moves.has(to) || taken.has(from)) continue;
|
|
11534
|
+
moves.set(to, from);
|
|
11535
|
+
taken.add(from);
|
|
11536
|
+
}
|
|
11537
|
+
return moves;
|
|
11538
|
+
}
|
|
11539
|
+
/**
|
|
11023
11540
|
* The baseline after a run over `checked` (file → its findings): those files
|
|
11024
11541
|
* get exactly their current findings, plus their previous entries for
|
|
11025
11542
|
* `disabled` rules; other files keep their entries while they still exist
|
|
11026
|
-
* under `root
|
|
11543
|
+
* under `root` by the same spelling.
|
|
11027
11544
|
*/
|
|
11028
11545
|
function updateBaseline(previous, checked, root, disabled = /* @__PURE__ */ new Set()) {
|
|
11029
11546
|
const disabledIn = typeof disabled === "function" ? disabled : () => disabled;
|
|
11030
11547
|
const next = /* @__PURE__ */ new Map();
|
|
11031
|
-
for (const [file, rules] of previous ?? []) if (!checked.has(file) &&
|
|
11548
|
+
for (const [file, rules] of previous ?? []) if (!checked.has(file) && existsAs(root, file)) next.set(file, rules);
|
|
11032
11549
|
for (const [file, diagnostics] of checked) {
|
|
11033
11550
|
const rules = /* @__PURE__ */ new Map();
|
|
11034
11551
|
const off = disabledIn(file);
|
|
@@ -11063,14 +11580,21 @@ function rulesRun(previous, next, checked, ran, wholeRoot) {
|
|
|
11063
11580
|
* The baseline without the entries a run over `checked` (file → its findings) shows no longer
|
|
11064
11581
|
* occur: a checked file keeps at most as many of each finding as it still has, plus its entries
|
|
11065
11582
|
* for `disabled` rules, and files the run did not check keep theirs while they still exist under
|
|
11066
|
-
* `root
|
|
11583
|
+
* `root` by the same spelling. Nothing is added, so a new finding stays new, unlike with
|
|
11584
|
+
* `updateBaseline`. A file of `moves` (new file → old file, from `findMoves`) takes the old
|
|
11585
|
+
* file's entries as its own.
|
|
11067
11586
|
*/
|
|
11068
|
-
function pruneBaseline(previous, checked, root, disabled) {
|
|
11587
|
+
function pruneBaseline(previous, checked, root, disabled, moves = /* @__PURE__ */ new Map()) {
|
|
11588
|
+
const files = new Map(previous);
|
|
11589
|
+
for (const [to, from] of moves) {
|
|
11590
|
+
const rules = previous.get(from);
|
|
11591
|
+
if (rules && !previous.has(to)) files.set(to, rules);
|
|
11592
|
+
}
|
|
11069
11593
|
const next = /* @__PURE__ */ new Map();
|
|
11070
|
-
for (const [file, rules] of
|
|
11594
|
+
for (const [file, rules] of files) {
|
|
11071
11595
|
const diagnostics = checked.get(file);
|
|
11072
11596
|
if (!diagnostics) {
|
|
11073
|
-
if (
|
|
11597
|
+
if (existsAs(root, file)) next.set(file, rules);
|
|
11074
11598
|
continue;
|
|
11075
11599
|
}
|
|
11076
11600
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -11153,7 +11677,7 @@ function isObject(value) {
|
|
|
11153
11677
|
var name = "onsystem";
|
|
11154
11678
|
//#endregion
|
|
11155
11679
|
//#region src/version.ts
|
|
11156
|
-
const VERSION = "0.
|
|
11680
|
+
const VERSION = "0.9.0";
|
|
11157
11681
|
const NAME = name;
|
|
11158
11682
|
//#endregion
|
|
11159
11683
|
//#region src/lint/sarif.ts
|
|
@@ -11837,6 +12361,89 @@ const WEIGHTS = {
|
|
|
11837
12361
|
props: 1,
|
|
11838
12362
|
examples: .5
|
|
11839
12363
|
};
|
|
12364
|
+
/**
|
|
12365
|
+
* What the components of shadcn/ui's registry are for, by the name its file exports them
|
|
12366
|
+
* under (`Badge` in `badge.tsx`). Its files have no JSDoc, so without these a stock shadcn/ui
|
|
12367
|
+
* app lists every component as undescribed and search finds them by name alone.
|
|
12368
|
+
*/
|
|
12369
|
+
const SHADCN_DESCRIPTIONS = {
|
|
12370
|
+
Accordion: "Stacked sections whose headings expand and collapse their content.",
|
|
12371
|
+
Alert: "A callout that draws attention to a message, with an optional title and icon.",
|
|
12372
|
+
AlertDialog: "A modal dialog that asks the user to confirm an action, such as a deletion, before it happens.",
|
|
12373
|
+
AspectRatio: "Keeps its content, such as an image or a video, at a fixed width-to-height ratio.",
|
|
12374
|
+
Avatar: "A user's picture, with a fallback such as initials while it loads or when it is missing.",
|
|
12375
|
+
Badge: "A small label for a status, a count or a category.",
|
|
12376
|
+
Breadcrumb: "The path to the current page, as a trail of links.",
|
|
12377
|
+
Button: "Triggers an action, such as submitting a form or opening a dialog.",
|
|
12378
|
+
ButtonGroup: "Joins related buttons into one bar.",
|
|
12379
|
+
Calendar: "A month grid to pick a date or a range of dates.",
|
|
12380
|
+
Card: "A bordered surface that groups related content, with a header, content and footer.",
|
|
12381
|
+
Carousel: "Shows a row of slides one at a time, with previous and next buttons.",
|
|
12382
|
+
Checkbox: "A box the user ticks to turn an option on or off.",
|
|
12383
|
+
Collapsible: "Shows and hides a panel of content from a trigger.",
|
|
12384
|
+
Combobox: "A text input with a list of options that narrows as the user types.",
|
|
12385
|
+
Command: "A searchable list of commands or options, as in a command palette.",
|
|
12386
|
+
ContextMenu: "A menu of actions that opens on right-click.",
|
|
12387
|
+
Dialog: "A modal window over the page, for a task or a message that needs the user first.",
|
|
12388
|
+
Drawer: "A panel that slides in from an edge of the screen, often from the bottom on phones.",
|
|
12389
|
+
DropdownMenu: "A menu of actions or options that opens from a button.",
|
|
12390
|
+
Empty: "An empty state: what to show when there is nothing yet, with an action to start.",
|
|
12391
|
+
Field: "A form field: its label, control, description and error message together.",
|
|
12392
|
+
HoverCard: "A card of preview content that opens while the pointer rests on a link.",
|
|
12393
|
+
Input: "A single-line text field.",
|
|
12394
|
+
InputGroup: "An input with icons, text or buttons inside its border.",
|
|
12395
|
+
InputOTP: "A row of one-character slots for a one-time code.",
|
|
12396
|
+
Item: "A row of content for lists: media, a title, a description and actions.",
|
|
12397
|
+
Kbd: "Shows a keyboard key or shortcut.",
|
|
12398
|
+
Label: "The text label of a form control.",
|
|
12399
|
+
Menubar: "A horizontal bar of menus, as in a desktop app.",
|
|
12400
|
+
NativeSelect: "A styled native <select> element.",
|
|
12401
|
+
NavigationMenu: "A site's main navigation links, with panels that open from them.",
|
|
12402
|
+
Pagination: "Links to move between pages of results, with previous and next.",
|
|
12403
|
+
Popover: "Rich content in a floating panel that opens from a button.",
|
|
12404
|
+
Progress: "A bar that shows how far a task has got.",
|
|
12405
|
+
RadioGroup: "A set of options of which the user picks exactly one.",
|
|
12406
|
+
ScrollArea: "A scrollable region with styled scrollbars.",
|
|
12407
|
+
Select: "Lets the user pick one value from a list that opens from a button.",
|
|
12408
|
+
Separator: "A thin line between content, horizontal or vertical.",
|
|
12409
|
+
Sheet: "A panel that slides in over the page from an edge of the screen, for secondary content.",
|
|
12410
|
+
Sidebar: "The app's side navigation: a collapsible panel of grouped menu items.",
|
|
12411
|
+
Skeleton: "A placeholder shape shown while content loads.",
|
|
12412
|
+
Slider: "Picks a number, or a range, by dragging along a track.",
|
|
12413
|
+
Spinner: "An animated indicator that something is loading.",
|
|
12414
|
+
Switch: "A toggle that turns a setting on or off at once.",
|
|
12415
|
+
Table: "Rows and columns of data.",
|
|
12416
|
+
Tabs: "Switches between panels of content, one visible at a time.",
|
|
12417
|
+
Textarea: "A multi-line text field.",
|
|
12418
|
+
Toggle: "A button that stays pressed or not, such as Bold in a toolbar.",
|
|
12419
|
+
ToggleGroup: "A set of toggle buttons, of which one or several can be on.",
|
|
12420
|
+
Tooltip: "A short hint that appears while the pointer rests on, or focus is in, an element."
|
|
12421
|
+
};
|
|
12422
|
+
/**
|
|
12423
|
+
* What shadcn/ui's component of this name is for, when `component` is laid out as `npx shadcn
|
|
12424
|
+
* add` writes it: exported under the registry's name from the file named for it, in a `ui`
|
|
12425
|
+
* folder (`Badge` from `components/ui/badge.tsx`, components.json's default `ui` alias). A
|
|
12426
|
+
* component of the same name elsewhere (`Sheet` in a design system's `src/sheet.tsx`) is not
|
|
12427
|
+
* shadcn/ui's, and may be something else entirely.
|
|
12428
|
+
*/
|
|
12429
|
+
function shadcnPurpose(component) {
|
|
12430
|
+
if (component.library || component.parent) return void 0;
|
|
12431
|
+
if (!Object.hasOwn(SHADCN_DESCRIPTIONS, component.name)) return void 0;
|
|
12432
|
+
const [folder, file = ""] = component.source.file.split(/[\\/]/).slice(-2);
|
|
12433
|
+
if (folder !== "ui") return void 0;
|
|
12434
|
+
const kebab = splitWords(component.name).join("-").toLowerCase();
|
|
12435
|
+
return file.replace(/\.[jt]sx?$/, "") === kebab ? SHADCN_DESCRIPTIONS[component.name] : void 0;
|
|
12436
|
+
}
|
|
12437
|
+
/**
|
|
12438
|
+
* The component's own description or, for a shadcn/ui component without one (`Badge` in
|
|
12439
|
+
* `components/ui/badge.tsx`, no JSDoc), what shadcn/ui's component of that name is for, said
|
|
12440
|
+
* as shadcn/ui's: `shadcn/ui's Badge: A small label for a status, a count or a category.`
|
|
12441
|
+
*/
|
|
12442
|
+
function descriptionOf(component) {
|
|
12443
|
+
if (component.description) return component.description;
|
|
12444
|
+
const purpose = shadcnPurpose(component);
|
|
12445
|
+
return purpose && `shadcn/ui's ${component.name}: ${purpose}`;
|
|
12446
|
+
}
|
|
11840
12447
|
function buildSearchIndex(components) {
|
|
11841
12448
|
const index = new Bm25Index();
|
|
11842
12449
|
for (const component of components) index.add(component, [
|
|
@@ -11845,7 +12452,7 @@ function buildSearchIndex(components) {
|
|
|
11845
12452
|
...component.aliases,
|
|
11846
12453
|
component.parent ?? ""
|
|
11847
12454
|
].join(" ")), WEIGHTS.name],
|
|
11848
|
-
[tokenize(component.description ?? ""), WEIGHTS.description],
|
|
12455
|
+
[tokenize(component.description ?? shadcnPurpose(component) ?? ""), WEIGHTS.description],
|
|
11849
12456
|
[tokenize(component.variants.flatMap((v) => [v.name, ...v.values]).join(" ")), WEIGHTS.variants],
|
|
11850
12457
|
[tokenize((component.docs?.sections ?? []).map((s) => `${s.heading} ${s.body}`).join(" ")), WEIGHTS.docs],
|
|
11851
12458
|
[tokenize(component.props.map((p) => `${p.name} ${p.description ?? ""}`).join(" ")), WEIGHTS.props],
|
|
@@ -11853,11 +12460,48 @@ function buildSearchIndex(components) {
|
|
|
11853
12460
|
]);
|
|
11854
12461
|
return index;
|
|
11855
12462
|
}
|
|
12463
|
+
/** The query's words, each with the terms that find it: `modal` is found by modal and dialog. */
|
|
12464
|
+
function queryWords(query) {
|
|
12465
|
+
const words = /* @__PURE__ */ new Map();
|
|
12466
|
+
for (const word of splitWords(query)) {
|
|
12467
|
+
const stems = tokenize(word);
|
|
12468
|
+
const key = stems.join(" ");
|
|
12469
|
+
if (stems.length && !words.has(key)) words.set(key, {
|
|
12470
|
+
stems,
|
|
12471
|
+
terms: [...expandQuery(word).keys()]
|
|
12472
|
+
});
|
|
12473
|
+
}
|
|
12474
|
+
return [...words.values()];
|
|
12475
|
+
}
|
|
12476
|
+
/**
|
|
12477
|
+
* Components for `query`, best first. A hit matches clearly when a word of the query is in
|
|
12478
|
+
* its name (`button` for Button) or when it has more than half of the query's words, each
|
|
12479
|
+
* itself or through a synonym (`modal` through `dialog`). The hits that do not are left out
|
|
12480
|
+
* when any does, and otherwise come back marked `weak`: "a dropdown menu to pick a country",
|
|
12481
|
+
* in a design system with no select, finds only a card's action slot that mentions a menu,
|
|
12482
|
+
* which is not an answer to give as the best match.
|
|
12483
|
+
*/
|
|
11856
12484
|
function searchComponents(index, query, limit = 5) {
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
12485
|
+
const words = queryWords(query);
|
|
12486
|
+
const hits = index.search(expandQuery(query), index.size).map((hit) => {
|
|
12487
|
+
const matched = new Set(hit.terms);
|
|
12488
|
+
const covered = words.filter((word) => word.terms.some((term) => matched.has(term))).length;
|
|
12489
|
+
const names = new Set(tokenize([hit.item.name, ...hit.item.aliases].join(" ")));
|
|
12490
|
+
const clear = covered * 2 > words.length || words.some((word) => word.stems.some((s) => names.has(s)));
|
|
12491
|
+
return {
|
|
12492
|
+
found: {
|
|
12493
|
+
component: hit.item,
|
|
12494
|
+
score: Math.round(hit.score * 100) / 100,
|
|
12495
|
+
matched: hit.terms
|
|
12496
|
+
},
|
|
12497
|
+
clear
|
|
12498
|
+
};
|
|
12499
|
+
});
|
|
12500
|
+
const clear = hits.filter((hit) => hit.clear);
|
|
12501
|
+
if (clear.length) return clear.slice(0, limit).map((hit) => hit.found);
|
|
12502
|
+
return hits.slice(0, limit).map((hit) => ({
|
|
12503
|
+
...hit.found,
|
|
12504
|
+
weak: true
|
|
11861
12505
|
}));
|
|
11862
12506
|
}
|
|
11863
12507
|
//#endregion
|
|
@@ -12255,7 +12899,7 @@ async function resolveFiles(config) {
|
|
|
12255
12899
|
* `@dub/tailwind-config` and its `themes.css`), with what they import. May repeat a file.
|
|
12256
12900
|
*/
|
|
12257
12901
|
function linkedStylesheets(config, found) {
|
|
12258
|
-
const resolve = stylesheetResolver(config.root);
|
|
12902
|
+
const resolve = stylesheetResolver(config.root, { importPath: config.importPath });
|
|
12259
12903
|
const preset = config.tailwindConfig ? presetStylesheets(readTailwindColors(config.tailwindConfig, presetResolver(config.root)), config.root) : [];
|
|
12260
12904
|
return [
|
|
12261
12905
|
...importedStylesheets(found.filter((f) => f.endsWith(".css")), resolve),
|
|
@@ -12595,4 +13239,4 @@ var DesignSystemHost = class {
|
|
|
12595
13239
|
}
|
|
12596
13240
|
};
|
|
12597
13241
|
//#endregion
|
|
12598
|
-
export {
|
|
13242
|
+
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$1 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 };
|