@decocms/start 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "0.31.1",
3
+ "version": "0.32.1",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
@@ -46,6 +46,8 @@
46
46
  "./scripts/generate-blocks": "./scripts/generate-blocks.ts",
47
47
  "./scripts/generate-schema": "./scripts/generate-schema.ts",
48
48
  "./scripts/generate-invoke": "./scripts/generate-invoke.ts",
49
+ "./scripts/migrate": "./scripts/migrate.ts",
50
+ "./scripts/tailwind-lint": "./scripts/tailwind-lint.ts",
49
51
  "./vite": "./src/vite/plugin.js"
50
52
  },
51
53
  "scripts": {
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Terminal color utilities — keeps output readable without adding dependencies.
3
+ */
4
+
5
+ const isColorSupported =
6
+ typeof process !== "undefined" &&
7
+ process.stdout?.isTTY &&
8
+ !process.env.NO_COLOR;
9
+
10
+ function wrap(code: number, resetCode: number) {
11
+ return (text: string) =>
12
+ isColorSupported ? `\x1b[${code}m${text}\x1b[${resetCode}m` : text;
13
+ }
14
+
15
+ export const bold = wrap(1, 22);
16
+ export const dim = wrap(2, 22);
17
+ export const red = wrap(31, 39);
18
+ export const green = wrap(32, 39);
19
+ export const yellow = wrap(33, 39);
20
+ export const blue = wrap(34, 39);
21
+ export const cyan = wrap(36, 39);
22
+ export const gray = wrap(90, 39);
23
+
24
+ export const icons = {
25
+ success: isColorSupported ? "\x1b[32m✓\x1b[0m" : "[OK]",
26
+ error: isColorSupported ? "\x1b[31m✗\x1b[0m" : "[FAIL]",
27
+ warning: isColorSupported ? "\x1b[33m⚠\x1b[0m" : "[WARN]",
28
+ info: isColorSupported ? "\x1b[34mℹ\x1b[0m" : "[INFO]",
29
+ arrow: isColorSupported ? "\x1b[36m→\x1b[0m" : "->",
30
+ bullet: isColorSupported ? "\x1b[90m•\x1b[0m" : "-",
31
+ };
32
+
33
+ export function banner(text: string) {
34
+ const line = "═".repeat(58);
35
+ console.log(`\n${cyan(`╔${line}╗`)}`);
36
+ console.log(`${cyan("║")} ${bold(text.padEnd(56))}${cyan("║")}`);
37
+ console.log(`${cyan(`╚${line}╝`)}`);
38
+ }
39
+
40
+ export function phase(name: string) {
41
+ console.log(`\n${bold(blue(`━━━ ${name} ━━━`))}\n`);
42
+ }
43
+
44
+ export function stat(label: string, value: string | number) {
45
+ console.log(` ${gray(label + ":")} ${bold(String(value))}`);
46
+ }
@@ -0,0 +1,402 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type {
4
+ DetectedPattern,
5
+ FileRecord,
6
+ MigrationContext,
7
+ Platform,
8
+ } from "./types.ts";
9
+ import { log, logPhase } from "./types.ts";
10
+
11
+ const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
12
+ ["preact-hooks", /from\s+["']preact\/hooks["']/],
13
+ ["preact-signals", /from\s+["']@preact\/signals/],
14
+ ["fresh-runtime", /from\s+["']\$fresh\/runtime/],
15
+ ["fresh-server", /from\s+["']\$fresh\/server/],
16
+ ["deco-hooks", /from\s+["']@deco\/deco\/hooks["']/],
17
+ ["deco-context", /Context\.active\(\)/],
18
+ ["deco-web", /from\s+["']@deco\/deco\/web["']/],
19
+ ["deco-blocks", /from\s+["']@deco\/deco\/blocks["']/],
20
+ ["apps-imports", /from\s+["']apps\//],
21
+ ["site-imports", /from\s+["']site\//],
22
+ ["class-attr", /<[a-zA-Z][^>]*\sclass\s*=/],
23
+ ["onInput-handler", /onInput\s*=/],
24
+ ["deno-lint-ignore", /deno-lint-ignore/],
25
+ ["npm-prefix", /from\s+["']npm:/],
26
+ ["component-children", /ComponentChildren/],
27
+ ["jsx-types", /JSX\.(?:SVG|HTML|Generic)/],
28
+ ["asset-function", /\basset\(/],
29
+ ["head-component", /<Head[\s>]/],
30
+ ["define-app", /defineApp\(/],
31
+ ["invoke-proxy", /proxy<Manifest/],
32
+ ];
33
+
34
+ /** Files/dirs that should be completely skipped during scanning */
35
+ const SKIP_DIRS = new Set([
36
+ "node_modules",
37
+ ".git",
38
+ ".github",
39
+ ".deco",
40
+ ".devcontainer",
41
+ ".vscode",
42
+ "_fresh",
43
+ "static",
44
+ ".context",
45
+ "scripts",
46
+ "src",
47
+ "public",
48
+ ".tanstack",
49
+ ]);
50
+
51
+ const SKIP_FILES = new Set([
52
+ "deno.lock",
53
+ ".gitignore",
54
+ "README.md",
55
+ "LICENSE",
56
+ "browserslist",
57
+ "bw_stats.json",
58
+ "package.json",
59
+ "package-lock.json",
60
+ ]);
61
+
62
+ /** Files that are generated and should be deleted */
63
+ const GENERATED_FILES = new Set([
64
+ "fresh.gen.ts",
65
+ "manifest.gen.ts",
66
+ "fresh.config.ts",
67
+ ]);
68
+
69
+ /** SDK files that have framework equivalents */
70
+ const SDK_DELETE = new Set([
71
+ "sdk/clx.ts",
72
+ "sdk/useId.ts",
73
+ "sdk/useOffer.ts",
74
+ "sdk/useVariantPossiblities.ts",
75
+ "sdk/usePlatform.tsx",
76
+ ]);
77
+
78
+ /** Loaders that depend on deleted admin tooling */
79
+ const LOADER_DELETE = new Set([
80
+ "loaders/availableIcons.ts",
81
+ "loaders/icons.ts",
82
+ ]);
83
+
84
+ /** Root config/infra files to delete */
85
+ const ROOT_DELETE = new Set([
86
+ "main.ts",
87
+ "dev.ts",
88
+ "deno.json",
89
+ "deno.lock",
90
+ "tailwind.css",
91
+ "tailwind.config.ts",
92
+ "runtime.ts",
93
+ "constants.ts",
94
+ "fresh.gen.ts",
95
+ "manifest.gen.ts",
96
+ "fresh.config.ts",
97
+ "browserslist",
98
+ "bw_stats.json",
99
+ ]);
100
+
101
+ /** Static files that are code/tooling, not assets — should be deleted */
102
+ const STATIC_DELETE = new Set([
103
+ "static/adminIcons.ts",
104
+ "static/generate-icons.ts",
105
+ "static/tailwind.css",
106
+ ]);
107
+
108
+ /**
109
+ * Scan file content for inline npm: imports and return { name: version } pairs.
110
+ * Matches patterns like: from "npm:fuse.js@7.0.0"
111
+ */
112
+ function extractInlineNpmDeps(content: string): Record<string, string> {
113
+ const deps: Record<string, string> = {};
114
+ const regex = /from\s+["']npm:(@?[^@"']+)(?:@([^"']+))?["']/g;
115
+ let match;
116
+ while ((match = regex.exec(content)) !== null) {
117
+ const name = match[1];
118
+ const version = match[2] || "*";
119
+ // Skip framework deps
120
+ if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
121
+ deps[name] = `^${version}`;
122
+ }
123
+ return deps;
124
+ }
125
+
126
+ function detectPatterns(content: string): DetectedPattern[] {
127
+ const patterns: DetectedPattern[] = [];
128
+ for (const [name, regex] of PATTERN_DETECTORS) {
129
+ if (regex.test(content)) {
130
+ patterns.push(name);
131
+ }
132
+ }
133
+ return patterns;
134
+ }
135
+
136
+ function isReExport(content: string): { is: boolean; target?: string } {
137
+ const match = content.match(
138
+ /^export\s+\{\s*default\s*\}\s+from\s+["']([^"']+)["']/m,
139
+ );
140
+ if (match) return { is: true, target: match[1] };
141
+ return { is: false };
142
+ }
143
+
144
+ function categorizeFile(
145
+ relPath: string,
146
+ ): FileRecord["category"] {
147
+ if (relPath.startsWith("sections/")) return "section";
148
+ if (relPath.startsWith("islands/")) return "island";
149
+ if (relPath.startsWith("components/")) return "component";
150
+ if (relPath.startsWith("sdk/")) return "sdk";
151
+ if (relPath.startsWith("loaders/")) return "loader";
152
+ if (relPath.startsWith("actions/")) return "action";
153
+ if (relPath.startsWith("routes/")) return "route";
154
+ if (relPath.startsWith("apps/")) return "app";
155
+ if (relPath.startsWith("static/")) return "static";
156
+ if (GENERATED_FILES.has(relPath)) return "generated";
157
+ if (
158
+ relPath === "deno.json" || relPath === "tsconfig.json" ||
159
+ relPath === "tailwind.config.ts"
160
+ ) {
161
+ return "config";
162
+ }
163
+ return "other";
164
+ }
165
+
166
+ function decideAction(
167
+ record: FileRecord,
168
+ ): { action: FileRecord["action"]; targetPath?: string; notes?: string } {
169
+ const { path: relPath, category, isReExport: isReExp } = record;
170
+
171
+ // Generated files → delete
172
+ if (category === "generated") {
173
+ return { action: "delete" };
174
+ }
175
+
176
+ // Root config/infra → delete (will be scaffolded)
177
+ if (ROOT_DELETE.has(relPath)) {
178
+ return { action: "delete", notes: "Replaced by scaffolded config" };
179
+ }
180
+
181
+ // Routes → delete (will be scaffolded)
182
+ if (category === "route") {
183
+ return { action: "delete", notes: "Routes are scaffolded fresh" };
184
+ }
185
+
186
+ // Apps deco/ dir → delete
187
+ if (relPath.startsWith("apps/deco/")) {
188
+ return { action: "delete", notes: "Deco apps not needed in TanStack" };
189
+ }
190
+
191
+ // apps/site.ts → delete (will be scaffolded)
192
+ if (relPath === "apps/site.ts") {
193
+ return { action: "delete", notes: "Rewritten from scratch" };
194
+ }
195
+
196
+ // Loaders that depend on deleted admin tooling
197
+ if (LOADER_DELETE.has(relPath)) {
198
+ return {
199
+ action: "delete",
200
+ notes: "Admin icon loader — depends on deleted static/adminIcons.ts",
201
+ };
202
+ }
203
+
204
+ // SDK files to delete
205
+ if (SDK_DELETE.has(relPath)) {
206
+ return {
207
+ action: "delete",
208
+ notes: "Use framework equivalent from @decocms/start or @decocms/apps",
209
+ };
210
+ }
211
+
212
+ // cart/ directory → delete
213
+ if (relPath.startsWith("sdk/cart/")) {
214
+ return { action: "delete", notes: "Use @decocms/apps cart hooks" };
215
+ }
216
+
217
+ // Islands — if the section is a re-export of this island, island becomes section
218
+ if (category === "island") {
219
+ const sectionPath = relPath.replace("islands/", "sections/");
220
+ return {
221
+ action: "transform",
222
+ targetPath: `src/${sectionPath}`,
223
+ notes: "Island merged into section",
224
+ };
225
+ }
226
+
227
+ // Sections that are re-exports of islands → delete (island takes their place)
228
+ if (category === "section" && isReExp) {
229
+ return { action: "delete", notes: "Re-export wrapper, island merged" };
230
+ }
231
+
232
+ // Session component → delete (analytics moves to __root.tsx)
233
+ if (
234
+ relPath === "components/Session.tsx" || relPath === "sections/Session.tsx"
235
+ ) {
236
+ return {
237
+ action: "delete",
238
+ notes: "Analytics SDK moved to __root.tsx scaffold",
239
+ };
240
+ }
241
+
242
+ // Static code/tooling files → delete
243
+ if (STATIC_DELETE.has(relPath)) {
244
+ return { action: "delete", notes: "Code/tooling file, not an asset" };
245
+ }
246
+
247
+ // Static files → move
248
+ if (category === "static") {
249
+ const publicPath = relPath.replace("static/", "public/");
250
+ return { action: "move", targetPath: publicPath };
251
+ }
252
+
253
+ // Everything else → transform into src/
254
+ return { action: "transform", targetPath: `src/${relPath}` };
255
+ }
256
+
257
+ function scanDir(
258
+ dir: string,
259
+ baseDir: string,
260
+ files: FileRecord[],
261
+ ) {
262
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
263
+
264
+ for (const entry of entries) {
265
+ const fullPath = path.join(dir, entry.name);
266
+ const relPath = path.relative(baseDir, fullPath);
267
+
268
+ if (entry.isDirectory()) {
269
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
270
+ scanDir(fullPath, baseDir, files);
271
+ continue;
272
+ }
273
+
274
+ // Skip dotfiles and known non-code files
275
+ if (SKIP_FILES.has(entry.name) || entry.name.startsWith(".")) continue;
276
+
277
+ // Only process .ts, .tsx, .css, .json files for transforms
278
+ const ext = path.extname(entry.name);
279
+ const isCode = [".ts", ".tsx", ".css", ".json"].includes(ext);
280
+
281
+ let content = "";
282
+ let patterns: DetectedPattern[] = [];
283
+ let reExport = { is: false, target: undefined as string | undefined };
284
+
285
+ if (isCode) {
286
+ content = fs.readFileSync(fullPath, "utf-8");
287
+ patterns = detectPatterns(content);
288
+ reExport = isReExport(content);
289
+ }
290
+
291
+ const record: FileRecord = {
292
+ path: relPath,
293
+ absPath: fullPath,
294
+ category: categorizeFile(relPath),
295
+ isReExport: reExport.is,
296
+ reExportTarget: reExport.target,
297
+ patterns,
298
+ action: "transform", // placeholder
299
+ };
300
+
301
+ const decision = decideAction(record);
302
+ record.action = decision.action;
303
+ record.targetPath = decision.targetPath;
304
+ record.notes = decision.notes;
305
+
306
+ files.push(record);
307
+ }
308
+ }
309
+
310
+ function extractGtmId(sourceDir: string): string | null {
311
+ const appPath = path.join(sourceDir, "routes", "_app.tsx");
312
+ if (!fs.existsSync(appPath)) return null;
313
+
314
+ const content = fs.readFileSync(appPath, "utf-8");
315
+ const match = content.match(/GTM-[A-Z0-9]+/);
316
+ return match ? match[0] : null;
317
+ }
318
+
319
+ function extractPlatform(sourceDir: string): Platform {
320
+ const sitePath = path.join(sourceDir, "apps", "site.ts");
321
+ if (!fs.existsSync(sitePath)) return "custom";
322
+
323
+ const content = fs.readFileSync(sitePath, "utf-8");
324
+
325
+ // Check for platform in Props or default
326
+ for (const p of ["vtex", "shopify", "wake", "vnda", "linx", "nuvemshop"] as const) {
327
+ if (content.includes(`"${p}"`) && content.includes("_platform")) {
328
+ // This is just detecting what's available, default is usually "custom"
329
+ }
330
+ }
331
+
332
+ return "custom";
333
+ }
334
+
335
+ function extractSiteName(sourceDir: string): string {
336
+ // Try to extract from .deco or directory name
337
+ const dirName = path.basename(path.resolve(sourceDir));
338
+
339
+ // Try deno.json
340
+ const denoPath = path.join(sourceDir, "deno.json");
341
+ if (fs.existsSync(denoPath)) {
342
+ const deno = JSON.parse(fs.readFileSync(denoPath, "utf-8"));
343
+ if (deno.name) return deno.name;
344
+ }
345
+
346
+ return dirName;
347
+ }
348
+
349
+ export function analyze(ctx: MigrationContext): void {
350
+ logPhase("Analyze");
351
+
352
+ // Parse deno.json for import map
353
+ const denoJsonPath = path.join(ctx.sourceDir, "deno.json");
354
+ if (fs.existsSync(denoJsonPath)) {
355
+ const denoJson = JSON.parse(fs.readFileSync(denoJsonPath, "utf-8"));
356
+ ctx.importMap = denoJson.imports || {};
357
+ log(
358
+ ctx,
359
+ `Found ${Object.keys(ctx.importMap).length} import map entries`,
360
+ );
361
+ }
362
+
363
+ // Extract metadata
364
+ ctx.siteName = extractSiteName(ctx.sourceDir);
365
+ ctx.platform = extractPlatform(ctx.sourceDir);
366
+ ctx.gtmId = extractGtmId(ctx.sourceDir);
367
+
368
+ console.log(` Site: ${ctx.siteName}`);
369
+ console.log(` Platform: ${ctx.platform}`);
370
+ console.log(` GTM ID: ${ctx.gtmId || "none"}`);
371
+
372
+ // Scan all files
373
+ scanDir(ctx.sourceDir, ctx.sourceDir, ctx.files);
374
+
375
+ // Summary
376
+ const byAction = { transform: 0, delete: 0, move: 0, scaffold: 0, "manual-review": 0 };
377
+ const byCategory: Record<string, number> = {};
378
+
379
+ for (const f of ctx.files) {
380
+ byAction[f.action]++;
381
+ byCategory[f.category] = (byCategory[f.category] || 0) + 1;
382
+ }
383
+
384
+ // Scan all source files for inline npm: imports
385
+ for (const f of ctx.files) {
386
+ if (f.action === "delete") continue;
387
+ const ext = path.extname(f.path);
388
+ if (![".ts", ".tsx"].includes(ext)) continue;
389
+ try {
390
+ const content = fs.readFileSync(f.absPath, "utf-8");
391
+ const deps = extractInlineNpmDeps(content);
392
+ Object.assign(ctx.discoveredNpmDeps, deps);
393
+ } catch {}
394
+ }
395
+ if (Object.keys(ctx.discoveredNpmDeps).length > 0) {
396
+ log(ctx, `Discovered npm deps from source: ${JSON.stringify(ctx.discoveredNpmDeps)}`);
397
+ }
398
+
399
+ console.log(`\n Files found: ${ctx.files.length}`);
400
+ console.log(` By category: ${JSON.stringify(byCategory)}`);
401
+ console.log(` By action: ${JSON.stringify(byAction)}`);
402
+ }
@@ -0,0 +1,212 @@
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
+
6
+ /** Directories to remove entirely after migration */
7
+ const DIRS_TO_DELETE = [
8
+ "islands",
9
+ "routes",
10
+ "apps/deco",
11
+ "sdk/cart",
12
+ ];
13
+
14
+ /** Individual root files to delete */
15
+ const ROOT_FILES_TO_DELETE = [
16
+ "main.ts",
17
+ "dev.ts",
18
+ "deno.json",
19
+ "deno.lock",
20
+ "tailwind.css",
21
+ "tailwind.config.ts",
22
+ "runtime.ts",
23
+ "constants.ts",
24
+ "fresh.gen.ts",
25
+ "manifest.gen.ts",
26
+ "fresh.config.ts",
27
+ "browserslist",
28
+ "bw_stats.json",
29
+ ];
30
+
31
+ /** SDK files that have framework equivalents */
32
+ const SDK_FILES_TO_DELETE = [
33
+ "sdk/clx.ts",
34
+ "sdk/useId.ts",
35
+ "sdk/useOffer.ts",
36
+ "sdk/useVariantPossiblities.ts",
37
+ "sdk/usePlatform.tsx",
38
+ ];
39
+
40
+ /** Section/component wrappers that are no longer needed */
41
+ const WRAPPER_FILES_TO_DELETE = [
42
+ "components/Session.tsx",
43
+ "sections/Session.tsx",
44
+ ];
45
+
46
+ /** Loaders that depend on deleted admin tooling */
47
+ const LOADER_FILES_TO_DELETE = [
48
+ "loaders/availableIcons.ts",
49
+ "loaders/icons.ts",
50
+ ];
51
+
52
+ function deleteFileIfExists(ctx: MigrationContext, relPath: string) {
53
+ const fullPath = path.join(ctx.sourceDir, relPath);
54
+ if (!fs.existsSync(fullPath)) return;
55
+
56
+ if (ctx.dryRun) {
57
+ log(ctx, `[DRY] Would delete: ${relPath}`);
58
+ ctx.deletedFiles.push(relPath);
59
+ return;
60
+ }
61
+
62
+ fs.unlinkSync(fullPath);
63
+ ctx.deletedFiles.push(relPath);
64
+ log(ctx, `Deleted: ${relPath}`);
65
+ }
66
+
67
+ function deleteDirIfExists(ctx: MigrationContext, relPath: string) {
68
+ const fullPath = path.join(ctx.sourceDir, relPath);
69
+ if (!fs.existsSync(fullPath)) return;
70
+
71
+ if (ctx.dryRun) {
72
+ log(ctx, `[DRY] Would delete dir: ${relPath}/`);
73
+ ctx.deletedFiles.push(`${relPath}/`);
74
+ return;
75
+ }
76
+
77
+ fs.rmSync(fullPath, { recursive: true, force: true });
78
+ ctx.deletedFiles.push(`${relPath}/`);
79
+ log(ctx, `Deleted dir: ${relPath}/`);
80
+ }
81
+
82
+ function moveStaticFiles(ctx: MigrationContext) {
83
+ const staticDir = path.join(ctx.sourceDir, "static");
84
+ if (!fs.existsSync(staticDir)) return;
85
+
86
+ const publicDir = path.join(ctx.sourceDir, "public");
87
+
88
+ function moveRecursive(dir: string) {
89
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
90
+ for (const entry of entries) {
91
+ const srcPath = path.join(dir, entry.name);
92
+ const relFromStatic = path.relative(staticDir, srcPath);
93
+ const destPath = path.join(publicDir, relFromStatic);
94
+
95
+ // Skip generated files
96
+ if (
97
+ entry.name === "tailwind.css" || entry.name === "adminIcons.ts" ||
98
+ entry.name === "generate-icons.ts"
99
+ ) {
100
+ continue;
101
+ }
102
+
103
+ if (entry.isDirectory()) {
104
+ moveRecursive(srcPath);
105
+ continue;
106
+ }
107
+
108
+ if (ctx.dryRun) {
109
+ log(ctx, `[DRY] Would move: static/${relFromStatic} → public/${relFromStatic}`);
110
+ ctx.movedFiles.push({
111
+ from: `static/${relFromStatic}`,
112
+ to: `public/${relFromStatic}`,
113
+ });
114
+ continue;
115
+ }
116
+
117
+ // Ensure dest dir exists
118
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
119
+ fs.copyFileSync(srcPath, destPath);
120
+ ctx.movedFiles.push({
121
+ from: `static/${relFromStatic}`,
122
+ to: `public/${relFromStatic}`,
123
+ });
124
+ log(ctx, `Moved: static/${relFromStatic} → public/${relFromStatic}`);
125
+ }
126
+ }
127
+
128
+ moveRecursive(staticDir);
129
+
130
+ // Now delete static/ dir
131
+ if (!ctx.dryRun) {
132
+ fs.rmSync(staticDir, { recursive: true, force: true });
133
+ log(ctx, "Deleted dir: static/");
134
+ }
135
+ }
136
+
137
+ function cleanupOldSourceDirs(ctx: MigrationContext) {
138
+ // After transforms, the original top-level dirs have been copied to src/.
139
+ // Delete the old top-level copies if they still exist and src/ has them.
140
+ const dirsToClean = [
141
+ "sections",
142
+ "components",
143
+ "sdk",
144
+ "loaders",
145
+ "actions",
146
+ "apps",
147
+ ];
148
+
149
+ for (const dir of dirsToClean) {
150
+ const oldDir = path.join(ctx.sourceDir, dir);
151
+ const newDir = path.join(ctx.sourceDir, "src", dir);
152
+ if (fs.existsSync(oldDir) && fs.existsSync(newDir)) {
153
+ if (ctx.dryRun) {
154
+ log(ctx, `[DRY] Would delete old dir: ${dir}/ (moved to src/${dir}/)`);
155
+ ctx.deletedFiles.push(`${dir}/`);
156
+ } else {
157
+ fs.rmSync(oldDir, { recursive: true, force: true });
158
+ ctx.deletedFiles.push(`${dir}/`);
159
+ log(ctx, `Deleted old dir: ${dir}/ (now at src/${dir}/)`);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ /** Delete sections that were re-export wrappers (their islands are now sections) */
166
+ function cleanupReExportSections(ctx: MigrationContext) {
167
+ const reExports = ctx.files.filter(
168
+ (f) => f.category === "section" && f.isReExport && f.action === "delete",
169
+ );
170
+ for (const f of reExports) {
171
+ // These were already not transformed, just make sure we note them
172
+ log(ctx, `Skipped re-export wrapper: ${f.path}`);
173
+ }
174
+ }
175
+
176
+ export function cleanup(ctx: MigrationContext): void {
177
+ logPhase("Cleanup");
178
+
179
+ // 1. Move static → public
180
+ console.log(" Moving static/ → public/...");
181
+ moveStaticFiles(ctx);
182
+
183
+ // 2. Delete specific files
184
+ console.log(" Deleting old files...");
185
+ for (const file of ROOT_FILES_TO_DELETE) {
186
+ deleteFileIfExists(ctx, file);
187
+ }
188
+ for (const file of SDK_FILES_TO_DELETE) {
189
+ deleteFileIfExists(ctx, file);
190
+ }
191
+ for (const file of WRAPPER_FILES_TO_DELETE) {
192
+ deleteFileIfExists(ctx, file);
193
+ }
194
+ for (const file of LOADER_FILES_TO_DELETE) {
195
+ deleteFileIfExists(ctx, file);
196
+ }
197
+
198
+ // 3. Delete directories
199
+ console.log(" Deleting old directories...");
200
+ for (const dir of DIRS_TO_DELETE) {
201
+ deleteDirIfExists(ctx, dir);
202
+ }
203
+
204
+ // 4. Clean up old source directories
205
+ console.log(" Cleaning up old source dirs...");
206
+ cleanupOldSourceDirs(ctx);
207
+ cleanupReExportSections(ctx);
208
+
209
+ console.log(
210
+ ` Deleted ${ctx.deletedFiles.length} files/dirs, moved ${ctx.movedFiles.length} files`,
211
+ );
212
+ }