@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.
@@ -1,13 +1,13 @@
1
- import { S 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 } from "./src-CSeISXoZ.mjs";
2
- import "./src-uPmBVSsB.mjs";
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
- ({ ...ts.sys });
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.slice(0, 60));
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).slice(0, 60));
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().slice(0, 60);
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$1(packageDir)) {
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$1(dir, found = [], depth = 0) {
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$1(path, found, depth + 1);
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
- if (Object.keys(generated).length > 0) sample.props = generated;
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
- if (!sample.props && !sample.children) delete relations.get(parent).sample;
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) continue;
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 category a custom property belongs in, from the words in its name, then its value: `--mantine-spacing-md` is
3722
- * spacing, `--fontSizeBase300` a font size, and anything whose value is a colour a colour.
3723
- */
3724
- function categorizeVar(name, value) {
3725
- const words = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[-_]/);
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 tokens = /* @__PURE__ */ new Map();
3821
- const visit = (list, media) => {
3822
- for (const block of list) {
3823
- if (/^@theme\b/.test(block.prelude)) declarations(block.declarations, "default");
3824
- else if (block.prelude.startsWith("@")) {
3825
- visit(block.children, block.prelude.startsWith("@media") ? `${media} ${block.prelude}` : media);
3826
- continue;
3827
- } else {
3828
- const theme = themeOfSelector(block.prelude, media);
3829
- if (theme) declarations(block.declarations, theme);
3830
- }
3831
- visit(block.children, media);
3832
- }
3833
- };
3834
- const declarations = (text, theme) => {
3835
- for (const match of text.matchAll(/--([\w-]+)\s*:\s*([^;]+)/g)) {
3836
- const name = match[1];
3837
- const value = match[2].trim();
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 version 3 `.lib.par`: its components found with the type
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 readCss(exportedFile(dir, pkg, styles)))) tokens.set(token.var, token);
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 config = appConfig(root);
4336
- if (!config) notes.push("No tsconfig.json or jsconfig.json, so every .tsx and .jsx file under src was read.");
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: (config?.fileNames ?? await sourceFiles(join(root, "src"))).filter((file) => /\.[jt]sx$/.test(file) && !/\.(stories|test|spec)\.[jt]sx$/.test(file) && !file.includes("/node_modules/")),
4762
+ entries: files.map((file) => join(root, file).replaceAll("\\", "/")),
4339
4763
  root,
4340
- ...config ? { compilerOptions: config.options } : {},
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 appStylesheets(root)) for (const token of cssTokens(await readCss(join(root, stylesheet)))) tokens.set(token.var, token);
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 path = await resolveImport(from, specifier) ?? resolveRequire(from, specifier);
4618
- if (path) return await import(pathToFileURL(path).href);
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 };