@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,455 @@
|
|
|
1
|
+
import type { TransformResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes dead code patterns from the old Deco stack that don't work
|
|
5
|
+
* in TanStack Start:
|
|
6
|
+
*
|
|
7
|
+
* - `export const cache = "stale-while-revalidate"` (old cache system)
|
|
8
|
+
* - `export const cacheKey = ...` (old cache key generation)
|
|
9
|
+
* - `crypto.subtle.digestSync(...)` (Deno-only sync API)
|
|
10
|
+
*
|
|
11
|
+
* NOTE: `export const loader` is kept — it's a server-side function the CMS calls.
|
|
12
|
+
* NOTE: invoke.* calls are NOT migrated — they are RPC calls to the server
|
|
13
|
+
* where the CMS config (API keys, etc.) is available. The runtime.ts invoke
|
|
14
|
+
* proxy handles routing them to /deco/invoke/*.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Remove an `export const <name> = ...` block using brace-counting
|
|
18
|
+
* so nested `{}` (for loops, if/else) don't cause premature truncation.
|
|
19
|
+
*/
|
|
20
|
+
function removeExportConstBlock(src: string, name: string): string {
|
|
21
|
+
const pattern = new RegExp(`^export\\s+const\\s+${name}\\s*=`, "m");
|
|
22
|
+
const match = pattern.exec(src);
|
|
23
|
+
if (!match) return src;
|
|
24
|
+
|
|
25
|
+
// Find the arrow `=>` first, then the opening `{` of the body.
|
|
26
|
+
// This avoids matching destructuring braces in parameters like
|
|
27
|
+
// `export const loader = ({ groups }: Props) => { ... }`
|
|
28
|
+
let pos = match.index + match[0].length;
|
|
29
|
+
// Look for `=>`
|
|
30
|
+
const arrowIdx = src.indexOf("=>", pos);
|
|
31
|
+
if (arrowIdx === -1) {
|
|
32
|
+
// No arrow function — try simple brace from current position
|
|
33
|
+
while (pos < src.length && src[pos] !== "{") pos++;
|
|
34
|
+
} else {
|
|
35
|
+
// Start searching for `{` after the arrow
|
|
36
|
+
pos = arrowIdx + 2;
|
|
37
|
+
while (pos < src.length && src[pos] !== "{") pos++;
|
|
38
|
+
}
|
|
39
|
+
if (pos >= src.length) return src; // no brace body, skip
|
|
40
|
+
|
|
41
|
+
// Count braces to find the matching closing brace
|
|
42
|
+
let depth = 0;
|
|
43
|
+
const start = match.index;
|
|
44
|
+
for (; pos < src.length; pos++) {
|
|
45
|
+
if (src[pos] === "{") depth++;
|
|
46
|
+
else if (src[pos] === "}") {
|
|
47
|
+
depth--;
|
|
48
|
+
if (depth === 0) {
|
|
49
|
+
// Skip optional semicolon and trailing newline
|
|
50
|
+
let end = pos + 1;
|
|
51
|
+
if (end < src.length && src[end] === ";") end++;
|
|
52
|
+
if (end < src.length && src[end] === "\n") end++;
|
|
53
|
+
return src.slice(0, start) + src.slice(end);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return src; // unbalanced braces, don't touch
|
|
58
|
+
}
|
|
59
|
+
|
|
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 {
|
|
168
|
+
const notes: string[] = [];
|
|
169
|
+
let changed = false;
|
|
170
|
+
let result = content;
|
|
171
|
+
|
|
172
|
+
// Remove old cache export: export const cache = "stale-while-revalidate" or { maxAge: ... }
|
|
173
|
+
if (/^export\s+const\s+cache\s*=/m.test(result)) {
|
|
174
|
+
// String form: export const cache = "stale-while-revalidate";
|
|
175
|
+
result = result.replace(
|
|
176
|
+
/^export\s+const\s+cache\s*=\s*["'][^"']*["'];?\s*\n?/gm,
|
|
177
|
+
"",
|
|
178
|
+
);
|
|
179
|
+
// Object form: export const cache = { maxAge: 60 * 10 };
|
|
180
|
+
result = result.replace(
|
|
181
|
+
/^export\s+const\s+cache\s*=\s*\{[^}]*\};?\s*\n?/gm,
|
|
182
|
+
"",
|
|
183
|
+
);
|
|
184
|
+
// Multiline object form (use brace-counting)
|
|
185
|
+
if (/^export\s+const\s+cache\s*=/m.test(result)) {
|
|
186
|
+
result = removeExportConstBlock(result, "cache");
|
|
187
|
+
}
|
|
188
|
+
changed = true;
|
|
189
|
+
notes.push("Removed dead `export const cache` (old caching system)");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Remove old cacheKey export (can be multiline with brace-counting)
|
|
193
|
+
if (/^export\s+const\s+cacheKey\s*=/m.test(result)) {
|
|
194
|
+
result = removeExportConstBlock(result, "cacheKey");
|
|
195
|
+
// Also handle simpler inline forms
|
|
196
|
+
result = result.replace(
|
|
197
|
+
/^export\s+const\s+cacheKey\s*=[^;]*;\s*\n?/gm,
|
|
198
|
+
"",
|
|
199
|
+
);
|
|
200
|
+
changed = true;
|
|
201
|
+
notes.push("Removed dead `export const cacheKey` (old caching system)");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// NOTE: `export const loader` is kept — these are server-side functions
|
|
205
|
+
// that the CMS calls to modify section props before rendering.
|
|
206
|
+
|
|
207
|
+
// Replace crypto.subtle.digestSync (Deno-only) with a note
|
|
208
|
+
if (result.includes("digestSync")) {
|
|
209
|
+
result = result.replace(
|
|
210
|
+
/crypto\.subtle\.digestSync\(/g,
|
|
211
|
+
"/* MIGRATION: digestSync is Deno-only, use await crypto.subtle.digest( */ crypto.subtle.digest(",
|
|
212
|
+
);
|
|
213
|
+
changed = true;
|
|
214
|
+
notes.push("MANUAL: crypto.subtle.digestSync is Deno-only — replaced with crypto.subtle.digest (needs await)");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Replace logger usage from @deco/deco/o11y with console
|
|
218
|
+
if (result.includes("logger.")) {
|
|
219
|
+
result = result.replace(/\blogger\.error\b/g, "console.error");
|
|
220
|
+
result = result.replace(/\blogger\.warn\b/g, "console.warn");
|
|
221
|
+
result = result.replace(/\blogger\.info\b/g, "console.info");
|
|
222
|
+
result = result.replace(/\blogger\.debug\b/g, "console.debug");
|
|
223
|
+
result = result.replace(/\blogger\.log\b/g, "console.log");
|
|
224
|
+
changed = true;
|
|
225
|
+
notes.push("Replaced logger.* with console.* (logger from @deco/deco/o11y removed)");
|
|
226
|
+
}
|
|
227
|
+
|
|
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
|
+
}
|
|
450
|
+
|
|
451
|
+
// Clean up blank lines
|
|
452
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
453
|
+
|
|
454
|
+
return { content: result, changed, notes };
|
|
455
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { TransformResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes Deno-specific patterns:
|
|
5
|
+
*
|
|
6
|
+
* - // deno-lint-ignore ... comments
|
|
7
|
+
* - // deno-lint-ignore-file comments
|
|
8
|
+
* - npm: prefix from import specifiers (already in imports.ts but this catches stragglers)
|
|
9
|
+
* - Strip .ts/.tsx extensions from local import paths
|
|
10
|
+
*/
|
|
11
|
+
export function transformDenoIsms(content: string): TransformResult {
|
|
12
|
+
const notes: string[] = [];
|
|
13
|
+
let changed = false;
|
|
14
|
+
let result = content;
|
|
15
|
+
|
|
16
|
+
// Remove deno-lint-ignore comments (single line and file-level)
|
|
17
|
+
// Also handle JSX comment form: {/* deno-lint-ignore ... */}
|
|
18
|
+
if (/deno-lint-ignore/.test(result)) {
|
|
19
|
+
result = result.replace(/^\s*\/\/\s*deno-lint-ignore[^\n]*\n?/gm, "");
|
|
20
|
+
result = result.replace(/\s*\{\/\*\s*deno-lint-ignore[^*]*\*\/\}\s*/g, " ");
|
|
21
|
+
changed = true;
|
|
22
|
+
notes.push("Removed deno-lint-ignore comments");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Remove npm: prefix in import specifiers that weren't caught by imports transform
|
|
26
|
+
if (/from\s+["']npm:/.test(result)) {
|
|
27
|
+
// npm:pkg@version → pkg (strip version)
|
|
28
|
+
result = result.replace(
|
|
29
|
+
/(from\s+["'])npm:(@?[^@"']+)@[^"']*(["'])/g,
|
|
30
|
+
"$1$2$3",
|
|
31
|
+
);
|
|
32
|
+
// npm:pkg → pkg (no version)
|
|
33
|
+
result = result.replace(
|
|
34
|
+
/(from\s+["'])npm:([^"'@][^"']*)(["'])/g,
|
|
35
|
+
"$1$2$3",
|
|
36
|
+
);
|
|
37
|
+
changed = true;
|
|
38
|
+
notes.push("Removed npm: prefix from imports");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// @ts-ignore → @ts-expect-error (TypeScript 5+ prefers @ts-expect-error)
|
|
42
|
+
if (/@ts-ignore/.test(result)) {
|
|
43
|
+
result = result.replace(/@ts-ignore/g, "@ts-expect-error");
|
|
44
|
+
changed = true;
|
|
45
|
+
notes.push("Replaced @ts-ignore with @ts-expect-error");
|
|
46
|
+
}
|
|
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
|
+
|
|
67
|
+
// Remove Deno.* API usages — flag for manual review
|
|
68
|
+
if (result.includes("Deno.")) {
|
|
69
|
+
notes.push("MANUAL: Deno.* API usage found — needs Node.js equivalent");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Remove /// <reference ... /> directives for Deno
|
|
73
|
+
const refDirectiveRegex =
|
|
74
|
+
/^\/\/\/\s*<reference\s+(?:lib|path|types)\s*=\s*"[^"]*deno[^"]*"\s*\/>\s*\n?/gm;
|
|
75
|
+
if (refDirectiveRegex.test(result)) {
|
|
76
|
+
result = result.replace(refDirectiveRegex, "");
|
|
77
|
+
changed = true;
|
|
78
|
+
notes.push("Removed Deno reference directives");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Clean up blank lines
|
|
82
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
83
|
+
|
|
84
|
+
return { content: result, changed, notes };
|
|
85
|
+
}
|