Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
parse-deps.js 7.31 KiB
#!/usr/bin/env node

const fs = require("fs");
const path = require("path");

const glob = require("glob");
const minimatch = require("minimatch");
const babel = require("@babel/core");
const readline = require("readline");

const PATTERN = "{enterprise/,}frontend/src/**/*.{js,jsx,ts,tsx}";

// after webpack.config.js
const ALIAS = {
  metabase: "frontend/src/metabase",
  "metabase-lib": "frontend/src/metabase-lib",
  "metabase-enterprise": "enterprise/frontend/src/metabase-enterprise",
  "metabase-types": "frontend/src/metabase-types",
};

function files() {
  return glob.sync(PATTERN);
}

function dependencies() {
  const deps = files().map(filename => {
    const contents = fs.readFileSync(filename, "utf-8");

    const importList = [];
    try {
      const file = babel.transformSync(contents, {
        filename,
        presets: ["@babel/preset-typescript"],
        ast: true,
        code: false,
      });

      babel.traverse(file.ast, {
        enter(path) {
          if (path.node.type === "ImportDeclaration") {
            importList.push(path.node.source.value);
          }
          if (path.node.type === "CallExpression") {
            const callee = path.node.callee;
            const args = path.node.arguments;
            if (callee.type === "Identifier" && callee.name === "require") {
              if (args.length === 1 && args[0].type === "StringLiteral") {
                importList.push(args[0].value);
              }
            }
          }
        },
      });
    } catch (e) {
      console.error(filename, e.toString());
      process.exit(-1);
    }
    const base = path.dirname(filename) + path.sep;
    const absoluteImportList = importList
      .map(name => {
        const absName = name[0] === "." ? path.normalize(base + name) : name;
        const parts = absName.split(path.sep);
        const realPath = ALIAS[parts[0]];
        parts[0] = realPath ? realPath : parts[0];
        const realName = parts.join(path.sep);
        return realName;
      })
      .map(getFilePathFromImportPath)
      .filter(name => minimatch(name, PATTERN));
    return { source: filename, dependencies: absoluteImportList.sort() };
  });
  return deps;
}

function getFilePathFromImportPath(name) {
  const scriptsExtensions = ["js", "ts"];
  const scriptsExtensionsWithJsx = [...scriptsExtensions, "jsx", "tsx"];

  for (let extension of scriptsExtensionsWithJsx) {
    const path = `${name}.${extension}`;

    if (fs.existsSync(path)) {
      return path;
    }
  }

  const isDirectory = fs.existsSync(name) && fs.lstatSync(name).isDirectory();

  for (let extension of scriptsExtensions) {
    const indexScriptPath = `${name}/index.${extension}`;

    if (isDirectory && fs.existsSync(indexScriptPath)) {
      return indexScriptPath;
    }
  }

  return name;
}

function dependents() {
  let dependents = {};
  dependencies().forEach(dep => {
    const { source, dependencies } = dep;
    dependencies.forEach(d => {
      if (!dependents[d]) {
        dependents[d] = [];
      }
      dependents[d].push(source);
    });
  });
  return dependents;
}

function getDependents(sources) {
  const allDependents = dependents();
  let filteredDependents = [];

  sources.forEach(name => {
    const list = allDependents[name];
    if (list && Array.isArray(list) && list.length > 0) {
      filteredDependents.push(...list);
    }
  });

  return Array.from(new Set(filteredDependents)).sort(); // unique
}

function filterDependents() {
  const rl = readline.createInterface({ input: process.stdin });

  const start = async () => {
    let sources = [];
    for await (const line of rl) {
      const name = line.trim();
      if (name.length > 0) {
        sources.push(name);
      }
    }
    const filteredDependents = getDependents(sources);
    console.log(filteredDependents.join("\n"));
  };
  start();
}

function filterAllDependents() {
  const rl = readline.createInterface({ input: process.stdin });

  const start = async () => {
    let sources = [];
    for await (const line of rl) {
      const name = line.trim();
      if (name.length > 0) {
        sources.push(name);
      }
    }
    let filteredDependents = getDependents(sources);

    const allDependents = dependents();
    for (let i = 0; i < filteredDependents.length; ++i) {
      const name = filteredDependents[i];
      const list = allDependents[name];
      if (list && Array.isArray(list) && list.length > 0) {
        const newAddition = list.filter(e => filteredDependents.indexOf(e) < 0);
        filteredDependents.push(...newAddition);
      }
    }
    console.log(filteredDependents.sort().join("\n"));
  };
  start();
}

function countDependents() {
  const allDependents = dependents();
  const sources = Object.keys(allDependents).sort();
  const tally = sources.map(name => {
    return { name, count: allDependents[name].length };
  });
  console.log(tally.map(({ name, count }) => `${count} ${name}`).join("\n"));
}

function countAllDependents() {
  const allDependents = dependents();
  const sources = Object.keys(allDependents).sort();
  const tally = sources.map(name => {
    const list = allDependents[name];
    for (let i = 0; i < list.length; ++i) {
      const deps = allDependents[list[i]];
      if (deps && Array.isArray(deps) && deps.length > 1) {
        const newAddition = deps.filter(e => list.indexOf(e) < 0);
        list.push(...newAddition);
      }
    }
    return { name, count: list.length };
  });
  console.log(tally.map(({ name, count }) => `${count} ${name}`).join("\n"));
}

function matrix() {
  const allDependents = dependents();
  const sources = Object.keys(allDependents).sort();
  const width = Math.max(...sources.map(s => s.length));
  const rows = sources.map(name => {
    const list = allDependents[name];
    const checks = sources.map(dep => (list.indexOf(dep) < 0 ? " " : "x"));
    return name.padEnd(width) + " | " + checks.join("");
  });
  console.log(rows.join("\n"));
}
const USAGE = `
parse-deps cmd

cmd must be one of:

                files   Display list of source files
         dependencies   Show the dependencies of each source file
           dependents   Show the dependents of each source file
    filter-dependents   Filter direct dependents based on stdin
filter-all-dependents   Filter all indirect and direct dependents based on stdin
     count-dependents   List the total count of direct dependents
 count-all-dependents   List the total count of its direct and indirect dependents
               matrix   Display 2-D matrix of dependent relationship
`;

function main(args) {
  const cmd = args[0];
  if (cmd) {
    switch (cmd.toLowerCase()) {
      case "files":
        console.log(files().join("\n"));
        break;
      case "dependencies":
        console.log(JSON.stringify(dependencies(), null, 2));
        break;
      case "dependents":
        console.log(JSON.stringify(dependents(), null, 2));
        break;
      case "filter-dependents":
        filterDependents();
        break;
      case "filter-all-dependents":
        filterAllDependents();
        break;
      case "count-dependents":
        countDependents();
        break;
      case "count-all-dependents":
        countAllDependents();
        break;
      case "matrix":
        matrix();
        break;
      default:
        console.log(USAGE);
        break;
    }
  } else {
    console.log(USAGE);
  }
}

let args = process.argv;
args.shift();
args.shift();
main(args);