package.cjs.configs.scripts.dead-code-eliminator.index.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of neeto-commons-frontend Show documentation
Show all versions of neeto-commons-frontend Show documentation
A package encapsulating common code across neeto projects including initializers, utility functions, common components and hooks and so on.
#!/usr/bin/env node
/* eslint-disable no-console */
const { exec, execSync } = require("child_process");
const fs = require("fs");
const pathLib = require("path");
const generate = require("@babel/generator").default;
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { isNotEmpty, existsBy } = require("@bigbinary/neeto-cist");
const enhancedResolve = require("enhanced-resolve");
const { isEmpty, includes, __ } = require("ramda");
const {
RAILS_MOUNTED_COMPONENTS_REGEX,
ADMIN_AND_PUBLIC_APP_REGEX,
TYPES,
PACKS_FILES_REGEX,
} = require("./constants");
// @ts-ignore
// eslint-disable-next-line import/extensions
const webpackConfig = require("../../../../../../config/webpack/webpack.config.js");
let modifiedFiles = [];
let importedItems = [];
let unusedFiles = [];
let iterationCount = 1;
const javascriptDir = pathLib.resolve(
__dirname,
"../../../../../../app/javascript"
);
const resolver = enhancedResolve.create.sync({
alias: webpackConfig.resolve.alias,
modules: webpackConfig.resolve.modules,
extensions: webpackConfig.resolve.extensions,
fallback: webpackConfig.resolve.fallback,
conditionNames: ["require", "import", "node", "default", "types"],
});
const isRailsMountedOrPacksFiles = filePath =>
RAILS_MOUNTED_COMPONENTS_REGEX.test(filePath) ||
PACKS_FILES_REGEX.test(filePath) ||
ADMIN_AND_PUBLIC_APP_REGEX.test(filePath);
const isInImportedList = (item, filePath) =>
existsBy({ item: includes(__, [item, "*"]), filePath }, importedItems);
const findImports = filePath => {
const code = fs.readFileSync(filePath, "utf8");
const ast = babelParser.parse(code, {
sourceType: "module",
plugins: ["jsx"],
});
traverse(ast, {
ImportDeclaration(path) {
const { node } = path;
const currentDir = pathLib.dirname(filePath);
const importSource = node.source.value;
const resolvedPath = resolver(currentDir, importSource);
const isNodeModule = resolvedPath.includes("node_modules");
if (isNodeModule) return;
if (isEmpty(node.specifiers)) {
importedItems.push({ item: "*", filePath: resolvedPath });
}
node.specifiers.forEach(specifier => {
if (specifier.type === TYPES.IMPORT_NAMESPACE_SPECIFIER) {
importedItems.push({ item: "*", filePath: resolvedPath });
} else if (specifier.type === TYPES.IMPORT_DEFAULT_SPECIFIER) {
importedItems.push({ item: "default", filePath: resolvedPath });
} else {
const item = specifier.imported.name;
importedItems.push({ item, filePath: resolvedPath });
}
});
},
});
};
const removeUnusedExports = filePath => {
if (isRailsMountedOrPacksFiles(filePath)) return;
if (!existsBy({ filePath }, importedItems)) {
unusedFiles.push(filePath);
return;
}
let isPathModified = false;
const code = fs.readFileSync(filePath, "utf8");
const ast = babelParser.parse(code, {
sourceType: "module",
plugins: ["jsx"],
});
traverse(ast, {
ExportNamedDeclaration(path) {
const { node } = path;
if (node.declaration) {
if (node.declaration.type === TYPES.VARIABLE_DECLARATION) {
node.declaration.declarations.forEach(declaration => {
const exportedItem = declaration.id.name;
if (isInImportedList(exportedItem, filePath)) return;
path.replaceWith(path.node.declaration);
isPathModified = true;
});
} else if (
node.declaration.type === TYPES.FUNCTION_DECLARATION ||
node.declaration.type === TYPES.CLASS_DECLARATION
) {
const exportedItem = node.declaration.id.name;
if (isInImportedList(exportedItem, filePath)) return;
path.replaceWith(path.node.declaration);
isPathModified = true;
}
} else {
path.get("specifiers").forEach(specifierPath => {
const exportedItem = specifierPath.node.exported.name;
if (isInImportedList(exportedItem, filePath)) return;
node.specifiers.length === 1 ? path.remove() : specifierPath.remove();
isPathModified = true;
});
}
},
ExportDefaultDeclaration(path) {
const exportedItem = "default";
if (isInImportedList(exportedItem, filePath)) return;
path.remove();
isPathModified = true;
},
});
if (!isPathModified) return;
const newCode = generate(ast, {
retainLines: true,
retainFunctionParens: true,
}).code;
fs.writeFileSync(filePath, newCode);
modifiedFiles.push(filePath);
};
const traverseDirectoryAndExecuteFunc = (dir, func) => {
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = pathLib.join(dir, file);
const stats = fs.statSync(filePath);
if (stats.isFile() && [".js", ".jsx"].includes(pathLib.extname(filePath))) {
func(filePath);
} else if (stats.isDirectory()) {
traverseDirectoryAndExecuteFunc(filePath, func);
}
});
};
const runLintersAndRemoveUnusedFiles = () => {
const commands = [];
if (isNotEmpty(unusedFiles)) commands.push(`rm -f ${unusedFiles.join(" ")}`);
if (isNotEmpty(modifiedFiles)) {
commands.push(`npx prettier --write ${modifiedFiles.join(" ")}`);
commands.push(`npx eslint --fix ${modifiedFiles.join(" ")}`);
}
// script will be executed recursively until we have no modified files or unused files
exec(commands.join(" && "), () => run());
};
const restoreEslintNeetoVersion = () => {
execSync("git restore package.json yarn.lock", { stdio: "ignore" });
execSync("yarn", { stdio: "ignore" });
};
const reportNonFixableEslintErrors = () => {
const modifiedFiles = execSync("git diff --name-only --diff-filter=M")
.toString()
.split("\n")
.join(" ");
if (!modifiedFiles) {
console.log("✅ Script executed successfully!\n");
return;
}
exec(`npx eslint ${modifiedFiles} --fix --quiet`, (_, stdout) => {
if (stdout) {
console.log(stdout);
console.log(
"⭕️ Auto-fixing is not implemented for the above eslint errors. Please fix them manually.\n"
);
} else {
console.log("- No errors found.\n\n✅ Script executed successfully!\n");
}
});
};
const postExecution = () => {
console.log("\n4. Restoring @bigbinary/eslint-plugin-neeto version.");
restoreEslintNeetoVersion();
console.log("\n5. Checking if there are any non-fixable eslint errors.");
reportNonFixableEslintErrors();
};
const installCustomEslintNeetoVersion = () =>
execSync(
"yarn add @bigbinary/[email protected]",
{ stdio: "ignore" }
);
const run = () => {
console.log(`\nIteration: ${iterationCount++}`);
console.log(".............");
modifiedFiles = [];
importedItems = [];
unusedFiles = [];
console.log("* Collecting all the imports.");
traverseDirectoryAndExecuteFunc(javascriptDir, findImports);
console.log("* Removing exports which are not imported.");
traverseDirectoryAndExecuteFunc(javascriptDir, removeUnusedExports);
const filesCountMsg = `\nModified files: ${modifiedFiles.length}\nUnused files: ${unusedFiles.length}`;
const isExecutionCompleted = isEmpty(modifiedFiles) && isEmpty(unusedFiles);
if (isExecutionCompleted) {
console.log(filesCountMsg);
postExecution();
} else {
console.log(
"* Running linters on modified files and removing unused files."
);
runLintersAndRemoveUnusedFiles();
console.log(filesCountMsg);
}
};
// Script execution starts here
console.log("\nPlease wait, this may take a few minutes...");
console.log(
"\n1. Installing @bigbinary/eslint-plugin-neeto custom release required for this script."
);
installCustomEslintNeetoVersion();
console.log(
"\n2. Executing script recursively until we have no modified or unused files."
);
run();