@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
|
@@ -34,17 +34,25 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
34
34
|
[/^"apps\/website\/components\/Picture\.tsx"$/, `"~/components/ui/Picture"`],
|
|
35
35
|
[/^"apps\/website\/components\/Video\.tsx"$/, `"~/components/ui/Video"`],
|
|
36
36
|
[/^"apps\/website\/components\/Theme\.tsx"$/, `"~/components/ui/Theme"`],
|
|
37
|
+
[/^"apps\/website\/components\/_seo\/[^"]+?"$/, null], // SEO preview — framework-only, remove
|
|
37
38
|
[/^"apps\/website\/components\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
|
|
38
39
|
[/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
|
|
39
40
|
[/^"apps\/commerce\/mod\.ts"$/, `"~/types/commerce-app"`],
|
|
40
41
|
[/^"apps\/commerce\/types"$/, `"@decocms/apps/commerce/types"`],
|
|
41
42
|
|
|
42
|
-
// Apps — VTEX (hooks
|
|
43
|
+
// Apps — VTEX hooks: useUser/useCart/useWishlist → local hooks (react-query based @decocms/apps hooks crash Workers SSR)
|
|
44
|
+
[/^"apps\/vtex\/hooks\/useUser(?:\.ts)?"$/, `"~/hooks/useUser"`],
|
|
45
|
+
[/^"apps\/vtex\/hooks\/useCart(?:\.ts)?"$/, `"~/hooks/useCart"`],
|
|
46
|
+
[/^"apps\/vtex\/hooks\/useWishlist(?:\.ts)?"$/, `"~/hooks/useWishlist"`],
|
|
43
47
|
[/^"apps\/vtex\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/hooks/$1"`],
|
|
48
|
+
// Specific VTEX utils that moved to different paths in @decocms/apps
|
|
49
|
+
[/^"apps\/vtex\/utils\/fetchVTEX(?:\.ts)?"$/, `"@decocms/apps/vtex/client"`],
|
|
50
|
+
[/^"apps\/vtex\/utils\/client(?:\.ts)?"$/, `"@decocms/apps/vtex/client"`],
|
|
44
51
|
[/^"apps\/vtex\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/utils/$1"`],
|
|
45
52
|
[/^"apps\/vtex\/actions\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/actions/$1"`],
|
|
46
53
|
[/^"apps\/vtex\/loaders\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/loaders/$1"`],
|
|
47
54
|
[/^"apps\/vtex\/types(?:\.ts)?"$/, `"@decocms/apps/vtex/types"`],
|
|
55
|
+
[/^"apps\/vtex\/mod(?:\.ts)?"$/, `"~/types/vtex-app"`],
|
|
48
56
|
// Apps — Shopify (hooks, utils, actions, loaders)
|
|
49
57
|
[/^"apps\/shopify\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/hooks/$1"`],
|
|
50
58
|
[/^"apps\/shopify\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/utils/$1"`],
|
|
@@ -54,48 +62,72 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
54
62
|
[/^"apps\/commerce\/sdk\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/sdk/$1"`],
|
|
55
63
|
[/^"apps\/commerce\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/utils/$1"`],
|
|
56
64
|
|
|
65
|
+
// Apps — shared utils (STALE, fetchSafe, createHttpClient, etc.)
|
|
66
|
+
[/^"apps\/utils\/fetch(?:\.ts)?"$/, `"~/lib/fetch-utils"`],
|
|
67
|
+
[/^"apps\/utils\/http(?:\.ts)?"$/, `"~/lib/http-utils"`],
|
|
68
|
+
[/^"apps\/utils\/graphql(?:\.ts)?"$/, `"~/lib/graphql-utils"`],
|
|
69
|
+
|
|
57
70
|
// Apps — catch-all (things like apps/website/mod.ts, apps/analytics/mod.ts, etc.)
|
|
58
71
|
[/^"apps\/([^"]+)"$/, null], // Remove — site.ts is rewritten
|
|
59
72
|
|
|
60
73
|
// Deco old CDN imports
|
|
61
74
|
[/^"deco\/([^"]+)"$/, null],
|
|
62
75
|
|
|
76
|
+
// Remote URL imports (esm.sh, cdn.esm.sh, skypack, etc.) — remove
|
|
77
|
+
[/^"https?:\/\/esm\.sh\/[^"]*"$/, null],
|
|
78
|
+
[/^"https?:\/\/cdn\.esm\.sh\/[^"]*"$/, null],
|
|
79
|
+
[/^"https?:\/\/cdn\.skypack\.dev\/[^"]*"$/, null],
|
|
80
|
+
[/^"https?:\/\/deno\.land\/[^"]*"$/, null],
|
|
81
|
+
|
|
63
82
|
// Std lib — redirect useful utils, remove the rest
|
|
64
83
|
[/^"std\/async\/debounce(?:\.ts)?"$/, `"~/sdk/debounce"`],
|
|
65
84
|
[/^"std\/([^"]+)"$/, null],
|
|
66
85
|
[/^"@std\/crypto"$/, null], // Use globalThis.crypto instead
|
|
67
86
|
|
|
68
87
|
// site/sdk/* → framework equivalents (before the catch-all site/ → ~/ rule)
|
|
69
|
-
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"
|
|
88
|
+
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
|
|
70
89
|
[/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
|
|
71
|
-
|
|
72
|
-
[/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
|
|
90
|
+
// useOffer and useVariantPossiblities kept as site files (~/sdk/)
|
|
73
91
|
[/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
|
|
74
92
|
|
|
75
|
-
//
|
|
76
|
-
[/^"\$store\/account\.json"$/, `"~/account
|
|
77
|
-
[/^"site\/account\.json"$/, `"~/account
|
|
93
|
+
// account.json → constants/account (JSON file replaced with TS module in cleanup)
|
|
94
|
+
[/^"\$store\/account\.json"$/, `"~/constants/account"`],
|
|
95
|
+
[/^"site\/account\.json"$/, `"~/constants/account"`],
|
|
96
|
+
[/^"~\/account\.json"$/, `"~/constants/account"`],
|
|
78
97
|
|
|
79
98
|
// $store/ → ~/ (common Deno import map alias for project root)
|
|
80
|
-
[/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"
|
|
99
|
+
[/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
|
|
81
100
|
[/^"\$store\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
|
|
82
|
-
|
|
101
|
+
// useOffer and useVariantPossiblities kept as site files (~/sdk/)
|
|
83
102
|
[/^"\$store\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
|
|
84
|
-
[/^"\$store\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
|
|
85
103
|
[/^"\$store\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
|
|
104
|
+
// islands → components (must be before $store catch-all)
|
|
105
|
+
[/^"\$store\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
|
|
106
|
+
[/^"\$store\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
|
|
86
107
|
[/^"\$store\/(.+)"$/, `"~/$1"`],
|
|
87
108
|
|
|
88
109
|
// $home/ → ~/ (another common alias)
|
|
89
110
|
[/^"\$home\/(.+)"$/, `"~/$1"`],
|
|
90
111
|
|
|
91
112
|
// site/ → ~/
|
|
92
|
-
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"
|
|
113
|
+
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
|
|
93
114
|
[/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
|
|
94
|
-
|
|
115
|
+
// useOffer and useVariantPossiblities kept as site files (~/sdk/)
|
|
95
116
|
[/^"site\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
|
|
96
|
-
[/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
|
|
97
117
|
[/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
|
|
118
|
+
// islands → components (must be before site/ catch-all)
|
|
119
|
+
[/^"site\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
|
|
120
|
+
[/^"site\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
|
|
98
121
|
[/^"site\/(.+)"$/, `"~/$1"`],
|
|
122
|
+
|
|
123
|
+
// ~/islands/* → ~/components/* (catch any that slipped through)
|
|
124
|
+
[/^"~\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
|
|
125
|
+
[/^"~\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
|
|
126
|
+
|
|
127
|
+
// @decocms/apps hooks → local hooks (react-query hooks crash Workers SSR at module eval)
|
|
128
|
+
[/^"@decocms\/apps\/vtex\/hooks\/useUser"$/, `"~/hooks/useUser"`],
|
|
129
|
+
[/^"@decocms\/apps\/vtex\/hooks\/useCart"$/, `"~/hooks/useCart"`],
|
|
130
|
+
[/^"@decocms\/apps\/vtex\/hooks\/useWishlist"$/, `"~/hooks/useWishlist"`],
|
|
99
131
|
];
|
|
100
132
|
|
|
101
133
|
/**
|
|
@@ -104,20 +136,21 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
104
136
|
* The key is the ending of the import path, the value is the replacement specifier.
|
|
105
137
|
*/
|
|
106
138
|
const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
|
|
107
|
-
// sdk/clx →
|
|
108
|
-
[/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "
|
|
139
|
+
// sdk/clx → @decocms/start/sdk/clx (framework utility)
|
|
140
|
+
[/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "@decocms/start/sdk/clx"],
|
|
109
141
|
// sdk/useId → react (useId is built-in in React 19)
|
|
110
142
|
[/(?:\.\.\/)*sdk\/useId(?:\.tsx?)?$/, "react"],
|
|
111
|
-
// sdk/useOffer
|
|
112
|
-
|
|
143
|
+
// sdk/useOffer — kept as-is (sites customize offer logic)
|
|
144
|
+
// sdk/useVariantPossiblities — kept as-is (sites customize variant logic)
|
|
113
145
|
// sdk/format → @decocms/apps/commerce/sdk/formatPrice
|
|
114
146
|
[/(?:\.\.\/)*sdk\/format(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/formatPrice"],
|
|
115
|
-
// sdk/useVariantPossiblities → @decocms/apps/commerce/sdk/useVariantPossibilities
|
|
116
|
-
[/(?:\.\.\/)*sdk\/useVariantPossiblities(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useVariantPossibilities"],
|
|
117
147
|
// sdk/usePlatform → remove entirely
|
|
118
148
|
[/(?:\.\.\/)*sdk\/usePlatform(?:\.tsx?)?$/, ""],
|
|
119
149
|
// static/adminIcons → deleted (icon loaders need rewriting)
|
|
120
150
|
[/(?:\.\.\/)*static\/adminIcons(?:\.ts)?$/, ""],
|
|
151
|
+
// islands/ui/* → components/ui/* (islands are merged into components)
|
|
152
|
+
[/(?:\.\.\/)*islands\/ui\/([^"]+?)(?:\.tsx?)?$/, "~/components/ui/$1"],
|
|
153
|
+
[/(?:\.\.\/)*islands\/([^"]+?)(?:\.tsx?)?$/, "~/components/$1"],
|
|
121
154
|
];
|
|
122
155
|
|
|
123
156
|
/**
|
|
@@ -132,10 +165,27 @@ const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
|
|
|
132
165
|
*
|
|
133
166
|
* When a rule maps to null, the entire import line is removed.
|
|
134
167
|
*/
|
|
135
|
-
export function transformImports(
|
|
168
|
+
export function transformImports(
|
|
169
|
+
content: string,
|
|
170
|
+
islandWrapperTargets?: Map<string, string>,
|
|
171
|
+
): TransformResult {
|
|
136
172
|
const notes: string[] = [];
|
|
137
173
|
let changed = false;
|
|
138
174
|
|
|
175
|
+
// Build dynamic rules from island wrapper targets (wrapper island → actual component)
|
|
176
|
+
const dynamicRules: Array<[RegExp, string]> = [];
|
|
177
|
+
if (islandWrapperTargets) {
|
|
178
|
+
for (const [islandPath, targetImport] of islandWrapperTargets) {
|
|
179
|
+
const componentPath = islandPath.replace("islands/", "components/").replace(/\.tsx?$/, "");
|
|
180
|
+
// Match ~/components/X and rewrite to the wrapper's actual target
|
|
181
|
+
const escaped = componentPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
182
|
+
dynamicRules.push([
|
|
183
|
+
new RegExp(`^"~/${escaped}"$`),
|
|
184
|
+
`"${targetImport}"`,
|
|
185
|
+
]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
139
189
|
// Strip BOM that prevents ^ matching on the first line
|
|
140
190
|
if (content.charCodeAt(0) === 0xfeff) {
|
|
141
191
|
content = content.slice(1);
|
|
@@ -180,6 +230,13 @@ export function transformImports(content: string): TransformResult {
|
|
|
180
230
|
);
|
|
181
231
|
}
|
|
182
232
|
|
|
233
|
+
function applyDynamicRules(result: string): string {
|
|
234
|
+
for (const [dynPattern, dynReplacement] of dynamicRules) {
|
|
235
|
+
if (dynPattern.test(result)) return dynReplacement;
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
183
240
|
function rewriteSpecifier(specifier: string): string | null {
|
|
184
241
|
// Remove quotes for matching
|
|
185
242
|
const inner = specifier.slice(1, -1);
|
|
@@ -187,17 +244,16 @@ export function transformImports(content: string): TransformResult {
|
|
|
187
244
|
for (const [pattern, replacement] of IMPORT_RULES) {
|
|
188
245
|
if (pattern.test(`"${inner}"`)) {
|
|
189
246
|
if (replacement === null) return null;
|
|
190
|
-
// Apply regex replacement
|
|
191
247
|
let result = `"${inner}"`.replace(pattern, replacement);
|
|
192
|
-
|
|
193
|
-
const resultInner = result.slice(1, -1);
|
|
248
|
+
let resultInner = result.slice(1, -1);
|
|
194
249
|
if (
|
|
195
250
|
(resultInner.startsWith("~/") || resultInner.startsWith("./") || resultInner.startsWith("../")) &&
|
|
196
251
|
(resultInner.endsWith(".ts") || resultInner.endsWith(".tsx"))
|
|
197
252
|
) {
|
|
198
|
-
|
|
253
|
+
resultInner = resultInner.replace(/\.tsx?$/, "");
|
|
254
|
+
result = `"${resultInner}"`;
|
|
199
255
|
}
|
|
200
|
-
return result;
|
|
256
|
+
return applyDynamicRules(result);
|
|
201
257
|
}
|
|
202
258
|
}
|
|
203
259
|
|
|
@@ -205,8 +261,9 @@ export function transformImports(content: string): TransformResult {
|
|
|
205
261
|
if (inner.startsWith("./") || inner.startsWith("../")) {
|
|
206
262
|
for (const [pattern, replacement] of RELATIVE_SDK_REWRITES) {
|
|
207
263
|
if (pattern.test(inner)) {
|
|
208
|
-
if (replacement === "") return null;
|
|
209
|
-
|
|
264
|
+
if (replacement === "") return null;
|
|
265
|
+
const resolved = inner.replace(pattern, replacement);
|
|
266
|
+
return applyDynamicRules(`"${resolved}"`);
|
|
210
267
|
}
|
|
211
268
|
}
|
|
212
269
|
}
|
|
@@ -215,7 +272,7 @@ export function transformImports(content: string): TransformResult {
|
|
|
215
272
|
if (inner.startsWith("npm:")) {
|
|
216
273
|
const cleaned = inner
|
|
217
274
|
.slice(4)
|
|
218
|
-
.replace(/@[\d^~>=<.*]+$/, "");
|
|
275
|
+
.replace(/@[\d^~>=<.*]+$/, "");
|
|
219
276
|
return `"${cleaned}"`;
|
|
220
277
|
}
|
|
221
278
|
|
|
@@ -226,7 +283,7 @@ export function transformImports(content: string): TransformResult {
|
|
|
226
283
|
(inner.endsWith(".ts") || inner.endsWith(".tsx"))
|
|
227
284
|
) {
|
|
228
285
|
const stripped = inner.replace(/\.tsx?$/, "");
|
|
229
|
-
return `"${stripped}"
|
|
286
|
+
return applyDynamicRules(`"${stripped}"`);
|
|
230
287
|
}
|
|
231
288
|
|
|
232
289
|
return specifier;
|
|
@@ -247,7 +304,13 @@ export function transformImports(content: string): TransformResult {
|
|
|
247
304
|
if (newSpec !== specifier) {
|
|
248
305
|
changed = true;
|
|
249
306
|
notes.push(`Rewrote: ${specifier} → ${newSpec}`);
|
|
250
|
-
|
|
307
|
+
// Strip import assertions (with/assert { type: "json" }) when the
|
|
308
|
+
// specifier no longer points to a JSON file (e.g. account.json → constants/account)
|
|
309
|
+
let cleanSuffix = suffix;
|
|
310
|
+
if (specifier.includes(".json") && !newSpec.includes(".json")) {
|
|
311
|
+
cleanSuffix = cleanSuffix.replace(/\s*(?:with|assert)\s*\{[^}]*\}\s*/, "");
|
|
312
|
+
}
|
|
313
|
+
return `${prefix}${newSpec}${cleanSuffix}`;
|
|
251
314
|
}
|
|
252
315
|
return `${prefix}${specifier}${suffix}`;
|
|
253
316
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { TransformResult } from "../types.ts";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Convert JSX attribute syntax to object literal entries.
|
|
5
|
+
* e.g. `page={page}` → `page`, `foo={bar.baz}` → `foo: bar.baz`, `label="hi"` → `label: "hi"`
|
|
6
|
+
*/
|
|
7
|
+
function jsxAttrsToObjectEntries(jsxAttrs: string): string {
|
|
8
|
+
return jsxAttrs
|
|
9
|
+
.replace(/(\w+)=\{(\1)\}/g, "$1") // shorthand: name={name} → name
|
|
10
|
+
.replace(/(\w+)=\{([^}]+)\}/g, "$1: $2") // name={expr} → name: expr
|
|
11
|
+
.replace(/(\w+)="([^"]*)"/g, '$1: "$2"') // name="str" → name: "str"
|
|
12
|
+
.replace(/(\w+)='([^']*)'/g, "$1: '$2'"); // name='str' → name: 'str'
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
/**
|
|
4
16
|
* Transforms Preact JSX patterns to React JSX patterns.
|
|
5
17
|
*
|
|
@@ -32,11 +44,23 @@ export function transformJsx(content: string): TransformResult {
|
|
|
32
44
|
changed = true;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
|
-
// onInput= → onChange=
|
|
47
|
+
// onInput= → onChange= (React onChange fires on every keystroke, like Preact onInput)
|
|
36
48
|
if (result.includes("onInput=")) {
|
|
37
|
-
result
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
if (result.includes("onChange=") && result.includes("onInput=")) {
|
|
50
|
+
// Both exist — remove onInput blocks to avoid duplicate JSX attributes.
|
|
51
|
+
// onInput is redundant in React since onChange already fires on every keystroke.
|
|
52
|
+
// Match onInput={...handler...} including multi-line arrow functions
|
|
53
|
+
result = result.replace(
|
|
54
|
+
/\s*onInput=\{[^{}]*(?:\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}[^{}]*)*\}/g,
|
|
55
|
+
""
|
|
56
|
+
);
|
|
57
|
+
changed = true;
|
|
58
|
+
notes.push("Removed onInput= (redundant with existing onChange= in React)");
|
|
59
|
+
} else {
|
|
60
|
+
result = result.replace(/onInput=/g, "onChange=");
|
|
61
|
+
changed = true;
|
|
62
|
+
notes.push("Replaced onInput= with onChange=");
|
|
63
|
+
}
|
|
40
64
|
}
|
|
41
65
|
|
|
42
66
|
// for= → htmlFor= in JSX (label elements)
|
|
@@ -229,6 +253,57 @@ export function transformJsx(content: string): TransformResult {
|
|
|
229
253
|
notes.push("Removed data-fresh-disable-lock attribute (Fresh-specific)");
|
|
230
254
|
}
|
|
231
255
|
|
|
256
|
+
// Replace <x.Component {...x.props} /> with <SectionRenderer section={x} />
|
|
257
|
+
// In TanStack Start, nested sections have Component as a string key, not a function.
|
|
258
|
+
// SectionRenderer from @decocms/start/hooks handles the lazy registry lookup.
|
|
259
|
+
//
|
|
260
|
+
// Gate on ANY variant of the .Component/.props pattern (simple, extra props, or multi-line).
|
|
261
|
+
const sectionPatternGate = /\.\s*Component[\s\n]+\{\.\.\.(\w+)\.props\}/;
|
|
262
|
+
if (sectionPatternGate.test(result)) {
|
|
263
|
+
// 1. Simple: <x.Component {...x.props} />
|
|
264
|
+
result = result.replace(
|
|
265
|
+
/<(\w+)\.Component\s+\{\.\.\.(\w+)\.props\}\s*\/>/g,
|
|
266
|
+
(_, v1) => `<SectionRenderer section={${v1}} />`,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// 2. Extra props: <x.Component {...x.props} page={page} />
|
|
270
|
+
// Convert JSX attrs to object entries: name={expr} → name: expr
|
|
271
|
+
result = result.replace(
|
|
272
|
+
/<(\w+)\.Component\s+\{\.\.\.(\w+)\.props\}\s+([^/]+?)\s*\/>/g,
|
|
273
|
+
(_, varName, _v2, extraJsx) => {
|
|
274
|
+
const objEntries = jsxAttrsToObjectEntries(extraJsx.trim());
|
|
275
|
+
return `<SectionRenderer section={{ ...${varName}, props: { ...${varName}.props, ${objEntries} } }} />`;
|
|
276
|
+
},
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// 3. Multi-line: <x.Component\n {...x.props}\n/>
|
|
280
|
+
result = result.replace(
|
|
281
|
+
/<(\w+)\.Component\s*\n\s*\{\.\.\.(\w+)\.props\}\s*\n\s*\/>/g,
|
|
282
|
+
(_, v1) => `<SectionRenderer section={${v1}} />`,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// 4. Multi-line with extra props: <x.Component\n {...x.props}\n extra={val}\n/>
|
|
286
|
+
result = result.replace(
|
|
287
|
+
/<(\w+)\.Component\s*\n\s*\{\.\.\.(\w+)\.props\}\s*\n\s*([^/]+?)\s*\n\s*\/>/g,
|
|
288
|
+
(_, varName, _v2, extraJsx) => {
|
|
289
|
+
const objEntries = jsxAttrsToObjectEntries(extraJsx.trim());
|
|
290
|
+
return `<SectionRenderer section={{ ...${varName}, props: { ...${varName}.props, ${objEntries} } }} />`;
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (!result.match(/^import\s.*SectionRenderer/m)) {
|
|
295
|
+
const hooksImportRe = /^import\s+\{([^}]*)\}\s+from\s+["']@decocms\/start\/hooks["'];?$/m;
|
|
296
|
+
const hooksMatch = result.match(hooksImportRe);
|
|
297
|
+
if (hooksMatch) {
|
|
298
|
+
result = result.replace(hooksImportRe, `import { ${hooksMatch[1].trim()}, SectionRenderer } from "@decocms/start/hooks";`);
|
|
299
|
+
} else {
|
|
300
|
+
result = `import { SectionRenderer } from "@decocms/start/hooks";\n${result}`;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
changed = true;
|
|
304
|
+
notes.push("Replaced .Component/.props section pattern with SectionRenderer");
|
|
305
|
+
}
|
|
306
|
+
|
|
232
307
|
// Ensure React import exists if we introduced React.* references
|
|
233
308
|
if (
|
|
234
309
|
(result.includes("React.") || result.includes("React,")) &&
|
|
@@ -7,6 +7,46 @@ import type { TransformResult, SectionMeta } from "../types.ts";
|
|
|
7
7
|
* These exports are read by generate-sections.ts in @decocms/start
|
|
8
8
|
* to build the sections.gen.ts registry.
|
|
9
9
|
*/
|
|
10
|
+
|
|
11
|
+
const EAGER_SYNC_SECTIONS = new Set([
|
|
12
|
+
"UtilLinks",
|
|
13
|
+
"DepartamentList",
|
|
14
|
+
"ImageGallery",
|
|
15
|
+
"BannersGrid",
|
|
16
|
+
"Carousel",
|
|
17
|
+
"Tipbar",
|
|
18
|
+
"Live",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SYNC_SECTIONS = new Set([
|
|
22
|
+
"ProductShelf",
|
|
23
|
+
"ProductShelfTabbed",
|
|
24
|
+
"ProductShelfGroup",
|
|
25
|
+
"ProductShelfTopSort",
|
|
26
|
+
"CouponList",
|
|
27
|
+
"NotFoundChallenge",
|
|
28
|
+
"MountedPDP",
|
|
29
|
+
"BackgroundWrapper",
|
|
30
|
+
"SearchResult",
|
|
31
|
+
"LpCartao",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const LISTING_CACHE_SECTIONS = new Set([
|
|
35
|
+
"ProductShelf",
|
|
36
|
+
"ProductShelfTabbed",
|
|
37
|
+
"ProductShelfGroup",
|
|
38
|
+
"ProductShelfTimedOffers",
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const STATIC_CACHE_SECTIONS = new Set([
|
|
42
|
+
"InstagramPosts",
|
|
43
|
+
"Faq",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
function getSectionBasename(filePath: string): string {
|
|
47
|
+
return filePath.split("/").pop()?.replace(/\.\w+$/, "") || "";
|
|
48
|
+
}
|
|
49
|
+
|
|
10
50
|
export function transformSectionConventions(
|
|
11
51
|
content: string,
|
|
12
52
|
sectionMeta: SectionMeta | undefined,
|
|
@@ -18,6 +58,7 @@ export function transformSectionConventions(
|
|
|
18
58
|
const notes: string[] = [];
|
|
19
59
|
let result = content;
|
|
20
60
|
let changed = false;
|
|
61
|
+
const basename = getSectionBasename(sectionMeta.path);
|
|
21
62
|
|
|
22
63
|
// Header, footer, theme → eager + sync + layout
|
|
23
64
|
if (sectionMeta.isHeader || sectionMeta.isFooter || sectionMeta.isTheme) {
|
|
@@ -31,14 +72,50 @@ export function transformSectionConventions(
|
|
|
31
72
|
notes.push("Added: export const sync = true");
|
|
32
73
|
changed = true;
|
|
33
74
|
}
|
|
34
|
-
|
|
75
|
+
// Header in golden does NOT have layout=true; only footer+theme do
|
|
76
|
+
if ((sectionMeta.isFooter || sectionMeta.isTheme) && !result.includes("export const layout")) {
|
|
35
77
|
result += "export const layout = true;\n";
|
|
36
78
|
notes.push("Added: export const layout = true");
|
|
37
79
|
changed = true;
|
|
38
80
|
}
|
|
39
81
|
}
|
|
40
82
|
|
|
41
|
-
//
|
|
83
|
+
// Known eager+sync sections (non-layout)
|
|
84
|
+
if (EAGER_SYNC_SECTIONS.has(basename)) {
|
|
85
|
+
if (!result.includes("export const eager")) {
|
|
86
|
+
result += "\nexport const eager = true;\n";
|
|
87
|
+
notes.push(`Added: export const eager = true (${basename})`);
|
|
88
|
+
changed = true;
|
|
89
|
+
}
|
|
90
|
+
if (!result.includes("export const sync")) {
|
|
91
|
+
result += "export const sync = true;\n";
|
|
92
|
+
notes.push(`Added: export const sync = true (${basename})`);
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Known sync-only sections
|
|
98
|
+
if (SYNC_SECTIONS.has(basename) && !result.includes("export const sync")) {
|
|
99
|
+
result += "\nexport const sync = true;\n";
|
|
100
|
+
notes.push(`Added: export const sync = true (${basename})`);
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Listing cache sections
|
|
105
|
+
if (LISTING_CACHE_SECTIONS.has(basename) && !result.includes("export const cache")) {
|
|
106
|
+
result += '\nexport const cache = "listing";\n';
|
|
107
|
+
notes.push(`Added: export const cache = "listing" (${basename})`);
|
|
108
|
+
changed = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Static cache sections
|
|
112
|
+
if (STATIC_CACHE_SECTIONS.has(basename) && !result.includes("export const cache")) {
|
|
113
|
+
result += '\nexport const cache = "static";\n';
|
|
114
|
+
notes.push(`Added: export const cache = "static" (${basename})`);
|
|
115
|
+
changed = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Generic: listing sections not already matched above
|
|
42
119
|
if (sectionMeta.isListing && !result.includes("export const cache")) {
|
|
43
120
|
result += '\nexport const cache = "listing";\n';
|
|
44
121
|
notes.push('Added: export const cache = "listing"');
|
|
@@ -52,9 +129,34 @@ export function transformSectionConventions(
|
|
|
52
129
|
changed = true;
|
|
53
130
|
}
|
|
54
131
|
|
|
132
|
+
// Sections that render nested Section children need sync so they're in
|
|
133
|
+
// the syncComponents registry (SectionRenderer resolves the string key).
|
|
134
|
+
const hasNestedSections =
|
|
135
|
+
/children:\s*Section\b/.test(result) || /fallback:\s*Section\b/.test(result);
|
|
136
|
+
if (hasNestedSections && !result.includes("export const sync")) {
|
|
137
|
+
result += "\nexport const sync = true;\n";
|
|
138
|
+
notes.push("Added: export const sync = true (renders nested Section children)");
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Re-export sections that wrap PDP/nested content need sync too.
|
|
143
|
+
// Detect: file is a re-export AND the target component renders nested Sections
|
|
144
|
+
const isReExport = /^export\s+\{[^}]*default[^}]*\}\s+from\s+/.test(result.trim());
|
|
145
|
+
if (isReExport && (basename === "MountedPDP" || basename === "NotFoundChallenge")) {
|
|
146
|
+
if (!result.includes("export const sync")) {
|
|
147
|
+
result += "\nexport const sync = true;\n";
|
|
148
|
+
notes.push(`Added: export const sync = true (re-export: ${basename})`);
|
|
149
|
+
changed = true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Don't add LoadingFallback re-exports to thin section files —
|
|
154
|
+
// we can't guarantee the target component exports it.
|
|
155
|
+
// Instead, if it's a listing section, a generic skeleton will be added below.
|
|
156
|
+
|
|
55
157
|
// Generate a basic LoadingFallback if the section doesn't have one
|
|
56
158
|
// and it's a listing section (visible skeleton improvement)
|
|
57
|
-
if (sectionMeta.isListing && !sectionMeta.hasLoadingFallback) {
|
|
159
|
+
if (sectionMeta.isListing && !sectionMeta.hasLoadingFallback && !result.includes("LoadingFallback")) {
|
|
58
160
|
result += `
|
|
59
161
|
export function LoadingFallback() {
|
|
60
162
|
return (
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -61,7 +61,9 @@ export type DetectedPattern =
|
|
|
61
61
|
| "asset-function"
|
|
62
62
|
| "head-component"
|
|
63
63
|
| "define-app"
|
|
64
|
-
| "invoke-proxy"
|
|
64
|
+
| "invoke-proxy"
|
|
65
|
+
| "use-component"
|
|
66
|
+
| "sections-component-loader";
|
|
65
67
|
|
|
66
68
|
/** Metadata extracted from a section file during analysis */
|
|
67
69
|
export interface SectionMeta {
|
|
@@ -131,6 +133,8 @@ export interface MigrationContext {
|
|
|
131
133
|
sourceDir: string;
|
|
132
134
|
siteName: string;
|
|
133
135
|
platform: Platform;
|
|
136
|
+
/** VTEX account name (e.g. "casaevideonewio") — extracted from source code */
|
|
137
|
+
vtexAccount: string | null;
|
|
134
138
|
gtmId: string | null;
|
|
135
139
|
|
|
136
140
|
/** deno.json import map entries */
|
|
@@ -151,6 +155,8 @@ export interface MigrationContext {
|
|
|
151
155
|
sectionMetas: SectionMeta[];
|
|
152
156
|
/** Island classifications */
|
|
153
157
|
islandClassifications: IslandClassification[];
|
|
158
|
+
/** Map from island path → wrapped component import path (for wrapper islands) */
|
|
159
|
+
islandWrapperTargets: Map<string, string>;
|
|
154
160
|
/** Loader inventory */
|
|
155
161
|
loaderInventory: LoaderInfo[];
|
|
156
162
|
|
|
@@ -191,6 +197,7 @@ export function createContext(
|
|
|
191
197
|
sourceDir,
|
|
192
198
|
siteName: "",
|
|
193
199
|
platform: "custom",
|
|
200
|
+
vtexAccount: null,
|
|
194
201
|
gtmId: null,
|
|
195
202
|
importMap: {},
|
|
196
203
|
discoveredNpmDeps: {},
|
|
@@ -199,6 +206,7 @@ export function createContext(
|
|
|
199
206
|
files: [],
|
|
200
207
|
sectionMetas: [],
|
|
201
208
|
islandClassifications: [],
|
|
209
|
+
islandWrapperTargets: new Map(),
|
|
202
210
|
loaderInventory: [],
|
|
203
211
|
scaffoldedFiles: [],
|
|
204
212
|
transformedFiles: [],
|
package/src/cms/resolve.ts
CHANGED
|
@@ -228,7 +228,18 @@ function isBot(userAgent?: string): boolean {
|
|
|
228
228
|
return botPatterns.some((re) => re.test(userAgent));
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
/**
|
|
232
|
+
* A loader registered against a `__resolveType` key. The runtime invokes it
|
|
233
|
+
* through two paths:
|
|
234
|
+
*
|
|
235
|
+
* 1. CMS resolution (`commerceLoader(resolvedProps)`) — 1-arg call.
|
|
236
|
+
* 2. `/deco/invoke/...` endpoint — `(props, request)` 2-arg call.
|
|
237
|
+
*
|
|
238
|
+
* Loaders that need the `Request` (cookies, geo, headers) declare the second
|
|
239
|
+
* parameter; pure loaders ignore it. This shape lets a single registry serve
|
|
240
|
+
* both invocation paths without `as any` casts at every wrapper.
|
|
241
|
+
*/
|
|
242
|
+
export type CommerceLoader = (props: any, request?: Request) => Promise<any>;
|
|
232
243
|
|
|
233
244
|
/**
|
|
234
245
|
* Context passed through the resolution pipeline.
|
package/src/sdk/useScript.ts
CHANGED
|
@@ -137,13 +137,34 @@ export function inlineScript(js: string) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
140
|
+
* @deprecated Removed in TanStack Start.
|
|
141
|
+
*
|
|
142
|
+
* The Fresh/Deno HTMX-based partial-section pattern (`useSection` /
|
|
143
|
+
* `usePartialSection` + `sections/Component.tsx`) does not apply on
|
|
144
|
+
* Cloudflare Workers and React. Replace call-sites with one of:
|
|
145
|
+
*
|
|
146
|
+
* 1. Local React state (`useState` + event handlers) for client-side toggles.
|
|
147
|
+
* 2. `createServerFn` + `useMutation` for server actions.
|
|
148
|
+
* 3. Direct `invoke` calls (`~/server/invoke`) for ad-hoc loaders.
|
|
149
|
+
*
|
|
150
|
+
* See: deco-to-tanstack-migration skill, "useComponent / partial sections"
|
|
151
|
+
* section, for the per-pattern recipes.
|
|
152
|
+
*
|
|
153
|
+
* Both stubs throw at runtime (and at import time, if you call them at
|
|
154
|
+
* module top level) so legacy code surfaces a clear error instead of a
|
|
155
|
+
* silent no-op.
|
|
142
156
|
*/
|
|
143
|
-
|
|
144
|
-
|
|
157
|
+
const DEPRECATION_MESSAGE =
|
|
158
|
+
"[@decocms/start] useSection / usePartialSection were removed. " +
|
|
159
|
+
"The Fresh/Deno HTMX partial-section pattern does not apply on " +
|
|
160
|
+
"TanStack Start / Cloudflare Workers. Replace call-sites with " +
|
|
161
|
+
"createServerFn + useMutation, or local React state. See the " +
|
|
162
|
+
"deco-to-tanstack-migration skill for per-pattern recipes.";
|
|
163
|
+
|
|
164
|
+
export function usePartialSection(_props?: Record<string, unknown>): never {
|
|
165
|
+
throw new Error(DEPRECATION_MESSAGE);
|
|
145
166
|
}
|
|
146
167
|
|
|
147
|
-
export function useSection(_props?: Record<string, unknown>) {
|
|
148
|
-
|
|
168
|
+
export function useSection(_props?: Record<string, unknown>): never {
|
|
169
|
+
throw new Error(DEPRECATION_MESSAGE);
|
|
149
170
|
}
|
package/src/sdk/workerEntry.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
} from "./cacheHeaders";
|
|
35
35
|
import { buildHtmlShell } from "./htmlShell";
|
|
36
36
|
import { cleanPathForCacheKey } from "./urlUtils";
|
|
37
|
-
import { isMobileUA } from "./useDevice";
|
|
37
|
+
import { type Device, isMobileUA } from "./useDevice";
|
|
38
38
|
import { getRenderShellConfig } from "../admin/setup";
|
|
39
39
|
import { RequestContext } from "./requestContext";
|
|
40
40
|
import { getAppMiddleware } from "./setupApps";
|
|
@@ -88,7 +88,16 @@ interface ServerEntry {
|
|
|
88
88
|
* cache entry; different segments get different cached responses.
|
|
89
89
|
*/
|
|
90
90
|
export interface SegmentKey {
|
|
91
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Device class derived from the request User-Agent.
|
|
93
|
+
*
|
|
94
|
+
* Accepts the full `Device` union (`"mobile" | "desktop" | "tablet"`) so
|
|
95
|
+
* that callers can pass `detectDevice(...)` directly without manual
|
|
96
|
+
* narrowing. Sites that want to share cache entries between mobile and
|
|
97
|
+
* tablet can collapse the value at the call site (e.g.
|
|
98
|
+
* `device === "tablet" ? "mobile" : device`).
|
|
99
|
+
*/
|
|
100
|
+
device: Device;
|
|
92
101
|
/** Whether the user is logged in (e.g., has a valid auth cookie). */
|
|
93
102
|
loggedIn?: boolean;
|
|
94
103
|
/** Commerce sales channel / price list. */
|
package/src/setup.ts
CHANGED
|
@@ -96,7 +96,7 @@ export interface SiteSetupOptions {
|
|
|
96
96
|
* { getCommerceLoaders: () => COMMERCE_LOADERS }
|
|
97
97
|
* ```
|
|
98
98
|
*/
|
|
99
|
-
getCommerceLoaders?: () => Record<string, (props: any) => Promise<any>>;
|
|
99
|
+
getCommerceLoaders?: () => Record<string, (props: any, request?: Request) => Promise<any>>;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|