@decocms/start 0.32.0 → 0.32.1
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 +1 -1
- package/scripts/migrate/phase-analyze.ts +67 -0
- package/scripts/migrate/phase-cleanup.ts +9 -0
- package/scripts/migrate/phase-verify.ts +63 -3
- package/scripts/migrate/templates/package-json.ts +40 -1
- package/scripts/migrate/templates/routes.ts +1 -1
- package/scripts/migrate/templates/server-entry.ts +22 -7
- package/scripts/migrate/templates/setup.ts +12 -14
- package/scripts/migrate/transforms/deno-isms.ts +10 -15
- package/scripts/migrate/transforms/fresh-apis.ts +109 -2
- package/scripts/migrate/transforms/imports.ts +88 -3
- package/scripts/migrate/transforms/jsx.ts +86 -11
- package/scripts/migrate/types.ts +4 -0
- package/src/types/widgets.ts +1 -0
package/package.json
CHANGED
|
@@ -43,6 +43,9 @@ const SKIP_DIRS = new Set([
|
|
|
43
43
|
"static",
|
|
44
44
|
".context",
|
|
45
45
|
"scripts",
|
|
46
|
+
"src",
|
|
47
|
+
"public",
|
|
48
|
+
".tanstack",
|
|
46
49
|
]);
|
|
47
50
|
|
|
48
51
|
const SKIP_FILES = new Set([
|
|
@@ -52,6 +55,8 @@ const SKIP_FILES = new Set([
|
|
|
52
55
|
"LICENSE",
|
|
53
56
|
"browserslist",
|
|
54
57
|
"bw_stats.json",
|
|
58
|
+
"package.json",
|
|
59
|
+
"package-lock.json",
|
|
55
60
|
]);
|
|
56
61
|
|
|
57
62
|
/** Files that are generated and should be deleted */
|
|
@@ -70,6 +75,12 @@ const SDK_DELETE = new Set([
|
|
|
70
75
|
"sdk/usePlatform.tsx",
|
|
71
76
|
]);
|
|
72
77
|
|
|
78
|
+
/** Loaders that depend on deleted admin tooling */
|
|
79
|
+
const LOADER_DELETE = new Set([
|
|
80
|
+
"loaders/availableIcons.ts",
|
|
81
|
+
"loaders/icons.ts",
|
|
82
|
+
]);
|
|
83
|
+
|
|
73
84
|
/** Root config/infra files to delete */
|
|
74
85
|
const ROOT_DELETE = new Set([
|
|
75
86
|
"main.ts",
|
|
@@ -79,11 +90,39 @@ const ROOT_DELETE = new Set([
|
|
|
79
90
|
"tailwind.css",
|
|
80
91
|
"tailwind.config.ts",
|
|
81
92
|
"runtime.ts",
|
|
93
|
+
"constants.ts",
|
|
82
94
|
"fresh.gen.ts",
|
|
83
95
|
"manifest.gen.ts",
|
|
84
96
|
"fresh.config.ts",
|
|
97
|
+
"browserslist",
|
|
98
|
+
"bw_stats.json",
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
/** Static files that are code/tooling, not assets — should be deleted */
|
|
102
|
+
const STATIC_DELETE = new Set([
|
|
103
|
+
"static/adminIcons.ts",
|
|
104
|
+
"static/generate-icons.ts",
|
|
105
|
+
"static/tailwind.css",
|
|
85
106
|
]);
|
|
86
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Scan file content for inline npm: imports and return { name: version } pairs.
|
|
110
|
+
* Matches patterns like: from "npm:fuse.js@7.0.0"
|
|
111
|
+
*/
|
|
112
|
+
function extractInlineNpmDeps(content: string): Record<string, string> {
|
|
113
|
+
const deps: Record<string, string> = {};
|
|
114
|
+
const regex = /from\s+["']npm:(@?[^@"']+)(?:@([^"']+))?["']/g;
|
|
115
|
+
let match;
|
|
116
|
+
while ((match = regex.exec(content)) !== null) {
|
|
117
|
+
const name = match[1];
|
|
118
|
+
const version = match[2] || "*";
|
|
119
|
+
// Skip framework deps
|
|
120
|
+
if (name.startsWith("preact") || name.startsWith("@preact/")) continue;
|
|
121
|
+
deps[name] = `^${version}`;
|
|
122
|
+
}
|
|
123
|
+
return deps;
|
|
124
|
+
}
|
|
125
|
+
|
|
87
126
|
function detectPatterns(content: string): DetectedPattern[] {
|
|
88
127
|
const patterns: DetectedPattern[] = [];
|
|
89
128
|
for (const [name, regex] of PATTERN_DETECTORS) {
|
|
@@ -154,6 +193,14 @@ function decideAction(
|
|
|
154
193
|
return { action: "delete", notes: "Rewritten from scratch" };
|
|
155
194
|
}
|
|
156
195
|
|
|
196
|
+
// Loaders that depend on deleted admin tooling
|
|
197
|
+
if (LOADER_DELETE.has(relPath)) {
|
|
198
|
+
return {
|
|
199
|
+
action: "delete",
|
|
200
|
+
notes: "Admin icon loader — depends on deleted static/adminIcons.ts",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
157
204
|
// SDK files to delete
|
|
158
205
|
if (SDK_DELETE.has(relPath)) {
|
|
159
206
|
return {
|
|
@@ -192,6 +239,11 @@ function decideAction(
|
|
|
192
239
|
};
|
|
193
240
|
}
|
|
194
241
|
|
|
242
|
+
// Static code/tooling files → delete
|
|
243
|
+
if (STATIC_DELETE.has(relPath)) {
|
|
244
|
+
return { action: "delete", notes: "Code/tooling file, not an asset" };
|
|
245
|
+
}
|
|
246
|
+
|
|
195
247
|
// Static files → move
|
|
196
248
|
if (category === "static") {
|
|
197
249
|
const publicPath = relPath.replace("static/", "public/");
|
|
@@ -329,6 +381,21 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
329
381
|
byCategory[f.category] = (byCategory[f.category] || 0) + 1;
|
|
330
382
|
}
|
|
331
383
|
|
|
384
|
+
// Scan all source files for inline npm: imports
|
|
385
|
+
for (const f of ctx.files) {
|
|
386
|
+
if (f.action === "delete") continue;
|
|
387
|
+
const ext = path.extname(f.path);
|
|
388
|
+
if (![".ts", ".tsx"].includes(ext)) continue;
|
|
389
|
+
try {
|
|
390
|
+
const content = fs.readFileSync(f.absPath, "utf-8");
|
|
391
|
+
const deps = extractInlineNpmDeps(content);
|
|
392
|
+
Object.assign(ctx.discoveredNpmDeps, deps);
|
|
393
|
+
} catch {}
|
|
394
|
+
}
|
|
395
|
+
if (Object.keys(ctx.discoveredNpmDeps).length > 0) {
|
|
396
|
+
log(ctx, `Discovered npm deps from source: ${JSON.stringify(ctx.discoveredNpmDeps)}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
332
399
|
console.log(`\n Files found: ${ctx.files.length}`);
|
|
333
400
|
console.log(` By category: ${JSON.stringify(byCategory)}`);
|
|
334
401
|
console.log(` By action: ${JSON.stringify(byAction)}`);
|
|
@@ -43,6 +43,12 @@ const WRAPPER_FILES_TO_DELETE = [
|
|
|
43
43
|
"sections/Session.tsx",
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
/** Loaders that depend on deleted admin tooling */
|
|
47
|
+
const LOADER_FILES_TO_DELETE = [
|
|
48
|
+
"loaders/availableIcons.ts",
|
|
49
|
+
"loaders/icons.ts",
|
|
50
|
+
];
|
|
51
|
+
|
|
46
52
|
function deleteFileIfExists(ctx: MigrationContext, relPath: string) {
|
|
47
53
|
const fullPath = path.join(ctx.sourceDir, relPath);
|
|
48
54
|
if (!fs.existsSync(fullPath)) return;
|
|
@@ -185,6 +191,9 @@ export function cleanup(ctx: MigrationContext): void {
|
|
|
185
191
|
for (const file of WRAPPER_FILES_TO_DELETE) {
|
|
186
192
|
deleteFileIfExists(ctx, file);
|
|
187
193
|
}
|
|
194
|
+
for (const file of LOADER_FILES_TO_DELETE) {
|
|
195
|
+
deleteFileIfExists(ctx, file);
|
|
196
|
+
}
|
|
188
197
|
|
|
189
198
|
// 3. Delete directories
|
|
190
199
|
console.log(" Deleting old directories...");
|
|
@@ -186,23 +186,83 @@ const checks: Check[] = [
|
|
|
186
186
|
return true;
|
|
187
187
|
},
|
|
188
188
|
},
|
|
189
|
+
{
|
|
190
|
+
name: "No .ts/.tsx extensions in relative import paths",
|
|
191
|
+
severity: "warning",
|
|
192
|
+
fn: (ctx) => {
|
|
193
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
194
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
195
|
+
// Match relative imports with .ts/.tsx extensions
|
|
196
|
+
const bad = findFilesWithPattern(srcDir, /from\s+["'](?:\.\.?\/|~\/)[^"']*\.tsx?["']/);
|
|
197
|
+
if (bad.length > 0) {
|
|
198
|
+
console.log(` Still has .ts/.tsx extensions in imports: ${bad.join(", ")}`);
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "No for= in JSX (should be htmlFor=)",
|
|
206
|
+
severity: "warning",
|
|
207
|
+
fn: (ctx) => {
|
|
208
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
209
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
210
|
+
const bad = findFilesWithPattern(srcDir, /<label[^>]*\sfor\s*=/);
|
|
211
|
+
if (bad.length > 0) {
|
|
212
|
+
console.log(` Still has for= in JSX: ${bad.join(", ")}`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "No relative imports to deleted SDK files",
|
|
220
|
+
severity: "error",
|
|
221
|
+
fn: (ctx) => {
|
|
222
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
223
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
224
|
+
// Only match relative imports (../ or ./) to deleted SDK files, not @decocms/* package imports
|
|
225
|
+
const bad = findFilesWithPattern(srcDir, /from\s+["'](?:\.\.?\/)[^"']*\/sdk\/(?:clx|useId|useOffer|useVariantPossiblities|usePlatform)["']/);
|
|
226
|
+
if (bad.length > 0) {
|
|
227
|
+
console.log(` Still has relative imports to deleted SDK files: ${bad.join(", ")}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: "No imports to deleted static files",
|
|
235
|
+
severity: "error",
|
|
236
|
+
fn: (ctx) => {
|
|
237
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
238
|
+
if (!fs.existsSync(srcDir)) return true;
|
|
239
|
+
const bad = findFilesWithPattern(srcDir, /from\s+["'][^"']*static\/adminIcons/);
|
|
240
|
+
if (bad.length > 0) {
|
|
241
|
+
console.log(` Still has imports to static/adminIcons: ${bad.join(", ")}`);
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
},
|
|
246
|
+
},
|
|
189
247
|
];
|
|
190
248
|
|
|
191
249
|
function findFilesWithPattern(
|
|
192
250
|
dir: string,
|
|
193
251
|
pattern: RegExp,
|
|
194
252
|
results: string[] = [],
|
|
253
|
+
baseDir?: string,
|
|
195
254
|
): string[] {
|
|
255
|
+
const root = baseDir ?? dir;
|
|
196
256
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
197
257
|
for (const entry of entries) {
|
|
198
258
|
const fullPath = path.join(dir, entry.name);
|
|
199
259
|
if (entry.isDirectory()) {
|
|
200
|
-
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
201
|
-
findFilesWithPattern(fullPath, pattern, results);
|
|
260
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "server") continue;
|
|
261
|
+
findFilesWithPattern(fullPath, pattern, results, root);
|
|
202
262
|
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
|
|
203
263
|
const content = fs.readFileSync(fullPath, "utf-8");
|
|
204
264
|
if (pattern.test(content)) {
|
|
205
|
-
results.push(path.
|
|
265
|
+
results.push(path.basename(fullPath));
|
|
206
266
|
}
|
|
207
267
|
}
|
|
208
268
|
}
|
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import type { MigrationContext } from "../types.ts";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Extract npm dependencies from deno.json import map.
|
|
5
|
+
* Entries like `"fuse.js": "npm:fuse.js@7.0.0"` become `"fuse.js": "^7.0.0"`.
|
|
6
|
+
*/
|
|
7
|
+
function extractNpmDeps(importMap: Record<string, string>): Record<string, string> {
|
|
8
|
+
const deps: Record<string, string> = {};
|
|
9
|
+
for (const [key, value] of Object.entries(importMap)) {
|
|
10
|
+
if (!value.startsWith("npm:")) continue;
|
|
11
|
+
// Skip preact, deco, and other framework deps we handle ourselves
|
|
12
|
+
if (key.startsWith("preact") || key.startsWith("@preact/")) continue;
|
|
13
|
+
if (key.startsWith("@deco/")) continue;
|
|
14
|
+
if (key === "daisyui") continue; // we pin our own version
|
|
15
|
+
|
|
16
|
+
const raw = value.slice(4); // remove "npm:"
|
|
17
|
+
const atIdx = raw.lastIndexOf("@");
|
|
18
|
+
if (atIdx <= 0) {
|
|
19
|
+
deps[raw] = "*";
|
|
20
|
+
} else {
|
|
21
|
+
const name = raw.slice(0, atIdx);
|
|
22
|
+
const version = raw.slice(atIdx + 1);
|
|
23
|
+
deps[name] = `^${version}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return deps;
|
|
27
|
+
}
|
|
28
|
+
|
|
3
29
|
export function generatePackageJson(ctx: MigrationContext): string {
|
|
30
|
+
const siteDeps = {
|
|
31
|
+
...extractNpmDeps(ctx.importMap),
|
|
32
|
+
...ctx.discoveredNpmDeps,
|
|
33
|
+
};
|
|
34
|
+
|
|
4
35
|
const pkg = {
|
|
5
36
|
name: ctx.siteName,
|
|
6
37
|
version: "0.1.0",
|
|
@@ -21,12 +52,18 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
21
52
|
format: 'prettier --write "src/**/*.{ts,tsx}"',
|
|
22
53
|
"format:check": 'prettier --check "src/**/*.{ts,tsx}"',
|
|
23
54
|
knip: "knip",
|
|
55
|
+
clean:
|
|
56
|
+
"rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && npm install",
|
|
57
|
+
"tailwind:lint":
|
|
58
|
+
"tsx node_modules/@decocms/start/scripts/tailwind-lint.ts",
|
|
59
|
+
"tailwind:fix":
|
|
60
|
+
"tsx node_modules/@decocms/start/scripts/tailwind-lint.ts --fix",
|
|
24
61
|
},
|
|
25
62
|
author: "deco.cx",
|
|
26
63
|
license: "MIT",
|
|
27
64
|
dependencies: {
|
|
28
65
|
"@decocms/apps": "^0.25.2",
|
|
29
|
-
"@decocms/start": "^0.
|
|
66
|
+
"@decocms/start": "^0.32.0",
|
|
30
67
|
"@tanstack/react-query": "5.90.21",
|
|
31
68
|
"@tanstack/react-router": "1.166.7",
|
|
32
69
|
"@tanstack/react-start": "1.166.8",
|
|
@@ -35,6 +72,7 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
35
72
|
"colorjs.io": "^0.6.1",
|
|
36
73
|
react: "^19.2.4",
|
|
37
74
|
"react-dom": "^19.2.4",
|
|
75
|
+
...siteDeps,
|
|
38
76
|
},
|
|
39
77
|
devDependencies: {
|
|
40
78
|
"@cloudflare/vite-plugin": "^1.27.0",
|
|
@@ -48,6 +86,7 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
48
86
|
knip: "^5.61.2",
|
|
49
87
|
prettier: "^3.5.3",
|
|
50
88
|
tailwindcss: "^4.2.1",
|
|
89
|
+
"ts-morph": "^27.0.2",
|
|
51
90
|
tsx: "^4.19.4",
|
|
52
91
|
typescript: "^5.9.3",
|
|
53
92
|
vite: "^7.3.1",
|
|
@@ -71,7 +71,7 @@ function NavigationProgress() {
|
|
|
71
71
|
function StableOutlet() {
|
|
72
72
|
const isLoading = useRouterState({ select: (s) => s.isLoading });
|
|
73
73
|
const ref = useRef<HTMLDivElement>(null);
|
|
74
|
-
const savedHeight = useRef<number | undefined>();
|
|
74
|
+
const savedHeight = useRef<number | undefined>(undefined);
|
|
75
75
|
|
|
76
76
|
useEffect(() => {
|
|
77
77
|
if (isLoading && ref.current) {
|
|
@@ -22,7 +22,7 @@ export default createStartHandler(defaultStreamHandler);
|
|
|
22
22
|
`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
function generateWorkerEntry(
|
|
25
|
+
function generateWorkerEntry(_ctx: MigrationContext): string {
|
|
26
26
|
return `/**
|
|
27
27
|
* Cloudflare Worker entry point.
|
|
28
28
|
*
|
|
@@ -30,13 +30,28 @@ function generateWorkerEntry(ctx: MigrationContext): string {
|
|
|
30
30
|
* around the TanStack Start handler. Add proxy logic, security headers, or
|
|
31
31
|
* A/B testing as needed.
|
|
32
32
|
*/
|
|
33
|
-
import
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
import "./setup";
|
|
34
|
+
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";
|
|
35
|
+
import { createDecoWorkerEntry } from "@decocms/start/sdk/workerEntry";
|
|
36
|
+
import {
|
|
37
|
+
handleMeta,
|
|
38
|
+
handleDecofileRead,
|
|
39
|
+
handleDecofileReload,
|
|
40
|
+
handleRender,
|
|
41
|
+
corsHeaders,
|
|
42
|
+
} from "@decocms/start/admin";
|
|
43
|
+
|
|
44
|
+
const serverEntry = createServerEntry({ fetch: handler.fetch });
|
|
45
|
+
|
|
46
|
+
export default createDecoWorkerEntry(serverEntry, {
|
|
47
|
+
admin: {
|
|
48
|
+
handleMeta,
|
|
49
|
+
handleDecofileRead,
|
|
50
|
+
handleDecofileReload,
|
|
51
|
+
handleRender,
|
|
52
|
+
corsHeaders,
|
|
53
|
+
},
|
|
37
54
|
});
|
|
38
|
-
|
|
39
|
-
export default handler;
|
|
40
55
|
`;
|
|
41
56
|
}
|
|
42
57
|
|
|
@@ -7,8 +7,17 @@ export function generateSetup(_ctx: MigrationContext): string {
|
|
|
7
7
|
* This file is imported by router.tsx at startup.
|
|
8
8
|
* It uses import.meta.glob to lazily discover all section components.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
registerSections,
|
|
12
|
+
loadBlocks,
|
|
13
|
+
setBlocks,
|
|
14
|
+
} from "@decocms/start/cms";
|
|
15
|
+
import { registerBuiltinMatchers } from "@decocms/start/matchers/builtins";
|
|
16
|
+
|
|
17
|
+
// -- CMS Blocks --
|
|
18
|
+
// Load generated blocks at module level so they're available for resolution.
|
|
19
|
+
const blocks = loadBlocks();
|
|
20
|
+
setBlocks(blocks);
|
|
12
21
|
|
|
13
22
|
// -- Section Registry --
|
|
14
23
|
// Discovers all .tsx files under src/sections/ and registers them as CMS blocks.
|
|
@@ -16,17 +25,6 @@ const sectionModules = import.meta.glob("./sections/**/*.tsx");
|
|
|
16
25
|
registerSections(sectionModules);
|
|
17
26
|
|
|
18
27
|
// -- Matchers --
|
|
19
|
-
|
|
20
|
-
// Example: registerMatcher("device", deviceMatcher);
|
|
21
|
-
|
|
22
|
-
// -- Loader Cache --
|
|
23
|
-
// Register cached loaders here if needed.
|
|
24
|
-
// Example:
|
|
25
|
-
// import { createCachedLoader } from "@decocms/start/loaders";
|
|
26
|
-
// registerLoader("productList", createCachedLoader(vtexProductList, { ttl: 60_000 }));
|
|
27
|
-
|
|
28
|
-
// -- CMS Blocks --
|
|
29
|
-
// Load generated blocks at module level so they're available for resolution.
|
|
30
|
-
import "./server/cms/blocks.gen";
|
|
28
|
+
registerBuiltinMatchers();
|
|
31
29
|
`;
|
|
32
30
|
}
|
|
@@ -14,33 +14,28 @@ export function transformDenoIsms(content: string): TransformResult {
|
|
|
14
14
|
let result = content;
|
|
15
15
|
|
|
16
16
|
// Remove deno-lint-ignore comments (single line and file-level)
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
result = result.replace(
|
|
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, " ");
|
|
20
21
|
changed = true;
|
|
21
22
|
notes.push("Removed deno-lint-ignore comments");
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
// Remove npm: prefix in import specifiers that weren't caught by imports transform
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if (/from\s+["']npm:/.test(result)) {
|
|
27
|
+
// npm:pkg@version → pkg (strip version)
|
|
27
28
|
result = result.replace(
|
|
28
|
-
/(from\s+["'])npm:([
|
|
29
|
+
/(from\s+["'])npm:(@?[^@"']+)@[^"']*(["'])/g,
|
|
29
30
|
"$1$2$3",
|
|
30
31
|
);
|
|
31
|
-
|
|
32
|
-
notes.push("Removed npm: prefix from imports");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// npm:pkg@version → pkg (strip version too)
|
|
36
|
-
const npmVersionRegex = /(from\s+["'])npm:(@?[^@"']+)@[^"']*(["'])/g;
|
|
37
|
-
if (npmVersionRegex.test(result)) {
|
|
32
|
+
// npm:pkg → pkg (no version)
|
|
38
33
|
result = result.replace(
|
|
39
|
-
/(from\s+["'])npm:(
|
|
34
|
+
/(from\s+["'])npm:([^"'@][^"']*)(["'])/g,
|
|
40
35
|
"$1$2$3",
|
|
41
36
|
);
|
|
42
37
|
changed = true;
|
|
43
|
-
notes.push("Removed npm: prefix
|
|
38
|
+
notes.push("Removed npm: prefix from imports");
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
// Remove Deno.* API usages — flag for manual review
|
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import type { TransformResult } from "../types.ts";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Fix JSX after scriptAsDataURI → useScript replacement.
|
|
5
|
+
*
|
|
6
|
+
* Transforms patterns like:
|
|
7
|
+
* <script dangerouslySetInnerHTML={{ __html: useScript(fn, { ...props, x })}
|
|
8
|
+
* defer
|
|
9
|
+
* />
|
|
10
|
+
* Into:
|
|
11
|
+
* <script dangerouslySetInnerHTML={{ __html: useScript(fn, { ...props, x }) }}
|
|
12
|
+
* />
|
|
13
|
+
*
|
|
14
|
+
* The key issue: the original `src={scriptAsDataURI(...)}` has one closing `}`,
|
|
15
|
+
* but `dangerouslySetInnerHTML={{ __html: useScript(...) }}` needs two closing `}}`.
|
|
16
|
+
* We also need to remove stray attrs like `defer` that sit between the call and `/>`.
|
|
17
|
+
*/
|
|
18
|
+
function rebalanceScriptDataUri(code: string): string {
|
|
19
|
+
const marker = "dangerouslySetInnerHTML={{ __html: useScript(";
|
|
20
|
+
let idx = code.indexOf(marker);
|
|
21
|
+
|
|
22
|
+
while (idx !== -1) {
|
|
23
|
+
const start = idx + marker.length;
|
|
24
|
+
// Find the balanced closing paren for useScript(
|
|
25
|
+
let depth = 1;
|
|
26
|
+
let i = start;
|
|
27
|
+
while (i < code.length && depth > 0) {
|
|
28
|
+
if (code[i] === "(") depth++;
|
|
29
|
+
else if (code[i] === ")") depth--;
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
// i is now right after the matching ) of useScript(...)
|
|
33
|
+
// We expect `}` next (closing the old src={...})
|
|
34
|
+
// We need to replace everything from ) to /> with `) }} />`
|
|
35
|
+
// and remove any stray attributes like `defer`, `type="module"`, etc.
|
|
36
|
+
const afterParen = code.substring(i);
|
|
37
|
+
const closingMatch = afterParen.match(/^\s*\}\s*\n?\s*([\s\S]*?)\s*\/>/);
|
|
38
|
+
if (closingMatch) {
|
|
39
|
+
const endOffset = i + closingMatch[0].length;
|
|
40
|
+
// i is already past the closing ), so just add the }} and />
|
|
41
|
+
const replacement = ` }}\n />`;
|
|
42
|
+
code = code.substring(0, i) + replacement + code.substring(endOffset);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
idx = code.indexOf(marker, idx + 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return code;
|
|
49
|
+
}
|
|
50
|
+
|
|
3
51
|
/**
|
|
4
52
|
* Removes or replaces Fresh-specific APIs:
|
|
5
53
|
*
|
|
@@ -15,8 +63,7 @@ export function transformFreshApis(content: string): TransformResult {
|
|
|
15
63
|
let result = content;
|
|
16
64
|
|
|
17
65
|
// asset("/path") → "/path" and asset(`/path`) → `/path`
|
|
18
|
-
|
|
19
|
-
if (assetCallRegex.test(result)) {
|
|
66
|
+
if (/\basset\(/.test(result)) {
|
|
20
67
|
result = result.replace(
|
|
21
68
|
/\basset\(\s*(`[^`]+`|"[^"]+"|'[^']+')\s*\)/g,
|
|
22
69
|
(_match, path) => {
|
|
@@ -104,6 +151,66 @@ export function transformFreshApis(content: string): TransformResult {
|
|
|
104
151
|
);
|
|
105
152
|
}
|
|
106
153
|
|
|
154
|
+
// scriptAsDataURI → useScript with dangerouslySetInnerHTML
|
|
155
|
+
// scriptAsDataURI is a Fresh pattern that returns a data: URI for <script src=...>.
|
|
156
|
+
// In React/TanStack, useScript returns a string for dangerouslySetInnerHTML.
|
|
157
|
+
//
|
|
158
|
+
// Before: <script src={scriptAsDataURI(fn, arg1, arg2)} defer />
|
|
159
|
+
// After: <script dangerouslySetInnerHTML={{ __html: useScript(fn, arg1, arg2) }} />
|
|
160
|
+
if (result.includes("scriptAsDataURI")) {
|
|
161
|
+
// Ensure useScript is imported
|
|
162
|
+
if (
|
|
163
|
+
!result.includes('"@decocms/start/sdk/useScript"') &&
|
|
164
|
+
!result.includes("'@decocms/start/sdk/useScript'")
|
|
165
|
+
) {
|
|
166
|
+
result = `import { useScript } from "@decocms/start/sdk/useScript";\n${result}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Transform src={scriptAsDataURI(...)} into dangerouslySetInnerHTML={{ __html: useScript(...) }}
|
|
170
|
+
// We need to match balanced parens to capture the full argument list.
|
|
171
|
+
result = result.replace(
|
|
172
|
+
/\bsrc=\{scriptAsDataURI\(/g,
|
|
173
|
+
"dangerouslySetInnerHTML={{ __html: useScript(",
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Now close the pattern: find the matching )} and replace with ) }}
|
|
177
|
+
// The pattern after replacement is: dangerouslySetInnerHTML={{ __html: useScript(...)}<maybe whitespace and other attrs>
|
|
178
|
+
// We need to find the closing )} that ends the JSX expression
|
|
179
|
+
result = rebalanceScriptDataUri(result);
|
|
180
|
+
|
|
181
|
+
// Replace any remaining standalone scriptAsDataURI references
|
|
182
|
+
result = result.replace(/\bscriptAsDataURI\b/g, "useScript");
|
|
183
|
+
|
|
184
|
+
changed = true;
|
|
185
|
+
notes.push("Replaced scriptAsDataURI with useScript + dangerouslySetInnerHTML");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// allowCorsFor — not available in @decocms/start, remove usage
|
|
189
|
+
if (result.includes("allowCorsFor")) {
|
|
190
|
+
result = result.replace(
|
|
191
|
+
/^import\s+\{[^}]*\ballowCorsFor\b[^}]*\}\s+from\s+["'][^"']+["'];?\s*\n?/gm,
|
|
192
|
+
"",
|
|
193
|
+
);
|
|
194
|
+
// Remove allowCorsFor calls
|
|
195
|
+
result = result.replace(/\ballowCorsFor\b\([^)]*\);?\s*\n?/g, "");
|
|
196
|
+
changed = true;
|
|
197
|
+
notes.push("Removed allowCorsFor (not needed in TanStack)");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ctx.response.headers → not available, flag
|
|
201
|
+
if (result.includes("ctx.response")) {
|
|
202
|
+
notes.push("MANUAL: ctx.response usage found — FnContext in @decocms/start does not have response object");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// { crypto } from "@std/crypto" → use globalThis.crypto (Web Crypto API)
|
|
206
|
+
// The import is already removed by imports transform, but `crypto` references
|
|
207
|
+
// need to be prefixed with globalThis if they'd shadow the global
|
|
208
|
+
if (result.match(/^import\s+\{[^}]*\bcrypto\b/m)) {
|
|
209
|
+
// Import already removed by imports transform, so just ensure bare `crypto` works
|
|
210
|
+
// No action needed — globalThis.crypto is available in Workers + Node 20+
|
|
211
|
+
notes.push("INFO: @std/crypto replaced with globalThis.crypto (Web Crypto API)");
|
|
212
|
+
}
|
|
213
|
+
|
|
107
214
|
// Clean up blank lines
|
|
108
215
|
result = result.replace(/\n{3,}/g, "\n\n");
|
|
109
216
|
|
|
@@ -25,7 +25,7 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
25
25
|
[/^"@deco\/deco"$/, `"@decocms/start"`],
|
|
26
26
|
|
|
27
27
|
// Apps — widgets & components
|
|
28
|
-
[/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/
|
|
28
|
+
[/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/types/widgets"`],
|
|
29
29
|
[/^"apps\/website\/components\/Image\.tsx"$/, `"@decocms/apps/commerce/components/Image"`],
|
|
30
30
|
[/^"apps\/website\/components\/Picture\.tsx"$/, `"@decocms/apps/commerce/components/Picture"`],
|
|
31
31
|
[/^"apps\/website\/components\/Video\.tsx"$/, `"@decocms/apps/commerce/components/Video"`],
|
|
@@ -37,13 +37,41 @@ const IMPORT_RULES: Array<[RegExp, string | null]> = [
|
|
|
37
37
|
// Deco old CDN imports
|
|
38
38
|
[/^"deco\/([^"]+)"$/, null],
|
|
39
39
|
|
|
40
|
-
// Std lib — not needed in Node
|
|
40
|
+
// Std lib — not needed in Node (Deno std lib)
|
|
41
41
|
[/^"std\/([^"]+)"$/, null],
|
|
42
|
+
[/^"@std\/crypto"$/, null], // Use globalThis.crypto instead
|
|
43
|
+
|
|
44
|
+
// site/sdk/* → framework equivalents (before the catch-all site/ → ~/ rule)
|
|
45
|
+
[/^"site\/sdk\/clx(?:\.tsx?)?.*"$/, `"@decocms/start/sdk/clx"`],
|
|
46
|
+
[/^"site\/sdk\/useId(?:\.tsx?)?.*"$/, `"react"`],
|
|
47
|
+
[/^"site\/sdk\/useOffer(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useOffer"`],
|
|
48
|
+
[/^"site\/sdk\/useVariantPossiblities(?:\.tsx?)?.*"$/, `"@decocms/apps/commerce/sdk/useVariantPossibilities"`],
|
|
49
|
+
[/^"site\/sdk\/usePlatform(?:\.tsx?)?.*"$/, null],
|
|
42
50
|
|
|
43
51
|
// site/ → ~/
|
|
44
52
|
[/^"site\/(.+)"$/, `"~/$1"`],
|
|
45
53
|
];
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Relative import rewrites for SDK files that are deleted during migration.
|
|
57
|
+
* These are matched against the resolved import path (after ../.. resolution).
|
|
58
|
+
* The key is the ending of the import path, the value is the replacement specifier.
|
|
59
|
+
*/
|
|
60
|
+
const RELATIVE_SDK_REWRITES: Array<[RegExp, string]> = [
|
|
61
|
+
// sdk/clx → @decocms/start/sdk/clx
|
|
62
|
+
[/(?:\.\.\/)*sdk\/clx(?:\.tsx?)?$/, "@decocms/start/sdk/clx"],
|
|
63
|
+
// sdk/useId → react (useId is built-in in React 19)
|
|
64
|
+
[/(?:\.\.\/)*sdk\/useId(?:\.tsx?)?$/, "react"],
|
|
65
|
+
// sdk/useOffer → @decocms/apps/commerce/sdk/useOffer
|
|
66
|
+
[/(?:\.\.\/)*sdk\/useOffer(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useOffer"],
|
|
67
|
+
// sdk/useVariantPossiblities → @decocms/apps/commerce/sdk/useVariantPossibilities
|
|
68
|
+
[/(?:\.\.\/)*sdk\/useVariantPossiblities(?:\.tsx?)?$/, "@decocms/apps/commerce/sdk/useVariantPossibilities"],
|
|
69
|
+
// sdk/usePlatform → remove entirely
|
|
70
|
+
[/(?:\.\.\/)*sdk\/usePlatform(?:\.tsx?)?$/, ""],
|
|
71
|
+
// static/adminIcons → deleted (icon loaders need rewriting)
|
|
72
|
+
[/(?:\.\.\/)*static\/adminIcons(?:\.ts)?$/, ""],
|
|
73
|
+
];
|
|
74
|
+
|
|
47
75
|
/**
|
|
48
76
|
* Rewrites import specifiers in a file.
|
|
49
77
|
*
|
|
@@ -67,6 +95,36 @@ export function transformImports(content: string): TransformResult {
|
|
|
67
95
|
/^(export\s+(?:type\s+)?\{[^}]*\}\s+from\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
|
|
68
96
|
const sideEffectImportRegex = /^(import\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
|
|
69
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Post-process: split @deco/deco/hooks imports.
|
|
100
|
+
* In the old stack, @deco/deco/hooks exported useDevice, useScript, useSection, etc.
|
|
101
|
+
* In @decocms/start, useDevice is at @decocms/start/sdk/useDevice.
|
|
102
|
+
* After import rewriting, we need to split lines like:
|
|
103
|
+
* import { useDevice, useScript } from "@decocms/start/sdk/useScript"
|
|
104
|
+
* into:
|
|
105
|
+
* import { useDevice } from "@decocms/start/sdk/useDevice"
|
|
106
|
+
* import { useScript } from "@decocms/start/sdk/useScript"
|
|
107
|
+
*/
|
|
108
|
+
function splitDecoHooksImports(code: string): string {
|
|
109
|
+
return code.replace(
|
|
110
|
+
/^(import\s+(?:type\s+)?\{)([^}]*\buseDevice\b[^}]*)(\}\s+from\s+["']@decocms\/start\/sdk\/useScript["'];?)$/gm,
|
|
111
|
+
(_match, _prefix, importList, _suffix) => {
|
|
112
|
+
const items = importList.split(",").map((s: string) => s.trim()).filter(Boolean);
|
|
113
|
+
const deviceItems = items.filter((s: string) => s.includes("useDevice"));
|
|
114
|
+
const otherItems = items.filter((s: string) => !s.includes("useDevice"));
|
|
115
|
+
|
|
116
|
+
const lines: string[] = [];
|
|
117
|
+
if (deviceItems.length > 0) {
|
|
118
|
+
lines.push(`import { ${deviceItems.join(", ")} } from "@decocms/start/sdk/useDevice";`);
|
|
119
|
+
}
|
|
120
|
+
if (otherItems.length > 0) {
|
|
121
|
+
lines.push(`import { ${otherItems.join(", ")} } from "@decocms/start/sdk/useScript";`);
|
|
122
|
+
}
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
70
128
|
function rewriteSpecifier(specifier: string): string | null {
|
|
71
129
|
// Remove quotes for matching
|
|
72
130
|
const inner = specifier.slice(1, -1);
|
|
@@ -75,7 +133,26 @@ export function transformImports(content: string): TransformResult {
|
|
|
75
133
|
if (pattern.test(`"${inner}"`)) {
|
|
76
134
|
if (replacement === null) return null;
|
|
77
135
|
// Apply regex replacement
|
|
78
|
-
|
|
136
|
+
let result = `"${inner}"`.replace(pattern, replacement);
|
|
137
|
+
// Strip .ts/.tsx extensions from the rewritten path if it's a relative/alias import
|
|
138
|
+
const resultInner = result.slice(1, -1);
|
|
139
|
+
if (
|
|
140
|
+
(resultInner.startsWith("~/") || resultInner.startsWith("./") || resultInner.startsWith("../")) &&
|
|
141
|
+
(resultInner.endsWith(".ts") || resultInner.endsWith(".tsx"))
|
|
142
|
+
) {
|
|
143
|
+
result = `"${resultInner.replace(/\.tsx?$/, "")}"`;
|
|
144
|
+
}
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Relative imports pointing to deleted SDK files → framework equivalents
|
|
150
|
+
if (inner.startsWith("./") || inner.startsWith("../")) {
|
|
151
|
+
for (const [pattern, replacement] of RELATIVE_SDK_REWRITES) {
|
|
152
|
+
if (pattern.test(inner)) {
|
|
153
|
+
if (replacement === "") return null; // remove the import
|
|
154
|
+
return `"${replacement}"`;
|
|
155
|
+
}
|
|
79
156
|
}
|
|
80
157
|
}
|
|
81
158
|
|
|
@@ -125,6 +202,14 @@ export function transformImports(content: string): TransformResult {
|
|
|
125
202
|
result = result.replace(reExportLineRegex, processLine);
|
|
126
203
|
result = result.replace(sideEffectImportRegex, processLine);
|
|
127
204
|
|
|
205
|
+
// Split @deco/deco/hooks imports that contain useDevice
|
|
206
|
+
const afterSplit = splitDecoHooksImports(result);
|
|
207
|
+
if (afterSplit !== result) {
|
|
208
|
+
result = afterSplit;
|
|
209
|
+
changed = true;
|
|
210
|
+
notes.push("Split useDevice into separate import from @decocms/start/sdk/useDevice");
|
|
211
|
+
}
|
|
212
|
+
|
|
128
213
|
// Clean up blank lines left by removed imports (collapse multiple to one)
|
|
129
214
|
result = result.replace(/\n{3,}/g, "\n\n");
|
|
130
215
|
|
|
@@ -17,8 +17,7 @@ export function transformJsx(content: string): TransformResult {
|
|
|
17
17
|
|
|
18
18
|
// class= → className= in JSX attributes
|
|
19
19
|
// Match class= that's preceded by whitespace and inside a JSX tag
|
|
20
|
-
|
|
21
|
-
if (classAttrRegex.test(result)) {
|
|
20
|
+
if (/(<[a-zA-Z][^>]*?\s)class(\s*=)/.test(result)) {
|
|
22
21
|
result = result.replace(
|
|
23
22
|
/(<[a-zA-Z][^>]*?\s)class(\s*=)/g,
|
|
24
23
|
"$1className$2",
|
|
@@ -28,9 +27,8 @@ export function transformJsx(content: string): TransformResult {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
// Also handle class= at the start of a line in JSX (multi-line attributes)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
result = result.replace(standaloneClassRegex, "$1className$2");
|
|
30
|
+
if (/^(\s+)class(\s*=)/m.test(result)) {
|
|
31
|
+
result = result.replace(/^(\s+)class(\s*=)/gm, "$1className$2");
|
|
34
32
|
changed = true;
|
|
35
33
|
}
|
|
36
34
|
|
|
@@ -41,15 +39,46 @@ export function transformJsx(content: string): TransformResult {
|
|
|
41
39
|
notes.push("Replaced onInput= with onChange=");
|
|
42
40
|
}
|
|
43
41
|
|
|
44
|
-
//
|
|
42
|
+
// for= → htmlFor= in JSX (label elements)
|
|
43
|
+
if (/(<(?:label|Label)[^>]*?\s)for(\s*=)/.test(result)) {
|
|
44
|
+
result = result.replace(
|
|
45
|
+
/(<(?:label|Label)[^>]*?\s)for(\s*=)/g,
|
|
46
|
+
"$1htmlFor$2",
|
|
47
|
+
);
|
|
48
|
+
changed = true;
|
|
49
|
+
notes.push("Replaced for= with htmlFor= on label elements");
|
|
50
|
+
}
|
|
51
|
+
// Also handle for= at the start of a line in multi-line JSX attributes
|
|
52
|
+
if (/^\s+for\s*=\s*\{/m.test(result)) {
|
|
53
|
+
result = result.replace(/^(\s+)for(\s*=\s*\{)/gm, "$1htmlFor$2");
|
|
54
|
+
changed = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ComponentChildren → ReactNode (named import, not React.ReactNode)
|
|
45
58
|
if (result.includes("ComponentChildren")) {
|
|
46
|
-
result = result.replace(/\bComponentChildren\b/g, "
|
|
47
|
-
// Add
|
|
48
|
-
if (
|
|
49
|
-
result
|
|
59
|
+
result = result.replace(/\bComponentChildren\b/g, "ReactNode");
|
|
60
|
+
// Add ReactNode import if not already imported
|
|
61
|
+
if (
|
|
62
|
+
!result.match(/\bReactNode\b.*from\s+["']react["']/) &&
|
|
63
|
+
!result.match(/from\s+["']react["'].*\bReactNode\b/)
|
|
64
|
+
) {
|
|
65
|
+
// Check if there's already a react import we can extend
|
|
66
|
+
const reactImportMatch = result.match(
|
|
67
|
+
/^(import\s+(?:type\s+)?\{)([^}]*?)(\}\s+from\s+["']react["'];?)$/m,
|
|
68
|
+
);
|
|
69
|
+
if (reactImportMatch) {
|
|
70
|
+
const [fullMatch, prefix, existing, suffix] = reactImportMatch;
|
|
71
|
+
const items = existing.trim();
|
|
72
|
+
result = result.replace(
|
|
73
|
+
fullMatch,
|
|
74
|
+
`${prefix}${items ? `${items}, ` : ""}type ReactNode${suffix}`,
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
result = `import type { ReactNode } from "react";\n${result}`;
|
|
78
|
+
}
|
|
50
79
|
}
|
|
51
80
|
changed = true;
|
|
52
|
-
notes.push("Replaced ComponentChildren with
|
|
81
|
+
notes.push("Replaced ComponentChildren with ReactNode");
|
|
53
82
|
}
|
|
54
83
|
|
|
55
84
|
// JSX.SVGAttributes<SVGSVGElement> → React.SVGAttributes<SVGSVGElement>
|
|
@@ -96,6 +125,52 @@ export function transformJsx(content: string): TransformResult {
|
|
|
96
125
|
"",
|
|
97
126
|
);
|
|
98
127
|
|
|
128
|
+
// tabindex → tabIndex in JSX
|
|
129
|
+
if (/\btabindex\s*=/.test(result)) {
|
|
130
|
+
result = result.replace(/\btabindex(\s*=)/g, "tabIndex$1");
|
|
131
|
+
changed = true;
|
|
132
|
+
notes.push("Replaced tabindex with tabIndex");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// frameBorder → frameBorder (already camelCase, but just in case)
|
|
136
|
+
// referrerpolicy → referrerPolicy
|
|
137
|
+
if (result.includes("referrerpolicy=")) {
|
|
138
|
+
result = result.replace(/referrerpolicy=/g, "referrerPolicy=");
|
|
139
|
+
changed = true;
|
|
140
|
+
notes.push("Replaced referrerpolicy with referrerPolicy");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// allowFullScreen={true} is fine in React, but allowfullscreen is not
|
|
144
|
+
if (result.includes("allowfullscreen")) {
|
|
145
|
+
result = result.replace(/\ballowfullscreen\b/g, "allowFullScreen");
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// `class` as a prop name in destructuring patterns → `className`
|
|
150
|
+
// Matches: { class: someVar } or { class, } or { ..., class: x } in function params
|
|
151
|
+
if (/[{,]\s*class\s*[,}:]/.test(result)) {
|
|
152
|
+
// class: varName → className: varName (anywhere in destructuring)
|
|
153
|
+
result = result.replace(
|
|
154
|
+
/([{,]\s*)class(\s*:\s*\w+)/g,
|
|
155
|
+
"$1className$2",
|
|
156
|
+
);
|
|
157
|
+
// class, → className, (shorthand, anywhere in destructuring)
|
|
158
|
+
result = result.replace(
|
|
159
|
+
/([{,]\s*)class(\s*[,}])/g,
|
|
160
|
+
"$1className$2",
|
|
161
|
+
);
|
|
162
|
+
changed = true;
|
|
163
|
+
notes.push("Replaced 'class' prop in destructuring with 'className'");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// `class` in interface/type definitions → className
|
|
167
|
+
// Matches: class?: string; or class: string;
|
|
168
|
+
if (/^\s+class\??\s*:/m.test(result)) {
|
|
169
|
+
result = result.replace(/^(\s+)class(\??\s*:)/gm, "$1className$2");
|
|
170
|
+
changed = true;
|
|
171
|
+
notes.push("Replaced 'class' in interface definitions with 'className'");
|
|
172
|
+
}
|
|
173
|
+
|
|
99
174
|
// Ensure React import exists if we introduced React.* references
|
|
100
175
|
if (
|
|
101
176
|
(result.includes("React.") || result.includes("React,")) &&
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -72,6 +72,9 @@ export interface MigrationContext {
|
|
|
72
72
|
/** deno.json import map entries */
|
|
73
73
|
importMap: Record<string, string>;
|
|
74
74
|
|
|
75
|
+
/** npm dependencies discovered from inline npm: imports in source files */
|
|
76
|
+
discoveredNpmDeps: Record<string, string>;
|
|
77
|
+
|
|
75
78
|
/** All categorized source files */
|
|
76
79
|
files: FileRecord[];
|
|
77
80
|
|
|
@@ -114,6 +117,7 @@ export function createContext(
|
|
|
114
117
|
platform: "custom",
|
|
115
118
|
gtmId: null,
|
|
116
119
|
importMap: {},
|
|
120
|
+
discoveredNpmDeps: {},
|
|
117
121
|
files: [],
|
|
118
122
|
scaffoldedFiles: [],
|
|
119
123
|
transformedFiles: [],
|
package/src/types/widgets.ts
CHANGED