@decocms/start 0.31.1 → 0.32.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.
@@ -0,0 +1,171 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { MigrationContext } from "./types.ts";
4
+ import { logPhase } from "./types.ts";
5
+
6
+ const FRAMEWORK_FINDINGS = [
7
+ "Session/analytics SDK is boilerplate duplicated across all sites — should be a single framework function",
8
+ "GTM event system (useGTMEvent, data-gtm-* listeners) is universal pattern — should be in @decocms/start",
9
+ "Route files (__root.tsx, index.tsx, $.tsx, deco/*) are near-identical across sites — should be generated by framework",
10
+ "server.ts, worker-entry.ts, router.tsx are pure boilerplate — should be a single createSite() call",
11
+ "setup.ts section registration via import.meta.glob is 100% boilerplate — framework should handle this",
12
+ "runtime.ts invoke proxy is identical across sites — already in @decocms/start but sites still have local copies",
13
+ "apps/site.ts is mostly empty after migration — platform config should be in a config file, not code",
14
+ ];
15
+
16
+ export function report(ctx: MigrationContext): void {
17
+ logPhase("Report");
18
+
19
+ ctx.frameworkFindings = FRAMEWORK_FINDINGS;
20
+
21
+ const lines: string[] = [];
22
+
23
+ lines.push("# Migration Report");
24
+ lines.push("");
25
+ lines.push(`**Site:** ${ctx.siteName}`);
26
+ lines.push(`**Platform:** ${ctx.platform}`);
27
+ lines.push(`**GTM ID:** ${ctx.gtmId || "none"}`);
28
+ lines.push(`**Date:** ${new Date().toISOString().split("T")[0]}`);
29
+ lines.push(`**Mode:** ${ctx.dryRun ? "DRY RUN" : "EXECUTED"}`);
30
+ lines.push("");
31
+
32
+ // Summary
33
+ lines.push("## Summary");
34
+ lines.push("");
35
+ lines.push(`| Metric | Count |`);
36
+ lines.push(`|--------|-------|`);
37
+ lines.push(`| Files analyzed | ${ctx.files.length} |`);
38
+ lines.push(`| Files scaffolded | ${ctx.scaffoldedFiles.length} |`);
39
+ lines.push(`| Files transformed | ${ctx.transformedFiles.length} |`);
40
+ lines.push(`| Files deleted | ${ctx.deletedFiles.length} |`);
41
+ lines.push(`| Files moved | ${ctx.movedFiles.length} |`);
42
+ lines.push(
43
+ `| Manual review items | ${ctx.manualReviewItems.length} |`,
44
+ );
45
+ lines.push("");
46
+
47
+ // Scaffolded files
48
+ lines.push("## Scaffolded Files (new)");
49
+ lines.push("");
50
+ for (const f of ctx.scaffoldedFiles) {
51
+ lines.push(`- \`${f}\``);
52
+ }
53
+ lines.push("");
54
+
55
+ // Transformed files
56
+ lines.push("## Transformed Files");
57
+ lines.push("");
58
+ for (const f of ctx.transformedFiles) {
59
+ lines.push(`- \`${f}\``);
60
+ }
61
+ lines.push("");
62
+
63
+ // Deleted files
64
+ lines.push("## Deleted Files");
65
+ lines.push("");
66
+ for (const f of ctx.deletedFiles) {
67
+ lines.push(`- \`${f}\``);
68
+ }
69
+ lines.push("");
70
+
71
+ // Moved files
72
+ if (ctx.movedFiles.length > 0) {
73
+ lines.push("## Moved Files");
74
+ lines.push("");
75
+ for (const { from, to } of ctx.movedFiles) {
76
+ lines.push(`- \`${from}\` → \`${to}\``);
77
+ }
78
+ lines.push("");
79
+ }
80
+
81
+ // Manual review
82
+ if (ctx.manualReviewItems.length > 0) {
83
+ lines.push("## Manual Review Required");
84
+ lines.push("");
85
+ for (const item of ctx.manualReviewItems) {
86
+ const icon = item.severity === "error"
87
+ ? "🔴"
88
+ : item.severity === "warning"
89
+ ? "🟡"
90
+ : "🔵";
91
+ lines.push(`${icon} **\`${item.file}\`**: ${item.reason}`);
92
+ }
93
+ lines.push("");
94
+ }
95
+
96
+ // Always-present manual review items
97
+ lines.push("## Always Check (site-specific)");
98
+ lines.push("");
99
+ lines.push(
100
+ "- [ ] `FormEmail.tsx` — `invoke.resend.actions.emails.send()` needs new server function pattern",
101
+ );
102
+ lines.push(
103
+ "- [ ] `Slider.tsx` — verify `scriptAsDataURI` pattern works with React",
104
+ );
105
+ lines.push(
106
+ "- [ ] `Theme.tsx` — verify `SiteTheme` mapping to `@decocms/start`",
107
+ );
108
+ lines.push("- [ ] DaisyUI v4 → v5 class name changes");
109
+ lines.push(
110
+ "- [ ] Tailwind v3 → v4: verify all utility classes still work",
111
+ );
112
+ lines.push(
113
+ "- [ ] Check `src/styles/app.css` theme colors match the original design",
114
+ );
115
+ lines.push("");
116
+
117
+ // Framework findings
118
+ lines.push("## Framework Findings");
119
+ lines.push("");
120
+ lines.push(
121
+ "> These are patterns found during migration that should eventually be handled by `@decocms/start` instead of being duplicated in every site.",
122
+ );
123
+ lines.push("");
124
+ for (const finding of ctx.frameworkFindings) {
125
+ lines.push(`- ${finding}`);
126
+ }
127
+ lines.push("");
128
+
129
+ // Next steps
130
+ lines.push("## Next Steps");
131
+ lines.push("");
132
+ lines.push("```bash");
133
+ lines.push("# 1. Install dependencies");
134
+ lines.push("npm install");
135
+ lines.push("");
136
+ lines.push("# 2. Generate CMS blocks and schema");
137
+ lines.push("npm run generate:blocks");
138
+ lines.push("npm run generate:schema");
139
+ lines.push("");
140
+ lines.push("# 3. Generate routes");
141
+ lines.push("npx tsr generate");
142
+ lines.push("");
143
+ lines.push("# 4. Type check");
144
+ lines.push("npx tsc --noEmit");
145
+ lines.push("");
146
+ lines.push("# 5. Find unused code");
147
+ lines.push("npm run knip");
148
+ lines.push("");
149
+ lines.push("# 6. Run dev server");
150
+ lines.push("npm run dev");
151
+ lines.push("```");
152
+ lines.push("");
153
+
154
+ const content = lines.join("\n");
155
+
156
+ if (ctx.dryRun) {
157
+ console.log("\n" + content);
158
+ } else {
159
+ const reportPath = path.join(ctx.sourceDir, "MIGRATION_REPORT.md");
160
+ fs.writeFileSync(reportPath, content, "utf-8");
161
+ console.log(` Report written to MIGRATION_REPORT.md`);
162
+ }
163
+
164
+ // Print summary to console
165
+ console.log(`\n === Migration ${ctx.dryRun ? "(DRY RUN)" : "COMPLETE"} ===`);
166
+ console.log(` Scaffolded: ${ctx.scaffoldedFiles.length}`);
167
+ console.log(` Transformed: ${ctx.transformedFiles.length}`);
168
+ console.log(` Deleted: ${ctx.deletedFiles.length}`);
169
+ console.log(` Moved: ${ctx.movedFiles.length}`);
170
+ console.log(` Manual review: ${ctx.manualReviewItems.length}`);
171
+ }
@@ -0,0 +1,133 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { MigrationContext } from "./types.ts";
4
+ import { log, logPhase } from "./types.ts";
5
+ import { generatePackageJson } from "./templates/package-json.ts";
6
+ import { generateTsconfig } from "./templates/tsconfig.ts";
7
+ import { generateViteConfig } from "./templates/vite-config.ts";
8
+ import { generateWrangler } from "./templates/wrangler.ts";
9
+ import { generateKnipConfig } from "./templates/knip-config.ts";
10
+ import { generateRoutes } from "./templates/routes.ts";
11
+ import { generateSetup } from "./templates/setup.ts";
12
+ import { generateServerEntry } from "./templates/server-entry.ts";
13
+
14
+ function writeFile(ctx: MigrationContext, relPath: string, content: string) {
15
+ const fullPath = path.join(ctx.sourceDir, relPath);
16
+
17
+ if (ctx.dryRun) {
18
+ log(ctx, `[DRY] Would create: ${relPath}`);
19
+ ctx.scaffoldedFiles.push(relPath);
20
+ return;
21
+ }
22
+
23
+ // Ensure directory exists
24
+ const dir = path.dirname(fullPath);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+
27
+ fs.writeFileSync(fullPath, content, "utf-8");
28
+ log(ctx, `Created: ${relPath}`);
29
+ ctx.scaffoldedFiles.push(relPath);
30
+ }
31
+
32
+ export function scaffold(ctx: MigrationContext): void {
33
+ logPhase("Scaffold");
34
+
35
+ // Root config files
36
+ writeFile(ctx, "package.json", generatePackageJson(ctx));
37
+ writeFile(ctx, "tsconfig.json", generateTsconfig());
38
+ writeFile(ctx, "vite.config.ts", generateViteConfig(ctx));
39
+ writeFile(ctx, "wrangler.jsonc", generateWrangler(ctx));
40
+ writeFile(ctx, "knip.config.ts", generateKnipConfig());
41
+ writeFile(ctx, ".prettierrc", JSON.stringify({
42
+ semi: true,
43
+ singleQuote: false,
44
+ trailingComma: "all" as const,
45
+ printWidth: 100,
46
+ tabWidth: 2,
47
+ }, null, 2) + "\n");
48
+
49
+ // Server entry files
50
+ const serverEntryFiles = generateServerEntry(ctx);
51
+ for (const [filePath, content] of Object.entries(serverEntryFiles)) {
52
+ writeFile(ctx, filePath, content);
53
+ }
54
+
55
+ // Route files
56
+ const routeFiles = generateRoutes(ctx);
57
+ for (const [filePath, content] of Object.entries(routeFiles)) {
58
+ writeFile(ctx, filePath, content);
59
+ }
60
+
61
+ // Setup
62
+ writeFile(ctx, "src/setup.ts", generateSetup(ctx));
63
+
64
+ // Styles
65
+ writeFile(ctx, "src/styles/app.css", generateAppCss(ctx));
66
+
67
+ // Apps
68
+ writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx));
69
+
70
+ // Create public/ directory
71
+ if (!ctx.dryRun) {
72
+ fs.mkdirSync(path.join(ctx.sourceDir, "public"), { recursive: true });
73
+ }
74
+
75
+ console.log(` Scaffolded ${ctx.scaffoldedFiles.length} files`);
76
+ }
77
+
78
+ function generateAppCss(_ctx: MigrationContext): string {
79
+ return `@import "tailwindcss";
80
+ @plugin "daisyui";
81
+ @plugin "daisyui/theme" {
82
+ name: "light";
83
+ default: true;
84
+ color-scheme: light;
85
+
86
+ /* TODO: Extract theme colors from the old Theme section's CMS config */
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;
98
+ }
99
+
100
+ @theme {
101
+ --color-*: initial;
102
+ --color-white: #fff;
103
+ --color-black: #000;
104
+ --color-transparent: transparent;
105
+ --color-current: currentColor;
106
+ --color-inherit: inherit;
107
+ }
108
+
109
+ /* View transitions */
110
+ @view-transition {
111
+ navigation: auto;
112
+ }
113
+ `;
114
+ }
115
+
116
+ function generateSiteApp(ctx: MigrationContext): string {
117
+ return `export type Platform =
118
+ | "vtex"
119
+ | "vnda"
120
+ | "shopify"
121
+ | "wake"
122
+ | "linx"
123
+ | "nuvemshop"
124
+ | "custom";
125
+
126
+ export const _platform: Platform = "${ctx.platform}";
127
+
128
+ export type AppContext = {
129
+ device: "mobile" | "desktop" | "tablet";
130
+ platform: Platform;
131
+ };
132
+ `;
133
+ }
@@ -0,0 +1,102 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { MigrationContext, TransformResult } from "./types.ts";
4
+ import { log, logPhase } from "./types.ts";
5
+ import { transformImports } from "./transforms/imports.ts";
6
+ import { transformJsx } from "./transforms/jsx.ts";
7
+ import { transformFreshApis } from "./transforms/fresh-apis.ts";
8
+ import { transformDenoIsms } from "./transforms/deno-isms.ts";
9
+ import { transformTailwind } from "./transforms/tailwind.ts";
10
+
11
+ /**
12
+ * Apply all transforms to a file's content in the correct order.
13
+ */
14
+ function applyTransforms(content: string, filePath: string): TransformResult {
15
+ const allNotes: string[] = [];
16
+ let currentContent = content;
17
+ let anyChanged = false;
18
+
19
+ // Only transform code files
20
+ const ext = path.extname(filePath);
21
+ if (![".ts", ".tsx"].includes(ext)) {
22
+ return { content, changed: false, notes: [] };
23
+ }
24
+
25
+ // Pipeline: imports → jsx → fresh-apis → deno-isms → tailwind
26
+ const pipeline = [
27
+ { name: "imports", fn: transformImports },
28
+ { name: "jsx", fn: transformJsx },
29
+ { name: "fresh-apis", fn: transformFreshApis },
30
+ { name: "deno-isms", fn: transformDenoIsms },
31
+ { name: "tailwind", fn: transformTailwind },
32
+ ];
33
+
34
+ for (const step of pipeline) {
35
+ const result = step.fn(currentContent);
36
+ if (result.changed) {
37
+ anyChanged = true;
38
+ currentContent = result.content;
39
+ allNotes.push(...result.notes.map((n) => `[${step.name}] ${n}`));
40
+ }
41
+ }
42
+
43
+ return { content: currentContent, changed: anyChanged, notes: allNotes };
44
+ }
45
+
46
+ export function transform(ctx: MigrationContext): void {
47
+ logPhase("Transform");
48
+
49
+ const toTransform = ctx.files.filter((f) => f.action === "transform");
50
+ console.log(` Files to transform: ${toTransform.length}`);
51
+
52
+ for (const record of toTransform) {
53
+ const { absPath, targetPath } = record;
54
+ if (!targetPath) continue;
55
+
56
+ // Read source
57
+ const content = fs.readFileSync(absPath, "utf-8");
58
+
59
+ // Apply transforms
60
+ const result = applyTransforms(content, absPath);
61
+
62
+ // Add manual review items
63
+ for (const note of result.notes) {
64
+ if (note.startsWith("[") && note.includes("MANUAL:")) {
65
+ ctx.manualReviewItems.push({
66
+ file: targetPath,
67
+ reason: note,
68
+ severity: "warning",
69
+ });
70
+ }
71
+ }
72
+
73
+ if (ctx.dryRun) {
74
+ if (result.changed) {
75
+ log(ctx, `[DRY] Would transform: ${record.path} → ${targetPath}`);
76
+ for (const note of result.notes) {
77
+ log(ctx, ` ${note}`);
78
+ }
79
+ }
80
+ ctx.transformedFiles.push(targetPath);
81
+ continue;
82
+ }
83
+
84
+ // Write to target path
85
+ const fullTargetPath = path.join(ctx.sourceDir, targetPath);
86
+ const dir = path.dirname(fullTargetPath);
87
+ fs.mkdirSync(dir, { recursive: true });
88
+ fs.writeFileSync(fullTargetPath, result.content, "utf-8");
89
+
90
+ ctx.transformedFiles.push(targetPath);
91
+ if (result.changed) {
92
+ log(
93
+ ctx,
94
+ `Transformed: ${record.path} → ${targetPath} (${result.notes.length} changes)`,
95
+ );
96
+ } else {
97
+ log(ctx, `Copied: ${record.path} → ${targetPath}`);
98
+ }
99
+ }
100
+
101
+ console.log(` Transformed ${ctx.transformedFiles.length} files`);
102
+ }
@@ -0,0 +1,248 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { MigrationContext } from "./types.ts";
4
+ import { logPhase } from "./types.ts";
5
+
6
+ interface Check {
7
+ name: string;
8
+ fn: (ctx: MigrationContext) => boolean;
9
+ severity: "error" | "warning";
10
+ }
11
+
12
+ const REQUIRED_FILES = [
13
+ "package.json",
14
+ "tsconfig.json",
15
+ "vite.config.ts",
16
+ "wrangler.jsonc",
17
+ "knip.config.ts",
18
+ ".prettierrc",
19
+ "src/server.ts",
20
+ "src/worker-entry.ts",
21
+ "src/router.tsx",
22
+ "src/runtime.ts",
23
+ "src/context.ts",
24
+ "src/setup.ts",
25
+ "src/styles/app.css",
26
+ "src/apps/site.ts",
27
+ "src/routes/__root.tsx",
28
+ "src/routes/index.tsx",
29
+ "src/routes/$.tsx",
30
+ "src/routes/deco/meta.ts",
31
+ "src/routes/deco/invoke.$.ts",
32
+ "src/routes/deco/render.ts",
33
+ ];
34
+
35
+ const MUST_NOT_EXIST = [
36
+ "deno.json",
37
+ "fresh.gen.ts",
38
+ "manifest.gen.ts",
39
+ "dev.ts",
40
+ "main.ts",
41
+ "islands/BlogFeed.tsx",
42
+ "routes/_app.tsx",
43
+ ];
44
+
45
+ const checks: Check[] = [
46
+ {
47
+ name: "All scaffolded files exist",
48
+ severity: "error",
49
+ fn: (ctx) => {
50
+ const missing = REQUIRED_FILES.filter(
51
+ (f) => !fs.existsSync(path.join(ctx.sourceDir, f)),
52
+ );
53
+ if (missing.length > 0) {
54
+ console.log(` Missing: ${missing.join(", ")}`);
55
+ return false;
56
+ }
57
+ return true;
58
+ },
59
+ },
60
+ {
61
+ name: "Old artifacts removed",
62
+ severity: "error",
63
+ fn: (ctx) => {
64
+ const remaining = MUST_NOT_EXIST.filter(
65
+ (f) => fs.existsSync(path.join(ctx.sourceDir, f)),
66
+ );
67
+ if (remaining.length > 0) {
68
+ console.log(` Still exists: ${remaining.join(", ")}`);
69
+ return false;
70
+ }
71
+ return true;
72
+ },
73
+ },
74
+ {
75
+ name: "No preact imports in src/",
76
+ severity: "error",
77
+ fn: (ctx) => {
78
+ const srcDir = path.join(ctx.sourceDir, "src");
79
+ if (!fs.existsSync(srcDir)) return true;
80
+ const bad = findFilesWithPattern(srcDir, /from\s+["']preact/);
81
+ if (bad.length > 0) {
82
+ console.log(` Still has preact imports: ${bad.join(", ")}`);
83
+ return false;
84
+ }
85
+ return true;
86
+ },
87
+ },
88
+ {
89
+ name: "No $fresh imports in src/",
90
+ severity: "error",
91
+ fn: (ctx) => {
92
+ const srcDir = path.join(ctx.sourceDir, "src");
93
+ if (!fs.existsSync(srcDir)) return true;
94
+ const bad = findFilesWithPattern(srcDir, /from\s+["']\$fresh/);
95
+ if (bad.length > 0) {
96
+ console.log(` Still has $fresh imports: ${bad.join(", ")}`);
97
+ return false;
98
+ }
99
+ return true;
100
+ },
101
+ },
102
+ {
103
+ name: "No deno-lint-ignore in src/",
104
+ severity: "warning",
105
+ fn: (ctx) => {
106
+ const srcDir = path.join(ctx.sourceDir, "src");
107
+ if (!fs.existsSync(srcDir)) return true;
108
+ const bad = findFilesWithPattern(srcDir, /deno-lint-ignore/);
109
+ if (bad.length > 0) {
110
+ console.log(` Still has deno-lint-ignore: ${bad.join(", ")}`);
111
+ return false;
112
+ }
113
+ return true;
114
+ },
115
+ },
116
+ {
117
+ name: 'No class= in JSX (should be className=)',
118
+ severity: "warning",
119
+ fn: (ctx) => {
120
+ const srcDir = path.join(ctx.sourceDir, "src");
121
+ if (!fs.existsSync(srcDir)) return true;
122
+ const bad = findFilesWithPattern(srcDir, /<[a-zA-Z][^>]*\sclass\s*=/);
123
+ if (bad.length > 0) {
124
+ console.log(` Still has class= in JSX: ${bad.slice(0, 5).join(", ")}${bad.length > 5 ? ` (+${bad.length - 5} more)` : ""}`);
125
+ return false;
126
+ }
127
+ return true;
128
+ },
129
+ },
130
+ {
131
+ name: "public/ has static assets",
132
+ severity: "warning",
133
+ fn: (ctx) => {
134
+ const publicDir = path.join(ctx.sourceDir, "public");
135
+ if (!fs.existsSync(publicDir)) {
136
+ console.log(" public/ directory missing");
137
+ return false;
138
+ }
139
+ const hasSprites = fs.existsSync(
140
+ path.join(publicDir, "sprites.svg"),
141
+ );
142
+ const hasFavicon = fs.existsSync(
143
+ path.join(publicDir, "favicon.ico"),
144
+ );
145
+ if (!hasSprites) console.log(" Missing: public/sprites.svg");
146
+ if (!hasFavicon) console.log(" Missing: public/favicon.ico");
147
+ return hasSprites && hasFavicon;
148
+ },
149
+ },
150
+ {
151
+ name: "package.json has correct dependencies",
152
+ severity: "error",
153
+ fn: (ctx) => {
154
+ const pkgPath = path.join(ctx.sourceDir, "package.json");
155
+ if (!fs.existsSync(pkgPath)) return false;
156
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
157
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
158
+ const required = [
159
+ "@decocms/start",
160
+ "@decocms/apps",
161
+ "react",
162
+ "react-dom",
163
+ "@tanstack/react-start",
164
+ "vite",
165
+ "knip",
166
+ ];
167
+ const missing = required.filter((d) => !deps[d]);
168
+ if (missing.length > 0) {
169
+ console.log(` Missing deps: ${missing.join(", ")}`);
170
+ return false;
171
+ }
172
+ return true;
173
+ },
174
+ },
175
+ {
176
+ name: "No site/ imports (should be ~/)",
177
+ severity: "warning",
178
+ fn: (ctx) => {
179
+ const srcDir = path.join(ctx.sourceDir, "src");
180
+ if (!fs.existsSync(srcDir)) return true;
181
+ const bad = findFilesWithPattern(srcDir, /from\s+["']site\//);
182
+ if (bad.length > 0) {
183
+ console.log(` Still has site/ imports: ${bad.join(", ")}`);
184
+ return false;
185
+ }
186
+ return true;
187
+ },
188
+ },
189
+ ];
190
+
191
+ function findFilesWithPattern(
192
+ dir: string,
193
+ pattern: RegExp,
194
+ results: string[] = [],
195
+ ): string[] {
196
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
197
+ for (const entry of entries) {
198
+ const fullPath = path.join(dir, entry.name);
199
+ if (entry.isDirectory()) {
200
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
201
+ findFilesWithPattern(fullPath, pattern, results);
202
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
203
+ const content = fs.readFileSync(fullPath, "utf-8");
204
+ if (pattern.test(content)) {
205
+ results.push(path.relative(dir, fullPath));
206
+ }
207
+ }
208
+ }
209
+ return results;
210
+ }
211
+
212
+ export function verify(ctx: MigrationContext): boolean {
213
+ logPhase("Verify (Smoke Test)");
214
+
215
+ if (ctx.dryRun) {
216
+ console.log(" Skipping verify in dry-run mode\n");
217
+ return true;
218
+ }
219
+
220
+ let errors = 0;
221
+ let warnings = 0;
222
+
223
+ for (const check of checks) {
224
+ const pass = check.fn(ctx);
225
+ const icon = pass ? "\x1b[32m✓\x1b[0m" : check.severity === "error" ? "\x1b[31m✗\x1b[0m" : "\x1b[33m⚠\x1b[0m";
226
+ console.log(` ${icon} ${check.name}`);
227
+ if (!pass) {
228
+ if (check.severity === "error") errors++;
229
+ else warnings++;
230
+ }
231
+ }
232
+
233
+ console.log(
234
+ `\n Result: ${checks.length - errors - warnings} passed, ${errors} errors, ${warnings} warnings`,
235
+ );
236
+
237
+ if (errors > 0) {
238
+ console.log(" \x1b[31mVerification FAILED — migration has issues that must be fixed\x1b[0m");
239
+ return false;
240
+ }
241
+ if (warnings > 0) {
242
+ console.log(" \x1b[33mVerification passed with warnings\x1b[0m");
243
+ } else {
244
+ console.log(" \x1b[32mVerification PASSED\x1b[0m");
245
+ }
246
+
247
+ return true;
248
+ }
@@ -0,0 +1,27 @@
1
+ export function generateKnipConfig(): string {
2
+ return `import type { KnipConfig } from "knip";
3
+
4
+ const config: KnipConfig = {
5
+ entry: [
6
+ "src/routes/**/*.{ts,tsx}",
7
+ "src/setup.ts",
8
+ "src/runtime.ts",
9
+ "src/sections/**/*.{ts,tsx}",
10
+ "vite.config.ts",
11
+ ],
12
+ project: ["src/**/*.{ts,tsx}"],
13
+ ignore: [
14
+ "src/server/invoke.gen.ts",
15
+ "src/server/cms/blocks.gen.ts",
16
+ "src/routeTree.gen.ts",
17
+ ],
18
+ ignoreDependencies: [
19
+ "babel-plugin-react-compiler",
20
+ "@vitejs/plugin-react",
21
+ "wrangler",
22
+ ],
23
+ };
24
+
25
+ export default config;
26
+ `;
27
+ }