@decocms/blocks-cli 7.21.0 → 7.22.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/package.json +4 -3
- package/scripts/lib/upgrade-6-to-7.test.ts +145 -0
- package/scripts/lib/upgrade-6-to-7.ts +157 -0
- 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-legacy-specifiers.test.ts +134 -0
- package/scripts/migrate/phase-verify.test.ts +1 -0
- package/scripts/migrate/phase-verify.ts +24 -0
- package/scripts/migrate/post-cleanup/migration-artifacts.test.ts +262 -0
- package/scripts/migrate/post-cleanup/rules.ts +321 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +2 -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/templates/server-entry-vtex-invoke.test.ts +51 -0
- package/scripts/migrate/templates/server-entry.ts +53 -1
- 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
- package/scripts/upgrade-6-to-7.test.ts +80 -0
- package/scripts/upgrade-6-to-7.ts +169 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { transformTailwind } from "./tailwind";
|
|
3
|
+
|
|
4
|
+
describe("transformTailwind — v3 -> v4 class renames", () => {
|
|
5
|
+
it("renames the flex-grow/shrink family", () => {
|
|
6
|
+
const r = transformTailwind(`<div className="flex-grow-0 flex-shrink" />`);
|
|
7
|
+
expect(r.content).toContain('className="grow-0 shrink"');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("removes transform/filter (v4 applies them automatically)", () => {
|
|
11
|
+
const r = transformTailwind(`<div className="transform filter scale-105" />`);
|
|
12
|
+
expect(r.content).toContain('className="scale-105"');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("bumps the default ring width (v3 3px -> v4 ring-3)", () => {
|
|
16
|
+
const r = transformTailwind(`<div className="ring ring-blue-500" />`);
|
|
17
|
+
expect(r.content).toContain("ring-3 ring-blue-500");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("shifts the shadow scale down one step without cascading (single-pass map lookup)", () => {
|
|
21
|
+
const r = transformTailwind(`<div className="shadow-sm" />`);
|
|
22
|
+
expect(r.content).toContain('className="shadow-xs"');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("shifts shadow, blur, rounded, and drop-shadow scales independently", () => {
|
|
26
|
+
const r = transformTailwind(
|
|
27
|
+
`<div className="shadow blur rounded drop-shadow" />`,
|
|
28
|
+
);
|
|
29
|
+
expect(r.content).toContain("shadow-sm");
|
|
30
|
+
expect(r.content).toContain("blur-sm");
|
|
31
|
+
expect(r.content).toContain("rounded-sm");
|
|
32
|
+
expect(r.content).toContain("drop-shadow-sm");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("renames outline-none to outline-hidden", () => {
|
|
36
|
+
const r = transformTailwind(`<div className="outline-none" />`);
|
|
37
|
+
expect(r.content).toContain('className="outline-hidden"');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("renames bg-gradient-to-* to bg-linear-to-*", () => {
|
|
41
|
+
const r = transformTailwind(`<div className="bg-gradient-to-r from-red-500" />`);
|
|
42
|
+
expect(r.content).toContain("bg-linear-to-r");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("renames known daisyUI v4 -> v5 classes", () => {
|
|
46
|
+
const r = transformTailwind(`<div className="badge-ghost card-compact" />`);
|
|
47
|
+
expect(r.content).toContain("badge-soft");
|
|
48
|
+
expect(r.content).toContain("card-sm");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("transformTailwind — gotcha detection (flag, don't auto-fix)", () => {
|
|
53
|
+
it("flags DaisyUI .collapse usage as a manual-review finding", () => {
|
|
54
|
+
const r = transformTailwind(
|
|
55
|
+
`<div className="collapse"><div className="collapse-title" /></div>`,
|
|
56
|
+
);
|
|
57
|
+
expect(r.notes.some((n) => n.startsWith("MANUAL:") && n.includes("collapse"))).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("flags btn-group and form-control as removed-in-v5", () => {
|
|
61
|
+
const r = transformTailwind(`<div className="btn-group" /><div className="form-control" />`);
|
|
62
|
+
expect(r.notes.filter((n) => n.startsWith("MANUAL:")).length).toBeGreaterThanOrEqual(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("flags a shorthand/longhand spacing conflict (px-* mixed with pl-*/pr-*)", () => {
|
|
66
|
+
const r = transformTailwind(`<div className="pl-4 sm:pl-0 md:px-6" />`);
|
|
67
|
+
expect(r.notes.some((n) => n.startsWith("MANUAL:") && n.includes("padding-inline"))).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("does not flag px-* used alone (no mixed longhand)", () => {
|
|
71
|
+
const r = transformTailwind(`<div className="px-4 md:px-6" />`);
|
|
72
|
+
expect(r.notes.some((n) => n.includes("padding-inline"))).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { TransformResult } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
PX_TO_SPACING,
|
|
4
|
+
TEXT_SIZE_MAP,
|
|
5
|
+
detectDaisyUiV5StructuralIssues,
|
|
6
|
+
detectLogicalPropertyConflict,
|
|
7
|
+
renameToken,
|
|
8
|
+
} from "./tailwind-renames";
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* Tailwind v3 → v4 class migration transform.
|
|
@@ -9,6 +16,8 @@ import type { TransformResult } from "../types";
|
|
|
9
16
|
* 3. Responsive class ordering (base → sm → md → lg → xl → 2xl)
|
|
10
17
|
* 4. Arbitrary values → native equivalents (px-[16px] → px-4)
|
|
11
18
|
* 5. Deprecated patterns
|
|
19
|
+
* 6. Flags (does not auto-fix) DaisyUI v5 structural breaks and v4
|
|
20
|
+
* logical-vs-physical spacing property conflicts
|
|
12
21
|
*/
|
|
13
22
|
|
|
14
23
|
// ── Breakpoint order (mobile-first) ─────────────────────────────
|
|
@@ -18,111 +27,6 @@ BREAKPOINT_ORDER.forEach((bp, i) => {
|
|
|
18
27
|
BP_INDEX[bp] = i + 1; // base = 0
|
|
19
28
|
});
|
|
20
29
|
|
|
21
|
-
// ── Tailwind v3 → v4 class renames ──────────────────────────────
|
|
22
|
-
// These are direct 1:1 replacements
|
|
23
|
-
const CLASS_RENAMES: Record<string, string> = {
|
|
24
|
-
// Flexbox/Grid
|
|
25
|
-
"flex-grow-0": "grow-0",
|
|
26
|
-
"flex-grow": "grow",
|
|
27
|
-
"flex-shrink-0": "shrink-0",
|
|
28
|
-
"flex-shrink": "shrink",
|
|
29
|
-
|
|
30
|
-
// Overflow
|
|
31
|
-
"overflow-ellipsis": "text-ellipsis",
|
|
32
|
-
|
|
33
|
-
// Decoration
|
|
34
|
-
"decoration-clone": "box-decoration-clone",
|
|
35
|
-
"decoration-slice": "box-decoration-slice",
|
|
36
|
-
|
|
37
|
-
// Transforms (v4 applies transforms automatically)
|
|
38
|
-
"transform": "", // remove — v4 applies automatically
|
|
39
|
-
"transform-gpu": "",
|
|
40
|
-
"transform-none": "transform-none", // this one stays
|
|
41
|
-
|
|
42
|
-
// Blur/filter (v4 applies automatically)
|
|
43
|
-
"filter": "", // remove
|
|
44
|
-
"backdrop-filter": "", // remove
|
|
45
|
-
|
|
46
|
-
// Ring width default
|
|
47
|
-
"ring": "ring-3", // v4 changed default from 3px to 1px
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// ── DaisyUI v4 → v5 class renames ──────────────────────────────
|
|
51
|
-
const DAISYUI_RENAMES: Record<string, string> = {
|
|
52
|
-
// Button changes
|
|
53
|
-
"btn-ghost": "btn-ghost", // kept
|
|
54
|
-
"btn-outline": "btn-outline", // kept
|
|
55
|
-
"btn-active": "btn-active", // kept
|
|
56
|
-
|
|
57
|
-
// Alert/Badge
|
|
58
|
-
"badge-ghost": "badge-soft",
|
|
59
|
-
"alert-info": "alert-info",
|
|
60
|
-
"alert-success": "alert-success",
|
|
61
|
-
"alert-warning": "alert-warning",
|
|
62
|
-
"alert-error": "alert-error",
|
|
63
|
-
|
|
64
|
-
// Card
|
|
65
|
-
"card-compact": "card-sm",
|
|
66
|
-
|
|
67
|
-
// Modal
|
|
68
|
-
"modal-open": "modal-open",
|
|
69
|
-
|
|
70
|
-
// Drawer
|
|
71
|
-
"drawer-end": "drawer-end",
|
|
72
|
-
|
|
73
|
-
// Menu
|
|
74
|
-
"menu-horizontal": "menu-horizontal",
|
|
75
|
-
|
|
76
|
-
// Toast position classes (daisy v5 uses different system)
|
|
77
|
-
"toast-top": "toast-top",
|
|
78
|
-
"toast-bottom": "toast-bottom",
|
|
79
|
-
"toast-center": "toast-center",
|
|
80
|
-
"toast-end": "toast-end",
|
|
81
|
-
"toast-start": "toast-start",
|
|
82
|
-
"toast-middle": "toast-middle",
|
|
83
|
-
|
|
84
|
-
// Loading
|
|
85
|
-
"loading-spinner": "loading-spinner",
|
|
86
|
-
"loading-dots": "loading-dots",
|
|
87
|
-
"loading-ring": "loading-ring",
|
|
88
|
-
"loading-ball": "loading-ball",
|
|
89
|
-
"loading-bars": "loading-bars",
|
|
90
|
-
"loading-infinity": "loading-infinity",
|
|
91
|
-
|
|
92
|
-
// Sizes (daisy v5 naming)
|
|
93
|
-
"btn-xs": "btn-xs",
|
|
94
|
-
"btn-sm": "btn-sm",
|
|
95
|
-
"btn-md": "btn-md",
|
|
96
|
-
"btn-lg": "btn-lg",
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// ── Spacing scale: px → Tailwind unit ───────────────────────────
|
|
100
|
-
const PX_TO_SPACING: Record<number, string> = {};
|
|
101
|
-
for (let i = 0; i <= 96; i++) {
|
|
102
|
-
PX_TO_SPACING[i * 4] = String(i);
|
|
103
|
-
}
|
|
104
|
-
PX_TO_SPACING[2] = "0.5";
|
|
105
|
-
PX_TO_SPACING[6] = "1.5";
|
|
106
|
-
PX_TO_SPACING[10] = "2.5";
|
|
107
|
-
PX_TO_SPACING[14] = "3.5";
|
|
108
|
-
|
|
109
|
-
// Text size: px → native class
|
|
110
|
-
const TEXT_SIZE_MAP: Record<string, string> = {
|
|
111
|
-
"12": "xs",
|
|
112
|
-
"14": "sm",
|
|
113
|
-
"16": "base",
|
|
114
|
-
"18": "lg",
|
|
115
|
-
"20": "xl",
|
|
116
|
-
"24": "2xl",
|
|
117
|
-
"30": "3xl",
|
|
118
|
-
"36": "4xl",
|
|
119
|
-
"48": "5xl",
|
|
120
|
-
"60": "6xl",
|
|
121
|
-
"72": "7xl",
|
|
122
|
-
"96": "8xl",
|
|
123
|
-
"128": "9xl",
|
|
124
|
-
};
|
|
125
|
-
|
|
126
30
|
// Properties that accept spacing values
|
|
127
31
|
const SPACING_PROPS = new Set([
|
|
128
32
|
"p", "px", "py", "pt", "pb", "pl", "pr",
|
|
@@ -200,24 +104,7 @@ function parseClass(cls: string): ParsedClass {
|
|
|
200
104
|
|
|
201
105
|
// ── Fix class renames ───────────────────────────────────────────
|
|
202
106
|
function fixRenames(cls: string): string {
|
|
203
|
-
|
|
204
|
-
const utility = parts.pop()!;
|
|
205
|
-
|
|
206
|
-
// Check direct rename
|
|
207
|
-
if (CLASS_RENAMES[utility] !== undefined) {
|
|
208
|
-
const renamed = CLASS_RENAMES[utility];
|
|
209
|
-
if (renamed === "") return ""; // Remove class entirely
|
|
210
|
-
parts.push(renamed);
|
|
211
|
-
return parts.join(":");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Check DaisyUI rename
|
|
215
|
-
if (DAISYUI_RENAMES[utility] && DAISYUI_RENAMES[utility] !== utility) {
|
|
216
|
-
parts.push(DAISYUI_RENAMES[utility]);
|
|
217
|
-
return parts.join(":");
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return cls;
|
|
107
|
+
return renameToken(cls);
|
|
221
108
|
}
|
|
222
109
|
|
|
223
110
|
// ── Fix arbitrary values ────────────────────────────────────────
|
|
@@ -402,6 +289,11 @@ function fixClassNameString(classes: string): { fixed: string; changes: string[]
|
|
|
402
289
|
}
|
|
403
290
|
}
|
|
404
291
|
|
|
292
|
+
// 5. Flag (don't auto-fix) shorthand/longhand spacing conflicts — gotcha #42
|
|
293
|
+
for (const finding of detectLogicalPropertyConflict(classList)) {
|
|
294
|
+
changes.push(`MANUAL: ${finding.message}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
405
297
|
return { fixed: classList.join(" "), changes };
|
|
406
298
|
}
|
|
407
299
|
|
|
@@ -626,6 +518,18 @@ export function transformTailwind(content: string): TransformResult {
|
|
|
626
518
|
let changed = false;
|
|
627
519
|
let result = content;
|
|
628
520
|
|
|
521
|
+
// ── Flag (don't auto-fix) DaisyUI v5 structural breaks — gotcha #37 ──
|
|
522
|
+
const daisyFindings = detectDaisyUiV5StructuralIssues(result);
|
|
523
|
+
if (daisyFindings.length > 0) {
|
|
524
|
+
// No content is rewritten here, but the caller (phase-transform) only
|
|
525
|
+
// surfaces `notes` when `changed` is true — mark it so these
|
|
526
|
+
// manual-review findings aren't silently dropped.
|
|
527
|
+
changed = true;
|
|
528
|
+
for (const finding of daisyFindings) {
|
|
529
|
+
notes.push(`MANUAL: ${finding.message}`);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
629
533
|
// ── Fix negative z-index on background images ──────────────────
|
|
630
534
|
const zFix = fixNegativeZIndex(result);
|
|
631
535
|
if (zFix.changed) {
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -157,6 +157,15 @@ export interface MigrationContext {
|
|
|
157
157
|
/** Font family from CMS config */
|
|
158
158
|
fontFamily: string | null;
|
|
159
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Statically-extracted content of the source site's `tailwind.config.ts`
|
|
162
|
+
* (colors, fontFamily, screens, safelist, plugins) — read during analyze,
|
|
163
|
+
* before the file is deleted in cleanup, so its content can be ported into
|
|
164
|
+
* the scaffolded `src/styles/app.css` @theme block instead of silently
|
|
165
|
+
* dropped. See analyzers/tailwind-config.ts.
|
|
166
|
+
*/
|
|
167
|
+
tailwindConfig: import("./analyzers/tailwind-config").TailwindConfigExtract;
|
|
168
|
+
|
|
160
169
|
/** All categorized source files */
|
|
161
170
|
files: FileRecord[];
|
|
162
171
|
|
|
@@ -217,6 +226,15 @@ export function createContext(
|
|
|
217
226
|
discoveredNpmDeps: {},
|
|
218
227
|
themeColors: {},
|
|
219
228
|
fontFamily: null,
|
|
229
|
+
tailwindConfig: {
|
|
230
|
+
colors: {},
|
|
231
|
+
fontFamily: {},
|
|
232
|
+
screens: {},
|
|
233
|
+
safelist: [],
|
|
234
|
+
safelistPatterns: [],
|
|
235
|
+
plugins: [],
|
|
236
|
+
reviewItems: [],
|
|
237
|
+
},
|
|
220
238
|
files: [],
|
|
221
239
|
sectionMetas: [],
|
|
222
240
|
islandClassifications: [],
|
package/scripts/tailwind-lint.ts
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
|
|
19
19
|
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
|
|
20
20
|
import { join, relative } from "node:path";
|
|
21
|
+
import {
|
|
22
|
+
CLASS_RENAMES,
|
|
23
|
+
DAISYUI_RENAMES,
|
|
24
|
+
PX_TO_SPACING,
|
|
25
|
+
TEXT_SIZE_MAP,
|
|
26
|
+
renameToken,
|
|
27
|
+
} from "./migrate/transforms/tailwind-renames";
|
|
21
28
|
|
|
22
29
|
// ── Breakpoint order (mobile-first) ─────────────────────────────
|
|
23
30
|
const BREAKPOINT_ORDER = ["sm", "md", "lg", "xl", "2xl"] as const;
|
|
@@ -26,44 +33,6 @@ BREAKPOINT_ORDER.forEach((bp, i) => {
|
|
|
26
33
|
BP_INDEX[bp] = i + 1;
|
|
27
34
|
});
|
|
28
35
|
|
|
29
|
-
// ── Tailwind v3 → v4 class renames ──────────────────────────────
|
|
30
|
-
const CLASS_RENAMES: Record<string, string> = {
|
|
31
|
-
"flex-grow-0": "grow-0",
|
|
32
|
-
"flex-grow": "grow",
|
|
33
|
-
"flex-shrink-0": "shrink-0",
|
|
34
|
-
"flex-shrink": "shrink",
|
|
35
|
-
"overflow-ellipsis": "text-ellipsis",
|
|
36
|
-
"decoration-clone": "box-decoration-clone",
|
|
37
|
-
"decoration-slice": "box-decoration-slice",
|
|
38
|
-
"transform": "",
|
|
39
|
-
"transform-gpu": "",
|
|
40
|
-
"filter": "",
|
|
41
|
-
"backdrop-filter": "",
|
|
42
|
-
"ring": "ring-3",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// ── DaisyUI v4 → v5 class renames ──────────────────────────────
|
|
46
|
-
const DAISYUI_RENAMES: Record<string, string> = {
|
|
47
|
-
"badge-ghost": "badge-soft",
|
|
48
|
-
"card-compact": "card-sm",
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// ── Spacing scale ───────────────────────────────────────────────
|
|
52
|
-
const PX_TO_SPACING: Record<number, string> = {};
|
|
53
|
-
for (let i = 0; i <= 96; i++) {
|
|
54
|
-
PX_TO_SPACING[i * 4] = String(i);
|
|
55
|
-
}
|
|
56
|
-
PX_TO_SPACING[2] = "0.5";
|
|
57
|
-
PX_TO_SPACING[6] = "1.5";
|
|
58
|
-
PX_TO_SPACING[10] = "2.5";
|
|
59
|
-
PX_TO_SPACING[14] = "3.5";
|
|
60
|
-
|
|
61
|
-
const TEXT_SIZE_MAP: Record<string, string> = {
|
|
62
|
-
"12": "xs", "14": "sm", "16": "base", "18": "lg", "20": "xl",
|
|
63
|
-
"24": "2xl", "30": "3xl", "36": "4xl", "48": "5xl", "60": "6xl",
|
|
64
|
-
"72": "7xl", "96": "8xl", "128": "9xl",
|
|
65
|
-
};
|
|
66
|
-
|
|
67
36
|
const SPACING_PROPS = new Set([
|
|
68
37
|
"p", "px", "py", "pt", "pb", "pl", "pr",
|
|
69
38
|
"m", "mx", "my", "mt", "mb", "ml", "mr",
|
|
@@ -247,7 +216,7 @@ function scanFile(filePath: string): Issue[] {
|
|
|
247
216
|
for (const cls of classList) {
|
|
248
217
|
const parts = cls.split(":");
|
|
249
218
|
const utility = parts[parts.length - 1];
|
|
250
|
-
if (CLASS_RENAMES[utility] !== undefined) {
|
|
219
|
+
if (CLASS_RENAMES[utility] !== undefined && CLASS_RENAMES[utility] !== utility) {
|
|
251
220
|
const renamed = CLASS_RENAMES[utility];
|
|
252
221
|
issues.push({
|
|
253
222
|
file: filePath, line, type: "rename",
|
|
@@ -291,21 +260,7 @@ function fixClassOrder(classes: string): string {
|
|
|
291
260
|
function fixClassName(classes: string): string {
|
|
292
261
|
let classList = classes.split(/\s+/).filter(Boolean);
|
|
293
262
|
|
|
294
|
-
classList = classList.map((
|
|
295
|
-
const parts = cls.split(":");
|
|
296
|
-
const utility = parts.pop()!;
|
|
297
|
-
if (CLASS_RENAMES[utility] !== undefined) {
|
|
298
|
-
const renamed = CLASS_RENAMES[utility];
|
|
299
|
-
if (renamed === "") return "";
|
|
300
|
-
parts.push(renamed);
|
|
301
|
-
return parts.join(":");
|
|
302
|
-
}
|
|
303
|
-
if (DAISYUI_RENAMES[utility] && DAISYUI_RENAMES[utility] !== utility) {
|
|
304
|
-
parts.push(DAISYUI_RENAMES[utility]);
|
|
305
|
-
return parts.join(":");
|
|
306
|
-
}
|
|
307
|
-
return cls;
|
|
308
|
-
}).filter(Boolean);
|
|
263
|
+
classList = classList.map(renameToken).filter(Boolean);
|
|
309
264
|
|
|
310
265
|
classList = classList.map((cls) => {
|
|
311
266
|
if (!cls.includes("[")) return cls;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke tests for the `deco-upgrade-6-to-7` CLI (#367) — drives the script
|
|
3
|
+
* as a child process against a tmp site fixture, verifying:
|
|
4
|
+
* - dry-run prints what would change without touching disk
|
|
5
|
+
* - --write applies the import-specifier rewrites and package.json swap
|
|
6
|
+
* - second --write run is a no-op (idempotency)
|
|
7
|
+
*/
|
|
8
|
+
import * as cp from "node:child_process";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
13
|
+
|
|
14
|
+
const SCRIPT = path.resolve(__dirname, "upgrade-6-to-7.ts");
|
|
15
|
+
|
|
16
|
+
function run(cwd: string, args: string[]): { stdout: string; code: number } {
|
|
17
|
+
const r = cp.spawnSync("npx", ["tsx", SCRIPT, ...args], { cwd, encoding: "utf8" });
|
|
18
|
+
return { stdout: r.stdout || "", code: r.status ?? 0 };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("deco-upgrade-6-to-7 CLI (#367)", () => {
|
|
22
|
+
let tmp: string;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "upgrade-6-to-7-"));
|
|
26
|
+
fs.mkdirSync(path.join(tmp, "src", "routes"), { recursive: true });
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(tmp, "src", "routes", "$.tsx"),
|
|
29
|
+
`import { cmsRouteConfig } from "@decocms/start/routes";\n` +
|
|
30
|
+
`import { DecoPageRenderer } from "@decocms/start/hooks";\n`,
|
|
31
|
+
);
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(tmp, "package.json"),
|
|
34
|
+
JSON.stringify(
|
|
35
|
+
{ name: "fixture-site", dependencies: { "@decocms/start": "6.30.0", "@decocms/apps": "5.4.0" } },
|
|
36
|
+
null,
|
|
37
|
+
2,
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("dry-run reports what would change without writing", () => {
|
|
47
|
+
const { stdout, code } = run(tmp, []);
|
|
48
|
+
expect(code).toBe(0);
|
|
49
|
+
expect(stdout).toContain("would upgrade");
|
|
50
|
+
expect(stdout).toContain("removing @decocms/start, @decocms/apps");
|
|
51
|
+
|
|
52
|
+
// Nothing actually changed on disk
|
|
53
|
+
const src = fs.readFileSync(path.join(tmp, "src", "routes", "$.tsx"), "utf-8");
|
|
54
|
+
expect(src).toContain("@decocms/start/routes");
|
|
55
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(tmp, "package.json"), "utf-8"));
|
|
56
|
+
expect(pkg.dependencies).toHaveProperty("@decocms/start");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("--write rewrites specifiers and package.json dependencies", () => {
|
|
60
|
+
const { code } = run(tmp, ["--write"]);
|
|
61
|
+
expect(code).toBe(0);
|
|
62
|
+
|
|
63
|
+
const src = fs.readFileSync(path.join(tmp, "src", "routes", "$.tsx"), "utf-8");
|
|
64
|
+
expect(src).toContain('import { cmsRouteConfig } from "@decocms/tanstack";');
|
|
65
|
+
expect(src).toContain('import { DecoPageRenderer } from "@decocms/tanstack";');
|
|
66
|
+
expect(src).not.toContain("@decocms/start");
|
|
67
|
+
|
|
68
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(tmp, "package.json"), "utf-8"));
|
|
69
|
+
expect(pkg.dependencies).not.toHaveProperty("@decocms/start");
|
|
70
|
+
expect(pkg.dependencies).not.toHaveProperty("@decocms/apps");
|
|
71
|
+
expect(pkg.dependencies).toHaveProperty("@decocms/tanstack");
|
|
72
|
+
expect(pkg.dependencies).toHaveProperty("@decocms/blocks");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("second --write run is idempotent (no-op)", () => {
|
|
76
|
+
run(tmp, ["--write"]);
|
|
77
|
+
const { stdout } = run(tmp, ["--write"]);
|
|
78
|
+
expect(stdout).toContain("Upgraded 0 file(s).");
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Upgrade a site off the frozen pre-split `@decocms/start@6.30.0` /
|
|
4
|
+
* `@decocms/apps@5.4.0` packages onto the current 7.x split
|
|
5
|
+
* (`@decocms/tanstack` + `@decocms/blocks` + `@decocms/blocks-admin` +
|
|
6
|
+
* `@decocms/apps-*`). See #367.
|
|
7
|
+
*
|
|
8
|
+
* This is NOT the Fresh→TanStack migrator (`deco-migrate`) — it assumes the
|
|
9
|
+
* site is already on TanStack Start and only needs its package dependency
|
|
10
|
+
* moved to the current split. Rewrites import specifiers (including the
|
|
11
|
+
* `@decocms/start/routes` / `@decocms/start/hooks` symbol fan-out and the
|
|
12
|
+
* `decoMetaRoute`/`RenderSection` renames — see scripts/lib/upgrade-6-to-7.ts
|
|
13
|
+
* for the full mapping) and updates package.json dependencies.
|
|
14
|
+
*
|
|
15
|
+
* Usage (from the site root):
|
|
16
|
+
* # dry-run (default): prints every file that would change, no writes
|
|
17
|
+
* npx -p @decocms/blocks-cli deco-upgrade-6-to-7
|
|
18
|
+
* # apply:
|
|
19
|
+
* npx -p @decocms/blocks-cli deco-upgrade-6-to-7 --write
|
|
20
|
+
*
|
|
21
|
+
* Options:
|
|
22
|
+
* --src-dir <dir> Source dir to scan (default: src)
|
|
23
|
+
* --write Apply changes (otherwise dry-run, exit 0)
|
|
24
|
+
* --help, -h Show this help
|
|
25
|
+
*
|
|
26
|
+
* Exit codes: 0 ok / dry-run; 2 error (bad dir)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as fs from "node:fs";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import { upgradeFileToV7 } from "./lib/upgrade-6-to-7";
|
|
32
|
+
|
|
33
|
+
const FRAMEWORK_DEPS = [
|
|
34
|
+
"@decocms/blocks",
|
|
35
|
+
"@decocms/blocks-admin",
|
|
36
|
+
"@decocms/tanstack",
|
|
37
|
+
"@decocms/apps-commerce",
|
|
38
|
+
];
|
|
39
|
+
const PLATFORM_APPS_PREFIX = "@decocms/apps-";
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv: string[]) {
|
|
42
|
+
const has = (f: string) => argv.includes(f);
|
|
43
|
+
const val = (f: string, d: string) => {
|
|
44
|
+
const i = argv.indexOf(f);
|
|
45
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : d;
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
help: has("--help") || has("-h"),
|
|
49
|
+
write: has("--write"),
|
|
50
|
+
srcDir: val("--src-dir", "src"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function walkTsFiles(dir: string, out: string[] = []): string[] {
|
|
55
|
+
if (!fs.existsSync(dir)) return out;
|
|
56
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
57
|
+
const full = path.join(dir, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
60
|
+
walkTsFiles(full, out);
|
|
61
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
62
|
+
out.push(full);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Removes @decocms/start and @decocms/apps (bare, monolith) from a deps
|
|
69
|
+
* object, and ensures the current split packages are present. Site- and
|
|
70
|
+
* platform-specific version pins are left to `bun install`/`npm install`
|
|
71
|
+
* against the fallback "latest" this writes — sites should pin exact
|
|
72
|
+
* versions afterward the same way the scaffolder does. */
|
|
73
|
+
function upgradeDeps(deps: Record<string, string> | undefined): {
|
|
74
|
+
next: Record<string, string>;
|
|
75
|
+
removed: string[];
|
|
76
|
+
added: string[];
|
|
77
|
+
} {
|
|
78
|
+
const next: Record<string, string> = { ...(deps ?? {}) };
|
|
79
|
+
const removed: string[] = [];
|
|
80
|
+
for (const key of Object.keys(next)) {
|
|
81
|
+
if (key === "@decocms/start" || key === "@decocms/apps") {
|
|
82
|
+
delete next[key];
|
|
83
|
+
removed.push(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const added: string[] = [];
|
|
87
|
+
if (removed.length > 0) {
|
|
88
|
+
for (const dep of FRAMEWORK_DEPS) {
|
|
89
|
+
if (!next[dep]) {
|
|
90
|
+
next[dep] = "latest";
|
|
91
|
+
added.push(dep);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { next, removed, added };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function upgradePackageJson(pkgPath: string, write: boolean): void {
|
|
99
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
100
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
101
|
+
|
|
102
|
+
const depsResult = upgradeDeps(pkg.dependencies);
|
|
103
|
+
const devDepsResult = upgradeDeps(pkg.devDependencies);
|
|
104
|
+
|
|
105
|
+
const removed = [...depsResult.removed, ...devDepsResult.removed];
|
|
106
|
+
if (removed.length === 0) return;
|
|
107
|
+
|
|
108
|
+
console.log(` package.json: removing ${removed.join(", ")}`);
|
|
109
|
+
const added = [...new Set([...depsResult.added, ...devDepsResult.added])];
|
|
110
|
+
if (added.length > 0) {
|
|
111
|
+
console.log(` package.json: adding ${added.join(", ")} at "latest" — pin exact versions after install`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!write) return;
|
|
115
|
+
pkg.dependencies = depsResult.next;
|
|
116
|
+
pkg.devDependencies = devDepsResult.next;
|
|
117
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function main() {
|
|
121
|
+
const args = parseArgs(process.argv.slice(2));
|
|
122
|
+
if (args.help) {
|
|
123
|
+
console.log(
|
|
124
|
+
"Usage: deco-upgrade-6-to-7 [--src-dir <dir>] [--write]\n\n" +
|
|
125
|
+
"Upgrades a site off @decocms/start@6.30.0 / @decocms/apps@5.4.0 onto the\n" +
|
|
126
|
+
"current 7.x split. Dry-run by default; pass --write to apply.",
|
|
127
|
+
);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const cwd = process.cwd();
|
|
132
|
+
const srcDir = path.resolve(cwd, args.srcDir);
|
|
133
|
+
if (!fs.existsSync(srcDir)) {
|
|
134
|
+
console.error(`Source dir not found: ${srcDir}`);
|
|
135
|
+
process.exitCode = 2;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const files = walkTsFiles(srcDir);
|
|
140
|
+
let changedCount = 0;
|
|
141
|
+
const allNotes: Array<{ file: string; note: string }> = [];
|
|
142
|
+
|
|
143
|
+
for (const abs of files) {
|
|
144
|
+
const content = fs.readFileSync(abs, "utf-8");
|
|
145
|
+
const result = upgradeFileToV7(content);
|
|
146
|
+
if (!result.changed) continue;
|
|
147
|
+
|
|
148
|
+
changedCount++;
|
|
149
|
+
const rel = path.relative(cwd, abs);
|
|
150
|
+
console.log(`${args.write ? "upgraded" : "would upgrade"}: ${rel}`);
|
|
151
|
+
for (const note of result.notes) allNotes.push({ file: rel, note });
|
|
152
|
+
|
|
153
|
+
if (args.write) fs.writeFileSync(abs, result.content);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
upgradePackageJson(path.join(cwd, "package.json"), args.write);
|
|
157
|
+
|
|
158
|
+
if (allNotes.length > 0) {
|
|
159
|
+
console.log("\nManual review needed:");
|
|
160
|
+
for (const { file, note } of allNotes) console.log(` [${file}] ${note}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log(
|
|
164
|
+
`\n${args.write ? "Upgraded" : "Would upgrade"} ${changedCount} file(s).` +
|
|
165
|
+
(args.write ? "" : " Re-run with --write to apply."),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
main();
|