@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.
Files changed (93) hide show
  1. package/package.json +44 -0
  2. package/scripts/analyze-traces.mjs +1117 -0
  3. package/scripts/audit-observability-config.test.ts +446 -0
  4. package/scripts/audit-observability-config.ts +511 -0
  5. package/scripts/deco-migrate-cli.ts +444 -0
  6. package/scripts/fast-deploy-kv.test.ts +131 -0
  7. package/scripts/generate-blocks.test.ts +94 -0
  8. package/scripts/generate-blocks.ts +274 -0
  9. package/scripts/generate-invoke.test.ts +195 -0
  10. package/scripts/generate-invoke.ts +469 -0
  11. package/scripts/generate-loaders.ts +217 -0
  12. package/scripts/generate-schema.ts +1287 -0
  13. package/scripts/generate-sections.ts +237 -0
  14. package/scripts/htmx-analyze.ts +226 -0
  15. package/scripts/lib/blocks-dedupe.test.ts +179 -0
  16. package/scripts/lib/blocks-dedupe.ts +142 -0
  17. package/scripts/lib/cf-kv-rest.ts +78 -0
  18. package/scripts/lib/jsonc.ts +122 -0
  19. package/scripts/lib/kv-snapshot.ts +51 -0
  20. package/scripts/lib/read-decofile.ts +70 -0
  21. package/scripts/lib/sync-helpers.ts +44 -0
  22. package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
  23. package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
  24. package/scripts/migrate/analyzers/island-classifier.ts +96 -0
  25. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  26. package/scripts/migrate/analyzers/section-metadata.ts +147 -0
  27. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  28. package/scripts/migrate/colors.ts +46 -0
  29. package/scripts/migrate/config.test.ts +202 -0
  30. package/scripts/migrate/config.ts +186 -0
  31. package/scripts/migrate/phase-analyze.test.ts +63 -0
  32. package/scripts/migrate/phase-analyze.ts +782 -0
  33. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  34. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  35. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  36. package/scripts/migrate/phase-cleanup.ts +1588 -0
  37. package/scripts/migrate/phase-compile.test.ts +193 -0
  38. package/scripts/migrate/phase-compile.ts +177 -0
  39. package/scripts/migrate/phase-report.ts +243 -0
  40. package/scripts/migrate/phase-scaffold.ts +593 -0
  41. package/scripts/migrate/phase-transform.ts +310 -0
  42. package/scripts/migrate/phase-verify.test.ts +127 -0
  43. package/scripts/migrate/phase-verify.ts +572 -0
  44. package/scripts/migrate/post-cleanup/rules.ts +1708 -0
  45. package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
  46. package/scripts/migrate/post-cleanup/runner.ts +137 -0
  47. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  48. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  49. package/scripts/migrate/post-cleanup/types.ts +106 -0
  50. package/scripts/migrate/source-layout.test.ts +111 -0
  51. package/scripts/migrate/source-layout.ts +103 -0
  52. package/scripts/migrate/templates/app-css.ts +366 -0
  53. package/scripts/migrate/templates/cache-config.ts +26 -0
  54. package/scripts/migrate/templates/commerce-loaders.ts +230 -0
  55. package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
  56. package/scripts/migrate/templates/cursor-rules.ts +70 -0
  57. package/scripts/migrate/templates/hooks.test.ts +141 -0
  58. package/scripts/migrate/templates/hooks.ts +152 -0
  59. package/scripts/migrate/templates/knip-config.ts +27 -0
  60. package/scripts/migrate/templates/lib-utils.test.ts +139 -0
  61. package/scripts/migrate/templates/lib-utils.ts +326 -0
  62. package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
  63. package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
  64. package/scripts/migrate/templates/package-json.ts +177 -0
  65. package/scripts/migrate/templates/routes.ts +237 -0
  66. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  67. package/scripts/migrate/templates/section-loaders.ts +456 -0
  68. package/scripts/migrate/templates/server-entry.ts +561 -0
  69. package/scripts/migrate/templates/setup.ts +148 -0
  70. package/scripts/migrate/templates/tsconfig.ts +21 -0
  71. package/scripts/migrate/templates/types-gen.ts +174 -0
  72. package/scripts/migrate/templates/ui-components.ts +144 -0
  73. package/scripts/migrate/templates/vite-config.ts +101 -0
  74. package/scripts/migrate/transforms/dead-code.ts +455 -0
  75. package/scripts/migrate/transforms/deno-isms.ts +85 -0
  76. package/scripts/migrate/transforms/fresh-apis.ts +223 -0
  77. package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
  78. package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
  79. package/scripts/migrate/transforms/imports.ts +385 -0
  80. package/scripts/migrate/transforms/jsx.ts +317 -0
  81. package/scripts/migrate/transforms/section-conventions.ts +210 -0
  82. package/scripts/migrate/transforms/tailwind.ts +739 -0
  83. package/scripts/migrate/types.ts +244 -0
  84. package/scripts/migrate-blocks-to-kv.ts +104 -0
  85. package/scripts/migrate-post-cleanup.ts +191 -0
  86. package/scripts/migrate-to-cf-observability.test.ts +215 -0
  87. package/scripts/migrate-to-cf-observability.ts +699 -0
  88. package/scripts/migrate.ts +282 -0
  89. package/scripts/smoke-otlp-errorlog.ts +46 -0
  90. package/scripts/smoke-otlp-meter.ts +48 -0
  91. package/scripts/sync-blocks-to-kv.ts +153 -0
  92. package/scripts/tailwind-lint.ts +518 -0
  93. 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
+ }