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.
- package/CHANGELOG.md +543 -0
- package/LICENSE +16 -17
- package/NOTICE.md +106 -0
- package/README.md +219 -65
- package/dist/browser.d.ts +4 -2
- package/dist/browser.mjs +12 -4
- package/dist/chunk-2T7V5XLK.mjs +912 -0
- package/dist/chunk-6OORICWF.mjs +16 -0
- package/dist/{chunk-KSNYSR3C.mjs → chunk-FJOZJIKB.mjs} +2499 -329
- package/dist/chunk-L56IRO7A.mjs +491 -0
- package/dist/chunk-PZDVDEZJ.mjs +196 -0
- package/dist/{chunk-3LWJTLOJ.mjs → chunk-RC6DDE4L.mjs} +23 -15
- package/dist/chunk-TQJYVQPE.mjs +217 -0
- package/dist/chunk-W756NVYI.mjs +33 -0
- package/dist/chunk-WBESS2ZD.mjs +598 -0
- package/dist/{chunk-3HRMFZGE.mjs → chunk-X66Z2YHT.mjs} +2 -1
- package/dist/{chunk-6U4IOFOS.mjs → chunk-XQGSG2HK.mjs} +199 -555
- package/dist/cli.mjs +1077 -123
- package/dist/{index-DSgpB6bS.d.ts → context-DcBtnnan.d.ts} +47 -103
- package/dist/editor.d.ts +71 -433
- package/dist/editor.mjs +51 -368
- package/dist/eslint.d.ts +16 -0
- package/dist/eslint.mjs +32 -0
- package/dist/{index-4Kyaq3IZ.d.ts → imports-C9esHd5Q.d.ts} +78 -84
- package/dist/index-CNqdL5U0.d.ts +56 -0
- package/dist/index-Czx-EUwh.d.ts +138 -0
- package/dist/index-DW8YSxTz.d.ts +104 -0
- package/dist/index.d.ts +46 -5
- package/dist/index.mjs +33 -9
- package/dist/oxlint.d.ts +21 -3
- package/dist/oxlint.mjs +19 -1
- package/dist/recipe.d.ts +111 -0
- package/dist/recipe.mjs +71 -0
- package/dist/safelist-CH3_PywB.d.ts +43 -0
- package/dist/session-CMaskdB7.d.ts +543 -0
- package/dist/tailwind.css +644 -0
- package/dist/theme-CIZiGlce.d.ts +115 -0
- package/dist/vite.d.ts +10 -1
- package/dist/vite.mjs +266 -118
- package/package.json +27 -5
- package/dist/chunk-WK6S4HTC.mjs +0 -1921
- package/dist/chunk-ZR7XJMUN.mjs +0 -251
- 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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
resolveDirectives,
|
|
42
|
+
stripCSSComments
|
|
43
|
+
} from "./chunk-FJOZJIKB.mjs";
|
|
44
|
+
import "./chunk-L56IRO7A.mjs";
|
|
27
45
|
import {
|
|
28
46
|
devWarn
|
|
29
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-XQGSG2HK.mjs";
|
|
30
48
|
|
|
31
49
|
// src/entries/cli.ts
|
|
32
50
|
import { realpathSync } from "fs";
|
|
33
|
-
import { resolve as
|
|
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
|
|
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
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
var
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
535
|
+
return load(cssFile2);
|
|
423
536
|
}
|
|
424
537
|
const cssFile = await findCSSFileAsync(cwd);
|
|
425
|
-
if (!cssFile) return { css: "", cssFile: null };
|
|
426
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
675
|
-
await mkdir(
|
|
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
|
|
823
|
-
import { relative as
|
|
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 =
|
|
1773
|
+
const path = resolve8(cwd, file);
|
|
832
1774
|
const warnings = [];
|
|
833
1775
|
const classes = extractClassesFromSource(
|
|
834
|
-
{ path, content: await
|
|
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
|
-
`${
|
|
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
|
|
854
|
-
import { dirname as
|
|
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 =
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
1875
|
+
`[rainbowindex] Created stylesheet: ${relative6(cwd, cssPath).replaceAll("\\", "/")}`
|
|
934
1876
|
);
|
|
935
1877
|
} else if (cssChanged) {
|
|
936
1878
|
console.log(
|
|
937
|
-
`[rainbowindex] Updated stylesheet: ${
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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(
|
|
983
|
-
if (existsSync3(
|
|
984
|
-
if (existsSync3(
|
|
985
|
-
if (existsSync3(
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
2110
|
+
const fullPath = resolve9(cwd, candidate);
|
|
1169
2111
|
if (existsSync3(fullPath)) return fullPath;
|
|
1170
2112
|
}
|
|
1171
|
-
return existsSync3(
|
|
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(
|
|
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
|
|
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
|
|
1207
|
-
const relativePath = toImportPath(
|
|
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 =
|
|
2159
|
+
const fullPath = resolve9(cwd, entryFile);
|
|
1218
2160
|
if (!existsSync3(fullPath)) continue;
|
|
1219
|
-
const content = await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
2245
|
+
const packagePath = resolve9(cwd, "package.json");
|
|
1304
2246
|
if (!existsSync3(packagePath)) return null;
|
|
1305
2247
|
try {
|
|
1306
|
-
return JSON.parse(
|
|
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.
|
|
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(
|
|
2315
|
+
return import.meta.url === pathToFileURL(realpathSync(resolve10(entry))).href;
|
|
1362
2316
|
} catch {
|
|
1363
2317
|
return false;
|
|
1364
2318
|
}
|