@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,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
|
+
}
|