@decocms/start 0.42.2 → 0.42.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/run-migration/SKILL.md +53 -0
- package/package.json +1 -1
- package/scripts/migrate/phase-scaffold.ts +115 -0
- package/scripts/migrate/phase-transform.ts +85 -0
- package/scripts/migrate/phase-verify.ts +3 -3
- package/scripts/migrate/transforms/dead-code.ts +49 -23
- package/scripts/migrate/transforms/fresh-apis.ts +10 -5
- package/scripts/migrate/transforms/imports.ts +43 -8
- package/src/sdk/clx.ts +2 -0
- package/src/sdk/signal.ts +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-migration
|
|
3
|
+
description: Run the deco-start migration script against a target site workspace. Resets the target to its Fresh/Deno state (origin/main), then runs the local migration script. Use for testing migration on real sites.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run Deco Migration
|
|
7
|
+
|
|
8
|
+
Runs the migration script from the local `@decocms/start` repo against a target site.
|
|
9
|
+
|
|
10
|
+
## How to use
|
|
11
|
+
|
|
12
|
+
1. **Identify the target site workspace.** The user will specify which site to migrate. Common targets:
|
|
13
|
+
- miess-01-tanstack: `/Users/jonasjesus/conductor/workspaces/miess-01-tanstack/istanbul`
|
|
14
|
+
|
|
15
|
+
2. **Reset the target to Fresh/Deno state:**
|
|
16
|
+
```bash
|
|
17
|
+
cd <target-dir>
|
|
18
|
+
git checkout origin/main -- .
|
|
19
|
+
git checkout -- .
|
|
20
|
+
git clean -fd
|
|
21
|
+
```
|
|
22
|
+
This restores the original Fresh/Deno source code.
|
|
23
|
+
|
|
24
|
+
3. **Remove old node_modules** (migration generates a new package.json):
|
|
25
|
+
```bash
|
|
26
|
+
rm -rf node_modules package-lock.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
4. **Run the migration script from local deco-start:**
|
|
30
|
+
```bash
|
|
31
|
+
cd <target-dir>
|
|
32
|
+
npx tsx /Users/jonasjesus/conductor/workspaces/deco-start/london/scripts/migrate.ts --verbose
|
|
33
|
+
```
|
|
34
|
+
The script uses `--source .` by default (current directory).
|
|
35
|
+
|
|
36
|
+
5. **Check the output** for errors and review `MIGRATION_REPORT.md`.
|
|
37
|
+
|
|
38
|
+
6. **If the user wants to test locally** after migration:
|
|
39
|
+
```bash
|
|
40
|
+
cd <target-dir>
|
|
41
|
+
# Ensure @decocms/start points to local repo (not npm)
|
|
42
|
+
npm link /Users/jonasjesus/conductor/workspaces/deco-start/london 2>/dev/null || true
|
|
43
|
+
npm install
|
|
44
|
+
npm run dev # or bun run dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Important notes
|
|
48
|
+
|
|
49
|
+
- The migration script is at the repo root: `scripts/migrate.ts`
|
|
50
|
+
- Changes to the script are immediately reflected when running (no build needed, tsx runs TS directly)
|
|
51
|
+
- The target site's `origin/main` branch has the original Fresh/Deno code
|
|
52
|
+
- NEVER push migration results to the target site's remote without explicit user confirmation
|
|
53
|
+
- After running, check for runtime errors and update the migration script if needed
|
package/package.json
CHANGED
|
@@ -65,6 +65,15 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
65
65
|
// Styles
|
|
66
66
|
writeFile(ctx, "src/styles/app.css", generateAppCss(ctx));
|
|
67
67
|
|
|
68
|
+
// SDK — signal shim (replaces @preact/signals)
|
|
69
|
+
writeFile(ctx, "src/sdk/signal.ts", generateSignalShim());
|
|
70
|
+
|
|
71
|
+
// SDK — clx (class name joiner, with default export for compat)
|
|
72
|
+
writeFile(ctx, "src/sdk/clx.ts", generateClxShim());
|
|
73
|
+
|
|
74
|
+
// SDK — debounce (replaces Deno std/async/debounce)
|
|
75
|
+
writeFile(ctx, "src/sdk/debounce.ts", generateDebounceShim());
|
|
76
|
+
|
|
68
77
|
// Apps
|
|
69
78
|
writeFile(ctx, "src/apps/site.ts", generateSiteApp(ctx));
|
|
70
79
|
|
|
@@ -224,6 +233,112 @@ vite.config.timestamp_*
|
|
|
224
233
|
`;
|
|
225
234
|
}
|
|
226
235
|
|
|
236
|
+
function generateClxShim(): string {
|
|
237
|
+
return `/** Filter out nullable values, join and minify class names */
|
|
238
|
+
export const clx = (...args: (string | null | undefined | false)[]) =>
|
|
239
|
+
args.filter(Boolean).join(" ").replace(/\\s\\s+/g, " ");
|
|
240
|
+
|
|
241
|
+
/** Alias for compat — some files import as clsx */
|
|
242
|
+
export const clsx = clx;
|
|
243
|
+
|
|
244
|
+
export default clx;
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function generateDebounceShim(): string {
|
|
249
|
+
return `/** Debounce a function call — drop-in replacement for Deno std/async/debounce */
|
|
250
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
251
|
+
fn: T,
|
|
252
|
+
delay = 250,
|
|
253
|
+
): T & { clear(): void } {
|
|
254
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
255
|
+
|
|
256
|
+
const debounced = ((...args: Parameters<T>) => {
|
|
257
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
258
|
+
timer = setTimeout(() => {
|
|
259
|
+
timer = undefined;
|
|
260
|
+
fn(...args);
|
|
261
|
+
}, delay);
|
|
262
|
+
}) as T & { clear(): void };
|
|
263
|
+
|
|
264
|
+
debounced.clear = () => {
|
|
265
|
+
if (timer !== undefined) {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
timer = undefined;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
return debounced;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export default debounce;
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function generateSignalShim(): string {
|
|
279
|
+
return `import { Store } from "@tanstack/store";
|
|
280
|
+
import { useSyncExternalStore, useMemo, useEffect } from "react";
|
|
281
|
+
|
|
282
|
+
export interface Signal<T> {
|
|
283
|
+
readonly store: Store<T>;
|
|
284
|
+
value: T;
|
|
285
|
+
peek(): T;
|
|
286
|
+
subscribe(fn: () => void): () => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function signal<T>(initialValue: T): Signal<T> {
|
|
290
|
+
const store = new Store<T>(initialValue);
|
|
291
|
+
return {
|
|
292
|
+
store,
|
|
293
|
+
get value() { return store.state; },
|
|
294
|
+
set value(v: T) { store.setState(() => v); },
|
|
295
|
+
peek() { return store.state; },
|
|
296
|
+
subscribe(fn) {
|
|
297
|
+
// @tanstack/store@0.9.x returns { unsubscribe: Function },
|
|
298
|
+
// NOT a plain function. React's useSyncExternalStore cleanup
|
|
299
|
+
// expects a bare function — unwrap it.
|
|
300
|
+
const sub = store.subscribe(() => fn());
|
|
301
|
+
return typeof sub === "function" ? sub : sub.unsubscribe;
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function useSignal<T>(initialValue: T): Signal<T> {
|
|
307
|
+
const sig = useMemo(() => signal(initialValue), []);
|
|
308
|
+
useSyncExternalStore(
|
|
309
|
+
(cb) => sig.subscribe(cb),
|
|
310
|
+
() => sig.value,
|
|
311
|
+
() => sig.value,
|
|
312
|
+
);
|
|
313
|
+
return sig;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export function useComputed<T>(fn: () => T): Signal<T> {
|
|
317
|
+
const sig = useMemo(() => signal(fn()), [fn]);
|
|
318
|
+
return sig;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function computed<T>(fn: () => T): Signal<T> {
|
|
322
|
+
return signal(fn());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function effect(fn: () => void | (() => void)): () => void {
|
|
326
|
+
const cleanup = fn();
|
|
327
|
+
return typeof cleanup === "function" ? cleanup : () => {};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function batch(fn: () => void): void {
|
|
331
|
+
fn();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function useSignalEffect(fn: () => void | (() => void)): void {
|
|
335
|
+
useEffect(fn);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export type { Signal as ReadonlySignal };
|
|
339
|
+
`;
|
|
340
|
+
}
|
|
341
|
+
|
|
227
342
|
function generateSiteApp(ctx: MigrationContext): string {
|
|
228
343
|
return `export type Platform =
|
|
229
344
|
| "vtex"
|
|
@@ -119,4 +119,89 @@ export function transform(ctx: MigrationContext): void {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
console.log(` Transformed ${ctx.transformedFiles.length} files`);
|
|
122
|
+
|
|
123
|
+
// Post-transform: resolve ~/islands/ imports to actual file locations.
|
|
124
|
+
// Islands are moved to src/sections/ during migration, but components
|
|
125
|
+
// import them via ~/islands/X which no longer exists. Scan src/ for
|
|
126
|
+
// the actual file and rewrite the import.
|
|
127
|
+
if (!ctx.dryRun) {
|
|
128
|
+
fixIslandImports(ctx);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scan all transformed files for ~/islands/ imports and rewrite them
|
|
134
|
+
* to the actual path where the file was placed (sections/, components/, etc.).
|
|
135
|
+
*/
|
|
136
|
+
function fixIslandImports(ctx: MigrationContext): void {
|
|
137
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
138
|
+
if (!fs.existsSync(srcDir)) return;
|
|
139
|
+
|
|
140
|
+
// Build a lookup: filename → relative path from src/
|
|
141
|
+
const fileLookup = new Map<string, string[]>();
|
|
142
|
+
function scanDir(dir: string) {
|
|
143
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
146
|
+
scanDir(path.join(dir, entry.name));
|
|
147
|
+
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
148
|
+
const relPath = path.relative(srcDir, path.join(dir, entry.name)).replace(/\\/g, "/");
|
|
149
|
+
const base = entry.name.replace(/\.tsx?$/, "");
|
|
150
|
+
if (!fileLookup.has(base)) fileLookup.set(base, []);
|
|
151
|
+
fileLookup.get(base)!.push(relPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
scanDir(srcDir);
|
|
156
|
+
|
|
157
|
+
// Scan all .ts/.tsx files in src/ for ~/islands/ imports
|
|
158
|
+
const islandImportRe = /from\s+["'](~\/islands\/([^"']+))["']/g;
|
|
159
|
+
let fixCount = 0;
|
|
160
|
+
|
|
161
|
+
function walkAndFix(dir: string) {
|
|
162
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
163
|
+
if (entry.isDirectory()) {
|
|
164
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
165
|
+
walkAndFix(path.join(dir, entry.name));
|
|
166
|
+
} else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
|
|
167
|
+
const filePath = path.join(dir, entry.name);
|
|
168
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
169
|
+
let modified = false;
|
|
170
|
+
|
|
171
|
+
content = content.replace(islandImportRe, (match, fullImport, islandPath) => {
|
|
172
|
+
// islandPath = "Cart/Indicator" or "SliderJS" or "Searchbar"
|
|
173
|
+
const basename = islandPath.replace(/\.tsx?$/, "").split("/").pop()!;
|
|
174
|
+
|
|
175
|
+
// Try to find the file — prefer components/ over sections/
|
|
176
|
+
const candidates = fileLookup.get(basename) || [];
|
|
177
|
+
// Exclude islands/ paths themselves and routes/
|
|
178
|
+
const valid = candidates.filter(
|
|
179
|
+
(c) => !c.startsWith("islands/") && !c.startsWith("routes/"),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (valid.length === 0) return match; // can't resolve, leave as-is
|
|
183
|
+
|
|
184
|
+
// Prefer components/ over sections/
|
|
185
|
+
const preferred =
|
|
186
|
+
valid.find((c) => c.startsWith("components/")) ??
|
|
187
|
+
valid.find((c) => c.startsWith("sections/")) ??
|
|
188
|
+
valid[0];
|
|
189
|
+
|
|
190
|
+
const newPath = "~/" + preferred.replace(/\.tsx?$/, "");
|
|
191
|
+
modified = true;
|
|
192
|
+
return match.replace(fullImport, newPath);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (modified) {
|
|
196
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
197
|
+
fixCount++;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
walkAndFix(srcDir);
|
|
204
|
+
if (fixCount > 0) {
|
|
205
|
+
console.log(` Fixed ~/islands/ imports in ${fixCount} files`);
|
|
206
|
+
}
|
|
122
207
|
}
|
|
@@ -261,14 +261,14 @@ const checks: Check[] = [
|
|
|
261
261
|
},
|
|
262
262
|
},
|
|
263
263
|
{
|
|
264
|
-
name: "No dead cache/cacheKey
|
|
264
|
+
name: "No dead cache/cacheKey exports",
|
|
265
265
|
severity: "warning",
|
|
266
266
|
fn: (ctx) => {
|
|
267
267
|
const srcDir = path.join(ctx.sourceDir, "src");
|
|
268
268
|
if (!fs.existsSync(srcDir)) return true;
|
|
269
|
-
const bad = findFilesWithPattern(srcDir, /^export\s+const\s+(?:cache|cacheKey
|
|
269
|
+
const bad = findFilesWithPattern(srcDir, /^export\s+const\s+(?:cache|cacheKey)\s*=/m);
|
|
270
270
|
if (bad.length > 0) {
|
|
271
|
-
console.log(` Dead exports found (old cache
|
|
271
|
+
console.log(` Dead exports found (old cache system): ${bad.join(", ")}`);
|
|
272
272
|
return false;
|
|
273
273
|
}
|
|
274
274
|
return true;
|
|
@@ -6,13 +6,57 @@ import type { TransformResult } from "../types.ts";
|
|
|
6
6
|
*
|
|
7
7
|
* - `export const cache = "stale-while-revalidate"` (old cache system)
|
|
8
8
|
* - `export const cacheKey = ...` (old cache key generation)
|
|
9
|
-
* - `export const loader = (props, req, ctx) => ...` (old section loader pattern)
|
|
10
9
|
* - `crypto.subtle.digestSync(...)` (Deno-only sync API)
|
|
11
10
|
*
|
|
11
|
+
* NOTE: `export const loader` is kept — it's a server-side function the CMS calls.
|
|
12
12
|
* NOTE: invoke.* calls are NOT migrated — they are RPC calls to the server
|
|
13
13
|
* where the CMS config (API keys, etc.) is available. The runtime.ts invoke
|
|
14
14
|
* proxy handles routing them to /deco/invoke/*.
|
|
15
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
|
+
|
|
16
60
|
export function transformDeadCode(content: string): TransformResult {
|
|
17
61
|
const notes: string[] = [];
|
|
18
62
|
let changed = false;
|
|
@@ -28,13 +72,9 @@ export function transformDeadCode(content: string): TransformResult {
|
|
|
28
72
|
notes.push("Removed dead `export const cache` (old caching system)");
|
|
29
73
|
}
|
|
30
74
|
|
|
31
|
-
// Remove old cacheKey export (can be multiline)
|
|
75
|
+
// Remove old cacheKey export (can be multiline with brace-counting)
|
|
32
76
|
if (/^export\s+const\s+cacheKey\s*=/m.test(result)) {
|
|
33
|
-
|
|
34
|
-
result = result.replace(
|
|
35
|
-
/^export\s+const\s+cacheKey\s*=\s*\([^)]*\)\s*(?::\s*\w+\s*)?=>\s*\{[\s\S]*?\n\};\s*\n?/gm,
|
|
36
|
-
"",
|
|
37
|
-
);
|
|
77
|
+
result = removeExportConstBlock(result, "cacheKey");
|
|
38
78
|
// Also handle simpler inline forms
|
|
39
79
|
result = result.replace(
|
|
40
80
|
/^export\s+const\s+cacheKey\s*=[^;]*;\s*\n?/gm,
|
|
@@ -44,22 +84,8 @@ export function transformDeadCode(content: string): TransformResult {
|
|
|
44
84
|
notes.push("Removed dead `export const cacheKey` (old caching system)");
|
|
45
85
|
}
|
|
46
86
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// In TanStack Start, section loaders are handled differently.
|
|
50
|
-
if (/^export\s+const\s+loader\s*=\s*\(/m.test(result)) {
|
|
51
|
-
result = result.replace(
|
|
52
|
-
/^export\s+const\s+loader\s*=\s*\([^)]*\)\s*(?::\s*[\w<>[\]|&\s]+)?\s*=>\s*\{[\s\S]*?\n\};\s*\n?/gm,
|
|
53
|
-
"",
|
|
54
|
-
);
|
|
55
|
-
// Also handle simpler inline forms
|
|
56
|
-
result = result.replace(
|
|
57
|
-
/^export\s+const\s+loader\s*=\s*\([^)]*\)\s*=>[^;]*;\s*\n?/gm,
|
|
58
|
-
"",
|
|
59
|
-
);
|
|
60
|
-
changed = true;
|
|
61
|
-
notes.push("Removed dead `export const loader` (old section loader — use section loaders in @decocms/start)");
|
|
62
|
-
}
|
|
87
|
+
// NOTE: `export const loader` is kept — these are server-side functions
|
|
88
|
+
// that the CMS calls to modify section props before rendering.
|
|
63
89
|
|
|
64
90
|
// Replace crypto.subtle.digestSync (Deno-only) with a note
|
|
65
91
|
if (result.includes("digestSync")) {
|
|
@@ -143,12 +143,17 @@ export function transformFreshApis(content: string): TransformResult {
|
|
|
143
143
|
);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
//
|
|
147
|
-
//
|
|
146
|
+
// Replace <Head>...</Head> with <>...</>
|
|
147
|
+
// React 19 auto-hoists <title>, <meta>, <link> tags to document <head>.
|
|
148
148
|
if (result.includes("<Head>") || result.includes("<Head ")) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
);
|
|
149
|
+
// Handle self-closing <Head ... /> first so it becomes <></> (not just <>)
|
|
150
|
+
result = result.replace(/<Head\s[^>]*\/>/g, "<></>");
|
|
151
|
+
result = result.replace(/<Head\s*\/>/g, "<></>");
|
|
152
|
+
result = result.replace(/<Head>/g, "<>");
|
|
153
|
+
result = result.replace(/<Head\s[^>]*>/g, "<>");
|
|
154
|
+
result = result.replace(/<\/Head>/g, "</>");
|
|
155
|
+
changed = true;
|
|
156
|
+
notes.push("Replaced <Head> with fragment — React 19 hoists head tags automatically");
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
// scriptAsDataURI → useScript with dangerouslySetInnerHTML
|
|
@@ -15,8 +15,8 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
15
15
|
[/^"preact\/jsx-runtime"$/, null],
|
|
16
16
|
[/^"preact\/compat"$/, `"react"`],
|
|
17
17
|
[/^"preact"$/, `"react"`],
|
|
18
|
-
[/^"@preact\/signals-core"$/,
|
|
19
|
-
[/^"@preact\/signals"$/,
|
|
18
|
+
[/^"@preact\/signals-core"$/, `"~/sdk/signal"`],
|
|
19
|
+
[/^"@preact\/signals"$/, `"~/sdk/signal"`],
|
|
20
20
|
|
|
21
21
|
// Deco framework
|
|
22
22
|
[/^"@deco\/deco\/hooks"$/, `"@decocms/start/sdk/useScript"`],
|
|
@@ -32,25 +32,41 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
32
32
|
[/^"apps\/website\/components\/Theme\.tsx"$/, `"~/components/ui/Theme"`],
|
|
33
33
|
[/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
|
|
34
34
|
|
|
35
|
-
// Apps —
|
|
35
|
+
// Apps — VTEX (hooks, utils, actions, loaders, types)
|
|
36
|
+
[/^"apps\/vtex\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/hooks/$1"`],
|
|
37
|
+
[/^"apps\/vtex\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/utils/$1"`],
|
|
38
|
+
[/^"apps\/vtex\/actions\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/actions/$1"`],
|
|
39
|
+
[/^"apps\/vtex\/loaders\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/vtex/loaders/$1"`],
|
|
40
|
+
[/^"apps\/vtex\/types(?:\.ts)?"$/, `"@decocms/apps/vtex/types"`],
|
|
41
|
+
// Apps — Shopify (hooks, utils, actions, loaders)
|
|
42
|
+
[/^"apps\/shopify\/hooks\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/hooks/$1"`],
|
|
43
|
+
[/^"apps\/shopify\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/utils/$1"`],
|
|
44
|
+
[/^"apps\/shopify\/actions\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/actions/$1"`],
|
|
45
|
+
[/^"apps\/shopify\/loaders\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/shopify/loaders/$1"`],
|
|
46
|
+
// Apps — commerce (types, SDK, utils)
|
|
47
|
+
[/^"apps\/commerce\/sdk\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/sdk/$1"`],
|
|
48
|
+
[/^"apps\/commerce\/utils\/([^"]+?)(?:\.ts)?"$/, `"@decocms/apps/commerce/utils/$1"`],
|
|
49
|
+
|
|
50
|
+
// Apps — catch-all (things like apps/website/mod.ts, apps/analytics/mod.ts, etc.)
|
|
36
51
|
[/^"apps\/([^"]+)"$/, null], // Remove — site.ts is rewritten
|
|
37
52
|
|
|
38
53
|
// Deco old CDN imports
|
|
39
54
|
[/^"deco\/([^"]+)"$/, null],
|
|
40
55
|
|
|
41
|
-
// Std lib —
|
|
56
|
+
// Std lib — redirect useful utils, remove the rest
|
|
57
|
+
[/^"std\/async\/debounce(?:\.ts)?"$/, `"~/sdk/debounce"`],
|
|
42
58
|
[/^"std\/([^"]+)"$/, null],
|
|
43
59
|
[/^"@std\/crypto"$/, null], // Use globalThis.crypto instead
|
|
44
60
|
|
|
45
61
|
// site/sdk/* → framework equivalents (before the catch-all site/ → ~/ rule)
|
|
46
|
-
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"
|
|
62
|
+
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
|
|
47
63
|
[/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
|
|
48
64
|
[/^"site\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
|
|
49
65
|
[/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
|
|
50
66
|
[/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
|
|
51
67
|
|
|
52
68
|
// $store/ → ~/ (common Deno import map alias for project root)
|
|
53
|
-
[/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"
|
|
69
|
+
[/^"\$store\/sdk\/clx(?:\.tsx?)?.*"$/, `"~/sdk/clx"`],
|
|
54
70
|
[/^"\$store\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
|
|
55
71
|
[/^"\$store\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
|
|
56
72
|
[/^"\$store\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
|
|
@@ -67,8 +83,8 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
67
83
|
* The key is the ending of the import path, the value is the replacement specifier.
|
|
68
84
|
*/
|
|
69
85
|
const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
|
|
70
|
-
// sdk/clx →
|
|
71
|
-
[/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "
|
|
86
|
+
// sdk/clx → ~/sdk/clx (scaffolded locally with default export)
|
|
87
|
+
[/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "~/sdk/clx"],
|
|
72
88
|
// sdk/useId → react (useId is built-in in React 19)
|
|
73
89
|
[/(?:\.\.\/)*sdk\/useId(?:\.tsx?)?$/, "react"],
|
|
74
90
|
// sdk/useOffer → @decocms/apps/commerce/sdk/useOffer
|
|
@@ -219,6 +235,25 @@ export function transformImports(content: string): TransformResult {
|
|
|
219
235
|
notes.push("Split useDevice into separate import from @decocms/start/sdk/useDevice");
|
|
220
236
|
}
|
|
221
237
|
|
|
238
|
+
// Rewrite dynamic imports: route through rewriteSpecifier so sdk-specific
|
|
239
|
+
// rules (e.g. site/sdk/useId → react) are applied consistently.
|
|
240
|
+
const dynamicImportRe = /\bimport\(\s*(["'])([^"']+)\1\s*\)/g;
|
|
241
|
+
result = result.replace(dynamicImportRe, (_match, quote, specifier) => {
|
|
242
|
+
const quoted = `"${specifier}"`;
|
|
243
|
+
const rewritten = rewriteSpecifier(quoted);
|
|
244
|
+
if (rewritten === null) {
|
|
245
|
+
// Rule says remove — leave the dynamic import as-is (caller must fix manually)
|
|
246
|
+
return _match;
|
|
247
|
+
}
|
|
248
|
+
if (rewritten !== quoted) {
|
|
249
|
+
const newSpecifier = rewritten.slice(1, -1);
|
|
250
|
+
changed = true;
|
|
251
|
+
notes.push(`Rewrote dynamic import: ${specifier} → ${newSpecifier}`);
|
|
252
|
+
return `import(${quote}${newSpecifier}${quote})`;
|
|
253
|
+
}
|
|
254
|
+
return _match;
|
|
255
|
+
});
|
|
256
|
+
|
|
222
257
|
// Clean up blank lines left by removed imports (collapse multiple to one)
|
|
223
258
|
result = result.replace(/\n{3,}/g, "\n\n");
|
|
224
259
|
|
package/src/sdk/clx.ts
CHANGED
package/src/sdk/signal.ts
CHANGED