@decocms/blocks-cli 7.32.1 → 7.33.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.ts +95 -1
- package/scripts/migrate/phase-transform.ts +31 -0
- package/scripts/migrate/templates/app-css.test.ts +24 -0
- package/scripts/migrate/templates/app-css.ts +27 -0
- package/scripts/migrate/transforms/tailwind-renames.test.ts +29 -0
- package/scripts/migrate/transforms/tailwind-renames.ts +34 -0
- package/scripts/migrate/types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.33.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint:unused": "knip"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@decocms/blocks": "7.
|
|
34
|
+
"@decocms/blocks": "7.33.1",
|
|
35
35
|
"ts-morph": "^27.0.0",
|
|
36
36
|
"tsx": "^4.22.5"
|
|
37
37
|
},
|
|
@@ -24,6 +24,10 @@ export interface TailwindConfigExtract {
|
|
|
24
24
|
fontFamily: Record<string, string>;
|
|
25
25
|
/** Custom breakpoints, e.g. "3xl" -> "1920px" */
|
|
26
26
|
screens: Record<string, string>;
|
|
27
|
+
/** Custom animations, e.g. "fade-in" -> "fadeIn 0.5s ease-in" */
|
|
28
|
+
animations: Record<string, string>;
|
|
29
|
+
/** Custom keyframe stops, e.g. "fadeIn" -> { "0%": "opacity: 0", "100%": "opacity: 1" } */
|
|
30
|
+
keyframes: Record<string, Record<string, string>>;
|
|
27
31
|
/** Literal safelist entries (string class names) */
|
|
28
32
|
safelist: string[];
|
|
29
33
|
/** Safelist regex patterns, kept as their source text */
|
|
@@ -40,6 +44,8 @@ function emptyExtract(): TailwindConfigExtract {
|
|
|
40
44
|
colors: {},
|
|
41
45
|
fontFamily: {},
|
|
42
46
|
screens: {},
|
|
47
|
+
animations: {},
|
|
48
|
+
keyframes: {},
|
|
43
49
|
safelist: [],
|
|
44
50
|
safelistPatterns: [],
|
|
45
51
|
plugins: [],
|
|
@@ -184,6 +190,84 @@ function extractScreens(obj: ObjectLiteralExpression): Record<string, string> {
|
|
|
184
190
|
return result;
|
|
185
191
|
}
|
|
186
192
|
|
|
193
|
+
function extractAnimations(
|
|
194
|
+
obj: ObjectLiteralExpression,
|
|
195
|
+
reviewItems: ReviewItem[],
|
|
196
|
+
): Record<string, string> {
|
|
197
|
+
const result: Record<string, string> = {};
|
|
198
|
+
for (const prop of obj.getProperties()) {
|
|
199
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
200
|
+
const name = stripQuotes(prop.getName());
|
|
201
|
+
const init = prop.getInitializer();
|
|
202
|
+
if (!init) continue;
|
|
203
|
+
if (
|
|
204
|
+
init.isKind(SyntaxKind.StringLiteral) ||
|
|
205
|
+
init.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
206
|
+
) {
|
|
207
|
+
result[name] = init.getLiteralText();
|
|
208
|
+
} else {
|
|
209
|
+
reviewItems.push({
|
|
210
|
+
file: CONFIG_REL_PATH,
|
|
211
|
+
reason: `theme.extend.animation.${name} is not a static string literal (${init.getKindName()}) — port manually to @theme { --animate-${name}: ...; } in src/styles/app.css`,
|
|
212
|
+
severity: "warning",
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function extractKeyframes(
|
|
220
|
+
obj: ObjectLiteralExpression,
|
|
221
|
+
reviewItems: ReviewItem[],
|
|
222
|
+
): Record<string, Record<string, string>> {
|
|
223
|
+
const result: Record<string, Record<string, string>> = {};
|
|
224
|
+
for (const frameProp of obj.getProperties()) {
|
|
225
|
+
if (!frameProp.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
226
|
+
const frameName = stripQuotes(frameProp.getName());
|
|
227
|
+
const frameInit = frameProp.getInitializer();
|
|
228
|
+
if (!frameInit?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
229
|
+
reviewItems.push({
|
|
230
|
+
file: CONFIG_REL_PATH,
|
|
231
|
+
reason: `theme.extend.keyframes.${frameName} is not an object literal — port manually to @keyframes ${frameName} { ... } in src/styles/app.css`,
|
|
232
|
+
severity: "warning",
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const stops: Record<string, string> = {};
|
|
237
|
+
for (const stopProp of frameInit.getProperties()) {
|
|
238
|
+
if (!stopProp.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
239
|
+
const stopKey = stripQuotes(stopProp.getName()); // "0%", "100%", "from", "to"
|
|
240
|
+
const stopInit = stopProp.getInitializer();
|
|
241
|
+
if (!stopInit?.isKind(SyntaxKind.ObjectLiteralExpression)) continue;
|
|
242
|
+
const cssParts: string[] = [];
|
|
243
|
+
for (const cssProp of stopInit.getProperties()) {
|
|
244
|
+
if (!cssProp.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
245
|
+
const cssName = stripQuotes(cssProp.getName());
|
|
246
|
+
const cssVal = cssProp.getInitializer();
|
|
247
|
+
if (
|
|
248
|
+
cssVal?.isKind(SyntaxKind.StringLiteral) ||
|
|
249
|
+
cssVal?.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
250
|
+
) {
|
|
251
|
+
// camelCase -> kebab-case (e.g. "transformOrigin" -> "transform-origin")
|
|
252
|
+
const kebab = cssName.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
253
|
+
cssParts.push(`${kebab}: ${cssVal.getLiteralText()}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (cssParts.length > 0) {
|
|
257
|
+
stops[stopKey] = cssParts.join("; ");
|
|
258
|
+
} else {
|
|
259
|
+
reviewItems.push({
|
|
260
|
+
file: CONFIG_REL_PATH,
|
|
261
|
+
reason: `theme.extend.keyframes.${frameName}["${stopKey}"] has no static string literal properties — port this keyframe stop manually to @keyframes ${frameName} { ... } in src/styles/app.css`,
|
|
262
|
+
severity: "warning",
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (Object.keys(stops).length > 0) result[frameName] = stops;
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
187
271
|
function extractSafelist(
|
|
188
272
|
arr: import("ts-morph").ArrayLiteralExpression,
|
|
189
273
|
reviewItems: ReviewItem[],
|
|
@@ -258,6 +342,16 @@ export function extractTailwindConfig(sourceDir: string): TailwindConfigExtract
|
|
|
258
342
|
(themeObj && getObjectChild(themeObj, "screens"));
|
|
259
343
|
const screens = screensObj ? extractScreens(screensObj) : {};
|
|
260
344
|
|
|
345
|
+
const animationObj =
|
|
346
|
+
(extendObj && getObjectChild(extendObj, "animation")) ||
|
|
347
|
+
(themeObj && getObjectChild(themeObj, "animation"));
|
|
348
|
+
const animations = animationObj ? extractAnimations(animationObj, reviewItems) : {};
|
|
349
|
+
|
|
350
|
+
const keyframesObj =
|
|
351
|
+
(extendObj && getObjectChild(extendObj, "keyframes")) ||
|
|
352
|
+
(themeObj && getObjectChild(themeObj, "keyframes"));
|
|
353
|
+
const keyframes = keyframesObj ? extractKeyframes(keyframesObj, reviewItems) : {};
|
|
354
|
+
|
|
261
355
|
let safelist: string[] = [];
|
|
262
356
|
let safelistPatterns: string[] = [];
|
|
263
357
|
const safelistProp = root.getProperty("safelist");
|
|
@@ -279,5 +373,5 @@ export function extractTailwindConfig(sourceDir: string): TailwindConfigExtract
|
|
|
279
373
|
}
|
|
280
374
|
}
|
|
281
375
|
|
|
282
|
-
return { colors, fontFamily, screens, safelist, safelistPatterns, plugins, reviewItems };
|
|
376
|
+
return { colors, fontFamily, screens, animations, keyframes, safelist, safelistPatterns, plugins, reviewItems };
|
|
283
377
|
}
|
|
@@ -146,6 +146,37 @@ export function transform(ctx: MigrationContext): void {
|
|
|
146
146
|
});
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
// Flag usePartialSection (Fresh load-more / "Ver mais" pattern) with an
|
|
150
|
+
// actionable useLoadMore recipe instead of a generic HTMX warning.
|
|
151
|
+
if (/usePartialSection/.test(result.content)) {
|
|
152
|
+
const hasAppend = /mode\s*:\s*["']append["']/.test(result.content);
|
|
153
|
+
ctx.manualReviewItems.push({
|
|
154
|
+
file: targetPath,
|
|
155
|
+
reason:
|
|
156
|
+
`usePartialSection${hasAppend ? " (mode:append)" : ""} detected — ` +
|
|
157
|
+
"this is the Fresh load-more / \"Ver mais\" pagination pattern. " +
|
|
158
|
+
"In TanStack it is a no-op stub; the button navigates replacing all products instead of appending. " +
|
|
159
|
+
"Convert to useLoadMore from @decocms/blocks/hooks:\n\n" +
|
|
160
|
+
' import { useLoadMore } from "@decocms/blocks/hooks"\n\n' +
|
|
161
|
+
' // At the top of your component (must be "use client"):\n' +
|
|
162
|
+
" const { pages, loadMore, loading, hasMore } = useLoadMore(\n" +
|
|
163
|
+
" props.page ?? { products: [], pageInfo: {} },\n" +
|
|
164
|
+
' "LOADER_KEY" // replace with your loader path, e.g.\n' +
|
|
165
|
+
' // "apps/vtex.ts/loaders/intelligentSearch/productListingPage.ts"\n' +
|
|
166
|
+
" )\n" +
|
|
167
|
+
" const allProducts = pages.flatMap(p => p.products ?? [])\n\n" +
|
|
168
|
+
" // Replace the usePartialSection anchor/button with:\n" +
|
|
169
|
+
" {hasMore && (\n" +
|
|
170
|
+
' <button onClick={loadMore} disabled={loading}>\n' +
|
|
171
|
+
' {loading ? "Carregando..." : "Ver mais"}\n' +
|
|
172
|
+
" </button>\n" +
|
|
173
|
+
" )}\n\n" +
|
|
174
|
+
"Add \"use client\" to the top of the file. " +
|
|
175
|
+
"See deco-to-tanstack-migration skill, 'Ver mais / Load More' section.",
|
|
176
|
+
severity: "warning",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
149
180
|
// Flag files with hx-on:click that use useScript (simpler pattern)
|
|
150
181
|
if (/hx-on:click=\{useScript/.test(result.content)) {
|
|
151
182
|
ctx.manualReviewItems.push({
|
|
@@ -12,6 +12,8 @@ describe("generateAppCss — tailwind.config.ts porting", () => {
|
|
|
12
12
|
colors: { perola: "#F5F0E8", "brand-500": "#B10200" },
|
|
13
13
|
fontFamily: { "bebas-neue": "Bebas Neue, sans-serif" },
|
|
14
14
|
screens: { "3xl": "1920px" },
|
|
15
|
+
animations: {},
|
|
16
|
+
keyframes: {},
|
|
15
17
|
safelist: ["bg-red-500", "text-brand-500"],
|
|
16
18
|
safelistPatterns: [],
|
|
17
19
|
plugins: [],
|
|
@@ -27,6 +29,28 @@ describe("generateAppCss — tailwind.config.ts porting", () => {
|
|
|
27
29
|
expect(css).toContain('@source inline("bg-red-500 text-brand-500");');
|
|
28
30
|
});
|
|
29
31
|
|
|
32
|
+
it("emits --animate-* in @theme and @keyframes blocks from tailwind.config.ts", () => {
|
|
33
|
+
const ctx = createContext("/tmp/does-not-exist");
|
|
34
|
+
ctx.tailwindConfig = {
|
|
35
|
+
colors: {},
|
|
36
|
+
fontFamily: {},
|
|
37
|
+
screens: {},
|
|
38
|
+
animations: { "fade-in": "fadeIn 0.5s ease-in" },
|
|
39
|
+
keyframes: { fadeIn: { "0%": "opacity: 0", "100%": "opacity: 1" } },
|
|
40
|
+
safelist: [],
|
|
41
|
+
safelistPatterns: [],
|
|
42
|
+
plugins: [],
|
|
43
|
+
reviewItems: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const css = generateAppCss(ctx);
|
|
47
|
+
|
|
48
|
+
expect(css).toContain("--animate-fade-in: fadeIn 0.5s ease-in;");
|
|
49
|
+
expect(css).toContain("@keyframes fadeIn {");
|
|
50
|
+
expect(css).toContain("0% { opacity: 0; }");
|
|
51
|
+
expect(css).toContain("100% { opacity: 1; }");
|
|
52
|
+
});
|
|
53
|
+
|
|
30
54
|
it("does not duplicate a custom color that collides with a daisyUI semantic key", () => {
|
|
31
55
|
const ctx = createContext("/tmp/does-not-exist");
|
|
32
56
|
ctx.tailwindConfig.colors = { primary: "#000000" };
|
|
@@ -240,6 +240,15 @@ ${colorLines}
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
const twAnimations = ctx.tailwindConfig.animations;
|
|
244
|
+
const twAnimationKeys = Object.keys(twAnimations);
|
|
245
|
+
if (twAnimationKeys.length > 0) {
|
|
246
|
+
themeBlock += `\n\n /* Custom animations ported from tailwind.config.ts */`;
|
|
247
|
+
for (const key of twAnimationKeys) {
|
|
248
|
+
themeBlock += `\n --animate-${key}: ${twAnimations[key]};`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
243
252
|
// Gray scale compat (Tailwind v3 had gray-50..gray-950 by default) — uses
|
|
244
253
|
// the site's own tailwind.config.ts gray-* override when present, falling
|
|
245
254
|
// back to the v3 default otherwise.
|
|
@@ -254,6 +263,16 @@ ${colorLines}
|
|
|
254
263
|
themeBlock += `\n}`;
|
|
255
264
|
sections.push(themeBlock);
|
|
256
265
|
|
|
266
|
+
// Emit @keyframes for each extracted keyframe from tailwind.config.ts.
|
|
267
|
+
// TW4 has no keyframes config — they must live in CSS directly.
|
|
268
|
+
const twKeyframes = ctx.tailwindConfig.keyframes;
|
|
269
|
+
for (const [name, stops] of Object.entries(twKeyframes)) {
|
|
270
|
+
const stopLines = Object.entries(stops)
|
|
271
|
+
.map(([pct, props]) => ` ${pct} { ${props}; }`)
|
|
272
|
+
.join("\n");
|
|
273
|
+
sections.push(`@keyframes ${name} {\n${stopLines}\n}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
// ── DaisyUI v5 compat ─────────────────────────────────────────────
|
|
258
277
|
sections.push(`/* DaisyUI v5: flatten depth/noise to match v4 look */
|
|
259
278
|
:root {
|
|
@@ -353,6 +372,14 @@ section[data-deferred="true"] {
|
|
|
353
372
|
overflow: hidden;
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
/* Tailwind v3 -> v4 compat: restore the default border-color.
|
|
376
|
+
TW4 Preflight changed border-color from gray-200 (#e5e7eb) to currentColor --
|
|
377
|
+
any element with "border" but no explicit "border-{color}" now renders black.
|
|
378
|
+
--color-gray-200 is always defined in the ported @theme (gray scale compat block). */
|
|
379
|
+
*, *::before, *::after {
|
|
380
|
+
border-color: var(--color-gray-200, #e5e7eb);
|
|
381
|
+
}
|
|
382
|
+
|
|
356
383
|
/* Tailwind v3 → v4 compat: restore the default cursor:pointer on interactive
|
|
357
384
|
elements. Tailwind v4 dropped this from Preflight, leaving Fresh-era sites
|
|
358
385
|
with hundreds of buttons looking unclickable. See:
|
|
@@ -35,3 +35,32 @@ describe("renameToken", () => {
|
|
|
35
35
|
expect(renameToken("md:bg-red-500")).toBe("md:bg-red-500");
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
import { detectDaisyUiV5StructuralIssues } from "./tailwind-renames";
|
|
40
|
+
|
|
41
|
+
describe("detectDaisyUiV5StructuralIssues", () => {
|
|
42
|
+
it("detects menu-compact removal", () => {
|
|
43
|
+
const findings = detectDaisyUiV5StructuralIssues(`<ul className="menu menu-compact">`);
|
|
44
|
+
expect(findings.some((f) => f.message.includes("menu-compact"))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("detects tab-bordered removal", () => {
|
|
48
|
+
const findings = detectDaisyUiV5StructuralIssues(`<a className="tab tab-bordered">`);
|
|
49
|
+
expect(findings.some((f) => f.message.includes("tab-bordered"))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects bare alert in className without a color modifier", () => {
|
|
53
|
+
const findings = detectDaisyUiV5StructuralIssues(`<div className="alert">`);
|
|
54
|
+
expect(findings.some((f) => f.message.includes("alert"))).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("does not flag alert when a color modifier is present", () => {
|
|
58
|
+
const findings = detectDaisyUiV5StructuralIssues(`<div className="alert alert-info">`);
|
|
59
|
+
expect(findings.some((f) => f.message.includes("alert default styling"))).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("does not flag alert() JS function calls as a DaisyUI alert", () => {
|
|
63
|
+
const findings = detectDaisyUiV5StructuralIssues(`window.alert("something went wrong");`);
|
|
64
|
+
expect(findings.some((f) => f.message.includes("alert default styling"))).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -79,6 +79,8 @@ export const CLASS_RENAMES: Record<string, string> = {
|
|
|
79
79
|
export const DAISYUI_RENAMES: Record<string, string> = {
|
|
80
80
|
"badge-ghost": "badge-soft",
|
|
81
81
|
"card-compact": "card-sm",
|
|
82
|
+
"card-normal": "card-md",
|
|
83
|
+
"tabs-boxed": "tabs-box",
|
|
82
84
|
};
|
|
83
85
|
|
|
84
86
|
/**
|
|
@@ -183,6 +185,38 @@ export function detectDaisyUiV5StructuralIssues(content: string): GotchaFinding[
|
|
|
183
185
|
});
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
if (/\bmenu-compact\b/.test(content)) {
|
|
189
|
+
findings.push({
|
|
190
|
+
gotcha: 37,
|
|
191
|
+
message:
|
|
192
|
+
"DaisyUI v4 `menu-compact` was removed in v5 with no drop-in class — use `menu-sm` for a smaller menu or remove the modifier.",
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (/\btab-bordered\b/.test(content)) {
|
|
197
|
+
findings.push({
|
|
198
|
+
gotcha: 37,
|
|
199
|
+
message:
|
|
200
|
+
"DaisyUI v4 `tab-bordered` was removed in v5 — use `tabs-bordered` on the `tabs` wrapper instead of a modifier on each `tab` element.",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Detect bare `alert` without a color modifier — DaisyUI v5 changed the
|
|
205
|
+
// default alert background; alerts without alert-info/success/warning/error
|
|
206
|
+
// may appear with no background color.
|
|
207
|
+
// Scoped to className= / class= attribute values to avoid matching the JS
|
|
208
|
+
// global `alert()` function, variable names, or comments in the same file.
|
|
209
|
+
if (
|
|
210
|
+
/className=["'][^"']*\balert\b[^"']*["']/.test(content) &&
|
|
211
|
+
!/\balert-(?:info|success|warning|error)\b/.test(content)
|
|
212
|
+
) {
|
|
213
|
+
findings.push({
|
|
214
|
+
gotcha: 37,
|
|
215
|
+
message:
|
|
216
|
+
"DaisyUI v5 changed `alert` default styling — a bare `alert` without a color modifier (alert-info/success/warning/error) may render with no background. Add an explicit color modifier or use `alert-soft` for a neutral variant.",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
186
220
|
return findings;
|
|
187
221
|
}
|
|
188
222
|
|