UNPKG

vue-code-transformer

Version:

Codemod tool for Vue projects, built using jscodeshift, postcss, and vue-eslint-parser

247 lines (228 loc) 9.34 kB
import fs from "node:fs"; import path from "node:path"; import chalk from "chalk"; import runTransformation from "./run-transformation.js"; global.outputReport = {}; global.processFilePath = []; global.errors = new Map(); // get all file paths in output directory function getFilesFromDir(dir) { const resolvedPaths = []; const stack = [path.resolve(dir)]; while (stack.length > 0) { const currentDir = stack.pop(); const entries = fs.readdirSync(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isFile()) { resolvedPaths.push(fullPath); } else if (entry.isDirectory()) { stack.push(fullPath); } } } return resolvedPaths; } function resolvePaths(paths) { return paths.flatMap(path => { const stat = fs.statSync(path); if (stat.isFile()) return [path]; if (stat.isDirectory()) return getFilesFromDir(path); throw new Error(`Unsupported path type: ${path}`); }); } function copyToOutputPath(inputPath, outputPath) { const absInputPath = path.resolve(inputPath); const absOutputPath = path.resolve(outputPath); if (!fs.existsSync(absInputPath)) { throw new Error(`Input path ${inputPath} does not exist`); } fs.cpSync(absInputPath, absOutputPath, { recursive: true }); } function validateInput(transformationModules, inputPaths, { transformMode, outputPaths, failOnError }) { // validate transformationModules if (!(typeof transformationModules === "function" || (Array.isArray(transformationModules) && transformationModules.every(fn => typeof fn === "function")))) { throw new TypeError( "transformationModules must be a function or an array of functions" ); } // validate transformMode const validTransformModes = ["in-place", "out-place", "dry"]; if (!validTransformModes.includes(transformMode)) { throw new Error( `transformMode must be either "in-place", "out-place" or "dry". Received: "${transformMode}"` ); } // validate inputPaths if (!(typeof inputPaths === "string" || (Array.isArray(inputPaths) && inputPaths.length > 0 && inputPaths.every(p => typeof p === "string")))) { throw new TypeError( "inputPaths must be a non-empty string or non-empty array of strings" ); } // validate output-place transform mode if (transformMode === "out-place") { // validate outputPaths exists and has correct type if (!(typeof outputPaths === "string" || (Array.isArray(outputPaths) && outputPaths.length > 0 && outputPaths.every(p => typeof p === "string")))) { throw new TypeError( "options.outputPaths must be a non-empty string or non-empty array of strings for out-place transformations" ); } // check matching lengths const inputPathsLength = Array.isArray(inputPaths) ? inputPaths.length : 1; const outputPathsLength = Array.isArray(outputPaths) ? outputPaths.length : 1; if (inputPathsLength !== outputPathsLength) { throw new Error( "inputPaths and options.outputPaths arrays must have the same length. " + `Received: inputPaths(${inputPathsLength}), options.outputPaths(${outputPathsLength})` ); } } if (typeof failOnError !== "boolean") { throw new TypeError("options.failOnError must be a boolean"); } } function formatContent(content) { const output = `${chalk.dim("┌───")} ${content.split("\n").map(line => chalk.dim("│ ") + line).join("\n")} ${chalk.dim("└───")}` console.log(output); } function showTransformationSummary(numFilesProcessed, numFilesUpdated, numFilesFailed) { console.log(chalk.bold("┌────────────────────────────────────────┐")); console.log(chalk.bold("│ TRANSFORMATION SUMMARY │")); console.log(chalk.bold("└────────────────────────────────────────┘")); // Files processed console.log(chalk.bold(`\n Files processed: ${chalk.cyan(numFilesProcessed)}`)); // Successful transformations console.log(chalk.bold(` Files updated: ${chalk.green(numFilesUpdated)}`)); // Failures if (numFilesFailed > 0) { console.log(chalk.bold(` Files failed: ${chalk.red(numFilesFailed)}`)); console.log("\n"); showErrors(); } console.log("\n"); } function showErrors() { if (global.errors.size === 0) { return; } console.log(chalk.bold.red("\u{274C} Transformation failures:")); let index = 1; for (const [key, value] of global.errors.entries()) { console.log(chalk.green(`${index}. ${key}`)); for (const entry of value) { console.log(chalk.red(` - ${entry.transformRule}: ${entry.err}`)); } console.log("\n"); index++; } } function vueCodeTransformer(transformationModules, inputPaths, options) { const DEFAULT_OPTIONS = { transformMode: "in-place", outputPaths: undefined, failOnError: false } let { transformMode = DEFAULT_OPTIONS.transformMode, outputPaths = DEFAULT_OPTIONS.outputPaths, failOnError = DEFAULT_OPTIONS.failOnError, } = options; validateInput(transformationModules, inputPaths, { transformMode, outputPaths, failOnError }); // convert single transformationModule to array if (typeof transformationModules === "function") { transformationModules = [transformationModules] } // convert single inputPath and outputPath to array if (typeof inputPaths === "string") { inputPaths = [inputPaths]; } if (typeof outputPaths === "string") { outputPaths = [outputPaths]; } if (transformMode === "out-place") { for (let i = 0; i < inputPaths.length; i++) { copyToOutputPath(inputPaths[i], outputPaths[i]); } } else { outputPaths = inputPaths; } console.log('output paths', outputPaths) let resolvedPaths = resolvePaths(outputPaths); console.log('resolved paths', resolvedPaths) for (const transformationModule of transformationModules) { let transformRule = transformationModule.ruleName; console.log(chalk.bold.magenta(`\u{1F3D7} Running ${transformRule}`)); const extensions = [ ".js", ".ts", ".vue", ".jsx", ".tsx", ".scss", ".css", ".less", ]; for (const path of resolvedPaths) { const retainedSource = fs .readFileSync(path) .toString() .split("\r\n") .join("\n"); const fileInfo = { path: path, source: retainedSource, }; const extension = (/\.([^.]*)$/.exec(fileInfo.path) || [])[0]; if (!extensions.includes(extension)) { continue; } try { const result = runTransformation(fileInfo, transformationModule, { transformMode, outputPaths, failOnError }); if (retainedSource !== result) { if (transformMode === "dry") { console.log(`${chalk.green("\u{2714}")} ${chalk.green(fileInfo.path)}:`); formatContent(result); } else { fs.writeFileSync(path, result); } // add path to list of processed file paths if (global.processFilePath.indexOf(path) === -1) { global.processFilePath.push(path); } } } catch (err) { if (failOnError) { throw err; } if (global.errors.has(path)) { const pathErrors = global.errors.get(path); pathErrors.push({ transformRule, err }); } else { global.errors.set(path, [{ transformRule, err }]); } } } const transformFiles = global.outputReport[transformRule]; const numFilesTransformed = transformFiles?.length === undefined ? 0 : transformFiles.length; console.log( chalk.bold.blue( `\u{1F3C1} ${transformRule} completed, ${numFilesTransformed} ${numFilesTransformed === 1 ? "file" : "files" } updated.` ) ); if (transformMode !== "dry") { console.log(chalk.bold.blue("\u{1F3C1} Updated file paths:"), transformFiles); } console.log("\n") } let numFilesProcessed = resolvedPaths.length; let numFilesUpdated = global.processFilePath.length; let numFilesFailed = global.errors.size; showTransformationSummary(numFilesProcessed, numFilesUpdated, numFilesFailed); } export default vueCodeTransformer;