@react-lib-tech/react-jsx-to-tsx
Version:
A tiny React component library (JS) for converting TSX → JSX and extracting text
222 lines (219 loc) • 8.21 kB
JavaScript
// src/utils/restore-project.jsx
import axios from "axios";
import fs from "fs";
import path from "path";
import https from "https";
var httpsAgent = new https.Agent({ rejectUnauthorized: false });
async function LovableDownload({
API_URL = "https://lovable-api.com/projects/5b9f446c-d608-4948-aed1-7e5ba9ad04c0/source-code",
Authorization = ""
}) {
try {
const res = await axios.get(API_URL, {
httpsAgent,
headers: {
Authorization
}
});
const data = res == null ? void 0 : res.data;
for (const file of data.files) {
const filePath = path.join(process.cwd(), file.name);
if (file.binary) {
console.log(`\u26A0\uFE0F Skipping binary file: ${file.name}`);
continue;
}
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, file.contents, "utf8");
console.log(`\u2705 Created: ${filePath}`);
}
console.log("\u{1F389} Project restored with correct folder structure!");
} catch (err) {
console.error("\u274C Error:", err.message);
}
}
// src/utils/rename-jsx-to-tsx.jsx
import fs2 from "fs";
import path2 from "path";
import babel from "@babel/core";
var presets = [
[
"@babel/preset-typescript",
{
isTSX: true,
allExtensions: true
}
]
];
function loadTsConfigAliases() {
var _a, _b;
const tsconfigPath = path2.resolve("tsconfig.json");
if (!fs2.existsSync(tsconfigPath)) return {};
try {
const tsconfig = JSON.parse(fs2.readFileSync(tsconfigPath, "utf-8"));
const paths = ((_a = tsconfig.compilerOptions) == null ? void 0 : _a.paths) || {};
const baseUrl = ((_b = tsconfig.compilerOptions) == null ? void 0 : _b.baseUrl) || ".";
const aliases = {};
for (const alias in paths) {
const target = paths[alias][0].replace(/\/\*$/, "");
const aliasKey = alias.replace(/\/\*$/, "");
aliases[aliasKey] = path2.resolve(baseUrl, target);
}
return aliases;
} catch {
console.warn("\u26A0\uFE0F Could not parse tsconfig.json paths");
return {};
}
}
function fixJsxAttributes(code) {
return code.replace(/\bclass=/g, "className=").replace(/\bfor=/g, "htmlFor=").replace(/\bonclick=/gi, "onClick=").replace(/\bonchange=/gi, "onChange=").replace(/\bonfocus=/gi, "onFocus=").replace(/\bonblur=/gi, "onBlur=");
}
function extractPropTypes(code, componentName) {
const propTypesRegex = new RegExp(
`${componentName}\\.propTypes\\s*=\\s*{([\\s\\S]*?)};?`,
"m"
);
const match = code.match(propTypesRegex);
if (!match) return null;
const propsBlock = match[1];
const lines = propsBlock.split("\n").map((l) => l.trim()).filter(Boolean);
const fields = lines.map((line) => {
const [rawKey, rawType] = line.replace(/[,}]/g, "").split(":").map((s) => s.trim());
let tsType = "any";
let optional = true;
if (/isRequired/.test(rawType)) optional = false;
if (/string/.test(rawType)) tsType = "string";
else if (/number/.test(rawType)) tsType = "number";
else if (/bool/.test(rawType)) tsType = "boolean";
else if (/array/.test(rawType)) tsType = "any[]";
else if (/object/.test(rawType)) tsType = "Record<string, any>";
else if (/func/.test(rawType)) tsType = "() => void";
return ` ${rawKey}${optional ? "?" : ""}: ${tsType};`;
});
return `
interface ${componentName}Props {
${fields.join("\n")}
}
`;
}
function extractDefaultProps(code, componentName) {
const defaultPropsRegex = new RegExp(
`${componentName}\\.defaultProps\\s*=\\s*{([\\s\\S]*?)};?`,
"m"
);
const match = code.match(defaultPropsRegex);
if (!match) return {};
const propsBlock = match[1];
const lines = propsBlock.split("\n").map((l) => l.trim()).filter(Boolean);
const defaults = {};
for (const line of lines) {
const [key, value] = line.replace(/[,}]/g, "").split(":").map((s) => s.trim());
if (key && value) defaults[key] = value;
}
return defaults;
}
function addInterfaceForProps(code, defaults) {
const funcRegex = /function\s+([A-Z][A-Za-z0-9_]*)\s*\(\s*{([^}]*)}\s*\)/;
const arrowRegex = /const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*\(\s*{([^}]*)}\s*\)\s*=>/;
let match = code.match(funcRegex) || code.match(arrowRegex);
if (!match) return code;
const componentName = match[1];
const propsRaw = match[2].split(",").map((p) => p.trim()).filter(Boolean);
if (propsRaw.length === 0) return code;
const interfaceName = `${componentName}Props`;
const interfaceFields = propsRaw.map((p) => ` ${p}?: any;`).join("\n");
const interfaceCode = `
interface ${interfaceName} {
${interfaceFields}
}
`;
const withDefaults = propsRaw.map((p) => defaults[p] ? `${p} = ${defaults[p]}` : p).join(", ");
if (funcRegex.test(code)) {
code = code.replace(
funcRegex,
`function ${componentName}({ ${withDefaults} }: ${interfaceName}): JSX.Element`
);
} else {
code = code.replace(
arrowRegex,
`const ${componentName}: React.FC<${interfaceName}> = ({ ${withDefaults} }) =>`
);
}
if (!code.includes(`interface ${interfaceName}`)) {
code = interfaceCode + code;
}
if (/React\.FC/.test(code) && !/import\s+.*React/.test(code)) {
code = `import React from "react";
` + code;
}
return code;
}
function addTypesForForwardRef(code) {
const forwardRefRegex = /const\s+([A-Z][A-Za-z0-9_]*)\s*=\s*React\.forwardRef\(\s*\(([^)]*)\)\s*=>/;
const match = code.match(forwardRefRegex);
if (!match) return code;
const componentName = match[1];
return code.replace(
forwardRefRegex,
`const ${componentName} = React.forwardRef<React.ElementRef<typeof Button>, React.ComponentProps<typeof Button>>(($2) =>`
);
}
async function jsxToTsx(srcPath, outPath) {
try {
const code = fs2.readFileSync(srcPath, "utf-8");
const result = await babel.transformAsync(code, { filename: srcPath, presets });
if (!(result == null ? void 0 : result.code)) throw new Error("Babel failed");
let output = result.code;
const componentMatch = output.match(/(function|const)\s+([A-Z][A-Za-z0-9_]*)/);
const componentName = componentMatch ? componentMatch[2] : null;
let defaults = {};
if (componentName) {
defaults = extractDefaultProps(code, componentName);
output = addInterfaceForProps(output, defaults);
const interfaceFromPT = extractPropTypes(code, componentName);
if (interfaceFromPT && !output.includes(`interface ${componentName}Props`)) {
output = interfaceFromPT + output;
}
}
output = addTypesForForwardRef(output);
output = fixJsxAttributes(output);
const aliases = loadTsConfigAliases();
for (const alias in aliases) {
const target = aliases[alias];
const aliasRegex = new RegExp(`${alias}/`, "g");
output = output.replace(aliasRegex, target + "/");
}
if (componentName) {
const propTypesRegex = new RegExp(`${componentName}\\.propTypes\\s*=\\s*{[\\s\\S]*?};?`, "m");
const defaultPropsRegex = new RegExp(`${componentName}\\.defaultProps\\s*=\\s*{[\\s\\S]*?};?`, "m");
output = output.replace(propTypesRegex, "");
output = output.replace(defaultPropsRegex, "");
}
fs2.mkdirSync(path2.dirname(outPath), { recursive: true });
fs2.writeFileSync(outPath, output, "utf-8");
console.log(`\u2705 Converted: ${srcPath} \u2192 ${outPath}`);
} catch (err) {
console.error(`\u274C Error converting ${srcPath}: ${err.message}`);
}
}
async function convertFolder(srcFolder, outFolder) {
const entries = fs2.readdirSync(srcFolder, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path2.join(srcFolder, entry.name);
const outPath = path2.join(outFolder, entry.name);
if (entry.isDirectory()) {
await convertFolder(srcPath, outPath);
} else if (entry.isFile()) {
if (entry.name.endsWith(".jsx")) {
await jsxToTsx(srcPath, outPath.replace(/\.jsx$/, ".tsx"));
} else if (entry.name.endsWith(".js")) {
await jsxToTsx(srcPath, outPath.replace(/\.js$/, ".ts"));
}
}
}
}
var rename_jsx_to_tsx_default = convertFolder;
export {
LovableDownload,
rename_jsx_to_tsx_default as convertFolder
};
//# sourceMappingURL=index.mjs.map