Skip to content

Instantly share code, notes, and snippets.

@LorenzoBloedow
Last active June 17, 2023 17:34
Show Gist options
  • Save LorenzoBloedow/3b5f8c8a5d97bb15735ee874abfb83ad to your computer and use it in GitHub Desktop.
Save LorenzoBloedow/3b5f8c8a5d97bb15735ee874abfb83ad to your computer and use it in GitHub Desktop.
Node.js Script to Fix Relative Imports of a File
const fs = require("fs");
const path = require("path");
const chalk = require("chalk");
/**
ARGUMENTS:
(-P | --paths): Comma-separated list of base paths. All files under these paths will be fixed.
(-I | --import-paths): Comma-separated list of import paths. All files under these paths will be used when searching for imports.
If not provided, the paths from (-P | --paths) will be used.
(-D | --dry-run): Prints the results but doesn't actually write them to the files.
It's highly recommended to use this if it's the first time you're using this script.
(-C | --comment-out): Comments out an import when more than one file for the import is found instead of just using the closest one.
NOTES:
USE THIS SCRIPT WITH CAUTION as it will write directly to all the files specified under the (-P | --paths) parameter
and because there are no tests written for it, though it probably won't, it could go nuts and destroy your whole codebase so
make sure all your code is backed up.
If you're using TypeScript you can use the "tsc --noEmit" command to check if your import errors are fixed
**/
function fixRelativeImports() {
let startingPaths;
let startingImportPaths;
let isDryRun = false;
let isCommentOut = false;
for (let i = 0; i < process.argv.length; i++) {
const arg = process.argv[i];
if (["--paths", "-P"].includes(arg)) {
const paths = process.argv[i + 1];
if (typeof paths !== "string") {
console.error(chalk.red(`Error: Missing argument for ${arg} parameter`));
return;
}
startingPaths = paths.replace(/\s*/, "").split(",");
}
if (["--import-paths", "-I"].includes(arg)) {
const paths = process.argv[i + 1];
if (typeof paths !== "string") {
console.error(chalk.red(`Error: Missing argument for ${arg} parameter`));
return;
}
startingImportPaths = paths.replace(/\s*/, "").split(",");
}
if (["--comment-out", "-C"].includes(arg)) {
isCommentOut = true;
}
if (["--dry-run", "-D"].includes(arg)) {
isDryRun = true;
}
}
if (!Array.isArray(startingPaths)) {
console.error(chalk.red("Error: You need to provide a base path with --path or -P. All files under the base path will be evaluated"));
return;
}
const files = getAllFiles(startingPaths);
const importFiles = getAllFiles(startingImportPaths || startingPaths);
if ([files.length, importFiles.length].includes(0)) {
console.error(chalk.red("Error: No files found"));
return;
}
files.forEach((file) => {
const fileContent = fs.readFileSync(file, "utf8");
const fixedContent = fixImports(fileContent, file);
if (!isDryRun) {
fs.writeFileSync(file, fixedContent);
}
});
if (isDryRun) {
console.log(chalk.green.bold("Dry run exited successfully. No files affected."));
} else {
console.log(chalk.green.bold("Relative imports have been fixed successfully."));
}
function getAllFiles(dirPath) {
let files = [];
for (let i = 0; i < dirPath.length; i++) {
const currDir = dirPath[i];
if (fs.existsSync(currDir)) {
const dirItems = fs.readdirSync(currDir);
dirItems.forEach((item) => {
const itemPath = path.join(currDir, item);
const isDirectory = fs.lstatSync(itemPath).isDirectory();
if (isDirectory) {
files = files.concat(getAllFiles([itemPath]));
} else {
files.push(itemPath);
}
});
}
}
return files;
}
function removeExtension(str) {
let fileName = path.basename(str);
let dirPath = path.dirname(str);
fileName = fileName.split(".")[0];
if (dirPath === ".") {
return fileName;
}
return `${dirPath}/${fileName}`;
}
function existsWithoutExt(filePath) {
let files;
try {
files = fs.readdirSync(path.dirname(filePath));
} catch (err) {
if (err.code === "ENOENT") {
return false;
}
throw err;
}
const originalFileWithoutExt = removeExtension(path.basename(filePath));
for (let i = 0; i < files.length; i++) {
const fileWithoutExt = removeExtension(files[i]);
if (originalFileWithoutExt === fileWithoutExt) {
return true;
}
}
return false;
}
function fixImports(content, filePath) {
const lines = content.split("\n");
lines.forEach((line, index) => {
const importMatch = line.match(/import\s+(?:(?:\*\s+as\s+\w+)|(?:{[^}]+})|\w+)\s+from\s+["']((.\/|..\/).+)["']/);
if (importMatch) {
const importPath = importMatch[1];
const correctedPath = findCorrectPath(importPath, filePath, line);
if (correctedPath !== importPath) {
const correctedLine = correctedPath ? line.replace(importPath, correctedPath) : "// " + line;
lines[index] = correctedLine;
}
}
});
return lines.join("\n");
}
function findCorrectPath(importPath, originalFilePath, line) {
const baseName = path.basename(importPath);
const absolutePath = path.resolve(path.dirname(originalFilePath), importPath);
if (existsWithoutExt(absolutePath)) {
if (isDryRun) {
console.log("\n");
console.log("File: ", originalFilePath);
console.log("Line: ", line);
console.log("Chosen import: ", importPath);
console.log("Note: ", "Import skipped because it's not broken");
}
return importPath;
}
const matchingFiles = importFiles.flatMap((file) => {
let fileName = path.basename(file);
fileName = removeExtension(fileName);
if (fileName === baseName) {
return path.relative(path.dirname(originalFilePath), file);
} else {
return [];
}
});
if (matchingFiles.length > 1) {
if (isCommentOut) {
console.warn(
chalk.yellow(
`Warning: Multiple files found with the name "${baseName}". Commenting out the the import in conflict.`
)
);
return false;
} else {
console.warn(
chalk.yellow(
`Warning: Multiple files found with the name "${baseName}". Importing the closest to the current file being fixed.`
)
);
}
}
const closestFilePath = matchingFiles.reduce((prev, curr) => {
if (prev === "") {
return curr || false;
}
const prevDirectoryAmount = prev.split(path.sep).length;
const currDirectoryAmount = curr.split(path.sep).length;
return (currDirectoryAmount <= prevDirectoryAmount) ? curr : prev;
}, "") || importPath;
let relativePath = closestFilePath.replace(/\\/g, "/");
relativePath = removeExtension(relativePath);
// path.relative doesn't add the ./ required by JavaScript for relative imports when the file is on the same folder
if (!relativePath.startsWith(".")) {
relativePath = "./" + relativePath;
}
if (isDryRun) {
console.log("\n");
console.log("File: ", originalFilePath);
console.log("Line: ", line);
console.log("Matches: ", matchingFiles);
console.log("Chosen import: ", relativePath);
}
return relativePath;
}
}
fixRelativeImports();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment