@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.
@@ -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-D9EMN48r.mjs";
2
- import "./src-DXZqtm6z.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,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.slice(0, 60));
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).slice(0, 60));
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().slice(0, 60);
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$1(packageDir)) {
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$1(dir, found = [], depth = 0) {
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$1(path, found, depth + 1);
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 category a custom property belongs in, from the words in its name, then its value: `--mantine-spacing-md` is
3804
- * spacing, `--fontSizeBase300` a font size, and anything whose value is a colour a colour.
3805
- */
3806
- function categorizeVar(name, value) {
3807
- const words = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().split(/[-_]/);
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 tokens = /* @__PURE__ */ new Map();
3903
- const visit = (list, media) => {
3904
- for (const block of list) {
3905
- if (/^@theme\b/.test(block.prelude)) declarations(block.declarations, "default");
3906
- else if (block.prelude.startsWith("@")) {
3907
- visit(block.children, block.prelude.startsWith("@media") ? `${media} ${block.prelude}` : media);
3908
- continue;
3909
- } else {
3910
- const theme = themeOfSelector(block.prelude, media);
3911
- if (theme) declarations(block.declarations, theme);
3912
- }
3913
- visit(block.children, media);
3914
- }
3915
- };
3916
- const declarations = (text, theme) => {
3917
- for (const match of text.matchAll(/--([\w-]+)\s*:\s*([^;]+)/g)) {
3918
- const name = match[1];
3919
- const value = match[2].trim();
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 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
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 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);
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 config = appConfig(root);
4418
- 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
+ }
4419
4761
  const scan = findComponents({
4420
- 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("\\", "/")),
4421
4763
  root,
4422
- ...config ? { compilerOptions: config.options } : {},
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 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);
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 };