@decocms/start 1.2.5 → 1.2.7
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 +1 -1
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/migrate/analyzers/island-classifier.ts +73 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +91 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/phase-analyze.ts +147 -17
- package/scripts/migrate/phase-cleanup.ts +124 -2
- package/scripts/migrate/phase-report.ts +44 -16
- package/scripts/migrate/phase-scaffold.ts +38 -132
- package/scripts/migrate/phase-transform.ts +28 -3
- package/scripts/migrate/phase-verify.ts +127 -5
- package/scripts/migrate/templates/app-css.ts +204 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +124 -0
- package/scripts/migrate/templates/hooks.ts +358 -0
- package/scripts/migrate/templates/package-json.ts +29 -6
- package/scripts/migrate/templates/routes.ts +41 -136
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +108 -0
- package/scripts/migrate/templates/server-entry.ts +174 -67
- package/scripts/migrate/templates/setup.ts +64 -55
- package/scripts/migrate/templates/types-gen.ts +119 -0
- package/scripts/migrate/templates/ui-components.ts +113 -0
- package/scripts/migrate/templates/vite-config.ts +18 -1
- package/scripts/migrate/templates/wrangler.ts +4 -1
- package/scripts/migrate/transforms/dead-code.ts +23 -2
- package/scripts/migrate/transforms/imports.ts +40 -10
- package/scripts/migrate/transforms/jsx.ts +9 -0
- package/scripts/migrate/transforms/section-conventions.ts +83 -0
- package/scripts/migrate/types.ts +74 -0
- package/src/cms/resolve.ts +10 -0
- package/src/routes/cmsRoute.ts +13 -0
|
@@ -10,6 +10,15 @@ import { generateKnipConfig } from "./templates/knip-config.ts";
|
|
|
10
10
|
import { generateRoutes } from "./templates/routes.ts";
|
|
11
11
|
import { generateSetup } from "./templates/setup.ts";
|
|
12
12
|
import { generateServerEntry } from "./templates/server-entry.ts";
|
|
13
|
+
import { generateAppCss } from "./templates/app-css.ts";
|
|
14
|
+
import { generateTypeFiles } from "./templates/types-gen.ts";
|
|
15
|
+
import { generateUiComponents } from "./templates/ui-components.ts";
|
|
16
|
+
import { generateHooks } from "./templates/hooks.ts";
|
|
17
|
+
import { generateCommerceLoaders } from "./templates/commerce-loaders.ts";
|
|
18
|
+
import { generateSectionLoaders } from "./templates/section-loaders.ts";
|
|
19
|
+
import { generateCacheConfig } from "./templates/cache-config.ts";
|
|
20
|
+
import { generateSdkFiles } from "./templates/sdk-gen.ts";
|
|
21
|
+
import { extractTheme } from "./analyzers/theme-extractor.ts";
|
|
13
22
|
|
|
14
23
|
function writeFile(ctx: MigrationContext, relPath: string, content: string) {
|
|
15
24
|
const fullPath = path.join(ctx.sourceDir, relPath);
|
|
@@ -20,7 +29,6 @@ function writeFile(ctx: MigrationContext, relPath: string, content: string) {
|
|
|
20
29
|
return;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
// Ensure directory exists
|
|
24
32
|
const dir = path.dirname(fullPath);
|
|
25
33
|
fs.mkdirSync(dir, { recursive: true });
|
|
26
34
|
|
|
@@ -29,6 +37,12 @@ function writeFile(ctx: MigrationContext, relPath: string, content: string) {
|
|
|
29
37
|
ctx.scaffoldedFiles.push(relPath);
|
|
30
38
|
}
|
|
31
39
|
|
|
40
|
+
function writeMultiFile(ctx: MigrationContext, files: Record<string, string>) {
|
|
41
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
42
|
+
writeFile(ctx, filePath, content);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
export function scaffold(ctx: MigrationContext): void {
|
|
33
47
|
logPhase("Scaffold");
|
|
34
48
|
|
|
@@ -47,38 +61,41 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
47
61
|
tabWidth: 2,
|
|
48
62
|
}, null, 2) + "\n");
|
|
49
63
|
|
|
50
|
-
// Server entry files
|
|
51
|
-
|
|
52
|
-
for (const [filePath, content] of Object.entries(serverEntryFiles)) {
|
|
53
|
-
writeFile(ctx, filePath, content);
|
|
54
|
-
}
|
|
64
|
+
// Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts)
|
|
65
|
+
writeMultiFile(ctx, generateServerEntry(ctx));
|
|
55
66
|
|
|
56
67
|
// Route files
|
|
57
|
-
|
|
58
|
-
for (const [filePath, content] of Object.entries(routeFiles)) {
|
|
59
|
-
writeFile(ctx, filePath, content);
|
|
60
|
-
}
|
|
68
|
+
writeMultiFile(ctx, generateRoutes(ctx));
|
|
61
69
|
|
|
62
|
-
// Setup
|
|
70
|
+
// Setup infrastructure
|
|
63
71
|
writeFile(ctx, "src/setup.ts", generateSetup(ctx));
|
|
72
|
+
writeFile(ctx, "src/cache-config.ts", generateCacheConfig(ctx));
|
|
73
|
+
writeFile(ctx, "src/setup/commerce-loaders.ts", generateCommerceLoaders(ctx));
|
|
74
|
+
writeFile(ctx, "src/setup/section-loaders.ts", generateSectionLoaders(ctx));
|
|
64
75
|
|
|
65
|
-
// Styles
|
|
66
|
-
|
|
76
|
+
// Theme extraction + Styles
|
|
77
|
+
const theme = extractTheme(ctx);
|
|
78
|
+
writeFile(ctx, "src/styles/app.css", generateAppCss(ctx, theme));
|
|
67
79
|
|
|
68
|
-
//
|
|
69
|
-
|
|
80
|
+
// Type definitions
|
|
81
|
+
writeMultiFile(ctx, generateTypeFiles(ctx));
|
|
70
82
|
|
|
71
|
-
//
|
|
72
|
-
|
|
83
|
+
// UI components (Image, Picture, Video)
|
|
84
|
+
writeMultiFile(ctx, generateUiComponents(ctx));
|
|
73
85
|
|
|
74
|
-
//
|
|
86
|
+
// Platform hooks (useCart, useUser, useWishlist)
|
|
87
|
+
writeMultiFile(ctx, generateHooks(ctx));
|
|
88
|
+
|
|
89
|
+
// SDK shims + generated utilities
|
|
90
|
+
writeFile(ctx, "src/sdk/signal.ts", generateSignalShim());
|
|
91
|
+
writeFile(ctx, "src/sdk/clx.ts", generateClxShim());
|
|
75
92
|
writeFile(ctx, "src/sdk/debounce.ts", generateDebounceShim());
|
|
93
|
+
writeMultiFile(ctx, generateSdkFiles(ctx));
|
|
76
94
|
|
|
77
95
|
// Apps
|
|
78
96
|
writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx));
|
|
79
97
|
|
|
80
98
|
// SiteTheme component (replaces apps/website/components/Theme.tsx)
|
|
81
|
-
// Check if any source file uses SiteTheme
|
|
82
99
|
const usesSiteTheme = ctx.files.some((f) => {
|
|
83
100
|
if (f.action === "delete") return false;
|
|
84
101
|
try {
|
|
@@ -112,10 +129,6 @@ export interface Props {
|
|
|
112
129
|
variables?: Array<{ name: string; value: string }>;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
/**
|
|
116
|
-
* SiteTheme — injects CSS custom properties and font stylesheets into the page.
|
|
117
|
-
* This replaces the old apps/website/components/Theme.tsx from the Deno stack.
|
|
118
|
-
*/
|
|
119
132
|
export default function SiteTheme({ variables, fonts, colorScheme }: Props) {
|
|
120
133
|
const cssVars = variables?.length
|
|
121
134
|
? \`:root { \${variables.map((v) => \`\${v.name}: \${v.value};\`).join(" ")} }\`
|
|
@@ -143,57 +156,6 @@ export { type Font as SiteThemeFont };
|
|
|
143
156
|
`;
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
function generateAppCss(ctx: MigrationContext): string {
|
|
147
|
-
const c = ctx.themeColors;
|
|
148
|
-
// Map CMS color names to DaisyUI v5 CSS variables
|
|
149
|
-
const colors: Record<string, string> = {
|
|
150
|
-
"--color-primary": c["primary"] || "#6B21A8",
|
|
151
|
-
"--color-secondary": c["secondary"] || "#141414",
|
|
152
|
-
"--color-accent": c["tertiary"] || "#FFF100",
|
|
153
|
-
"--color-neutral": c["neutral"] || "#393939",
|
|
154
|
-
"--color-base-100": c["base-100"] || "#FFFFFF",
|
|
155
|
-
"--color-base-200": c["base-200"] || "#F3F3F3",
|
|
156
|
-
"--color-base-300": c["base-300"] || "#868686",
|
|
157
|
-
"--color-info": c["info"] || "#006CA1",
|
|
158
|
-
"--color-success": c["success"] || "#007552",
|
|
159
|
-
"--color-warning": c["warning"] || "#F8D13A",
|
|
160
|
-
"--color-error": c["error"] || "#CF040A",
|
|
161
|
-
};
|
|
162
|
-
// Add content colors if specified
|
|
163
|
-
if (c["primary-content"]) colors["--color-primary-content"] = c["primary-content"];
|
|
164
|
-
if (c["secondary-content"]) colors["--color-secondary-content"] = c["secondary-content"];
|
|
165
|
-
if (c["base-content"]) colors["--color-base-content"] = c["base-content"];
|
|
166
|
-
|
|
167
|
-
const colorLines = Object.entries(colors)
|
|
168
|
-
.map(([k, v]) => ` ${k}: ${v};`)
|
|
169
|
-
.join("\n");
|
|
170
|
-
|
|
171
|
-
return `@import "tailwindcss";
|
|
172
|
-
@plugin "daisyui";
|
|
173
|
-
@plugin "daisyui/theme" {
|
|
174
|
-
name: "light";
|
|
175
|
-
default: true;
|
|
176
|
-
color-scheme: light;
|
|
177
|
-
|
|
178
|
-
${colorLines}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
@theme {
|
|
182
|
-
--color-*: initial;
|
|
183
|
-
--color-white: #fff;
|
|
184
|
-
--color-black: #000;
|
|
185
|
-
--color-transparent: transparent;
|
|
186
|
-
--color-current: currentColor;
|
|
187
|
-
--color-inherit: inherit;${ctx.fontFamily ? `\n --font-sans: "${ctx.fontFamily}", ui-sans-serif, system-ui, sans-serif;` : ""}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/* View transitions */
|
|
191
|
-
@view-transition {
|
|
192
|
-
navigation: auto;
|
|
193
|
-
}
|
|
194
|
-
`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
159
|
function generateGitignore(): string {
|
|
198
160
|
return `# Dependencies
|
|
199
161
|
node_modules/
|
|
@@ -224,9 +186,6 @@ vite.config.timestamp_*
|
|
|
224
186
|
# Deco CMS
|
|
225
187
|
.deco/metadata/*
|
|
226
188
|
|
|
227
|
-
# Bun lock file (if using npm, keep package-lock.json instead)
|
|
228
|
-
# package-lock.json
|
|
229
|
-
|
|
230
189
|
# IDE
|
|
231
190
|
.vscode/
|
|
232
191
|
.idea/
|
|
@@ -276,66 +235,13 @@ export default debounce;
|
|
|
276
235
|
}
|
|
277
236
|
|
|
278
237
|
function generateSignalShim(): string {
|
|
279
|
-
return `
|
|
280
|
-
import { useSyncExternalStore, useMemo, useEffect } from "react";
|
|
281
|
-
|
|
282
|
-
export interface Signal<T> {
|
|
283
|
-
readonly store: Store<T>;
|
|
284
|
-
value: T;
|
|
285
|
-
peek(): T;
|
|
286
|
-
subscribe(fn: () => void): () => void;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
export function signal<T>(initialValue: T): Signal<T> {
|
|
290
|
-
const store = new Store<T>(initialValue);
|
|
291
|
-
return {
|
|
292
|
-
store,
|
|
293
|
-
get value() { return store.state; },
|
|
294
|
-
set value(v: T) { store.setState(() => v); },
|
|
295
|
-
peek() { return store.state; },
|
|
296
|
-
subscribe(fn) {
|
|
297
|
-
// @tanstack/store@0.9.x returns { unsubscribe: Function },
|
|
298
|
-
// NOT a plain function. React's useSyncExternalStore cleanup
|
|
299
|
-
// expects a bare function — unwrap it.
|
|
300
|
-
const sub = store.subscribe(() => fn());
|
|
301
|
-
return typeof sub === "function" ? sub : sub.unsubscribe;
|
|
302
|
-
},
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function useSignal<T>(initialValue: T): Signal<T> {
|
|
307
|
-
const sig = useMemo(() => signal(initialValue), []);
|
|
308
|
-
useSyncExternalStore(
|
|
309
|
-
(cb) => sig.subscribe(cb),
|
|
310
|
-
() => sig.value,
|
|
311
|
-
() => sig.value,
|
|
312
|
-
);
|
|
313
|
-
return sig;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function useComputed<T>(fn: () => T): Signal<T> {
|
|
317
|
-
const sig = useMemo(() => signal(fn()), [fn]);
|
|
318
|
-
return sig;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function computed<T>(fn: () => T): Signal<T> {
|
|
322
|
-
return signal(fn());
|
|
323
|
-
}
|
|
238
|
+
return `export { signal, type ReactiveSignal } from "@decocms/start/sdk/signal";
|
|
324
239
|
|
|
240
|
+
/** Run a function immediately. Kept for legacy module-level side effects. */
|
|
325
241
|
export function effect(fn: () => void | (() => void)): () => void {
|
|
326
242
|
const cleanup = fn();
|
|
327
243
|
return typeof cleanup === "function" ? cleanup : () => {};
|
|
328
244
|
}
|
|
329
|
-
|
|
330
|
-
export function batch(fn: () => void): void {
|
|
331
|
-
fn();
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function useSignalEffect(fn: () => void | (() => void)): void {
|
|
335
|
-
useEffect(fn);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export type { Signal as ReadonlySignal };
|
|
339
245
|
`;
|
|
340
246
|
}
|
|
341
247
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import type { MigrationContext, TransformResult } from "./types.ts";
|
|
3
|
+
import type { MigrationContext, TransformResult, SectionMeta } from "./types.ts";
|
|
4
4
|
import { log, logPhase } from "./types.ts";
|
|
5
5
|
import { transformImports } from "./transforms/imports.ts";
|
|
6
6
|
import { transformJsx } from "./transforms/jsx.ts";
|
|
@@ -8,11 +8,25 @@ import { transformFreshApis } from "./transforms/fresh-apis.ts";
|
|
|
8
8
|
import { transformDenoIsms } from "./transforms/deno-isms.ts";
|
|
9
9
|
import { transformTailwind } from "./transforms/tailwind.ts";
|
|
10
10
|
import { transformDeadCode } from "./transforms/dead-code.ts";
|
|
11
|
+
import { transformSectionConventions } from "./transforms/section-conventions.ts";
|
|
12
|
+
|
|
13
|
+
/** Map of section path → metadata, populated per-run */
|
|
14
|
+
let sectionMetaMap: Map<string, SectionMeta> | null = null;
|
|
15
|
+
|
|
16
|
+
function getSectionMeta(ctx: MigrationContext, relPath: string): SectionMeta | undefined {
|
|
17
|
+
if (!sectionMetaMap) {
|
|
18
|
+
sectionMetaMap = new Map();
|
|
19
|
+
for (const m of ctx.sectionMetas) {
|
|
20
|
+
sectionMetaMap.set(m.path, m);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return sectionMetaMap.get(relPath);
|
|
24
|
+
}
|
|
11
25
|
|
|
12
26
|
/**
|
|
13
27
|
* Apply all transforms to a file's content in the correct order.
|
|
14
28
|
*/
|
|
15
|
-
function applyTransforms(content: string, filePath: string): TransformResult {
|
|
29
|
+
function applyTransforms(content: string, filePath: string, ctx?: MigrationContext, relPath?: string): TransformResult {
|
|
16
30
|
const allNotes: string[] = [];
|
|
17
31
|
let currentContent = content;
|
|
18
32
|
let anyChanged = false;
|
|
@@ -42,6 +56,17 @@ function applyTransforms(content: string, filePath: string): TransformResult {
|
|
|
42
56
|
}
|
|
43
57
|
}
|
|
44
58
|
|
|
59
|
+
// Section conventions (sync/eager/layout/cache) — only for section files
|
|
60
|
+
if (ctx && relPath && relPath.startsWith("sections/")) {
|
|
61
|
+
const meta = getSectionMeta(ctx, relPath);
|
|
62
|
+
const result = transformSectionConventions(currentContent, meta);
|
|
63
|
+
if (result.changed) {
|
|
64
|
+
anyChanged = true;
|
|
65
|
+
currentContent = result.content;
|
|
66
|
+
allNotes.push(...result.notes.map((n) => `[section-conventions] ${n}`));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
return { content: currentContent, changed: anyChanged, notes: allNotes };
|
|
46
71
|
}
|
|
47
72
|
|
|
@@ -59,7 +84,7 @@ export function transform(ctx: MigrationContext): void {
|
|
|
59
84
|
const content = fs.readFileSync(absPath, "utf-8");
|
|
60
85
|
|
|
61
86
|
// Apply transforms
|
|
62
|
-
const result = applyTransforms(content, absPath);
|
|
87
|
+
const result = applyTransforms(content, absPath, ctx, record.path);
|
|
63
88
|
|
|
64
89
|
// Add manual review items
|
|
65
90
|
for (const note of result.notes) {
|
|
@@ -22,8 +22,19 @@ const REQUIRED_FILES = [
|
|
|
22
22
|
"src/runtime.ts",
|
|
23
23
|
"src/context.ts",
|
|
24
24
|
"src/setup.ts",
|
|
25
|
+
"src/cache-config.ts",
|
|
26
|
+
"src/setup/commerce-loaders.ts",
|
|
27
|
+
"src/setup/section-loaders.ts",
|
|
25
28
|
"src/styles/app.css",
|
|
26
29
|
"src/apps/site.ts",
|
|
30
|
+
"src/hooks/useCart.ts",
|
|
31
|
+
"src/hooks/useUser.ts",
|
|
32
|
+
"src/hooks/useWishlist.ts",
|
|
33
|
+
"src/types/widgets.ts",
|
|
34
|
+
"src/types/deco.ts",
|
|
35
|
+
"src/types/commerce-app.ts",
|
|
36
|
+
"src/components/ui/Image.tsx",
|
|
37
|
+
"src/components/ui/Picture.tsx",
|
|
27
38
|
"src/routes/__root.tsx",
|
|
28
39
|
"src/routes/index.tsx",
|
|
29
40
|
"src/routes/$.tsx",
|
|
@@ -38,8 +49,8 @@ const MUST_NOT_EXIST = [
|
|
|
38
49
|
"manifest.gen.ts",
|
|
39
50
|
"dev.ts",
|
|
40
51
|
"main.ts",
|
|
41
|
-
"islands/BlogFeed.tsx",
|
|
42
52
|
"routes/_app.tsx",
|
|
53
|
+
"routes/_middleware.ts",
|
|
43
54
|
];
|
|
44
55
|
|
|
45
56
|
const checks: Check[] = [
|
|
@@ -261,14 +272,16 @@ const checks: Check[] = [
|
|
|
261
272
|
},
|
|
262
273
|
},
|
|
263
274
|
{
|
|
264
|
-
name: "No dead cache/cacheKey exports",
|
|
275
|
+
name: "No dead cache/cacheKey exports (old SWR system)",
|
|
265
276
|
severity: "warning",
|
|
266
277
|
fn: (ctx) => {
|
|
267
278
|
const srcDir = path.join(ctx.sourceDir, "src");
|
|
268
279
|
if (!fs.existsSync(srcDir)) return true;
|
|
269
|
-
|
|
280
|
+
// Only flag the OLD cache patterns: cache = "stale-while-revalidate" or cache = { maxAge: ... }
|
|
281
|
+
// NOT the new-stack section convention: cache = "listing" / "product" / "search" / "static"
|
|
282
|
+
const bad = findFilesWithPattern(srcDir, /^export\s+const\s+cacheKey\s*=/m);
|
|
270
283
|
if (bad.length > 0) {
|
|
271
|
-
console.log(` Dead exports found
|
|
284
|
+
console.log(` Dead cacheKey exports found: ${bad.join(", ")}`);
|
|
272
285
|
return false;
|
|
273
286
|
}
|
|
274
287
|
return true;
|
|
@@ -339,6 +352,110 @@ const checks: Check[] = [
|
|
|
339
352
|
return true;
|
|
340
353
|
},
|
|
341
354
|
},
|
|
355
|
+
{
|
|
356
|
+
name: "No @deco/deco imports in src/",
|
|
357
|
+
severity: "error",
|
|
358
|
+
fn: (ctx) => {
|
|
359
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
360
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
361
|
+
const bad = findFilesWithPattern(srcDir, /from\s+["']@deco\/deco/);
|
|
362
|
+
if (bad.length > 0) {
|
|
363
|
+
console.log(` Still has @deco/deco imports: ${bad.join(", ")}`);
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
return true;
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: "No apps/ imports in src/ (should be @decocms/apps or ~/)",
|
|
371
|
+
severity: "error",
|
|
372
|
+
fn: (ctx) => {
|
|
373
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
374
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
375
|
+
const bad = findFilesWithPattern(srcDir, /from\s+["']apps\//);
|
|
376
|
+
if (bad.length > 0) {
|
|
377
|
+
console.log(` Still has apps/ imports: ${bad.join(", ")}`);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
return true;
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "Setup infrastructure is complete",
|
|
385
|
+
severity: "error",
|
|
386
|
+
fn: (ctx) => {
|
|
387
|
+
const setupFiles = [
|
|
388
|
+
"src/setup.ts",
|
|
389
|
+
"src/cache-config.ts",
|
|
390
|
+
"src/setup/commerce-loaders.ts",
|
|
391
|
+
"src/setup/section-loaders.ts",
|
|
392
|
+
];
|
|
393
|
+
const missing = setupFiles.filter(
|
|
394
|
+
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
395
|
+
);
|
|
396
|
+
if (missing.length > 0) {
|
|
397
|
+
console.log(` Missing setup infrastructure: ${missing.join(", ")}`);
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
return true;
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: "No islands/ directory (should be eliminated)",
|
|
405
|
+
severity: "warning",
|
|
406
|
+
fn: (ctx) => {
|
|
407
|
+
const islandsDir = path.join(ctx.sourceDir, "src", "islands");
|
|
408
|
+
if (fs.existsSync(islandsDir)) {
|
|
409
|
+
try {
|
|
410
|
+
const files = fs.readdirSync(islandsDir, { recursive: true });
|
|
411
|
+
const tsxFiles = (files as string[]).filter((f: string) => f.endsWith(".tsx") || f.endsWith(".ts"));
|
|
412
|
+
if (tsxFiles.length > 0) {
|
|
413
|
+
console.log(` src/islands/ still has ${tsxFiles.length} files — should be moved to components/`);
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
} catch {}
|
|
417
|
+
}
|
|
418
|
+
return true;
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: "Hooks are scaffolded",
|
|
423
|
+
severity: "warning",
|
|
424
|
+
fn: (ctx) => {
|
|
425
|
+
const hookFiles = [
|
|
426
|
+
"src/hooks/useCart.ts",
|
|
427
|
+
"src/hooks/useUser.ts",
|
|
428
|
+
"src/hooks/useWishlist.ts",
|
|
429
|
+
];
|
|
430
|
+
const missing = hookFiles.filter(
|
|
431
|
+
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
432
|
+
);
|
|
433
|
+
if (missing.length > 0) {
|
|
434
|
+
console.log(` Missing hooks: ${missing.join(", ")}`);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
return true;
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
name: "Type files are scaffolded",
|
|
442
|
+
severity: "warning",
|
|
443
|
+
fn: (ctx) => {
|
|
444
|
+
const typeFiles = [
|
|
445
|
+
"src/types/widgets.ts",
|
|
446
|
+
"src/types/deco.ts",
|
|
447
|
+
"src/types/commerce-app.ts",
|
|
448
|
+
];
|
|
449
|
+
const missing = typeFiles.filter(
|
|
450
|
+
(f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
|
|
451
|
+
);
|
|
452
|
+
if (missing.length > 0) {
|
|
453
|
+
console.log(` Missing type files: ${missing.join(", ")}`);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
return true;
|
|
457
|
+
},
|
|
458
|
+
},
|
|
342
459
|
];
|
|
343
460
|
|
|
344
461
|
function findFilesWithPattern(
|
|
@@ -356,7 +473,12 @@ function findFilesWithPattern(
|
|
|
356
473
|
findFilesWithPattern(fullPath, pattern, results, root);
|
|
357
474
|
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
358
475
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
359
|
-
|
|
476
|
+
// Only test non-comment lines
|
|
477
|
+
const uncommented = content
|
|
478
|
+
.split("\n")
|
|
479
|
+
.filter((line) => !line.trimStart().startsWith("//") && !line.trimStart().startsWith("*"))
|
|
480
|
+
.join("\n");
|
|
481
|
+
if (pattern.test(uncommented)) {
|
|
360
482
|
results.push(path.basename(fullPath));
|
|
361
483
|
}
|
|
362
484
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type { MigrationContext } from "../types.ts";
|
|
2
|
+
import type { ExtractedTheme } from "../analyzers/theme-extractor.ts";
|
|
3
|
+
|
|
4
|
+
export function generateAppCss(ctx: MigrationContext, theme?: ExtractedTheme): string {
|
|
5
|
+
const sections: string[] = [];
|
|
6
|
+
|
|
7
|
+
// ── DaisyUI theme plugin ──────────────────────────────────────────
|
|
8
|
+
const daisyColors = theme?.daisyUiColors ?? {};
|
|
9
|
+
const c = ctx.themeColors;
|
|
10
|
+
|
|
11
|
+
const semanticColors: Record<string, string> = {
|
|
12
|
+
"--color-primary": daisyColors["--color-primary"] || c["primary"] || "#B10200",
|
|
13
|
+
"--color-secondary": daisyColors["--color-secondary"] || c["secondary"] || "#141414",
|
|
14
|
+
"--color-accent": daisyColors["--color-accent"] || c["tertiary"] || "#FFF100",
|
|
15
|
+
"--color-neutral": daisyColors["--color-neutral"] || c["neutral"] || "#393939",
|
|
16
|
+
"--color-base-100": daisyColors["--color-base-100"] || c["base-100"] || "#FFFFFF",
|
|
17
|
+
"--color-base-200": daisyColors["--color-base-200"] || c["base-200"] || "#F3F3F3",
|
|
18
|
+
"--color-base-300": daisyColors["--color-base-300"] || c["base-300"] || "#868686",
|
|
19
|
+
"--color-info": daisyColors["--color-info"] || c["info"] || "#006CA1",
|
|
20
|
+
"--color-success": daisyColors["--color-success"] || c["success"] || "#007552",
|
|
21
|
+
"--color-warning": daisyColors["--color-warning"] || c["warning"] || "#F8D13A",
|
|
22
|
+
"--color-error": daisyColors["--color-error"] || c["error"] || "#CF040A",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const colorLines = Object.entries(semanticColors)
|
|
26
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
27
|
+
.join("\n");
|
|
28
|
+
|
|
29
|
+
sections.push(`@import "tailwindcss";
|
|
30
|
+
@plugin "daisyui";
|
|
31
|
+
@plugin "daisyui/theme" {
|
|
32
|
+
name: "light";
|
|
33
|
+
default: true;
|
|
34
|
+
color-scheme: light;
|
|
35
|
+
${colorLines}
|
|
36
|
+
}`);
|
|
37
|
+
|
|
38
|
+
// ── @theme block: Tailwind v3->v4 color migration ─────────────────
|
|
39
|
+
const fontFamily = theme?.fontFamily || ctx.fontFamily;
|
|
40
|
+
let fontLine = "";
|
|
41
|
+
if (fontFamily) {
|
|
42
|
+
const firstFont = fontFamily.split(",")[0].trim().replace(/['"]/g, "");
|
|
43
|
+
fontLine = `\n --font-sans: "${firstFont}", ui-sans-serif, system-ui, sans-serif;`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let themeBlock = `/* Tailwind v4: reset default palette (old sites replaced it entirely via theme.colors)
|
|
47
|
+
then re-add only the colors used by this site. */
|
|
48
|
+
@theme {
|
|
49
|
+
--color-*: initial;
|
|
50
|
+
|
|
51
|
+
--color-white: #fff;
|
|
52
|
+
--color-black: #000;
|
|
53
|
+
--color-transparent: transparent;
|
|
54
|
+
--color-current: currentColor;
|
|
55
|
+
--color-inherit: inherit;${fontLine}`;
|
|
56
|
+
|
|
57
|
+
// Add extracted theme variables to @theme
|
|
58
|
+
const vars = theme?.variables ?? {};
|
|
59
|
+
if (Object.keys(vars).length > 0) {
|
|
60
|
+
themeBlock += `\n`;
|
|
61
|
+
const grouped: Record<string, Array<[string, string]>> = {};
|
|
62
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
63
|
+
const prefix = k.replace(/^--/, "").split("-").slice(0, 2).join("-");
|
|
64
|
+
if (!grouped[prefix]) grouped[prefix] = [];
|
|
65
|
+
grouped[prefix].push([k, v]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const [prefix, entries] of Object.entries(grouped)) {
|
|
69
|
+
themeBlock += `\n /* ${prefix} */`;
|
|
70
|
+
for (const [k, v] of entries) {
|
|
71
|
+
themeBlock += `\n ${k}: ${v};`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Gray scale compat (Tailwind v3 had gray-50..gray-950 by default)
|
|
77
|
+
themeBlock += `
|
|
78
|
+
|
|
79
|
+
/* Gray scale (Tailwind v3 default, required for bg-gray-*, text-gray-*, etc.) */
|
|
80
|
+
--color-gray-50: #f9fafb;
|
|
81
|
+
--color-gray-100: #f3f4f6;
|
|
82
|
+
--color-gray-200: #e5e7eb;
|
|
83
|
+
--color-gray-300: #d1d5db;
|
|
84
|
+
--color-gray-400: #9ca3af;
|
|
85
|
+
--color-gray-500: #6b7280;
|
|
86
|
+
--color-gray-600: #4b5563;
|
|
87
|
+
--color-gray-700: #374151;
|
|
88
|
+
--color-gray-800: #1f2937;
|
|
89
|
+
--color-gray-900: #111827;
|
|
90
|
+
--color-gray-950: #030712;
|
|
91
|
+
}`;
|
|
92
|
+
sections.push(themeBlock);
|
|
93
|
+
|
|
94
|
+
// ── DaisyUI v5 compat ─────────────────────────────────────────────
|
|
95
|
+
sections.push(`/* DaisyUI v5: flatten depth/noise to match v4 look */
|
|
96
|
+
:root {
|
|
97
|
+
--depth: 0;
|
|
98
|
+
--noise: 0;
|
|
99
|
+
}`);
|
|
100
|
+
|
|
101
|
+
// ── :root theme variables ─────────────────────────────────────────
|
|
102
|
+
if (Object.keys(vars).length > 0) {
|
|
103
|
+
let rootBlock = `:root {\n`;
|
|
104
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
105
|
+
rootBlock += ` ${k}: ${v};\n`;
|
|
106
|
+
}
|
|
107
|
+
if (fontFamily) {
|
|
108
|
+
const firstFont = fontFamily.split(",")[0].trim().replace(/['"]/g, "");
|
|
109
|
+
rootBlock += ` --font-family: ${firstFont}, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;\n`;
|
|
110
|
+
}
|
|
111
|
+
rootBlock += `}`;
|
|
112
|
+
sections.push(rootBlock);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── DaisyUI v5 carousel overflow fix ──────────────────────────────
|
|
116
|
+
sections.push(`/* DaisyUI v5 removed carousel overflow — re-add for horizontal scroll */
|
|
117
|
+
.carousel {
|
|
118
|
+
-webkit-overflow-scrolling: touch;
|
|
119
|
+
overflow-x: auto;
|
|
120
|
+
scroll-snap-type: x mandatory;
|
|
121
|
+
scroll-behavior: smooth;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.carousel > * {
|
|
125
|
+
scroll-snap-align: start;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ul.carousel,
|
|
129
|
+
ol.carousel {
|
|
130
|
+
list-style: none;
|
|
131
|
+
padding: 0;
|
|
132
|
+
}`);
|
|
133
|
+
|
|
134
|
+
// ── Container utility (v3 -> v4 migration) ────────────────────────
|
|
135
|
+
sections.push(`/* Container: replaces Tailwind v3 container plugin config */
|
|
136
|
+
@utility container {
|
|
137
|
+
margin-inline: auto;
|
|
138
|
+
padding-inline: 1rem;
|
|
139
|
+
width: 100%;
|
|
140
|
+
|
|
141
|
+
@media (width >= 640px) { max-width: 640px; }
|
|
142
|
+
@media (width >= 768px) { max-width: 768px; }
|
|
143
|
+
@media (width >= 1024px) { max-width: 1024px; }
|
|
144
|
+
@media (width >= 1280px) { max-width: 1280px; }
|
|
145
|
+
@media (width >= 1536px) { max-width: 1536px; }
|
|
146
|
+
}`);
|
|
147
|
+
|
|
148
|
+
// ── Deferred section visibility ───────────────────────────────────
|
|
149
|
+
sections.push(`/* Deferred section visibility — reduces layout shift while loading */
|
|
150
|
+
section[data-deferred="true"] {
|
|
151
|
+
content-visibility: auto;
|
|
152
|
+
contain-intrinsic-size: auto 300px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.deferred-section {
|
|
156
|
+
content-visibility: auto;
|
|
157
|
+
contain-intrinsic-size: auto 300px;
|
|
158
|
+
}`);
|
|
159
|
+
|
|
160
|
+
// ── View transitions ──────────────────────────────────────────────
|
|
161
|
+
sections.push(`@view-transition {
|
|
162
|
+
navigation: auto;
|
|
163
|
+
}`);
|
|
164
|
+
|
|
165
|
+
// ── Base layer resets ─────────────────────────────────────────────
|
|
166
|
+
sections.push(`@layer base {
|
|
167
|
+
*,
|
|
168
|
+
*::before,
|
|
169
|
+
*::after {
|
|
170
|
+
box-sizing: border-box;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
body {
|
|
174
|
+
-webkit-font-smoothing: antialiased;
|
|
175
|
+
-moz-osx-font-smoothing: grayscale;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
img,
|
|
179
|
+
picture,
|
|
180
|
+
video,
|
|
181
|
+
canvas,
|
|
182
|
+
svg {
|
|
183
|
+
display: block;
|
|
184
|
+
max-width: 100%;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Drawer / modal scroll lock */
|
|
188
|
+
body:has(dialog[open]),
|
|
189
|
+
body:has(.drawer-toggle:checked) {
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
}
|
|
192
|
+
}`);
|
|
193
|
+
|
|
194
|
+
// ── Scrollbar utility ─────────────────────────────────────────────
|
|
195
|
+
sections.push(`.scrollbar-none {
|
|
196
|
+
scrollbar-width: none;
|
|
197
|
+
-ms-overflow-style: none;
|
|
198
|
+
}
|
|
199
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
200
|
+
display: none;
|
|
201
|
+
}`);
|
|
202
|
+
|
|
203
|
+
return sections.join("\n\n") + "\n";
|
|
204
|
+
}
|