@decocms/start 0.31.0 → 0.32.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 +3 -1
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/phase-analyze.ts +335 -0
- package/scripts/migrate/phase-cleanup.ts +203 -0
- package/scripts/migrate/phase-report.ts +171 -0
- package/scripts/migrate/phase-scaffold.ts +133 -0
- package/scripts/migrate/phase-transform.ts +102 -0
- package/scripts/migrate/phase-verify.ts +248 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/package-json.ts +59 -0
- package/scripts/migrate/templates/routes.ts +280 -0
- package/scripts/migrate/templates/server-entry.ts +148 -0
- package/scripts/migrate/templates/setup.ts +32 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/vite-config.ts +108 -0
- package/scripts/migrate/templates/wrangler.ts +25 -0
- package/scripts/migrate/transforms/deno-isms.ts +64 -0
- package/scripts/migrate/transforms/fresh-apis.ts +111 -0
- package/scripts/migrate/transforms/imports.ts +132 -0
- package/scripts/migrate/transforms/jsx.ts +109 -0
- package/scripts/migrate/transforms/tailwind.ts +409 -0
- package/scripts/migrate/types.ts +137 -0
- package/scripts/migrate.ts +135 -0
- package/scripts/tailwind-lint.ts +518 -0
- package/src/cms/resolve.ts +27 -5
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { TransformResult } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes Deno-specific patterns:
|
|
5
|
+
*
|
|
6
|
+
* - // deno-lint-ignore ... comments
|
|
7
|
+
* - // deno-lint-ignore-file comments
|
|
8
|
+
* - npm: prefix from import specifiers (already in imports.ts but this catches stragglers)
|
|
9
|
+
* - Strip .ts/.tsx extensions from local import paths
|
|
10
|
+
*/
|
|
11
|
+
export function transformDenoIsms(content: string): TransformResult {
|
|
12
|
+
const notes: string[] = [];
|
|
13
|
+
let changed = false;
|
|
14
|
+
let result = content;
|
|
15
|
+
|
|
16
|
+
// Remove deno-lint-ignore comments (single line and file-level)
|
|
17
|
+
const denoLintRegex = /^\s*\/\/\s*deno-lint-ignore[^\n]*\n?/gm;
|
|
18
|
+
if (denoLintRegex.test(result)) {
|
|
19
|
+
result = result.replace(denoLintRegex, "");
|
|
20
|
+
changed = true;
|
|
21
|
+
notes.push("Removed deno-lint-ignore comments");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Remove npm: prefix in import specifiers that weren't caught by imports transform
|
|
25
|
+
const npmPrefixRegex = /(from\s+["'])npm:([^"'@][^"']*)(["'])/g;
|
|
26
|
+
if (npmPrefixRegex.test(result)) {
|
|
27
|
+
result = result.replace(
|
|
28
|
+
/(from\s+["'])npm:([^"'@][^"']*)(["'])/g,
|
|
29
|
+
"$1$2$3",
|
|
30
|
+
);
|
|
31
|
+
changed = true;
|
|
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)) {
|
|
38
|
+
result = result.replace(
|
|
39
|
+
/(from\s+["'])npm:(@?[^@"']+)@[^"']*(["'])/g,
|
|
40
|
+
"$1$2$3",
|
|
41
|
+
);
|
|
42
|
+
changed = true;
|
|
43
|
+
notes.push("Removed npm: prefix and version from imports");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Remove Deno.* API usages — flag for manual review
|
|
47
|
+
if (result.includes("Deno.")) {
|
|
48
|
+
notes.push("MANUAL: Deno.* API usage found — needs Node.js equivalent");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Remove /// <reference ... /> directives for Deno
|
|
52
|
+
const refDirectiveRegex =
|
|
53
|
+
/^\/\/\/\s*<reference\s+(?:lib|path|types)\s*=\s*"[^"]*deno[^"]*"\s*\/>\s*\n?/gm;
|
|
54
|
+
if (refDirectiveRegex.test(result)) {
|
|
55
|
+
result = result.replace(refDirectiveRegex, "");
|
|
56
|
+
changed = true;
|
|
57
|
+
notes.push("Removed Deno reference directives");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Clean up blank lines
|
|
61
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
62
|
+
|
|
63
|
+
return { content: result, changed, notes };
|
|
64
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { TransformResult } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removes or replaces Fresh-specific APIs:
|
|
5
|
+
*
|
|
6
|
+
* - asset("/path") → "/path"
|
|
7
|
+
* - <Head>...</Head> → content extracted or removed
|
|
8
|
+
* - defineApp wrapper → unwrap
|
|
9
|
+
* - IS_BROWSER → typeof window !== "undefined"
|
|
10
|
+
* - Context.active().release?.revision() → "" (Vite handles cache busting)
|
|
11
|
+
*/
|
|
12
|
+
export function transformFreshApis(content: string): TransformResult {
|
|
13
|
+
const notes: string[] = [];
|
|
14
|
+
let changed = false;
|
|
15
|
+
let result = content;
|
|
16
|
+
|
|
17
|
+
// asset("/path") → "/path" and asset(`/path`) → `/path`
|
|
18
|
+
const assetCallRegex = /\basset\(\s*(`[^`]+`|"[^"]+"|'[^']+')\s*\)/g;
|
|
19
|
+
if (assetCallRegex.test(result)) {
|
|
20
|
+
result = result.replace(
|
|
21
|
+
/\basset\(\s*(`[^`]+`|"[^"]+"|'[^']+')\s*\)/g,
|
|
22
|
+
(_match, path) => {
|
|
23
|
+
// For template literals with revision, simplify
|
|
24
|
+
const inner = path.slice(1, -1);
|
|
25
|
+
if (inner.includes("${revision}") || inner.includes("?revision=")) {
|
|
26
|
+
// Remove cache-busting query — Vite handles it
|
|
27
|
+
const clean = inner
|
|
28
|
+
.replace(/\?revision=\$\{revision\}/, "")
|
|
29
|
+
.replace(/\$\{revision\}/, "");
|
|
30
|
+
return `"${clean}"`;
|
|
31
|
+
}
|
|
32
|
+
return path;
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
changed = true;
|
|
36
|
+
notes.push("Replaced asset() calls with direct paths");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Remove import { asset, Head } from "$fresh/runtime.ts"
|
|
40
|
+
// (the imports transform handles the specifier, but we also need to handle
|
|
41
|
+
// cases where the import line wasn't fully removed)
|
|
42
|
+
result = result.replace(
|
|
43
|
+
/^import\s+\{[^}]*\b(?:asset|Head)\b[^}]*\}\s+from\s+["']\$fresh\/runtime\.ts["'];?\s*\n?/gm,
|
|
44
|
+
"",
|
|
45
|
+
);
|
|
46
|
+
result = result.replace(
|
|
47
|
+
/^import\s+\{[^}]*\}\s+from\s+["']\$fresh\/server\.ts["'];?\s*\n?/gm,
|
|
48
|
+
"",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// IS_BROWSER → typeof window !== "undefined"
|
|
52
|
+
if (result.includes("IS_BROWSER")) {
|
|
53
|
+
result = result.replace(
|
|
54
|
+
/\bIS_BROWSER\b/g,
|
|
55
|
+
'(typeof window !== "undefined")',
|
|
56
|
+
);
|
|
57
|
+
// Remove the import
|
|
58
|
+
result = result.replace(
|
|
59
|
+
/^import\s+\{[^}]*\bIS_BROWSER\b[^}]*\}\s+from\s+["'][^"']+["'];?\s*\n?/gm,
|
|
60
|
+
"",
|
|
61
|
+
);
|
|
62
|
+
changed = true;
|
|
63
|
+
notes.push('Replaced IS_BROWSER with typeof window !== "undefined"');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Context.active().release?.revision() → "" or remove the entire await line
|
|
67
|
+
if (result.includes("Context.active()")) {
|
|
68
|
+
result = result.replace(
|
|
69
|
+
/(?:const|let)\s+\w+\s*=\s*await\s+Context\.active\(\)\.release\?\.revision\(\);?\s*\n?/g,
|
|
70
|
+
"",
|
|
71
|
+
);
|
|
72
|
+
result = result.replace(
|
|
73
|
+
/Context\.active\(\)\.release\?\.revision\(\)/g,
|
|
74
|
+
'""',
|
|
75
|
+
);
|
|
76
|
+
result = result.replace(
|
|
77
|
+
/^import\s+\{\s*Context\s*\}\s+from\s+["']@deco\/deco["'];?\s*\n?/gm,
|
|
78
|
+
"",
|
|
79
|
+
);
|
|
80
|
+
changed = true;
|
|
81
|
+
notes.push("Removed Context.active().release?.revision()");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// defineApp wrapper → unwrap to a plain function
|
|
85
|
+
// Matches: export default defineApp(async (_req, ctx) => { ... });
|
|
86
|
+
if (result.includes("defineApp")) {
|
|
87
|
+
result = result.replace(
|
|
88
|
+
/export\s+default\s+defineApp\(\s*(?:async\s+)?\([^)]*\)\s*=>\s*\{/,
|
|
89
|
+
"// NOTE: defineApp removed — this file needs manual conversion to a route\nexport default function AppLayout() {",
|
|
90
|
+
);
|
|
91
|
+
// Remove trailing ); that closed defineApp
|
|
92
|
+
// This is tricky — we'll flag for manual review instead of guessing
|
|
93
|
+
changed = true;
|
|
94
|
+
notes.push(
|
|
95
|
+
"MANUAL: defineApp wrapper partially unwrapped — verify closing brackets",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Remove <Head> wrapper — its children should go into route head() config
|
|
100
|
+
// This is complex to do with regex, so we flag it
|
|
101
|
+
if (result.includes("<Head>") || result.includes("<Head ")) {
|
|
102
|
+
notes.push(
|
|
103
|
+
"MANUAL: <Head> component found — move contents to route head() config",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Clean up blank lines
|
|
108
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
109
|
+
|
|
110
|
+
return { content: result, changed, notes };
|
|
111
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { TransformResult } from "../types.ts";
|
|
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
|
+
|
|
13
|
+
// Preact → React
|
|
14
|
+
[/^"preact\/hooks"$/, `"react"`],
|
|
15
|
+
[/^"preact\/jsx-runtime"$/, null],
|
|
16
|
+
[/^"preact\/compat"$/, `"react"`],
|
|
17
|
+
[/^"preact"$/, `"react"`],
|
|
18
|
+
[/^"@preact\/signals-core"$/, null],
|
|
19
|
+
[/^"@preact\/signals"$/, null],
|
|
20
|
+
|
|
21
|
+
// Deco framework
|
|
22
|
+
[/^"@deco\/deco\/hooks"$/, `"@decocms/start/sdk/useScript"`],
|
|
23
|
+
[/^"@deco\/deco\/blocks"$/, `"@decocms/start/types"`],
|
|
24
|
+
[/^"@deco\/deco\/web"$/, null], // runtime.ts is rewritten
|
|
25
|
+
[/^"@deco\/deco"$/, `"@decocms/start"`],
|
|
26
|
+
|
|
27
|
+
// Apps — widgets & components
|
|
28
|
+
[/^"apps\/admin\/widgets\.ts"$/, `"@decocms/start/admin/widgets"`],
|
|
29
|
+
[/^"apps\/website\/components\/Image\.tsx"$/, `"@decocms/apps/commerce/components/Image"`],
|
|
30
|
+
[/^"apps\/website\/components\/Picture\.tsx"$/, `"@decocms/apps/commerce/components/Picture"`],
|
|
31
|
+
[/^"apps\/website\/components\/Video\.tsx"$/, `"@decocms/apps/commerce/components/Video"`],
|
|
32
|
+
[/^"apps\/commerce\/types\.ts"$/, `"@decocms/apps/commerce/types"`],
|
|
33
|
+
|
|
34
|
+
// Apps — catch-all (things like apps/website/mod.ts, apps/vtex/mod.ts, etc.)
|
|
35
|
+
[/^"apps\/([^"]+)"$/, null], // Remove — site.ts is rewritten
|
|
36
|
+
|
|
37
|
+
// Deco old CDN imports
|
|
38
|
+
[/^"deco\/([^"]+)"$/, null],
|
|
39
|
+
|
|
40
|
+
// Std lib — not needed in Node
|
|
41
|
+
[/^"std\/([^"]+)"$/, null],
|
|
42
|
+
|
|
43
|
+
// site/ → ~/
|
|
44
|
+
[/^"site\/(.+)"$/, `"~/$1"`],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Rewrites import specifiers in a file.
|
|
49
|
+
*
|
|
50
|
+
* Handles:
|
|
51
|
+
* - import X from "old" → import X from "new"
|
|
52
|
+
* - import { X } from "old" → import { X } from "new"
|
|
53
|
+
* - import type { X } from "old" → import type { X } from "new"
|
|
54
|
+
* - export { X } from "old" → export { X } from "new"
|
|
55
|
+
* - import "old" → import "new"
|
|
56
|
+
*
|
|
57
|
+
* When a rule maps to null, the entire import line is removed.
|
|
58
|
+
*/
|
|
59
|
+
export function transformImports(content: string): TransformResult {
|
|
60
|
+
const notes: string[] = [];
|
|
61
|
+
let changed = false;
|
|
62
|
+
|
|
63
|
+
// Match import/export lines with their specifiers
|
|
64
|
+
const importLineRegex =
|
|
65
|
+
/^(import\s+(?:type\s+)?(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?)\s+from\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
|
|
66
|
+
const reExportLineRegex =
|
|
67
|
+
/^(export\s+(?:type\s+)?\{[^}]*\}\s+from\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
|
|
68
|
+
const sideEffectImportRegex = /^(import\s+)("[^"]+"|'[^']+')(;?\s*)$/gm;
|
|
69
|
+
|
|
70
|
+
function rewriteSpecifier(specifier: string): string | null {
|
|
71
|
+
// Remove quotes for matching
|
|
72
|
+
const inner = specifier.slice(1, -1);
|
|
73
|
+
|
|
74
|
+
for (const [pattern, replacement] of IMPORT_RULES) {
|
|
75
|
+
if (pattern.test(`"${inner}"`)) {
|
|
76
|
+
if (replacement === null) return null;
|
|
77
|
+
// Apply regex replacement
|
|
78
|
+
return `"${inner}"`.replace(pattern, replacement);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// npm: prefix removal
|
|
83
|
+
if (inner.startsWith("npm:")) {
|
|
84
|
+
const cleaned = inner
|
|
85
|
+
.slice(4)
|
|
86
|
+
.replace(/@[\d^~>=<.*]+$/, ""); // strip version
|
|
87
|
+
return `"${cleaned}"`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Strip .ts/.tsx extensions from relative imports
|
|
91
|
+
if (
|
|
92
|
+
(inner.startsWith("./") || inner.startsWith("../") ||
|
|
93
|
+
inner.startsWith("~/")) &&
|
|
94
|
+
(inner.endsWith(".ts") || inner.endsWith(".tsx"))
|
|
95
|
+
) {
|
|
96
|
+
const stripped = inner.replace(/\.tsx?$/, "");
|
|
97
|
+
return `"${stripped}"`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return specifier;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function processLine(
|
|
104
|
+
_match: string,
|
|
105
|
+
prefix: string,
|
|
106
|
+
specifier: string,
|
|
107
|
+
suffix: string,
|
|
108
|
+
): string {
|
|
109
|
+
const newSpec = rewriteSpecifier(specifier);
|
|
110
|
+
if (newSpec === null) {
|
|
111
|
+
changed = true;
|
|
112
|
+
notes.push(`Removed import: ${specifier}`);
|
|
113
|
+
return ""; // Remove the line
|
|
114
|
+
}
|
|
115
|
+
if (newSpec !== specifier) {
|
|
116
|
+
changed = true;
|
|
117
|
+
notes.push(`Rewrote: ${specifier} → ${newSpec}`);
|
|
118
|
+
return `${prefix}${newSpec}${suffix}`;
|
|
119
|
+
}
|
|
120
|
+
return `${prefix}${specifier}${suffix}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let result = content;
|
|
124
|
+
result = result.replace(importLineRegex, processLine);
|
|
125
|
+
result = result.replace(reExportLineRegex, processLine);
|
|
126
|
+
result = result.replace(sideEffectImportRegex, processLine);
|
|
127
|
+
|
|
128
|
+
// Clean up blank lines left by removed imports (collapse multiple to one)
|
|
129
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
130
|
+
|
|
131
|
+
return { content: result, changed, notes };
|
|
132
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { TransformResult } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transforms Preact JSX patterns to React JSX patterns.
|
|
5
|
+
*
|
|
6
|
+
* - class= → className= (in JSX context)
|
|
7
|
+
* - onInput= → onChange= (React's onChange fires on every keystroke)
|
|
8
|
+
* - ComponentChildren → React.ReactNode
|
|
9
|
+
* - JSX.SVGAttributes → React.SVGAttributes
|
|
10
|
+
* - JSX.GenericEventHandler → React.FormEventHandler / React.EventHandler
|
|
11
|
+
* - type { JSX } from "preact" → (removed, use React types)
|
|
12
|
+
*/
|
|
13
|
+
export function transformJsx(content: string): TransformResult {
|
|
14
|
+
const notes: string[] = [];
|
|
15
|
+
let changed = false;
|
|
16
|
+
let result = content;
|
|
17
|
+
|
|
18
|
+
// class= → className= in JSX attributes
|
|
19
|
+
// Match class= that's preceded by whitespace and inside a JSX tag
|
|
20
|
+
const classAttrRegex = /(<[a-zA-Z][^>]*?\s)class(\s*=)/g;
|
|
21
|
+
if (classAttrRegex.test(result)) {
|
|
22
|
+
result = result.replace(
|
|
23
|
+
/(<[a-zA-Z][^>]*?\s)class(\s*=)/g,
|
|
24
|
+
"$1className$2",
|
|
25
|
+
);
|
|
26
|
+
changed = true;
|
|
27
|
+
notes.push("Replaced class= with className=");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Also handle class= at the start of a line in JSX (multi-line attributes)
|
|
31
|
+
const standaloneClassRegex = /^(\s+)class(\s*=)/gm;
|
|
32
|
+
if (standaloneClassRegex.test(result)) {
|
|
33
|
+
result = result.replace(standaloneClassRegex, "$1className$2");
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// onInput= → onChange=
|
|
38
|
+
if (result.includes("onInput=")) {
|
|
39
|
+
result = result.replace(/onInput=/g, "onChange=");
|
|
40
|
+
changed = true;
|
|
41
|
+
notes.push("Replaced onInput= with onChange=");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ComponentChildren → React.ReactNode
|
|
45
|
+
if (result.includes("ComponentChildren")) {
|
|
46
|
+
result = result.replace(/\bComponentChildren\b/g, "React.ReactNode");
|
|
47
|
+
// Add React import if not present
|
|
48
|
+
if (!result.includes('from "react"') && !result.includes("from 'react'")) {
|
|
49
|
+
result = `import React from "react";\n${result}`;
|
|
50
|
+
}
|
|
51
|
+
changed = true;
|
|
52
|
+
notes.push("Replaced ComponentChildren with React.ReactNode");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// JSX.SVGAttributes<SVGSVGElement> → React.SVGAttributes<SVGSVGElement>
|
|
56
|
+
if (result.includes("JSX.SVGAttributes")) {
|
|
57
|
+
result = result.replace(/\bJSX\.SVGAttributes/g, "React.SVGAttributes");
|
|
58
|
+
changed = true;
|
|
59
|
+
notes.push("Replaced JSX.SVGAttributes with React.SVGAttributes");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// JSX.GenericEventHandler<X> → React.FormEventHandler<X>
|
|
63
|
+
if (result.includes("JSX.GenericEventHandler")) {
|
|
64
|
+
result = result.replace(
|
|
65
|
+
/\bJSX\.GenericEventHandler/g,
|
|
66
|
+
"React.FormEventHandler",
|
|
67
|
+
);
|
|
68
|
+
changed = true;
|
|
69
|
+
notes.push(
|
|
70
|
+
"Replaced JSX.GenericEventHandler with React.FormEventHandler",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// JSX.HTMLAttributes<X> → React.HTMLAttributes<X>
|
|
75
|
+
if (result.includes("JSX.HTMLAttributes")) {
|
|
76
|
+
result = result.replace(
|
|
77
|
+
/\bJSX\.HTMLAttributes/g,
|
|
78
|
+
"React.HTMLAttributes",
|
|
79
|
+
);
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// JSX.IntrinsicElements → React.JSX.IntrinsicElements
|
|
84
|
+
if (result.includes("JSX.IntrinsicElements")) {
|
|
85
|
+
result = result.replace(
|
|
86
|
+
/\bJSX\.IntrinsicElements/g,
|
|
87
|
+
"React.JSX.IntrinsicElements",
|
|
88
|
+
);
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remove standalone "import type { JSX } from 'preact'" if JSX no longer used
|
|
93
|
+
// (it was already removed by imports transform, but double check)
|
|
94
|
+
result = result.replace(
|
|
95
|
+
/^import\s+type\s+\{\s*JSX\s*\}\s+from\s+["']preact["'];?\s*\n?/gm,
|
|
96
|
+
"",
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Ensure React import exists if we introduced React.* references
|
|
100
|
+
if (
|
|
101
|
+
(result.includes("React.") || result.includes("React,")) &&
|
|
102
|
+
!result.match(/^import\s.*React/m)
|
|
103
|
+
) {
|
|
104
|
+
result = `import React from "react";\n${result}`;
|
|
105
|
+
changed = true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { content: result, changed, notes };
|
|
109
|
+
}
|