@decocms/blocks-cli 7.21.0 → 7.21.1
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/package.json +2 -2
- package/scripts/migrate/analyzers/tailwind-config.test.ts +149 -0
- package/scripts/migrate/analyzers/tailwind-config.ts +283 -0
- package/scripts/migrate/css-compile-check.test.ts +115 -0
- package/scripts/migrate/css-compile-check.ts +137 -0
- package/scripts/migrate/phase-analyze.ts +13 -0
- package/scripts/migrate/phase-cleanup.test.ts +1 -0
- package/scripts/migrate/phase-compile.test.ts +48 -0
- package/scripts/migrate/phase-compile.ts +38 -0
- package/scripts/migrate/phase-report.test.ts +103 -0
- package/scripts/migrate/phase-report.ts +64 -4
- package/scripts/migrate/phase-verify.test.ts +1 -0
- package/scripts/migrate/templates/app-css.test.ts +150 -0
- package/scripts/migrate/templates/app-css.ts +142 -52
- package/scripts/migrate/templates/hooks.test.ts +1 -0
- package/scripts/migrate/templates/package-json.ts +5 -0
- package/scripts/migrate/transforms/color-oklch.test.ts +83 -0
- package/scripts/migrate/transforms/color-oklch.ts +122 -0
- package/scripts/migrate/transforms/css.test.ts +109 -0
- package/scripts/migrate/transforms/css.ts +186 -0
- package/scripts/migrate/transforms/tailwind-renames.test.ts +37 -0
- package/scripts/migrate/transforms/tailwind-renames.ts +224 -0
- package/scripts/migrate/transforms/tailwind.test.ts +74 -0
- package/scripts/migrate/transforms/tailwind.ts +27 -123
- package/scripts/migrate/types.ts +18 -0
- package/scripts/tailwind-lint.ts +9 -54
|
@@ -10,6 +10,7 @@ import { log, logPhase } from "./types";
|
|
|
10
10
|
import { extractSectionMetadata } from "./analyzers/section-metadata";
|
|
11
11
|
import { classifyIslands } from "./analyzers/island-classifier";
|
|
12
12
|
import { inventoryLoaders } from "./analyzers/loader-inventory";
|
|
13
|
+
import { extractTailwindConfig } from "./analyzers/tailwind-config";
|
|
13
14
|
|
|
14
15
|
const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
15
16
|
["preact-hooks", /from\s+["']preact\/hooks["']/],
|
|
@@ -632,6 +633,18 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
632
633
|
ctx.themeColors = theme.colors;
|
|
633
634
|
ctx.fontFamily = theme.fontFamily;
|
|
634
635
|
|
|
636
|
+
// Extract the source site's tailwind.config.ts BEFORE it gets deleted in
|
|
637
|
+
// cleanup — otherwise custom colors/fontFamily/safelist are silently
|
|
638
|
+
// dropped (see decocms/blocks#369, gotcha #48 in the migration skill).
|
|
639
|
+
ctx.tailwindConfig = extractTailwindConfig(ctx.sourceDir);
|
|
640
|
+
const twColorCount = Object.keys(ctx.tailwindConfig.colors).length;
|
|
641
|
+
if (twColorCount > 0) {
|
|
642
|
+
console.log(` Tailwind config: ${twColorCount} custom color(s) ported from tailwind.config.ts`);
|
|
643
|
+
}
|
|
644
|
+
if (ctx.tailwindConfig.reviewItems.length > 0) {
|
|
645
|
+
ctx.manualReviewItems.push(...ctx.tailwindConfig.reviewItems);
|
|
646
|
+
}
|
|
647
|
+
|
|
635
648
|
console.log(` Site: ${ctx.siteName}`);
|
|
636
649
|
console.log(` Platform: ${ctx.platform}${ctx.vtexAccount ? ` (account: ${ctx.vtexAccount})` : ""}`);
|
|
637
650
|
console.log(` GTM ID: ${ctx.gtmId || "none"}`);
|
|
@@ -20,6 +20,7 @@ function makeCtx(sourceDir: string, dryRun = false): MigrationContext {
|
|
|
20
20
|
importMap: {},
|
|
21
21
|
discoveredNpmDeps: {},
|
|
22
22
|
themeColors: {},
|
|
23
|
+
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
23
24
|
fontFamily: null,
|
|
24
25
|
files: [],
|
|
25
26
|
sectionMetas: [],
|
|
@@ -16,6 +16,7 @@ function makeCtx(sourceDir: string, dryRun = false): MigrationContext {
|
|
|
16
16
|
importMap: {},
|
|
17
17
|
discoveredNpmDeps: {},
|
|
18
18
|
themeColors: {},
|
|
19
|
+
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
19
20
|
fontFamily: null,
|
|
20
21
|
files: [],
|
|
21
22
|
sectionMetas: [],
|
|
@@ -172,6 +173,53 @@ describe("compile (phase 8)", () => {
|
|
|
172
173
|
expect(result.typecheck.passed).toBe(false);
|
|
173
174
|
expect(result.build.passed).toBe(true);
|
|
174
175
|
});
|
|
176
|
+
|
|
177
|
+
it("skips the CSS compile check when src/styles/app.css doesn't exist", () => {
|
|
178
|
+
fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
|
|
179
|
+
const ctx = makeCtx(tmpDir);
|
|
180
|
+
const { runner, calls } = fakeRunner({
|
|
181
|
+
"npx tsc --noEmit": { ok: true },
|
|
182
|
+
});
|
|
183
|
+
const result = compile(ctx, { runner });
|
|
184
|
+
expect(result.css.ran).toBe(false);
|
|
185
|
+
expect(calls).toEqual(["npx tsc --noEmit"]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("runs and fails the CSS compile check when app.css exists and the CLI errors", () => {
|
|
189
|
+
fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
|
|
190
|
+
fs.mkdirSync(path.join(tmpDir, "src", "styles"), { recursive: true });
|
|
191
|
+
fs.writeFileSync(path.join(tmpDir, "src", "styles", "app.css"), "@import \"tailwindcss\";\n");
|
|
192
|
+
const ctx = makeCtx(tmpDir);
|
|
193
|
+
const { runner } = fakeRunner({
|
|
194
|
+
"npx tsc --noEmit": { ok: true },
|
|
195
|
+
});
|
|
196
|
+
const cssAwareRunner = (cmd: string, cwd: string) => {
|
|
197
|
+
if (cmd.includes("@tailwindcss/cli")) {
|
|
198
|
+
return { ok: false, output: "Cannot apply unknown utility class `font-bebas-neue`" };
|
|
199
|
+
}
|
|
200
|
+
return runner(cmd, cwd);
|
|
201
|
+
};
|
|
202
|
+
const result = compile(ctx, { runner: cssAwareRunner });
|
|
203
|
+
expect(result.css.ran).toBe(true);
|
|
204
|
+
expect(result.css.passed).toBe(false);
|
|
205
|
+
expect(result.css.output).toContain("font-bebas-neue");
|
|
206
|
+
expect(result.shouldFail).toBe(false); // non-strict: warning only
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("promotes a CSS compile failure to abort in --strict mode", () => {
|
|
210
|
+
fs.mkdirSync(path.join(tmpDir, "node_modules"), { recursive: true });
|
|
211
|
+
fs.mkdirSync(path.join(tmpDir, "src", "styles"), { recursive: true });
|
|
212
|
+
fs.writeFileSync(path.join(tmpDir, "src", "styles", "app.css"), "@import \"tailwindcss\";\n");
|
|
213
|
+
const ctx = makeCtx(tmpDir);
|
|
214
|
+
const cssAwareRunner = (cmd: string): CompileRunResult => {
|
|
215
|
+
if (cmd.includes("@tailwindcss/cli")) return { ok: false, output: "invalid CSS" };
|
|
216
|
+
return { ok: true };
|
|
217
|
+
};
|
|
218
|
+
const result = compile(ctx, { runner: cssAwareRunner, strict: true });
|
|
219
|
+
expect(result.typecheck.passed).toBe(true);
|
|
220
|
+
expect(result.css.passed).toBe(false);
|
|
221
|
+
expect(result.shouldFail).toBe(true);
|
|
222
|
+
});
|
|
175
223
|
});
|
|
176
224
|
|
|
177
225
|
describe("truncate helper", () => {
|
|
@@ -25,6 +25,7 @@ import { execSync } from "node:child_process";
|
|
|
25
25
|
import * as fs from "node:fs";
|
|
26
26
|
import * as path from "node:path";
|
|
27
27
|
import { gray, green, red, yellow } from "./colors";
|
|
28
|
+
import { checkCssCompiles } from "./css-compile-check";
|
|
28
29
|
import type { MigrationContext } from "./types";
|
|
29
30
|
import { logPhase } from "./types";
|
|
30
31
|
|
|
@@ -48,6 +49,15 @@ export interface CompileResult {
|
|
|
48
49
|
skipped: boolean;
|
|
49
50
|
typecheck: { ran: boolean; passed: boolean; output?: string };
|
|
50
51
|
build: { ran: boolean; passed: boolean; output?: string };
|
|
52
|
+
/**
|
|
53
|
+
* Real Tailwind v4 compilation of src/styles/app.css via the site's own
|
|
54
|
+
* `@tailwindcss/cli` install. Catches the exact failure mode of
|
|
55
|
+
* decocms/blocks#369 / skill gotcha #48 (dropped custom theme tokens ->
|
|
56
|
+
* "Cannot apply unknown utility class") during migration instead of at
|
|
57
|
+
* runtime. `unmatchedClassWarnings` is informational only — it never
|
|
58
|
+
* fails the build (see css-compile-check.ts for why).
|
|
59
|
+
*/
|
|
60
|
+
css: { ran: boolean; passed: boolean; output?: string; unmatchedClassWarnings: string[] };
|
|
51
61
|
/** True iff compile decided the migration should fail (only in strict mode). */
|
|
52
62
|
shouldFail: boolean;
|
|
53
63
|
}
|
|
@@ -96,6 +106,7 @@ export function compile(
|
|
|
96
106
|
skipped: false,
|
|
97
107
|
typecheck: { ran: false, passed: false },
|
|
98
108
|
build: { ran: false, passed: false },
|
|
109
|
+
css: { ran: false, passed: false, unmatchedClassWarnings: [] },
|
|
99
110
|
shouldFail: false,
|
|
100
111
|
};
|
|
101
112
|
|
|
@@ -136,6 +147,32 @@ export function compile(
|
|
|
136
147
|
console.log("");
|
|
137
148
|
}
|
|
138
149
|
|
|
150
|
+
// CSS compile check — real Tailwind v4 build of src/styles/app.css.
|
|
151
|
+
console.log(" Running: CSS compile check (npx @tailwindcss/cli)...");
|
|
152
|
+
const cssResult = checkCssCompiles(ctx, runner);
|
|
153
|
+
result.css.ran = cssResult.ran;
|
|
154
|
+
result.css.passed = cssResult.passed;
|
|
155
|
+
result.css.unmatchedClassWarnings = cssResult.unmatchedClassWarnings;
|
|
156
|
+
|
|
157
|
+
if (!cssResult.ran) {
|
|
158
|
+
console.log(` ${gray("–")} Skipped (no src/styles/app.css)`);
|
|
159
|
+
} else if (cssResult.passed) {
|
|
160
|
+
console.log(` ${green("✓")} CSS compiled`);
|
|
161
|
+
if (cssResult.unmatchedClassWarnings.length > 0) {
|
|
162
|
+
console.log(
|
|
163
|
+
` ${yellow("⚠")} ${cssResult.unmatchedClassWarnings.length} class(es) used in src/ produced no CSS output (dropped theme token or typo?):`,
|
|
164
|
+
);
|
|
165
|
+
console.log(` ${cssResult.unmatchedClassWarnings.slice(0, 20).join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
result.css.output = cssResult.output;
|
|
169
|
+
const output = truncate(cssResult.output ?? "", MAX_OUTPUT_LINES);
|
|
170
|
+
const icon = opts.strict ? red("✗") : yellow("⚠");
|
|
171
|
+
console.log(` ${icon} CSS compile failed:\n`);
|
|
172
|
+
console.log(output);
|
|
173
|
+
console.log("");
|
|
174
|
+
}
|
|
175
|
+
|
|
139
176
|
// Optional Vite build (heavier, opt-in).
|
|
140
177
|
if (opts.withBuild) {
|
|
141
178
|
console.log(" Running: Vite build (npx vite build)...");
|
|
@@ -158,6 +195,7 @@ export function compile(
|
|
|
158
195
|
// Decide whether to fail the migration.
|
|
159
196
|
const anyFailed =
|
|
160
197
|
(result.typecheck.ran && !result.typecheck.passed) ||
|
|
198
|
+
(result.css.ran && !result.css.passed) ||
|
|
161
199
|
(result.build.ran && !result.build.passed);
|
|
162
200
|
|
|
163
201
|
if (anyFailed && opts.strict) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { report } from "./phase-report";
|
|
6
|
+
import { createContext } from "./types";
|
|
7
|
+
|
|
8
|
+
let tmpDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "phase-report-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("report — CSS Migration section", () => {
|
|
19
|
+
it("summarizes tokens ported from tailwind.config.ts", () => {
|
|
20
|
+
const ctx = createContext(tmpDir);
|
|
21
|
+
ctx.tailwindConfig = {
|
|
22
|
+
colors: { perola: "#F5F0E8", "brand-500": "#B10200" },
|
|
23
|
+
fontFamily: { "bebas-neue": "Bebas Neue, sans-serif" },
|
|
24
|
+
screens: {},
|
|
25
|
+
safelist: ["bg-red-500"],
|
|
26
|
+
safelistPatterns: [],
|
|
27
|
+
plugins: [],
|
|
28
|
+
reviewItems: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
report(ctx);
|
|
32
|
+
|
|
33
|
+
const content = fs.readFileSync(path.join(tmpDir, "MIGRATION_REPORT.md"), "utf-8");
|
|
34
|
+
expect(content).toContain("## CSS Migration");
|
|
35
|
+
expect(content).toContain("2 color(s), 1 font stack(s), 0 breakpoint(s)");
|
|
36
|
+
expect(content).toContain("1 safelist entrie(s)");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("lists CSS-specific manual review items separately from unrelated ones", () => {
|
|
40
|
+
const ctx = createContext(tmpDir);
|
|
41
|
+
ctx.manualReviewItems = [
|
|
42
|
+
{
|
|
43
|
+
file: "src/styles/app.css",
|
|
44
|
+
reason: "safelist pattern /^grid-cols-/ cannot be auto-converted",
|
|
45
|
+
severity: "warning",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
file: "src/components/Foo.tsx",
|
|
49
|
+
reason: "useComponent(...) call site detected — needs manual React conversion",
|
|
50
|
+
severity: "error",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
report(ctx);
|
|
55
|
+
|
|
56
|
+
const content = fs.readFileSync(path.join(tmpDir, "MIGRATION_REPORT.md"), "utf-8");
|
|
57
|
+
const cssSection = content.split("## CSS Migration")[1].split("## Always Check")[0];
|
|
58
|
+
expect(cssSection).toContain("safelist pattern");
|
|
59
|
+
expect(cssSection).not.toContain("useComponent");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("reports no CSS findings when none exist", () => {
|
|
63
|
+
const ctx = createContext(tmpDir);
|
|
64
|
+
report(ctx);
|
|
65
|
+
const content = fs.readFileSync(path.join(tmpDir, "MIGRATION_REPORT.md"), "utf-8");
|
|
66
|
+
expect(content).toContain("No CSS-specific findings from this run.");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("does not miscategorize an unrelated finding whose reason merely contains 'collapse' as a substring of another word", () => {
|
|
70
|
+
const ctx = createContext(tmpDir);
|
|
71
|
+
ctx.manualReviewItems = [
|
|
72
|
+
{
|
|
73
|
+
file: "src/components/Nav.tsx",
|
|
74
|
+
reason: "the mobile nav state collapsed unexpectedly during the smoke test — needs manual QA",
|
|
75
|
+
severity: "warning",
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
report(ctx);
|
|
80
|
+
|
|
81
|
+
const content = fs.readFileSync(path.join(tmpDir, "MIGRATION_REPORT.md"), "utf-8");
|
|
82
|
+
const cssSection = content.split("## CSS Migration")[1].split("## Always Check")[0];
|
|
83
|
+
expect(cssSection).toContain("No CSS-specific findings from this run.");
|
|
84
|
+
expect(cssSection).not.toContain("collapsed unexpectedly");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("still categorizes a genuine DaisyUI collapse finding as CSS-specific", () => {
|
|
88
|
+
const ctx = createContext(tmpDir);
|
|
89
|
+
ctx.manualReviewItems = [
|
|
90
|
+
{
|
|
91
|
+
file: "src/components/Filters.tsx",
|
|
92
|
+
reason: "DaisyUI .collapse usage found — its expand/collapse chain breaks under Tailwind v4.",
|
|
93
|
+
severity: "warning",
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
report(ctx);
|
|
98
|
+
|
|
99
|
+
const content = fs.readFileSync(path.join(tmpDir, "MIGRATION_REPORT.md"), "utf-8");
|
|
100
|
+
const cssSection = content.split("## CSS Migration")[1].split("## Always Check")[0];
|
|
101
|
+
expect(cssSection).toContain("DaisyUI .collapse usage found");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { MigrationContext } from "./types";
|
|
3
|
+
import type { MigrationContext, ReviewItem } from "./types";
|
|
4
4
|
import { logPhase } from "./types";
|
|
5
5
|
|
|
6
|
+
// Word-only keywords are matched with \b boundaries so a generic English
|
|
7
|
+
// word like "collapse" doesn't misfire on an unrelated finding's reason
|
|
8
|
+
// text (e.g. a JS/UX note that happens to mention "collapse" in a
|
|
9
|
+
// non-DaisyUI sense). Path-/symbol-like keywords (containing non-word
|
|
10
|
+
// characters) are matched as plain substrings since \b doesn't apply to them.
|
|
11
|
+
const CSS_REVIEW_KEYWORDS = [
|
|
12
|
+
"tailwind.config.ts",
|
|
13
|
+
"safelist",
|
|
14
|
+
"oklch",
|
|
15
|
+
"css-styling.md",
|
|
16
|
+
"@theme",
|
|
17
|
+
"@apply",
|
|
18
|
+
"@utility",
|
|
19
|
+
"DaisyUI",
|
|
20
|
+
"collapse",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const CSS_REVIEW_KEYWORD_MATCHERS = CSS_REVIEW_KEYWORDS.map((k) =>
|
|
24
|
+
/^\w+$/.test(k) ? new RegExp(`\\b${k}\\b`, "i") : k,
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function isCssReviewItem(item: ReviewItem): boolean {
|
|
28
|
+
return (
|
|
29
|
+
item.file.endsWith("app.css") ||
|
|
30
|
+
item.file === "tailwind.config.ts" ||
|
|
31
|
+
CSS_REVIEW_KEYWORD_MATCHERS.some((k) =>
|
|
32
|
+
typeof k === "string" ? item.reason.includes(k) : k.test(item.reason),
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
6
37
|
const FRAMEWORK_FINDINGS = [
|
|
7
38
|
"Session/analytics SDK is boilerplate duplicated across all sites — should be a single framework function",
|
|
8
39
|
"GTM event system (useGTMEvent, data-gtm-* listeners) is universal pattern — should be in @decocms/blocks",
|
|
@@ -129,6 +160,37 @@ export function report(ctx: MigrationContext): void {
|
|
|
129
160
|
lines.push("");
|
|
130
161
|
}
|
|
131
162
|
|
|
163
|
+
// CSS Migration — replaces the old generic "DaisyUI/Tailwind" checkboxes
|
|
164
|
+
// with what was actually ported/detected for THIS site, so a human
|
|
165
|
+
// doesn't have to re-derive it from scratch.
|
|
166
|
+
lines.push("## CSS Migration");
|
|
167
|
+
lines.push("");
|
|
168
|
+
const twColorCount = Object.keys(ctx.tailwindConfig.colors).length;
|
|
169
|
+
const twFontCount = Object.keys(ctx.tailwindConfig.fontFamily).length;
|
|
170
|
+
const twScreenCount = Object.keys(ctx.tailwindConfig.screens).length;
|
|
171
|
+
const twSafelistCount = ctx.tailwindConfig.safelist.length;
|
|
172
|
+
lines.push(
|
|
173
|
+
`- **tailwind.config.ts porting**: ${twColorCount} color(s), ${twFontCount} font stack(s), ${twScreenCount} breakpoint(s) ported into \`src/styles/app.css\` \`@theme\`; ${twSafelistCount} safelist entrie(s) ported via \`@source inline(...)\`.`,
|
|
174
|
+
);
|
|
175
|
+
lines.push(
|
|
176
|
+
"- **Real CSS compile check**: runs in the compile phase (`npx @tailwindcss/cli`) against `src/styles/app.css` — catches unknown-utility-class errors before they ship. Re-run any time with `bun run tailwind:lint`.",
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const cssReviewItems = ctx.manualReviewItems.filter(isCssReviewItem);
|
|
180
|
+
if (cssReviewItems.length > 0) {
|
|
181
|
+
lines.push("");
|
|
182
|
+
lines.push(`**${cssReviewItems.length} CSS-specific finding(s) requiring manual review:**`);
|
|
183
|
+
lines.push("");
|
|
184
|
+
for (const item of cssReviewItems) {
|
|
185
|
+
const icon = item.severity === "error" ? "🔴" : item.severity === "warning" ? "🟡" : "🔵";
|
|
186
|
+
lines.push(`${icon} **\`${item.file}\`**: ${item.reason}`);
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
lines.push("");
|
|
190
|
+
lines.push("- No CSS-specific findings from this run.");
|
|
191
|
+
}
|
|
192
|
+
lines.push("");
|
|
193
|
+
|
|
132
194
|
// Always-present manual review items
|
|
133
195
|
lines.push("## Always Check (site-specific)");
|
|
134
196
|
lines.push("");
|
|
@@ -136,9 +198,7 @@ export function report(ctx: MigrationContext): void {
|
|
|
136
198
|
lines.push("- [ ] `src/setup/section-loaders.ts` — verify extracted loaders work correctly");
|
|
137
199
|
lines.push("- [ ] `src/hooks/useCart.ts` — wire to actual server functions for your platform");
|
|
138
200
|
lines.push("- [ ] `src/worker-entry.ts` — verify CSP, proxy, and segment builder");
|
|
139
|
-
lines.push("- [ ] DaisyUI v4
|
|
140
|
-
lines.push("- [ ] Tailwind v3 to v4: verify all utility classes still work");
|
|
141
|
-
lines.push("- [ ] Check `src/styles/app.css` theme colors match the original design");
|
|
201
|
+
lines.push("- [ ] See the **CSS Migration** section above for DaisyUI v4→v5 / Tailwind v3→v4 findings specific to this site");
|
|
142
202
|
lines.push("- [ ] Run `npm run generate:blocks` and `npm run generate:schema` after migration");
|
|
143
203
|
lines.push("");
|
|
144
204
|
|
|
@@ -28,6 +28,7 @@ function makeCtx(sourceDir: string): MigrationContext {
|
|
|
28
28
|
importMap: {},
|
|
29
29
|
discoveredNpmDeps: {},
|
|
30
30
|
themeColors: {},
|
|
31
|
+
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
31
32
|
fontFamily: null,
|
|
32
33
|
files: [],
|
|
33
34
|
sectionMetas: [],
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { createContext } from "../types";
|
|
6
|
+
import { generateAppCss } from "./app-css";
|
|
7
|
+
|
|
8
|
+
describe("generateAppCss — tailwind.config.ts porting", () => {
|
|
9
|
+
it("emits custom colors, fonts, and breakpoints ported from tailwind.config.ts", () => {
|
|
10
|
+
const ctx = createContext("/tmp/does-not-exist");
|
|
11
|
+
ctx.tailwindConfig = {
|
|
12
|
+
colors: { perola: "#F5F0E8", "brand-500": "#B10200" },
|
|
13
|
+
fontFamily: { "bebas-neue": "Bebas Neue, sans-serif" },
|
|
14
|
+
screens: { "3xl": "1920px" },
|
|
15
|
+
safelist: ["bg-red-500", "text-brand-500"],
|
|
16
|
+
safelistPatterns: [],
|
|
17
|
+
plugins: [],
|
|
18
|
+
reviewItems: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const css = generateAppCss(ctx);
|
|
22
|
+
|
|
23
|
+
expect(css).toContain("--color-perola: #F5F0E8;");
|
|
24
|
+
expect(css).toContain("--color-brand-500: #B10200;");
|
|
25
|
+
expect(css).toContain("--font-bebas-neue: Bebas Neue, sans-serif;");
|
|
26
|
+
expect(css).toContain("--breakpoint-3xl: 1920px;");
|
|
27
|
+
expect(css).toContain('@source inline("bg-red-500 text-brand-500");');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("does not duplicate a custom color that collides with a daisyUI semantic key", () => {
|
|
31
|
+
const ctx = createContext("/tmp/does-not-exist");
|
|
32
|
+
ctx.tailwindConfig.colors = { primary: "#000000" };
|
|
33
|
+
|
|
34
|
+
const css = generateAppCss(ctx);
|
|
35
|
+
// The daisyUI theme's own --color-primary is emitted once in the plugin
|
|
36
|
+
// block; the ported tailwind.config.ts value must not add a second,
|
|
37
|
+
// conflicting @theme declaration.
|
|
38
|
+
const themeBlockMatch = css.match(/@theme \{[\s\S]*?\n\}/);
|
|
39
|
+
expect(themeBlockMatch).not.toBeNull();
|
|
40
|
+
const occurrences = (themeBlockMatch![0].match(/--color-primary:/g) ?? []).length;
|
|
41
|
+
expect(occurrences).toBe(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("flags unconvertible safelist regex patterns as manual review items", () => {
|
|
45
|
+
const ctx = createContext("/tmp/does-not-exist");
|
|
46
|
+
ctx.tailwindConfig.safelistPatterns = ["/^grid-cols-/"];
|
|
47
|
+
|
|
48
|
+
generateAppCss(ctx);
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
ctx.manualReviewItems.some((i) => i.reason.includes("/^grid-cols-/")),
|
|
52
|
+
).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("uses a custom tailwind.config.ts gray-* override instead of declaring the v3 default gray scale a second time", () => {
|
|
56
|
+
const ctx = createContext("/tmp/does-not-exist");
|
|
57
|
+
ctx.tailwindConfig.colors = { "gray-50": "#111111", "gray-500": "#222222" };
|
|
58
|
+
|
|
59
|
+
const css = generateAppCss(ctx);
|
|
60
|
+
const themeBlockMatch = css.match(/@theme \{[\s\S]*?\n\}/);
|
|
61
|
+
expect(themeBlockMatch).not.toBeNull();
|
|
62
|
+
const themeBlock = themeBlockMatch![0];
|
|
63
|
+
|
|
64
|
+
// gray-50/500 must be declared exactly once, with the custom value —
|
|
65
|
+
// not once with the custom value and once more with the hardcoded
|
|
66
|
+
// default (which would silently win via CSS's last-declaration-wins).
|
|
67
|
+
expect((themeBlock.match(/--color-gray-50:/g) ?? []).length).toBe(1);
|
|
68
|
+
expect(themeBlock).toContain("--color-gray-50: #111111;");
|
|
69
|
+
expect(themeBlock).not.toContain("--color-gray-50: #f9fafb;");
|
|
70
|
+
expect((themeBlock.match(/--color-gray-500:/g) ?? []).length).toBe(1);
|
|
71
|
+
expect(themeBlock).toContain("--color-gray-500: #222222;");
|
|
72
|
+
// Untouched shades still fall back to the v3 default.
|
|
73
|
+
expect(themeBlock).toContain("--color-gray-950: #030712;");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("generateAppCss — oklch fixer scope (gotcha #43)", () => {
|
|
78
|
+
let tmpDir: string;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "app-css-oklch-scope-test-"));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
afterEach(() => {
|
|
85
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not rewrite a hex/oklch(var()) pair declared in the site's own original CSS", () => {
|
|
89
|
+
fs.writeFileSync(
|
|
90
|
+
path.join(tmpDir, "tailwind.css"),
|
|
91
|
+
`@tailwind base;
|
|
92
|
+
@tailwind components;
|
|
93
|
+
@tailwind utilities;
|
|
94
|
+
|
|
95
|
+
:root {
|
|
96
|
+
--icon-color: #1a1a1a;
|
|
97
|
+
}
|
|
98
|
+
.icon {
|
|
99
|
+
fill: oklch(var(--icon-color));
|
|
100
|
+
}
|
|
101
|
+
`,
|
|
102
|
+
"utf-8",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const ctx = createContext(tmpDir);
|
|
106
|
+
const css = generateAppCss(ctx);
|
|
107
|
+
|
|
108
|
+
// The site's own declaration must survive untouched — the oklch fixer
|
|
109
|
+
// must not reach into content it doesn't own.
|
|
110
|
+
expect(css).toContain("--icon-color: #1a1a1a;");
|
|
111
|
+
expect(css).not.toMatch(/--icon-color:\s*[\d.]+\s+[\d.]+\s+[\d.]+;/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("generateAppCss — @apply rewrites classes behind a modifier prefix", () => {
|
|
116
|
+
let tmpDir: string;
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "app-css-apply-variant-test-"));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("renames a variant-prefixed class inside @apply, not just bare tokens", () => {
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(tmpDir, "tailwind.css"),
|
|
129
|
+
`@tailwind base;
|
|
130
|
+
@tailwind components;
|
|
131
|
+
@tailwind utilities;
|
|
132
|
+
|
|
133
|
+
@layer components {
|
|
134
|
+
.btn-brand {
|
|
135
|
+
@apply md:flex-grow hover:ring rounded;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
`,
|
|
139
|
+
"utf-8",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const ctx = createContext(tmpDir);
|
|
143
|
+
const css = generateAppCss(ctx);
|
|
144
|
+
|
|
145
|
+
expect(css).toContain("md:grow");
|
|
146
|
+
expect(css).toContain("hover:ring-3");
|
|
147
|
+
expect(css).not.toContain("md:flex-grow");
|
|
148
|
+
expect(css).not.toMatch(/hover:ring(?!-3)/);
|
|
149
|
+
});
|
|
150
|
+
});
|