rainbowindex 0.5.1 → 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 +876 -0
  2. package/LICENSE +16 -17
  3. package/NOTICE.md +106 -0
  4. package/README.md +225 -69
  5. package/dist/browser.d.ts +4 -2
  6. package/dist/browser.mjs +12 -6
  7. package/dist/chunk-2T7V5XLK.mjs +912 -0
  8. package/dist/chunk-6OORICWF.mjs +16 -0
  9. package/dist/{chunk-PDORZSQX.mjs → chunk-FJOZJIKB.mjs} +7814 -5174
  10. package/dist/chunk-L56IRO7A.mjs +491 -0
  11. package/dist/chunk-PZDVDEZJ.mjs +196 -0
  12. package/dist/{chunk-4CTJLMYM.mjs → chunk-RC6DDE4L.mjs} +37 -23
  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-4UKFK2GE.mjs → chunk-XQGSG2HK.mjs} +213 -756
  18. package/dist/cli.mjs +1101 -125
  19. package/dist/{context-B9yhJxd5.d.ts → context-DcBtnnan.d.ts} +55 -108
  20. package/dist/editor.d.ts +82 -421
  21. package/dist/editor.mjs +68 -363
  22. package/dist/eslint.d.ts +16 -0
  23. package/dist/eslint.mjs +32 -0
  24. package/dist/{index-Dx-NpFFx.d.ts → imports-C9esHd5Q.d.ts} +98 -81
  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 +49 -5
  29. package/dist/index.mjs +40 -13
  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 +273 -113
  40. package/package.json +27 -5
  41. package/dist/chunk-F4VCBISU.mjs +0 -1866
  42. package/dist/chunk-RU4756NG.mjs +0 -243
  43. package/dist/safelist-DAkKuxCk.d.ts +0 -96
package/dist/cli.mjs CHANGED
@@ -1,43 +1,82 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- CSS_ENTRY_CANDIDATES,
4
- enumerateClassNames
5
- } from "./chunk-RU4756NG.mjs";
3
+ snapshotFromCSS
4
+ } from "./chunk-6OORICWF.mjs";
6
5
  import {
7
6
  DEFAULT_EXCLUDES,
8
7
  DEFAULT_PATTERNS,
9
8
  compileScannedProject,
10
- getFontPreloadLinks
11
- } from "./chunk-F4VCBISU.mjs";
9
+ disableScanChangeTracking,
10
+ enableScanChangeTracking,
11
+ enableSourceFileListCache,
12
+ invalidateSourceFileListCache,
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";
12
28
  import {
13
29
  MAX_DIRECTIVE_INPUT_SIZE,
30
+ analyzeProjectCSS,
14
31
  codepointCompare,
32
+ extractClassCandidates,
15
33
  extractClassesFromSource,
16
34
  extractDirectives,
35
+ findClosingBrace,
36
+ getFontPreloadLinks,
17
37
  hasApplyLikeDirective,
18
38
  hasRIActivation,
39
+ inlineDirectiveImports,
19
40
  listVariants,
20
- resolveDirectives
21
- } from "./chunk-PDORZSQX.mjs";
41
+ resolveDirectives,
42
+ stripCSSComments
43
+ } from "./chunk-FJOZJIKB.mjs";
44
+ import "./chunk-L56IRO7A.mjs";
22
45
  import {
23
46
  devWarn
24
- } from "./chunk-4UKFK2GE.mjs";
47
+ } from "./chunk-XQGSG2HK.mjs";
25
48
 
26
49
  // src/entries/cli.ts
27
50
  import { realpathSync } from "fs";
28
- import { resolve as resolve7 } from "path";
51
+ import { resolve as resolve10 } from "path";
29
52
  import { pathToFileURL } from "url";
30
53
 
31
54
  // src/cli/args.ts
32
55
  import { relative, resolve } from "path";
33
- 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) => ({
34
72
  names: ["-o", "--output"],
35
73
  valueName: "<file>",
36
- describe: "Output CSS file path (omit to write to stdout)",
74
+ describe,
37
75
  apply(opts, value) {
38
76
  opts.output = value;
39
77
  }
40
- };
78
+ });
79
+ var OUTPUT = output("Output CSS file path (omit to write to stdout)");
41
80
  var WATCH = {
42
81
  names: ["--watch"],
43
82
  describe: "Re-run on source-file changes (requires --output)",
@@ -68,33 +107,14 @@ var TEMPLATE = {
68
107
  opts.template = value;
69
108
  }
70
109
  };
71
- var applyCSSFile = (opts, value) => {
72
- opts.cssFile = value;
73
- };
74
- var CSS_INPUT = {
75
- names: ["--css"],
76
- valueName: "<file>",
77
- describe: "CSS input with directives (auto-detected if omitted)",
78
- apply: applyCSSFile
79
- };
80
- var CSS_INPUT_TYPES = {
81
- names: ["--css"],
82
- valueName: "<file>",
83
- describe: "CSS input with @color/@text/@utility/etc directives",
84
- apply: applyCSSFile
85
- };
86
- var CSS_INPUT_FONTS = {
87
- names: ["--css"],
88
- valueName: "<file>",
89
- describe: "CSS input with @font directives",
90
- apply: applyCSSFile
91
- };
92
- var CSS_TARGET = {
93
- names: ["--css"],
94
- valueName: "<file>",
95
- describe: "Stylesheet path to create/patch (default: src/index.css)",
96
- apply: applyCSSFile
97
- };
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)");
98
118
  var COMMANDS = {
99
119
  build: {
100
120
  summary: "Generate CSS from source files",
@@ -142,6 +162,27 @@ Examples:
142
162
  rainbowindex create my-app
143
163
  rainbowindex create my-app --template vue-ts`
144
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
+ },
145
186
  "generate-types": {
146
187
  summary: "Generate TypeScript types for ri() autocomplete",
147
188
  usage: " rainbowindex generate-types [options]",
@@ -155,6 +196,57 @@ Examples:
155
196
  rainbowindex generate-types
156
197
  rainbowindex generate-types --strict --css src/styles.css`
157
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
+ },
158
250
  scan: {
159
251
  summary: "Show what the class scanner extracts (debugging)",
160
252
  usage: " rainbowindex scan <file|glob...>",
@@ -206,6 +298,7 @@ function parseArgs(argv, callbacks) {
206
298
  watch: false,
207
299
  minify: false,
208
300
  strict: false,
301
+ write: false,
209
302
  subcommandExplicit: false,
210
303
  earlyExit: false
211
304
  };
@@ -275,8 +368,18 @@ function parseArgs(argv, callbacks) {
275
368
  opts.targetDir = arg;
276
369
  continue;
277
370
  }
371
+ if (spec.positionals === "migrate") {
372
+ if (opts.migrateSource === void 0) opts.migrateSource = arg;
373
+ else opts.globs.push(arg);
374
+ continue;
375
+ }
278
376
  throw new Error(`Unexpected extra argument "${arg}" for ${opts.command}.`);
279
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
+ }
280
383
  if (opts.command === "create" && !opts.targetDir) {
281
384
  throw new Error("create requires a project directory. Example: rainbowindex create my-app");
282
385
  }
@@ -342,7 +445,10 @@ Usage:
342
445
  rainbowindex <glob> [options] Generate CSS from source files
343
446
  rainbowindex init Wire Rainbow Index into the current Vite app
344
447
  rainbowindex create <dir> Scaffold a Vite app with Rainbow Index ready
448
+ rainbowindex migrate tailwind Translate a Tailwind v4 project into directives
345
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
346
452
  rainbowindex preload-fonts Generate font preload link tags
347
453
  rainbowindex scan <file...> Show what the class scanner extracts
348
454
 
@@ -409,20 +515,32 @@ async function readCSSFileAsync(cssFile) {
409
515
  return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
410
516
  }
411
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
+ };
412
530
  if (opts.cssFile) {
413
531
  const cssFile2 = resolve2(cwd, opts.cssFile);
414
532
  if (!existsSync(cssFile2)) {
415
533
  throw new Error(`[RI-1605] CSS input file not found: ${cssFile2}`);
416
534
  }
417
- return { css: await readCSSFileAsync(cssFile2), cssFile: cssFile2 };
535
+ return load(cssFile2);
418
536
  }
419
537
  const cssFile = await findCSSFileAsync(cwd);
420
- if (!cssFile) return { css: "", cssFile: null };
421
- return { css: await readCSSFileAsync(cssFile), cssFile };
538
+ if (!cssFile) return { css: "", cssFile: null, imports: [], warnings: [] };
539
+ return load(cssFile);
422
540
  }
423
541
  async function loadProjectTheme(opts, cwd) {
424
- const { css } = await loadProjectCSS(opts, cwd);
425
- const parseWarnings = [];
542
+ const { css, warnings } = await loadProjectCSS(opts, cwd);
543
+ const parseWarnings = [...warnings];
426
544
  const directives = extractDirectives(css, parseWarnings);
427
545
  const theme = resolveDirectives(directives);
428
546
  for (const w of parseWarnings) console.error(w);
@@ -431,17 +549,21 @@ async function loadProjectTheme(opts, cwd) {
431
549
 
432
550
  // src/cli/build.ts
433
551
  async function buildCSS(opts, cwd) {
434
- const { css: cssSource, cssFile } = await loadProjectCSS(opts, cwd);
552
+ const { css: cssSource, cssFile, warnings: importWarnings } = await loadProjectCSS(opts, cwd);
435
553
  const { compiled } = await compileScannedProject({
436
554
  css: cssSource,
555
+ cssPath: cssFile ?? void 0,
437
556
  cwd,
438
557
  surfacePatterns: opts.globs,
439
558
  onInvalidPattern: (err) => {
440
559
  console.error(`[RI-1404] CLI glob pattern rejected: ${err}`);
441
560
  return void 0;
442
- }
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
443
565
  });
444
- const buildWarnings = [];
566
+ const buildWarnings = [...importWarnings];
445
567
  if (hasApplyLikeDirective(cssSource)) {
446
568
  buildWarnings.push(
447
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.'
@@ -470,10 +592,227 @@ async function buildCSS(opts, cwd) {
470
592
  return { css: compiled.css, theme: compiled.theme, cssFile };
471
593
  }
472
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
+
473
812
  // src/cli/generate-types.ts
474
813
  import { existsSync as existsSync2 } from "fs";
475
- import { readFile as readFile2, writeFile } from "fs/promises";
476
- import { resolve as resolve3 } from "path";
814
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
815
+ import { resolve as resolve5 } from "path";
477
816
  async function generateTypes(opts, cwd) {
478
817
  const theme = await loadProjectTheme(opts, cwd);
479
818
  const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
@@ -576,14 +915,14 @@ async function generateTypes(opts, cwd) {
576
915
  "}",
577
916
  ""
578
917
  );
579
- const outputPath = resolve3(cwd, "rainbowindex-env.d.ts");
918
+ const outputPath = resolve5(cwd, "rainbowindex-env.d.ts");
580
919
  const newContent = lines.join("\n");
581
920
  if (existsSync2(outputPath)) {
582
921
  const existing = await readFile2(outputPath, "utf-8");
583
922
  if (!existing.startsWith("// rainbowindex-env.d.ts (auto-generated")) {
584
923
  const backupPath = `${outputPath}.bak`;
585
924
  try {
586
- await writeFile(backupPath, existing);
925
+ await writeFile2(backupPath, existing);
587
926
  console.warn(
588
927
  `[RI-1604] "${outputPath}" exists and does not appear to be auto-generated. Original saved to "${backupPath}" before overwrite.`
589
928
  );
@@ -598,7 +937,7 @@ async function generateTypes(opts, cwd) {
598
937
  }
599
938
  }
600
939
  try {
601
- await writeFile(outputPath, newContent);
940
+ await writeFile2(outputPath, newContent);
602
941
  } catch (err) {
603
942
  const msg = err instanceof Error ? err.message : String(err);
604
943
  console.error(`[rainbowindex] Failed to write types file "${outputPath}": ${msg}`);
@@ -611,6 +950,642 @@ async function generateTypes(opts, cwd) {
611
950
  );
612
951
  }
613
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
+
614
1589
  // src/cli/preload-fonts.ts
615
1590
  async function preloadFonts(opts, cwd) {
616
1591
  const theme = await loadProjectTheme(opts, cwd);
@@ -629,34 +1604,7 @@ async function preloadFonts(opts, cwd) {
629
1604
 
630
1605
  // src/cli/watch.ts
631
1606
  import { mkdir } from "fs/promises";
632
- import { resolve as resolve4, dirname as dirname2 } from "path";
633
-
634
- // src/cli/atomic-write.ts
635
- import { writeFile as writeFile2, rename, unlink } from "fs/promises";
636
- import { dirname, join } from "path";
637
- function isIgnorableCleanupError(err) {
638
- return !!err && typeof err === "object" && "code" in err && (err.code === "ENOENT" || err.code === "EPERM");
639
- }
640
- async function writeFileAtomic(filePath, content) {
641
- const dir = dirname(filePath);
642
- const tmp = join(dir, `.ri-tmp-${process.pid}-${Date.now()}`);
643
- try {
644
- await writeFile2(tmp, content);
645
- await rename(tmp, filePath);
646
- } catch (err) {
647
- try {
648
- await unlink(tmp);
649
- } catch (cleanupErr) {
650
- if (!isIgnorableCleanupError(cleanupErr)) {
651
- const msg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
652
- devWarn(`[RI-DEV] Failed to remove temp file "${tmp}": ${msg}`);
653
- }
654
- }
655
- throw err;
656
- }
657
- }
658
-
659
- // src/cli/watch.ts
1607
+ import { resolve as resolve7, dirname as dirname3 } from "path";
660
1608
  async function minifyIfRequested(css, opts) {
661
1609
  if (!opts.minify) return css;
662
1610
  const { optimizeCSS } = await import("./optimize-6NWJVT6W.mjs");
@@ -665,8 +1613,8 @@ async function minifyIfRequested(css, opts) {
665
1613
  async function buildAndWrite(opts, cwd, outputFile) {
666
1614
  const result = await buildCSS(opts, cwd);
667
1615
  const css = await minifyIfRequested(result.css, opts);
668
- const outputPath = resolve4(cwd, outputFile);
669
- await mkdir(dirname2(outputPath), { recursive: true });
1616
+ const outputPath = resolve7(cwd, outputFile);
1617
+ await mkdir(dirname3(outputPath), { recursive: true });
670
1618
  await writeFileAtomic(outputPath, css);
671
1619
  return result;
672
1620
  }
@@ -765,9 +1713,23 @@ async function watchMode(opts, cwd) {
765
1713
  currentBuild = runBuild();
766
1714
  }, delay);
767
1715
  };
768
- watcher.on("change", scheduleRebuild);
769
- watcher.on("add", scheduleRebuild);
770
- watcher.on("unlink", scheduleRebuild);
1716
+ enableSourceFileListCache();
1717
+ enableScanChangeTracking();
1718
+ watcher.on("change", (file) => {
1719
+ markSourceFileChanged(file, cwd);
1720
+ scheduleRebuild();
1721
+ });
1722
+ watcher.on("add", (file) => {
1723
+ invalidateSourceFileListCache();
1724
+ markSourceFileChanged(file, cwd);
1725
+ scheduleRebuild();
1726
+ });
1727
+ watcher.on("unlink", (file) => {
1728
+ invalidateSourceFileListCache();
1729
+ markSourceFileChanged(file, cwd);
1730
+ scheduleRebuild();
1731
+ });
1732
+ watcher.on("unlinkDir", invalidateSourceFileListCache);
771
1733
  watcher.on("error", (err) => {
772
1734
  console.error("[rainbowindex] Watcher error:", err);
773
1735
  });
@@ -777,6 +1739,8 @@ async function watchMode(opts, cwd) {
777
1739
  cleanupCalled = true;
778
1740
  if (debounceTimer) clearTimeout(debounceTimer);
779
1741
  dirty = false;
1742
+ invalidateSourceFileListCache();
1743
+ disableScanChangeTracking();
780
1744
  const doExit = () => {
781
1745
  watcher.close().then(() => {
782
1746
  console.log("\n[rainbowindex] Watcher stopped.");
@@ -797,8 +1761,8 @@ async function watchMode(opts, cwd) {
797
1761
  }
798
1762
 
799
1763
  // src/cli/scan.ts
800
- import { readFile as readFile3 } from "fs/promises";
801
- 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";
802
1766
  import { glob } from "tinyglobby";
803
1767
  async function scanFiles(opts, cwd) {
804
1768
  const files = [...new Set(await glob(opts.globs, { cwd }))].sort(codepointCompare);
@@ -806,15 +1770,15 @@ async function scanFiles(opts, cwd) {
806
1770
  throw new Error(`No files matched ${opts.globs.map((g) => `"${g}"`).join(", ")}.`);
807
1771
  }
808
1772
  for (const file of files) {
809
- const path = resolve5(cwd, file);
1773
+ const path = resolve8(cwd, file);
810
1774
  const warnings = [];
811
1775
  const classes = extractClassesFromSource(
812
- { path, content: await readFile3(path, "utf-8") },
1776
+ { path, content: await readFile4(path, "utf-8") },
813
1777
  warnings
814
1778
  );
815
1779
  const sorted = [...classes].sort(codepointCompare);
816
1780
  console.log(
817
- `${relative2(cwd, path)} (${sorted.length} class${sorted.length === 1 ? "" : "es"})`
1781
+ `${relative5(cwd, path)} (${sorted.length} class${sorted.length === 1 ? "" : "es"})`
818
1782
  );
819
1783
  for (const cls of sorted) {
820
1784
  console.log(` ${cls}`);
@@ -827,9 +1791,9 @@ async function scanFiles(opts, cwd) {
827
1791
 
828
1792
  // src/cli/vite-setup.ts
829
1793
  import { spawn } from "child_process";
830
- import { existsSync as existsSync3, readFileSync } from "fs";
831
- import { mkdir as mkdir2, readFile as readFile4, readdir, writeFile as writeFile3 } from "fs/promises";
832
- 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";
833
1797
  var VITE_CONFIG_FILES = [
834
1798
  "vite.config.ts",
835
1799
  "vite.config.mts",
@@ -892,32 +1856,32 @@ async function initViteProject(opts, cwd, deps = {}) {
892
1856
  ensureStylesheet(cssPath)
893
1857
  ]);
894
1858
  const entryChanged = cssCreated ? await ensureStylesheetImport(entryFile, cssPath) : false;
895
- const rootLabel = relative3(process.cwd(), cwd) || ".";
1859
+ const rootLabel = relative6(process.cwd(), cwd) || ".";
896
1860
  console.log(`[rainbowindex] Initialized Vite project: ${rootLabel}`);
897
1861
  if (dependencyInstalled) {
898
1862
  console.log("[rainbowindex] Added dev dependency: rainbowindex");
899
1863
  }
900
1864
  if (configCreated) {
901
1865
  console.log(
902
- `[rainbowindex] Created Vite config: ${relative3(cwd, configPath).replaceAll("\\", "/")}`
1866
+ `[rainbowindex] Created Vite config: ${relative6(cwd, configPath).replaceAll("\\", "/")}`
903
1867
  );
904
1868
  } else if (configChanged) {
905
1869
  console.log(
906
- `[rainbowindex] Updated Vite config: ${relative3(cwd, configPath).replaceAll("\\", "/")}`
1870
+ `[rainbowindex] Updated Vite config: ${relative6(cwd, configPath).replaceAll("\\", "/")}`
907
1871
  );
908
1872
  }
909
1873
  if (cssCreated) {
910
1874
  console.log(
911
- `[rainbowindex] Created stylesheet: ${relative3(cwd, cssPath).replaceAll("\\", "/")}`
1875
+ `[rainbowindex] Created stylesheet: ${relative6(cwd, cssPath).replaceAll("\\", "/")}`
912
1876
  );
913
1877
  } else if (cssChanged) {
914
1878
  console.log(
915
- `[rainbowindex] Updated stylesheet: ${relative3(cwd, cssPath).replaceAll("\\", "/")}`
1879
+ `[rainbowindex] Updated stylesheet: ${relative6(cwd, cssPath).replaceAll("\\", "/")}`
916
1880
  );
917
1881
  }
918
1882
  if (entryChanged && entryFile) {
919
1883
  console.log(
920
- `[rainbowindex] Added stylesheet import: ${relative3(cwd, entryFile).replaceAll("\\", "/")}`
1884
+ `[rainbowindex] Added stylesheet import: ${relative6(cwd, entryFile).replaceAll("\\", "/")}`
921
1885
  );
922
1886
  }
923
1887
  if (!dependencyInstalled && !configChanged && !configCreated && !cssChanged && !cssCreated) {
@@ -941,7 +1905,7 @@ async function createViteProject(opts, cwd, deps = {}) {
941
1905
  const packageManager = deps.packageManager ?? detectPackageManager(cwd, deps.env);
942
1906
  const runner = deps.runner ?? DEFAULT_RUNNER;
943
1907
  const targetDir = opts.targetDir;
944
- const targetRoot = resolve6(cwd, targetDir);
1908
+ const targetRoot = resolve9(cwd, targetDir);
945
1909
  await ensureScaffoldTargetIsUsable(targetRoot);
946
1910
  const template = opts.template || DEFAULT_CREATE_TEMPLATE;
947
1911
  const scaffold = getCreateCommand(packageManager, targetDir, template);
@@ -952,15 +1916,15 @@ async function createViteProject(opts, cwd, deps = {}) {
952
1916
  packageManager,
953
1917
  runner
954
1918
  });
955
- const displayTarget = relative3(cwd, targetRoot) || ".";
1919
+ const displayTarget = relative6(cwd, targetRoot) || ".";
956
1920
  console.log(`[rainbowindex] Ready: ${displayTarget}`);
957
1921
  console.log(`[rainbowindex] Next: cd ${displayTarget} && ${packageManager} run dev`);
958
1922
  }
959
1923
  function detectPackageManager(cwd, env = process.env) {
960
- if (existsSync3(resolve6(cwd, "pnpm-lock.yaml"))) return "pnpm";
961
- if (existsSync3(resolve6(cwd, "yarn.lock"))) return "yarn";
962
- if (existsSync3(resolve6(cwd, "bun.lock")) || existsSync3(resolve6(cwd, "bun.lockb"))) return "bun";
963
- 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"))) {
964
1928
  return "npm";
965
1929
  }
966
1930
  const packageJSON = readPackageJSONSync(cwd);
@@ -1005,12 +1969,12 @@ async function installRainbowIndex(cwd, packageManager, runner) {
1005
1969
  async function ensureViteConfig(cwd, packageJSON, existingPath) {
1006
1970
  if (!existingPath) {
1007
1971
  const fileName = chooseViteConfigName(cwd, packageJSON);
1008
- const configPath = resolve6(cwd, fileName);
1972
+ const configPath = resolve9(cwd, fileName);
1009
1973
  const content = 'import { defineConfig } from "vite";\nimport rainbowindex from "rainbowindex/vite";\n\nexport default defineConfig({\n plugins: [rainbowindex()],\n});\n';
1010
1974
  await writeFile3(configPath, content, "utf-8");
1011
1975
  return { path: configPath, changed: true, created: true };
1012
1976
  }
1013
- const original = await readFile4(existingPath, "utf-8");
1977
+ const original = await readFile5(existingPath, "utf-8");
1014
1978
  const updated = patchViteConfig(original, existingPath);
1015
1979
  if (updated === original) {
1016
1980
  return { path: existingPath, changed: false, created: false };
@@ -1139,22 +2103,22 @@ function findMatchingDelimiter(input, start, open, close) {
1139
2103
  return -1;
1140
2104
  }
1141
2105
  async function resolveStylesheetPath(cwd, cssFile) {
1142
- if (cssFile) return resolve6(cwd, cssFile);
2106
+ if (cssFile) return resolve9(cwd, cssFile);
1143
2107
  const importedCSS = await findImportedStylesheet(cwd);
1144
2108
  if (importedCSS) return importedCSS;
1145
2109
  for (const candidate of CSS_ENTRY_CANDIDATES) {
1146
- const fullPath = resolve6(cwd, candidate);
2110
+ const fullPath = resolve9(cwd, candidate);
1147
2111
  if (existsSync3(fullPath)) return fullPath;
1148
2112
  }
1149
- 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");
1150
2114
  }
1151
2115
  async function ensureStylesheet(cssPath) {
1152
2116
  if (!existsSync3(cssPath)) {
1153
- await mkdir2(dirname3(cssPath), { recursive: true });
2117
+ await mkdir2(dirname4(cssPath), { recursive: true });
1154
2118
  await writeFile3(cssPath, '@import "rainbowindex";\n', "utf-8");
1155
2119
  return { changed: true, created: true };
1156
2120
  }
1157
- const original = await readFile4(cssPath, "utf-8");
2121
+ const original = await readFile5(cssPath, "utf-8");
1158
2122
  if (/@import\s+["']rainbowindex["']/.test(original)) {
1159
2123
  return { changed: false, created: false };
1160
2124
  }
@@ -1181,8 +2145,8 @@ ${content}`;
1181
2145
  }
1182
2146
  async function ensureStylesheetImport(entryFile, cssPath) {
1183
2147
  if (!entryFile || !existsSync3(entryFile)) return false;
1184
- const original = await readFile4(entryFile, "utf-8");
1185
- const relativePath = toImportPath(dirname3(entryFile), cssPath);
2148
+ const original = await readFile5(entryFile, "utf-8");
2149
+ const relativePath = toImportPath(dirname4(entryFile), cssPath);
1186
2150
  if (hasStylesheetImport(original, relativePath)) {
1187
2151
  return false;
1188
2152
  }
@@ -1192,16 +2156,16 @@ async function ensureStylesheetImport(entryFile, cssPath) {
1192
2156
  }
1193
2157
  async function findImportedStylesheet(cwd) {
1194
2158
  for (const entryFile of ENTRY_FILES) {
1195
- const fullPath = resolve6(cwd, entryFile);
2159
+ const fullPath = resolve9(cwd, entryFile);
1196
2160
  if (!existsSync3(fullPath)) continue;
1197
- const content = await readFile4(fullPath, "utf-8");
2161
+ const content = await readFile5(fullPath, "utf-8");
1198
2162
  const matches = content.matchAll(
1199
2163
  /import\s+(?:.+?\s+from\s+)?["']([^"']+\.css(?:\?[^"']*)?)["']/g
1200
2164
  );
1201
2165
  for (const match of matches) {
1202
2166
  const importPath = match[1].split("?")[0];
1203
2167
  if (!importPath.startsWith(".")) continue;
1204
- const stylesheet = resolve6(dirname3(fullPath), importPath);
2168
+ const stylesheet = resolve9(dirname4(fullPath), importPath);
1205
2169
  if (existsSync3(stylesheet)) return stylesheet;
1206
2170
  }
1207
2171
  }
@@ -1209,7 +2173,7 @@ async function findImportedStylesheet(cwd) {
1209
2173
  }
1210
2174
  async function findEntryFile(cwd) {
1211
2175
  for (const entryFile of ENTRY_FILES) {
1212
- const fullPath = resolve6(cwd, entryFile);
2176
+ const fullPath = resolve9(cwd, entryFile);
1213
2177
  if (existsSync3(fullPath)) return fullPath;
1214
2178
  }
1215
2179
  return null;
@@ -1219,18 +2183,18 @@ function hasStylesheetImport(source, importPath) {
1219
2183
  return new RegExp(`import\\s+(?:.+?\\s+from\\s+)?["']${escapedPath}["']`).test(source);
1220
2184
  }
1221
2185
  function toImportPath(fromDir, targetFile) {
1222
- const rel = relative3(fromDir, targetFile).replaceAll("\\", "/");
2186
+ const rel = relative6(fromDir, targetFile).replaceAll("\\", "/");
1223
2187
  return rel.startsWith(".") ? rel : `./${rel}`;
1224
2188
  }
1225
2189
  function findExistingViteConfig(cwd) {
1226
2190
  for (const name of VITE_CONFIG_FILES) {
1227
- const fullPath = resolve6(cwd, name);
2191
+ const fullPath = resolve9(cwd, name);
1228
2192
  if (existsSync3(fullPath)) return fullPath;
1229
2193
  }
1230
2194
  return null;
1231
2195
  }
1232
2196
  function chooseViteConfigName(cwd, packageJSON) {
1233
- 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) {
1234
2198
  return "vite.config.ts";
1235
2199
  }
1236
2200
  return packageJSON?.type === "module" ? "vite.config.js" : "vite.config.mjs";
@@ -1278,10 +2242,10 @@ function getInstallCommand(packageManager) {
1278
2242
  }
1279
2243
  }
1280
2244
  function readPackageJSONSync(cwd) {
1281
- const packagePath = resolve6(cwd, "package.json");
2245
+ const packagePath = resolve9(cwd, "package.json");
1282
2246
  if (!existsSync3(packagePath)) return null;
1283
2247
  try {
1284
- return JSON.parse(readFileSync(packagePath, "utf-8"));
2248
+ return JSON.parse(readFileSync2(packagePath, "utf-8"));
1285
2249
  } catch {
1286
2250
  return null;
1287
2251
  }
@@ -1289,7 +2253,7 @@ function readPackageJSONSync(cwd) {
1289
2253
 
1290
2254
  // src/entries/cli.ts
1291
2255
  function getVersion() {
1292
- return true ? "0.5.1" : "unknown";
2256
+ return true ? "0.7.0" : "unknown";
1293
2257
  }
1294
2258
  async function main() {
1295
2259
  const args = process.argv.slice(2);
@@ -1310,12 +2274,24 @@ async function main() {
1310
2274
  case "generate-types":
1311
2275
  await generateTypes(opts, cwd);
1312
2276
  break;
2277
+ case "generate-snapshot":
2278
+ await generateSnapshot(opts, cwd);
2279
+ break;
2280
+ case "generate-tokens":
2281
+ await generateTokens(opts, cwd);
2282
+ break;
1313
2283
  case "preload-fonts":
1314
2284
  await preloadFonts(opts, cwd);
1315
2285
  break;
1316
2286
  case "scan":
1317
2287
  await scanFiles(opts, cwd);
1318
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
+ }
1319
2295
  case "build": {
1320
2296
  if (opts.watch) {
1321
2297
  await watchMode(opts, cwd);
@@ -1336,7 +2312,7 @@ var isDirectExecution = (() => {
1336
2312
  const entry = process.argv[1];
1337
2313
  if (entry === void 0) return false;
1338
2314
  try {
1339
- return import.meta.url === pathToFileURL(realpathSync(resolve7(entry))).href;
2315
+ return import.meta.url === pathToFileURL(realpathSync(resolve10(entry))).href;
1340
2316
  } catch {
1341
2317
  return false;
1342
2318
  }