@decocms/blocks-cli 7.0.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/package.json +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,782 @@
|
|
|
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";
|
|
9
|
+
import { log, logPhase } from "./types";
|
|
10
|
+
import { extractSectionMetadata } from "./analyzers/section-metadata";
|
|
11
|
+
import { classifyIslands } from "./analyzers/island-classifier";
|
|
12
|
+
import { inventoryLoaders } from "./analyzers/loader-inventory";
|
|
13
|
+
|
|
14
|
+
const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
15
|
+
["preact-hooks", /from\s+["']preact\/hooks["']/],
|
|
16
|
+
["preact-signals", /from\s+["']@preact\/signals/],
|
|
17
|
+
["fresh-runtime", /from\s+["']\$fresh\/runtime/],
|
|
18
|
+
["fresh-server", /from\s+["']\$fresh\/server/],
|
|
19
|
+
["deco-hooks", /from\s+["']@deco\/deco\/hooks["']/],
|
|
20
|
+
["deco-context", /Context\.active\(\)/],
|
|
21
|
+
["deco-web", /from\s+["']@deco\/deco\/web["']/],
|
|
22
|
+
["deco-blocks", /from\s+["']@deco\/deco\/blocks["']/],
|
|
23
|
+
["apps-imports", /from\s+["']apps\//],
|
|
24
|
+
["site-imports", /from\s+["']site\//],
|
|
25
|
+
["class-attr", /<[a-zA-Z][^>]*\sclass\s*=/],
|
|
26
|
+
["onInput-handler", /onInput\s*=/],
|
|
27
|
+
["deno-lint-ignore", /deno-lint-ignore/],
|
|
28
|
+
["npm-prefix", /from\s+["']npm:/],
|
|
29
|
+
["component-children", /ComponentChildren/],
|
|
30
|
+
["jsx-types", /JSX\.(?:SVG|HTML|Generic)/],
|
|
31
|
+
["asset-function", /\basset\(/],
|
|
32
|
+
["head-component", /<Head[\s>]/],
|
|
33
|
+
["define-app", /defineApp\(/],
|
|
34
|
+
["invoke-proxy", /proxy<Manifest/],
|
|
35
|
+
// Bagaggio-style HTMX dynamic-section loader. Both the source file and
|
|
36
|
+
// every call site need manual conversion to React state / createServerFn.
|
|
37
|
+
["sections-component-loader", /sections\/Component\.tsx?$/m],
|
|
38
|
+
["use-component", /import\s*\{[^}]*\buseComponent\b[^}]*\}\s*from\s*["'][^"']*sections\/Component(?:\.tsx?)?["']/],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** Files/dirs that should be completely skipped during scanning */
|
|
42
|
+
const SKIP_DIRS = new Set([
|
|
43
|
+
"node_modules",
|
|
44
|
+
".git",
|
|
45
|
+
".github",
|
|
46
|
+
".deco",
|
|
47
|
+
".devcontainer",
|
|
48
|
+
".vscode",
|
|
49
|
+
".claude",
|
|
50
|
+
".cursor",
|
|
51
|
+
"_fresh",
|
|
52
|
+
"static",
|
|
53
|
+
".context",
|
|
54
|
+
"scripts",
|
|
55
|
+
"src",
|
|
56
|
+
"public",
|
|
57
|
+
".tanstack",
|
|
58
|
+
"tests",
|
|
59
|
+
"bin",
|
|
60
|
+
"fonts",
|
|
61
|
+
".pilot",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const SKIP_FILES = new Set([
|
|
65
|
+
"deno.lock",
|
|
66
|
+
".gitignore",
|
|
67
|
+
"README.md",
|
|
68
|
+
"AGENTS.md",
|
|
69
|
+
"LICENSE",
|
|
70
|
+
"browserslist",
|
|
71
|
+
"bw_stats.json",
|
|
72
|
+
"biome.json",
|
|
73
|
+
"package.json",
|
|
74
|
+
"package-lock.json",
|
|
75
|
+
"yarn.lock",
|
|
76
|
+
"bun.lock",
|
|
77
|
+
"bun.lockb",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
/** Files that are generated and should be deleted */
|
|
81
|
+
const GENERATED_FILES = new Set([
|
|
82
|
+
"fresh.gen.ts",
|
|
83
|
+
"manifest.gen.ts",
|
|
84
|
+
"fresh.config.ts",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/** SDK files that have framework equivalents or are scaffolded fresh */
|
|
88
|
+
const SDK_DELETE = new Set([
|
|
89
|
+
"sdk/clx.ts",
|
|
90
|
+
"sdk/useId.ts",
|
|
91
|
+
// sdk/useOffer.ts — kept: sites often customize offer logic
|
|
92
|
+
// sdk/useVariantPossiblities.ts — kept: sites often customize variant logic
|
|
93
|
+
"sdk/usePlatform.tsx",
|
|
94
|
+
"sdk/signal.ts",
|
|
95
|
+
"sdk/format.ts",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
/** Component files that are scaffolded fresh (old versions must not overwrite) */
|
|
99
|
+
const COMPONENT_DELETE = new Set([
|
|
100
|
+
"components/ui/Image.tsx",
|
|
101
|
+
"components/ui/Picture.tsx",
|
|
102
|
+
"components/ui/Video.tsx",
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
/** Loaders that depend on deleted admin tooling */
|
|
106
|
+
const LOADER_DELETE = new Set([
|
|
107
|
+
"loaders/availableIcons.ts",
|
|
108
|
+
"loaders/icons.ts",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
/** Root config/infra files to delete */
|
|
112
|
+
const ROOT_DELETE = new Set([
|
|
113
|
+
"main.ts",
|
|
114
|
+
"dev.ts",
|
|
115
|
+
"deno.json",
|
|
116
|
+
"deno.lock",
|
|
117
|
+
"tailwind.css",
|
|
118
|
+
"tailwind.config.ts",
|
|
119
|
+
"runtime.ts",
|
|
120
|
+
"constants.ts",
|
|
121
|
+
"fresh.gen.ts",
|
|
122
|
+
"manifest.gen.ts",
|
|
123
|
+
"fresh.config.ts",
|
|
124
|
+
"browserslist",
|
|
125
|
+
"bw_stats.json",
|
|
126
|
+
"islands.ts",
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
/** Static files that are code/tooling, not assets — should be deleted */
|
|
130
|
+
const STATIC_DELETE = new Set([
|
|
131
|
+
"static/adminIcons.ts",
|
|
132
|
+
"static/generate-icons.ts",
|
|
133
|
+
"static/tailwind.css",
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Scan file content for inline npm: imports and return { name: version } pairs.
|
|
138
|
+
* Matches patterns like: from "npm:fuse.js@7.0.0"
|
|
139
|
+
*/
|
|
140
|
+
function extractInlineNpmDeps(content: string): Record<string, string> {
|
|
141
|
+
const deps: Record<string, string> = {};
|
|
142
|
+
const regex = /from\s+["']npm:(@?[^@"']+)(?:@([^"']+))?["']/g;
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = regex.exec(content)) !== null) {
|
|
145
|
+
const name = match[1];
|
|
146
|
+
const version = match[2] || "*";
|
|
147
|
+
if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
|
|
148
|
+
deps[name] = `^${version}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Detect well-known third-party packages imported without npm: prefix
|
|
152
|
+
const KNOWN_THIRD_PARTY: Record<string, string> = {
|
|
153
|
+
"@sentry/react": "^10.43.0",
|
|
154
|
+
"dompurify": "^3.3.3",
|
|
155
|
+
"fuse.js": "^7.0.0",
|
|
156
|
+
"swiper": "^11.2.6",
|
|
157
|
+
"lottie-web": "^5.12.2",
|
|
158
|
+
"class-variance-authority": "^0.7.1",
|
|
159
|
+
"clsx": "^2.1.1",
|
|
160
|
+
};
|
|
161
|
+
for (const [pkg, version] of Object.entries(KNOWN_THIRD_PARTY)) {
|
|
162
|
+
const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
163
|
+
if (new RegExp(`from\\s+["']${escaped}`, "m").test(content)) {
|
|
164
|
+
deps[pkg] = version;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return deps;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function detectPatterns(content: string): DetectedPattern[] {
|
|
172
|
+
const patterns: DetectedPattern[] = [];
|
|
173
|
+
for (const [name, regex] of PATTERN_DETECTORS) {
|
|
174
|
+
if (regex.test(content)) {
|
|
175
|
+
patterns.push(name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return patterns;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isReExport(content: string): { is: boolean; target?: string } {
|
|
182
|
+
const match = content.match(
|
|
183
|
+
/^export\s+\{\s*default\s*\}\s+from\s+["']([^"']+)["']/m,
|
|
184
|
+
);
|
|
185
|
+
if (match) return { is: true, target: match[1] };
|
|
186
|
+
return { is: false };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function categorizeFile(
|
|
190
|
+
relPath: string,
|
|
191
|
+
): FileRecord["category"] {
|
|
192
|
+
if (relPath.startsWith("sections/")) return "section";
|
|
193
|
+
if (relPath.startsWith("islands/")) return "island";
|
|
194
|
+
if (relPath.startsWith("components/")) return "component";
|
|
195
|
+
if (relPath.startsWith("sdk/")) return "sdk";
|
|
196
|
+
if (relPath.startsWith("loaders/")) return "loader";
|
|
197
|
+
if (relPath.startsWith("actions/")) return "action";
|
|
198
|
+
if (relPath.startsWith("routes/")) return "route";
|
|
199
|
+
if (relPath.startsWith("apps/")) return "app";
|
|
200
|
+
if (relPath.startsWith("static/") || relPath.startsWith("static-")) return "static";
|
|
201
|
+
if (GENERATED_FILES.has(relPath)) return "generated";
|
|
202
|
+
if (
|
|
203
|
+
relPath === "deno.json" || relPath === "tsconfig.json" ||
|
|
204
|
+
relPath === "tailwind.config.ts"
|
|
205
|
+
) {
|
|
206
|
+
return "config";
|
|
207
|
+
}
|
|
208
|
+
return "other";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function decideAction(
|
|
212
|
+
record: FileRecord,
|
|
213
|
+
): { action: FileRecord["action"]; targetPath?: string; notes?: string } {
|
|
214
|
+
const { path: relPath, category, isReExport: isReExp } = record;
|
|
215
|
+
|
|
216
|
+
// Generated files → delete
|
|
217
|
+
if (category === "generated") {
|
|
218
|
+
return { action: "delete" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Root config/infra → delete (will be scaffolded)
|
|
222
|
+
if (ROOT_DELETE.has(relPath)) {
|
|
223
|
+
return { action: "delete", notes: "Replaced by scaffolded config" };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Routes → delete (will be scaffolded)
|
|
227
|
+
if (category === "route") {
|
|
228
|
+
return { action: "delete", notes: "Routes are scaffolded fresh" };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Apps deco/ dir → delete
|
|
232
|
+
if (relPath.startsWith("apps/deco/")) {
|
|
233
|
+
return { action: "delete", notes: "Deco apps not needed in TanStack" };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// apps/site.ts → delete (will be scaffolded)
|
|
237
|
+
if (relPath === "apps/site.ts") {
|
|
238
|
+
return { action: "delete", notes: "Rewritten from scratch" };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// All remaining apps/ files → delete (platform config is in setup.ts now)
|
|
242
|
+
if (relPath.startsWith("apps/")) {
|
|
243
|
+
return { action: "delete", notes: "Old app config — platform setup is in setup.ts" };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Loaders that depend on deleted admin tooling
|
|
247
|
+
if (LOADER_DELETE.has(relPath)) {
|
|
248
|
+
return {
|
|
249
|
+
action: "delete",
|
|
250
|
+
notes: "Admin icon loader — depends on deleted static/adminIcons.ts",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// SDK files to delete (replaced by scaffolded or framework equivalents)
|
|
255
|
+
if (SDK_DELETE.has(relPath)) {
|
|
256
|
+
return {
|
|
257
|
+
action: "delete",
|
|
258
|
+
notes: "Use framework equivalent from @decocms/start or @decocms/apps",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Component files replaced by scaffolded versions
|
|
263
|
+
if (COMPONENT_DELETE.has(relPath)) {
|
|
264
|
+
return {
|
|
265
|
+
action: "delete",
|
|
266
|
+
notes: "Scaffolded fresh from @decocms/apps re-exports",
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// cart/ directory → delete
|
|
271
|
+
if (relPath.startsWith("sdk/cart/")) {
|
|
272
|
+
return { action: "delete", notes: "Use @decocms/apps cart hooks" };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Non-platform cleanup is done in analyze() post-processing (needs ctx.platform)
|
|
276
|
+
|
|
277
|
+
// Islands — classify and route to appropriate target
|
|
278
|
+
if (category === "island") {
|
|
279
|
+
const classification = (record as any).__islandClassification;
|
|
280
|
+
if (classification?.type === "wrapper") {
|
|
281
|
+
return { action: "delete", notes: "Island wrapper — imports repointed to component" };
|
|
282
|
+
}
|
|
283
|
+
// Standalone islands go to components/, not sections/
|
|
284
|
+
const componentPath = relPath.replace("islands/", "components/");
|
|
285
|
+
return {
|
|
286
|
+
action: "transform",
|
|
287
|
+
targetPath: `src/${componentPath}`,
|
|
288
|
+
notes: "Standalone island moved to components",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Sections that re-export from islands/ → TRANSFORM (rewrite to ~/components/)
|
|
293
|
+
// The CMS still references these section keys, so the file must exist.
|
|
294
|
+
// The island was moved to components/, so we rewrite the re-export target.
|
|
295
|
+
if (category === "section" && isReExp) {
|
|
296
|
+
const target = record.reExportTarget || "";
|
|
297
|
+
const isIslandReExport = target.includes("islands/") ||
|
|
298
|
+
target.includes("islands\\");
|
|
299
|
+
if (isIslandReExport) {
|
|
300
|
+
return {
|
|
301
|
+
action: "transform",
|
|
302
|
+
targetPath: `src/${relPath}`,
|
|
303
|
+
notes: "Section re-export — island target rewritten to ~/components/",
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// Section re-exports from components/ — keep and transform
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Session component → delete (analytics moves to __root.tsx)
|
|
310
|
+
if (
|
|
311
|
+
relPath === "components/Session.tsx" || relPath === "sections/Session.tsx"
|
|
312
|
+
) {
|
|
313
|
+
return {
|
|
314
|
+
action: "delete",
|
|
315
|
+
notes: "Analytics SDK moved to __root.tsx scaffold",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Bagaggio-style HTMX dynamic-section loader → delete and flag.
|
|
320
|
+
// The file uses Deno-only APIs (`toFileUrl(Deno.cwd())`, `import.meta.resolve`)
|
|
321
|
+
// and the `useComponent(component, props)` HTMX render-and-swap pattern, none
|
|
322
|
+
// of which work on TanStack Start / Cloudflare Workers. The site author must
|
|
323
|
+
// refactor every `useComponent(...)` call site to React state, `createServerFn`
|
|
324
|
+
// + `useMutation`, or a direct `~/server/invoke` call BEFORE this file is
|
|
325
|
+
// safe to remove.
|
|
326
|
+
if (
|
|
327
|
+
relPath === "sections/Component.tsx" ||
|
|
328
|
+
relPath === "sections/Component.ts"
|
|
329
|
+
) {
|
|
330
|
+
return {
|
|
331
|
+
action: "delete",
|
|
332
|
+
notes:
|
|
333
|
+
"HTMX dynamic-section loader (useComponent) — incompatible with TanStack Start. " +
|
|
334
|
+
"Migrate every useComponent(...) call site to React state / createServerFn before deploy.",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Static code/tooling files → delete
|
|
339
|
+
if (STATIC_DELETE.has(relPath)) {
|
|
340
|
+
return { action: "delete", notes: "Code/tooling file, not an asset" };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Static files → move
|
|
344
|
+
if (category === "static") {
|
|
345
|
+
const publicPath = relPath.replace("static/", "public/");
|
|
346
|
+
return { action: "move", targetPath: publicPath };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Non-code root files that shouldn't go into src/
|
|
350
|
+
const ext = path.extname(relPath);
|
|
351
|
+
const nonCodeExts = new Set([".md", ".csv", ".json", ".sh", ".lock", ".yml", ".yaml", ".xml", ".html", ".txt", ".log"]);
|
|
352
|
+
const keepRootFiles = new Set(["account.json"]);
|
|
353
|
+
if (!relPath.includes("/") && nonCodeExts.has(ext) && !keepRootFiles.has(relPath)) {
|
|
354
|
+
return { action: "delete", notes: "Root-level non-code file" };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Root-level tooling files — not app code
|
|
358
|
+
const rootToolingFiles = new Set(["islands.ts", "sync.sh"]);
|
|
359
|
+
if (!relPath.includes("/") && rootToolingFiles.has(relPath)) {
|
|
360
|
+
return { action: "delete", notes: "Root-level tooling file" };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Everything else → transform into src/
|
|
364
|
+
return { action: "transform", targetPath: `src/${relPath}` };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function scanDir(
|
|
368
|
+
dir: string,
|
|
369
|
+
baseDir: string,
|
|
370
|
+
files: FileRecord[],
|
|
371
|
+
) {
|
|
372
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
373
|
+
|
|
374
|
+
for (const entry of entries) {
|
|
375
|
+
const fullPath = path.join(dir, entry.name);
|
|
376
|
+
const relPath = path.relative(baseDir, fullPath);
|
|
377
|
+
|
|
378
|
+
if (entry.isDirectory()) {
|
|
379
|
+
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".") || entry.name.startsWith("static-")) continue;
|
|
380
|
+
scanDir(fullPath, baseDir, files);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Skip dotfiles and known non-code files
|
|
385
|
+
if (SKIP_FILES.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
386
|
+
|
|
387
|
+
// Only process .ts, .tsx, .css, .json files for transforms
|
|
388
|
+
const ext = path.extname(entry.name);
|
|
389
|
+
const isCode = [".ts", ".tsx", ".css", ".json"].includes(ext);
|
|
390
|
+
|
|
391
|
+
let content = "";
|
|
392
|
+
let patterns: DetectedPattern[] = [];
|
|
393
|
+
let reExport: { is: boolean; target?: string } = { is: false };
|
|
394
|
+
|
|
395
|
+
if (isCode) {
|
|
396
|
+
content = fs.readFileSync(fullPath, "utf-8");
|
|
397
|
+
patterns = detectPatterns(content);
|
|
398
|
+
reExport = isReExport(content);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const record: FileRecord = {
|
|
402
|
+
path: relPath,
|
|
403
|
+
absPath: fullPath,
|
|
404
|
+
category: categorizeFile(relPath),
|
|
405
|
+
isReExport: reExport.is,
|
|
406
|
+
reExportTarget: reExport.target,
|
|
407
|
+
patterns,
|
|
408
|
+
action: "transform", // placeholder
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const decision = decideAction(record);
|
|
412
|
+
record.action = decision.action;
|
|
413
|
+
record.targetPath = decision.targetPath;
|
|
414
|
+
record.notes = decision.notes;
|
|
415
|
+
|
|
416
|
+
files.push(record);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function extractGtmId(sourceDir: string): string | null {
|
|
421
|
+
const appPath = path.join(sourceDir, "routes", "_app.tsx");
|
|
422
|
+
if (!fs.existsSync(appPath)) return null;
|
|
423
|
+
|
|
424
|
+
const content = fs.readFileSync(appPath, "utf-8");
|
|
425
|
+
const match = content.match(/GTM-[A-Z0-9]+/);
|
|
426
|
+
return match ? match[0] : null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function extractVtexAccount(sourceDir: string): string | null {
|
|
430
|
+
// Strategy 1: utils/sitename.ts — DICT_VTEX_AN mapping
|
|
431
|
+
for (const candidate of ["utils/sitename.ts", "sdk/sitename.ts"]) {
|
|
432
|
+
const fp = path.join(sourceDir, candidate);
|
|
433
|
+
if (!fs.existsSync(fp)) continue;
|
|
434
|
+
const content = fs.readFileSync(fp, "utf-8");
|
|
435
|
+
// Match patterns like: casaevideo: "casaevideonewio"
|
|
436
|
+
const match = content.match(/vtexAn|VTEX_AN/i);
|
|
437
|
+
if (match) {
|
|
438
|
+
const dictMatch = content.match(/["'](\w+)["']\s*:\s*["'](\w+myvtex|\w+newio|\w+)["']/);
|
|
439
|
+
if (dictMatch) return dictMatch[2];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Strategy 2: .myvtex.com references in loaders/routes
|
|
444
|
+
const candidates = [
|
|
445
|
+
"routes/_app.tsx",
|
|
446
|
+
"loaders/reviews/productReviews.ts",
|
|
447
|
+
"apps/vtex.ts",
|
|
448
|
+
];
|
|
449
|
+
for (const candidate of candidates) {
|
|
450
|
+
const fp = path.join(sourceDir, candidate);
|
|
451
|
+
if (!fs.existsSync(fp)) continue;
|
|
452
|
+
const content = fs.readFileSync(fp, "utf-8");
|
|
453
|
+
const match = content.match(/["'](\w+)\.myvtex\.com/);
|
|
454
|
+
if (match) return match[1];
|
|
455
|
+
const match2 = content.match(/account\s*[:=]\s*["'](\w+)["']/);
|
|
456
|
+
if (match2) return match2[1];
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Strategy 3: deno.json import map or apps/ config
|
|
460
|
+
const appsVtexPath = path.join(sourceDir, "apps", "vtex.ts");
|
|
461
|
+
if (fs.existsSync(appsVtexPath)) {
|
|
462
|
+
const content = fs.readFileSync(appsVtexPath, "utf-8");
|
|
463
|
+
const match = content.match(/account\s*:\s*["'](\w+)["']/);
|
|
464
|
+
if (match) return match[1];
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export function extractPlatform(sourceDir: string): Platform {
|
|
471
|
+
// Strategy 2 (apps/{p}.ts existence) is the most reliable signal, but it
|
|
472
|
+
// only matches when the platform name appears here — so a site whose only
|
|
473
|
+
// app file is `apps/magento.ts` previously fell through to Strategy 3
|
|
474
|
+
// and got mis-detected as "vtex" because site.ts imports
|
|
475
|
+
// `apps/vtex/mod.ts` for color palettes. See #211.
|
|
476
|
+
const platforms: Platform[] = [
|
|
477
|
+
"vtex",
|
|
478
|
+
"shopify",
|
|
479
|
+
"wake",
|
|
480
|
+
"vnda",
|
|
481
|
+
"linx",
|
|
482
|
+
"nuvemshop",
|
|
483
|
+
"magento",
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
// Strategy 1: Check deno.json imports for platform-specific app imports
|
|
487
|
+
const denoPath = path.join(sourceDir, "deno.json");
|
|
488
|
+
if (fs.existsSync(denoPath)) {
|
|
489
|
+
try {
|
|
490
|
+
const deno = JSON.parse(fs.readFileSync(denoPath, "utf-8"));
|
|
491
|
+
const imports = deno.imports || {};
|
|
492
|
+
for (const p of platforms) {
|
|
493
|
+
// e.g. "apps/vtex/" or direct app import containing the platform name
|
|
494
|
+
const hasAppImport = Object.keys(imports).some(
|
|
495
|
+
(k) => k === `apps/${p}/` || k.includes(`/${p}/mod.ts`) || k.includes(`deco-apps`) && imports[k].includes(`/${p}/`),
|
|
496
|
+
);
|
|
497
|
+
const hasAppValue = Object.values(imports).some(
|
|
498
|
+
(v) => typeof v === "string" && (v as string).includes(`/${p}/`),
|
|
499
|
+
);
|
|
500
|
+
if (hasAppImport || hasAppValue) return p;
|
|
501
|
+
}
|
|
502
|
+
// Check if the import map value for "apps/" contains a platform hint
|
|
503
|
+
const appsUrl = imports["apps/"];
|
|
504
|
+
if (typeof appsUrl === "string") {
|
|
505
|
+
for (const p of platforms) {
|
|
506
|
+
// The apps/ URL itself doesn't indicate platform, but let's check apps/vtex.ts
|
|
507
|
+
const vtexAppPath = path.join(sourceDir, "apps", `${p}.ts`);
|
|
508
|
+
if (fs.existsSync(vtexAppPath)) return p;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
} catch {}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Strategy 2: Check for apps/{platform}.ts file existence
|
|
515
|
+
for (const p of platforms) {
|
|
516
|
+
if (fs.existsSync(path.join(sourceDir, "apps", `${p}.ts`))) return p;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Strategy 3: Check apps/site.ts for platform type and default value
|
|
520
|
+
const sitePath = path.join(sourceDir, "apps", "site.ts");
|
|
521
|
+
if (fs.existsSync(sitePath)) {
|
|
522
|
+
const content = fs.readFileSync(sitePath, "utf-8");
|
|
523
|
+
// Look for platform default in state or props: state.platform || "vtex"
|
|
524
|
+
const defaultMatch = content.match(/(?:state\.platform|props\.platform)\s*\|\|\s*["'](\w+)["']/);
|
|
525
|
+
if (defaultMatch) {
|
|
526
|
+
const p = defaultMatch[1] as Platform;
|
|
527
|
+
if (platforms.includes(p)) return p;
|
|
528
|
+
}
|
|
529
|
+
// Look for platform in the Props type
|
|
530
|
+
for (const p of platforms) {
|
|
531
|
+
if (content.includes(`"${p}"`) && (content.includes("Platform") || content.includes("platform"))) {
|
|
532
|
+
return p;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Strategy 4: Check .deco/blocks for platform-specific block files
|
|
538
|
+
const blocksDir = path.join(sourceDir, ".deco", "blocks");
|
|
539
|
+
if (fs.existsSync(blocksDir)) {
|
|
540
|
+
const blockFiles = fs.readdirSync(blocksDir);
|
|
541
|
+
for (const p of platforms) {
|
|
542
|
+
if (blockFiles.some((f) => f.includes(`deco-${p}`) || f === `${p}.json`)) return p;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return "custom";
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function extractSiteName(sourceDir: string): string {
|
|
550
|
+
// Try to extract from .deco or directory name
|
|
551
|
+
const dirName = path.basename(path.resolve(sourceDir));
|
|
552
|
+
|
|
553
|
+
// Try deno.json
|
|
554
|
+
const denoPath = path.join(sourceDir, "deno.json");
|
|
555
|
+
if (fs.existsSync(denoPath)) {
|
|
556
|
+
const deno = JSON.parse(fs.readFileSync(denoPath, "utf-8"));
|
|
557
|
+
if (deno.name) return deno.name;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return dirName;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function extractThemeFromCms(sourceDir: string): { colors: Record<string, string>; fontFamily: string | null } {
|
|
564
|
+
const colors: Record<string, string> = {};
|
|
565
|
+
let fontFamily: string | null = null;
|
|
566
|
+
|
|
567
|
+
// Look for Theme config in .deco/blocks/
|
|
568
|
+
const blocksDir = path.join(sourceDir, ".deco", "blocks");
|
|
569
|
+
if (!fs.existsSync(blocksDir)) return { colors, fontFamily };
|
|
570
|
+
|
|
571
|
+
const files = fs.readdirSync(blocksDir);
|
|
572
|
+
for (const file of files) {
|
|
573
|
+
if (!file.endsWith(".json")) continue;
|
|
574
|
+
try {
|
|
575
|
+
const content = fs.readFileSync(path.join(blocksDir, file), "utf-8");
|
|
576
|
+
if (!content.includes("mainColors") && !content.includes("Theme")) continue;
|
|
577
|
+
|
|
578
|
+
const data = JSON.parse(content);
|
|
579
|
+
// Direct Theme block (e.g. Deco.json with __resolveType: "site/sections/Theme/Theme.tsx")
|
|
580
|
+
if (data.mainColors) {
|
|
581
|
+
Object.assign(colors, data.mainColors);
|
|
582
|
+
if (data.complementaryColors) {
|
|
583
|
+
Object.assign(colors, data.complementaryColors);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// Font
|
|
587
|
+
if (data.font?.fonts?.[0]?.family) {
|
|
588
|
+
fontFamily = data.font.fonts[0].family;
|
|
589
|
+
}
|
|
590
|
+
// Check sections array (page blocks may contain Theme)
|
|
591
|
+
if (data.sections) {
|
|
592
|
+
for (const section of data.sections) {
|
|
593
|
+
if (section.__resolveType?.includes("Theme") && section.mainColors) {
|
|
594
|
+
Object.assign(colors, section.mainColors);
|
|
595
|
+
if (section.complementaryColors) {
|
|
596
|
+
Object.assign(colors, section.complementaryColors);
|
|
597
|
+
}
|
|
598
|
+
if (section.font?.fonts?.[0]?.family) {
|
|
599
|
+
fontFamily = section.font.fonts[0].family;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} catch {}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return { colors, fontFamily };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function analyze(ctx: MigrationContext): void {
|
|
611
|
+
logPhase("Analyze");
|
|
612
|
+
|
|
613
|
+
// Parse deno.json for import map
|
|
614
|
+
const denoJsonPath = path.join(ctx.sourceDir, "deno.json");
|
|
615
|
+
if (fs.existsSync(denoJsonPath)) {
|
|
616
|
+
const denoJson = JSON.parse(fs.readFileSync(denoJsonPath, "utf-8"));
|
|
617
|
+
ctx.importMap = denoJson.imports || {};
|
|
618
|
+
log(
|
|
619
|
+
ctx,
|
|
620
|
+
`Found ${Object.keys(ctx.importMap).length} import map entries`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Extract metadata
|
|
625
|
+
ctx.siteName = extractSiteName(ctx.sourceDir);
|
|
626
|
+
ctx.platform = extractPlatform(ctx.sourceDir);
|
|
627
|
+
ctx.vtexAccount = ctx.platform === "vtex" ? extractVtexAccount(ctx.sourceDir) : null;
|
|
628
|
+
ctx.gtmId = extractGtmId(ctx.sourceDir);
|
|
629
|
+
|
|
630
|
+
// Extract theme colors and font from CMS
|
|
631
|
+
const theme = extractThemeFromCms(ctx.sourceDir);
|
|
632
|
+
ctx.themeColors = theme.colors;
|
|
633
|
+
ctx.fontFamily = theme.fontFamily;
|
|
634
|
+
|
|
635
|
+
console.log(` Site: ${ctx.siteName}`);
|
|
636
|
+
console.log(` Platform: ${ctx.platform}${ctx.vtexAccount ? ` (account: ${ctx.vtexAccount})` : ""}`);
|
|
637
|
+
console.log(` GTM ID: ${ctx.gtmId || "none"}`);
|
|
638
|
+
if (Object.keys(ctx.themeColors).length > 0) {
|
|
639
|
+
console.log(` Theme: ${Object.keys(ctx.themeColors).length} colors from CMS`);
|
|
640
|
+
}
|
|
641
|
+
if (ctx.fontFamily) {
|
|
642
|
+
console.log(` Font: ${ctx.fontFamily}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Scan all files
|
|
646
|
+
scanDir(ctx.sourceDir, ctx.sourceDir, ctx.files);
|
|
647
|
+
|
|
648
|
+
// Summary
|
|
649
|
+
const byAction = { transform: 0, delete: 0, move: 0, scaffold: 0, "manual-review": 0 };
|
|
650
|
+
const byCategory: Record<string, number> = {};
|
|
651
|
+
|
|
652
|
+
for (const f of ctx.files) {
|
|
653
|
+
byAction[f.action]++;
|
|
654
|
+
byCategory[f.category] = (byCategory[f.category] || 0) + 1;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Scan all source files for inline npm: imports
|
|
658
|
+
for (const f of ctx.files) {
|
|
659
|
+
if (f.action === "delete") continue;
|
|
660
|
+
const ext = path.extname(f.path);
|
|
661
|
+
if (![".ts", ".tsx"].includes(ext)) continue;
|
|
662
|
+
try {
|
|
663
|
+
const content = fs.readFileSync(f.absPath, "utf-8");
|
|
664
|
+
const deps = extractInlineNpmDeps(content);
|
|
665
|
+
Object.assign(ctx.discoveredNpmDeps, deps);
|
|
666
|
+
} catch {}
|
|
667
|
+
}
|
|
668
|
+
if (Object.keys(ctx.discoveredNpmDeps).length > 0) {
|
|
669
|
+
log(ctx, `Discovered npm deps from source: ${JSON.stringify(ctx.discoveredNpmDeps)}`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log(`\n Files found: ${ctx.files.length}`);
|
|
673
|
+
console.log(` By category: ${JSON.stringify(byCategory)}`);
|
|
674
|
+
console.log(` By action: ${JSON.stringify(byAction)}`);
|
|
675
|
+
|
|
676
|
+
// Surface the HTMX dynamic-section loader and every `useComponent` call site
|
|
677
|
+
// up-front. These need manual conversion to React state / createServerFn before
|
|
678
|
+
// the migrated site will run on Cloudflare Workers — the analyzer cannot do
|
|
679
|
+
// it automatically, so it must be loud enough to land in the report.
|
|
680
|
+
const useComponentSites = ctx.files.filter(
|
|
681
|
+
(f) => f.patterns.includes("use-component"),
|
|
682
|
+
);
|
|
683
|
+
const componentLoaderFile = ctx.files.find(
|
|
684
|
+
(f) =>
|
|
685
|
+
f.path === "sections/Component.tsx" ||
|
|
686
|
+
f.path === "sections/Component.ts",
|
|
687
|
+
);
|
|
688
|
+
if (componentLoaderFile || useComponentSites.length > 0) {
|
|
689
|
+
console.log("\n ⚠ HTMX dynamic-section loader detected (Bagaggio-style)");
|
|
690
|
+
if (componentLoaderFile) {
|
|
691
|
+
console.log(` • ${componentLoaderFile.path} (will be deleted)`);
|
|
692
|
+
}
|
|
693
|
+
if (useComponentSites.length > 0) {
|
|
694
|
+
console.log(` • ${useComponentSites.length} useComponent(...) call site(s) need manual conversion:`);
|
|
695
|
+
for (const f of useComponentSites.slice(0, 10)) {
|
|
696
|
+
console.log(` - ${f.path}`);
|
|
697
|
+
}
|
|
698
|
+
if (useComponentSites.length > 10) {
|
|
699
|
+
console.log(` ... and ${useComponentSites.length - 10} more`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
console.log(
|
|
703
|
+
" See: deco-to-tanstack-migration skill, 'useComponent / partial sections' section",
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Run analyzers
|
|
708
|
+
extractSectionMetadata(ctx);
|
|
709
|
+
classifyIslands(ctx);
|
|
710
|
+
inventoryLoaders(ctx);
|
|
711
|
+
|
|
712
|
+
// Apply island classifications to file records
|
|
713
|
+
const classMap = new Map(ctx.islandClassifications.map((c) => [c.path, c]));
|
|
714
|
+
for (const f of ctx.files) {
|
|
715
|
+
if (f.category !== "island") continue;
|
|
716
|
+
const classification = classMap.get(f.path);
|
|
717
|
+
if (!classification) continue;
|
|
718
|
+
|
|
719
|
+
if (classification.type === "wrapper") {
|
|
720
|
+
f.action = "delete";
|
|
721
|
+
f.notes = "Island wrapper — imports repointed to component";
|
|
722
|
+
// Build redirect map: island path → wrapped component's migrated import path
|
|
723
|
+
if (classification.wrapsComponent) {
|
|
724
|
+
let wrappedImport = classification.wrapsComponent;
|
|
725
|
+
if (wrappedImport.startsWith("./") || wrappedImport.startsWith("../")) {
|
|
726
|
+
// Resolve relative to the island's directory
|
|
727
|
+
const islandDir = path.dirname(f.path);
|
|
728
|
+
wrappedImport = path.posix.normalize(path.posix.join(islandDir, wrappedImport));
|
|
729
|
+
wrappedImport = "~/" + wrappedImport;
|
|
730
|
+
} else {
|
|
731
|
+
wrappedImport = wrappedImport.replace(/^(\$store|site)\//, "~/");
|
|
732
|
+
}
|
|
733
|
+
wrappedImport = wrappedImport.replace(/\.tsx?$/, "");
|
|
734
|
+
ctx.islandWrapperTargets.set(f.path, wrappedImport);
|
|
735
|
+
}
|
|
736
|
+
} else {
|
|
737
|
+
f.action = "transform";
|
|
738
|
+
f.targetPath = classification.suggestedTarget;
|
|
739
|
+
f.notes = "Standalone island moved to components";
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Fix section re-exports from wrapper islands — rewrite to the wrapper's target component
|
|
744
|
+
if (ctx.islandWrapperTargets.size > 0) {
|
|
745
|
+
for (const f of ctx.files) {
|
|
746
|
+
if (f.category !== "section" || !f.isReExport || !f.reExportTarget) continue;
|
|
747
|
+
let target = f.reExportTarget;
|
|
748
|
+
// Resolve relative paths (../../islands/X) against section's directory
|
|
749
|
+
if (target.startsWith("./") || target.startsWith("../")) {
|
|
750
|
+
target = path.posix.normalize(path.posix.join(path.dirname(f.path), target));
|
|
751
|
+
}
|
|
752
|
+
target = target.replace(/^(\$store|site)\//, "").replace(/\.tsx?$/, "");
|
|
753
|
+
if (!target.startsWith("islands/")) continue;
|
|
754
|
+
const wrappedImport = ctx.islandWrapperTargets.get(target + ".tsx") ||
|
|
755
|
+
ctx.islandWrapperTargets.get(target);
|
|
756
|
+
if (wrappedImport) {
|
|
757
|
+
(f as any).__resolvedReExportTarget = wrappedImport;
|
|
758
|
+
f.notes = `Section re-export — island wrapper resolved to ${wrappedImport}`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Delete non-platform files (e.g. shopify/linx/vnda/wake/nuvemshop on VTEX sites)
|
|
764
|
+
const NON_PLATFORM_PATTERNS: Record<string, RegExp> = {
|
|
765
|
+
vtex: /\/(shopify|linx|vnda|wake|nuvemshop)\b/i,
|
|
766
|
+
shopify: /\/(vtex|linx|vnda|wake|nuvemshop)\b/i,
|
|
767
|
+
vnda: /\/(vtex|shopify|linx|wake|nuvemshop)\b/i,
|
|
768
|
+
wake: /\/(vtex|shopify|linx|vnda|nuvemshop)\b/i,
|
|
769
|
+
linx: /\/(vtex|shopify|vnda|wake|nuvemshop)\b/i,
|
|
770
|
+
nuvemshop: /\/(vtex|shopify|linx|vnda|wake)\b/i,
|
|
771
|
+
};
|
|
772
|
+
const nonPlatformRe = NON_PLATFORM_PATTERNS[ctx.platform];
|
|
773
|
+
if (nonPlatformRe) {
|
|
774
|
+
for (const f of ctx.files) {
|
|
775
|
+
if (f.action === "delete") continue;
|
|
776
|
+
if (nonPlatformRe.test("/" + f.path)) {
|
|
777
|
+
f.action = "delete";
|
|
778
|
+
f.notes = `Non-${ctx.platform} platform file`;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|