@react-lib-tech/react-jsx-to-tsx
Version:
A tiny React component library (JS) for converting TSX → JSX and extracting text
259 lines (254 loc) • 10.3 kB
JavaScript
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.js
var index_exports = {};
__export(index_exports, {
LovableDownload: () => LovableDownload,
convertFolder: () => rename_jsx_to_tsx_default
});
module.exports = __toCommonJS(index_exports);
// src/utils/restore-project.jsx
var import_axios = __toESM(require("axios"), 1);
var import_fs = __toESM(require("fs"), 1);
var import_path = __toESM(require("path"), 1);
var import_https = __toESM(require("https"), 1);
var httpsAgent = new import_https.default.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 import_axios.default.get(API_URL, {
httpsAgent,
headers: {
Authorization
}
});
const data = res == null ? void 0 : res.data;
for (const file of data.files) {
const filePath = import_path.default.join(process.cwd(), file.name);
if (file.binary) {
console.log(`\u26A0\uFE0F Skipping binary file: ${file.name}`);
continue;
}
import_fs.default.mkdirSync(import_path.default.dirname(filePath), { recursive: true });
import_fs.default.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
var import_fs2 = __toESM(require("fs"), 1);
var import_path2 = __toESM(require("path"), 1);
var import_core = __toESM(require("@babel/core"), 1);
var presets = [
[
"@babel/preset-typescript",
{
isTSX: true,
allExtensions: true
}
]
];
function loadTsConfigAliases() {
var _a, _b;
const tsconfigPath = import_path2.default.resolve("tsconfig.json");
if (!import_fs2.default.existsSync(tsconfigPath)) return {};
try {
const tsconfig = JSON.parse(import_fs2.default.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] = import_path2.default.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 = import_fs2.default.readFileSync(srcPath, "utf-8");
const result = await import_core.default.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, "");
}
import_fs2.default.mkdirSync(import_path2.default.dirname(outPath), { recursive: true });
import_fs2.default.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 = import_fs2.default.readdirSync(srcFolder, { withFileTypes: true });
for (const entry of entries) {
const srcPath = import_path2.default.join(srcFolder, entry.name);
const outPath = import_path2.default.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;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
LovableDownload,
convertFolder
});
//# sourceMappingURL=index.cjs.map