Skip to content

Instantly share code, notes, and snippets.

@thepassle
Created February 25, 2024 14:10
Show Gist options
  • Save thepassle/06343ecb739ecafbf345b8d12c0f289c to your computer and use it in GitHub Desktop.
Save thepassle/06343ecb739ecafbf345b8d12c0f289c to your computer and use it in GitHub Desktop.
import fs from "fs";
import path from "path";
import { pathToFileURL, fileURLToPath } from "url";
import { builtinModules } from "module";
import { init, parse } from "es-module-lexer";
import { moduleResolve } from "import-meta-resolve";
class ModuleGraph {
/**
* @param {string} basePath
* @param {string} entrypoint
*/
constructor(basePath, entrypoint) {
this.graph = new Map();
this.entrypoint = path.normalize(entrypoint);
this.basePath = basePath;
}
/**
* @returns {string[]}
*/
getUniqueModules() {
const uniqueModules = new Set();
for (const [module, dependencies] of this.graph.entries()) {
uniqueModules.add(module);
for (const dependency of dependencies) {
uniqueModules.add(dependency);
}
}
return [...uniqueModules].map((p) => path.relative(this.basePath, p));
}
/**
* @param {string | ((path: string) => boolean)} targetModule
* @returns {string[][]}
*/
findImportChains(targetModule) {
const chains = [];
const dfs = (module, path) => {
const condition =
typeof targetModule === "function"
? targetModule(module)
: module === targetModule;
if (condition) {
chains.push(path);
return;
}
const dependencies = this.graph.get(module);
if (dependencies) {
for (const dependency of dependencies) {
if (!path.includes(dependency)) {
dfs(dependency, [...path, dependency]);
}
}
}
};
dfs(this.entrypoint, [this.entrypoint]);
return chains;
}
}
/**
*
* @param {string} entrypoint
* @param {{
* conditions?: string[],
* preserveSymlinks?: boolean,
* basePath?: string,
* analyze?: (source: string) => void
* }} options
* @returns {Promise<ModuleGraph>}
*/
export async function createModuleGraph(entrypoint, options = {}) {
const basePath = options?.basePath ?? process.cwd();
const conditions = new Set(options?.conditions ?? ["node", "import"]);
const preserveSymlinks = options?.preserveSymlinks ?? false;
const module = path.relative(
basePath,
fileURLToPath(
moduleResolve(entrypoint, pathToFileURL(path.join(basePath, entrypoint)))
)
);
const importsToScan = new Set([module]);
const moduleGraph = new ModuleGraph(basePath, entrypoint);
/** Init es-module-lexer wasm */
await init;
while (importsToScan.size) {
importsToScan.forEach((dep) => {
importsToScan.delete(dep);
const source = fs.readFileSync(dep).toString();
options?.analyze?.(source);
const [imports] = parse(source);
imports?.forEach((i) => {
if (!i.n) return;
/** Skip built-in modules like fs, path, etc */
if (builtinModules.includes(i.n.replace("node:", ""))) return;
let pathToDependency;
try {
pathToDependency = path.relative(
basePath,
fileURLToPath(
moduleResolve(
i.n,
pathToFileURL(dep),
conditions,
preserveSymlinks
)
)
);
importsToScan.add(pathToDependency);
if (!moduleGraph.graph.has(dep)) {
moduleGraph.graph.set(dep, new Set());
}
moduleGraph.graph.get(dep).add(pathToDependency);
} catch (e) {
console.log(`Failed to resolve dependency "${i.n}".`, e);
}
});
});
}
return moduleGraph;
}
const moduleGraph = await createModuleGraph("./foo.js", {
basePath: process.cwd(),
conditions: ["node", "import"],
analyze: (source) => {},
});
console.log(moduleGraph);
console.log("\n");
const chains = moduleGraph.findImportChains("baz.js");
const chains2 = moduleGraph.findImportChains((p) => p.endsWith("baz.js"));
console.log({ chains2 });
chains.forEach((c) => console.log(c.join(" -> ")));
console.log("\n");
const uniqueModules = moduleGraph.getUniqueModules();
console.log(uniqueModules);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment