UNPKG

convex

Version:

Client for the Convex Cloud

169 lines (146 loc) 5.32 kB
/** * Normally esbuild can output a metafile containing the dependency * graph. However if bundling fails (say no dependency can be found) * then no metafile is produced. * * This plugin produces a similar dependency graph even in incomplete * bundling runs that are aborted early due to an error. * * It is WAY SLOWER! * * This enables a bundler error to be annotated with an import trace * describing why that file was imported. */ import * as esbuild from "esbuild"; import * as path from "path"; // Interface for the tracer object returned by the plugin interface ImportTracer { /** * Traces all import chains from a specific entry point to the specified file. * @param entryPoint The entry point to start the trace from. * @param filename The file to trace import chains to. * @returns An array of import chains, each chain being an array of file paths. */ traceImportChains(entryPoint: string, filename: string): string[][]; /** * Returns a copy of the entire dependency graph. * @returns A map where keys are importers and values are sets of imported files. */ getDependencyGraph(): Map<string, Set<string>>; } // Interface for the combined plugin and tracer interface ImportTracerPlugin { plugin: esbuild.Plugin; tracer: ImportTracer; } /** * Creates an esbuild plugin that tracks import dependencies. * The plugin builds a dependency graph during bundling without * reimplementing module resolution logic. * * @returns An object containing the plugin and a tracer for analyzing import chains. */ function createImportTracerPlugin(): ImportTracerPlugin { // Dependency graph: Map<importer, Set<imported>> const dependencyGraph = new Map<string, Set<string>>(); // Set of entry points const entryPoints = new Set<string>(); // Set of imports currently being processed to avoid infinite recursion const processingImports = new Set<string>(); const plugin: esbuild.Plugin = { name: "import-tracer", setup(build) { // Reset state on new build build.onStart(() => { dependencyGraph.clear(); entryPoints.clear(); processingImports.clear(); }); // Capture entry points build.onResolve({ filter: /.*/ }, (args) => { if (args.kind === "entry-point") { entryPoints.add(args.path); } return null; // Continue with normal resolution }); // Track resolved imports build.onResolve({ filter: /.*/ }, async (args) => { if ( args.importer && (args.kind === "import-statement" || args.kind === "require-call" || args.kind === "dynamic-import" || args.kind === "require-resolve") ) { const importKey = `${args.importer}:${args.path}`; // Avoid infinite recursion if (processingImports.has(importKey)) { return null; } try { processingImports.add(importKey); //console.log("-------------> ", args.path); // Use esbuild's resolution logic - this lets us avoid // reimplementing module resolution ourselves const result = await build.resolve(args.path, { // Does it work to pretendit's always an import??? kind: "import-statement", resolveDir: args.resolveDir, }); if (result.errors.length === 0) { // Record the dependency relationship if (!dependencyGraph.has(args.importer)) { dependencyGraph.set(args.importer, new Set()); } dependencyGraph.get(args.importer)!.add(result.path); } } finally { processingImports.delete(importKey); } } return null; // Let esbuild continue with normal resolution }); }, }; const tracer: ImportTracer = { traceImportChains(entryPoint: string, filename: string): string[][] { const resolvedEntryPoint = path.resolve(entryPoint); // Find shortest path using BFS const findShortestPath = ( start: string, target: string, ): string[] | null => { const queue: { node: string; path: string[] }[] = [ { node: start, path: [start] }, ]; const visited = new Set<string>([start]); while (queue.length > 0) { const { node, path } = queue.shift()!; if (node === target) { return path; } const imports = dependencyGraph.get(node) || new Set(); for (const imp of imports) { if (!visited.has(imp)) { visited.add(imp); queue.push({ node: imp, path: [...path, imp] }); } } } return null; }; const result = findShortestPath(resolvedEntryPoint, filename); return result ? [result] : []; }, getDependencyGraph(): Map<string, Set<string>> { // Return a deep copy of the dependency graph const copy = new Map<string, Set<string>>(); for (const [key, value] of dependencyGraph.entries()) { copy.set(key, new Set(value)); } return copy; }, }; return { plugin, tracer }; } export default createImportTracerPlugin;