import { transformSync } from "oxc-transform"; const stripTypeScriptCache = new Map(); const stripTypeScriptCacheLimit = 512; /** Removes TypeScript syntax from a snippet with OXC while preserving JSX. */ export function stripTypeScriptWithOxc(source: string): string { if (!needsTypeScriptStripping(source)) { return source.trimEnd(); } const cached = stripTypeScriptCache.get(source); if (cached !== undefined) { return cached; } const stripped = transformTypeScriptWithOxc(source); rememberStrippedTypeScript(source, stripped); return stripped; } /** Removes TypeScript syntax from a standalone expression without heuristic skipping. */ export function stripTypeScriptExpressionWithOxc(source: string): string { const prefix = "const __mreactExpression = "; const stripped = transformTypeScriptWithOxc(`${prefix}${source};`); if (!stripped.startsWith(prefix)) { return transformTypeScriptWithOxc(source).replace(/;\s*$/, ""); } return stripped.slice(prefix.length).replace(/;\s*$/, ""); } function transformTypeScriptWithOxc(source: string): string { const result = transformSync("snippet.tsx", source, { lang: "tsx", sourceType: "module", target: "es2022", jsx: "preserve", typescript: { onlyRemoveTypeImports: true, }, }); return result.errors.length > 0 ? source.trimEnd() : result.code.trimEnd(); } function rememberStrippedTypeScript(source: string, stripped: string): void { if (stripTypeScriptCache.size >= stripTypeScriptCacheLimit) { const first = stripTypeScriptCache.keys().next().value; if (first !== undefined) { stripTypeScriptCache.delete(first); } } stripTypeScriptCache.set(source, stripped); } function needsTypeScriptStripping(source: string): boolean { // This is intentionally an over-approximation: false positives cost an oxc // pass, but false negatives leak TypeScript syntax into emitted JavaScript. return ( /\bimport\s+type\b/.test(source) || /\bexport\s+type\b/.test(source) || /\btype\s+[A-Za-z_$][\w$]*\b/.test(source) || /\binterface\s+[A-Za-z_$][\w$]*\b/.test(source) || /<[^>\n]+\bextends\b[^>\n]*>\s*\(/.test(source) || /\b[A-Za-z_$][\w$.]*\s*<[^>\n]+>\s*\(/.test(source) || /\bas\s+(?:const|[A-Za-z_$][\w$]*)\b/.test(source) || /\bsatisfies\s+[A-Za-z_$][\w$<>,\s|&.[\]?]*/.test(source) || /[A-Za-z_$)\]}]\s*!\s*(?=[,.;)\]}])/.test(source) || /:\s*[A-Za-z_$][\w$<>,\s|&.[\]?]*(?=[,)=;{])/.test(source) ); } export function transformJsxWithOxc(source: string): string { const result = transformSync("snippet.tsx", source, { lang: "tsx", sourceType: "module", target: "es2022", jsx: { runtime: "automatic", importSource: "@reckona/mreact-compat", }, typescript: { onlyRemoveTypeImports: true, }, }); if (result.errors.length > 0 && result.code === "") { return stripTypeScriptWithOxc(source); } return result.code.trimEnd(); } export function transformJsxToCreateElementWithOxc( source: string, options: { pragma?: string; pragmaFrag?: string; } = {}, ): string { const result = transformSync("snippet.tsx", source, { lang: "tsx", sourceType: "module", target: "es2022", jsx: { runtime: "classic", pragma: options.pragma ?? "createElement", pragmaFrag: options.pragmaFrag ?? "Fragment", }, typescript: { onlyRemoveTypeImports: true, }, }); if (result.errors.length > 0 && result.code === "") { return stripTypeScriptWithOxc(source); } return result.code.trimEnd(); }