@decocms/start 1.6.2 → 1.6.3
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/package.json +1 -1
- package/scripts/generate-blocks.ts +8 -5
- package/scripts/migrate/analyzers/island-classifier.ts +23 -0
- package/scripts/migrate/analyzers/section-metadata.ts +63 -7
- package/scripts/migrate/phase-analyze.ts +136 -11
- package/scripts/migrate/phase-cleanup.ts +1057 -6
- package/scripts/migrate/phase-scaffold.ts +294 -5
- package/scripts/migrate/phase-transform.ts +14 -3
- package/scripts/migrate/templates/app-css.ts +149 -2
- package/scripts/migrate/templates/commerce-loaders.ts +173 -68
- 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 +365 -32
- 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 +6 -0
|
@@ -16,7 +16,8 @@ export function generateUiComponents(_ctx: MigrationContext): Record<string, str
|
|
|
16
16
|
} from "@decocms/apps/commerce/components/Image";
|
|
17
17
|
`;
|
|
18
18
|
|
|
19
|
-
files["src/components/ui/Picture.tsx"] = `import {
|
|
19
|
+
files["src/components/ui/Picture.tsx"] = `import type { ReactNode } from "react";
|
|
20
|
+
import {
|
|
20
21
|
Image,
|
|
21
22
|
getSrcSet,
|
|
22
23
|
type FitOptions,
|
|
@@ -36,6 +37,19 @@ export interface PictureProps extends Omit<ImageProps, "sizes"> {
|
|
|
36
37
|
sources: PictureSourceProps[];
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export function Source(props: PictureSourceProps & { fit?: FitOptions }) {
|
|
41
|
+
const srcSet = getSrcSet(props.src, props.width, props.height, props.fit ?? "cover");
|
|
42
|
+
return (
|
|
43
|
+
<source
|
|
44
|
+
srcSet={srcSet}
|
|
45
|
+
media={props.media}
|
|
46
|
+
width={props.width}
|
|
47
|
+
height={props.height}
|
|
48
|
+
sizes={props.sizes ?? \`\${props.width}px\`}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
export function Picture({
|
|
40
54
|
sources,
|
|
41
55
|
src,
|
|
@@ -43,24 +57,15 @@ export function Picture({
|
|
|
43
57
|
height,
|
|
44
58
|
fit = "cover",
|
|
45
59
|
preload,
|
|
60
|
+
children,
|
|
46
61
|
...rest
|
|
47
|
-
}: PictureProps) {
|
|
62
|
+
}: PictureProps & { children?: ReactNode }) {
|
|
48
63
|
return (
|
|
49
64
|
<picture>
|
|
50
|
-
{sources
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
key={i}
|
|
55
|
-
srcSet={srcSet}
|
|
56
|
-
media={source.media}
|
|
57
|
-
width={source.width}
|
|
58
|
-
height={source.height}
|
|
59
|
-
sizes={source.sizes ?? \`\${source.width}px\`}
|
|
60
|
-
/>
|
|
61
|
-
);
|
|
62
|
-
})}
|
|
63
|
-
<Image src={src} width={width} height={height} fit={fit} preload={preload} {...rest} />
|
|
65
|
+
{children ?? sources?.map((source, i) => (
|
|
66
|
+
<Source key={i} {...source} fit={source.fit ?? fit} />
|
|
67
|
+
))}
|
|
68
|
+
{src && <Image src={src} width={width} height={height} fit={fit} preload={preload} {...rest} />}
|
|
64
69
|
</picture>
|
|
65
70
|
);
|
|
66
71
|
}
|
|
@@ -107,6 +112,32 @@ export default function Video({
|
|
|
107
112
|
/>
|
|
108
113
|
);
|
|
109
114
|
}
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
files["src/components/ui/Seo.tsx"] = `export interface Props {
|
|
118
|
+
title?: string;
|
|
119
|
+
description?: string;
|
|
120
|
+
canonical?: string;
|
|
121
|
+
image?: string;
|
|
122
|
+
noIndexing?: boolean;
|
|
123
|
+
jsonLDs?: unknown[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export default function Seo({ jsonLDs }: Props) {
|
|
127
|
+
if (!jsonLDs?.length) return null;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
{jsonLDs.map((jsonLD, i) => (
|
|
132
|
+
<script
|
|
133
|
+
key={i}
|
|
134
|
+
type="application/ld+json"
|
|
135
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLD) }}
|
|
136
|
+
/>
|
|
137
|
+
))}
|
|
138
|
+
</>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
110
141
|
`;
|
|
111
142
|
|
|
112
143
|
return files;
|
|
@@ -3,21 +3,30 @@ import type { MigrationContext } from "../types.ts";
|
|
|
3
3
|
export function generateViteConfig(ctx: MigrationContext): string {
|
|
4
4
|
const isVtex = ctx.platform === "vtex";
|
|
5
5
|
|
|
6
|
+
const vtexAccount = ctx.vtexAccount || ctx.siteName.replace(/-migrated$/, "").replace(/-storefront$/, "");
|
|
7
|
+
|
|
6
8
|
const vtexProxy = isVtex ? `
|
|
7
9
|
// VTEX API proxy for local development
|
|
8
10
|
proxy: {
|
|
9
11
|
"/api/": {
|
|
10
|
-
target:
|
|
12
|
+
target: VTEX_ORIGIN,
|
|
11
13
|
changeOrigin: true,
|
|
12
|
-
|
|
14
|
+
cookieDomainRewrite: { "*": "" },
|
|
13
15
|
},
|
|
14
|
-
"/checkout
|
|
15
|
-
target:
|
|
16
|
+
"/checkout": {
|
|
17
|
+
target: VTEX_ORIGIN,
|
|
16
18
|
changeOrigin: true,
|
|
17
|
-
|
|
19
|
+
cookieDomainRewrite: { "*": "" },
|
|
18
20
|
},
|
|
19
21
|
},` : "";
|
|
20
22
|
|
|
23
|
+
const vtexConstants = isVtex ? `
|
|
24
|
+
const VTEX_ACCOUNT = "${vtexAccount}";
|
|
25
|
+
const VTEX_ENVIRONMENT = "vtexcommercestable";
|
|
26
|
+
const VTEX_DOMAIN = "com.br";
|
|
27
|
+
const VTEX_ORIGIN = \`https://\${VTEX_ACCOUNT}.\${VTEX_ENVIRONMENT}.\${VTEX_DOMAIN}\`;
|
|
28
|
+
` : "";
|
|
29
|
+
|
|
21
30
|
return `import { cloudflare } from "@cloudflare/vite-plugin";
|
|
22
31
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
23
32
|
import { decoVitePlugin } from "@decocms/start/vite";
|
|
@@ -27,7 +36,7 @@ import { defineConfig } from "vite";
|
|
|
27
36
|
import path from "path";
|
|
28
37
|
|
|
29
38
|
const srcDir = path.resolve(__dirname, "src");
|
|
30
|
-
|
|
39
|
+
${vtexConstants}
|
|
31
40
|
export default defineConfig({
|
|
32
41
|
server: {
|
|
33
42
|
allowedHosts: [".decocdn.com"],${vtexProxy}
|
|
@@ -106,6 +115,8 @@ export default defineConfig({
|
|
|
106
115
|
},
|
|
107
116
|
resolve: {
|
|
108
117
|
dedupe: [
|
|
118
|
+
"@decocms/start",
|
|
119
|
+
"@decocms/apps",
|
|
109
120
|
"@tanstack/react-start",
|
|
110
121
|
"@tanstack/react-router",
|
|
111
122
|
"@tanstack/react-start-server",
|
|
@@ -8,12 +8,14 @@ export function generateWrangler(ctx: MigrationContext): string {
|
|
|
8
8
|
|
|
9
9
|
return `{
|
|
10
10
|
"name": "${workerName}-tanstack",
|
|
11
|
+
// TODO: Set your Cloudflare account_id for deployment
|
|
12
|
+
// "account_id": "YOUR_ACCOUNT_ID",
|
|
11
13
|
"compatibility_date": "2026-02-14",
|
|
12
14
|
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
|
|
13
15
|
"main": "./src/worker-entry.ts",
|
|
14
16
|
"workers_dev": true,
|
|
15
17
|
"preview_urls": true,
|
|
16
|
-
// Uncomment for redirect/AB testing
|
|
18
|
+
// Uncomment and set KV namespace ID for redirect/AB testing:
|
|
17
19
|
// "kv_namespaces": [
|
|
18
20
|
// { "binding": "SITES_KV", "id": "YOUR_KV_NAMESPACE_ID" }
|
|
19
21
|
// ],
|
|
@@ -57,7 +57,114 @@ function removeExportConstBlock(src: string, name: string): string {
|
|
|
57
57
|
return src; // unbalanced braces, don't touch
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
const ALL_PLATFORMS = ["vtex", "shopify", "linx", "vnda", "wake", "nuvemshop"];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Strip imports, JSX conditionals, and if-blocks for non-active platforms.
|
|
64
|
+
* E.g. on a VTEX site, remove all shopify/linx/vnda/wake/nuvemshop branches.
|
|
65
|
+
*/
|
|
66
|
+
function stripNonPlatformCode(src: string, platform: string): { content: string; stripped: boolean } {
|
|
67
|
+
const deadPlatforms = ALL_PLATFORMS.filter((p) => p !== platform);
|
|
68
|
+
if (deadPlatforms.length === 0) return { content: src, stripped: false };
|
|
69
|
+
|
|
70
|
+
const deadRe = new RegExp(`\\b(${deadPlatforms.join("|")})\\b`, "i");
|
|
71
|
+
if (!deadRe.test(src)) return { content: src, stripped: false };
|
|
72
|
+
|
|
73
|
+
let result = src;
|
|
74
|
+
const removedIdentifiers: string[] = [];
|
|
75
|
+
|
|
76
|
+
// 1. Remove import lines referencing dead platforms
|
|
77
|
+
const importLineRe = /^import\s+(\w+)\s+from\s+["'][^"']*["'];?\s*$/gm;
|
|
78
|
+
result = result.replace(importLineRe, (line, ident) => {
|
|
79
|
+
if (deadRe.test(line)) {
|
|
80
|
+
removedIdentifiers.push(ident);
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
return line;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Also handle: import type { X } from "path/platform"
|
|
87
|
+
const importTypeLineRe = /^import\s+type\s+\{[^}]*\}\s+from\s+["'][^"']*["'];?\s*$/gm;
|
|
88
|
+
result = result.replace(importTypeLineRe, (line) => {
|
|
89
|
+
if (deadRe.test(line)) return "";
|
|
90
|
+
return line;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Handle lazy dynamic imports: const X = lazy(() => import("./platform/Cart"));
|
|
94
|
+
const lazyImportRe = /^const\s+(\w+)\s*=\s*lazy\(\s*\(\)\s*=>\s*import\(["'][^"']*["']\)\s*\);?\s*$/gm;
|
|
95
|
+
result = result.replace(lazyImportRe, (line, ident) => {
|
|
96
|
+
if (deadRe.test(line)) {
|
|
97
|
+
removedIdentifiers.push(ident);
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
return line;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 2. Remove JSX conditionals: {platform === "shopify" && (...)} using paren-counting
|
|
104
|
+
for (const dp of deadPlatforms) {
|
|
105
|
+
const jsxPatternStr = `\\{\\s*platform\\s*===\\s*["']${dp}["']\\s*&&\\s*\\(`;
|
|
106
|
+
const jsxPattern = new RegExp(jsxPatternStr);
|
|
107
|
+
let match: RegExpExecArray | null;
|
|
108
|
+
while ((match = jsxPattern.exec(result)) !== null) {
|
|
109
|
+
const start = match.index;
|
|
110
|
+
// Find the opening paren after &&
|
|
111
|
+
let pos = result.indexOf("(", match.index + match[0].length - 1);
|
|
112
|
+
if (pos === -1) break;
|
|
113
|
+
let depth = 0;
|
|
114
|
+
for (; pos < result.length; pos++) {
|
|
115
|
+
if (result[pos] === "(") depth++;
|
|
116
|
+
else if (result[pos] === ")") {
|
|
117
|
+
depth--;
|
|
118
|
+
if (depth === 0) break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// pos is at the closing paren. Find closing }
|
|
122
|
+
let end = pos + 1;
|
|
123
|
+
while (end < result.length && /\s/.test(result[end])) end++;
|
|
124
|
+
if (end < result.length && result[end] === "}") end++;
|
|
125
|
+
// Remove trailing newline
|
|
126
|
+
if (end < result.length && result[end] === "\n") end++;
|
|
127
|
+
result = result.slice(0, start) + result.slice(end);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. Remove if-blocks: if (platform === "shopify") { ... }
|
|
132
|
+
for (const dp of deadPlatforms) {
|
|
133
|
+
const ifPatternStr = `if\\s*\\(\\s*platform\\s*===\\s*["']${dp}["']\\s*\\)\\s*\\{`;
|
|
134
|
+
const ifPattern = new RegExp(ifPatternStr);
|
|
135
|
+
let match: RegExpExecArray | null;
|
|
136
|
+
while ((match = ifPattern.exec(result)) !== null) {
|
|
137
|
+
const start = match.index;
|
|
138
|
+
let pos = result.indexOf("{", match.index);
|
|
139
|
+
if (pos === -1) break;
|
|
140
|
+
let depth = 0;
|
|
141
|
+
for (; pos < result.length; pos++) {
|
|
142
|
+
if (result[pos] === "{") depth++;
|
|
143
|
+
else if (result[pos] === "}") {
|
|
144
|
+
depth--;
|
|
145
|
+
if (depth === 0) break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
let end = pos + 1;
|
|
149
|
+
if (end < result.length && result[end] === "\n") end++;
|
|
150
|
+
result = result.slice(0, start) + result.slice(end);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 4. Remove JSX references to removed identifiers: <RemovedIdent ... />
|
|
155
|
+
for (const ident of removedIdentifiers) {
|
|
156
|
+
// Self-closing: <Ident ... />
|
|
157
|
+
const selfClose = new RegExp(`\\s*<${ident}\\b[^>]*/>`, "g");
|
|
158
|
+
result = result.replace(selfClose, "");
|
|
159
|
+
// Open/close pair (rare for platform buttons, but handle it)
|
|
160
|
+
const openClose = new RegExp(`\\s*<${ident}\\b[^>]*>[\\s\\S]*?</${ident}>`, "g");
|
|
161
|
+
result = result.replace(openClose, "");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { content: result, stripped: result !== src };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function transformDeadCode(content: string, platform?: string): TransformResult {
|
|
61
168
|
const notes: string[] = [];
|
|
62
169
|
let changed = false;
|
|
63
170
|
let result = content;
|
|
@@ -118,9 +225,228 @@ export function transformDeadCode(content: string): TransformResult {
|
|
|
118
225
|
notes.push("Replaced logger.* with console.* (logger from @deco/deco/o11y removed)");
|
|
119
226
|
}
|
|
120
227
|
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
228
|
+
// Remove re-exports of framework-only SEO Preview components
|
|
229
|
+
const seoPreviewRe = /^export\s*\{[^}]*\}\s*from\s*["'][^"']*_seo[^"']*["'];?\s*$/gm;
|
|
230
|
+
if (seoPreviewRe.test(result)) {
|
|
231
|
+
result = result.replace(seoPreviewRe, "");
|
|
232
|
+
changed = true;
|
|
233
|
+
notes.push("Removed re-export of _seo/Preview (framework-only component)");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Replace imports of removed framework APIs with inline stubs
|
|
237
|
+
const removedApis: Array<{ import: RegExp; replacement: string; note: string }> = [
|
|
238
|
+
{
|
|
239
|
+
import: /import\s+\{([^}]*)\bgetSegmentFromBag\b([^}]*)\}\s+from\s+["'][^"']*segment["'];?\n?/,
|
|
240
|
+
replacement: `const getSegmentFromBag = (_ctx: any) => ({ value: {} as any });\n`,
|
|
241
|
+
note: "Stubbed getSegmentFromBag (Deco bag API removed — uses empty segment)",
|
|
242
|
+
},
|
|
243
|
+
// createHttpClient is now provided by ~/lib/http-utils (generated during scaffold)
|
|
244
|
+
// — no need to remove it here; the import rewrite handles it.
|
|
245
|
+
{
|
|
246
|
+
import: /import\s+\{([^}]*)\bfetchSafe\b([^}]*)\}\s+from\s+["'][^"']*["'];?\n?/,
|
|
247
|
+
replacement: `const fetchSafe = async (url: string | URL | Request, init?: RequestInit) => { const r = await fetch(url, init); if (!r.ok) throw new Error(\`fetchSafe: \${r.status}\`); return r; };\n`,
|
|
248
|
+
note: "Stubbed fetchSafe (old Deco fetch utility — using native fetch with error check)",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
import: /import\s+\{([^}]*)\bgetISCookiesFromBag\b([^}]*)\}\s+from\s+["'][^"']*intelligentSearch["'];?\n?/,
|
|
252
|
+
replacement: `const getISCookiesFromBag = (_ctx: any) => ({});\n`,
|
|
253
|
+
note: "Stubbed getISCookiesFromBag (Deco bag API removed — returns empty cookies)",
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
|
|
257
|
+
for (const api of removedApis) {
|
|
258
|
+
if (api.import.test(result)) {
|
|
259
|
+
const match = result.match(api.import);
|
|
260
|
+
if (match) {
|
|
261
|
+
const otherImports = (match[1] + match[2])
|
|
262
|
+
.split(",")
|
|
263
|
+
.map((s: string) => s.trim())
|
|
264
|
+
.filter((s: string) => s && s !== "getSegmentFromBag");
|
|
265
|
+
let importLine = "";
|
|
266
|
+
if (otherImports.length > 0) {
|
|
267
|
+
const fromMatch = match[0].match(/from\s+["']([^"']+)["']/);
|
|
268
|
+
const fromPath = fromMatch ? fromMatch[1] : "";
|
|
269
|
+
importLine = `import { ${otherImports.join(", ")} } from "${fromPath}";\n`;
|
|
270
|
+
}
|
|
271
|
+
result = result.replace(api.import, importLine + api.replacement);
|
|
272
|
+
changed = true;
|
|
273
|
+
notes.push(api.note);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Strip code for non-active platforms (e.g. remove shopify/linx imports on VTEX site)
|
|
279
|
+
if (platform) {
|
|
280
|
+
const platformResult = stripNonPlatformCode(result, platform);
|
|
281
|
+
if (platformResult.stripped) {
|
|
282
|
+
result = platformResult.content;
|
|
283
|
+
changed = true;
|
|
284
|
+
notes.push(`Stripped non-${platform} platform code`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Remove usePlatform() — the hook and all platform prop threading.
|
|
288
|
+
// On a single-platform site the golden reference removes it entirely:
|
|
289
|
+
// - `const platform = usePlatform()` → deleted
|
|
290
|
+
// - `{platform === "vtex" && (<X />)}` → `<X />`
|
|
291
|
+
// - `platform={platform}` / `platform={usePlatform()}` JSX attrs → deleted
|
|
292
|
+
// - `platform: ReturnType<typeof usePlatform>` in types → deleted
|
|
293
|
+
// - `platform,` in destructuring / param lists → deleted
|
|
294
|
+
if (result.includes("usePlatform")) {
|
|
295
|
+
const before = result;
|
|
296
|
+
|
|
297
|
+
// a) Remove `const platform = usePlatform();` declarations
|
|
298
|
+
result = result.replace(/^\s*const\s+platform\s*=\s*usePlatform\(\);?\s*\n?/gm, "");
|
|
299
|
+
|
|
300
|
+
// b) Collapse JSX conditionals for the ACTIVE platform:
|
|
301
|
+
// {platform === "vtex" && (<Component .../>)} → <Component .../>
|
|
302
|
+
// {platform === "vtex" && <Component .../> } → <Component .../>
|
|
303
|
+
const activeJsxParen = new RegExp(
|
|
304
|
+
`\\{\\s*platform\\s*===\\s*["']${platform}["']\\s*&&\\s*\\(([\\s\\S]*?)\\)\\s*\\}`,
|
|
305
|
+
"g",
|
|
306
|
+
);
|
|
307
|
+
result = result.replace(activeJsxParen, (_m, inner) => inner.trim());
|
|
308
|
+
|
|
309
|
+
const activeJsxNoParen = new RegExp(
|
|
310
|
+
`\\{\\s*platform\\s*===\\s*["']${platform}["']\\s*&&\\s*(<[^{}]*/>)\\s*\\}`,
|
|
311
|
+
"g",
|
|
312
|
+
);
|
|
313
|
+
result = result.replace(activeJsxNoParen, (_m, jsx) => jsx.trim());
|
|
314
|
+
|
|
315
|
+
// c) Remove platform JSX attributes: platform={platform}, platform={usePlatform()}
|
|
316
|
+
result = result.replace(/\s+platform=\{usePlatform\(\)\}/g, "");
|
|
317
|
+
result = result.replace(/\s+platform=\{platform\}/g, "");
|
|
318
|
+
|
|
319
|
+
// d) Remove platform from type definitions / interfaces
|
|
320
|
+
// platform: ReturnType<typeof usePlatform>;
|
|
321
|
+
result = result.replace(/^\s*platform\s*:\s*ReturnType<typeof usePlatform>;?\s*\n?/gm, "");
|
|
322
|
+
// platform: Platform; (when from ~/apps/site.ts)
|
|
323
|
+
result = result.replace(/^\s*platform\??\s*:\s*Platform;?\s*\n?/gm, "");
|
|
324
|
+
// platform?: string;
|
|
325
|
+
result = result.replace(/^\s*platform\??\s*:\s*string;?\s*\n?/gm, "");
|
|
326
|
+
|
|
327
|
+
// e) Remove `platform` from Omit<..., "platform"> → just the base type
|
|
328
|
+
result = result.replace(/Omit<(\w+),\s*["']platform["']>/g, "$1");
|
|
329
|
+
|
|
330
|
+
// f) Clean up remaining references in destructuring and param lists
|
|
331
|
+
// { platform, ...rest } → { ...rest } (or just remove the comma'd entry)
|
|
332
|
+
result = result.replace(/\bplatform\s*,\s*/g, "");
|
|
333
|
+
result = result.replace(/,\s*platform\b/g, "");
|
|
334
|
+
|
|
335
|
+
if (result !== before) {
|
|
336
|
+
changed = true;
|
|
337
|
+
notes.push(`Removed usePlatform() — collapsed to ${platform}-only code`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ── Module-scope React hook calls ────────────────────────────────────
|
|
343
|
+
// In Preact + signals, hooks like useUser()/useCart() can be called at module
|
|
344
|
+
// scope because they return signals (no component context needed).
|
|
345
|
+
// In React, hooks MUST be inside components. Detect and move them.
|
|
346
|
+
//
|
|
347
|
+
// Pattern: `const { a, b: alias } = useHookName();` at the top level
|
|
348
|
+
// (not indented, not inside a function body).
|
|
349
|
+
const moduleScopeHookRe = /^(const\s+\{([^}]+)\}\s*=\s*(use\w+)\(\);?\s*$)/gm;
|
|
350
|
+
const moduleScopeHooks: Array<{
|
|
351
|
+
fullMatch: string;
|
|
352
|
+
vars: string[];
|
|
353
|
+
hookCall: string;
|
|
354
|
+
hookName: string;
|
|
355
|
+
}> = [];
|
|
356
|
+
|
|
357
|
+
let hookMatch: RegExpExecArray | null;
|
|
358
|
+
while ((hookMatch = moduleScopeHookRe.exec(result)) !== null) {
|
|
359
|
+
const line = hookMatch[1];
|
|
360
|
+
const destructured = hookMatch[2];
|
|
361
|
+
const hookName = hookMatch[3];
|
|
362
|
+
|
|
363
|
+
// Skip non-React hooks (useAccount from ~/utils/sitename is just a function,
|
|
364
|
+
// useUI returns signals — both are safe at module scope)
|
|
365
|
+
const knownUnsafeHooks = ["useUser", "useCart", "useWishlist"];
|
|
366
|
+
if (!knownUnsafeHooks.includes(hookName)) continue;
|
|
367
|
+
|
|
368
|
+
const vars = destructured
|
|
369
|
+
.split(",")
|
|
370
|
+
.map((v) => {
|
|
371
|
+
const trimmed = v.trim();
|
|
372
|
+
const aliasMatch = trimmed.match(/(\w+)\s*:\s*(\w+)/);
|
|
373
|
+
return aliasMatch ? aliasMatch[2] : trimmed;
|
|
374
|
+
})
|
|
375
|
+
.filter(Boolean);
|
|
376
|
+
|
|
377
|
+
moduleScopeHooks.push({
|
|
378
|
+
fullMatch: line,
|
|
379
|
+
vars,
|
|
380
|
+
hookCall: `const { ${destructured.trim()} } = ${hookName}();`,
|
|
381
|
+
hookName,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (moduleScopeHooks.length > 0) {
|
|
386
|
+
for (const hook of moduleScopeHooks) {
|
|
387
|
+
// Remove the module-scope line
|
|
388
|
+
result = result.replace(hook.fullMatch + "\n", "");
|
|
389
|
+
result = result.replace(hook.fullMatch, "");
|
|
390
|
+
|
|
391
|
+
// Find exported component functions and locate their body opener.
|
|
392
|
+
// Arrow: `export const Foo = (...) => {` — body starts after `=> {`
|
|
393
|
+
// Function: `export function Foo(...) {` — body starts after `) {`
|
|
394
|
+
const insertions: Array<{ index: number; hookCall: string }> = [];
|
|
395
|
+
|
|
396
|
+
// Arrow function body openers: "=> {"
|
|
397
|
+
const arrowBodyRe = /=>\s*\{/g;
|
|
398
|
+
// Function body openers: line starting with export ... function ... ) {
|
|
399
|
+
const funcDeclRe = /^export\s+(?:default\s+)?function\s+\w+/gm;
|
|
400
|
+
|
|
401
|
+
// Strategy: find all `=> {` and `) {` that follow an export declaration,
|
|
402
|
+
// then check the body for variable references.
|
|
403
|
+
const lines = result.split("\n");
|
|
404
|
+
let inExportDecl = false;
|
|
405
|
+
let charOffset = 0;
|
|
406
|
+
|
|
407
|
+
for (let i = 0; i < lines.length; i++) {
|
|
408
|
+
const line = lines[i];
|
|
409
|
+
|
|
410
|
+
// Track exported function/const declarations
|
|
411
|
+
if (/^export\s+(default\s+)?(function|const)\s+\w+/.test(line)) {
|
|
412
|
+
inExportDecl = true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (inExportDecl) {
|
|
416
|
+
// Look for body opener: `=> {` or `) {` (for function declarations)
|
|
417
|
+
let bodyOpenerIdx = -1;
|
|
418
|
+
const arrowMatch = line.match(/=>\s*\{/);
|
|
419
|
+
const funcMatch = line.match(/\)\s*\{$/);
|
|
420
|
+
|
|
421
|
+
if (arrowMatch && arrowMatch.index !== undefined) {
|
|
422
|
+
bodyOpenerIdx = charOffset + arrowMatch.index + arrowMatch[0].length;
|
|
423
|
+
} else if (funcMatch && funcMatch.index !== undefined) {
|
|
424
|
+
bodyOpenerIdx = charOffset + funcMatch.index + funcMatch[0].length;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (bodyOpenerIdx >= 0) {
|
|
428
|
+
inExportDecl = false;
|
|
429
|
+
const bodySlice = result.slice(bodyOpenerIdx, bodyOpenerIdx + 3000);
|
|
430
|
+
const usesVars = hook.vars.some((v) => new RegExp(`\\b${v}\\b`).test(bodySlice));
|
|
431
|
+
if (usesVars) {
|
|
432
|
+
insertions.push({ index: bodyOpenerIdx, hookCall: `\n ${hook.hookCall}` });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
charOffset += line.length + 1; // +1 for \n
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
for (const ins of insertions.reverse()) {
|
|
441
|
+
result = result.slice(0, ins.index) + ins.hookCall + result.slice(ins.index);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (insertions.length > 0) {
|
|
445
|
+
changed = true;
|
|
446
|
+
notes.push(`Moved module-scope ${hook.hookName}() into ${insertions.length} component(s)`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
124
450
|
|
|
125
451
|
// Clean up blank lines
|
|
126
452
|
result = result.replace(/\n{3,}/g, "\n\n");
|
|
@@ -45,6 +45,25 @@ export function transformDenoIsms(content: string): TransformResult {
|
|
|
45
45
|
notes.push("Replaced @ts-ignore with @ts-expect-error");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Remove jsdom/dompurify — these don't work on Cloudflare Workers.
|
|
49
|
+
// CMS rich-text content is trusted, so sanitization is unnecessary.
|
|
50
|
+
if (/from\s+["']jsdom["']/.test(result) || /from\s+["']dompurify["']/.test(result)) {
|
|
51
|
+
result = result.replace(/^import\s+.*from\s+["']jsdom["'];?\s*\n?/gm, "");
|
|
52
|
+
result = result.replace(/^import\s+.*from\s+["']dompurify["'];?\s*\n?/gm, "");
|
|
53
|
+
// Replace JSDOM + DOMPurify sanitization pattern with pass-through
|
|
54
|
+
result = result.replace(
|
|
55
|
+
/const\s+window\s*=\s*new\s+JSDOM\([^)]*\)\.window;\s*\n\s*const\s+DOMPurifyServer\s*=\s*DOMPurify\(window\);\s*\n\s*const\s+(\w+)\s*=\s*DOMPurifyServer\.sanitize\([^)]+\);/g,
|
|
56
|
+
"const $1 = text;",
|
|
57
|
+
);
|
|
58
|
+
// Simpler pattern: const sanitized = DOMPurify.sanitize(text)
|
|
59
|
+
result = result.replace(
|
|
60
|
+
/const\s+(\w+)\s*=\s*DOMPurify(?:Server)?\.sanitize\([^)]+\);/g,
|
|
61
|
+
"const $1 = text;",
|
|
62
|
+
);
|
|
63
|
+
changed = true;
|
|
64
|
+
notes.push("Stripped jsdom/dompurify — CMS content is trusted on Workers");
|
|
65
|
+
}
|
|
66
|
+
|
|
48
67
|
// Remove Deno.* API usages — flag for manual review
|
|
49
68
|
if (result.includes("Deno.")) {
|
|
50
69
|
notes.push("MANUAL: Deno.* API usage found — needs Node.js equivalent");
|