UNPKG

fixclosure

Version:

JavaScript dependency checker/fixer for Closure Library based on ECMAScript AST

243 lines (242 loc) 9.11 kB
"use strict"; 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); }; }