fixclosure
Version:
JavaScript dependency checker/fixer for Closure Library based on ECMAScript AST
243 lines (242 loc) • 9.11 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cli = exports.resolveConfig = void 0;
const cli_color_1 = __importDefault(require("cli-color"));
const commander_1 = __importDefault(require("commander"));
const fs_1 = __importDefault(require("fs"));
const google_closure_deps_1 = require("google-closure-deps");
const lodash_difference_1 = __importDefault(require("lodash.difference"));
const path_1 = __importDefault(require("path"));
const util_1 = require("util");
const clilogger_1 = __importDefault(require("./clilogger"));
const fix_1 = require("./fix");
const parser_1 = require("./parser");
// Dont't use `import from` to avoid creating nested directory `./lib/src`.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("../package.json");
function list(val) {
return val.split(",");
}
function map(val) {
const mapping = new Map();
val.split(",").forEach((item) => {
const [key, value] = item.split(":");
mapping.set(key, value);
});
return mapping;
}
function setCommandOptions(command) {
return command
.version(version, "-v, --version")
.usage("[options] files...")
.option("-f, --fix-in-place", "Fix the file in-place.")
.option("--provideRoots <roots>", "Root namespaces to provide separated by comma.", list)
.option("--ignoreProvides", "Provides will remain unchanged")
.option("--namespaces <methods>", "Provided namespaces separated by comma.", list)
.option("--replaceMap <map>", 'Methods or properties to namespaces mapping like "before1:after1,before2:after2".', map)
.option("--useForwardDeclare", "Use goog.forwardDeclare() instead of goog.requireType().")
.option("--config <file>", ".fixclosurerc file path.")
.option("--depsJs <files>", "deps.js file paths separated by comma.", list)
.option("--showSuccess", "Show success ouput.")
.option("--no-color", "Disable color highlight.");
}
function getDuplicated(namespaces) {
const dups = new Set();
namespaces.reduce((prev, cur) => {
if (prev === cur) {
dups.add(cur);
}
return cur;
}, null);
return [...dups];
}
/**
* Find .fixclosurerc up from current working dir
*/
function findConfig(opt_dir) {
return findConfig_(opt_dir || process.cwd());
}
const findConfig_ = memoize((dir) => {
const filename = ".fixclosurerc";
const filepath = path_1.default.normalize(path_1.default.join(dir, filename));
try {
fs_1.default.accessSync(filepath);
return filepath;
}
catch {
// ignore
}
const parent = path_1.default.resolve(dir, "../");
if (dir === parent) {
return null;
}
return findConfig_(parent);
});
function parseArgs(argv, opt_dir) {
const program = new commander_1.default.Command();
const opts = setCommandOptions(program).parse(argv).opts();
if (Array.isArray(opts.depsJs) && opts.depsJs.length > 0) {
const results = opts.depsJs.map((file) => google_closure_deps_1.parser.parseFile(path_1.default.resolve(opt_dir || process.cwd(), file)));
const symbols = results
.map((result) => result.dependencies.map((dep) => dep.closureSymbols))
.flat(2);
opts.depsJsSymbols = symbols;
}
return { opts, program };
}
function resolveConfig({ config, cwd } = {}) {
const configPath = config || findConfig(cwd);
if (!configPath) {
return null;
}
const opts = fs_1.default.readFileSync(configPath, "utf8").trim().split(/\s+/);
const argv = ["node", "fixclosure", ...opts];
return parseArgs(argv, path_1.default.dirname(configPath));
}
exports.resolveConfig = resolveConfig;
async function getFiles(args) {
const { globby } = await import("globby");
return globby(args, {
expandDirectories: { files: ["*"], extensions: ["js"] },
});
}
async function main(argv, stdout, stderr, exit) {
const { opts: argsOptions, program } = parseArgs(argv);
const { opts: rcOptions } = resolveConfig({ config: argsOptions.config }) ?? {};
const options = { ...rcOptions, ...argsOptions };
if (program.args.length < 1) {
program.outputHelp();
exit(1);
}
// for Parser
options.providedNamespace = (options.depsJsSymbols || []).concat(options.namespaces || []);
let ok = 0;
let ng = 0;
let fixed = 0;
const files = await getFiles(program.args);
const promises = files.map(async (file) => {
const log = new clilogger_1.default(options.color, stdout, stderr);
log.warn(`File: ${file}\n`);
const src = await (0, util_1.promisify)(fs_1.default.readFile)(file, "utf8");
const parser = new parser_1.Parser(options);
const info = parser.parse(src);
if (options.useForwardDeclare) {
info.toForwardDeclare = info.toRequireType;
info.toRequireType = [];
}
log.info("Provided:");
log.items(info.provided.map((item) => item + (info.ignoredProvide.includes(item) ? " (ignored)" : "")));
log.info("");
log.info("Required:");
log.items(info.required.map((item) => item + (info.ignoredRequire.includes(item) ? " (ignored)" : "")));
log.info("");
if (info.requireTyped.length > 0) {
log.info("RequireTyped:");
log.items(info.requireTyped.map((item) => item + (info.ignoredRequireType.includes(item) ? " (ignored)" : "")));
log.info("");
}
if (info.forwardDeclared.length > 0) {
log.info("ForwardDeclared:");
log.items(info.forwardDeclared.map((item) => item +
(info.ignoredForwardDeclare.includes(item) ? " (ignored)" : "")));
log.info("");
}
let needToFix = false;
needToFix =
checkDeclare(log, "Provide", info.provided, info.toProvide, info.ignoredProvide) || needToFix;
needToFix =
checkDeclare(log, "Require", info.required, info.toRequire, info.ignoredRequire) || needToFix;
needToFix =
checkDeclare(log, "RequireType", info.requireTyped, info.toRequireType, info.ignoredRequireType, info.forwardDeclared, info.toForwardDeclare) || needToFix;
needToFix =
checkDeclare(log, "ForwardDeclare", info.forwardDeclared, info.toForwardDeclare, info.ignoredForwardDeclare, info.requireTyped, info.toRequireType) || needToFix;
if (needToFix) {
if (options.fixInPlace) {
await (0, fix_1.fixInPlace)(file, src, info);
log.raw("FIXED!", cli_color_1.default.cyan);
fixed++;
}
else {
log.error("FAIL!");
ng++;
}
log.flush(false);
}
else {
ok++;
log.success("GREEN!");
if (options.showSuccess) {
log.flush(true);
}
}
});
let hasException = false;
try {
await Promise.all(promises);
}
catch (e) {
console.error(e);
hasException = true;
}
const log = new clilogger_1.default(options.color, stdout, stderr);
const total = files.length;
log.info("");
log.info(`Total: ${total} files`);
log.success(`Passed: ${ok} files`);
if (ng) {
log.error(`Failed: ${ng} files`);
}
if (fixed) {
log.warn(`Fixed: ${fixed} files`);
}
if (ng || hasException) {
log.flush(false);
exit(1);
}
else {
log.flush(true);
}
}
exports.cli = main;
function checkDeclare(log, method, declared, toDeclare, ignoredDeclare, optionalDeclared = [], optionalToDeclare = []) {
let needToFix = false;
const duplicated = getDuplicated(declared);
if (duplicated.length > 0) {
needToFix = true;
log.error(`Duplicated ${method}:`);
log.items(duplicated);
log.info("");
}
const missing = (0, lodash_difference_1.default)(toDeclare, declared, optionalDeclared);
if (missing.length > 0) {
needToFix = true;
log.error(`Missing ${method}:`);
log.items(missing);
log.info("");
}
let unnecessary = (0, lodash_difference_1.default)(declared, toDeclare, ignoredDeclare, optionalToDeclare);
unnecessary = uniqArray(unnecessary);
if (unnecessary.length > 0) {
needToFix = true;
log.error(`Unnecessary ${method}:`);
log.items(unnecessary);
log.info("");
}
return needToFix;
}
function uniqArray(array) {
return [...new Set(array)];
}
function memoize(func) {
const cache = new Map();
return (key, ...args) => {
if (!cache.has(key)) {
cache.set(key, func(key, ...args));
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cache.get(key);
};
}