rainbowindex 0.6.0 → 0.7.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/LICENSE +16 -17
  3. package/NOTICE.md +106 -0
  4. package/README.md +219 -65
  5. package/dist/browser.d.ts +4 -2
  6. package/dist/browser.mjs +12 -4
  7. package/dist/chunk-2T7V5XLK.mjs +912 -0
  8. package/dist/chunk-6OORICWF.mjs +16 -0
  9. package/dist/{chunk-KSNYSR3C.mjs → chunk-FJOZJIKB.mjs} +2499 -329
  10. package/dist/chunk-L56IRO7A.mjs +491 -0
  11. package/dist/chunk-PZDVDEZJ.mjs +196 -0
  12. package/dist/{chunk-3LWJTLOJ.mjs → chunk-RC6DDE4L.mjs} +23 -15
  13. package/dist/chunk-TQJYVQPE.mjs +217 -0
  14. package/dist/chunk-W756NVYI.mjs +33 -0
  15. package/dist/chunk-WBESS2ZD.mjs +598 -0
  16. package/dist/{chunk-3HRMFZGE.mjs → chunk-X66Z2YHT.mjs} +2 -1
  17. package/dist/{chunk-6U4IOFOS.mjs → chunk-XQGSG2HK.mjs} +199 -555
  18. package/dist/cli.mjs +1077 -123
  19. package/dist/{index-DSgpB6bS.d.ts → context-DcBtnnan.d.ts} +47 -103
  20. package/dist/editor.d.ts +71 -433
  21. package/dist/editor.mjs +51 -368
  22. package/dist/eslint.d.ts +16 -0
  23. package/dist/eslint.mjs +32 -0
  24. package/dist/{index-4Kyaq3IZ.d.ts → imports-C9esHd5Q.d.ts} +78 -84
  25. package/dist/index-CNqdL5U0.d.ts +56 -0
  26. package/dist/index-Czx-EUwh.d.ts +138 -0
  27. package/dist/index-DW8YSxTz.d.ts +104 -0
  28. package/dist/index.d.ts +46 -5
  29. package/dist/index.mjs +33 -9
  30. package/dist/oxlint.d.ts +21 -3
  31. package/dist/oxlint.mjs +19 -1
  32. package/dist/recipe.d.ts +111 -0
  33. package/dist/recipe.mjs +71 -0
  34. package/dist/safelist-CH3_PywB.d.ts +43 -0
  35. package/dist/session-CMaskdB7.d.ts +543 -0
  36. package/dist/tailwind.css +644 -0
  37. package/dist/theme-CIZiGlce.d.ts +115 -0
  38. package/dist/vite.d.ts +10 -1
  39. package/dist/vite.mjs +266 -118
  40. package/package.json +27 -5
  41. package/dist/chunk-WK6S4HTC.mjs +0 -1921
  42. package/dist/chunk-ZR7XJMUN.mjs +0 -251
  43. package/dist/safelist-CGCtF-Fr.d.ts +0 -96
package/dist/cli.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- CSS_ENTRY_CANDIDATES,
4
- enumerateClassNames
5
- } from "./chunk-ZR7XJMUN.mjs";
3
+ snapshotFromCSS
4
+ } from "./chunk-6OORICWF.mjs";
6
5
  import {
7
6
  DEFAULT_EXCLUDES,
8
7
  DEFAULT_PATTERNS,
@@ -10,39 +9,74 @@ import {
10
9
  disableScanChangeTracking,
11
10
  enableScanChangeTracking,
12
11
  enableSourceFileListCache,
13
- getFontPreloadLinks,
14
12
  invalidateSourceFileListCache,
15
- markSourceFileChanged
16
- } from "./chunk-WK6S4HTC.mjs";
13
+ markSourceFileChanged,
14
+ resolveSourceFilesAsync
15
+ } from "./chunk-2T7V5XLK.mjs";
16
+ import {
17
+ CANONICAL_COLOR_STOPS,
18
+ CSS_ENTRY_CANDIDATES,
19
+ createClassInspector,
20
+ enumerateClassNames,
21
+ listThemeTokens,
22
+ outermostCandidates,
23
+ resolveColorSwatch
24
+ } from "./chunk-WBESS2ZD.mjs";
25
+ import {
26
+ createNodeImportResolver
27
+ } from "./chunk-W756NVYI.mjs";
17
28
  import {
18
29
  MAX_DIRECTIVE_INPUT_SIZE,
30
+ analyzeProjectCSS,
19
31
  codepointCompare,
32
+ extractClassCandidates,
20
33
  extractClassesFromSource,
21
34
  extractDirectives,
35
+ findClosingBrace,
36
+ getFontPreloadLinks,
22
37
  hasApplyLikeDirective,
23
38
  hasRIActivation,
39
+ inlineDirectiveImports,
24
40
  listVariants,
25
- resolveDirectives
26
- } from "./chunk-KSNYSR3C.mjs";
41
+ resolveDirectives,
42
+ stripCSSComments
43
+ } from "./chunk-FJOZJIKB.mjs";
44
+ import "./chunk-L56IRO7A.mjs";
27
45
  import {
28
46
  devWarn
29
- } from "./chunk-6U4IOFOS.mjs";
47
+ } from "./chunk-XQGSG2HK.mjs";
30
48
 
31
49
  // src/entries/cli.ts
32
50
  import { realpathSync } from "fs";
33
- import { resolve as resolve7 } from "path";
51
+ import { resolve as resolve10 } from "path";
34
52
  import { pathToFileURL } from "url";
35
53
 
36
54
  // src/cli/args.ts
37
55
  import { relative, resolve } from "path";
38
- var OUTPUT = {
56
+ var WRITE = {
57
+ names: ["--write"],
58
+ describe: "Apply the migration in place (default: write a preview beside it)",
59
+ apply(opts) {
60
+ opts.write = true;
61
+ }
62
+ };
63
+ var cssInput = (describe) => ({
64
+ names: ["--css"],
65
+ valueName: "<file>",
66
+ describe,
67
+ apply(opts, value) {
68
+ opts.cssFile = value;
69
+ }
70
+ });
71
+ var output = (describe) => ({
39
72
  names: ["-o", "--output"],
40
73
  valueName: "<file>",
41
- describe: "Output CSS file path (omit to write to stdout)",
74
+ describe,
42
75
  apply(opts, value) {
43
76
  opts.output = value;
44
77
  }
45
- };
78
+ });
79
+ var OUTPUT = output("Output CSS file path (omit to write to stdout)");
46
80
  var WATCH = {
47
81
  names: ["--watch"],
48
82
  describe: "Re-run on source-file changes (requires --output)",
@@ -73,33 +107,14 @@ var TEMPLATE = {
73
107
  opts.template = value;
74
108
  }
75
109
  };
76
- var applyCSSFile = (opts, value) => {
77
- opts.cssFile = value;
78
- };
79
- var CSS_INPUT = {
80
- names: ["--css"],
81
- valueName: "<file>",
82
- describe: "CSS input with directives (auto-detected if omitted)",
83
- apply: applyCSSFile
84
- };
85
- var CSS_INPUT_TYPES = {
86
- names: ["--css"],
87
- valueName: "<file>",
88
- describe: "CSS input with @color/@text/@utility/etc directives",
89
- apply: applyCSSFile
90
- };
91
- var CSS_INPUT_FONTS = {
92
- names: ["--css"],
93
- valueName: "<file>",
94
- describe: "CSS input with @font directives",
95
- apply: applyCSSFile
96
- };
97
- var CSS_TARGET = {
98
- names: ["--css"],
99
- valueName: "<file>",
100
- describe: "Stylesheet path to create/patch (default: src/index.css)",
101
- apply: applyCSSFile
102
- };
110
+ var CSS_INPUT = cssInput("CSS input with directives (auto-detected if omitted)");
111
+ var CSS_INPUT_TYPES = cssInput("CSS input with @color/@text/@utility/etc directives");
112
+ var CSS_INPUT_FONTS = cssInput("CSS input with @font directives");
113
+ var TOKENS_OUTPUT = output(
114
+ "Token module path (default: rainbowindex-tokens.ts); tokens.json lands beside it"
115
+ );
116
+ var SNAPSHOT_OUTPUT = output("Snapshot module path (default: rainbowindex-snapshot.ts)");
117
+ var CSS_TARGET = cssInput("Stylesheet path to create/patch (default: src/index.css)");
103
118
  var COMMANDS = {
104
119
  build: {
105
120
  summary: "Generate CSS from source files",
@@ -147,6 +162,27 @@ Examples:
147
162
  rainbowindex create my-app
148
163
  rainbowindex create my-app --template vue-ts`
149
164
  },
165
+ migrate: {
166
+ summary: "Translate a Tailwind v4 project into Rainbow Index directives",
167
+ usage: " rainbowindex migrate tailwind [glob...] [options]",
168
+ intro: `What this does:
169
+ 1. Finds the Tailwind CSS entry and translates its @theme into directives.
170
+ 2. Adds @import "rainbowindex/tailwind.css" so the familiar class names
171
+ keep resolving, and keeps your dark-mode strategy.
172
+ 3. Scans the globs you pass and reports every class the new theme rejects.
173
+ 4. Writes migration-report.md with what still needs a person.`,
174
+ flags: [CSS_INPUT, WRITE],
175
+ positionals: "migrate",
176
+ body: `Safety:
177
+ Nothing is overwritten without --write. By default the translated entry is
178
+ written beside the original as <name>.rainbowindex.css so you can read it
179
+ first. With --write, the original is kept as <name>.css.tailwind.bak.
180
+
181
+ Examples:
182
+ rainbowindex migrate tailwind
183
+ rainbowindex migrate tailwind "src/**/*.{ts,tsx}"
184
+ rainbowindex migrate tailwind --css app/globals.css --write`
185
+ },
150
186
  "generate-types": {
151
187
  summary: "Generate TypeScript types for ri() autocomplete",
152
188
  usage: " rainbowindex generate-types [options]",
@@ -160,6 +196,57 @@ Examples:
160
196
  rainbowindex generate-types
161
197
  rainbowindex generate-types --strict --css src/styles.css`
162
198
  },
199
+ "generate-snapshot": {
200
+ summary: "Generate a theme snapshot module so client ri() is theme-aware",
201
+ usage: " rainbowindex generate-snapshot [options]",
202
+ flags: [CSS_INPUT_TYPES, SNAPSHOT_OUTPUT],
203
+ positionals: "none",
204
+ body: `Why:
205
+ \`ri()\` asks the published theme whether \`text-lg\` is a size or a color. A
206
+ browser bundle runs no compile, so without a published theme it guesses, and
207
+ a size can be read as a color and dropped. The Vite plugin handles this on
208
+ its own \u2014 this command is for every other bundler.
209
+
210
+ Output:
211
+ Writes \`rainbowindex-snapshot.ts\` to the current working directory. Import
212
+ it once, as early as possible in your entry point:
213
+
214
+ import "./rainbowindex-snapshot";
215
+
216
+ It also exports \`ri\` bound to that snapshot, for multi-tenant rendering.
217
+ Re-run it when the theme changes; unchanged input rewrites nothing.
218
+
219
+ Examples:
220
+ rainbowindex generate-snapshot
221
+ rainbowindex generate-snapshot --css src/styles.css -o src/theme.ts`
222
+ },
223
+ "generate-tokens": {
224
+ summary: "Generate typed token exports and a W3C Design Tokens file",
225
+ usage: " rainbowindex generate-tokens [options]",
226
+ flags: [CSS_INPUT_TYPES, TOKENS_OUTPUT],
227
+ positionals: "none",
228
+ body: `Why:
229
+ A class is the right way to style an element and the wrong way to hand a
230
+ chart library a series color or a canvas a fill. Those need the token's
231
+ value. This writes the theme as data, so that code can read it without
232
+ re-declaring the palette in JavaScript.
233
+
234
+ Output:
235
+ \`rainbowindex-tokens.ts\` \u2014 \`tokens.color.brand[500]\`, typed \`as const\`, so a
236
+ typo is a compile error and the editor completes the names. The values are
237
+ \`var()\` references rather than literals, so they still follow the cascade.
238
+
239
+ \`tokens.json\` beside it \u2014 the same theme in the W3C Design Tokens format,
240
+ with values resolved to hex, which is what Style Dictionary and Figma
241
+ importers read. A design tool has no cascade to resolve a \`var()\` against.
242
+
243
+ Both are sorted and derived from the CSS entry alone, so re-running on an
244
+ unchanged theme rewrites nothing.
245
+
246
+ Examples:
247
+ rainbowindex generate-tokens
248
+ rainbowindex generate-tokens --css src/styles.css -o src/tokens.ts`
249
+ },
163
250
  scan: {
164
251
  summary: "Show what the class scanner extracts (debugging)",
165
252
  usage: " rainbowindex scan <file|glob...>",
@@ -211,6 +298,7 @@ function parseArgs(argv, callbacks) {
211
298
  watch: false,
212
299
  minify: false,
213
300
  strict: false,
301
+ write: false,
214
302
  subcommandExplicit: false,
215
303
  earlyExit: false
216
304
  };
@@ -280,8 +368,18 @@ function parseArgs(argv, callbacks) {
280
368
  opts.targetDir = arg;
281
369
  continue;
282
370
  }
371
+ if (spec.positionals === "migrate") {
372
+ if (opts.migrateSource === void 0) opts.migrateSource = arg;
373
+ else opts.globs.push(arg);
374
+ continue;
375
+ }
283
376
  throw new Error(`Unexpected extra argument "${arg}" for ${opts.command}.`);
284
377
  }
378
+ if (opts.command === "migrate" && opts.migrateSource === void 0) {
379
+ throw new Error(
380
+ "migrate requires a source. The only one is `tailwind`: rainbowindex migrate tailwind"
381
+ );
382
+ }
285
383
  if (opts.command === "create" && !opts.targetDir) {
286
384
  throw new Error("create requires a project directory. Example: rainbowindex create my-app");
287
385
  }
@@ -347,7 +445,10 @@ Usage:
347
445
  rainbowindex <glob> [options] Generate CSS from source files
348
446
  rainbowindex init Wire Rainbow Index into the current Vite app
349
447
  rainbowindex create <dir> Scaffold a Vite app with Rainbow Index ready
448
+ rainbowindex migrate tailwind Translate a Tailwind v4 project into directives
350
449
  rainbowindex generate-types Generate TypeScript types for ri()
450
+ rainbowindex generate-snapshot Generate the theme snapshot for client ri()
451
+ rainbowindex generate-tokens Generate typed token exports + tokens.json
351
452
  rainbowindex preload-fonts Generate font preload link tags
352
453
  rainbowindex scan <file...> Show what the class scanner extracts
353
454
 
@@ -414,20 +515,32 @@ async function readCSSFileAsync(cssFile) {
414
515
  return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
415
516
  }
416
517
  async function loadProjectCSS(opts, cwd) {
518
+ const load = async (cssFile2) => {
519
+ const inlined = inlineDirectiveImports(await readCSSFileAsync(cssFile2), {
520
+ resolve: createNodeImportResolver({ cwd }),
521
+ from: cssFile2
522
+ });
523
+ return {
524
+ css: inlined.css,
525
+ cssFile: cssFile2,
526
+ imports: inlined.files,
527
+ warnings: inlined.warnings
528
+ };
529
+ };
417
530
  if (opts.cssFile) {
418
531
  const cssFile2 = resolve2(cwd, opts.cssFile);
419
532
  if (!existsSync(cssFile2)) {
420
533
  throw new Error(`[RI-1605] CSS input file not found: ${cssFile2}`);
421
534
  }
422
- return { css: await readCSSFileAsync(cssFile2), cssFile: cssFile2 };
535
+ return load(cssFile2);
423
536
  }
424
537
  const cssFile = await findCSSFileAsync(cwd);
425
- if (!cssFile) return { css: "", cssFile: null };
426
- return { css: await readCSSFileAsync(cssFile), cssFile };
538
+ if (!cssFile) return { css: "", cssFile: null, imports: [], warnings: [] };
539
+ return load(cssFile);
427
540
  }
428
541
  async function loadProjectTheme(opts, cwd) {
429
- const { css } = await loadProjectCSS(opts, cwd);
430
- const parseWarnings = [];
542
+ const { css, warnings } = await loadProjectCSS(opts, cwd);
543
+ const parseWarnings = [...warnings];
431
544
  const directives = extractDirectives(css, parseWarnings);
432
545
  const theme = resolveDirectives(directives);
433
546
  for (const w of parseWarnings) console.error(w);
@@ -436,7 +549,7 @@ async function loadProjectTheme(opts, cwd) {
436
549
 
437
550
  // src/cli/build.ts
438
551
  async function buildCSS(opts, cwd) {
439
- const { css: cssSource, cssFile } = await loadProjectCSS(opts, cwd);
552
+ const { css: cssSource, cssFile, warnings: importWarnings } = await loadProjectCSS(opts, cwd);
440
553
  const { compiled } = await compileScannedProject({
441
554
  css: cssSource,
442
555
  cssPath: cssFile ?? void 0,
@@ -445,9 +558,12 @@ async function buildCSS(opts, cwd) {
445
558
  onInvalidPattern: (err) => {
446
559
  console.error(`[RI-1404] CLI glob pattern rejected: ${err}`);
447
560
  return void 0;
448
- }
561
+ },
562
+ // loadProjectCSS already inlined them; a second pass would find nothing
563
+ // and would re-emit the same unresolved-import warnings.
564
+ resolveImport: null
449
565
  });
450
- const buildWarnings = [];
566
+ const buildWarnings = [...importWarnings];
451
567
  if (hasApplyLikeDirective(cssSource)) {
452
568
  buildWarnings.push(
453
569
  '[RI-1009] @apply is only supported via the PostCSS plugin. To expand @apply: (1) add `postcss.config.js` with `import rainbowindex from "rainbowindex"; export default { plugins: [rainbowindex()] };`, or (2) use the Vite plugin (`import rainbowindex from "rainbowindex/vite"`), which wires PostCSS automatically. The CLI does not run PostCSS so it cannot expand @apply.'
@@ -476,10 +592,227 @@ async function buildCSS(opts, cwd) {
476
592
  return { css: compiled.css, theme: compiled.theme, cssFile };
477
593
  }
478
594
 
595
+ // src/cli/generate-snapshot.ts
596
+ import { relative as relative2, resolve as resolve3 } from "path";
597
+
598
+ // src/cli/atomic-write.ts
599
+ import { writeFile, rename, unlink } from "fs/promises";
600
+ import { dirname, join } from "path";
601
+ function isIgnorableCleanupError(err) {
602
+ return !!err && typeof err === "object" && "code" in err && (err.code === "ENOENT" || err.code === "EPERM");
603
+ }
604
+ async function writeFileAtomic(filePath, content) {
605
+ const dir = dirname(filePath);
606
+ const tmp = join(dir, `.ri-tmp-${process.pid}-${Date.now()}`);
607
+ try {
608
+ await writeFile(tmp, content);
609
+ await rename(tmp, filePath);
610
+ } catch (err) {
611
+ try {
612
+ await unlink(tmp);
613
+ } catch (cleanupErr) {
614
+ if (!isIgnorableCleanupError(cleanupErr)) {
615
+ const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
616
+ devWarn(`[RI-DEV] Failed to remove temp file "${tmp}": ${msg}`);
617
+ }
618
+ }
619
+ throw err;
620
+ }
621
+ }
622
+
623
+ // src/cli/generate-snapshot.ts
624
+ var SNAPSHOT_FILE = "rainbowindex-snapshot.ts";
625
+ async function generateSnapshot(opts, cwd) {
626
+ const { css, cssFile, warnings } = await loadProjectCSS(opts, cwd);
627
+ for (const warning of warnings) console.error(warning);
628
+ const analysis = analyzeProjectCSS(css);
629
+ for (const warning of analysis.warnings) console.error(warning);
630
+ const snapshot = snapshotFromCSS(css);
631
+ const source = cssFile === null ? "no CSS entry found" : relative2(cwd, cssFile);
632
+ const outPath = resolve3(cwd, opts.output ?? SNAPSHOT_FILE);
633
+ const body = `/**
634
+ * Generated by \`rainbowindex generate-snapshot\` from ${source}.
635
+ * Do not edit. Re-run the command when the theme changes.
636
+ *
637
+ * Import this once, as early as possible in your entry point:
638
+ *
639
+ * import "./${relative2(cwd, outPath).split("\\\\").join("/")}";
640
+ *
641
+ * That publishes the theme, so the default \`ri()\` resolves your project's
642
+ * text sizes, weights, font slots, and color names. Without it \`ri()\` has no
643
+ * theme and can read a size as a color and drop it.
644
+ *
645
+ * For multi-tenant rendering \u2014 more than one theme in one process \u2014 import
646
+ * \`ri\` from here instead of from "rainbowindex": it is bound to this snapshot
647
+ * and shares no module state.
648
+ */
649
+ import { createRi, hydrateSnapshot, publishSnapshot } from "rainbowindex";
650
+
651
+ export const snapshot = hydrateSnapshot(${JSON.stringify(snapshot, null, " ")});
652
+
653
+ publishSnapshot(snapshot);
654
+
655
+ export const ri = createRi(snapshot);
656
+ `;
657
+ await writeFileAtomic(outPath, body);
658
+ console.log(`[rainbowindex] Wrote: ${relative2(cwd, outPath)}`);
659
+ }
660
+
661
+ // src/cli/generate-tokens.ts
662
+ import { relative as relative3, resolve as resolve4 } from "path";
663
+ var TOKENS_FILE = "rainbowindex-tokens.ts";
664
+ var TOKENS_JSON_FILE = "tokens.json";
665
+ function byKey([a], [b]) {
666
+ const na = /^\d+$/.test(a);
667
+ const nb = /^\d+$/.test(b);
668
+ if (na && nb) return Number(a) - Number(b);
669
+ if (na !== nb) return na ? -1 : 1;
670
+ return a < b ? -1 : a > b ? 1 : 0;
671
+ }
672
+ var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
673
+ function key(name) {
674
+ return IDENT_RE.test(name) ? name : JSON.stringify(name);
675
+ }
676
+ function renderObject(value, indent) {
677
+ if (typeof value === "string") return JSON.stringify(value);
678
+ if (typeof value === "number") return String(value);
679
+ const entries = Object.entries(value).sort(byKey);
680
+ if (entries.length === 0) return "{}";
681
+ const inner = entries.map(([k, v]) => `${indent} ${key(k)}: ${renderObject(v, `${indent} `)},`).join("\n");
682
+ return `{
683
+ ${inner}
684
+ ${indent}}`;
685
+ }
686
+ function buildVarTokens(theme) {
687
+ const tokens = listThemeTokens(theme);
688
+ const out = {};
689
+ const color = {};
690
+ for (const { name, kind } of tokens.colors) {
691
+ if (kind === "generative") {
692
+ const stops = {};
693
+ for (const stop of CANONICAL_COLOR_STOPS) {
694
+ stops[String(stop)] = `var(--color-${name}-${stop})`;
695
+ }
696
+ color[name] = stops;
697
+ } else {
698
+ color[name] = `var(--color-${name})`;
699
+ }
700
+ }
701
+ if (Object.keys(color).length > 0) out.color = color;
702
+ const simple = [
703
+ ["text", "text", Object.fromEntries(tokens.textSizes.map((t) => [t.name, t.fontSize]))],
704
+ ["breakpoint", "breakpoint", tokens.breakpoints],
705
+ ["shadow", "shadow", tokens.shadows],
706
+ ["weight", "weight", tokens.weights],
707
+ ["ease", "ease", tokens.easing],
708
+ ["blur", "blur", tokens.blur],
709
+ ["z", "z", tokens.z],
710
+ ["leading", "leading", tokens.leading],
711
+ ["tracking", "tracking", tokens.tracking],
712
+ ["opacity", "opacity", tokens.opacity],
713
+ ["duration", "duration", tokens.duration],
714
+ ["rounded", "rounded", tokens.radii],
715
+ ["animate", "animate", Object.fromEntries(tokens.animations.map((n) => [n, n]))]
716
+ ];
717
+ for (const [group, prefix, values] of simple) {
718
+ const names = Object.keys(values);
719
+ if (names.length === 0) continue;
720
+ out[group] = Object.fromEntries(names.map((n) => [n, `var(--${prefix}-${n})`]));
721
+ }
722
+ if (tokens.fonts.length > 0) {
723
+ out.font = Object.fromEntries(tokens.fonts.map((f) => [f.slot, `var(--font-${f.slot})`]));
724
+ }
725
+ return out;
726
+ }
727
+ function buildDesignTokens(theme) {
728
+ const tokens = listThemeTokens(theme);
729
+ const out = {};
730
+ const color = {};
731
+ for (const { name, kind } of tokens.colors) {
732
+ if (kind === "generative") {
733
+ const stops = {};
734
+ for (const stop of CANONICAL_COLOR_STOPS) {
735
+ const swatch2 = resolveColorSwatch(theme, name, stop);
736
+ if (swatch2 === null) continue;
737
+ stops[String(stop)] = { $type: "color", $value: swatch2.light.hex };
738
+ }
739
+ if (Object.keys(stops).length > 0) color[name] = stops;
740
+ continue;
741
+ }
742
+ const swatch = resolveColorSwatch(theme, name);
743
+ if (swatch !== null) color[name] = { $type: "color", $value: swatch.light.hex };
744
+ }
745
+ if (Object.keys(color).length > 0) out.color = color;
746
+ const typed = [
747
+ ["text", "dimension", Object.fromEntries(tokens.textSizes.map((t) => [t.name, t.fontSize]))],
748
+ ["breakpoint", "dimension", tokens.breakpoints],
749
+ ["tracking", "dimension", tokens.tracking],
750
+ ["blur", "dimension", tokens.blur],
751
+ ["rounded", "dimension", tokens.radii],
752
+ ["weight", "fontWeight", tokens.weights],
753
+ ["duration", "duration", tokens.duration],
754
+ ["ease", "cubicBezier", tokens.easing],
755
+ ["shadow", "shadow", tokens.shadows],
756
+ ["leading", "number", tokens.leading],
757
+ ["opacity", "number", tokens.opacity],
758
+ ["z", "number", tokens.z]
759
+ ];
760
+ for (const [group, type, values] of typed) {
761
+ const names = Object.keys(values);
762
+ if (names.length === 0) continue;
763
+ out[group] = Object.fromEntries(
764
+ names.map((n) => [n, { $type: type, $value: String(values[n]) }])
765
+ );
766
+ }
767
+ if (tokens.fonts.length > 0) {
768
+ out.font = Object.fromEntries(
769
+ tokens.fonts.map((f) => [f.slot, { $type: "fontFamily", $value: f.family }])
770
+ );
771
+ }
772
+ return out;
773
+ }
774
+ function sortDeep(value) {
775
+ if (Array.isArray(value)) return value.map(sortDeep);
776
+ if (value === null || typeof value !== "object") return value;
777
+ const entries = Object.entries(value).sort(byKey);
778
+ return Object.fromEntries(entries.map(([k, v]) => [k, sortDeep(v)]));
779
+ }
780
+ async function generateTokens(opts, cwd) {
781
+ const { css, cssFile, warnings } = await loadProjectCSS(opts, cwd);
782
+ for (const warning of warnings) console.error(warning);
783
+ const analysis = analyzeProjectCSS(css);
784
+ for (const warning of analysis.warnings) console.error(warning);
785
+ const source = cssFile === null ? "no CSS entry found" : relative3(cwd, cssFile);
786
+ const outPath = resolve4(cwd, opts.output ?? TOKENS_FILE);
787
+ const jsonPath = resolve4(outPath, "..", TOKENS_JSON_FILE);
788
+ const body = `/**
789
+ * Generated by \`rainbowindex generate-tokens\` from ${source}.
790
+ * Do not edit. Re-run the command when the theme changes.
791
+ *
792
+ * Every value is a \`var()\` reference, not a literal, so it still follows the
793
+ * cascade \u2014 a \`[data-theme]\` override or a dark-mode flip changes what it
794
+ * resolves to. Use these where a class cannot go: a chart's series colors, a
795
+ * canvas fill, an inline style computed at runtime.
796
+ *
797
+ * import { tokens } from "./${relative3(cwd, outPath).split("\\\\").join("/").replace(/\.tsx?$/, "")}";
798
+ * <Chart color={tokens.color.brand[500]} />
799
+ */
800
+ export const tokens = ${renderObject(buildVarTokens(analysis.theme), "")} as const;
801
+
802
+ export type Tokens = typeof tokens;
803
+ `;
804
+ await writeFileAtomic(outPath, body);
805
+ console.log(`[rainbowindex] Wrote: ${relative3(cwd, outPath)}`);
806
+ const designTokens = sortDeep(buildDesignTokens(analysis.theme));
807
+ await writeFileAtomic(jsonPath, `${JSON.stringify(designTokens, null, " ")}
808
+ `);
809
+ console.log(`[rainbowindex] Wrote: ${relative3(cwd, jsonPath)}`);
810
+ }
811
+
479
812
  // src/cli/generate-types.ts
480
813
  import { existsSync as existsSync2 } from "fs";
481
- import { readFile as readFile2, writeFile } from "fs/promises";
482
- import { resolve as resolve3 } from "path";
814
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
815
+ import { resolve as resolve5 } from "path";
483
816
  async function generateTypes(opts, cwd) {
484
817
  const theme = await loadProjectTheme(opts, cwd);
485
818
  const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
@@ -582,14 +915,14 @@ async function generateTypes(opts, cwd) {
582
915
  "}",
583
916
  ""
584
917
  );
585
- const outputPath = resolve3(cwd, "rainbowindex-env.d.ts");
918
+ const outputPath = resolve5(cwd, "rainbowindex-env.d.ts");
586
919
  const newContent = lines.join("\n");
587
920
  if (existsSync2(outputPath)) {
588
921
  const existing = await readFile2(outputPath, "utf-8");
589
922
  if (!existing.startsWith("// rainbowindex-env.d.ts (auto-generated")) {
590
923
  const backupPath = `${outputPath}.bak`;
591
924
  try {
592
- await writeFile(backupPath, existing);
925
+ await writeFile2(backupPath, existing);
593
926
  console.warn(
594
927
  `[RI-1604] "${outputPath}" exists and does not appear to be auto-generated. Original saved to "${backupPath}" before overwrite.`
595
928
  );
@@ -604,7 +937,7 @@ async function generateTypes(opts, cwd) {
604
937
  }
605
938
  }
606
939
  try {
607
- await writeFile(outputPath, newContent);
940
+ await writeFile2(outputPath, newContent);
608
941
  } catch (err) {
609
942
  const msg = err instanceof Error ? err.message : String(err);
610
943
  console.error(`[rainbowindex] Failed to write types file "${outputPath}": ${msg}`);
@@ -617,6 +950,642 @@ async function generateTypes(opts, cwd) {
617
950
  );
618
951
  }
619
952
 
953
+ // src/cli/migrate.ts
954
+ import { readFile as readFile3 } from "fs/promises";
955
+ import { readdirSync, readFileSync } from "fs";
956
+ import { basename, dirname as dirname2, join as join2, relative as relative4, resolve as resolve6 } from "path";
957
+
958
+ // src/migrate/tailwind.ts
959
+ var TAILWIND_IMPORT_RE = /@import\s+["']tailwindcss(?:\/[\w-]+)?["']/;
960
+ var RI_IMPORT_RE = /@import\s+["']rainbowindex(?:\/[\w.-]+)?["']/;
961
+ var TAILWIND_PACKAGES = [
962
+ "tailwindcss",
963
+ "@tailwindcss/vite",
964
+ "@tailwindcss/postcss",
965
+ "@tailwindcss/cli"
966
+ ];
967
+ var LEGACY_CONFIG_FILES = [
968
+ "tailwind.config.js",
969
+ "tailwind.config.mjs",
970
+ "tailwind.config.cjs",
971
+ "tailwind.config.ts"
972
+ ];
973
+ function detectTailwind(options) {
974
+ const signals = [];
975
+ const css = stripCSSComments(options.css);
976
+ if (TAILWIND_IMPORT_RE.test(css)) signals.push('@import "tailwindcss"');
977
+ if (/@theme\b/.test(css)) signals.push("@theme");
978
+ if (/@custom-variant\b/.test(css)) signals.push("@custom-variant");
979
+ const deps = {
980
+ ...options.packageJson?.dependencies ?? {},
981
+ ...options.packageJson?.devDependencies ?? {}
982
+ };
983
+ for (const name of TAILWIND_PACKAGES) {
984
+ if (Object.hasOwn(deps, name)) signals.push(`${name} in package.json`);
985
+ }
986
+ const legacyConfig = (options.files ?? []).find((f) => LEGACY_CONFIG_FILES.includes(f)) ?? null;
987
+ return { isTailwind: signals.length > 0, signals, legacyConfig };
988
+ }
989
+ function scanAtRules(css) {
990
+ const out = [];
991
+ for (let i = 0; i < css.length; i++) {
992
+ if (css[i] !== "@") continue;
993
+ let nameEnd = i + 1;
994
+ while (nameEnd < css.length && /[\w-]/.test(css[nameEnd])) nameEnd++;
995
+ const name = css.slice(i + 1, nameEnd);
996
+ if (!name) continue;
997
+ let j = nameEnd;
998
+ while (j < css.length && css[j] !== "{" && css[j] !== ";") j++;
999
+ if (j >= css.length) break;
1000
+ const prelude = css.slice(nameEnd, j).trim();
1001
+ if (css[j] === ";") {
1002
+ out.push({ name, prelude, body: null, start: i, end: j + 1 });
1003
+ i = j;
1004
+ continue;
1005
+ }
1006
+ const close = findClosingBrace(css, j);
1007
+ if (close === -1) break;
1008
+ out.push({ name, prelude, body: css.slice(j + 1, close), start: i, end: close + 1 });
1009
+ i = close;
1010
+ }
1011
+ return out;
1012
+ }
1013
+ var NAMESPACES = [
1014
+ { prefix: "--font-weight-", directive: "weight" },
1015
+ { prefix: "--color-", directive: "color" },
1016
+ { prefix: "--font-", directive: "font" },
1017
+ { prefix: "--text-", directive: "text" },
1018
+ { prefix: "--tracking-", directive: "tracking" },
1019
+ { prefix: "--leading-", directive: "leading" },
1020
+ { prefix: "--breakpoint-", directive: "breakpoint" },
1021
+ { prefix: "--radius-", directive: "rounded" },
1022
+ { prefix: "--shadow-", directive: "shadow" },
1023
+ { prefix: "--blur-", directive: "blur" },
1024
+ { prefix: "--ease-", directive: "ease" },
1025
+ { prefix: "--animate-", directive: "animate" }
1026
+ ];
1027
+ var UNSUPPORTED_NAMESPACES = [
1028
+ { prefix: "--container-", action: "Rainbow Index's container ladder is built in; drop these." },
1029
+ {
1030
+ prefix: "--default-",
1031
+ action: "A Tailwind internal, and its `--theme()` value is Tailwind's own function. Drop it."
1032
+ },
1033
+ { prefix: "--max-width-", action: "Use an arbitrary value: `max-w-[65ch]`." },
1034
+ {
1035
+ prefix: "--inset-shadow-",
1036
+ action: "There is no theme namespace for inset shadows \u2014 define an `@utility` per name."
1037
+ },
1038
+ {
1039
+ prefix: "--drop-shadow-",
1040
+ action: "There is no theme namespace for drop shadows \u2014 define an `@utility` per name."
1041
+ },
1042
+ {
1043
+ prefix: "--text-shadow-",
1044
+ action: "There is no theme namespace for text shadows \u2014 define an `@utility` per name."
1045
+ },
1046
+ { prefix: "--perspective-", action: "Use an arbitrary value: `perspective-[800px]`." },
1047
+ { prefix: "--aspect-", action: "Use an arbitrary value: `aspect-[16/10]`." }
1048
+ ];
1049
+ function tokenName(key2, prefix) {
1050
+ return key2.slice(prefix.length);
1051
+ }
1052
+ function emitBlock(directive, bucket) {
1053
+ const lines = [...bucket].map(
1054
+ ([key2, value]) => value.startsWith("{") ? ` ${key2} ${value}` : ` ${key2}: ${value};`
1055
+ );
1056
+ return `@${directive} {
1057
+ ${lines.join("\n")}
1058
+ }
1059
+ `;
1060
+ }
1061
+ function reindent(body, depth) {
1062
+ const pad = " ".repeat(depth);
1063
+ const lines = body.trim().split("\n").map((line) => line.trim()).filter((line) => line !== "");
1064
+ return lines.map((line) => pad + line).join("\n");
1065
+ }
1066
+ function migrateTailwindCSS(input) {
1067
+ const css = stripCSSComments(input);
1068
+ const atRules = scanAtRules(css);
1069
+ const buckets = /* @__PURE__ */ new Map();
1070
+ const bucket = (directive) => {
1071
+ let existing = buckets.get(directive);
1072
+ if (existing === void 0) {
1073
+ existing = /* @__PURE__ */ new Map();
1074
+ buckets.set(directive, existing);
1075
+ }
1076
+ return existing;
1077
+ };
1078
+ const manual = [];
1079
+ const keyframes = /* @__PURE__ */ new Map();
1080
+ const consumed = [];
1081
+ let darkVariant = null;
1082
+ const lineHeights = /* @__PURE__ */ new Map();
1083
+ for (const rule of atRules) {
1084
+ switch (rule.name) {
1085
+ case "import": {
1086
+ const target = `@import ${rule.prelude}`;
1087
+ if (TAILWIND_IMPORT_RE.test(target) || RI_IMPORT_RE.test(target)) {
1088
+ consumed.push([rule.start, rule.end]);
1089
+ }
1090
+ break;
1091
+ }
1092
+ case "color": {
1093
+ if (rule.prelude.trim() !== "dark" || rule.body === null) break;
1094
+ const variant = /variant\s*:\s*([^;}]+)/.exec(rule.body)?.[1]?.trim();
1095
+ if (variant === void 0) break;
1096
+ consumed.push([rule.start, rule.end]);
1097
+ darkVariant = variant.startsWith("selector") ? "selector" : "media";
1098
+ break;
1099
+ }
1100
+ case "theme": {
1101
+ consumed.push([rule.start, rule.end]);
1102
+ if (rule.body === null) break;
1103
+ if (/\binline\b/.test(rule.prelude)) {
1104
+ manual.push({
1105
+ found: `@theme ${rule.prelude}`,
1106
+ reason: "`@theme inline` emits resolved values rather than variable references.",
1107
+ action: "Rainbow Index emits `var()` references so a token follows the cascade. Check any place you relied on the inlined value."
1108
+ });
1109
+ }
1110
+ translateThemeBody(rule.body, bucket, lineHeights, manual, keyframes);
1111
+ break;
1112
+ }
1113
+ case "keyframes": {
1114
+ const name = rule.prelude.trim();
1115
+ if (name && rule.body !== null) {
1116
+ keyframes.set(name, rule.body.trim());
1117
+ consumed.push([rule.start, rule.end]);
1118
+ }
1119
+ break;
1120
+ }
1121
+ case "custom-variant": {
1122
+ consumed.push([rule.start, rule.end]);
1123
+ const name = rule.prelude.split(/[\s(]/)[0]?.trim() ?? "";
1124
+ const rest = rule.prelude.slice(name.length).trim();
1125
+ if (name === "dark") {
1126
+ darkVariant = /prefers-color-scheme/.test(rest) ? "media" : "selector";
1127
+ break;
1128
+ }
1129
+ const body = rule.body === null ? rest : `{
1130
+ ${rule.body.trim()}
1131
+ }`;
1132
+ bucketCustom(buckets, `@custom ${name} ${body}${rule.body === null ? ";" : ""}`);
1133
+ break;
1134
+ }
1135
+ case "plugin":
1136
+ case "config":
1137
+ case "reference": {
1138
+ consumed.push([rule.start, rule.end]);
1139
+ manual.push({
1140
+ found: `@${rule.name} ${rule.prelude}`.trim(),
1141
+ reason: `\`@${rule.name}\` has no Rainbow Index equivalent.`,
1142
+ action: rule.name === "plugin" ? "Reimplement what the plugin added with `@utility` and `@custom`." : rule.name === "config" ? "A v3 JavaScript config is not read. Move the values into directives." : "Remove it; Rainbow Index resolves the theme from the entry itself."
1143
+ });
1144
+ break;
1145
+ }
1146
+ case "variant": {
1147
+ manual.push({
1148
+ found: `@variant ${rule.prelude}`,
1149
+ reason: "`@variant` applies a variant to a rule body; there is no equivalent.",
1150
+ action: "Write the variant on the class, or express it as a nested selector."
1151
+ });
1152
+ break;
1153
+ }
1154
+ default:
1155
+ break;
1156
+ }
1157
+ }
1158
+ const textBucket = buckets.get("text");
1159
+ if (textBucket) {
1160
+ for (const [name, leading] of lineHeights) {
1161
+ const size = textBucket.get(name);
1162
+ if (size !== void 0) textBucket.set(name, `${size}, ${leading}`);
1163
+ }
1164
+ }
1165
+ const animateBucket = buckets.get("animate");
1166
+ if (animateBucket) {
1167
+ for (const [name, shorthand] of [...animateBucket]) {
1168
+ const frames = keyframes.get(firstWord(shorthand)) ?? keyframes.get(name);
1169
+ if (frames === void 0) {
1170
+ animateBucket.delete(name);
1171
+ manual.push({
1172
+ found: `--animate-${name}: ${shorthand}`,
1173
+ reason: "No matching `@keyframes` was found in this file.",
1174
+ action: "Add the keyframes to an `@animate` entry, or drop the token."
1175
+ });
1176
+ continue;
1177
+ }
1178
+ animateBucket.set(
1179
+ name,
1180
+ `{
1181
+ animation: ${shorthand};
1182
+ @keyframes ${name} {
1183
+ ${reindent(frames, 3)}
1184
+ }
1185
+ }`
1186
+ );
1187
+ }
1188
+ if (animateBucket.size === 0) buckets.delete("animate");
1189
+ }
1190
+ return {
1191
+ css: assemble(css, consumed, buckets, darkVariant),
1192
+ translated: [...buckets].map(([directive, entries]) => ({
1193
+ directive: `@${directive}`,
1194
+ entries: entries.size
1195
+ })),
1196
+ manual,
1197
+ darkVariant
1198
+ };
1199
+ }
1200
+ function bucketCustom(buckets, statement) {
1201
+ let existing = buckets.get("__custom");
1202
+ if (existing === void 0) {
1203
+ existing = /* @__PURE__ */ new Map();
1204
+ buckets.set("__custom", existing);
1205
+ }
1206
+ existing.set(statement, "");
1207
+ }
1208
+ function firstWord(value) {
1209
+ return value.trim().split(/\s+/)[0] ?? "";
1210
+ }
1211
+ function readThemeBody(body) {
1212
+ const keyframes = /* @__PURE__ */ new Map();
1213
+ let rest = "";
1214
+ let at = 0;
1215
+ for (const rule of scanAtRules(body)) {
1216
+ rest += body.slice(at, rule.start);
1217
+ at = rule.end;
1218
+ if (rule.name === "keyframes" && rule.body !== null) {
1219
+ keyframes.set(rule.prelude.trim(), rule.body.trim());
1220
+ }
1221
+ }
1222
+ rest += body.slice(at);
1223
+ const entries = [];
1224
+ for (const statement of splitTopLevel(rest, ";")) {
1225
+ const text = statement.trim();
1226
+ if (!text) continue;
1227
+ const colon = text.indexOf(":");
1228
+ if (colon === -1) continue;
1229
+ const key2 = text.slice(0, colon).trim();
1230
+ const value = text.slice(colon + 1).trim().replace(/\s+/g, " ");
1231
+ if (key2 && value) entries.push([key2, value]);
1232
+ }
1233
+ return { entries, keyframes };
1234
+ }
1235
+ function splitTopLevel(input, char) {
1236
+ const out = [];
1237
+ let depth = 0;
1238
+ let start = 0;
1239
+ for (let i = 0; i < input.length; i++) {
1240
+ const ch = input[i];
1241
+ if (ch === '"' || ch === "'") {
1242
+ const quote = ch;
1243
+ i++;
1244
+ while (i < input.length && input[i] !== quote) {
1245
+ if (input[i] === "\\") i++;
1246
+ i++;
1247
+ }
1248
+ continue;
1249
+ }
1250
+ if (ch === "(" || ch === "[" || ch === "{") depth++;
1251
+ else if (ch === ")" || ch === "]" || ch === "}") depth = Math.max(0, depth - 1);
1252
+ else if (ch === char && depth === 0) {
1253
+ out.push(input.slice(start, i));
1254
+ start = i + 1;
1255
+ }
1256
+ }
1257
+ out.push(input.slice(start));
1258
+ return out;
1259
+ }
1260
+ function translateThemeBody(body, bucket, lineHeights, manual, keyframes) {
1261
+ const parsed = readThemeBody(body);
1262
+ for (const [name, frames] of parsed.keyframes) keyframes.set(name, frames);
1263
+ for (const [key2, value] of parsed.entries) {
1264
+ if (key2 === "--spacing") {
1265
+ bucket("spacing").set("base", value);
1266
+ continue;
1267
+ }
1268
+ if (key2.endsWith("-*") || value === "initial") continue;
1269
+ if (key2.endsWith("--line-height")) {
1270
+ const size = key2.slice("--text-".length, -"--line-height".length);
1271
+ if (key2.startsWith("--text-")) lineHeights.set(size, value);
1272
+ continue;
1273
+ }
1274
+ const modifier = /--(?:font-weight|letter-spacing|tracking)$/.exec(key2);
1275
+ if (modifier) {
1276
+ manual.push({
1277
+ found: `${key2}: ${value}`,
1278
+ reason: "A per-size weight or tracking modifier has no `@text` equivalent.",
1279
+ action: "Set it on the element, or define an `@utility`."
1280
+ });
1281
+ continue;
1282
+ }
1283
+ const unsupported = UNSUPPORTED_NAMESPACES.find(
1284
+ (n) => key2.startsWith(n.prefix) || key2 === n.prefix.slice(0, -1)
1285
+ );
1286
+ if (unsupported) {
1287
+ manual.push({
1288
+ found: `${key2}: ${value}`,
1289
+ reason: `The \`${unsupported.prefix}*\` namespace has no counterpart.`,
1290
+ action: unsupported.action
1291
+ });
1292
+ continue;
1293
+ }
1294
+ const bare = NAMESPACES.find((n) => key2 === n.prefix.slice(0, -1));
1295
+ if (bare) {
1296
+ bucket(bare.directive).set("DEFAULT", value);
1297
+ continue;
1298
+ }
1299
+ const namespace = NAMESPACES.find((n) => key2.startsWith(n.prefix));
1300
+ if (!namespace) {
1301
+ bucket("__root").set(key2, value);
1302
+ continue;
1303
+ }
1304
+ bucket(namespace.directive).set(tokenName(key2, namespace.prefix), value);
1305
+ }
1306
+ }
1307
+ function assemble(css, consumed, buckets, darkVariant) {
1308
+ const parts = [
1309
+ '@import "rainbowindex";',
1310
+ "/* Tailwind's default scales, so the familiar class names keep resolving.",
1311
+ " Drop this line and only what you declare below exists. */",
1312
+ '@import "rainbowindex/tailwind.css";',
1313
+ ""
1314
+ ];
1315
+ if (darkVariant === "selector") {
1316
+ parts.push("/* `dark:` follows the .dark class, as it did under Tailwind. */");
1317
+ parts.push("@color dark {\n variant: selector(.dark);\n}");
1318
+ parts.push("");
1319
+ }
1320
+ const root = buckets.get("__root");
1321
+ const customs = buckets.get("__custom");
1322
+ for (const [directive, entries] of buckets) {
1323
+ if (directive === "__root" || directive === "__custom") continue;
1324
+ if (entries.size === 0) continue;
1325
+ parts.push(emitBlock(directive, entries));
1326
+ }
1327
+ if (customs) {
1328
+ for (const statement of customs.keys()) parts.push(statement);
1329
+ parts.push("");
1330
+ }
1331
+ if (root && root.size > 0) {
1332
+ parts.push(`:root {
1333
+ ${[...root].map(([k, v]) => ` ${k}: ${v};`).join("\n")}
1334
+ }
1335
+ `);
1336
+ }
1337
+ consumed.sort((a, b) => a[0] - b[0]);
1338
+ let rest = "";
1339
+ let at = 0;
1340
+ for (const [start, end] of consumed) {
1341
+ if (start < at) continue;
1342
+ rest += css.slice(at, start);
1343
+ at = end;
1344
+ }
1345
+ rest += css.slice(at);
1346
+ const tail = rest.trim();
1347
+ if (tail) parts.push(tail, "");
1348
+ return `${parts.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd()}
1349
+ `;
1350
+ }
1351
+
1352
+ // src/cli/migrate.ts
1353
+ var ENTRY_CANDIDATES = [
1354
+ "src/index.css",
1355
+ "src/style.css",
1356
+ "src/styles.css",
1357
+ "src/app.css",
1358
+ "src/global.css",
1359
+ "src/globals.css",
1360
+ "app/globals.css",
1361
+ "styles/globals.css",
1362
+ "index.css",
1363
+ "style.css",
1364
+ "styles.css",
1365
+ "app.css",
1366
+ "global.css"
1367
+ ];
1368
+ function readJSON(path) {
1369
+ try {
1370
+ return JSON.parse(readFileSync(path, "utf8"));
1371
+ } catch {
1372
+ return void 0;
1373
+ }
1374
+ }
1375
+ function findEntry(cwd, explicit) {
1376
+ if (explicit) {
1377
+ const path = resolve6(cwd, explicit);
1378
+ return readFileSync(path, "utf8") ? path : null;
1379
+ }
1380
+ for (const candidate of ENTRY_CANDIDATES) {
1381
+ const path = resolve6(cwd, candidate);
1382
+ let css;
1383
+ try {
1384
+ css = readFileSync(path, "utf8");
1385
+ } catch {
1386
+ continue;
1387
+ }
1388
+ if (detectTailwind({ css }).isTailwind) return path;
1389
+ }
1390
+ return null;
1391
+ }
1392
+ async function scanAuthoredClasses(globs, cwd) {
1393
+ if (globs.length === 0) return { classes: /* @__PURE__ */ new Set(), files: [] };
1394
+ const { files } = await resolveSourceFilesAsync(
1395
+ globs.map((pattern) => ({ pattern, negated: false, inline: false, absolute: false })),
1396
+ cwd
1397
+ );
1398
+ const classes = /* @__PURE__ */ new Set();
1399
+ for (const file of files) {
1400
+ let content;
1401
+ try {
1402
+ content = await readFile3(file, "utf8");
1403
+ } catch {
1404
+ continue;
1405
+ }
1406
+ for (const candidate of outermostCandidates(extractClassCandidates({ content, path: file }))) {
1407
+ if (AUTHORED_ORIGINS.has(candidate.origin)) classes.add(candidate.value);
1408
+ }
1409
+ }
1410
+ return { classes, files };
1411
+ }
1412
+ var AUTHORED_ORIGINS = /* @__PURE__ */ new Set(["attribute", "helper", "safelist"]);
1413
+ function bullet(step) {
1414
+ return `- \`${step.found}\`
1415
+ - ${step.reason}${step.action ? `
1416
+ - ${step.action}` : ""}`;
1417
+ }
1418
+ async function migrateProject(opts, cwd) {
1419
+ const source = opts.migrateSource ?? "tailwind";
1420
+ if (source !== "tailwind") {
1421
+ return {
1422
+ code: 1,
1423
+ summary: `Unknown migration source "${source}". The only one is \`tailwind\`.`,
1424
+ written: []
1425
+ };
1426
+ }
1427
+ const entry = findEntry(cwd, opts.cssFile);
1428
+ if (entry === null) {
1429
+ return {
1430
+ code: 1,
1431
+ summary: `No Tailwind CSS entry found. Looked for a stylesheet with \`@import "tailwindcss"\` or \`@theme\` at: ${ENTRY_CANDIDATES.join(", ")}.
1432
+ Point at it with --css <file>.`,
1433
+ written: []
1434
+ };
1435
+ }
1436
+ const original = readFileSync(entry, "utf8");
1437
+ const detection = detectTailwind({
1438
+ css: original,
1439
+ packageJson: readJSON(join2(cwd, "package.json")),
1440
+ files: readdirSync(cwd, { withFileTypes: true }).filter((e) => e.isFile()).map((e) => e.name)
1441
+ });
1442
+ if (!detection.isTailwind) {
1443
+ return {
1444
+ code: 1,
1445
+ summary: `${relative4(cwd, entry)} does not look like a Tailwind v4 entry \u2014 no \`@import "tailwindcss"\`, no \`@theme\`, and no Tailwind package.`,
1446
+ written: []
1447
+ };
1448
+ }
1449
+ const result = migrateTailwindCSS(original);
1450
+ const inlined = inlineDirectiveImports(result.css, {
1451
+ resolve: createNodeImportResolver({ cwd }),
1452
+ from: entry
1453
+ });
1454
+ const presetResolved = inlined.css !== result.css;
1455
+ const analysis = analyzeProjectCSS(inlined.css);
1456
+ const inspector = createClassInspector(analysis.theme);
1457
+ const { classes: authored, files } = await scanAuthoredClasses(opts.globs, cwd);
1458
+ const rejected = /* @__PURE__ */ new Map();
1459
+ if (presetResolved) {
1460
+ for (const className of authored) {
1461
+ const validation = inspector.validate(className);
1462
+ if (validation.ok) continue;
1463
+ const bucket = rejected.get(validation.reason) ?? [];
1464
+ bucket.push(className);
1465
+ rejected.set(validation.reason, bucket);
1466
+ }
1467
+ }
1468
+ const outPath = join2(dirname2(entry), `${basename(entry, ".css")}.rainbowindex.css`);
1469
+ const reportPath = join2(cwd, "migration-report.md");
1470
+ const report = buildReport({
1471
+ entry: relative4(cwd, entry),
1472
+ detection,
1473
+ result,
1474
+ rejected,
1475
+ scanned: authored.size,
1476
+ files: files.length,
1477
+ presetResolved
1478
+ });
1479
+ const written = [];
1480
+ if (opts.write) {
1481
+ await writeFileAtomic(`${entry}.tailwind.bak`, original);
1482
+ await writeFileAtomic(entry, result.css);
1483
+ written.push(relative4(cwd, `${entry}.tailwind.bak`), relative4(cwd, entry));
1484
+ } else {
1485
+ await writeFileAtomic(outPath, result.css);
1486
+ written.push(relative4(cwd, outPath));
1487
+ }
1488
+ await writeFileAtomic(reportPath, report);
1489
+ written.push(relative4(cwd, reportPath));
1490
+ const tokens = result.translated.reduce((n, t) => n + t.entries, 0);
1491
+ const summary = [
1492
+ `Migrated ${relative4(cwd, entry)} \u2014 ${tokens} tokens across ${result.translated.length} directives.`,
1493
+ result.darkVariant === "selector" ? " dark: kept on the .dark class." : null,
1494
+ result.manual.length === 1 ? " 1 thing needs a person." : ` ${result.manual.length} things need a person.`,
1495
+ !presetResolved ? " Could not resolve rainbowindex/tailwind.css \u2014 install the package to have\n your classes checked against the migrated theme." : rejected.size > 0 ? ` ${[...rejected.values()].flat().length} of ${authored.size} classes in your source do not resolve.` : authored.size > 0 ? ` All ${authored.size} classes in your source resolve.` : null,
1496
+ "",
1497
+ ...written.map((file) => ` wrote ${file}`),
1498
+ "",
1499
+ opts.write ? "Applied. The original is beside it as *.css.tailwind.bak." : "Nothing was overwritten. Re-run with --write to apply."
1500
+ ].filter((line) => line !== null).join("\n");
1501
+ return { code: 0, summary, written };
1502
+ }
1503
+ function buildReport(input) {
1504
+ const { entry, detection, result, rejected, scanned } = input;
1505
+ const lines = [
1506
+ "# Tailwind \u2192 Rainbow Index",
1507
+ "",
1508
+ `Entry: \`${entry}\``,
1509
+ `Detected by: ${detection.signals.map((s) => `\`${s}\``).join(", ")}`,
1510
+ ""
1511
+ ];
1512
+ if (detection.legacyConfig) {
1513
+ lines.push(
1514
+ "> [!WARNING]",
1515
+ `> \`${detection.legacyConfig}\` is a Tailwind v3 JavaScript config. It was **not** read.`,
1516
+ "> Anything defined only there has to be moved into the CSS entry by hand.",
1517
+ ""
1518
+ );
1519
+ }
1520
+ lines.push("## Translated", "", "| Directive | Tokens |", "| --- | --- |");
1521
+ for (const { directive, entries } of result.translated) {
1522
+ lines.push(`| \`${directive}\` | ${entries} |`);
1523
+ }
1524
+ lines.push("");
1525
+ if (result.darkVariant !== null) {
1526
+ lines.push(
1527
+ "## Dark mode",
1528
+ "",
1529
+ result.darkVariant === "selector" ? "Your `dark` custom variant used a class, so the theme now says\n`@color dark { variant: selector(.dark); }` \u2014 `dark:` and the colour tokens\nboth follow `.dark`, as they did." : "Your `dark` custom variant used `prefers-color-scheme`, which is already the\ndefault here. Nothing was added.",
1530
+ ""
1531
+ );
1532
+ }
1533
+ lines.push("## Needs a person", "");
1534
+ if (result.manual.length === 0) {
1535
+ lines.push("Nothing. Every construct in the entry had a counterpart.", "");
1536
+ } else {
1537
+ lines.push(...result.manual.map(bullet), "");
1538
+ }
1539
+ lines.push("## Classes in your source", "");
1540
+ if (!input.presetResolved) {
1541
+ lines.push(
1542
+ "> [!NOTE]",
1543
+ "> `rainbowindex/tailwind.css` could not be resolved, so this check was",
1544
+ "> skipped. Install the package (`pnpm add -D rainbowindex`) and re-run \u2014",
1545
+ "> without the preset every class from Tailwind's own scales would read as",
1546
+ "> unresolved, which would be alarming and wrong.",
1547
+ ""
1548
+ );
1549
+ } else if (scanned === 0) {
1550
+ lines.push(
1551
+ 'No source files were scanned. Pass globs \u2014 `rainbowindex migrate tailwind "src/**/*.tsx"`',
1552
+ "\u2014 to have every class checked against the migrated theme.",
1553
+ ""
1554
+ );
1555
+ } else if (rejected.size === 0) {
1556
+ lines.push(`All ${scanned} classes found in your source resolve against the new theme.`, "");
1557
+ } else {
1558
+ const total = [...rejected.values()].flat().length;
1559
+ lines.push(
1560
+ `${total} of ${scanned} classes across ${input.files} file${input.files === 1 ? "" : "s"} do not resolve. Grouped by why:`,
1561
+ "",
1562
+ ...[...rejected].map(
1563
+ ([reason, classes]) => `### ${reason} (${classes.length})
1564
+
1565
+ ${classes.slice(0, 40).map((c) => `- \`${c}\``).join("\n")}${classes.length > 40 ? `
1566
+ - \u2026and ${classes.length - 40} more` : ""}`
1567
+ ),
1568
+ ""
1569
+ );
1570
+ }
1571
+ lines.push(
1572
+ "## Differences to expect",
1573
+ "",
1574
+ "These are by design, not migration defects:",
1575
+ "",
1576
+ "- **Directional utilities emit logical properties.** `pl-4` is",
1577
+ " `padding-inline-start`, not `padding-left`. Identical in LTR; mirrored in RTL.",
1578
+ "- **`opacity-50` emits `50%`**, where Tailwind emits `0.5`. Same rendering.",
1579
+ "- **Colour tokens are one `light-dark()` declaration**, not a `:root` block plus",
1580
+ " a `.dark` block. See [theming.md](https://rainbowindex.dev/theming).",
1581
+ "- **Only what you name exists.** The preset import restores Tailwind's scales;",
1582
+ " drop it and the theme is exactly your own directives.",
1583
+ ""
1584
+ );
1585
+ return `${lines.join("\n")}
1586
+ `;
1587
+ }
1588
+
620
1589
  // src/cli/preload-fonts.ts
621
1590
  async function preloadFonts(opts, cwd) {
622
1591
  const theme = await loadProjectTheme(opts, cwd);
@@ -635,34 +1604,7 @@ async function preloadFonts(opts, cwd) {
635
1604
 
636
1605
  // src/cli/watch.ts
637
1606
  import { mkdir } from "fs/promises";
638
- import { resolve as resolve4, dirname as dirname2 } from "path";
639
-
640
- // src/cli/atomic-write.ts
641
- import { writeFile as writeFile2, rename, unlink } from "fs/promises";
642
- import { dirname, join } from "path";
643
- function isIgnorableCleanupError(err) {
644
- return !!err && typeof err === "object" && "code" in err && (err.code === "ENOENT" || err.code === "EPERM");
645
- }
646
- async function writeFileAtomic(filePath, content) {
647
- const dir = dirname(filePath);
648
- const tmp = join(dir, `.ri-tmp-${process.pid}-${Date.now()}`);
649
- try {
650
- await writeFile2(tmp, content);
651
- await rename(tmp, filePath);
652
- } catch (err) {
653
- try {
654
- await unlink(tmp);
655
- } catch (cleanupErr) {
656
- if (!isIgnorableCleanupError(cleanupErr)) {
657
- const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
658
- devWarn(`[RI-DEV] Failed to remove temp file "${tmp}": ${msg}`);
659
- }
660
- }
661
- throw err;
662
- }
663
- }
664
-
665
- // src/cli/watch.ts
1607
+ import { resolve as resolve7, dirname as dirname3 } from "path";
666
1608
  async function minifyIfRequested(css, opts) {
667
1609
  if (!opts.minify) return css;
668
1610
  const { optimizeCSS } = await import("./optimize-6NWJVT6W.mjs");
@@ -671,8 +1613,8 @@ async function minifyIfRequested(css, opts) {
671
1613
  async function buildAndWrite(opts, cwd, outputFile) {
672
1614
  const result = await buildCSS(opts, cwd);
673
1615
  const css = await minifyIfRequested(result.css, opts);
674
- const outputPath = resolve4(cwd, outputFile);
675
- await mkdir(dirname2(outputPath), { recursive: true });
1616
+ const outputPath = resolve7(cwd, outputFile);
1617
+ await mkdir(dirname3(outputPath), { recursive: true });
676
1618
  await writeFileAtomic(outputPath, css);
677
1619
  return result;
678
1620
  }
@@ -819,8 +1761,8 @@ async function watchMode(opts, cwd) {
819
1761
  }
820
1762
 
821
1763
  // src/cli/scan.ts
822
- import { readFile as readFile3 } from "fs/promises";
823
- import { relative as relative2, resolve as resolve5 } from "path";
1764
+ import { readFile as readFile4 } from "fs/promises";
1765
+ import { relative as relative5, resolve as resolve8 } from "path";
824
1766
  import { glob } from "tinyglobby";
825
1767
  async function scanFiles(opts, cwd) {
826
1768
  const files = [...new Set(await glob(opts.globs, { cwd }))].sort(codepointCompare);
@@ -828,15 +1770,15 @@ async function scanFiles(opts, cwd) {
828
1770
  throw new Error(`No files matched ${opts.globs.map((g) => `"${g}"`).join(", ")}.`);
829
1771
  }
830
1772
  for (const file of files) {
831
- const path = resolve5(cwd, file);
1773
+ const path = resolve8(cwd, file);
832
1774
  const warnings = [];
833
1775
  const classes = extractClassesFromSource(
834
- { path, content: await readFile3(path, "utf-8") },
1776
+ { path, content: await readFile4(path, "utf-8") },
835
1777
  warnings
836
1778
  );
837
1779
  const sorted = [...classes].sort(codepointCompare);
838
1780
  console.log(
839
- `${relative2(cwd, path)} (${sorted.length} class${sorted.length === 1 ? "" : "es"})`
1781
+ `${relative5(cwd, path)} (${sorted.length} class${sorted.length === 1 ? "" : "es"})`
840
1782
  );
841
1783
  for (const cls of sorted) {
842
1784
  console.log(` ${cls}`);
@@ -849,9 +1791,9 @@ async function scanFiles(opts, cwd) {
849
1791
 
850
1792
  // src/cli/vite-setup.ts
851
1793
  import { spawn } from "child_process";
852
- import { existsSync as existsSync3, readFileSync } from "fs";
853
- import { mkdir as mkdir2, readFile as readFile4, readdir, writeFile as writeFile3 } from "fs/promises";
854
- import { dirname as dirname3, relative as relative3, resolve as resolve6 } from "path";
1794
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
1795
+ import { mkdir as mkdir2, readFile as readFile5, readdir, writeFile as writeFile3 } from "fs/promises";
1796
+ import { dirname as dirname4, relative as relative6, resolve as resolve9 } from "path";
855
1797
  var VITE_CONFIG_FILES = [
856
1798
  "vite.config.ts",
857
1799
  "vite.config.mts",
@@ -914,32 +1856,32 @@ async function initViteProject(opts, cwd, deps = {}) {
914
1856
  ensureStylesheet(cssPath)
915
1857
  ]);
916
1858
  const entryChanged = cssCreated ? await ensureStylesheetImport(entryFile, cssPath) : false;
917
- const rootLabel = relative3(process.cwd(), cwd) || ".";
1859
+ const rootLabel = relative6(process.cwd(), cwd) || ".";
918
1860
  console.log(`[rainbowindex] Initialized Vite project: ${rootLabel}`);
919
1861
  if (dependencyInstalled) {
920
1862
  console.log("[rainbowindex] Added dev dependency: rainbowindex");
921
1863
  }
922
1864
  if (configCreated) {
923
1865
  console.log(
924
- `[rainbowindex] Created Vite config: ${relative3(cwd, configPath).replaceAll("\\", "/")}`
1866
+ `[rainbowindex] Created Vite config: ${relative6(cwd, configPath).replaceAll("\\", "/")}`
925
1867
  );
926
1868
  } else if (configChanged) {
927
1869
  console.log(
928
- `[rainbowindex] Updated Vite config: ${relative3(cwd, configPath).replaceAll("\\", "/")}`
1870
+ `[rainbowindex] Updated Vite config: ${relative6(cwd, configPath).replaceAll("\\", "/")}`
929
1871
  );
930
1872
  }
931
1873
  if (cssCreated) {
932
1874
  console.log(
933
- `[rainbowindex] Created stylesheet: ${relative3(cwd, cssPath).replaceAll("\\", "/")}`
1875
+ `[rainbowindex] Created stylesheet: ${relative6(cwd, cssPath).replaceAll("\\", "/")}`
934
1876
  );
935
1877
  } else if (cssChanged) {
936
1878
  console.log(
937
- `[rainbowindex] Updated stylesheet: ${relative3(cwd, cssPath).replaceAll("\\", "/")}`
1879
+ `[rainbowindex] Updated stylesheet: ${relative6(cwd, cssPath).replaceAll("\\", "/")}`
938
1880
  );
939
1881
  }
940
1882
  if (entryChanged && entryFile) {
941
1883
  console.log(
942
- `[rainbowindex] Added stylesheet import: ${relative3(cwd, entryFile).replaceAll("\\", "/")}`
1884
+ `[rainbowindex] Added stylesheet import: ${relative6(cwd, entryFile).replaceAll("\\", "/")}`
943
1885
  );
944
1886
  }
945
1887
  if (!dependencyInstalled && !configChanged && !configCreated && !cssChanged && !cssCreated) {
@@ -963,7 +1905,7 @@ async function createViteProject(opts, cwd, deps = {}) {
963
1905
  const packageManager = deps.packageManager ?? detectPackageManager(cwd, deps.env);
964
1906
  const runner = deps.runner ?? DEFAULT_RUNNER;
965
1907
  const targetDir = opts.targetDir;
966
- const targetRoot = resolve6(cwd, targetDir);
1908
+ const targetRoot = resolve9(cwd, targetDir);
967
1909
  await ensureScaffoldTargetIsUsable(targetRoot);
968
1910
  const template = opts.template || DEFAULT_CREATE_TEMPLATE;
969
1911
  const scaffold = getCreateCommand(packageManager, targetDir, template);
@@ -974,15 +1916,15 @@ async function createViteProject(opts, cwd, deps = {}) {
974
1916
  packageManager,
975
1917
  runner
976
1918
  });
977
- const displayTarget = relative3(cwd, targetRoot) || ".";
1919
+ const displayTarget = relative6(cwd, targetRoot) || ".";
978
1920
  console.log(`[rainbowindex] Ready: ${displayTarget}`);
979
1921
  console.log(`[rainbowindex] Next: cd ${displayTarget} && ${packageManager} run dev`);
980
1922
  }
981
1923
  function detectPackageManager(cwd, env = process.env) {
982
- if (existsSync3(resolve6(cwd, "pnpm-lock.yaml"))) return "pnpm";
983
- if (existsSync3(resolve6(cwd, "yarn.lock"))) return "yarn";
984
- if (existsSync3(resolve6(cwd, "bun.lock")) || existsSync3(resolve6(cwd, "bun.lockb"))) return "bun";
985
- if (existsSync3(resolve6(cwd, "package-lock.json")) || existsSync3(resolve6(cwd, "npm-shrinkwrap.json"))) {
1924
+ if (existsSync3(resolve9(cwd, "pnpm-lock.yaml"))) return "pnpm";
1925
+ if (existsSync3(resolve9(cwd, "yarn.lock"))) return "yarn";
1926
+ if (existsSync3(resolve9(cwd, "bun.lock")) || existsSync3(resolve9(cwd, "bun.lockb"))) return "bun";
1927
+ if (existsSync3(resolve9(cwd, "package-lock.json")) || existsSync3(resolve9(cwd, "npm-shrinkwrap.json"))) {
986
1928
  return "npm";
987
1929
  }
988
1930
  const packageJSON = readPackageJSONSync(cwd);
@@ -1027,12 +1969,12 @@ async function installRainbowIndex(cwd, packageManager, runner) {
1027
1969
  async function ensureViteConfig(cwd, packageJSON, existingPath) {
1028
1970
  if (!existingPath) {
1029
1971
  const fileName = chooseViteConfigName(cwd, packageJSON);
1030
- const configPath = resolve6(cwd, fileName);
1972
+ const configPath = resolve9(cwd, fileName);
1031
1973
  const content = 'import { defineConfig } from "vite";\nimport rainbowindex from "rainbowindex/vite";\n\nexport default defineConfig({\n plugins: [rainbowindex()],\n});\n';
1032
1974
  await writeFile3(configPath, content, "utf-8");
1033
1975
  return { path: configPath, changed: true, created: true };
1034
1976
  }
1035
- const original = await readFile4(existingPath, "utf-8");
1977
+ const original = await readFile5(existingPath, "utf-8");
1036
1978
  const updated = patchViteConfig(original, existingPath);
1037
1979
  if (updated === original) {
1038
1980
  return { path: existingPath, changed: false, created: false };
@@ -1161,22 +2103,22 @@ function findMatchingDelimiter(input, start, open, close) {
1161
2103
  return -1;
1162
2104
  }
1163
2105
  async function resolveStylesheetPath(cwd, cssFile) {
1164
- if (cssFile) return resolve6(cwd, cssFile);
2106
+ if (cssFile) return resolve9(cwd, cssFile);
1165
2107
  const importedCSS = await findImportedStylesheet(cwd);
1166
2108
  if (importedCSS) return importedCSS;
1167
2109
  for (const candidate of CSS_ENTRY_CANDIDATES) {
1168
- const fullPath = resolve6(cwd, candidate);
2110
+ const fullPath = resolve9(cwd, candidate);
1169
2111
  if (existsSync3(fullPath)) return fullPath;
1170
2112
  }
1171
- return existsSync3(resolve6(cwd, "src")) ? resolve6(cwd, "src/index.css") : resolve6(cwd, "index.css");
2113
+ return existsSync3(resolve9(cwd, "src")) ? resolve9(cwd, "src/index.css") : resolve9(cwd, "index.css");
1172
2114
  }
1173
2115
  async function ensureStylesheet(cssPath) {
1174
2116
  if (!existsSync3(cssPath)) {
1175
- await mkdir2(dirname3(cssPath), { recursive: true });
2117
+ await mkdir2(dirname4(cssPath), { recursive: true });
1176
2118
  await writeFile3(cssPath, '@import "rainbowindex";\n', "utf-8");
1177
2119
  return { changed: true, created: true };
1178
2120
  }
1179
- const original = await readFile4(cssPath, "utf-8");
2121
+ const original = await readFile5(cssPath, "utf-8");
1180
2122
  if (/@import\s+["']rainbowindex["']/.test(original)) {
1181
2123
  return { changed: false, created: false };
1182
2124
  }
@@ -1203,8 +2145,8 @@ ${content}`;
1203
2145
  }
1204
2146
  async function ensureStylesheetImport(entryFile, cssPath) {
1205
2147
  if (!entryFile || !existsSync3(entryFile)) return false;
1206
- const original = await readFile4(entryFile, "utf-8");
1207
- const relativePath = toImportPath(dirname3(entryFile), cssPath);
2148
+ const original = await readFile5(entryFile, "utf-8");
2149
+ const relativePath = toImportPath(dirname4(entryFile), cssPath);
1208
2150
  if (hasStylesheetImport(original, relativePath)) {
1209
2151
  return false;
1210
2152
  }
@@ -1214,16 +2156,16 @@ async function ensureStylesheetImport(entryFile, cssPath) {
1214
2156
  }
1215
2157
  async function findImportedStylesheet(cwd) {
1216
2158
  for (const entryFile of ENTRY_FILES) {
1217
- const fullPath = resolve6(cwd, entryFile);
2159
+ const fullPath = resolve9(cwd, entryFile);
1218
2160
  if (!existsSync3(fullPath)) continue;
1219
- const content = await readFile4(fullPath, "utf-8");
2161
+ const content = await readFile5(fullPath, "utf-8");
1220
2162
  const matches = content.matchAll(
1221
2163
  /import\s+(?:.+?\s+from\s+)?["']([^"']+\.css(?:\?[^"']*)?)["']/g
1222
2164
  );
1223
2165
  for (const match of matches) {
1224
2166
  const importPath = match[1].split("?")[0];
1225
2167
  if (!importPath.startsWith(".")) continue;
1226
- const stylesheet = resolve6(dirname3(fullPath), importPath);
2168
+ const stylesheet = resolve9(dirname4(fullPath), importPath);
1227
2169
  if (existsSync3(stylesheet)) return stylesheet;
1228
2170
  }
1229
2171
  }
@@ -1231,7 +2173,7 @@ async function findImportedStylesheet(cwd) {
1231
2173
  }
1232
2174
  async function findEntryFile(cwd) {
1233
2175
  for (const entryFile of ENTRY_FILES) {
1234
- const fullPath = resolve6(cwd, entryFile);
2176
+ const fullPath = resolve9(cwd, entryFile);
1235
2177
  if (existsSync3(fullPath)) return fullPath;
1236
2178
  }
1237
2179
  return null;
@@ -1241,18 +2183,18 @@ function hasStylesheetImport(source, importPath) {
1241
2183
  return new RegExp(`import\\s+(?:.+?\\s+from\\s+)?["']${escapedPath}["']`).test(source);
1242
2184
  }
1243
2185
  function toImportPath(fromDir, targetFile) {
1244
- const rel = relative3(fromDir, targetFile).replaceAll("\\", "/");
2186
+ const rel = relative6(fromDir, targetFile).replaceAll("\\", "/");
1245
2187
  return rel.startsWith(".") ? rel : `./${rel}`;
1246
2188
  }
1247
2189
  function findExistingViteConfig(cwd) {
1248
2190
  for (const name of VITE_CONFIG_FILES) {
1249
- const fullPath = resolve6(cwd, name);
2191
+ const fullPath = resolve9(cwd, name);
1250
2192
  if (existsSync3(fullPath)) return fullPath;
1251
2193
  }
1252
2194
  return null;
1253
2195
  }
1254
2196
  function chooseViteConfigName(cwd, packageJSON) {
1255
- if (existsSync3(resolve6(cwd, "tsconfig.json")) || existsSync3(resolve6(cwd, "tsconfig.app.json")) || existsSync3(resolve6(cwd, "src/main.ts")) || existsSync3(resolve6(cwd, "src/main.tsx")) || packageJSON?.dependencies?.typescript || packageJSON?.devDependencies?.typescript) {
2197
+ if (existsSync3(resolve9(cwd, "tsconfig.json")) || existsSync3(resolve9(cwd, "tsconfig.app.json")) || existsSync3(resolve9(cwd, "src/main.ts")) || existsSync3(resolve9(cwd, "src/main.tsx")) || packageJSON?.dependencies?.typescript || packageJSON?.devDependencies?.typescript) {
1256
2198
  return "vite.config.ts";
1257
2199
  }
1258
2200
  return packageJSON?.type === "module" ? "vite.config.js" : "vite.config.mjs";
@@ -1300,10 +2242,10 @@ function getInstallCommand(packageManager) {
1300
2242
  }
1301
2243
  }
1302
2244
  function readPackageJSONSync(cwd) {
1303
- const packagePath = resolve6(cwd, "package.json");
2245
+ const packagePath = resolve9(cwd, "package.json");
1304
2246
  if (!existsSync3(packagePath)) return null;
1305
2247
  try {
1306
- return JSON.parse(readFileSync(packagePath, "utf-8"));
2248
+ return JSON.parse(readFileSync2(packagePath, "utf-8"));
1307
2249
  } catch {
1308
2250
  return null;
1309
2251
  }
@@ -1311,7 +2253,7 @@ function readPackageJSONSync(cwd) {
1311
2253
 
1312
2254
  // src/entries/cli.ts
1313
2255
  function getVersion() {
1314
- return true ? "0.6.0" : "unknown";
2256
+ return true ? "0.7.0" : "unknown";
1315
2257
  }
1316
2258
  async function main() {
1317
2259
  const args = process.argv.slice(2);
@@ -1332,12 +2274,24 @@ async function main() {
1332
2274
  case "generate-types":
1333
2275
  await generateTypes(opts, cwd);
1334
2276
  break;
2277
+ case "generate-snapshot":
2278
+ await generateSnapshot(opts, cwd);
2279
+ break;
2280
+ case "generate-tokens":
2281
+ await generateTokens(opts, cwd);
2282
+ break;
1335
2283
  case "preload-fonts":
1336
2284
  await preloadFonts(opts, cwd);
1337
2285
  break;
1338
2286
  case "scan":
1339
2287
  await scanFiles(opts, cwd);
1340
2288
  break;
2289
+ case "migrate": {
2290
+ const result = await migrateProject(opts, cwd);
2291
+ (result.code === 0 ? console.log : console.error)(result.summary);
2292
+ if (result.code !== 0) process.exitCode = result.code;
2293
+ break;
2294
+ }
1341
2295
  case "build": {
1342
2296
  if (opts.watch) {
1343
2297
  await watchMode(opts, cwd);
@@ -1358,7 +2312,7 @@ var isDirectExecution = (() => {
1358
2312
  const entry = process.argv[1];
1359
2313
  if (entry === void 0) return false;
1360
2314
  try {
1361
- return import.meta.url === pathToFileURL(realpathSync(resolve7(entry))).href;
2315
+ return import.meta.url === pathToFileURL(realpathSync(resolve10(entry))).href;
1362
2316
  } catch {
1363
2317
  return false;
1364
2318
  }