@decocms/start 0.32.2 → 0.33.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/FRAMEWORK_TODO.md +110 -0
- package/package.json +1 -1
- package/scripts/migrate/phase-analyze.ts +58 -0
- package/scripts/migrate/phase-report.ts +29 -0
- package/scripts/migrate/phase-scaffold.ts +125 -14
- package/scripts/migrate/phase-verify.ts +16 -0
- package/scripts/migrate/templates/package-json.ts +14 -4
- package/scripts/migrate/templates/routes.ts +4 -1
- package/scripts/migrate/templates/setup.ts +40 -6
- package/scripts/migrate/transforms/imports.ts +1 -0
- package/scripts/migrate/transforms/tailwind.ts +331 -1
- package/scripts/migrate/types.ts +7 -0
- package/scripts/migrate.ts +41 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# @decocms/start — Framework TODO
|
|
2
|
+
|
|
3
|
+
Issues discovered during the portal-davinci migration script development.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Tier 0 — Blocking migrations
|
|
8
|
+
|
|
9
|
+
### `setup.ts` should be framework-injected
|
|
10
|
+
- **Current**: Every site has a `setup.ts` with identical boilerplate (import.meta.glob, registerSections, setBlocks, registerBuiltinMatchers)
|
|
11
|
+
- **Ideal**: Site declares only its custom loaders/actions. Framework injects the rest via Vite plugin or a single `createSiteSetup()` call
|
|
12
|
+
- **Impact**: Migration script has to scaffold this file. Any framework change requires re-migrating all sites
|
|
13
|
+
|
|
14
|
+
### `SiteTheme` component missing from `@decocms/start` or `@decocms/apps`
|
|
15
|
+
- **Current**: `apps/website/components/Theme.tsx` was removed. Migration script scaffolds a local replacement
|
|
16
|
+
- **Ideal**: Export `SiteTheme` from `@decocms/start/components/Theme` or `@decocms/apps/commerce/components/Theme`
|
|
17
|
+
- **Impact**: Every migrated site gets a local copy that diverges over time
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Tier 1 — Developer experience
|
|
22
|
+
|
|
23
|
+
### `useScript(fn)` hydration mismatch warning
|
|
24
|
+
- **Current**: `useScript` calls `fn.toString()` which produces different output in SSR vs client builds (minification, variable renaming)
|
|
25
|
+
- **Warning**: `[useScript] Using fn.toString() for "setup". This may produce different output in SSR vs client builds`
|
|
26
|
+
- **Ideal**: Provide `inlineScript()` that accepts a plain string constant, or make `useScript` stable across builds
|
|
27
|
+
- **Impact**: Console noise in every dev session, potential hydration bugs in production
|
|
28
|
+
|
|
29
|
+
### Route files are 100% boilerplate
|
|
30
|
+
- **Files**: `__root.tsx`, `index.tsx`, `$.tsx`, `deco/meta.ts`, `deco/invoke.$.ts`, `deco/render.ts`
|
|
31
|
+
- **Current**: Migration script scaffolds identical route files for every site
|
|
32
|
+
- **Ideal**: Framework provides a `createDecoRoutes()` or auto-generates via Vite plugin. Site only customizes (e.g. GTM ID, site name)
|
|
33
|
+
- **Impact**: Route files are copy-pasted across sites and diverge
|
|
34
|
+
|
|
35
|
+
### `server.ts`, `worker-entry.ts`, `router.tsx` are boilerplate
|
|
36
|
+
- **Current**: Every site has identical server infrastructure files
|
|
37
|
+
- **Ideal**: Single function call like `createDecoServer()` / `createDecoRouter()`. Or auto-generated by Vite plugin
|
|
38
|
+
- **Impact**: Sites copy these files and never update them when framework improves
|
|
39
|
+
|
|
40
|
+
### `dev:clean` script should be built into framework
|
|
41
|
+
- **Current**: Migration script adds `"dev:clean": "rm -rf node_modules/.vite .wrangler/state .tanstack && vite dev"`
|
|
42
|
+
- **Ideal**: `@decocms/start` CLI command or Vite plugin hook that auto-cleans stale caches on startup
|
|
43
|
+
- **Impact**: Developers hit mysterious caching bugs and don't know to clean
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Tier 2 — Quality & correctness
|
|
48
|
+
|
|
49
|
+
### GTM/Analytics SDK is duplicated across sites
|
|
50
|
+
- **Current**: Every site has a custom `Session.tsx` with identical analytics SDK (data-event listeners, data-gtm-event listeners, IntersectionObserver)
|
|
51
|
+
- **Ideal**: `@decocms/start/analytics` exports a `<DecoAnalytics gtmId="GTM-XXX" />` component used in `__root.tsx`
|
|
52
|
+
- **Impact**: Analytics bugs are fixed per-site instead of once in the framework
|
|
53
|
+
|
|
54
|
+
### `useGTMEvent` hook is universal
|
|
55
|
+
- **Current**: Sites have a local `sdk/useGTMEvent.ts` that creates `data-gtm-event` / `data-gtm-trigger` attributes
|
|
56
|
+
- **Ideal**: Export from `@decocms/start/analytics/useGTMEvent`
|
|
57
|
+
- **Impact**: Same utility duplicated in every site
|
|
58
|
+
|
|
59
|
+
### Negative z-index (`-z-10`) breaks in Tailwind v4 stacking contexts
|
|
60
|
+
- **Current**: Migration script auto-fixes `-z-N` → `z-0` on images and adds `relative z-10` to content siblings
|
|
61
|
+
- **Root cause**: React/TanStack wraps sections in `<section>` elements that create new stacking contexts. Negative z-index gets trapped inside
|
|
62
|
+
- **Ideal**: Document this pattern. Consider framework-level CSS reset that prevents section wrappers from creating stacking contexts (e.g. `section { isolation: auto; }`)
|
|
63
|
+
- **Impact**: Every migrated site has background images that are invisible until manually fixed
|
|
64
|
+
|
|
65
|
+
### Tailwind v3 → v4 opacity classes
|
|
66
|
+
- **Current**: Migration script converts `bg-black bg-opacity-20` → `bg-black/20`
|
|
67
|
+
- **Edge cases**: Non-adjacent opacity classes (e.g. `bg-black flex ... hover:bg-opacity-30`) are harder to detect
|
|
68
|
+
- **Ideal**: Provide a lint rule or postcss plugin that warns about orphaned opacity classes
|
|
69
|
+
- **Impact**: Subtle visual bugs that are hard to spot
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Tier 3 — Nice to have
|
|
74
|
+
|
|
75
|
+
### `registerLayoutSections` auto-detection
|
|
76
|
+
- **Current**: Migration script detects Header/Footer/Theme by name pattern and adds to `registerLayoutSections()`
|
|
77
|
+
- **Ideal**: Framework auto-detects layout sections from CMS page structure (sections that appear on every page = layout)
|
|
78
|
+
- **Impact**: New sections added as layout require manual setup.ts update
|
|
79
|
+
|
|
80
|
+
### Icon loaders (`availableIcons.ts`, `icons.ts`) depend on deleted `static/adminIcons.ts`
|
|
81
|
+
- **Current**: Migration script deletes these loaders entirely
|
|
82
|
+
- **Ideal**: Framework provides a built-in icon discovery mechanism (scan `public/sprites.svg` at build time)
|
|
83
|
+
- **Impact**: Admin loses icon picker functionality after migration
|
|
84
|
+
|
|
85
|
+
### `import.meta.glob` section discovery could be smarter
|
|
86
|
+
- **Current**: Glob `./sections/**/*.tsx` discovers all files, including utility exports
|
|
87
|
+
- **Ideal**: Use a convention (e.g. default export = section) or a marker comment to distinguish sections from helpers
|
|
88
|
+
- **Impact**: Non-section files in `sections/` directory get registered as CMS sections
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Discovered during migration (portal-davinci)
|
|
93
|
+
|
|
94
|
+
| Issue | Where | Status |
|
|
95
|
+
|-------|-------|--------|
|
|
96
|
+
| `scriptAsDataURI` → `useScript` leaves malformed JSX | `fresh-apis.ts` | Fixed in script |
|
|
97
|
+
| `.test()` then `.replace()` regex bug (lastIndex statefulness) | All transforms | Fixed in script |
|
|
98
|
+
| `site/` → `~/` doesn't strip `.tsx` extension | `imports.ts` | Fixed in script |
|
|
99
|
+
| `class` in destructuring/interface not converted to `className` | `jsx.ts` | Fixed in script |
|
|
100
|
+
| `for=` → `htmlFor=` missing | `jsx.ts` | Fixed in script |
|
|
101
|
+
| `tabindex` → `tabIndex` missing | `jsx.ts` | Fixed in script |
|
|
102
|
+
| Multi-line `className={clx()}` z-index insertion fails | `tailwind.ts` | Fixed in script |
|
|
103
|
+
| `src/` directory scanned as source → `src/src/` created | `phase-analyze.ts` | Fixed in script |
|
|
104
|
+
| `package.json`/`package-lock.json` treated as source files | `phase-analyze.ts` | Fixed in script |
|
|
105
|
+
| `constants.ts` not deleted | `phase-analyze.ts` | Fixed in script |
|
|
106
|
+
| `deno-lint-ignore` in JSX comments not removed | `deno-isms.ts` | Fixed in script |
|
|
107
|
+
| `useDevice` import not split from `useScript` module | `imports.ts` | Fixed in script |
|
|
108
|
+
| `@decocms/start/worker` wrong path → `@decocms/start/sdk/workerEntry` | `server-entry.ts` | Fixed in script |
|
|
109
|
+
| `SiteTheme` / `Font` import removed by catch-all rule | `imports.ts` | Fixed in script |
|
|
110
|
+
| Bootstrap failures silently ignored | `migrate.ts` | Fixed in script |
|
package/package.json
CHANGED
|
@@ -346,6 +346,53 @@ function extractSiteName(sourceDir: string): string {
|
|
|
346
346
|
return dirName;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
function extractThemeFromCms(sourceDir: string): { colors: Record<string, string>; fontFamily: string | null } {
|
|
350
|
+
const colors: Record<string, string> = {};
|
|
351
|
+
let fontFamily: string | null = null;
|
|
352
|
+
|
|
353
|
+
// Look for Theme config in .deco/blocks/
|
|
354
|
+
const blocksDir = path.join(sourceDir, ".deco", "blocks");
|
|
355
|
+
if (!fs.existsSync(blocksDir)) return { colors, fontFamily };
|
|
356
|
+
|
|
357
|
+
const files = fs.readdirSync(blocksDir);
|
|
358
|
+
for (const file of files) {
|
|
359
|
+
if (!file.endsWith(".json")) continue;
|
|
360
|
+
try {
|
|
361
|
+
const content = fs.readFileSync(path.join(blocksDir, file), "utf-8");
|
|
362
|
+
if (!content.includes("mainColors") && !content.includes("Theme")) continue;
|
|
363
|
+
|
|
364
|
+
const data = JSON.parse(content);
|
|
365
|
+
// Direct Theme block (e.g. Deco.json with __resolveType: "site/sections/Theme/Theme.tsx")
|
|
366
|
+
if (data.mainColors) {
|
|
367
|
+
Object.assign(colors, data.mainColors);
|
|
368
|
+
if (data.complementaryColors) {
|
|
369
|
+
Object.assign(colors, data.complementaryColors);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// Font
|
|
373
|
+
if (data.font?.fonts?.[0]?.family) {
|
|
374
|
+
fontFamily = data.font.fonts[0].family;
|
|
375
|
+
}
|
|
376
|
+
// Check sections array (page blocks may contain Theme)
|
|
377
|
+
if (data.sections) {
|
|
378
|
+
for (const section of data.sections) {
|
|
379
|
+
if (section.__resolveType?.includes("Theme") && section.mainColors) {
|
|
380
|
+
Object.assign(colors, section.mainColors);
|
|
381
|
+
if (section.complementaryColors) {
|
|
382
|
+
Object.assign(colors, section.complementaryColors);
|
|
383
|
+
}
|
|
384
|
+
if (section.font?.fonts?.[0]?.family) {
|
|
385
|
+
fontFamily = section.font.fonts[0].family;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch {}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { colors, fontFamily };
|
|
394
|
+
}
|
|
395
|
+
|
|
349
396
|
export function analyze(ctx: MigrationContext): void {
|
|
350
397
|
logPhase("Analyze");
|
|
351
398
|
|
|
@@ -365,9 +412,20 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
365
412
|
ctx.platform = extractPlatform(ctx.sourceDir);
|
|
366
413
|
ctx.gtmId = extractGtmId(ctx.sourceDir);
|
|
367
414
|
|
|
415
|
+
// Extract theme colors and font from CMS
|
|
416
|
+
const theme = extractThemeFromCms(ctx.sourceDir);
|
|
417
|
+
ctx.themeColors = theme.colors;
|
|
418
|
+
ctx.fontFamily = theme.fontFamily;
|
|
419
|
+
|
|
368
420
|
console.log(` Site: ${ctx.siteName}`);
|
|
369
421
|
console.log(` Platform: ${ctx.platform}`);
|
|
370
422
|
console.log(` GTM ID: ${ctx.gtmId || "none"}`);
|
|
423
|
+
if (Object.keys(ctx.themeColors).length > 0) {
|
|
424
|
+
console.log(` Theme: ${Object.keys(ctx.themeColors).length} colors from CMS`);
|
|
425
|
+
}
|
|
426
|
+
if (ctx.fontFamily) {
|
|
427
|
+
console.log(` Font: ${ctx.fontFamily}`);
|
|
428
|
+
}
|
|
371
429
|
|
|
372
430
|
// Scan all files
|
|
373
431
|
scanDir(ctx.sourceDir, ctx.sourceDir, ctx.files);
|
|
@@ -114,6 +114,35 @@ export function report(ctx: MigrationContext): void {
|
|
|
114
114
|
);
|
|
115
115
|
lines.push("");
|
|
116
116
|
|
|
117
|
+
// Known Issues
|
|
118
|
+
lines.push("## Known Issues (Tailwind v3 → v4 + React)");
|
|
119
|
+
lines.push("");
|
|
120
|
+
lines.push("### Negative z-index on background images");
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push("The migration script automatically converts `-z-{n}` to `z-0` on `<img>` and `<Image>` elements.");
|
|
123
|
+
lines.push("However, if you have **non-image elements** with negative z-index (e.g. `-z-10` on a `<div>` used as a background layer), they may become invisible.");
|
|
124
|
+
lines.push("");
|
|
125
|
+
lines.push("**Why it breaks:** In TanStack Start/React, section wrappers (`<section>`) or parent elements can create");
|
|
126
|
+
lines.push("CSS stacking contexts (via `animation`, `transform`, `will-change`, `filter`, `isolation`, etc.).");
|
|
127
|
+
lines.push("A child with negative z-index gets trapped inside that stacking context and renders behind the parent's background — making it invisible.");
|
|
128
|
+
lines.push("");
|
|
129
|
+
lines.push("**How to fix:**");
|
|
130
|
+
lines.push("1. Replace `-z-{n}` with `z-0` on the background element");
|
|
131
|
+
lines.push("2. Content siblings render on top naturally via DOM order (they come after in the HTML)");
|
|
132
|
+
lines.push("3. If needed, add `relative z-10` to content siblings to ensure they stay above");
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push("**How to detect:** Search for remaining negative z-index: `grep -rn '\\-z-' src/ --include='*.tsx'`");
|
|
135
|
+
lines.push("");
|
|
136
|
+
lines.push("### Opacity utility classes");
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push("Tailwind v4 removed `bg-opacity-{n}`, `text-opacity-{n}`, etc. The script converts them to");
|
|
139
|
+
lines.push("the modifier syntax (e.g. `bg-black bg-opacity-20` → `bg-black/20`). If a color and its opacity");
|
|
140
|
+
lines.push("are not in the same className string (e.g. set via different conditional branches), the script");
|
|
141
|
+
lines.push("flags them for manual review.");
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("**How to detect:** `grep -rn 'opacity-' src/ --include='*.tsx'`");
|
|
144
|
+
lines.push("");
|
|
145
|
+
|
|
117
146
|
// Framework findings
|
|
118
147
|
lines.push("## Framework Findings");
|
|
119
148
|
lines.push("");
|
|
@@ -38,6 +38,7 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
38
38
|
writeFile(ctx, "vite.config.ts", generateViteConfig(ctx));
|
|
39
39
|
writeFile(ctx, "wrangler.jsonc", generateWrangler(ctx));
|
|
40
40
|
writeFile(ctx, "knip.config.ts", generateKnipConfig());
|
|
41
|
+
writeFile(ctx, ".gitignore", generateGitignore());
|
|
41
42
|
writeFile(ctx, ".prettierrc", JSON.stringify({
|
|
42
43
|
semi: true,
|
|
43
44
|
singleQuote: false,
|
|
@@ -67,6 +68,21 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
67
68
|
// Apps
|
|
68
69
|
writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx));
|
|
69
70
|
|
|
71
|
+
// SiteTheme component (replaces apps/website/components/Theme.tsx)
|
|
72
|
+
// Check if any source file uses SiteTheme
|
|
73
|
+
const usesSiteTheme = ctx.files.some((f) => {
|
|
74
|
+
if (f.action === "delete") return false;
|
|
75
|
+
try {
|
|
76
|
+
const content = fs.readFileSync(f.absPath, "utf-8");
|
|
77
|
+
return content.includes("SiteTheme");
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
if (usesSiteTheme) {
|
|
83
|
+
writeFile(ctx, "src/components/ui/Theme.tsx", generateSiteThemeComponent());
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
// Create public/ directory
|
|
71
87
|
if (!ctx.dryRun) {
|
|
72
88
|
fs.mkdirSync(path.join(ctx.sourceDir, "public"), { recursive: true });
|
|
@@ -75,7 +91,74 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
75
91
|
console.log(` Scaffolded ${ctx.scaffoldedFiles.length} files`);
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
function
|
|
94
|
+
function generateSiteThemeComponent(): string {
|
|
95
|
+
return `export interface Font {
|
|
96
|
+
family: string;
|
|
97
|
+
styleSheet?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface Props {
|
|
101
|
+
colorScheme?: "light" | "dark" | "any";
|
|
102
|
+
fonts?: Font[];
|
|
103
|
+
variables?: Array<{ name: string; value: string }>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* SiteTheme — injects CSS custom properties and font stylesheets into the page.
|
|
108
|
+
* This replaces the old apps/website/components/Theme.tsx from the Deno stack.
|
|
109
|
+
*/
|
|
110
|
+
export default function SiteTheme({ variables, fonts, colorScheme }: Props) {
|
|
111
|
+
const cssVars = variables?.length
|
|
112
|
+
? \`:root { \${variables.map((v) => \`\${v.name}: \${v.value};\`).join(" ")} }\`
|
|
113
|
+
: "";
|
|
114
|
+
|
|
115
|
+
const colorSchemeCss = colorScheme && colorScheme !== "any"
|
|
116
|
+
? \`:root { color-scheme: \${colorScheme}; }\`
|
|
117
|
+
: "";
|
|
118
|
+
|
|
119
|
+
const css = [cssVars, colorSchemeCss].filter(Boolean).join("\\n");
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
{fonts?.map((font) =>
|
|
124
|
+
font.styleSheet ? (
|
|
125
|
+
<link key={font.family} rel="stylesheet" href={font.styleSheet} />
|
|
126
|
+
) : null
|
|
127
|
+
)}
|
|
128
|
+
{css && <style dangerouslySetInnerHTML={{ __html: css }} />}
|
|
129
|
+
</>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { type Font as SiteThemeFont };
|
|
134
|
+
`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function generateAppCss(ctx: MigrationContext): string {
|
|
138
|
+
const c = ctx.themeColors;
|
|
139
|
+
// Map CMS color names to DaisyUI v5 CSS variables
|
|
140
|
+
const colors: Record<string, string> = {
|
|
141
|
+
"--color-primary": c["primary"] || "#6B21A8",
|
|
142
|
+
"--color-secondary": c["secondary"] || "#141414",
|
|
143
|
+
"--color-accent": c["tertiary"] || "#FFF100",
|
|
144
|
+
"--color-neutral": c["neutral"] || "#393939",
|
|
145
|
+
"--color-base-100": c["base-100"] || "#FFFFFF",
|
|
146
|
+
"--color-base-200": c["base-200"] || "#F3F3F3",
|
|
147
|
+
"--color-base-300": c["base-300"] || "#868686",
|
|
148
|
+
"--color-info": c["info"] || "#006CA1",
|
|
149
|
+
"--color-success": c["success"] || "#007552",
|
|
150
|
+
"--color-warning": c["warning"] || "#F8D13A",
|
|
151
|
+
"--color-error": c["error"] || "#CF040A",
|
|
152
|
+
};
|
|
153
|
+
// Add content colors if specified
|
|
154
|
+
if (c["primary-content"]) colors["--color-primary-content"] = c["primary-content"];
|
|
155
|
+
if (c["secondary-content"]) colors["--color-secondary-content"] = c["secondary-content"];
|
|
156
|
+
if (c["base-content"]) colors["--color-base-content"] = c["base-content"];
|
|
157
|
+
|
|
158
|
+
const colorLines = Object.entries(colors)
|
|
159
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
160
|
+
.join("\n");
|
|
161
|
+
|
|
79
162
|
return `@import "tailwindcss";
|
|
80
163
|
@plugin "daisyui";
|
|
81
164
|
@plugin "daisyui/theme" {
|
|
@@ -83,18 +166,7 @@ function generateAppCss(_ctx: MigrationContext): string {
|
|
|
83
166
|
default: true;
|
|
84
167
|
color-scheme: light;
|
|
85
168
|
|
|
86
|
-
|
|
87
|
-
--color-primary: #6B21A8;
|
|
88
|
-
--color-secondary: #141414;
|
|
89
|
-
--color-accent: #FFF100;
|
|
90
|
-
--color-neutral: #393939;
|
|
91
|
-
--color-base-100: #FFFFFF;
|
|
92
|
-
--color-base-200: #F3F3F3;
|
|
93
|
-
--color-base-300: #868686;
|
|
94
|
-
--color-info: #006CA1;
|
|
95
|
-
--color-success: #007552;
|
|
96
|
-
--color-warning: #F8D13A;
|
|
97
|
-
--color-error: #CF040A;
|
|
169
|
+
${colorLines}
|
|
98
170
|
}
|
|
99
171
|
|
|
100
172
|
@theme {
|
|
@@ -103,7 +175,7 @@ function generateAppCss(_ctx: MigrationContext): string {
|
|
|
103
175
|
--color-black: #000;
|
|
104
176
|
--color-transparent: transparent;
|
|
105
177
|
--color-current: currentColor;
|
|
106
|
-
--color-inherit: inherit
|
|
178
|
+
--color-inherit: inherit;${ctx.fontFamily ? `\n --font-sans: "${ctx.fontFamily}", ui-sans-serif, system-ui, sans-serif;` : ""}
|
|
107
179
|
}
|
|
108
180
|
|
|
109
181
|
/* View transitions */
|
|
@@ -113,6 +185,45 @@ function generateAppCss(_ctx: MigrationContext): string {
|
|
|
113
185
|
`;
|
|
114
186
|
}
|
|
115
187
|
|
|
188
|
+
function generateGitignore(): string {
|
|
189
|
+
return `# Dependencies
|
|
190
|
+
node_modules/
|
|
191
|
+
|
|
192
|
+
# Build output
|
|
193
|
+
dist/
|
|
194
|
+
.cache/
|
|
195
|
+
|
|
196
|
+
# Cloudflare Workers
|
|
197
|
+
.wrangler/
|
|
198
|
+
.dev.vars
|
|
199
|
+
|
|
200
|
+
# TanStack Router (auto-generated)
|
|
201
|
+
src/routeTree.gen.ts
|
|
202
|
+
.tanstack/
|
|
203
|
+
|
|
204
|
+
# Vite
|
|
205
|
+
vite.config.timestamp_*
|
|
206
|
+
*.local
|
|
207
|
+
|
|
208
|
+
# Environment
|
|
209
|
+
.env
|
|
210
|
+
.env.*
|
|
211
|
+
|
|
212
|
+
# OS
|
|
213
|
+
.DS_Store
|
|
214
|
+
|
|
215
|
+
# Deco CMS
|
|
216
|
+
.deco/metadata/*
|
|
217
|
+
|
|
218
|
+
# Bun lock file (if using npm, keep package-lock.json instead)
|
|
219
|
+
# package-lock.json
|
|
220
|
+
|
|
221
|
+
# IDE
|
|
222
|
+
.vscode/
|
|
223
|
+
.idea/
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
|
|
116
227
|
function generateSiteApp(ctx: MigrationContext): string {
|
|
117
228
|
return `export type Platform =
|
|
118
229
|
| "vtex"
|
|
@@ -230,6 +230,22 @@ const checks: Check[] = [
|
|
|
230
230
|
return true;
|
|
231
231
|
},
|
|
232
232
|
},
|
|
233
|
+
{
|
|
234
|
+
name: "No negative z-index on non-image elements",
|
|
235
|
+
severity: "warning",
|
|
236
|
+
fn: (ctx) => {
|
|
237
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
238
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
239
|
+
// Find -z-{n} that are NOT on img/Image elements (those are auto-fixed to z-0)
|
|
240
|
+
const bad = findFilesWithPattern(srcDir, /(?<!<(?:img|Image)[^>]*)-z-\d+/);
|
|
241
|
+
if (bad.length > 0) {
|
|
242
|
+
console.log(` Negative z-index on non-image elements: ${bad.join(", ")}`);
|
|
243
|
+
console.log(` These may be invisible due to stacking contexts. Replace with z-0 or positive z-index.`);
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
},
|
|
248
|
+
},
|
|
233
249
|
{
|
|
234
250
|
name: "No imports to deleted static files",
|
|
235
251
|
severity: "error",
|
|
@@ -8,10 +8,13 @@ function extractNpmDeps(importMap: Record<string, string>): Record<string, strin
|
|
|
8
8
|
const deps: Record<string, string> = {};
|
|
9
9
|
for (const [key, value] of Object.entries(importMap)) {
|
|
10
10
|
if (!value.startsWith("npm:")) continue;
|
|
11
|
-
// Skip
|
|
11
|
+
// Skip framework deps we handle ourselves
|
|
12
12
|
if (key.startsWith("preact") || key.startsWith("@preact/")) continue;
|
|
13
13
|
if (key.startsWith("@deco/")) continue;
|
|
14
|
-
if (key === "daisyui") continue;
|
|
14
|
+
if (key === "daisyui") continue;
|
|
15
|
+
if (key === "preact-render-to-string") continue;
|
|
16
|
+
if (key === "simple-git") continue; // dev tool, not needed in runtime
|
|
17
|
+
if (key === "fast-json-patch") continue; // used by old deco runtime
|
|
15
18
|
|
|
16
19
|
const raw = value.slice(4); // remove "npm:"
|
|
17
20
|
const atIdx = raw.lastIndexOf("@");
|
|
@@ -19,8 +22,13 @@ function extractNpmDeps(importMap: Record<string, string>): Record<string, strin
|
|
|
19
22
|
deps[raw] = "*";
|
|
20
23
|
} else {
|
|
21
24
|
const name = raw.slice(0, atIdx);
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
let version = raw.slice(atIdx + 1);
|
|
26
|
+
// Don't double-prefix with ^ if version already has a range prefix
|
|
27
|
+
if (/^[~^>=<]/.test(version)) {
|
|
28
|
+
deps[name] = version;
|
|
29
|
+
} else {
|
|
30
|
+
deps[name] = `^${version}`;
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
return deps;
|
|
@@ -39,6 +47,8 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
39
47
|
description: `${ctx.siteName} storefront powered by TanStack Start`,
|
|
40
48
|
scripts: {
|
|
41
49
|
dev: "vite dev",
|
|
50
|
+
"dev:clean":
|
|
51
|
+
"rm -rf node_modules/.vite .wrangler/state .tanstack && vite dev",
|
|
42
52
|
"generate:blocks":
|
|
43
53
|
"tsx node_modules/@decocms/start/scripts/generate-blocks.ts",
|
|
44
54
|
"generate:routes": "tsr generate",
|
|
@@ -147,7 +147,10 @@ ${gtmScript}
|
|
|
147
147
|
return (
|
|
148
148
|
<html lang="pt-BR" data-theme="light" suppressHydrationWarning>
|
|
149
149
|
<head>
|
|
150
|
-
<HeadContent
|
|
150
|
+
<HeadContent />${ctx.fontFamily ? `
|
|
151
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
152
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
153
|
+
<link href="https://fonts.googleapis.com/css2?family=${encodeURIComponent(ctx.fontFamily)}:wght@400;500;600;700&display=swap" rel="stylesheet" />` : ""}
|
|
151
154
|
</head>
|
|
152
155
|
<body className="bg-base-200 text-base-content" suppressHydrationWarning>
|
|
153
156
|
<script dangerouslySetInnerHTML={{ __html: DECO_EVENTS_BOOTSTRAP }} />
|
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import type { MigrationContext } from "../types.ts";
|
|
2
2
|
|
|
3
|
-
export function generateSetup(
|
|
3
|
+
export function generateSetup(ctx: MigrationContext): string {
|
|
4
|
+
// Detect layout sections (Header, Footer, Theme) from source files
|
|
5
|
+
const layoutSections: string[] = [];
|
|
6
|
+
for (const f of ctx.files) {
|
|
7
|
+
if (f.category !== "section" || f.action === "delete") continue;
|
|
8
|
+
const name = f.path.replace(/^sections\//, "").replace(/\.tsx$/, "");
|
|
9
|
+
const lower = name.toLowerCase();
|
|
10
|
+
if (lower.includes("header") || lower.includes("footer") || lower.includes("theme")) {
|
|
11
|
+
layoutSections.push(`site/sections/${name}.tsx`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// Also check islands that became sections
|
|
15
|
+
for (const f of ctx.files) {
|
|
16
|
+
if (f.category !== "island") continue;
|
|
17
|
+
const name = f.path.replace(/^islands\//, "").replace(/\.tsx$/, "");
|
|
18
|
+
const lower = name.toLowerCase();
|
|
19
|
+
if (lower.includes("header") || lower.includes("footer") || lower.includes("theme")) {
|
|
20
|
+
layoutSections.push(`site/sections/${name}.tsx`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const layoutRegistration = layoutSections.length > 0
|
|
25
|
+
? `\n// -- Layout Sections (cached across navigations) --
|
|
26
|
+
registerLayoutSections([
|
|
27
|
+
${layoutSections.map((s) => ` "${s}",`).join("\n")}
|
|
28
|
+
]);\n`
|
|
29
|
+
: "";
|
|
30
|
+
|
|
31
|
+
const layoutImport = layoutSections.length > 0
|
|
32
|
+
? "\n registerLayoutSections," : "";
|
|
33
|
+
|
|
4
34
|
return `/**
|
|
5
35
|
* Site setup — registers all sections, loaders and matchers with the CMS.
|
|
6
36
|
*
|
|
@@ -9,7 +39,7 @@ export function generateSetup(_ctx: MigrationContext): string {
|
|
|
9
39
|
*/
|
|
10
40
|
import { blocks as generatedBlocks } from "./server/cms/blocks.gen";
|
|
11
41
|
import {
|
|
12
|
-
registerSections
|
|
42
|
+
registerSections,${layoutImport}
|
|
13
43
|
setBlocks,
|
|
14
44
|
} from "@decocms/start/cms";
|
|
15
45
|
import { registerBuiltinMatchers } from "@decocms/start/matchers/builtins";
|
|
@@ -21,10 +51,14 @@ if (typeof document === "undefined") {
|
|
|
21
51
|
}
|
|
22
52
|
|
|
23
53
|
// -- Section Registry --
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
// CMS blocks reference sections as "site/sections/X.tsx", so we remap the glob keys.
|
|
55
|
+
const sectionGlob = import.meta.glob("./sections/**/*.tsx") as Record<string, () => Promise<any>>;
|
|
56
|
+
const sections: Record<string, () => Promise<any>> = {};
|
|
57
|
+
for (const [path, loader] of Object.entries(sectionGlob)) {
|
|
58
|
+
sections["site/" + path.slice(2)] = loader;
|
|
59
|
+
}
|
|
60
|
+
registerSections(sections);
|
|
61
|
+
${layoutRegistration}
|
|
28
62
|
// -- Matchers --
|
|
29
63
|
registerBuiltinMatchers();
|
|
30
64
|
`;
|
|
@@ -29,6 +29,7 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
29
29
|
[/^"apps\/website\/components\/Image\.tsx"$/, `"@decocms/apps/commerce/components/Image"`],
|
|
30
30
|
[/^"apps\/website\/components\/Picture\.tsx"$/, `"@decocms/apps/commerce/components/Picture"`],
|
|
31
31
|
[/^"apps\/website\/components\/Video\.tsx"$/, `"@decocms/apps/commerce/components/Video"`],
|
|
32
|
+
[/^"apps\/website\/components\/Theme\.tsx"$/, `"~/components/ui/Theme"`],
|
|
32
33
|
[/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
|
|
33
34
|
|
|
34
35
|
// Apps — catch-all (things like apps/website/mod.ts, apps/vtex/mod.ts, etc.)
|
|
@@ -312,6 +312,52 @@ function hasOrderIssues(classes: string[]): boolean {
|
|
|
312
312
|
return false;
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// ── Fix opacity modifier classes within a class list ────────────
|
|
316
|
+
// Handles non-adjacent cases like: "bg-black/50 flex ... hover:bg-opacity-30"
|
|
317
|
+
// Finds the base color class and merges the opacity into it.
|
|
318
|
+
function fixOrphanedOpacity(classList: string[]): { result: string[]; changes: string[] } {
|
|
319
|
+
const changes: string[] = [];
|
|
320
|
+
const prefixes = ["bg", "text", "border", "ring", "divide", "placeholder"];
|
|
321
|
+
|
|
322
|
+
// Find base color classes: bg-{color} or bg-{color}/{opacity}
|
|
323
|
+
const colorClasses: Record<string, { color: string; prefix: string }> = {};
|
|
324
|
+
for (const cls of classList) {
|
|
325
|
+
for (const pfx of prefixes) {
|
|
326
|
+
const match = cls.match(new RegExp(`^${pfx}-(\\w[\\w-]*?)(?:\\/(\\d+))?$`));
|
|
327
|
+
if (match && match[1] !== "opacity") {
|
|
328
|
+
colorClasses[pfx] = { color: match[1], prefix: pfx };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Replace orphaned opacity classes with proper merged versions
|
|
334
|
+
const result: string[] = [];
|
|
335
|
+
for (const cls of classList) {
|
|
336
|
+
const opMatch = cls.match(/^((?:hover:|focus:|active:)*)(\w+)-opacity-(\d+)$/);
|
|
337
|
+
if (!opMatch) {
|
|
338
|
+
result.push(cls);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const modifier = opMatch[1]; // "hover:" or ""
|
|
343
|
+
const prefix = opMatch[2]; // "bg", "text", etc.
|
|
344
|
+
const opacity = opMatch[3]; // "20", "50", etc.
|
|
345
|
+
|
|
346
|
+
const base = colorClasses[prefix];
|
|
347
|
+
if (!base) {
|
|
348
|
+
result.push(cls); // No base color found, keep as-is
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const opacityStr = opacity === "100" ? "" : `/${opacity}`;
|
|
353
|
+
const replacement = `${modifier}${prefix}-${base.color}${opacityStr}`;
|
|
354
|
+
result.push(replacement);
|
|
355
|
+
changes.push(`Merged ${cls} → ${replacement}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return { result, changes };
|
|
359
|
+
}
|
|
360
|
+
|
|
315
361
|
// ── Fix a className string ──────────────────────────────────────
|
|
316
362
|
function fixClassNameString(classes: string): { fixed: string; changes: string[] } {
|
|
317
363
|
const changes: string[] = [];
|
|
@@ -340,7 +386,14 @@ function fixClassNameString(classes: string): { fixed: string; changes: string[]
|
|
|
340
386
|
return fixed;
|
|
341
387
|
});
|
|
342
388
|
|
|
343
|
-
// 3. Fix
|
|
389
|
+
// 3. Fix orphaned opacity classes (non-adjacent to color class)
|
|
390
|
+
const opacityFix = fixOrphanedOpacity(classList);
|
|
391
|
+
if (opacityFix.changes.length > 0) {
|
|
392
|
+
classList = opacityFix.result;
|
|
393
|
+
changes.push(...opacityFix.changes);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 4. Fix responsive ordering
|
|
344
397
|
if (hasOrderIssues(classList)) {
|
|
345
398
|
const reordered = fixResponsiveOrder(classList);
|
|
346
399
|
if (reordered.join(" ") !== classList.join(" ")) {
|
|
@@ -352,6 +405,213 @@ function fixClassNameString(classes: string): { fixed: string; changes: string[]
|
|
|
352
405
|
return { fixed: classList.join(" "), changes };
|
|
353
406
|
}
|
|
354
407
|
|
|
408
|
+
// ── Fix negative z-index on background images ──────────────────
|
|
409
|
+
// In Tailwind v3, `-z-10` on an absolute image inside a relative parent worked
|
|
410
|
+
// to push the image behind the parent's content. In Tailwind v4 + React,
|
|
411
|
+
// stacking contexts from wrappers (section elements, animation, etc.) can trap
|
|
412
|
+
// the negative z-index, making the image invisible.
|
|
413
|
+
// Fix: replace `-z-{n}` with `z-0` on images. Since the image comes first in DOM,
|
|
414
|
+
// content siblings (which have z-index: auto) render on top naturally.
|
|
415
|
+
const NEG_Z_ON_IMAGE_REGEX = /\b-z-\d+\b/g;
|
|
416
|
+
|
|
417
|
+
function fixNegativeZIndex(content: string): { content: string; changed: boolean; notes: string[] } {
|
|
418
|
+
const notes: string[] = [];
|
|
419
|
+
let changed = false;
|
|
420
|
+
let result = content;
|
|
421
|
+
|
|
422
|
+
// Step 1: Replace -z-{n} with z-0 on img/Image elements + add inset-0
|
|
423
|
+
result = result.replace(
|
|
424
|
+
/<(?:img|Image)\b[\s\S]*?(?:\/>|>)/g,
|
|
425
|
+
(tag) => {
|
|
426
|
+
if (!/-z-\d+/.test(tag)) return tag;
|
|
427
|
+
let fixed = tag.replace(/(?<=\s|"|`)-z-(\d+)\b/g, (m) => {
|
|
428
|
+
changed = true;
|
|
429
|
+
notes.push(`Background image: replaced ${m} with z-0`);
|
|
430
|
+
return "z-0";
|
|
431
|
+
});
|
|
432
|
+
// Add inset-0 if not present (ensures absolute image covers parent)
|
|
433
|
+
if (fixed.includes("absolute") && !fixed.includes("inset-0")) {
|
|
434
|
+
fixed = fixed.replace(/\babsolute\b/, "absolute inset-0");
|
|
435
|
+
notes.push("Added inset-0 to absolute background image");
|
|
436
|
+
}
|
|
437
|
+
return fixed;
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Step 2: When parent div has backgroundColor inline style + child img with z-0,
|
|
442
|
+
// extract backgroundColor into a separate overlay div and bump content to z-20.
|
|
443
|
+
// Use a simple two-pass approach to avoid JSX nesting issues:
|
|
444
|
+
// a) Extract and remove backgroundColor from parent div
|
|
445
|
+
// b) Insert overlay div before the content div (after the image conditional block)
|
|
446
|
+
if (changed && /style=\{\{[^}]*backgroundColor/.test(result)) {
|
|
447
|
+
const bgMatch = result.match(/style=\{\{\s*backgroundColor:\s*"([^"]+)"\s*\}\}/);
|
|
448
|
+
if (bgMatch) {
|
|
449
|
+
const bgValue = bgMatch[1];
|
|
450
|
+
|
|
451
|
+
// Remove the style attribute from the parent div
|
|
452
|
+
result = result.replace(/\s*style=\{\{\s*backgroundColor:\s*"[^"]+"\s*\}\}/, "");
|
|
453
|
+
|
|
454
|
+
// Insert overlay div. Find the closing of the image conditional block:
|
|
455
|
+
// Pattern: )} followed by whitespace then <div
|
|
456
|
+
// This handles {imageBg && (<Image ... />)} <div content>
|
|
457
|
+
let insertedOverlay = false;
|
|
458
|
+
|
|
459
|
+
// Try pattern: )} then <div (conditional image)
|
|
460
|
+
if (/\)\}\s*\n\s*<div/.test(result)) {
|
|
461
|
+
result = result.replace(
|
|
462
|
+
/(\)\})([\s\n]*)(<div\b)/,
|
|
463
|
+
(m, closing, ws, divTag) => {
|
|
464
|
+
if (insertedOverlay) return m;
|
|
465
|
+
insertedOverlay = true;
|
|
466
|
+
return `${closing}${ws}{/* Overlay */}\n <div className="absolute inset-0 z-10" style={{ backgroundColor: "${bgValue}" }} />${ws}${divTag}`;
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Try pattern: /> then <div (direct image, no conditional)
|
|
472
|
+
if (!insertedOverlay && /\/>\s*\n\s*<div/.test(result)) {
|
|
473
|
+
result = result.replace(
|
|
474
|
+
/(\/>\s*\n)([\s]*)(<div\b)/,
|
|
475
|
+
(m, closing, ws, divTag) => {
|
|
476
|
+
if (insertedOverlay) return m;
|
|
477
|
+
insertedOverlay = true;
|
|
478
|
+
return `${closing}${ws}{/* Overlay */}\n${ws}<div className="absolute inset-0 z-10" style={{ backgroundColor: "${bgValue}" }} />\n${ws}${divTag}`;
|
|
479
|
+
},
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (insertedOverlay) {
|
|
484
|
+
// Bump the first content div after the overlay to z-20.
|
|
485
|
+
// Find the overlay marker, then the next className= string after it.
|
|
486
|
+
const overlayMarker = `style={{ backgroundColor: "${bgValue}" }} />`;
|
|
487
|
+
const overlayIdx = result.indexOf(overlayMarker);
|
|
488
|
+
if (overlayIdx !== -1) {
|
|
489
|
+
const afterOverlay = result.substring(overlayIdx + overlayMarker.length);
|
|
490
|
+
// Find first className= after overlay (handles both className="..." and className={clx("...")})
|
|
491
|
+
const classMatch = afterOverlay.match(/className=(?:\{clx\(\s*)?[""`]([^""`]*)/);
|
|
492
|
+
if (classMatch && classMatch[1] && !/z-\d+/.test(classMatch[1])) {
|
|
493
|
+
const originalClass = classMatch[1];
|
|
494
|
+
const fixedClass = `relative z-20 ${originalClass}`;
|
|
495
|
+
// Replace only the first occurrence after the overlay
|
|
496
|
+
const beforeOverlay = result.substring(0, overlayIdx + overlayMarker.length);
|
|
497
|
+
const fixed = afterOverlay.replace(originalClass, fixedClass);
|
|
498
|
+
result = beforeOverlay + fixed;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
notes.push(`Extracted backgroundColor overlay: ${bgValue}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Step 3: For content divs that are siblings of z-0 images,
|
|
507
|
+
// add `relative z-10` so content renders above the background image.
|
|
508
|
+
// Uses a line-by-line approach to avoid regex issues with JSX nesting.
|
|
509
|
+
if (changed) {
|
|
510
|
+
const lines = result.split("\n");
|
|
511
|
+
let foundZ0Image = false;
|
|
512
|
+
let needsContentZIndex = false;
|
|
513
|
+
|
|
514
|
+
for (let i = 0; i < lines.length; i++) {
|
|
515
|
+
const line = lines[i];
|
|
516
|
+
|
|
517
|
+
// Detect start of an image element (may span multiple lines)
|
|
518
|
+
if (/<(?:img|Image)\b/.test(line)) {
|
|
519
|
+
foundZ0Image = true;
|
|
520
|
+
if (/\bz-0\b/.test(line)) {
|
|
521
|
+
needsContentZIndex = true;
|
|
522
|
+
}
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
// Detect z-0 on a subsequent line of the image element
|
|
526
|
+
if (foundZ0Image && !needsContentZIndex && /\bz-0\b/.test(line)) {
|
|
527
|
+
needsContentZIndex = true;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
// Detect end of multi-line image element (self-closing />)
|
|
531
|
+
if (foundZ0Image && !needsContentZIndex && /\/>/.test(line)) {
|
|
532
|
+
// Image closed without z-0, reset
|
|
533
|
+
foundZ0Image = false;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// When we hit the closing of an Image block, start looking for content div
|
|
538
|
+
if (foundZ0Image && /\)\}/.test(line)) {
|
|
539
|
+
// The conditional image block closed, content div should be next
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Skip overlay divs we inserted
|
|
544
|
+
if (/Overlay/.test(line)) continue;
|
|
545
|
+
if (/absolute inset-0 z-10/.test(line)) continue;
|
|
546
|
+
|
|
547
|
+
// Find the first content div after the image
|
|
548
|
+
if (needsContentZIndex && /^\s*<div\b/.test(line)) {
|
|
549
|
+
// Check if this div already has a z-index
|
|
550
|
+
if (/z-\d+/.test(line)) {
|
|
551
|
+
needsContentZIndex = false;
|
|
552
|
+
foundZ0Image = false;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
// Also check the next few lines for z-index (multi-line className)
|
|
556
|
+
let hasZ = false;
|
|
557
|
+
for (let j = i; j < Math.min(i + 5, lines.length); j++) {
|
|
558
|
+
if (/z-\d+/.test(lines[j])) { hasZ = true; break; }
|
|
559
|
+
if (/>/.test(lines[j]) && j !== i) break;
|
|
560
|
+
}
|
|
561
|
+
if (hasZ) {
|
|
562
|
+
needsContentZIndex = false;
|
|
563
|
+
foundZ0Image = false;
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Add relative z-10 to the className — may be on this line or a subsequent one
|
|
568
|
+
let classLine = i;
|
|
569
|
+
for (let k = i; k < Math.min(i + 4, lines.length); k++) {
|
|
570
|
+
if (/className=/.test(lines[k])) {
|
|
571
|
+
classLine = k;
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (/className="/.test(lines[classLine])) {
|
|
577
|
+
lines[classLine] = lines[classLine].replace(/className="/, 'className="relative z-10 ');
|
|
578
|
+
notes.push("Added relative z-10 to content div sibling of background image");
|
|
579
|
+
} else if (/className=\{clx\(/.test(lines[classLine])) {
|
|
580
|
+
// clx( may have its first string on the same line or the next
|
|
581
|
+
if (/className=\{clx\(\s*"/.test(lines[classLine])) {
|
|
582
|
+
lines[classLine] = lines[classLine].replace(/className=\{clx\(\s*"/, 'className={clx("relative z-10 ');
|
|
583
|
+
} else {
|
|
584
|
+
// First string argument is on the next line
|
|
585
|
+
for (let k = classLine + 1; k < Math.min(classLine + 3, lines.length); k++) {
|
|
586
|
+
if (/^\s*"/.test(lines[k])) {
|
|
587
|
+
lines[k] = lines[k].replace(/^(\s*)"/, '$1"relative z-10 ');
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
notes.push("Added relative z-10 to content div sibling of background image");
|
|
593
|
+
} else if (/className=\{`/.test(lines[classLine])) {
|
|
594
|
+
lines[classLine] = lines[classLine].replace(/className=\{`/, 'className={`relative z-10 ');
|
|
595
|
+
notes.push("Added relative z-10 to content div sibling of background image");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
needsContentZIndex = false;
|
|
599
|
+
foundZ0Image = false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
result = lines.join("\n");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Step 4: Flag remaining -z-{n} on non-image elements for manual review
|
|
607
|
+
const remainingNegZ = result.match(/(?<=\s|"|`)-z-\d+/g);
|
|
608
|
+
if (remainingNegZ) {
|
|
609
|
+
notes.push(`MANUAL: ${remainingNegZ.length} remaining negative z-index usage(s) — may need manual fix for stacking context issues`);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { content: result, changed, notes };
|
|
613
|
+
}
|
|
614
|
+
|
|
355
615
|
/**
|
|
356
616
|
* Transform Tailwind classes in a file.
|
|
357
617
|
*
|
|
@@ -366,6 +626,76 @@ export function transformTailwind(content: string): TransformResult {
|
|
|
366
626
|
let changed = false;
|
|
367
627
|
let result = content;
|
|
368
628
|
|
|
629
|
+
// ── Fix negative z-index on background images ──────────────────
|
|
630
|
+
const zFix = fixNegativeZIndex(result);
|
|
631
|
+
if (zFix.changed) {
|
|
632
|
+
result = zFix.content;
|
|
633
|
+
changed = true;
|
|
634
|
+
notes.push(...zFix.notes);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Fix opacity utility pattern (Tailwind v4 breaking change) ──
|
|
638
|
+
// bg-black bg-opacity-20 → bg-black/20
|
|
639
|
+
// border-white border-opacity-20 → border-white/20
|
|
640
|
+
// text-gray-600 text-opacity-50 → text-gray-600/50
|
|
641
|
+
//
|
|
642
|
+
// The pattern is: {prefix}-{color} {prefix}-opacity-{N} → {prefix}-{color}/{N}
|
|
643
|
+
// prefix can be: bg, text, border, ring, divide, placeholder
|
|
644
|
+
if (/(?:bg|text|border|ring|divide|placeholder)-opacity-\d+/.test(result)) {
|
|
645
|
+
// Match: bg-{color} bg-opacity-{N}
|
|
646
|
+
result = result.replace(
|
|
647
|
+
/\b(bg-[\w-]+?)\s+bg-opacity-(\d+)/g,
|
|
648
|
+
"$1/$2",
|
|
649
|
+
);
|
|
650
|
+
// Match: text-{color} text-opacity-{N}
|
|
651
|
+
result = result.replace(
|
|
652
|
+
/\b(text-[\w-]+?)\s+text-opacity-(\d+)/g,
|
|
653
|
+
"$1/$2",
|
|
654
|
+
);
|
|
655
|
+
// Match: border-{color} border-opacity-{N}
|
|
656
|
+
result = result.replace(
|
|
657
|
+
/\b(border-[\w-]+?)\s+border-opacity-(\d+)/g,
|
|
658
|
+
"$1/$2",
|
|
659
|
+
);
|
|
660
|
+
// Match: ring-{color} ring-opacity-{N}
|
|
661
|
+
result = result.replace(
|
|
662
|
+
/\b(ring-[\w-]+?)\s+ring-opacity-(\d+)/g,
|
|
663
|
+
"$1/$2",
|
|
664
|
+
);
|
|
665
|
+
// Match: divide-{color} divide-opacity-{N}
|
|
666
|
+
result = result.replace(
|
|
667
|
+
/\b(divide-[\w-]+?)\s+divide-opacity-(\d+)/g,
|
|
668
|
+
"$1/$2",
|
|
669
|
+
);
|
|
670
|
+
// Match: placeholder-{color} placeholder-opacity-{N}
|
|
671
|
+
result = result.replace(
|
|
672
|
+
/\b(placeholder-[\w-]+?)\s+placeholder-opacity-(\d+)/g,
|
|
673
|
+
"$1/$2",
|
|
674
|
+
);
|
|
675
|
+
// Handle hover:/focus:/active: prefixed opacity (e.g. hover:bg-opacity-100)
|
|
676
|
+
result = result.replace(
|
|
677
|
+
/\b((?:hover:|focus:|active:)(?:bg|text|border|ring)-[\w-]+?)\s+(?:hover:|focus:|active:)(?:bg|text|border|ring)-opacity-(\d+)/g,
|
|
678
|
+
"$1/$2",
|
|
679
|
+
);
|
|
680
|
+
// Handle hover:bg-opacity-N when bg-{color}/{N} already exists
|
|
681
|
+
// e.g. "bg-white/80 hover:bg-opacity-100" → "bg-white/80 hover:bg-white"
|
|
682
|
+
// e.g. "bg-black/60 hover:bg-opacity-50" → "bg-black/60 hover:bg-black/50"
|
|
683
|
+
result = result.replace(
|
|
684
|
+
/\b(bg|text|border|ring)-([\w-]+?)\/(\d+)\s+((?:hover:|focus:|active:)+)\1-opacity-(\d+)/g,
|
|
685
|
+
(_m, prefix, color, _baseOp, modifier, hoverOp) => {
|
|
686
|
+
const opacityStr = hoverOp === "100" ? "" : `/${hoverOp}`;
|
|
687
|
+
return `${prefix}-${color}/${_baseOp} ${modifier}${prefix}-${color}${opacityStr}`;
|
|
688
|
+
},
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
// Handle standalone orphaned opacity classes that weren't caught
|
|
692
|
+
if (/(?:bg|text|border|ring)-opacity-\d+/.test(result)) {
|
|
693
|
+
notes.push("MANUAL: Some *-opacity-N classes remain — color class may not be adjacent");
|
|
694
|
+
}
|
|
695
|
+
changed = true;
|
|
696
|
+
notes.push("Converted *-opacity-N to modifier syntax (e.g. bg-black/20)");
|
|
697
|
+
}
|
|
698
|
+
|
|
369
699
|
// Match className="...", className={`...`}, class="..."
|
|
370
700
|
const patterns = [
|
|
371
701
|
/(?<=className\s*=\s*")([^"]+)(?=")/g,
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -75,6 +75,11 @@ export interface MigrationContext {
|
|
|
75
75
|
/** npm dependencies discovered from inline npm: imports in source files */
|
|
76
76
|
discoveredNpmDeps: Record<string, string>;
|
|
77
77
|
|
|
78
|
+
/** Theme colors extracted from .deco/blocks CMS config */
|
|
79
|
+
themeColors: Record<string, string>;
|
|
80
|
+
/** Font family from CMS config */
|
|
81
|
+
fontFamily: string | null;
|
|
82
|
+
|
|
78
83
|
/** All categorized source files */
|
|
79
84
|
files: FileRecord[];
|
|
80
85
|
|
|
@@ -118,6 +123,8 @@ export function createContext(
|
|
|
118
123
|
gtmId: null,
|
|
119
124
|
importMap: {},
|
|
120
125
|
discoveredNpmDeps: {},
|
|
126
|
+
themeColors: {},
|
|
127
|
+
fontFamily: null,
|
|
121
128
|
files: [],
|
|
122
129
|
scaffoldedFiles: [],
|
|
123
130
|
transformedFiles: [],
|
package/scripts/migrate.ts
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import * as path from "node:path";
|
|
27
|
-
import {
|
|
27
|
+
import { execSync } from "node:child_process";
|
|
28
|
+
import { createContext, logPhase } from "./migrate/types.ts";
|
|
28
29
|
import { analyze } from "./migrate/phase-analyze.ts";
|
|
29
30
|
import { scaffold } from "./migrate/phase-scaffold.ts";
|
|
30
31
|
import { transform } from "./migrate/phase-transform.ts";
|
|
@@ -126,10 +127,49 @@ async function main() {
|
|
|
126
127
|
if (!ok) {
|
|
127
128
|
process.exit(2);
|
|
128
129
|
}
|
|
130
|
+
|
|
131
|
+
// Phase 7: Bootstrap (install + generate)
|
|
132
|
+
if (!ctx.dryRun) {
|
|
133
|
+
bootstrap(ctx);
|
|
134
|
+
}
|
|
129
135
|
} catch (error) {
|
|
130
136
|
console.error(`\n ${red("Migration failed:")}`, error);
|
|
131
137
|
process.exit(1);
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
function bootstrap(ctx: { sourceDir: string }) {
|
|
142
|
+
logPhase("Bootstrap (install + generate)");
|
|
143
|
+
|
|
144
|
+
let failures = 0;
|
|
145
|
+
const run = (cmd: string, label: string, critical = false) => {
|
|
146
|
+
console.log(` Running: ${label}...`);
|
|
147
|
+
try {
|
|
148
|
+
execSync(cmd, { cwd: ctx.sourceDir, stdio: "pipe" });
|
|
149
|
+
console.log(` ${green("✓")} ${label}`);
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
failures++;
|
|
152
|
+
const icon = critical ? red("✗") : yellow("⚠");
|
|
153
|
+
console.log(` ${icon} ${label} failed: ${e.message?.split("\n")[0]}`);
|
|
154
|
+
if (critical) {
|
|
155
|
+
console.log(`\n ${red("Bootstrap aborted.")} Fix the error above and run manually.\n`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Detect package manager
|
|
163
|
+
const pm = process.env.npm_execpath?.includes("bun") ? "bun" : "npm";
|
|
164
|
+
if (!run(`${pm} install`, "Install dependencies", true)) return;
|
|
165
|
+
run("npx tsx node_modules/@decocms/start/scripts/generate-blocks.ts", "Generate CMS blocks");
|
|
166
|
+
run("npx tsr generate", "Generate TanStack routes");
|
|
167
|
+
|
|
168
|
+
if (failures > 0) {
|
|
169
|
+
console.log(`\n ${yellow("Bootstrap completed with warnings.")} Check errors above before running dev.\n`);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(`\n ${green("Ready!")} Run \`${pm} run dev\` to start the dev server.\n`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
135
175
|
main();
|