@parity-app/cli 0.0.0 → 0.2.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 +88 -3
- package/dist/bin.mjs +1133 -273
- package/dist/client-Cf66-n3R.mjs +724 -0
- package/dist/index.d.mts +211 -17
- package/dist/index.mjs +1 -1
- package/dist/libraries/aria.lib.par +5 -5
- package/dist/libraries/baseui.lib.par +25 -25
- package/dist/libraries/chakra.lib.par +61 -22
- package/dist/libraries/fluent.lib.par +27 -27
- package/dist/libraries/luma.lib.par +20 -20
- package/dist/libraries/lyra.lib.par +20 -20
- package/dist/libraries/maia.lib.par +20 -20
- package/dist/libraries/mantine.lib.par +14 -14
- package/dist/libraries/mira.lib.par +20 -20
- package/dist/libraries/mui.lib.par +5 -5
- package/dist/libraries/nova.lib.par +20 -20
- package/dist/libraries/radix.lib.par +11 -11
- package/dist/libraries/recharts.lib.par +5445 -0
- package/dist/libraries/rhea.lib.par +20 -20
- package/dist/libraries/sera.lib.par +20 -20
- package/dist/libraries/vega.lib.par +20 -20
- package/dist/{src-C8DCd2a1.mjs → src-BOUQtcWB.mjs} +1 -1
- package/dist/{src-WcAfq6g1.mjs → src-BRgxMRtE.mjs} +3 -2
- package/dist/{src-CSeISXoZ.mjs → src-BZ1p_ip_.mjs} +135 -38
- package/dist/{src-uPmBVSsB.mjs → src-CAm6EBIK.mjs} +1 -1
- package/dist/{src-rlZNC2MU.mjs → src-CcWc-KsX.mjs} +593 -212
- package/package.json +8 -6
- package/skills/SKILL.md +61 -14
- package/skills/document.md +6 -1
- package/skills/execute.md +96 -8
- package/skills/guide/states-and-modes.md +5 -3
- package/LICENSE +0 -21
- package/dist/client-DsTLQ2lT.mjs +0 -286
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "./src-
|
|
1
|
+
import { T as lengthToPixels, a as applyLibraryEdits, c as isSampleElement, f as parseLibraryEdits, l as isSampleRender, m as renderSlotOf, p as parseLibraryManifest, s as isMadeValue, u as libraryManifestExtension, x as collectCustomProperties } from "./src-BZ1p_ip_.mjs";
|
|
2
|
+
import "./src-CAm6EBIK.mjs";
|
|
3
3
|
import { a as publishedVersions, n as packageFile, o as repositoryFiles, r as publishedSource } from "./published-CdXoUgfD.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
-
import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { basename, dirname, extname, join, posix, relative, resolve, sep } from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
12
|
import ts from "typescript";
|
|
13
13
|
import "semver";
|
|
@@ -105,7 +105,7 @@ function componentOptions(compilerOptions) {
|
|
|
105
105
|
}
|
|
106
106
|
/** A file only the checker sees, naming React's `ReactNode` so it can tell what React renders. */
|
|
107
107
|
function reactProbe(root) {
|
|
108
|
-
return slashes(join(root, probeName));
|
|
108
|
+
return slashes$2(join(root, probeName));
|
|
109
109
|
}
|
|
110
110
|
/** A compiler host that also serves `reactProbe(root)`. */
|
|
111
111
|
function withReactProbe(host, root) {
|
|
@@ -113,9 +113,9 @@ function withReactProbe(host, root) {
|
|
|
113
113
|
const getSourceFile = host.getSourceFile.bind(host);
|
|
114
114
|
const fileExists = host.fileExists.bind(host);
|
|
115
115
|
const readFile = host.readFile.bind(host);
|
|
116
|
-
host.getSourceFile = (fileName, language, onError, create) => slashes(fileName) === probe ? ts.createSourceFile(fileName, probeText, language, true) : getSourceFile(fileName, language, onError, create);
|
|
117
|
-
host.fileExists = (fileName) => slashes(fileName) === probe || fileExists(fileName);
|
|
118
|
-
host.readFile = (fileName) => slashes(fileName) === probe ? probeText : readFile(fileName);
|
|
116
|
+
host.getSourceFile = (fileName, language, onError, create) => slashes$2(fileName) === probe ? ts.createSourceFile(fileName, probeText, language, true) : getSourceFile(fileName, language, onError, create);
|
|
117
|
+
host.fileExists = (fileName) => slashes$2(fileName) === probe || fileExists(fileName);
|
|
118
|
+
host.readFile = (fileName) => slashes$2(fileName) === probe ? probeText : readFile(fileName);
|
|
119
119
|
return host;
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
@@ -169,7 +169,7 @@ function findComponents(scan) {
|
|
|
169
169
|
});
|
|
170
170
|
const existing = seen.get(symbol);
|
|
171
171
|
if (existing) {
|
|
172
|
-
const declaredIn = (entry) => slashes(entry) === slashes(declaration.getSourceFile().fileName);
|
|
172
|
+
const declaredIn = (entry) => slashes$2(entry) === slashes$2(declaration.getSourceFile().fileName);
|
|
173
173
|
if (scan.defaultExports && declaredIn(from.entry) !== declaredIn(existing.entry) ? declaredIn(from.entry) : preferred(name, existing.name, namespace)) Object.assign(existing, {
|
|
174
174
|
name,
|
|
175
175
|
...from
|
|
@@ -364,12 +364,12 @@ function defaultExportName(symbol, entry) {
|
|
|
364
364
|
function shorten(text, length = 60) {
|
|
365
365
|
return text.length > length ? `${text.slice(0, length - 1)}…` : text;
|
|
366
366
|
}
|
|
367
|
-
function slashes(path) {
|
|
367
|
+
function slashes$2(path) {
|
|
368
368
|
return path.replaceAll("\\", "/");
|
|
369
369
|
}
|
|
370
370
|
/** A path relative to a folder, with forward slashes. */
|
|
371
371
|
function relativePath(from, to) {
|
|
372
|
-
return slashes(relative(from, to));
|
|
372
|
+
return slashes$2(relative(from, to));
|
|
373
373
|
}
|
|
374
374
|
/**
|
|
375
375
|
* A component's own props: those its package declares, not the HTML attributes and React props it passes on, which
|
|
@@ -413,7 +413,118 @@ function sampleOf$1(checker, type, name) {
|
|
|
413
413
|
function hash(text) {
|
|
414
414
|
return createHash("sha1").update(text).digest("hex").slice(0, 8);
|
|
415
415
|
}
|
|
416
|
-
|
|
416
|
+
// ../code/src/tsconfig.ts
|
|
417
|
+
/** The configs a project's TypeScript setup starts from, at its root, in the order they're looked for. */
|
|
418
|
+
const rootConfigNames = ["tsconfig.json", "jsconfig.json"];
|
|
419
|
+
/**
|
|
420
|
+
* Reads a project's tsconfig (or jsconfig) the way TypeScript does, with its own parser: `extends` chains, project
|
|
421
|
+
* references (a Vite app's `tsconfig.json` is often `"files": []` with references to `tsconfig.app.json` and
|
|
422
|
+
* `tsconfig.node.json`) and every `paths` entry. Parity uses its own TypeScript, so a project on a newer one still reads.
|
|
423
|
+
*/
|
|
424
|
+
function readTsconfig(root) {
|
|
425
|
+
const start = rootConfigNames.map((name) => join(root, name)).find((path) => existsSync(path));
|
|
426
|
+
const configs = [];
|
|
427
|
+
const seen = /* @__PURE__ */ new Set();
|
|
428
|
+
const read = /* @__PURE__ */ new Set();
|
|
429
|
+
const realRoot = realPath(root);
|
|
430
|
+
const real = (path) => existsSync(path) ? realPath(path) : join(realPath(dirname(path)), basename(path));
|
|
431
|
+
const readFile = (path) => read.add(slashes$1(relative(realRoot, real(path))));
|
|
432
|
+
const visit = (path) => {
|
|
433
|
+
const key = slashes$1(path);
|
|
434
|
+
if (seen.has(key)) return;
|
|
435
|
+
seen.add(key);
|
|
436
|
+
readFile(path);
|
|
437
|
+
if (!existsSync(path)) return;
|
|
438
|
+
const parsed = ts.getParsedCommandLineOfConfigFile(path, {}, host);
|
|
439
|
+
if (!parsed) return;
|
|
440
|
+
for (const base of extendedFiles(parsed.options)) readFile(base);
|
|
441
|
+
configs.push({
|
|
442
|
+
path: slashes$1(relative(root, path)),
|
|
443
|
+
files: parsed.fileNames.map(slashes$1),
|
|
444
|
+
options: parsed.options
|
|
445
|
+
});
|
|
446
|
+
for (const reference of parsed.projectReferences ?? []) visit(ts.resolveProjectReferencePath(reference));
|
|
447
|
+
};
|
|
448
|
+
if (start) visit(start);
|
|
449
|
+
const withFiles = configs.filter((config) => config.files.length > 0);
|
|
450
|
+
const app = withFiles.find((config) => config.options.jsx !== void 0) ?? withFiles[0];
|
|
451
|
+
return {
|
|
452
|
+
configs,
|
|
453
|
+
...app ? { app } : {},
|
|
454
|
+
read: [...read]
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** A path with its links and short names resolved, or as it is when it can't be. */
|
|
458
|
+
function realPath(path) {
|
|
459
|
+
try {
|
|
460
|
+
return realpathSync.native(path);
|
|
461
|
+
} catch {
|
|
462
|
+
return path;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* The configs a parsed config's `extends` chain read, absolute. TypeScript keeps them on the config's own source file,
|
|
467
|
+
* which `CompilerOptions` types only by its index signature.
|
|
468
|
+
*/
|
|
469
|
+
function extendedFiles(options) {
|
|
470
|
+
const file = options.configFile;
|
|
471
|
+
if (typeof file !== "object" || file === null || Array.isArray(file) || !("extendedSourceFiles" in file)) return [];
|
|
472
|
+
const extended = file.extendedSourceFiles;
|
|
473
|
+
return Array.isArray(extended) ? extended.filter((path) => typeof path === "string") : [];
|
|
474
|
+
}
|
|
475
|
+
const host = {
|
|
476
|
+
...ts.sys,
|
|
477
|
+
onUnRecoverableConfigFileDiagnostic: () => void 0
|
|
478
|
+
};
|
|
479
|
+
function slashes$1(path) {
|
|
480
|
+
return path.replaceAll("\\", "/");
|
|
481
|
+
}
|
|
482
|
+
// ../code/src/listing.ts
|
|
483
|
+
/** Installed packages, build output, and folders of tests and scratch files. */
|
|
484
|
+
const skippedFolders = /* @__PURE__ */ new Set([
|
|
485
|
+
"node_modules",
|
|
486
|
+
"dist",
|
|
487
|
+
"build",
|
|
488
|
+
"out",
|
|
489
|
+
"coverage",
|
|
490
|
+
"storybook-static",
|
|
491
|
+
"__tests__",
|
|
492
|
+
"__mocks__",
|
|
493
|
+
"e2e",
|
|
494
|
+
"tmp",
|
|
495
|
+
"temp"
|
|
496
|
+
]);
|
|
497
|
+
/** Stories, tests, generated and config files sit beside components and export everything but components. */
|
|
498
|
+
const notComponents = /\.(stories|story|test|spec|gen|generated|config)\.[cm]?[jt]sx?$|\.d\.[cm]?ts$/;
|
|
499
|
+
/**
|
|
500
|
+
* The app's files the parse reads, relative to the root: those its tsconfigs include (references followed), else every
|
|
501
|
+
* file under it. App libraries are generated from the same files, with their own extensions.
|
|
502
|
+
*/
|
|
503
|
+
async function appFiles(root, renderer, tsconfig) {
|
|
504
|
+
const fromConfigs = tsconfig.configs.flatMap((config) => config.files);
|
|
505
|
+
const files = (fromConfigs.length > 0 ? fromConfigs : await walk(root, renderer.componentExtensions)).map((file) => slashes(relative(root, file))).filter((path) => isCandidate(path, renderer.componentExtensions));
|
|
506
|
+
const from = fromConfigs.length > 0 ? tsconfig.app?.path ?? tsconfig.configs[0].path : "the project's files";
|
|
507
|
+
return {
|
|
508
|
+
files: [...new Set(files)].sort(),
|
|
509
|
+
from
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function isCandidate(path, extensions) {
|
|
513
|
+
if (path.startsWith("../") || path.includes(":")) return false;
|
|
514
|
+
if (!extensions.includes(extname(path)) || notComponents.test(path)) return false;
|
|
515
|
+
return !path.split("/").some((part) => skippedFolders.has(part) || part.startsWith("."));
|
|
516
|
+
}
|
|
517
|
+
async function walk(folder, extensions) {
|
|
518
|
+
const entries = await readdir(folder, { withFileTypes: true }).catch(() => []);
|
|
519
|
+
return (await Promise.all(entries.map(async (entry) => {
|
|
520
|
+
const path = join(folder, entry.name);
|
|
521
|
+
if (entry.isDirectory()) return skippedFolders.has(entry.name) || entry.name.startsWith(".") ? [] : walk(path, extensions);
|
|
522
|
+
return extensions.includes(extname(entry.name)) ? [path] : [];
|
|
523
|
+
}))).flat();
|
|
524
|
+
}
|
|
525
|
+
function slashes(path) {
|
|
526
|
+
return path.replaceAll("\\", "/");
|
|
527
|
+
}
|
|
417
528
|
ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive | ts.TypeFlags.TypeParameter;
|
|
418
529
|
// ../project/src/jsonc.ts
|
|
419
530
|
/** Parses JSON with comments and trailing commas, as tsconfig files allow. Comment markers inside strings are kept. */
|
|
@@ -437,6 +548,322 @@ function parseJsonc(text) {
|
|
|
437
548
|
}
|
|
438
549
|
return JSON.parse(output.replace(/,(\s*[}\]])/g, "$1"));
|
|
439
550
|
}
|
|
551
|
+
// ../tokens/src/policy.ts
|
|
552
|
+
/**
|
|
553
|
+
* At-rules that don't pick a theme: a cascade layer only orders rules, and a feature check holds whatever the theme.
|
|
554
|
+
* Any other (`@media`, `@container`, `@scope`…) makes a value conditional, so it's no theme's value.
|
|
555
|
+
*/
|
|
556
|
+
const unconditionalAtRules = /* @__PURE__ */ new Set(["layer", "supports"]);
|
|
557
|
+
/** The theme a `prefers-color-scheme` query picks, when it's the whole query. */
|
|
558
|
+
function colorSchemeOf(atRule) {
|
|
559
|
+
if (atRule.name !== "media") return void 0;
|
|
560
|
+
const scheme = /^\(\s*prefers-color-scheme\s*:\s*(dark|light)\s*\)$/i.exec(atRule.prelude)?.[1]?.toLowerCase();
|
|
561
|
+
if (scheme === "light") return "default";
|
|
562
|
+
return scheme;
|
|
563
|
+
}
|
|
564
|
+
/** Whether a value comes from a `prefers-color-scheme` block, which a theme's own selector overrides. */
|
|
565
|
+
function fromColorScheme(atRules) {
|
|
566
|
+
return atRules.some((atRule) => colorSchemeOf(atRule) !== void 0);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* The theme a rule's values belong to: `default` for the root, otherwise the theme it names, or none.
|
|
570
|
+
*
|
|
571
|
+
* - The root is `:root`, `:host`, `html` or `:where(:root)` (and, with `tailwindTheme`, Tailwind's `@theme`).
|
|
572
|
+
* - A theme is named by a class or an attribute's value, alone or on the root: `.dark`, `[data-theme="dark"]`,
|
|
573
|
+
* `:root.dark`, `html[data-theme=dark]` and `:host([data-theme='dark'])` are all `dark`. `light` names the default,
|
|
574
|
+
* as Parity's own Light mode is the frame without a theme.
|
|
575
|
+
* - `:not(…)` doesn't change the theme, so `:root:not(.dark)` is the root.
|
|
576
|
+
* - In a selector list, a part that's the root makes the rule the root's; otherwise every part must name one theme.
|
|
577
|
+
*
|
|
578
|
+
* Left unsupported, so naming no theme: a selector with a combinator (`.dark .card` styles a component), two names at
|
|
579
|
+
* once (`.dark.contrast`), attribute operators other than `=`, other pseudo-classes, a `:where()` or `:is()` holding a
|
|
580
|
+
* list, and any at-rule besides `@layer` and `@supports` (and the ones the options allow).
|
|
581
|
+
*/
|
|
582
|
+
function themeOfSelector(selector, atRules, options = {}) {
|
|
583
|
+
let scheme;
|
|
584
|
+
for (const atRule of atRules) {
|
|
585
|
+
if (unconditionalAtRules.has(atRule.name) || options.tailwindTheme && atRule.name === "theme") continue;
|
|
586
|
+
const picked = options.colorScheme ? colorSchemeOf(atRule) : void 0;
|
|
587
|
+
if (picked === void 0 || scheme !== void 0 && scheme !== picked) return void 0;
|
|
588
|
+
scheme = picked;
|
|
589
|
+
}
|
|
590
|
+
const inTailwindTheme = options.tailwindTheme === true && atRules.at(-1)?.name === "theme";
|
|
591
|
+
let theme;
|
|
592
|
+
if (selector.trim() !== "") theme = listTheme(selector);
|
|
593
|
+
else if (inTailwindTheme) theme = "default";
|
|
594
|
+
if (theme === void 0 || scheme === void 0) return theme;
|
|
595
|
+
return theme === "default" || theme === scheme ? scheme : void 0;
|
|
596
|
+
}
|
|
597
|
+
function listTheme(selector) {
|
|
598
|
+
const themes = splitTopLevel(selector, ",").map((part) => compoundTheme(part.trim()));
|
|
599
|
+
if (themes.some((theme) => theme?.root && theme.name === void 0)) return "default";
|
|
600
|
+
const names = new Set(themes.map((theme) => theme ? theme.name ?? "default" : void 0));
|
|
601
|
+
const [only] = names;
|
|
602
|
+
return names.size === 1 ? only : void 0;
|
|
603
|
+
}
|
|
604
|
+
const simpleSelector = /^(?:(?<root>html|:root|:host)(?![\w(-])|\.(?<className>[\w-]+)|\[\s*[\w-]+\s*(?:=\s*(?:"(?<double>[^"]*)"|'(?<single>[^']*)'|(?<bare>[\w-]+))\s*(?:[is]\s*)?)?\]|(?<functional>:(?:not|where|is|host)\())/;
|
|
605
|
+
/** The theme one compound selector (no combinators) names, or undefined when it isn't a theme's. */
|
|
606
|
+
function compoundTheme(part) {
|
|
607
|
+
let root = false;
|
|
608
|
+
const names = [];
|
|
609
|
+
let rest = part;
|
|
610
|
+
while (rest.length > 0) {
|
|
611
|
+
const match = simpleSelector.exec(rest);
|
|
612
|
+
if (!match?.groups) return void 0;
|
|
613
|
+
const { groups } = match;
|
|
614
|
+
if (groups.functional) {
|
|
615
|
+
const open = groups.functional.length;
|
|
616
|
+
const close = matchingParen(rest, open - 1);
|
|
617
|
+
if (close === -1) return void 0;
|
|
618
|
+
const inner = rest.slice(open, close).trim();
|
|
619
|
+
rest = rest.slice(close + 1);
|
|
620
|
+
if (groups.functional === ":not(") continue;
|
|
621
|
+
if (groups.functional === ":host(") root = true;
|
|
622
|
+
const nested = compoundTheme(inner);
|
|
623
|
+
if (!nested) return void 0;
|
|
624
|
+
root ||= nested.root;
|
|
625
|
+
if (nested.name !== void 0) names.push(nested.name);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
rest = rest.slice(match[0].length);
|
|
629
|
+
if (groups.root) root = true;
|
|
630
|
+
const name = groups.className ?? groups.double ?? groups.single ?? groups.bare;
|
|
631
|
+
if (name !== void 0) names.push(name);
|
|
632
|
+
}
|
|
633
|
+
if (names.length > 1) return void 0;
|
|
634
|
+
const name = names[0] === "light" ? void 0 : names[0];
|
|
635
|
+
if (!root && names.length === 0) return void 0;
|
|
636
|
+
return {
|
|
637
|
+
root: root || names[0] === "light",
|
|
638
|
+
...name === void 0 ? {} : { name }
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function matchingParen(text, open) {
|
|
642
|
+
let depth = 0;
|
|
643
|
+
for (let index = open; index < text.length; index++) if (text[index] === "(") depth++;
|
|
644
|
+
else if (text[index] === ")" && --depth === 0) return index;
|
|
645
|
+
return -1;
|
|
646
|
+
}
|
|
647
|
+
/** Splits on a separator outside parentheses, brackets and quotes. */
|
|
648
|
+
function splitTopLevel(text, separator) {
|
|
649
|
+
const parts = [];
|
|
650
|
+
let depth = 0;
|
|
651
|
+
let quote;
|
|
652
|
+
let start = 0;
|
|
653
|
+
for (let index = 0; index < text.length; index++) {
|
|
654
|
+
const char = text[index];
|
|
655
|
+
if (quote) {
|
|
656
|
+
if (char === "\\") index++;
|
|
657
|
+
else if (char === quote) quote = void 0;
|
|
658
|
+
} else if (char === "\"" || char === "'") quote = char;
|
|
659
|
+
else if (char === "(" || char === "[") depth++;
|
|
660
|
+
else if (char === ")" || char === "]") depth--;
|
|
661
|
+
else if (char === separator && depth === 0) {
|
|
662
|
+
parts.push(text.slice(start, index));
|
|
663
|
+
start = index + 1;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
parts.push(text.slice(start));
|
|
667
|
+
return parts;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Custom properties by theme, in the order they're first declared. A later declaration in the same theme wins, as in
|
|
671
|
+
* the cascade, except that a `prefers-color-scheme` block never replaces a value a theme's own selector gives. A class
|
|
672
|
+
* or attribute is a theme only if it overrides something the root defines; one that sets only its own variables, such
|
|
673
|
+
* as a component's `.card` or a `.scroll-fade-b` utility, isn't.
|
|
674
|
+
*/
|
|
675
|
+
function themedProperties(declarations, options = {}) {
|
|
676
|
+
const themed = declarations.flatMap((declaration) => {
|
|
677
|
+
const theme = themeOfSelector(declaration.selector, declaration.atRules, options);
|
|
678
|
+
return theme === void 0 ? [] : [{
|
|
679
|
+
...declaration,
|
|
680
|
+
theme,
|
|
681
|
+
scheme: fromColorScheme(declaration.atRules)
|
|
682
|
+
}];
|
|
683
|
+
});
|
|
684
|
+
const rootNames = new Set(themed.flatMap(({ theme, name }) => theme === "default" ? [name] : []));
|
|
685
|
+
const themes = new Set(themed.flatMap(({ theme, name }) => rootNames.has(name) ? [theme] : []));
|
|
686
|
+
const properties = /* @__PURE__ */ new Map();
|
|
687
|
+
const fromScheme = /* @__PURE__ */ new Set();
|
|
688
|
+
for (const { name, value, theme, scheme } of themed) {
|
|
689
|
+
if (!themes.has(theme)) continue;
|
|
690
|
+
const property = properties.get(name) ?? {
|
|
691
|
+
name,
|
|
692
|
+
values: {},
|
|
693
|
+
first: value
|
|
694
|
+
};
|
|
695
|
+
const key = `${theme}\n${name}`;
|
|
696
|
+
if (scheme && property.values[theme] !== void 0 && !fromScheme.has(key)) continue;
|
|
697
|
+
property.values[theme] = value;
|
|
698
|
+
if (scheme) fromScheme.add(key);
|
|
699
|
+
else fromScheme.delete(key);
|
|
700
|
+
properties.set(name, property);
|
|
701
|
+
}
|
|
702
|
+
return [...properties.values()];
|
|
703
|
+
}
|
|
704
|
+
/** Framework internals and per-token modifiers (`text-sm--line-height`) aren't tokens a designer picks. */
|
|
705
|
+
function isTokenName(name) {
|
|
706
|
+
return !name.startsWith("tw-") && !name.startsWith("default-") && !name.includes("--");
|
|
707
|
+
}
|
|
708
|
+
const colorValue = /^(?:#[\da-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color|color-mix|light-dark)\(|transparent$|currentcolor$)/i;
|
|
709
|
+
/** Whether a CSS value is a colour. */
|
|
710
|
+
function isColor(value) {
|
|
711
|
+
return colorValue.test(value.trim());
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* The category a custom property belongs in, from the words in its name, then its value: `--mantine-spacing-md` is
|
|
715
|
+
* spacing, `--fontSizeBase300` a font size, Tailwind's `--text-sm` a font size but `--text-shadow-sm` a shadow, and
|
|
716
|
+
* anything whose value is a colour a colour.
|
|
717
|
+
*/
|
|
718
|
+
function categorize(name, value) {
|
|
719
|
+
const words = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[-_]/).filter(Boolean);
|
|
720
|
+
const has = (...sequence) => words.some((_, index) => sequence.every((word, offset) => words[index + offset] === word));
|
|
721
|
+
if (has("breakpoint") || has("breakpoints") || has("screen") || has("screens")) return "breakpoint";
|
|
722
|
+
const textSize = words[0] === "text" && words.length > 1 && ![
|
|
723
|
+
"wrap",
|
|
724
|
+
"color",
|
|
725
|
+
"decoration",
|
|
726
|
+
"shadow"
|
|
727
|
+
].includes(words[1]);
|
|
728
|
+
if (has("font", "size") || has("font", "sizes") || textSize) return "font-size";
|
|
729
|
+
if (has("line", "height") || has("leading") || has("line", "heights")) return "leading";
|
|
730
|
+
if (has("font", "weight") || has("font", "weights")) return "font-weight";
|
|
731
|
+
if (has("letter", "spacing") || has("tracking")) return "tracking";
|
|
732
|
+
if (has("font", "family") || has("font") || has("fonts")) return "font";
|
|
733
|
+
if (has("radius") || has("radii") || has("rounded")) return "radius";
|
|
734
|
+
if (has("shadow") || has("shadows") || has("elevation")) return "shadow";
|
|
735
|
+
if (has("spacing") || has("space") || has("gap")) return "spacing";
|
|
736
|
+
if (has("blur") || has("perspective") || has("aspect")) return "effect";
|
|
737
|
+
if ([
|
|
738
|
+
"duration",
|
|
739
|
+
"easing",
|
|
740
|
+
"curve",
|
|
741
|
+
"ease",
|
|
742
|
+
"transition",
|
|
743
|
+
"animation",
|
|
744
|
+
"animate",
|
|
745
|
+
"delay"
|
|
746
|
+
].some((word) => has(word))) return "motion";
|
|
747
|
+
if (has("container") && !has("color")) return "container";
|
|
748
|
+
if (has("color") || has("colors") || has("palette") || isColor(value)) return "color";
|
|
749
|
+
return "other";
|
|
750
|
+
}
|
|
751
|
+
new Intl.Collator("en", {
|
|
752
|
+
numeric: true,
|
|
753
|
+
sensitivity: "base"
|
|
754
|
+
});
|
|
755
|
+
// ../project/src/styles.ts
|
|
756
|
+
/**
|
|
757
|
+
* Where an app keeps its global styles when neither `components.json` nor the entry says: Vite's and Create React App's
|
|
758
|
+
* `src/index.css`, the usual alternatives, and Next's `globals.css`, which its layout imports with no HTML entry.
|
|
759
|
+
*/
|
|
760
|
+
const conventionalStylesheets = [
|
|
761
|
+
"src/index.css",
|
|
762
|
+
"src/styles.css",
|
|
763
|
+
"src/app.css",
|
|
764
|
+
"src/globals.css",
|
|
765
|
+
"src/app/globals.css",
|
|
766
|
+
"app/globals.css",
|
|
767
|
+
"styles/globals.css"
|
|
768
|
+
];
|
|
769
|
+
/**
|
|
770
|
+
* The app's global stylesheets, by one set of rules that detection and app libraries share: the one shadcn's
|
|
771
|
+
* `components.json` names; else what the app entry imports (index.html, its module script, that module's stylesheet
|
|
772
|
+
* imports, side-effect or default); else the conventional places that exist. How they were found goes in `notes`.
|
|
773
|
+
*/
|
|
774
|
+
async function findStyles(root, notes = []) {
|
|
775
|
+
return stylesWith(root, await shadcnConfig(root, notes), notes);
|
|
776
|
+
}
|
|
777
|
+
/** `findStyles`, with `components.json` already read. */
|
|
778
|
+
async function stylesWith(root, shadcn, notes) {
|
|
779
|
+
const fromShadcn = field(shadcn, "tailwind", "css");
|
|
780
|
+
if (typeof fromShadcn === "string" && fromShadcn) {
|
|
781
|
+
notes.push(`styles from components.json: ${fromShadcn}`);
|
|
782
|
+
return {
|
|
783
|
+
styles: [toPosix(fromShadcn)],
|
|
784
|
+
packageStyles: []
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const html = await readText(join(root, "index.html"));
|
|
788
|
+
const entry = html && /<script[^>]*type=["']module["'][^>]*src=["']([^"']+)["']/i.exec(html)?.[1];
|
|
789
|
+
if (entry) {
|
|
790
|
+
const entryPath = join(root, entry.replace(/^\//, ""));
|
|
791
|
+
const specifiers = stylesheetImports(await readText(entryPath) ?? "");
|
|
792
|
+
const local = (specifier) => specifier.startsWith(".") || specifier.startsWith("/");
|
|
793
|
+
const imports = specifiers.filter(local).map((specifier) => toPosix(specifier.startsWith("/") ? specifier.slice(1) : relative(root, join(dirname(entryPath), specifier))));
|
|
794
|
+
const packageStyles = specifiers.filter((specifier) => !local(specifier));
|
|
795
|
+
if (specifiers.length > 0) {
|
|
796
|
+
notes.push(`styles from ${toPosix(entry)}: ${[...packageStyles, ...imports].join(", ")}`);
|
|
797
|
+
return {
|
|
798
|
+
styles: imports,
|
|
799
|
+
packageStyles
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const conventional = conventionalStylesheets.filter((path) => existsSync(join(root, path)));
|
|
804
|
+
if (conventional.length > 0) {
|
|
805
|
+
notes.push(`styles from the conventional place: ${conventional.join(", ")}`);
|
|
806
|
+
return {
|
|
807
|
+
styles: conventional,
|
|
808
|
+
packageStyles: []
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
notes.push("no global styles found");
|
|
812
|
+
return {
|
|
813
|
+
styles: [],
|
|
814
|
+
packageStyles: []
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* The global stylesheets a module imports, in order: `import "./a.css"` and `import styles from "./a.css"`, plain CSS
|
|
819
|
+
* or Sass. CSS modules (`.module.css`) are scoped to the component that imports them, so they aren't global styles.
|
|
820
|
+
*/
|
|
821
|
+
function stylesheetImports(source) {
|
|
822
|
+
return [...source.matchAll(/\bimport\s+(?:[\w$*{}\s,]+?\s+from\s+)?["']([^"'?]+\.s?css)["']/g)].map((match) => match[1]).filter((specifier) => !/\.module\.s?css$/.test(specifier));
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* A stylesheet's text with its relative `@import`s inlined, recursively and each file once, so tokens a Tailwind entry
|
|
826
|
+
* keeps in a theme file it imports are read with it. Imports of packages stay as they are. A missing file reads as
|
|
827
|
+
* empty.
|
|
828
|
+
*/
|
|
829
|
+
async function readStylesheet(path, seen = /* @__PURE__ */ new Set()) {
|
|
830
|
+
if (seen.has(path)) return "";
|
|
831
|
+
seen.add(path);
|
|
832
|
+
const source = await readText(path) ?? "";
|
|
833
|
+
const parts = [];
|
|
834
|
+
let last = 0;
|
|
835
|
+
for (const match of source.matchAll(/@import\s+(?:url\()?["'](\.{1,2}\/[^"']+\.s?css)["']\)?[^;]*;/g)) {
|
|
836
|
+
parts.push(source.slice(last, match.index), await readStylesheet(join(dirname(path), match[1]), seen));
|
|
837
|
+
last = match.index + match[0].length;
|
|
838
|
+
}
|
|
839
|
+
parts.push(source.slice(last));
|
|
840
|
+
return parts.join("");
|
|
841
|
+
}
|
|
842
|
+
/** shadcn's `components.json`, which it reads from the project's own folder only and which may have comments. */
|
|
843
|
+
async function shadcnConfig(root, notes) {
|
|
844
|
+
const text = await readText(join(root, "components.json"));
|
|
845
|
+
if (text === void 0) return void 0;
|
|
846
|
+
try {
|
|
847
|
+
return parseJsonc(text);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
notes.push(`Couldn't load components: ${error instanceof Error ? error.message : String(error)}`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
function field(value, ...keys) {
|
|
854
|
+
let current = value;
|
|
855
|
+
for (const key of keys) {
|
|
856
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
857
|
+
current = current[key];
|
|
858
|
+
}
|
|
859
|
+
return current;
|
|
860
|
+
}
|
|
861
|
+
async function readText(path) {
|
|
862
|
+
return readFile(path, "utf8").catch(() => void 0);
|
|
863
|
+
}
|
|
864
|
+
function toPosix(path) {
|
|
865
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
866
|
+
}
|
|
440
867
|
// ../project/src/libraries.ts
|
|
441
868
|
/**
|
|
442
869
|
* Where a dependency is installed, found the way Node finds packages: in `node_modules` here or in a folder above. It
|
|
@@ -452,6 +879,25 @@ async function installedPackage(root, importName) {
|
|
|
452
879
|
async function isFile(path) {
|
|
453
880
|
return stat(path).then((stats) => stats.isFile(), () => false);
|
|
454
881
|
}
|
|
882
|
+
/** A sentence that ends this far in is kept whole, rather than its words cut down to the length. */
|
|
883
|
+
const shortestSentence = 20;
|
|
884
|
+
/**
|
|
885
|
+
* An example's text as a sample keeps it: whole when it's short, else shortened (see `shortenedText`), so a preview
|
|
886
|
+
* reads "Manage how you receive notifications." rather than "…You can enable email a".
|
|
887
|
+
*/
|
|
888
|
+
function sampleText(text) {
|
|
889
|
+
return text.length <= 60 ? text : shortenedText(text.slice(0, 60));
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Text cut at `sampleTextLength`, ended where its last sentence in it ends, else after its last whole word with an
|
|
893
|
+
* ellipsis. It reads only what was kept, so text earlier versions cut there mid-word is shortened as it would be now.
|
|
894
|
+
*/
|
|
895
|
+
function shortenedText(head) {
|
|
896
|
+
const end = [...head.matchAll(/[.!?](?=\s|$)/g)].findLast((match) => match.index >= shortestSentence);
|
|
897
|
+
if (end) return head.slice(0, end.index + 1);
|
|
898
|
+
const space = head.lastIndexOf(" ");
|
|
899
|
+
return `${(space > 0 ? head.slice(0, space) : head.slice(0, -1)).replace(/[\s,;:–—-]+$/, "")}…`;
|
|
900
|
+
}
|
|
455
901
|
// ../libgen/src/examples.ts
|
|
456
902
|
const run = promisify(execFile);
|
|
457
903
|
/** Example and story files: stories, demos, examples and docs' demo files, in TypeScript or JavaScript. */
|
|
@@ -1148,7 +1594,7 @@ function collectChildren(nodes, context, depth, children) {
|
|
|
1148
1594
|
if (children.length >= 4) return;
|
|
1149
1595
|
if (ts.isJsxText(child)) {
|
|
1150
1596
|
const text = child.text.replace(/\s+/g, " ").trim();
|
|
1151
|
-
if (text) children.push(text
|
|
1597
|
+
if (text) children.push(sampleText(text));
|
|
1152
1598
|
} else if (ts.isJsxFragment(child)) collectChildren(child.children, context, depth, children);
|
|
1153
1599
|
else if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) pushElement(child, context, depth, children);
|
|
1154
1600
|
else if (ts.isJsxExpression(child) && child.expression) pushExpression(unwrap$1(child.expression), context, depth, children);
|
|
@@ -1216,7 +1662,7 @@ function pushExpression(expression, context, depth, children) {
|
|
|
1216
1662
|
return;
|
|
1217
1663
|
}
|
|
1218
1664
|
const value = evaluate(expression, context, 0);
|
|
1219
|
-
if (typeof value === "string" || typeof value === "number") children.push(String(value)
|
|
1665
|
+
if (typeof value === "string" || typeof value === "number") children.push(sampleText(String(value)));
|
|
1220
1666
|
}
|
|
1221
1667
|
/**
|
|
1222
1668
|
* A function that returns elements, as a sample's render function: what it returns, and whether it spreads its first
|
|
@@ -1595,7 +2041,7 @@ function textIn(node) {
|
|
|
1595
2041
|
ts.forEachChild(child, visit);
|
|
1596
2042
|
};
|
|
1597
2043
|
visit(node);
|
|
1598
|
-
return parts.join(" ").replace(/\s+/g, " ").trim()
|
|
2044
|
+
return sampleText(parts.join(" ").replace(/\s+/g, " ").trim());
|
|
1599
2045
|
}
|
|
1600
2046
|
function sampleWeight(sample) {
|
|
1601
2047
|
const children = sample.children ?? [];
|
|
@@ -1746,7 +2192,7 @@ function checkAnatomy(anatomy, list) {
|
|
|
1746
2192
|
*/
|
|
1747
2193
|
async function readRenders(packageDir, names, calls, made) {
|
|
1748
2194
|
const renders = /* @__PURE__ */ new Map();
|
|
1749
|
-
for (const file of await sourceFiles
|
|
2195
|
+
for (const file of await sourceFiles(packageDir)) {
|
|
1750
2196
|
const text = await readFile(file, "utf8");
|
|
1751
2197
|
if (!names.some((name) => text.includes(name))) continue;
|
|
1752
2198
|
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
@@ -1929,13 +2375,13 @@ function renderedBy(node, source) {
|
|
|
1929
2375
|
return first && ts.isIdentifier(first) && /^[A-Z]/.test(first.text) ? first.text : void 0;
|
|
1930
2376
|
}
|
|
1931
2377
|
/** The package's own script files, skipping type declarations, source maps and its dependencies. */
|
|
1932
|
-
async function sourceFiles
|
|
2378
|
+
async function sourceFiles(dir, found = [], depth = 0) {
|
|
1933
2379
|
if (depth > 8 || found.length >= 4e3) return found;
|
|
1934
2380
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1935
2381
|
for (const entry of entries) {
|
|
1936
2382
|
const path = join(dir, entry.name);
|
|
1937
2383
|
if (entry.isDirectory()) {
|
|
1938
|
-
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) await sourceFiles
|
|
2384
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) await sourceFiles(path, found, depth + 1);
|
|
1939
2385
|
} else if (/\.(?:m?js|cjs|jsx)$/.test(entry.name) && (await stat(path)).size < 1e6) found.push(path);
|
|
1940
2386
|
}
|
|
1941
2387
|
return found;
|
|
@@ -2923,14 +3369,39 @@ async function relateParts(load, specifier, components, providers = [], edits =
|
|
|
2923
3369
|
if (!relation.parents || relation.inside || relation.visual === false) continue;
|
|
2924
3370
|
if (!relation.parents.some((parent) => loose.has(parent))) continue;
|
|
2925
3371
|
if (!asInserted(relation.parents, name, relation.sample?.children)) continue;
|
|
3372
|
+
const cleared = [];
|
|
2926
3373
|
for (const parent of relation.parents) {
|
|
2927
3374
|
const generated = loose.get(parent);
|
|
2928
3375
|
const sample = relations.get(parent)?.sample;
|
|
2929
3376
|
if (!generated || !sample) continue;
|
|
2930
3377
|
loose.delete(parent);
|
|
2931
|
-
|
|
3378
|
+
const made = Object.entries(sample.props ?? {}).filter(([, value]) => isMadeValue(value) || Array.isArray(value) && value.some(isMadeValue));
|
|
3379
|
+
const kept = {
|
|
3380
|
+
...generated,
|
|
3381
|
+
...Object.fromEntries(made)
|
|
3382
|
+
};
|
|
3383
|
+
if (made.length > 0) cleared.push([
|
|
3384
|
+
parent,
|
|
3385
|
+
generated,
|
|
3386
|
+
kept
|
|
3387
|
+
]);
|
|
3388
|
+
if (Object.keys(kept).length > 0) sample.props = kept;
|
|
2932
3389
|
else delete sample.props;
|
|
2933
|
-
|
|
3390
|
+
}
|
|
3391
|
+
if (cleared.length > 0) {
|
|
3392
|
+
const saved = cleared.map(([parent]) => [parent, samples.get(parent)]);
|
|
3393
|
+
for (const [parent, , kept] of cleared) samples.set(parent, kept);
|
|
3394
|
+
const fails = attempt(relation.parents, name, relation.sample?.children);
|
|
3395
|
+
for (const [parent, sample] of saved) samples.set(parent, sample ?? {});
|
|
3396
|
+
if (fails) for (const [parent, generated] of cleared) {
|
|
3397
|
+
const sample = relations.get(parent).sample;
|
|
3398
|
+
if (Object.keys(generated).length > 0) sample.props = generated;
|
|
3399
|
+
else delete sample.props;
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
for (const parent of relation.parents) {
|
|
3403
|
+
const sample = relations.get(parent)?.sample;
|
|
3404
|
+
if (sample && !sample.props && !sample.children) delete relations.get(parent).sample;
|
|
2934
3405
|
}
|
|
2935
3406
|
}
|
|
2936
3407
|
for (const [name, error] of Object.entries(appErrors)) {
|
|
@@ -2946,7 +3417,15 @@ async function relateParts(load, specifier, components, providers = [], edits =
|
|
|
2946
3417
|
if (relation.parentsFrom === "edit") continue;
|
|
2947
3418
|
const parents = relation.parents ?? [];
|
|
2948
3419
|
const needed = named(error, name, parents, false);
|
|
2949
|
-
if (!needed)
|
|
3420
|
+
if (!needed) {
|
|
3421
|
+
const shown = exampleChain(name);
|
|
3422
|
+
if (shown && shown.join() !== parents.join() && !attempt(shown, name, relation.sample?.children)) {
|
|
3423
|
+
relation.parents = shown;
|
|
3424
|
+
relation.parentsFrom = "app";
|
|
3425
|
+
delete relation.optionalParents;
|
|
3426
|
+
}
|
|
3427
|
+
continue;
|
|
3428
|
+
}
|
|
2950
3429
|
const outer = (relations.get(needed)?.parents ?? []).filter((parent) => parent !== name);
|
|
2951
3430
|
relation.parents = namespaceOf(needed) === namespaceOf(name) ? [.../* @__PURE__ */ new Set([
|
|
2952
3431
|
...outer,
|
|
@@ -3012,6 +3491,46 @@ async function relateParts(load, specifier, components, providers = [], edits =
|
|
|
3012
3491
|
delete relation.inside;
|
|
3013
3492
|
}
|
|
3014
3493
|
}
|
|
3494
|
+
const textOnly = /* @__PURE__ */ new Set();
|
|
3495
|
+
for (const [name, relation] of relations) {
|
|
3496
|
+
const texts = relation.sample?.children;
|
|
3497
|
+
if (!texts?.length || !texts.every((child) => typeof child === "string")) continue;
|
|
3498
|
+
const parents = relation.parents ?? [];
|
|
3499
|
+
if (!attempt(parents, name, [{
|
|
3500
|
+
component: "p",
|
|
3501
|
+
children: texts
|
|
3502
|
+
}]) || attempt(parents, name, texts)) continue;
|
|
3503
|
+
const { children: _, ...sample } = relation.sample;
|
|
3504
|
+
relation.sample = {
|
|
3505
|
+
...sample,
|
|
3506
|
+
props: {
|
|
3507
|
+
...sample.props,
|
|
3508
|
+
children: texts.join("")
|
|
3509
|
+
}
|
|
3510
|
+
};
|
|
3511
|
+
textOnly.add(name);
|
|
3512
|
+
}
|
|
3513
|
+
const asText = (children) => children.map((child) => {
|
|
3514
|
+
if (typeof child !== "object" || !("component" in child) || !child.children) return child;
|
|
3515
|
+
const { children: inner, ...node } = child;
|
|
3516
|
+
if (textOnly.has(child.component) && inner.length > 0 && inner.every((item) => typeof item === "string")) return {
|
|
3517
|
+
...node,
|
|
3518
|
+
props: {
|
|
3519
|
+
...node.props,
|
|
3520
|
+
children: inner.join("")
|
|
3521
|
+
}
|
|
3522
|
+
};
|
|
3523
|
+
return {
|
|
3524
|
+
...node,
|
|
3525
|
+
children: asText(inner)
|
|
3526
|
+
};
|
|
3527
|
+
});
|
|
3528
|
+
if (textOnly.size > 0) {
|
|
3529
|
+
for (const relation of relations.values()) if (relation.sample?.children) relation.sample = {
|
|
3530
|
+
...relation.sample,
|
|
3531
|
+
children: asText(relation.sample.children)
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3015
3534
|
for (const relation of relations.values()) {
|
|
3016
3535
|
const nearest = relation.parents?.at(-1);
|
|
3017
3536
|
if (nearest && relations.get(nearest)?.sample?.children?.length === 0 && relation.parentsFrom !== "edit") {
|
|
@@ -3567,6 +4086,11 @@ const presets = {
|
|
|
3567
4086
|
return { tokens: [...tokens.values()] };
|
|
3568
4087
|
}
|
|
3569
4088
|
},
|
|
4089
|
+
recharts: {
|
|
4090
|
+
id: "recharts",
|
|
4091
|
+
name: "Recharts",
|
|
4092
|
+
description: "Charts, axes, series and grids to compose, as shadcn's charts are."
|
|
4093
|
+
},
|
|
3570
4094
|
"lucide-react": {
|
|
3571
4095
|
id: "lucide",
|
|
3572
4096
|
name: "Lucide",
|
|
@@ -3718,139 +4242,31 @@ function fluentCategory(name) {
|
|
|
3718
4242
|
}
|
|
3719
4243
|
// ../libgen/src/tokens.ts
|
|
3720
4244
|
/**
|
|
3721
|
-
* The
|
|
3722
|
-
*
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
const has = (...sequence) => words.some((_, index) => sequence.every((word, offset) => words[index + offset] === word));
|
|
3727
|
-
if (has("breakpoint") || has("breakpoints") || has("screen")) return "breakpoint";
|
|
3728
|
-
const textSize = words[0] === "text" && ![
|
|
3729
|
-
"wrap",
|
|
3730
|
-
"color",
|
|
3731
|
-
"decoration",
|
|
3732
|
-
"shadow"
|
|
3733
|
-
].includes(words[1] ?? "");
|
|
3734
|
-
if (has("font", "size") || has("font", "sizes") || textSize) return "font-size";
|
|
3735
|
-
if (has("line", "height") || has("leading") || has("line", "heights")) return "leading";
|
|
3736
|
-
if (has("font", "weight") || has("font", "weights")) return "font-weight";
|
|
3737
|
-
if (has("letter", "spacing") || has("tracking")) return "tracking";
|
|
3738
|
-
if (has("font", "family") || has("font") || has("fonts")) return "font";
|
|
3739
|
-
if (has("radius") || has("radii") || has("rounded")) return "radius";
|
|
3740
|
-
if (has("shadow") || has("shadows") || has("elevation")) return "shadow";
|
|
3741
|
-
if (has("spacing") || has("space") || has("gap")) return "spacing";
|
|
3742
|
-
if ([
|
|
3743
|
-
"duration",
|
|
3744
|
-
"easing",
|
|
3745
|
-
"curve",
|
|
3746
|
-
"ease",
|
|
3747
|
-
"transition",
|
|
3748
|
-
"animation",
|
|
3749
|
-
"animate",
|
|
3750
|
-
"delay"
|
|
3751
|
-
].some((word) => has(word))) return "motion";
|
|
3752
|
-
if (has("container") && !has("color")) return "container";
|
|
3753
|
-
if (has("color") || has("colors") || has("palette") || isColor(value)) return "color";
|
|
3754
|
-
return "other";
|
|
3755
|
-
}
|
|
3756
|
-
const colorValue = /^(?:#[\da-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color|color-mix|light-dark)\(|transparent$)/i;
|
|
3757
|
-
function isColor(value) {
|
|
3758
|
-
return colorValue.test(value.trim());
|
|
3759
|
-
}
|
|
3760
|
-
/** A theme a selector belongs to: the root's defaults, or a dark theme however it's written. Anything else, none. */
|
|
3761
|
-
function themeOfSelector(selector, media = "") {
|
|
3762
|
-
const parts = selector.split(",").map((part) => part.trim().replaceAll(/\s+/g, " "));
|
|
3763
|
-
const dark = /prefers-color-scheme:\s*dark/.test(media);
|
|
3764
|
-
const isRoot = (part) => /^(?::where\()?(?::root|:host|html)(?:\)|\s|$|\[|\.|,)/.test(part) || part === ":host";
|
|
3765
|
-
const isDark = (part) => /(?:^|[.\s])dark(?:[\s.:[]|$)|\[[\w-]+=["']?dark["']?\]/.test(part);
|
|
3766
|
-
const isLight = (part) => /(?:^|[.\s])light(?:[\s.:[]|$)|\[[\w-]+=["']?light["']?\]/.test(part);
|
|
3767
|
-
if (parts.some(isDark)) return "dark";
|
|
3768
|
-
if (parts.some((part) => isRoot(part) || isLight(part))) return dark ? "dark" : "default";
|
|
3769
|
-
}
|
|
3770
|
-
/** Splits CSS into nested blocks, each with its prelude and its own declarations. */
|
|
3771
|
-
function blocks(css) {
|
|
3772
|
-
let index = 0;
|
|
3773
|
-
const level = () => {
|
|
3774
|
-
let text = "";
|
|
3775
|
-
let declarations = "";
|
|
3776
|
-
const children = [];
|
|
3777
|
-
while (index < css.length) {
|
|
3778
|
-
const char = css[index];
|
|
3779
|
-
if (char === "/" && css[index + 1] === "*") {
|
|
3780
|
-
const end = css.indexOf("*/", index + 2);
|
|
3781
|
-
index = end === -1 ? css.length : end + 2;
|
|
3782
|
-
} else if (char === "\"" || char === "'") {
|
|
3783
|
-
const end = css.indexOf(char, index + 1);
|
|
3784
|
-
text += css.slice(index, end === -1 ? css.length : end + 1);
|
|
3785
|
-
index = end === -1 ? css.length : end + 1;
|
|
3786
|
-
} else if (char === "{") {
|
|
3787
|
-
index++;
|
|
3788
|
-
const split = text.lastIndexOf(";") + 1;
|
|
3789
|
-
declarations += text.slice(0, split);
|
|
3790
|
-
const inner = level();
|
|
3791
|
-
children.push({
|
|
3792
|
-
prelude: text.slice(split).trim(),
|
|
3793
|
-
...inner
|
|
3794
|
-
});
|
|
3795
|
-
text = "";
|
|
3796
|
-
} else if (char === "}") {
|
|
3797
|
-
index++;
|
|
3798
|
-
return {
|
|
3799
|
-
declarations: declarations + text,
|
|
3800
|
-
children
|
|
3801
|
-
};
|
|
3802
|
-
} else {
|
|
3803
|
-
text += char;
|
|
3804
|
-
index++;
|
|
3805
|
-
}
|
|
3806
|
-
}
|
|
3807
|
-
return {
|
|
3808
|
-
declarations: declarations + text,
|
|
3809
|
-
children
|
|
3810
|
-
};
|
|
3811
|
-
};
|
|
3812
|
-
return level().children;
|
|
3813
|
-
}
|
|
3814
|
-
/**
|
|
3815
|
-
* The tokens a library's CSS defines: custom properties on the root, on a dark theme (a `.dark` class, a
|
|
3816
|
-
* `[data-…=dark]` attribute or a `prefers-color-scheme: dark` block), and in Tailwind's `@theme`. Properties scoped to
|
|
3817
|
-
* one component's class aren't tokens.
|
|
4245
|
+
* The tokens a library's CSS defines, read with the same rules the app reads a project's CSS with (`themedProperties`
|
|
4246
|
+
* and `categorize` in `@parity-app/tokens`): custom properties on the root, and on each theme a class or attribute
|
|
4247
|
+
* names (`.dark`, `[data-theme="dark"]`). Two differences are deliberate, because this reads source CSS whose themes
|
|
4248
|
+
* reach a frame as the class blocks `describedTokenCss` writes: Tailwind's `@theme` is the default, and a
|
|
4249
|
+
* `prefers-color-scheme` block is its theme. Properties scoped to one component's class aren't tokens.
|
|
3818
4250
|
*/
|
|
3819
4251
|
function cssTokens(css) {
|
|
3820
|
-
const
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
}
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
if (name.includes("--") || name.startsWith("tw-")) continue;
|
|
3839
|
-
const token = tokens.get(name) ?? {
|
|
3840
|
-
var: name,
|
|
3841
|
-
category: categorizeVar(name, value),
|
|
3842
|
-
values: {}
|
|
3843
|
-
};
|
|
3844
|
-
token.values[theme] = value;
|
|
3845
|
-
tokens.set(name, token);
|
|
3846
|
-
}
|
|
3847
|
-
for (const match of text.matchAll(/--([\w-]+)--line-height\s*:\s*([^;]+)/g)) {
|
|
3848
|
-
const token = tokens.get(match[1]);
|
|
3849
|
-
if (token && !token.lineHeight) token.lineHeight = match[2].trim().replace(/^var\(--([\w-]+)\)$/, "$1");
|
|
3850
|
-
}
|
|
3851
|
-
};
|
|
3852
|
-
visit(blocks(css), "");
|
|
3853
|
-
return [...tokens.values()];
|
|
4252
|
+
const properties = themedProperties(collectCustomProperties(css), {
|
|
4253
|
+
colorScheme: true,
|
|
4254
|
+
tailwindTheme: true
|
|
4255
|
+
});
|
|
4256
|
+
const byName = new Map(properties.map((property) => [property.name, property]));
|
|
4257
|
+
const tokens = [];
|
|
4258
|
+
for (const { name, values, first } of properties) {
|
|
4259
|
+
if (!isTokenName(name)) continue;
|
|
4260
|
+
const token = {
|
|
4261
|
+
var: name,
|
|
4262
|
+
category: categorize(name, first),
|
|
4263
|
+
values
|
|
4264
|
+
};
|
|
4265
|
+
const lineHeight = byName.get(`${name}--line-height`)?.values.default;
|
|
4266
|
+
if (lineHeight) token.lineHeight = lineHeight.trim().replace(/^var\(--([\w-]+)\)$/, "$1");
|
|
4267
|
+
tokens.push(token);
|
|
4268
|
+
}
|
|
4269
|
+
return tokens;
|
|
3854
4270
|
}
|
|
3855
4271
|
/**
|
|
3856
4272
|
* Pairs each font size with the line height of the same step, as Parity's type controls do with Tailwind's
|
|
@@ -4019,7 +4435,7 @@ function dtcgValue(value, category, paths) {
|
|
|
4019
4435
|
}
|
|
4020
4436
|
// ../libgen/src/index.ts
|
|
4021
4437
|
/**
|
|
4022
|
-
* Describes a component library, an icon set or an app as a
|
|
4438
|
+
* Describes a component library, an icon set or an app as a `.lib.par`: its components found with the type
|
|
4023
4439
|
* checker, its tokens wherever it keeps them, written as DTCG in Parity's categories, its breakpoints, its icons' shape
|
|
4024
4440
|
* and the decorators its components need.
|
|
4025
4441
|
*/
|
|
@@ -4053,7 +4469,7 @@ async function describePackage(options, importName) {
|
|
|
4053
4469
|
const isIcons = preset?.icons !== void 0 || looksLikeIcons(scan);
|
|
4054
4470
|
const tokens = /* @__PURE__ */ new Map();
|
|
4055
4471
|
const styles = preset?.styles ?? stylesheetOf(dir, pkg);
|
|
4056
|
-
if (styles) for (const token of cssTokens(await
|
|
4472
|
+
if (styles) for (const token of cssTokens(await readStylesheet(exportedFile(dir, pkg, styles)))) tokens.set(token.var, token);
|
|
4057
4473
|
let breakpoints;
|
|
4058
4474
|
if (preset?.theme) try {
|
|
4059
4475
|
const theme = await preset.theme(loader(root, dir));
|
|
@@ -4328,16 +4744,24 @@ function summarizeParts(relations) {
|
|
|
4328
4744
|
}
|
|
4329
4745
|
return summary;
|
|
4330
4746
|
}
|
|
4747
|
+
/** The files an app library's components are read from: the type checker finds React components in JSX files. */
|
|
4748
|
+
const appExtensions = [".tsx", ".jsx"];
|
|
4331
4749
|
async function describeApp(options) {
|
|
4332
4750
|
const root = resolve(options.root);
|
|
4333
4751
|
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
4334
4752
|
const notes = [];
|
|
4335
|
-
const
|
|
4336
|
-
|
|
4753
|
+
const tsconfig = readTsconfig(root);
|
|
4754
|
+
const { files } = await appFiles(root, { componentExtensions: appExtensions }, tsconfig);
|
|
4755
|
+
if (tsconfig.configs.length === 0) notes.push("No tsconfig.json or jsconfig.json, so every .tsx and .jsx file in the project was read.");
|
|
4756
|
+
else if (!tsconfig.configs.some((config) => config.files.length > 0)) {
|
|
4757
|
+
const names = tsconfig.configs.map((config) => config.path).join(", ");
|
|
4758
|
+
const verb = tsconfig.configs.length === 1 ? "includes" : "include";
|
|
4759
|
+
notes.push(`${names} ${verb} no files, so every .tsx and .jsx file in the project was read.`);
|
|
4760
|
+
}
|
|
4337
4761
|
const scan = findComponents({
|
|
4338
|
-
entries:
|
|
4762
|
+
entries: files.map((file) => join(root, file).replaceAll("\\", "/")),
|
|
4339
4763
|
root,
|
|
4340
|
-
...
|
|
4764
|
+
...tsconfig.app ? { compilerOptions: tsconfig.app.options } : {},
|
|
4341
4765
|
defaultExports: true
|
|
4342
4766
|
});
|
|
4343
4767
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -4353,7 +4777,7 @@ async function describeApp(options) {
|
|
|
4353
4777
|
};
|
|
4354
4778
|
}
|
|
4355
4779
|
const tokens = /* @__PURE__ */ new Map();
|
|
4356
|
-
for (const stylesheet of await
|
|
4780
|
+
for (const stylesheet of (await findStyles(root)).styles) for (const token of cssTokens(await readStylesheet(join(root, stylesheet)))) tokens.set(token.var, token);
|
|
4357
4781
|
pairLineHeights(tokens);
|
|
4358
4782
|
const breakpoints = breakpointsOf([...tokens.values()]);
|
|
4359
4783
|
const name = pkg.name ?? basename(root);
|
|
@@ -4493,20 +4917,6 @@ function exportedFile(dir, pkg, specifier) {
|
|
|
4493
4917
|
const file = typeof target === "string" ? target : target?.default;
|
|
4494
4918
|
return join(dir, typeof file === "string" ? file : specifier);
|
|
4495
4919
|
}
|
|
4496
|
-
/** A stylesheet with its relative `@import`s inlined, so a Tailwind entry brings its theme file with it. */
|
|
4497
|
-
async function readCss(path, seen = /* @__PURE__ */ new Set()) {
|
|
4498
|
-
if (seen.has(path)) return "";
|
|
4499
|
-
seen.add(path);
|
|
4500
|
-
const source = await readFile(path, "utf8").catch(() => "");
|
|
4501
|
-
const parts = [];
|
|
4502
|
-
let last = 0;
|
|
4503
|
-
for (const match of source.matchAll(/@import\s+(?:url\()?["'](\.{1,2}\/[^"']+\.css)["']\)?[^;]*;/g)) {
|
|
4504
|
-
parts.push(source.slice(last, match.index), await readCss(join(dirname(path), match[1]), seen));
|
|
4505
|
-
last = match.index + match[0].length;
|
|
4506
|
-
}
|
|
4507
|
-
parts.push(source.slice(last));
|
|
4508
|
-
return parts.join("");
|
|
4509
|
-
}
|
|
4510
4920
|
/** A DTCG token file the package ships, if it has one in the format; Buildkit's `tokens.json` isn't yet. */
|
|
4511
4921
|
async function dtcgIn(dir) {
|
|
4512
4922
|
for (const candidate of [
|
|
@@ -4614,8 +5024,16 @@ function exportsByName(file, name) {
|
|
|
4614
5024
|
function loader(root, packageDir) {
|
|
4615
5025
|
return async (specifier) => {
|
|
4616
5026
|
for (const from of [root, packageDir]) {
|
|
4617
|
-
const
|
|
4618
|
-
|
|
5027
|
+
const esm = await resolveImport(from, specifier);
|
|
5028
|
+
const cjs = resolveRequire(from, specifier);
|
|
5029
|
+
const path = esm ?? cjs;
|
|
5030
|
+
if (!path) continue;
|
|
5031
|
+
try {
|
|
5032
|
+
return await import(pathToFileURL(path).href);
|
|
5033
|
+
} catch (error) {
|
|
5034
|
+
if (!cjs || cjs === path) throw error;
|
|
5035
|
+
return await import(pathToFileURL(cjs).href);
|
|
5036
|
+
}
|
|
4619
5037
|
}
|
|
4620
5038
|
throw new Error(`Can't find ${specifier} from ${root}`);
|
|
4621
5039
|
};
|
|
@@ -4650,41 +5068,4 @@ async function resolveImport(from, specifier) {
|
|
|
4650
5068
|
const file = pick(exported[subpath(specifier, name)]) ?? (specifier === name ? pkg.module : void 0);
|
|
4651
5069
|
return file ? join(dir, file) : void 0;
|
|
4652
5070
|
}
|
|
4653
|
-
function appConfig(root) {
|
|
4654
|
-
for (const name of [
|
|
4655
|
-
"tsconfig.app.json",
|
|
4656
|
-
"tsconfig.json",
|
|
4657
|
-
"jsconfig.json"
|
|
4658
|
-
]) {
|
|
4659
|
-
const path = join(root, name);
|
|
4660
|
-
if (!existsSync(path)) continue;
|
|
4661
|
-
const parsed = ts.getParsedCommandLineOfConfigFile(path, {}, {
|
|
4662
|
-
...ts.sys,
|
|
4663
|
-
onUnRecoverableConfigFileDiagnostic: () => void 0
|
|
4664
|
-
});
|
|
4665
|
-
if (parsed && parsed.fileNames.length > 0) return parsed;
|
|
4666
|
-
}
|
|
4667
|
-
}
|
|
4668
|
-
async function sourceFiles(folder) {
|
|
4669
|
-
const entries = await readdir(folder, { withFileTypes: true }).catch(() => []);
|
|
4670
|
-
const files = [];
|
|
4671
|
-
for (const entry of entries) {
|
|
4672
|
-
const path = join(folder, entry.name).replaceAll("\\", "/");
|
|
4673
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") files.push(...await sourceFiles(path));
|
|
4674
|
-
else if (/\.[jt]sx?$/.test(entry.name)) files.push(path);
|
|
4675
|
-
}
|
|
4676
|
-
return files;
|
|
4677
|
-
}
|
|
4678
|
-
/** An app's global stylesheets: shadcn's `components.json` names one; otherwise the usual places. */
|
|
4679
|
-
async function appStylesheets(root) {
|
|
4680
|
-
const shadcn = await readFile(join(root, "components.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
|
|
4681
|
-
if (shadcn?.tailwind?.css) return [shadcn.tailwind.css];
|
|
4682
|
-
return [
|
|
4683
|
-
"src/index.css",
|
|
4684
|
-
"src/styles.css",
|
|
4685
|
-
"src/app.css",
|
|
4686
|
-
"src/globals.css",
|
|
4687
|
-
"app/globals.css"
|
|
4688
|
-
].filter((path) => existsSync(join(root, path)));
|
|
4689
|
-
}
|
|
4690
5071
|
export { appManifestPath, generateLibrary, manifestText, mergeManifest, parseLibraryEdits, reviseLibrary, iconVariants as t };
|