Skip to content

Instantly share code, notes, and snippets.

@thepassle
Created February 27, 2024 20:00
Show Gist options
  • Save thepassle/15db3bd10cebfd0f35ab08e5b31d826f to your computer and use it in GitHub Desktop.
Save thepassle/15db3bd10cebfd0f35ab08e5b31d826f 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";
/**
* @param {string} specifier
* @returns {boolean}
*/
export const isBareModuleSpecifier = (specifier) => !!specifier?.replace(/'/g, '')[0].match(/[@a-zA-Z]/g);
/**
* @TODO
* - add `externalDependencies` on ModuleGraph object, we need that in CEM/A
*/
class ModuleGraph {
/**
* @param {string} basePath
* @param {string} entrypoint
*/
constructor(basePath, entrypoint) {
/**
* @type {Map<string, Set<string>>}
*/
this.graph = new Map();
this.entrypoint = path.normalize(entrypoint);
this.basePath = basePath;
this.externalDependencies = new Set();
/**
* @type {Map<string, Module>}
*/
this.modules = new Map();
}
/**
*
* @param {string | ((path: string) => boolean)} targetModule} targetModule
* @returns {Module | undefined}
*/
get (targetModule) {
if (typeof targetModule === "function") {
for (const [module, value] of this.modules.entries()) {
if (targetModule(module)) {
return value;
}
}
} else {
return this.modules.get(targetModule);
}
}
/**
* @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;
}
}
/**
* @typedef {{
* href: `file://${string}`,
* pathname: string,
* path: string,
* source: string
* }} Module
*/
/**
*
* @param {string} entrypoint
* @param {{
* conditions?: string[],
* preserveSymlinks?: boolean,
* basePath?: string,
* analyze?: (module: Module) => Module | 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);
moduleGraph.modules.set(module, {
href: pathToFileURL(module).href,
pathname: pathToFileURL(module).pathname,
path: module,
});
/** Init es-module-lexer wasm */
await init;
while (importsToScan.size) {
importsToScan.forEach((dep) => {
importsToScan.delete(dep);
const source = fs.readFileSync(dep).toString();
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;
if (isBareModuleSpecifier(i.n)) {
moduleGraph.externalDependencies.add(i.n);
}
let pathToDependency;
try {
const fileURL = pathToFileURL(dep);
const resolvedURL = moduleResolve(i.n, fileURL, conditions, preserveSymlinks);
pathToDependency = path.relative(basePath, fileURLToPath(resolvedURL));
const module = {
href: resolvedURL.href,
pathname: resolvedURL.pathname,
path: pathToDependency,
importedBy: [],
}
importsToScan.add(pathToDependency);
if (!moduleGraph.modules.has(pathToDependency)) {
moduleGraph.modules.set(pathToDependency, module);
}
if (!moduleGraph.graph.has(dep)) {
moduleGraph.graph.set(dep, new Set());
}
moduleGraph.graph.get(dep).add(pathToDependency);
const importedModule = moduleGraph.modules.get(pathToDependency);
if (importedModule && !importedModule.importedBy.includes(dep)) {
importedModule.importedBy.push(dep);
}
} catch (e) {
console.log(`Failed to resolve dependency "${i.n}".`, e);
}
});
const currentModule = moduleGraph.modules.get(dep);
currentModule.source = source;
const analyzeResult = options?.analyze?.(currentModule);
if (analyzeResult) {
moduleGraph.graph.set(dep, analyzeResult);
}
});
}
return moduleGraph;
}
const moduleGraph = await createModuleGraph("./foo.js", {
basePath: process.cwd(),
conditions: ["node", "import"],
analyze: (module) => {
},
});
const baz = moduleGraph.get('baz.js');
console.log();
console.log(baz);
moduleGraph.get((p) => p.endsWith('test-pkg/index.js'));
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