@parity-app/cli 0.1.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 +87 -1
- package/dist/bin.mjs +1132 -272
- 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 +17 -17
- 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/rhea.lib.par +20 -20
- package/dist/libraries/sera.lib.par +20 -20
- package/dist/libraries/vega.lib.par +20 -20
- package/dist/{src-DYi23Ypb.mjs → src-BOUQtcWB.mjs} +1 -1
- package/dist/{src-FF8eY6EI.mjs → src-BRgxMRtE.mjs} +3 -2
- package/dist/{src-D9EMN48r.mjs → src-BZ1p_ip_.mjs} +125 -36
- package/dist/{src-DXZqtm6z.mjs → src-CAm6EBIK.mjs} +1 -1
- package/dist/{src-8MhDV_sA.mjs → src-CcWc-KsX.mjs} +498 -207
- package/package.json +8 -6
- package/skills/SKILL.md +61 -14
- package/skills/document.md +2 -1
- package/skills/execute.md +74 -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,10 +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
|
+
}
|
|
440
751
|
new Intl.Collator("en", {
|
|
441
752
|
numeric: true,
|
|
442
753
|
sensitivity: "base"
|
|
443
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
|
+
}
|
|
444
867
|
// ../project/src/libraries.ts
|
|
445
868
|
/**
|
|
446
869
|
* Where a dependency is installed, found the way Node finds packages: in `node_modules` here or in a folder above. It
|
|
@@ -456,6 +879,25 @@ async function installedPackage(root, importName) {
|
|
|
456
879
|
async function isFile(path) {
|
|
457
880
|
return stat(path).then((stats) => stats.isFile(), () => false);
|
|
458
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
|
+
}
|
|
459
901
|
// ../libgen/src/examples.ts
|
|
460
902
|
const run = promisify(execFile);
|
|
461
903
|
/** Example and story files: stories, demos, examples and docs' demo files, in TypeScript or JavaScript. */
|
|
@@ -1152,7 +1594,7 @@ function collectChildren(nodes, context, depth, children) {
|
|
|
1152
1594
|
if (children.length >= 4) return;
|
|
1153
1595
|
if (ts.isJsxText(child)) {
|
|
1154
1596
|
const text = child.text.replace(/\s+/g, " ").trim();
|
|
1155
|
-
if (text) children.push(text
|
|
1597
|
+
if (text) children.push(sampleText(text));
|
|
1156
1598
|
} else if (ts.isJsxFragment(child)) collectChildren(child.children, context, depth, children);
|
|
1157
1599
|
else if (ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) pushElement(child, context, depth, children);
|
|
1158
1600
|
else if (ts.isJsxExpression(child) && child.expression) pushExpression(unwrap$1(child.expression), context, depth, children);
|
|
@@ -1220,7 +1662,7 @@ function pushExpression(expression, context, depth, children) {
|
|
|
1220
1662
|
return;
|
|
1221
1663
|
}
|
|
1222
1664
|
const value = evaluate(expression, context, 0);
|
|
1223
|
-
if (typeof value === "string" || typeof value === "number") children.push(String(value)
|
|
1665
|
+
if (typeof value === "string" || typeof value === "number") children.push(sampleText(String(value)));
|
|
1224
1666
|
}
|
|
1225
1667
|
/**
|
|
1226
1668
|
* A function that returns elements, as a sample's render function: what it returns, and whether it spreads its first
|
|
@@ -1599,7 +2041,7 @@ function textIn(node) {
|
|
|
1599
2041
|
ts.forEachChild(child, visit);
|
|
1600
2042
|
};
|
|
1601
2043
|
visit(node);
|
|
1602
|
-
return parts.join(" ").replace(/\s+/g, " ").trim()
|
|
2044
|
+
return sampleText(parts.join(" ").replace(/\s+/g, " ").trim());
|
|
1603
2045
|
}
|
|
1604
2046
|
function sampleWeight(sample) {
|
|
1605
2047
|
const children = sample.children ?? [];
|
|
@@ -1750,7 +2192,7 @@ function checkAnatomy(anatomy, list) {
|
|
|
1750
2192
|
*/
|
|
1751
2193
|
async function readRenders(packageDir, names, calls, made) {
|
|
1752
2194
|
const renders = /* @__PURE__ */ new Map();
|
|
1753
|
-
for (const file of await sourceFiles
|
|
2195
|
+
for (const file of await sourceFiles(packageDir)) {
|
|
1754
2196
|
const text = await readFile(file, "utf8");
|
|
1755
2197
|
if (!names.some((name) => text.includes(name))) continue;
|
|
1756
2198
|
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false, ts.ScriptKind.TSX);
|
|
@@ -1933,13 +2375,13 @@ function renderedBy(node, source) {
|
|
|
1933
2375
|
return first && ts.isIdentifier(first) && /^[A-Z]/.test(first.text) ? first.text : void 0;
|
|
1934
2376
|
}
|
|
1935
2377
|
/** The package's own script files, skipping type declarations, source maps and its dependencies. */
|
|
1936
|
-
async function sourceFiles
|
|
2378
|
+
async function sourceFiles(dir, found = [], depth = 0) {
|
|
1937
2379
|
if (depth > 8 || found.length >= 4e3) return found;
|
|
1938
2380
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
1939
2381
|
for (const entry of entries) {
|
|
1940
2382
|
const path = join(dir, entry.name);
|
|
1941
2383
|
if (entry.isDirectory()) {
|
|
1942
|
-
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);
|
|
1943
2385
|
} else if (/\.(?:m?js|cjs|jsx)$/.test(entry.name) && (await stat(path)).size < 1e6) found.push(path);
|
|
1944
2386
|
}
|
|
1945
2387
|
return found;
|
|
@@ -3800,139 +4242,31 @@ function fluentCategory(name) {
|
|
|
3800
4242
|
}
|
|
3801
4243
|
// ../libgen/src/tokens.ts
|
|
3802
4244
|
/**
|
|
3803
|
-
* The
|
|
3804
|
-
*
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
const has = (...sequence) => words.some((_, index) => sequence.every((word, offset) => words[index + offset] === word));
|
|
3809
|
-
if (has("breakpoint") || has("breakpoints") || has("screen")) return "breakpoint";
|
|
3810
|
-
const textSize = words[0] === "text" && ![
|
|
3811
|
-
"wrap",
|
|
3812
|
-
"color",
|
|
3813
|
-
"decoration",
|
|
3814
|
-
"shadow"
|
|
3815
|
-
].includes(words[1] ?? "");
|
|
3816
|
-
if (has("font", "size") || has("font", "sizes") || textSize) return "font-size";
|
|
3817
|
-
if (has("line", "height") || has("leading") || has("line", "heights")) return "leading";
|
|
3818
|
-
if (has("font", "weight") || has("font", "weights")) return "font-weight";
|
|
3819
|
-
if (has("letter", "spacing") || has("tracking")) return "tracking";
|
|
3820
|
-
if (has("font", "family") || has("font") || has("fonts")) return "font";
|
|
3821
|
-
if (has("radius") || has("radii") || has("rounded")) return "radius";
|
|
3822
|
-
if (has("shadow") || has("shadows") || has("elevation")) return "shadow";
|
|
3823
|
-
if (has("spacing") || has("space") || has("gap")) return "spacing";
|
|
3824
|
-
if ([
|
|
3825
|
-
"duration",
|
|
3826
|
-
"easing",
|
|
3827
|
-
"curve",
|
|
3828
|
-
"ease",
|
|
3829
|
-
"transition",
|
|
3830
|
-
"animation",
|
|
3831
|
-
"animate",
|
|
3832
|
-
"delay"
|
|
3833
|
-
].some((word) => has(word))) return "motion";
|
|
3834
|
-
if (has("container") && !has("color")) return "container";
|
|
3835
|
-
if (has("color") || has("colors") || has("palette") || isColor(value)) return "color";
|
|
3836
|
-
return "other";
|
|
3837
|
-
}
|
|
3838
|
-
const colorValue = /^(?:#[\da-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|hwb|color|color-mix|light-dark)\(|transparent$)/i;
|
|
3839
|
-
function isColor(value) {
|
|
3840
|
-
return colorValue.test(value.trim());
|
|
3841
|
-
}
|
|
3842
|
-
/** A theme a selector belongs to: the root's defaults, or a dark theme however it's written. Anything else, none. */
|
|
3843
|
-
function themeOfSelector(selector, media = "") {
|
|
3844
|
-
const parts = selector.split(",").map((part) => part.trim().replaceAll(/\s+/g, " "));
|
|
3845
|
-
const dark = /prefers-color-scheme:\s*dark/.test(media);
|
|
3846
|
-
const isRoot = (part) => /^(?::where\()?(?::root|:host|html)(?:\)|\s|$|\[|\.|,)/.test(part) || part === ":host";
|
|
3847
|
-
const isDark = (part) => /(?:^|[.\s])dark(?:[\s.:[]|$)|\[[\w-]+=["']?dark["']?\]/.test(part);
|
|
3848
|
-
const isLight = (part) => /(?:^|[.\s])light(?:[\s.:[]|$)|\[[\w-]+=["']?light["']?\]/.test(part);
|
|
3849
|
-
if (parts.some(isDark)) return "dark";
|
|
3850
|
-
if (parts.some((part) => isRoot(part) || isLight(part))) return dark ? "dark" : "default";
|
|
3851
|
-
}
|
|
3852
|
-
/** Splits CSS into nested blocks, each with its prelude and its own declarations. */
|
|
3853
|
-
function blocks(css) {
|
|
3854
|
-
let index = 0;
|
|
3855
|
-
const level = () => {
|
|
3856
|
-
let text = "";
|
|
3857
|
-
let declarations = "";
|
|
3858
|
-
const children = [];
|
|
3859
|
-
while (index < css.length) {
|
|
3860
|
-
const char = css[index];
|
|
3861
|
-
if (char === "/" && css[index + 1] === "*") {
|
|
3862
|
-
const end = css.indexOf("*/", index + 2);
|
|
3863
|
-
index = end === -1 ? css.length : end + 2;
|
|
3864
|
-
} else if (char === "\"" || char === "'") {
|
|
3865
|
-
const end = css.indexOf(char, index + 1);
|
|
3866
|
-
text += css.slice(index, end === -1 ? css.length : end + 1);
|
|
3867
|
-
index = end === -1 ? css.length : end + 1;
|
|
3868
|
-
} else if (char === "{") {
|
|
3869
|
-
index++;
|
|
3870
|
-
const split = text.lastIndexOf(";") + 1;
|
|
3871
|
-
declarations += text.slice(0, split);
|
|
3872
|
-
const inner = level();
|
|
3873
|
-
children.push({
|
|
3874
|
-
prelude: text.slice(split).trim(),
|
|
3875
|
-
...inner
|
|
3876
|
-
});
|
|
3877
|
-
text = "";
|
|
3878
|
-
} else if (char === "}") {
|
|
3879
|
-
index++;
|
|
3880
|
-
return {
|
|
3881
|
-
declarations: declarations + text,
|
|
3882
|
-
children
|
|
3883
|
-
};
|
|
3884
|
-
} else {
|
|
3885
|
-
text += char;
|
|
3886
|
-
index++;
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
return {
|
|
3890
|
-
declarations: declarations + text,
|
|
3891
|
-
children
|
|
3892
|
-
};
|
|
3893
|
-
};
|
|
3894
|
-
return level().children;
|
|
3895
|
-
}
|
|
3896
|
-
/**
|
|
3897
|
-
* The tokens a library's CSS defines: custom properties on the root, on a dark theme (a `.dark` class, a
|
|
3898
|
-
* `[data-…=dark]` attribute or a `prefers-color-scheme: dark` block), and in Tailwind's `@theme`. Properties scoped to
|
|
3899
|
-
* 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.
|
|
3900
4250
|
*/
|
|
3901
4251
|
function cssTokens(css) {
|
|
3902
|
-
const
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
}
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
if (name.includes("--") || name.startsWith("tw-")) continue;
|
|
3921
|
-
const token = tokens.get(name) ?? {
|
|
3922
|
-
var: name,
|
|
3923
|
-
category: categorizeVar(name, value),
|
|
3924
|
-
values: {}
|
|
3925
|
-
};
|
|
3926
|
-
token.values[theme] = value;
|
|
3927
|
-
tokens.set(name, token);
|
|
3928
|
-
}
|
|
3929
|
-
for (const match of text.matchAll(/--([\w-]+)--line-height\s*:\s*([^;]+)/g)) {
|
|
3930
|
-
const token = tokens.get(match[1]);
|
|
3931
|
-
if (token && !token.lineHeight) token.lineHeight = match[2].trim().replace(/^var\(--([\w-]+)\)$/, "$1");
|
|
3932
|
-
}
|
|
3933
|
-
};
|
|
3934
|
-
visit(blocks(css), "");
|
|
3935
|
-
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;
|
|
3936
4270
|
}
|
|
3937
4271
|
/**
|
|
3938
4272
|
* Pairs each font size with the line height of the same step, as Parity's type controls do with Tailwind's
|
|
@@ -4101,7 +4435,7 @@ function dtcgValue(value, category, paths) {
|
|
|
4101
4435
|
}
|
|
4102
4436
|
// ../libgen/src/index.ts
|
|
4103
4437
|
/**
|
|
4104
|
-
* 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
|
|
4105
4439
|
* checker, its tokens wherever it keeps them, written as DTCG in Parity's categories, its breakpoints, its icons' shape
|
|
4106
4440
|
* and the decorators its components need.
|
|
4107
4441
|
*/
|
|
@@ -4135,7 +4469,7 @@ async function describePackage(options, importName) {
|
|
|
4135
4469
|
const isIcons = preset?.icons !== void 0 || looksLikeIcons(scan);
|
|
4136
4470
|
const tokens = /* @__PURE__ */ new Map();
|
|
4137
4471
|
const styles = preset?.styles ?? stylesheetOf(dir, pkg);
|
|
4138
|
-
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);
|
|
4139
4473
|
let breakpoints;
|
|
4140
4474
|
if (preset?.theme) try {
|
|
4141
4475
|
const theme = await preset.theme(loader(root, dir));
|
|
@@ -4410,16 +4744,24 @@ function summarizeParts(relations) {
|
|
|
4410
4744
|
}
|
|
4411
4745
|
return summary;
|
|
4412
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"];
|
|
4413
4749
|
async function describeApp(options) {
|
|
4414
4750
|
const root = resolve(options.root);
|
|
4415
4751
|
const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8"));
|
|
4416
4752
|
const notes = [];
|
|
4417
|
-
const
|
|
4418
|
-
|
|
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
|
+
}
|
|
4419
4761
|
const scan = findComponents({
|
|
4420
|
-
entries:
|
|
4762
|
+
entries: files.map((file) => join(root, file).replaceAll("\\", "/")),
|
|
4421
4763
|
root,
|
|
4422
|
-
...
|
|
4764
|
+
...tsconfig.app ? { compilerOptions: tsconfig.app.options } : {},
|
|
4423
4765
|
defaultExports: true
|
|
4424
4766
|
});
|
|
4425
4767
|
const counts = /* @__PURE__ */ new Map();
|
|
@@ -4435,7 +4777,7 @@ async function describeApp(options) {
|
|
|
4435
4777
|
};
|
|
4436
4778
|
}
|
|
4437
4779
|
const tokens = /* @__PURE__ */ new Map();
|
|
4438
|
-
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);
|
|
4439
4781
|
pairLineHeights(tokens);
|
|
4440
4782
|
const breakpoints = breakpointsOf([...tokens.values()]);
|
|
4441
4783
|
const name = pkg.name ?? basename(root);
|
|
@@ -4575,20 +4917,6 @@ function exportedFile(dir, pkg, specifier) {
|
|
|
4575
4917
|
const file = typeof target === "string" ? target : target?.default;
|
|
4576
4918
|
return join(dir, typeof file === "string" ? file : specifier);
|
|
4577
4919
|
}
|
|
4578
|
-
/** A stylesheet with its relative `@import`s inlined, so a Tailwind entry brings its theme file with it. */
|
|
4579
|
-
async function readCss(path, seen = /* @__PURE__ */ new Set()) {
|
|
4580
|
-
if (seen.has(path)) return "";
|
|
4581
|
-
seen.add(path);
|
|
4582
|
-
const source = await readFile(path, "utf8").catch(() => "");
|
|
4583
|
-
const parts = [];
|
|
4584
|
-
let last = 0;
|
|
4585
|
-
for (const match of source.matchAll(/@import\s+(?:url\()?["'](\.{1,2}\/[^"']+\.css)["']\)?[^;]*;/g)) {
|
|
4586
|
-
parts.push(source.slice(last, match.index), await readCss(join(dirname(path), match[1]), seen));
|
|
4587
|
-
last = match.index + match[0].length;
|
|
4588
|
-
}
|
|
4589
|
-
parts.push(source.slice(last));
|
|
4590
|
-
return parts.join("");
|
|
4591
|
-
}
|
|
4592
4920
|
/** A DTCG token file the package ships, if it has one in the format; Buildkit's `tokens.json` isn't yet. */
|
|
4593
4921
|
async function dtcgIn(dir) {
|
|
4594
4922
|
for (const candidate of [
|
|
@@ -4740,41 +5068,4 @@ async function resolveImport(from, specifier) {
|
|
|
4740
5068
|
const file = pick(exported[subpath(specifier, name)]) ?? (specifier === name ? pkg.module : void 0);
|
|
4741
5069
|
return file ? join(dir, file) : void 0;
|
|
4742
5070
|
}
|
|
4743
|
-
function appConfig(root) {
|
|
4744
|
-
for (const name of [
|
|
4745
|
-
"tsconfig.app.json",
|
|
4746
|
-
"tsconfig.json",
|
|
4747
|
-
"jsconfig.json"
|
|
4748
|
-
]) {
|
|
4749
|
-
const path = join(root, name);
|
|
4750
|
-
if (!existsSync(path)) continue;
|
|
4751
|
-
const parsed = ts.getParsedCommandLineOfConfigFile(path, {}, {
|
|
4752
|
-
...ts.sys,
|
|
4753
|
-
onUnRecoverableConfigFileDiagnostic: () => void 0
|
|
4754
|
-
});
|
|
4755
|
-
if (parsed && parsed.fileNames.length > 0) return parsed;
|
|
4756
|
-
}
|
|
4757
|
-
}
|
|
4758
|
-
async function sourceFiles(folder) {
|
|
4759
|
-
const entries = await readdir(folder, { withFileTypes: true }).catch(() => []);
|
|
4760
|
-
const files = [];
|
|
4761
|
-
for (const entry of entries) {
|
|
4762
|
-
const path = join(folder, entry.name).replaceAll("\\", "/");
|
|
4763
|
-
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") files.push(...await sourceFiles(path));
|
|
4764
|
-
else if (/\.[jt]sx?$/.test(entry.name)) files.push(path);
|
|
4765
|
-
}
|
|
4766
|
-
return files;
|
|
4767
|
-
}
|
|
4768
|
-
/** An app's global stylesheets: shadcn's `components.json` names one; otherwise the usual places. */
|
|
4769
|
-
async function appStylesheets(root) {
|
|
4770
|
-
const shadcn = await readFile(join(root, "components.json"), "utf8").then((text) => JSON.parse(text), () => void 0);
|
|
4771
|
-
if (shadcn?.tailwind?.css) return [shadcn.tailwind.css];
|
|
4772
|
-
return [
|
|
4773
|
-
"src/index.css",
|
|
4774
|
-
"src/styles.css",
|
|
4775
|
-
"src/app.css",
|
|
4776
|
-
"src/globals.css",
|
|
4777
|
-
"app/globals.css"
|
|
4778
|
-
].filter((path) => existsSync(join(root, path)));
|
|
4779
|
-
}
|
|
4780
5071
|
export { appManifestPath, generateLibrary, manifestText, mergeManifest, parseLibraryEdits, reviseLibrary, iconVariants as t };
|