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.
- package/CHANGELOG.md +876 -0
- package/LICENSE +16 -17
- package/NOTICE.md +106 -0
- package/README.md +225 -69
- package/dist/browser.d.ts +4 -2
- package/dist/browser.mjs +12 -6
- package/dist/chunk-2T7V5XLK.mjs +912 -0
- package/dist/chunk-6OORICWF.mjs +16 -0
- package/dist/{chunk-PDORZSQX.mjs → chunk-FJOZJIKB.mjs} +7814 -5174
- package/dist/chunk-L56IRO7A.mjs +491 -0
- package/dist/chunk-PZDVDEZJ.mjs +196 -0
- package/dist/{chunk-4CTJLMYM.mjs → chunk-RC6DDE4L.mjs} +37 -23
- 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-4UKFK2GE.mjs → chunk-XQGSG2HK.mjs} +213 -756
- package/dist/cli.mjs +1101 -125
- package/dist/{context-B9yhJxd5.d.ts → context-DcBtnnan.d.ts} +55 -108
- package/dist/editor.d.ts +82 -421
- package/dist/editor.mjs +68 -363
- package/dist/eslint.d.ts +16 -0
- package/dist/eslint.mjs +32 -0
- package/dist/{index-Dx-NpFFx.d.ts → imports-C9esHd5Q.d.ts} +98 -81
- 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 +49 -5
- package/dist/index.mjs +40 -13
- 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 +273 -113
- package/package.json +27 -5
- package/dist/chunk-F4VCBISU.mjs +0 -1866
- package/dist/chunk-RU4756NG.mjs +0 -243
- 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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
41
|
+
resolveDirectives,
|
|
42
|
+
stripCSSComments
|
|
43
|
+
} from "./chunk-FJOZJIKB.mjs";
|
|
44
|
+
import "./chunk-L56IRO7A.mjs";
|
|
22
45
|
import {
|
|
23
46
|
devWarn
|
|
24
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-XQGSG2HK.mjs";
|
|
25
48
|
|
|
26
49
|
// src/entries/cli.ts
|
|
27
50
|
import { realpathSync } from "fs";
|
|
28
|
-
import { resolve as
|
|
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
|
|
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
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
var
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
535
|
+
return load(cssFile2);
|
|
418
536
|
}
|
|
419
537
|
const cssFile = await findCSSFileAsync(cwd);
|
|
420
|
-
if (!cssFile) return { css: "", cssFile: null };
|
|
421
|
-
return
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
669
|
-
await mkdir(
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
watcher.on("
|
|
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
|
|
801
|
-
import { relative as
|
|
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 =
|
|
1773
|
+
const path = resolve8(cwd, file);
|
|
810
1774
|
const warnings = [];
|
|
811
1775
|
const classes = extractClassesFromSource(
|
|
812
|
-
{ path, content: await
|
|
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
|
-
`${
|
|
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
|
|
832
|
-
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";
|
|
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 =
|
|
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: ${
|
|
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: ${
|
|
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: ${
|
|
1875
|
+
`[rainbowindex] Created stylesheet: ${relative6(cwd, cssPath).replaceAll("\\", "/")}`
|
|
912
1876
|
);
|
|
913
1877
|
} else if (cssChanged) {
|
|
914
1878
|
console.log(
|
|
915
|
-
`[rainbowindex] Updated stylesheet: ${
|
|
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: ${
|
|
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 =
|
|
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 =
|
|
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(
|
|
961
|
-
if (existsSync3(
|
|
962
|
-
if (existsSync3(
|
|
963
|
-
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"))) {
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
2110
|
+
const fullPath = resolve9(cwd, candidate);
|
|
1147
2111
|
if (existsSync3(fullPath)) return fullPath;
|
|
1148
2112
|
}
|
|
1149
|
-
return existsSync3(
|
|
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(
|
|
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
|
|
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
|
|
1185
|
-
const relativePath = toImportPath(
|
|
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 =
|
|
2159
|
+
const fullPath = resolve9(cwd, entryFile);
|
|
1196
2160
|
if (!existsSync3(fullPath)) continue;
|
|
1197
|
-
const content = await
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
2245
|
+
const packagePath = resolve9(cwd, "package.json");
|
|
1282
2246
|
if (!existsSync3(packagePath)) return null;
|
|
1283
2247
|
try {
|
|
1284
|
-
return JSON.parse(
|
|
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.
|
|
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(
|
|
2315
|
+
return import.meta.url === pathToFileURL(realpathSync(resolve10(entry))).href;
|
|
1340
2316
|
} catch {
|
|
1341
2317
|
return false;
|
|
1342
2318
|
}
|