UNPKG

@react-lib-tech/react-tsx-to-jsx

Version:

A tiny React component library (JS) for converting TSX → JSX and extracting text

223 lines (190 loc) 6.72 kB
import axios from 'axios'; import fs from 'fs'; import path from 'path'; import https from 'https'; import babel from '@babel/core'; const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const API_URL = "https://lovable-api.com/projects/"; /** * LovableDownload options: * - API_URL: string (source URL) * - Authorization: string (auth header) * - outputDir: string (where to write files) - defaults to process.cwd() * - allowBinary: boolean - if true, will write binary files (assumed base64). Default false. */ async function LovableDownload({ PROJECT_Id = "5b9f446c-d608-4948-aed1-7e5ba9ad04c0", Authorization = "", outputDir = process.cwd(), allowBinary = false } = {}) { // resolve and normalize output directory const baseOut = path.resolve(outputDir); // helper to ensure destination stays inside outputDir function safeResolve(rel) { // normalize incoming path so ../ segments are collapsed const resolved = path.resolve(baseOut, rel); // allow exactly baseOut or anything under it if (resolved === baseOut || resolved.startsWith(baseOut + path.sep)) { return resolved; } throw new Error(`Unsafe path detected: ${rel}`); } try { // 1. Fetch JSON from API const res = await axios.get(API_URL+PROJECT_Id+"/source-code", { httpsAgent, headers: { Authorization: Authorization }, }); const data = res?.data; if (!data || !Array.isArray(data.files)) { console.log("No files returned by API"); return; } for (const file of data?.files) { try { // compute the destination path safely (prevents ../ traversal) const destPath = safeResolve(file.name); if (file.binary && !allowBinary) { console.log(`⚠️ Skipping binary file: ${file.name}`); continue; } // ensure parent folder exists fs.mkdirSync(path.dirname(destPath), { recursive: true }); // write file if (file.binary) { // assume base64-encoded binary content const contents = file.contents ?? ""; try { const buffer = Buffer.from(contents, "base64"); fs.writeFileSync(destPath, buffer); } catch (e) { console.warn(`⚠️ Failed to write binary file ${file.name}: ${e.message}`); continue; } } else { // ensure we write string data const contents = typeof file.contents === "string" ? file.contents : JSON.stringify(file.contents ?? "", null, 2); fs.writeFileSync(destPath, contents, "utf8"); } console.log(`✅ Created: ${destPath}`); } catch (innerErr) { console.warn(`⚠️ Skipped file ${file.name}: ${innerErr.message}`); } } console.log("🎉 Project restored with correct folder structure!"); console.log(`Files written to: ${baseOut}`); } catch (err) { console.error("❌ Error:", err?.message ?? err); } } // ✅ Load and parse .gitignore manually function loadGitignore(rootPath = ".") { const gitignorePath = path.join(rootPath, ".gitignore"); if (!fs.existsSync(gitignorePath)) return []; return fs .readFileSync(gitignorePath, "utf8") .split("\n") .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#")); } // ✅ Convert gitignore patterns → regex function patternToRegex(pattern) { let regexStr = pattern .replace(/[.+^${}()|[\]\\]/g, "\\$&") // escape regex special chars .replace(/\*\*/g, ".*") // ** → deep any .replace(/\*/g, "[^/]*"); // * → single folder/file if (pattern.endsWith("/")) { regexStr = regexStr + ".*"; // match everything under folder } return new RegExp(regexStr); } function createIgnoreMatcher(rootPath, userSkips = []) { const patterns = [ ...loadGitignore(rootPath).map(patternToRegex), ...userSkips.map(patternToRegex), ]; return function isIgnored(relPath) { // Normalize to forward-slashes const normalized = relPath.replace(/\\/g, "/"); return patterns.some((regex) => regex.test(normalized)); }; } // ✅ Default Replacement Rules const defaultReplacementRules = [ { find: /@\/components\//g, replace: "../components/" }, { find: /@\/lib\//g, replace: "../../lib/" }, { find: /@\/utils\//g, replace: "../utils/" }, { find: /@\/assets\//g, replace: "../public/assets/" }, ]; // ✅ Convert single file async function tsxToJsx( tsPath, outPath, isTsx, InputreplacementRules = [] ) { try { const code = fs.readFileSync(tsPath, "utf-8"); const result = await babel.transformAsync(code, { filename: tsPath, presets: ["@babel/preset-typescript"], }); let output = result.code || ""; const replacementRules = [ ...defaultReplacementRules, ...InputreplacementRules, ]; replacementRules.forEach((rule) => { output = output.replace(rule.find, rule.replace); }); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, output, "utf-8"); console.log(`✅ Converted: ${tsPath} -> ${outPath}`); } catch (err) { console.error(`❌ Error converting ${tsPath}:`, err.message); } } // ✅ Walk folders recursively const convertFolder = async ({ srcFolder = "src", outFolder = "dist", InputreplacementRules = [], typeConvert = ".jsx", skipFiles = [], }) => { if (!fs.existsSync(srcFolder)) { console.error(`❌ Source folder not found: ${srcFolder}`); return; } const isIgnored = createIgnoreMatcher(process.cwd(), skipFiles); const entries = fs.readdirSync(srcFolder, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(srcFolder, entry.name); const relPath = path.relative(process.cwd(), srcPath); // relative to root // 🔄 Skip gitignored OR user-specified if (isIgnored(relPath)) { console.log(`⏭️ Skipped: ${relPath}`); continue; } const outPath = path.join(outFolder, entry.name); if (entry.isDirectory()) { await convertFolder({ srcFolder: srcPath, outFolder: outPath, InputreplacementRules, typeConvert, skipFiles, }); } else if (entry.isFile()) { if (entry.name.endsWith(".tsx")) { const jsxPath = outPath.replace(/\.tsx$/, typeConvert); await tsxToJsx(srcPath, jsxPath, true, InputreplacementRules); } else if (entry.name.endsWith(".ts")) { const jsPath = outPath.replace(/\.ts$/, ".js"); await tsxToJsx(srcPath, jsPath, false, InputreplacementRules); } } } }; export { LovableDownload, convertFolder };