@decocms/start 1.6.2 → 1.7.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/.cursor/skills/deco-to-tanstack-migration/SKILL.md +85 -12
- package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +98 -0
- package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +45 -25
- package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +56 -39
- package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +122 -141
- package/.releaserc.json +1 -0
- package/package.json +1 -1
- package/scripts/generate-blocks.ts +8 -5
- package/scripts/generate-loaders.ts +79 -12
- package/scripts/migrate/analyzers/island-classifier.ts +23 -0
- package/scripts/migrate/analyzers/section-metadata.ts +63 -7
- package/scripts/migrate/phase-analyze.ts +190 -11
- package/scripts/migrate/phase-cleanup.ts +1162 -7
- package/scripts/migrate/phase-scaffold.ts +294 -5
- package/scripts/migrate/phase-transform.ts +56 -3
- package/scripts/migrate/templates/app-css.ts +149 -2
- package/scripts/migrate/templates/commerce-loaders.ts +174 -69
- package/scripts/migrate/templates/lib-utils.ts +255 -0
- package/scripts/migrate/templates/package-json.ts +30 -22
- package/scripts/migrate/templates/routes.ts +81 -11
- package/scripts/migrate/templates/section-loaders.ts +369 -33
- package/scripts/migrate/templates/server-entry.ts +350 -80
- package/scripts/migrate/templates/setup.ts +78 -8
- package/scripts/migrate/templates/types-gen.ts +58 -0
- package/scripts/migrate/templates/ui-components.ts +47 -16
- package/scripts/migrate/templates/vite-config.ts +17 -6
- package/scripts/migrate/templates/wrangler.ts +3 -1
- package/scripts/migrate/transforms/dead-code.ts +330 -4
- package/scripts/migrate/transforms/deno-isms.ts +19 -0
- package/scripts/migrate/transforms/imports.ts +93 -30
- package/scripts/migrate/transforms/jsx.ts +79 -4
- package/scripts/migrate/transforms/section-conventions.ts +105 -3
- package/scripts/migrate/types.ts +9 -1
- package/src/cms/resolve.ts +12 -1
- package/src/sdk/useScript.ts +27 -6
- package/src/sdk/workerEntry.ts +11 -2
- package/src/setup.ts +1 -1
|
@@ -32,6 +32,10 @@ const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
|
32
32
|
["head-component", /<Head[\s>]/],
|
|
33
33
|
["define-app", /defineApp\(/],
|
|
34
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?)?["']/],
|
|
35
39
|
];
|
|
36
40
|
|
|
37
41
|
/** Files/dirs that should be completely skipped during scanning */
|
|
@@ -71,7 +75,6 @@ const SKIP_FILES = new Set([
|
|
|
71
75
|
"yarn.lock",
|
|
72
76
|
"bun.lock",
|
|
73
77
|
"bun.lockb",
|
|
74
|
-
"account.json",
|
|
75
78
|
]);
|
|
76
79
|
|
|
77
80
|
/** Files that are generated and should be deleted */
|
|
@@ -85,8 +88,8 @@ const GENERATED_FILES = new Set([
|
|
|
85
88
|
const SDK_DELETE = new Set([
|
|
86
89
|
"sdk/clx.ts",
|
|
87
90
|
"sdk/useId.ts",
|
|
88
|
-
|
|
89
|
-
|
|
91
|
+
// sdk/useOffer.ts — kept: sites often customize offer logic
|
|
92
|
+
// sdk/useVariantPossiblities.ts — kept: sites often customize variant logic
|
|
90
93
|
"sdk/usePlatform.tsx",
|
|
91
94
|
"sdk/signal.ts",
|
|
92
95
|
"sdk/format.ts",
|
|
@@ -141,10 +144,27 @@ function extractInlineNpmDeps(content: string): Record<string, string> {
|
|
|
141
144
|
while ((match = regex.exec(content)) !== null) {
|
|
142
145
|
const name = match[1];
|
|
143
146
|
const version = match[2] || "*";
|
|
144
|
-
// Skip framework deps
|
|
145
147
|
if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
|
|
146
148
|
deps[name] = `^${version}`;
|
|
147
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
|
+
|
|
148
168
|
return deps;
|
|
149
169
|
}
|
|
150
170
|
|
|
@@ -218,6 +238,11 @@ function decideAction(
|
|
|
218
238
|
return { action: "delete", notes: "Rewritten from scratch" };
|
|
219
239
|
}
|
|
220
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
|
+
|
|
221
246
|
// Loaders that depend on deleted admin tooling
|
|
222
247
|
if (LOADER_DELETE.has(relPath)) {
|
|
223
248
|
return {
|
|
@@ -247,6 +272,8 @@ function decideAction(
|
|
|
247
272
|
return { action: "delete", notes: "Use @decocms/apps cart hooks" };
|
|
248
273
|
}
|
|
249
274
|
|
|
275
|
+
// Non-platform cleanup is done in analyze() post-processing (needs ctx.platform)
|
|
276
|
+
|
|
250
277
|
// Islands — classify and route to appropriate target
|
|
251
278
|
if (category === "island") {
|
|
252
279
|
const classification = (record as any).__islandClassification;
|
|
@@ -262,14 +289,19 @@ function decideAction(
|
|
|
262
289
|
};
|
|
263
290
|
}
|
|
264
291
|
|
|
265
|
-
// Sections that re-export from islands/ →
|
|
266
|
-
//
|
|
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.
|
|
267
295
|
if (category === "section" && isReExp) {
|
|
268
296
|
const target = record.reExportTarget || "";
|
|
269
297
|
const isIslandReExport = target.includes("islands/") ||
|
|
270
298
|
target.includes("islands\\");
|
|
271
299
|
if (isIslandReExport) {
|
|
272
|
-
return {
|
|
300
|
+
return {
|
|
301
|
+
action: "transform",
|
|
302
|
+
targetPath: `src/${relPath}`,
|
|
303
|
+
notes: "Section re-export — island target rewritten to ~/components/",
|
|
304
|
+
};
|
|
273
305
|
}
|
|
274
306
|
// Section re-exports from components/ — keep and transform
|
|
275
307
|
}
|
|
@@ -284,6 +316,25 @@ function decideAction(
|
|
|
284
316
|
};
|
|
285
317
|
}
|
|
286
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
|
+
|
|
287
338
|
// Static code/tooling files → delete
|
|
288
339
|
if (STATIC_DELETE.has(relPath)) {
|
|
289
340
|
return { action: "delete", notes: "Code/tooling file, not an asset" };
|
|
@@ -298,12 +349,13 @@ function decideAction(
|
|
|
298
349
|
// Non-code root files that shouldn't go into src/
|
|
299
350
|
const ext = path.extname(relPath);
|
|
300
351
|
const nonCodeExts = new Set([".md", ".csv", ".json", ".sh", ".lock", ".yml", ".yaml", ".xml", ".html", ".txt", ".log"]);
|
|
301
|
-
|
|
352
|
+
const keepRootFiles = new Set(["account.json"]);
|
|
353
|
+
if (!relPath.includes("/") && nonCodeExts.has(ext) && !keepRootFiles.has(relPath)) {
|
|
302
354
|
return { action: "delete", notes: "Root-level non-code file" };
|
|
303
355
|
}
|
|
304
356
|
|
|
305
|
-
// Root-level
|
|
306
|
-
const rootToolingFiles = new Set(["islands.ts", "
|
|
357
|
+
// Root-level tooling files — not app code
|
|
358
|
+
const rootToolingFiles = new Set(["islands.ts", "sync.sh"]);
|
|
307
359
|
if (!relPath.includes("/") && rootToolingFiles.has(relPath)) {
|
|
308
360
|
return { action: "delete", notes: "Root-level tooling file" };
|
|
309
361
|
}
|
|
@@ -374,6 +426,47 @@ function extractGtmId(sourceDir: string): string | null {
|
|
|
374
426
|
return match ? match[0] : null;
|
|
375
427
|
}
|
|
376
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
|
+
|
|
377
470
|
function extractPlatform(sourceDir: string): Platform {
|
|
378
471
|
const platforms: Platform[] = ["vtex", "shopify", "wake", "vnda", "linx", "nuvemshop"];
|
|
379
472
|
|
|
@@ -518,6 +611,7 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
518
611
|
// Extract metadata
|
|
519
612
|
ctx.siteName = extractSiteName(ctx.sourceDir);
|
|
520
613
|
ctx.platform = extractPlatform(ctx.sourceDir);
|
|
614
|
+
ctx.vtexAccount = ctx.platform === "vtex" ? extractVtexAccount(ctx.sourceDir) : null;
|
|
521
615
|
ctx.gtmId = extractGtmId(ctx.sourceDir);
|
|
522
616
|
|
|
523
617
|
// Extract theme colors and font from CMS
|
|
@@ -526,7 +620,7 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
526
620
|
ctx.fontFamily = theme.fontFamily;
|
|
527
621
|
|
|
528
622
|
console.log(` Site: ${ctx.siteName}`);
|
|
529
|
-
console.log(` Platform: ${ctx.platform}`);
|
|
623
|
+
console.log(` Platform: ${ctx.platform}${ctx.vtexAccount ? ` (account: ${ctx.vtexAccount})` : ""}`);
|
|
530
624
|
console.log(` GTM ID: ${ctx.gtmId || "none"}`);
|
|
531
625
|
if (Object.keys(ctx.themeColors).length > 0) {
|
|
532
626
|
console.log(` Theme: ${Object.keys(ctx.themeColors).length} colors from CMS`);
|
|
@@ -566,6 +660,37 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
566
660
|
console.log(` By category: ${JSON.stringify(byCategory)}`);
|
|
567
661
|
console.log(` By action: ${JSON.stringify(byAction)}`);
|
|
568
662
|
|
|
663
|
+
// Surface the HTMX dynamic-section loader and every `useComponent` call site
|
|
664
|
+
// up-front. These need manual conversion to React state / createServerFn before
|
|
665
|
+
// the migrated site will run on Cloudflare Workers — the analyzer cannot do
|
|
666
|
+
// it automatically, so it must be loud enough to land in the report.
|
|
667
|
+
const useComponentSites = ctx.files.filter(
|
|
668
|
+
(f) => f.patterns.includes("use-component"),
|
|
669
|
+
);
|
|
670
|
+
const componentLoaderFile = ctx.files.find(
|
|
671
|
+
(f) =>
|
|
672
|
+
f.path === "sections/Component.tsx" ||
|
|
673
|
+
f.path === "sections/Component.ts",
|
|
674
|
+
);
|
|
675
|
+
if (componentLoaderFile || useComponentSites.length > 0) {
|
|
676
|
+
console.log("\n ⚠ HTMX dynamic-section loader detected (Bagaggio-style)");
|
|
677
|
+
if (componentLoaderFile) {
|
|
678
|
+
console.log(` • ${componentLoaderFile.path} (will be deleted)`);
|
|
679
|
+
}
|
|
680
|
+
if (useComponentSites.length > 0) {
|
|
681
|
+
console.log(` • ${useComponentSites.length} useComponent(...) call site(s) need manual conversion:`);
|
|
682
|
+
for (const f of useComponentSites.slice(0, 10)) {
|
|
683
|
+
console.log(` - ${f.path}`);
|
|
684
|
+
}
|
|
685
|
+
if (useComponentSites.length > 10) {
|
|
686
|
+
console.log(` ... and ${useComponentSites.length - 10} more`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
console.log(
|
|
690
|
+
" See: deco-to-tanstack-migration skill, 'useComponent / partial sections' section",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
569
694
|
// Run analyzers
|
|
570
695
|
extractSectionMetadata(ctx);
|
|
571
696
|
classifyIslands(ctx);
|
|
@@ -581,10 +706,64 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
581
706
|
if (classification.type === "wrapper") {
|
|
582
707
|
f.action = "delete";
|
|
583
708
|
f.notes = "Island wrapper — imports repointed to component";
|
|
709
|
+
// Build redirect map: island path → wrapped component's migrated import path
|
|
710
|
+
if (classification.wrapsComponent) {
|
|
711
|
+
let wrappedImport = classification.wrapsComponent;
|
|
712
|
+
if (wrappedImport.startsWith("./") || wrappedImport.startsWith("../")) {
|
|
713
|
+
// Resolve relative to the island's directory
|
|
714
|
+
const islandDir = path.dirname(f.path);
|
|
715
|
+
wrappedImport = path.posix.normalize(path.posix.join(islandDir, wrappedImport));
|
|
716
|
+
wrappedImport = "~/" + wrappedImport;
|
|
717
|
+
} else {
|
|
718
|
+
wrappedImport = wrappedImport.replace(/^(\$store|site)\//, "~/");
|
|
719
|
+
}
|
|
720
|
+
wrappedImport = wrappedImport.replace(/\.tsx?$/, "");
|
|
721
|
+
ctx.islandWrapperTargets.set(f.path, wrappedImport);
|
|
722
|
+
}
|
|
584
723
|
} else {
|
|
585
724
|
f.action = "transform";
|
|
586
725
|
f.targetPath = classification.suggestedTarget;
|
|
587
726
|
f.notes = "Standalone island moved to components";
|
|
588
727
|
}
|
|
589
728
|
}
|
|
729
|
+
|
|
730
|
+
// Fix section re-exports from wrapper islands — rewrite to the wrapper's target component
|
|
731
|
+
if (ctx.islandWrapperTargets.size > 0) {
|
|
732
|
+
for (const f of ctx.files) {
|
|
733
|
+
if (f.category !== "section" || !f.isReExport || !f.reExportTarget) continue;
|
|
734
|
+
let target = f.reExportTarget;
|
|
735
|
+
// Resolve relative paths (../../islands/X) against section's directory
|
|
736
|
+
if (target.startsWith("./") || target.startsWith("../")) {
|
|
737
|
+
target = path.posix.normalize(path.posix.join(path.dirname(f.path), target));
|
|
738
|
+
}
|
|
739
|
+
target = target.replace(/^(\$store|site)\//, "").replace(/\.tsx?$/, "");
|
|
740
|
+
if (!target.startsWith("islands/")) continue;
|
|
741
|
+
const wrappedImport = ctx.islandWrapperTargets.get(target + ".tsx") ||
|
|
742
|
+
ctx.islandWrapperTargets.get(target);
|
|
743
|
+
if (wrappedImport) {
|
|
744
|
+
(f as any).__resolvedReExportTarget = wrappedImport;
|
|
745
|
+
f.notes = `Section re-export — island wrapper resolved to ${wrappedImport}`;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Delete non-platform files (e.g. shopify/linx/vnda/wake/nuvemshop on VTEX sites)
|
|
751
|
+
const NON_PLATFORM_PATTERNS: Record<string, RegExp> = {
|
|
752
|
+
vtex: /\/(shopify|linx|vnda|wake|nuvemshop)\b/i,
|
|
753
|
+
shopify: /\/(vtex|linx|vnda|wake|nuvemshop)\b/i,
|
|
754
|
+
vnda: /\/(vtex|shopify|linx|wake|nuvemshop)\b/i,
|
|
755
|
+
wake: /\/(vtex|shopify|linx|vnda|nuvemshop)\b/i,
|
|
756
|
+
linx: /\/(vtex|shopify|vnda|wake|nuvemshop)\b/i,
|
|
757
|
+
nuvemshop: /\/(vtex|shopify|linx|vnda|wake)\b/i,
|
|
758
|
+
};
|
|
759
|
+
const nonPlatformRe = NON_PLATFORM_PATTERNS[ctx.platform];
|
|
760
|
+
if (nonPlatformRe) {
|
|
761
|
+
for (const f of ctx.files) {
|
|
762
|
+
if (f.action === "delete") continue;
|
|
763
|
+
if (nonPlatformRe.test("/" + f.path)) {
|
|
764
|
+
f.action = "delete";
|
|
765
|
+
f.notes = `Non-${ctx.platform} platform file`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
590
769
|
}
|