@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,193 @@
1
+ import type { TransformResult } from "../types";
2
+
3
+ /**
4
+ * htmx → React event-name mapping for the seven `hx-on:*` / `hx-on-*`
5
+ * patterns that have a 1:1 React equivalent.
6
+ *
7
+ * The codemod is intentionally narrow: only events present in this map
8
+ * get renamed. Custom DOM events (`hx-on:my-thing`), htmx lifecycle
9
+ * events (`hx-on:htmx-config-request`), and any event that requires a
10
+ * React `addEventListener` in `useEffect` get left alone — the
11
+ * `htmx-residue` audit rule catches them and points at the per-pattern
12
+ * `htmx-rewrite.md` skill.
13
+ *
14
+ * Kept lowercase to mirror htmx's own naming and to keep the regex
15
+ * simple. JSX attribute names ARE case-sensitive; htmx writers always
16
+ * use lowercase.
17
+ */
18
+ const STANDARD_EVENT_MAP: Record<string, string> = {
19
+ click: "onClick",
20
+ dblclick: "onDoubleClick",
21
+ submit: "onSubmit",
22
+ reset: "onReset",
23
+ change: "onChange",
24
+ input: "onInput",
25
+ keyup: "onKeyUp",
26
+ keydown: "onKeyDown",
27
+ keypress: "onKeyPress",
28
+ focus: "onFocus",
29
+ blur: "onBlur",
30
+ focusin: "onFocus",
31
+ focusout: "onBlur",
32
+ mouseover: "onMouseOver",
33
+ mouseout: "onMouseOut",
34
+ mouseenter: "onMouseEnter",
35
+ mouseleave: "onMouseLeave",
36
+ mousedown: "onMouseDown",
37
+ mouseup: "onMouseUp",
38
+ mousemove: "onMouseMove",
39
+ contextmenu: "onContextMenu",
40
+ load: "onLoad",
41
+ scroll: "onScroll",
42
+ paste: "onPaste",
43
+ copy: "onCopy",
44
+ cut: "onCut",
45
+ dragstart: "onDragStart",
46
+ drag: "onDrag",
47
+ dragend: "onDragEnd",
48
+ drop: "onDrop",
49
+ dragenter: "onDragEnter",
50
+ dragleave: "onDragLeave",
51
+ dragover: "onDragOver",
52
+ wheel: "onWheel",
53
+ touchstart: "onTouchStart",
54
+ touchend: "onTouchEnd",
55
+ touchmove: "onTouchMove",
56
+ touchcancel: "onTouchCancel",
57
+ };
58
+
59
+ /**
60
+ * Marker comment we inject when a rename happened *and* the surviving
61
+ * handler bodies reference Fresh-only globals (`useScript`,
62
+ * `globalThis.window.STOREFRONT`, `STOREFRONT.*`). The comment is the
63
+ * only file-level annotation the codemod emits — per-occurrence
64
+ * comments would balloon the diff for a 88-rename file like
65
+ * als-storefront's hot paths. It is detected by an idempotency check
66
+ * so re-running the codemod does not double-inject.
67
+ */
68
+ const TODO_MARKER = "// MIGRATION TODO (codemod: htmx-on-event-rename):";
69
+
70
+ const TODO_BLOCK = `${TODO_MARKER}
71
+ // hx-on:* attributes were auto-renamed to React event handlers, but
72
+ // the handler bodies were preserved verbatim. They may reference
73
+ // Fresh-only globals like \`globalThis.window.STOREFRONT\` or
74
+ // \`useScript(...)\`. Verify each handler matches a TanStack Start
75
+ // equivalent (state hook, platform hook, or server function) — see
76
+ // .agents/skills/deco-to-tanstack-migration/references/htmx-rewrite.md
77
+ // § Pattern 1 (event-handler).`;
78
+
79
+ /**
80
+ * Matches a single `hx-on:eventname=` or `hx-on-eventname=` attribute
81
+ * occurrence in the source.
82
+ *
83
+ * `\b` before `hx-on` keeps us from matching inside identifiers like
84
+ * `withHx-on` (impossible in TS but defensive). The separator capture
85
+ * group distinguishes colon vs dash so we can flip both syntactic
86
+ * variants in one pass.
87
+ *
88
+ * The event name allows a-zA-Z0-9 and `-` so multi-segment htmx events
89
+ * (`htmx-config-request`, `htmx-before-request`) are captured intact and
90
+ * we can decide *after* the match whether to rename or skip.
91
+ */
92
+ const HX_ON_ATTR_RE = /\bhx-on([:\-])([a-zA-Z][a-zA-Z0-9-]*)(\s*=)/g;
93
+
94
+ /**
95
+ * Heuristic patterns for handler bodies that reference Fresh-specific
96
+ * globals. Used only to gate TODO injection — false positives are
97
+ * harmless (extra comment), false negatives are tolerable (audit will
98
+ * still catch htmx residue elsewhere).
99
+ */
100
+ const FRESH_BODY_PATTERNS: readonly RegExp[] = [
101
+ /\buseScript\s*\(/,
102
+ /\bglobalThis\.window\.STOREFRONT\b/,
103
+ /\bSTOREFRONT\./,
104
+ ];
105
+
106
+ /**
107
+ * Rewrite `hx-on:click={...}` and `hx-on-click={...}` attributes to
108
+ * the React equivalent (`onClick={...}`), preserving the handler value
109
+ * verbatim. Renames only happen for events with a known React mapping.
110
+ *
111
+ * - htmx lifecycle events (`htmx-config-request`, `htmx-before-request`,
112
+ * `htmx-after-swap`, etc.) are left alone — they require manual
113
+ * rewrite per the htmx-rewrite skill, and the `htmx-residue` audit
114
+ * rule will catch them post-migration.
115
+ * - Unknown custom events (e.g. `hx-on:my-custom-thing`) are left alone
116
+ * — React doesn't have synthetic equivalents for arbitrary custom
117
+ * events; the engineer must wire those via `addEventListener` in
118
+ * `useEffect`, which the codemod cannot generate safely.
119
+ *
120
+ * If any rename happens AND the file contains Fresh-only body
121
+ * patterns, a single file-level TODO comment is injected at the top so
122
+ * reviewers know the bodies still need attention. Idempotent — running
123
+ * the codemod twice produces identical output.
124
+ */
125
+ export function transformHtmxOnEvents(content: string): TransformResult {
126
+ if (!content.includes("hx-on")) {
127
+ return { content, changed: false, notes: [] };
128
+ }
129
+
130
+ const renamesByEvent = new Map<string, number>();
131
+ let renamed = 0;
132
+
133
+ const next = content.replace(
134
+ HX_ON_ATTR_RE,
135
+ (match, _sep: string, eventName: string, equals: string) => {
136
+ const lower = eventName.toLowerCase();
137
+ if (lower.startsWith("htmx-")) return match;
138
+
139
+ const reactName = STANDARD_EVENT_MAP[lower];
140
+ if (!reactName) return match;
141
+
142
+ renamed += 1;
143
+ renamesByEvent.set(reactName, (renamesByEvent.get(reactName) ?? 0) + 1);
144
+ return `${reactName}${equals}`;
145
+ },
146
+ );
147
+
148
+ if (renamed === 0) {
149
+ return { content, changed: false, notes: [] };
150
+ }
151
+
152
+ const notes: string[] = [];
153
+ const breakdown = [...renamesByEvent.entries()]
154
+ .sort(([a], [b]) => a.localeCompare(b))
155
+ .map(([name, n]) => `${name}=${n}`)
156
+ .join(", ");
157
+ notes.push(`Renamed ${renamed} hx-on:* attribute(s) → React events (${breakdown})`);
158
+
159
+ const hasFreshIsms = FRESH_BODY_PATTERNS.some((re) => re.test(next));
160
+ const hasMarker = next.includes(TODO_MARKER);
161
+
162
+ if (!hasFreshIsms || hasMarker) {
163
+ return { content: next, changed: true, notes };
164
+ }
165
+
166
+ const final = injectTopOfFileTodo(next);
167
+ notes.push(
168
+ "Injected MIGRATION TODO — handler body references Fresh-only globals (useScript / globalThis.window.STOREFRONT)",
169
+ );
170
+ return { content: final, changed: true, notes };
171
+ }
172
+
173
+ /**
174
+ * Insert TODO_BLOCK as a top-of-file comment, *after* any leading
175
+ * shebang line or block comment. Keeps directives like `"use client"`
176
+ * intact (they live below the comment block, which is fine).
177
+ */
178
+ function injectTopOfFileTodo(source: string): string {
179
+ if (source.startsWith("#!")) {
180
+ const newlineIdx = source.indexOf("\n");
181
+ if (newlineIdx === -1) return `${source}\n${TODO_BLOCK}\n`;
182
+ return `${source.slice(0, newlineIdx + 1)}${TODO_BLOCK}\n${source.slice(newlineIdx + 1)}`;
183
+ }
184
+ return `${TODO_BLOCK}\n${source}`;
185
+ }
186
+
187
+ /** Exported for direct unit tests. */
188
+ export const _internals = {
189
+ STANDARD_EVENT_MAP,
190
+ HX_ON_ATTR_RE,
191
+ TODO_MARKER,
192
+ FRESH_BODY_PATTERNS,
193
+ };
@@ -0,0 +1,385 @@
1
+ import type { TransformResult } from "../types";
2
+
3
+ /**
4
+ * Import rewriting rules: from (Deno/Fresh/Preact) → to (Node/TanStack/React)
5
+ *
6
+ * Order matters: more specific rules should come first.
7
+ */
8
+ const IMPORT_RULES: Array<[RegExp, string | null]> = [
9
+ // Fresh — remove entirely (handled by fresh-apis transform)
10
+ [/^"\$fresh\/runtime\.ts"/, null],
11
+ [/^"\$fresh\/server\.ts"/, null],
12
+ [/^"\$fresh\//, null], // catch-all for any $fresh/* import
13
+
14
+ // Preact → React
15
+ [/^"preact\/hooks"$/, `"react"`],
16
+ [/^"preact\/jsx-runtime"$/, null],
17
+ [/^"preact\/compat"$/, `"react"`],
18
+ [/^"preact"$/, `"react"`],
19
+ [/^"@preact\/signals-core"$/, `"~/sdk/signal"`],
20
+ [/^"@preact\/signals"$/, `"~/sdk/signal"`],
21
+
22
+ // Deco framework — hooks need splitting (useDevice, useScript, useSection)
23
+ [/^"@deco\/deco\/hooks"$/, `"@decocms/start/sdk/useScript"`],
24
+ [/^"@deco\/deco\/blocks"$/, `"~/types/deco"`],
25
+ [/^"@deco\/deco\/o11y"$/, null], // logger — use console.log/warn/error instead
26
+ [/^"@deco\/deco\/web"$/, null], // runtime.ts is rewritten
27
+ [/^"@deco\/deco\/utils\/invoke\.types\.ts"$/, null],
28
+ [/^"@deco\/deco\/utils\/([^"]+)"$/, null],
29
+ [/^"@deco\/deco"$/, `"~/types/deco"`],
30
+
31
+ // Apps — widgets & components
32
+ // Widget aliases (ImageWidget, HTMLWidget, ...) are framework-owned —
33
+ // every site has the same type set, and the schema generator detects
34
+ // them via type-text matching, not module identity. Re-export from
35
+ // @decocms/start/types/widgets so we don't keep a duplicated 8-line
36
+ // file in every site.
37
+ [/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/types/widgets"`],
38
+ [/^"apps\/website\/components\/Image\.tsx"$/, `"~/components/ui/Image"`],
39
+ [/^"apps\/website\/components\/Picture\.tsx"$/, `"~/components/ui/Picture"`],
40
+ [/^"apps\/website\/components\/Video\.tsx"$/, `"~/components/ui/Video"`],
41
+ [/^"apps\/website\/components\/Theme\.tsx"$/, `"~/components/ui/Theme"`],
42
+ [/^"apps\/website\/components\/_seo\/[^"]+?"$/, null], // SEO preview — framework-only, remove
43
+ [/^"apps\/website\/components\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
44
+ [/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
45
+ [/^"apps\/commerce\/mod\.ts"$/, `"~/types/commerce-app"`],
46
+ [/^"apps\/commerce\/types"$/, `"@decocms/apps/commerce/types"`],
47
+
48
+ // Apps — VTEX hooks: useUser/useCart/useWishlist → local hooks (react-query based @decocms/apps hooks crash Workers SSR)
49
+ [/^"apps\/vtex\/hooks\/useUser(?:\.ts)?"$/, `"~/hooks/useUser"`],
50
+ [/^"apps\/vtex\/hooks\/useCart(?:\.ts)?"$/, `"~/hooks/useCart"`],
51
+ [/^"apps\/vtex\/hooks\/useWishlist(?:\.ts)?"$/, `"~/hooks/useWishlist"`],
52
+ [/^"apps\/vtex\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/hooks/$1"`],
53
+ // Specific VTEX utils that moved to different paths in @decocms/apps
54
+ // fetchVTEX (generic fetchSafe + QS sanitization) lives at vtex/utils/fetch in apps-start.
55
+ [/^"apps\/vtex\/utils\/fetchVTEX(?:\.ts)?"$/, `"@decocms/apps/vtex/utils/fetch"`],
56
+ [/^"apps\/vtex\/utils\/client(?:\.ts)?"$/, `"@decocms/apps/vtex/client"`],
57
+ [/^"apps\/vtex\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/utils/$1"`],
58
+ [/^"apps\/vtex\/actions\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/actions/$1"`],
59
+ // Tier B loader path rewrites (apps-start has no `intelligentSearch/`, `legacy/<file>`, or `paths/` subdirs).
60
+ // Intelligent Search loaders moved to inline-loaders/.
61
+ [
62
+ /^"apps\/vtex\/loaders\/intelligentSearch\/productList(?:\.ts)?"$/,
63
+ `"@decocms/apps/vtex/inline-loaders/productList"`,
64
+ ],
65
+ [
66
+ /^"apps\/vtex\/loaders\/intelligentSearch\/productListingPage(?:\.ts)?"$/,
67
+ `"@decocms/apps/vtex/inline-loaders/productListingPage"`,
68
+ ],
69
+ [
70
+ /^"apps\/vtex\/loaders\/intelligentSearch\/productDetailsPage(?:\.ts)?"$/,
71
+ `"@decocms/apps/vtex/inline-loaders/productDetailsPage"`,
72
+ ],
73
+ [
74
+ /^"apps\/vtex\/loaders\/intelligentSearch\/suggestions(?:\.ts)?"$/,
75
+ `"@decocms/apps/vtex/inline-loaders/suggestions"`,
76
+ ],
77
+ // Legacy product loaders are consolidated into a single file (named exports).
78
+ [
79
+ /^"apps\/vtex\/loaders\/legacy\/(?:productList|productListingPage|productDetailsPage|search|category)(?:\.ts)?"$/,
80
+ `"@decocms/apps/vtex/loaders/legacy"`,
81
+ ],
82
+ // Path-default loaders (sitemap seeds) don't exist in TanStack Start — paths resolve at request time.
83
+ [/^"apps\/vtex\/loaders\/paths\/(?:[^"]+)(?:\.ts)?"$/, null],
84
+ [/^"apps\/vtex\/loaders\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/loaders/$1"`],
85
+ [/^"apps\/vtex\/types(?:\.ts)?"$/, `"@decocms/apps/vtex/types"`],
86
+ [/^"apps\/vtex\/mod(?:\.ts)?"$/, `"~/types/vtex-app"`],
87
+ // Apps — Shopify (hooks, utils, actions, loaders)
88
+ [/^"apps\/shopify\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/hooks/$1"`],
89
+ [/^"apps\/shopify\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/utils/$1"`],
90
+ [/^"apps\/shopify\/actions\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/actions/$1"`],
91
+ [/^"apps\/shopify\/loaders\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/loaders/$1"`],
92
+ // Apps — commerce (types, SDK, utils)
93
+ [/^"apps\/commerce\/sdk\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/sdk/$1"`],
94
+ [/^"apps\/commerce\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/utils/$1"`],
95
+
96
+ // Apps — shared utils (STALE, fetchSafe, createHttpClient, etc.)
97
+ [/^"apps\/utils\/fetch(?:\.ts)?"$/, `"~/lib/fetch-utils"`],
98
+ [/^"apps\/utils\/http(?:\.ts)?"$/, `"~/lib/http-utils"`],
99
+ [/^"apps\/utils\/graphql(?:\.ts)?"$/, `"~/lib/graphql-utils"`],
100
+
101
+ // Apps — catch-all (things like apps/website/mod.ts, apps/analytics/mod.ts, etc.)
102
+ [/^"apps\/([^"]+)"$/, null], // Remove — site.ts is rewritten
103
+
104
+ // Deco old CDN imports
105
+ [/^"deco\/([^"]+)"$/, null],
106
+
107
+ // Remote URL imports (esm.sh, cdn.esm.sh, skypack, etc.) — remove
108
+ [/^"https?:\/\/esm\.sh\/[^"]*"$/, null],
109
+ [/^"https?:\/\/cdn\.esm\.sh\/[^"]*"$/, null],
110
+ [/^"https?:\/\/cdn\.skypack\.dev\/[^"]*"$/, null],
111
+ [/^"https?:\/\/deno\.land\/[^"]*"$/, null],
112
+
113
+ // Std lib — redirect useful utils, remove the rest
114
+ [/^"std\/async\/debounce(?:\.ts)?"$/, `"~/sdk/debounce"`],
115
+ [/^"std\/([^"]+)"$/, null],
116
+ [/^"@std\/crypto"$/, null], // Use globalThis.crypto instead
117
+
118
+ // site/sdk/* → framework equivalents (before the catch-all site/ → ~/ rule)
119
+ [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
120
+ [/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
121
+ // useOffer and useVariantPossiblities kept as site files (~/sdk/)
122
+ [/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
123
+
124
+ // account.json → constants/account (JSON file replaced with TS module in cleanup)
125
+ [/^"\$store\/account\.json"$/, `"~/constants/account"`],
126
+ [/^"site\/account\.json"$/, `"~/constants/account"`],
127
+ [/^"~\/account\.json"$/, `"~/constants/account"`],
128
+
129
+ // $store/ → ~/ (common Deno import map alias for project root)
130
+ [/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
131
+ [/^"\$store\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
132
+ // useOffer and useVariantPossiblities kept as site files (~/sdk/)
133
+ [/^"\$store\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
134
+ [/^"\$store\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
135
+ // islands → components (must be before $store catch-all)
136
+ [/^"\$store\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
137
+ [/^"\$store\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
138
+ [/^"\$store\/(.+)"$/, `"~/$1"`],
139
+
140
+ // $home/ → ~/ (another common alias)
141
+ [/^"\$home\/(.+)"$/, `"~/$1"`],
142
+
143
+ // site/ → ~/
144
+ [/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
145
+ [/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
146
+ // useOffer and useVariantPossiblities kept as site files (~/sdk/)
147
+ [/^"site\/sdk\/format(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/formatPrice"`],
148
+ [/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
149
+ // islands → components (must be before site/ catch-all)
150
+ [/^"site\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
151
+ [/^"site\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
152
+ [/^"site\/(.+)"$/, `"~/$1"`],
153
+
154
+ // ~/islands/* → ~/components/* (catch any that slipped through)
155
+ [/^"~\/islands\/ui\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/ui/$1"`],
156
+ [/^"~\/islands\/([^"]+?)(?:\.tsx?)?"$/, `"~/components/$1"`],
157
+
158
+ // @decocms/apps hooks → local hooks (react-query hooks crash Workers SSR at module eval)
159
+ [/^"@decocms\/apps\/vtex\/hooks\/useUser"$/, `"~/hooks/useUser"`],
160
+ [/^"@decocms\/apps\/vtex\/hooks\/useCart"$/, `"~/hooks/useCart"`],
161
+ [/^"@decocms\/apps\/vtex\/hooks\/useWishlist"$/, `"~/hooks/useWishlist"`],
162
+ ];
163
+
164
+ /**
165
+ * Relative import rewrites for SDK files that are deleted during migration.
166
+ * These are matched against the resolved import path (after ../.. resolution).
167
+ * The key is the ending of the import path, the value is the replacement specifier.
168
+ */
169
+ const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
170
+ // sdk/clx → @decocms/start/sdk/clx (framework utility)
171
+ [/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "@decocms/start/sdk/clx"],
172
+ // sdk/useId → react (useId is built-in in React 19)
173
+ [/(?:\.\.\/)*sdk\/useId(?:\.tsx?)?$/, "react"],
174
+ // sdk/useOffer — kept as-is (sites customize offer logic)
175
+ // sdk/useVariantPossiblities — kept as-is (sites customize variant logic)
176
+ // sdk/format → @decocms/apps/commerce/sdk/formatPrice
177
+ [/(?:\.\.\/)*sdk\/format(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/formatPrice"],
178
+ // sdk/usePlatform → remove entirely
179
+ [/(?:\.\.\/)*sdk\/usePlatform(?:\.tsx?)?$/, ""],
180
+ // static/adminIcons → deleted (icon loaders need rewriting)
181
+ [/(?:\.\.\/)*static\/adminIcons(?:\.ts)?$/, ""],
182
+ // islands/ui/* → components/ui/* (islands are merged into components)
183
+ [/(?:\.\.\/)*islands\/ui\/([^"]+?)(?:\.tsx?)?$/, "~/components/ui/$1"],
184
+ [/(?:\.\.\/)*islands\/([^"]+?)(?:\.tsx?)?$/, "~/components/$1"],
185
+ ];
186
+
187
+ /**
188
+ * Rewrites import specifiers in a file.
189
+ *
190
+ * Handles:
191
+ * - import X from "old" → import X from "new"
192
+ * - import { X } from "old" → import { X } from "new"
193
+ * - import type { X } from "old" → import type { X } from "new"
194
+ * - export { X } from "old" → export { X } from "new"
195
+ * - import "old" → import "new"
196
+ *
197
+ * When a rule maps to null, the entire import line is removed.
198
+ */
199
+ export function transformImports(
200
+ content: string,
201
+ islandWrapperTargets?: Map<string, string>,
202
+ ): TransformResult {
203
+ const notes: string[] = [];
204
+ let changed = false;
205
+
206
+ // Build dynamic rules from island wrapper targets (wrapper island → actual component)
207
+ const dynamicRules: Array<[RegExp, string]> = [];
208
+ if (islandWrapperTargets) {
209
+ for (const [islandPath, targetImport] of islandWrapperTargets) {
210
+ const componentPath = islandPath.replace("islands/", "components/").replace(/\.tsx?$/, "");
211
+ // Match ~/components/X and rewrite to the wrapper's actual target
212
+ const escaped = componentPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
213
+ dynamicRules.push([
214
+ new RegExp(`^"~/${escaped}"$`),
215
+ `"${targetImport}"`,
216
+ ]);
217
+ }
218
+ }
219
+
220
+ // Strip BOM that prevents ^ matching on the first line
221
+ if (content.charCodeAt(0) === 0xfeff) {
222
+ content = content.slice(1);
223
+ changed = true;
224
+ }
225
+
226
+ // Match import/export lines with their specifiers
227
+ // The suffix group also captures import assertions (with { type: "json" }) and assert syntax
228
+ const importLineRegex =
229
+ /^(import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?)\s+from\s+)("[^"]+"|'[^']+')((?:\s+(?:with|assert)\s+\{[^}]*\})?;?\s*)$/gm;
230
+ const reExportLineRegex =
231
+ /^(export\s+(?:type\s+)?\{[^}]*\}\s+from\s+)("[^"]+"|'[^']+')((?:\s+(?:with|assert)\s+\{[^}]*\})?;?\s*)$/gm;
232
+ const sideEffectImportRegex = /^(import\s+)("[^"]+"|'[^']+')((?:\s+(?:with|assert)\s+\{[^}]*\})?;?\s*)$/gm;
233
+
234
+ /**
235
+ * Post-process: split @deco/deco/hooks imports.
236
+ * In the old stack, @deco/deco/hooks exported useDevice, useScript, useSection, etc.
237
+ * In @decocms/start, useDevice is at @decocms/start/sdk/useDevice.
238
+ * After import rewriting, we need to split lines like:
239
+ * import { useDevice, useScript } from "@decocms/start/sdk/useScript"
240
+ * into:
241
+ * import { useDevice } from "@decocms/start/sdk/useDevice"
242
+ * import { useScript } from "@decocms/start/sdk/useScript"
243
+ */
244
+ function splitDecoHooksImports(code: string): string {
245
+ return code.replace(
246
+ /^(import\s+(?:type\s+)?\{)([^}]*\buseDevice\b[^}]*)(\}\s+from\s+["']@decocms\/start\/sdk\/useScript["'];?)$/gm,
247
+ (_match, _prefix, importList, _suffix) => {
248
+ const items = importList.split(",").map((s: string) => s.trim()).filter(Boolean);
249
+ const deviceItems = items.filter((s: string) => s.includes("useDevice"));
250
+ const otherItems = items.filter((s: string) => !s.includes("useDevice"));
251
+
252
+ const lines: string[] = [];
253
+ if (deviceItems.length > 0) {
254
+ lines.push(`import { ${deviceItems.join(", ")} } from "@decocms/start/sdk/useDevice";`);
255
+ }
256
+ if (otherItems.length > 0) {
257
+ lines.push(`import { ${otherItems.join(", ")} } from "@decocms/start/sdk/useScript";`);
258
+ }
259
+ return lines.join("\n");
260
+ },
261
+ );
262
+ }
263
+
264
+ function applyDynamicRules(result: string): string {
265
+ for (const [dynPattern, dynReplacement] of dynamicRules) {
266
+ if (dynPattern.test(result)) return dynReplacement;
267
+ }
268
+ return result;
269
+ }
270
+
271
+ function rewriteSpecifier(specifier: string): string | null {
272
+ // Remove quotes for matching
273
+ const inner = specifier.slice(1, -1);
274
+
275
+ for (const [pattern, replacement] of IMPORT_RULES) {
276
+ if (pattern.test(`"${inner}"`)) {
277
+ if (replacement === null) return null;
278
+ let result = `"${inner}"`.replace(pattern, replacement);
279
+ let resultInner = result.slice(1, -1);
280
+ if (
281
+ (resultInner.startsWith("~/") || resultInner.startsWith("./") || resultInner.startsWith("../")) &&
282
+ (resultInner.endsWith(".ts") || resultInner.endsWith(".tsx"))
283
+ ) {
284
+ resultInner = resultInner.replace(/\.tsx?$/, "");
285
+ result = `"${resultInner}"`;
286
+ }
287
+ return applyDynamicRules(result);
288
+ }
289
+ }
290
+
291
+ // Relative imports pointing to deleted SDK files → framework equivalents
292
+ if (inner.startsWith("./") || inner.startsWith("../")) {
293
+ for (const [pattern, replacement] of RELATIVE_SDK_REWRITES) {
294
+ if (pattern.test(inner)) {
295
+ if (replacement === "") return null;
296
+ const resolved = inner.replace(pattern, replacement);
297
+ return applyDynamicRules(`"${resolved}"`);
298
+ }
299
+ }
300
+ }
301
+
302
+ // npm: prefix removal
303
+ if (inner.startsWith("npm:")) {
304
+ const cleaned = inner
305
+ .slice(4)
306
+ .replace(/@[\d^~>=<.*]+$/, "");
307
+ return `"${cleaned}"`;
308
+ }
309
+
310
+ // Strip .ts/.tsx extensions from relative imports
311
+ if (
312
+ (inner.startsWith("./") || inner.startsWith("../") ||
313
+ inner.startsWith("~/")) &&
314
+ (inner.endsWith(".ts") || inner.endsWith(".tsx"))
315
+ ) {
316
+ const stripped = inner.replace(/\.tsx?$/, "");
317
+ return applyDynamicRules(`"${stripped}"`);
318
+ }
319
+
320
+ return specifier;
321
+ }
322
+
323
+ function processLine(
324
+ _match: string,
325
+ prefix: string,
326
+ specifier: string,
327
+ suffix: string,
328
+ ): string {
329
+ const newSpec = rewriteSpecifier(specifier);
330
+ if (newSpec === null) {
331
+ changed = true;
332
+ notes.push(`Removed import: ${specifier}`);
333
+ return ""; // Remove the line
334
+ }
335
+ if (newSpec !== specifier) {
336
+ changed = true;
337
+ notes.push(`Rewrote: ${specifier} → ${newSpec}`);
338
+ // Strip import assertions (with/assert { type: "json" }) when the
339
+ // specifier no longer points to a JSON file (e.g. account.json → constants/account)
340
+ let cleanSuffix = suffix;
341
+ if (specifier.includes(".json") && !newSpec.includes(".json")) {
342
+ cleanSuffix = cleanSuffix.replace(/\s*(?:with|assert)\s*\{[^}]*\}\s*/, "");
343
+ }
344
+ return `${prefix}${newSpec}${cleanSuffix}`;
345
+ }
346
+ return `${prefix}${specifier}${suffix}`;
347
+ }
348
+
349
+ let result = content;
350
+ result = result.replace(importLineRegex, processLine);
351
+ result = result.replace(reExportLineRegex, processLine);
352
+ result = result.replace(sideEffectImportRegex, processLine);
353
+
354
+ // Split @deco/deco/hooks imports that contain useDevice
355
+ const afterSplit = splitDecoHooksImports(result);
356
+ if (afterSplit !== result) {
357
+ result = afterSplit;
358
+ changed = true;
359
+ notes.push("Split useDevice into separate import from @decocms/start/sdk/useDevice");
360
+ }
361
+
362
+ // Rewrite dynamic imports: route through rewriteSpecifier so sdk-specific
363
+ // rules (e.g. site/sdk/useId → react) are applied consistently.
364
+ const dynamicImportRe = /\bimport\(\s*(["'])([^"']+)\1\s*\)/g;
365
+ result = result.replace(dynamicImportRe, (_match, quote, specifier) => {
366
+ const quoted = `"${specifier}"`;
367
+ const rewritten = rewriteSpecifier(quoted);
368
+ if (rewritten === null) {
369
+ // Rule says remove — leave the dynamic import as-is (caller must fix manually)
370
+ return _match;
371
+ }
372
+ if (rewritten !== quoted) {
373
+ const newSpecifier = rewritten.slice(1, -1);
374
+ changed = true;
375
+ notes.push(`Rewrote dynamic import: ${specifier} → ${newSpecifier}`);
376
+ return `import(${quote}${newSpecifier}${quote})`;
377
+ }
378
+ return _match;
379
+ });
380
+
381
+ // Clean up blank lines left by removed imports (collapse multiple to one)
382
+ result = result.replace(/\n{3,}/g, "\n\n");
383
+
384
+ return { content: result, changed, notes };
385
+ }