Skip to content

Commit 9085b29

Browse files
committed
fix(build): prevent circular dependencies in external bundling
Add createForceNodeModulesPlugin() to force npm package resolution from node_modules, preventing tsconfig.json path mappings from creating circular dependencies during external dependency bundling. This resolves issues with packages like cacache, make-fetch-happen, fast-sort, and pacote that have tsconfig path mappings. Source files retain 'use strict' directives for correctness while the esbuild banner ensures all bundled output includes the directive.
1 parent fc1552b commit 9085b29

1 file changed

Lines changed: 97 additions & 1 deletion

File tree

scripts/build-externals/esbuild-config.mjs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,102 @@ const STUB_MAP = {
2121
'^debug$': 'debug.cjs',
2222
}
2323

24+
// Import createRequire at top level
25+
import { createRequire } from 'node:module'
26+
27+
const requireResolve = createRequire(import.meta.url)
28+
29+
/**
30+
* Create esbuild plugin to force npm packages to resolve from node_modules.
31+
* This prevents tsconfig.json path mappings from creating circular dependencies.
32+
*
33+
* @returns {import('esbuild').Plugin}
34+
*/
35+
function createForceNodeModulesPlugin() {
36+
/**
37+
* Packages that must be resolved from node_modules to prevent circular dependencies.
38+
*
39+
* THE PROBLEM:
40+
* ────────────
41+
* Some packages have tsconfig.json path mappings like:
42+
* "cacache": ["./src/external/cacache"]
43+
*
44+
* This creates a circular dependency during bundling:
45+
*
46+
* ┌─────────────────────────────────────────────────┐
47+
* │ │
48+
* │ esbuild bundles: src/external/cacache.js │
49+
* │ ↓ │
50+
* │ File contains: require('cacache') │
51+
* │ ↓ │
52+
* │ tsconfig redirects: 'cacache' → src/external/ │ ← LOOP!
53+
* │ ↓ │
54+
* │ esbuild tries to bundle: src/external/cacache │
55+
* │ ↓ │
56+
* │ Circular reference! ⚠️ │
57+
* └─────────────────────────────────────────────────┘
58+
*
59+
* THE SOLUTION:
60+
* ─────────────
61+
* This plugin intercepts resolution and forces these packages to resolve
62+
* from node_modules, bypassing the tsconfig path mappings:
63+
*
64+
* src/external/cacache.js
65+
* ↓
66+
* require('cacache')
67+
* ↓
68+
* Plugin intercepts → node_modules/cacache ✓
69+
*
70+
* PACKAGES WITH ACTUAL TSCONFIG MAPPINGS (as of now):
71+
* ────────────────────────────────────────────────────
72+
* ✓ cacache - line 37 in tsconfig.json
73+
* ✓ make-fetch-happen - line 38 in tsconfig.json
74+
* ✓ fast-sort - line 39 in tsconfig.json
75+
* ✓ pacote - line 40 in tsconfig.json
76+
*
77+
* ADDITIONAL PACKAGES (defensive):
78+
* ────────────────────────────────
79+
* · libnpmexec - Related to pacote, included for consistency
80+
* · libnpmpack - Related to pacote, included for consistency
81+
* · npm-package-arg - Related to pacote, included for consistency
82+
* · normalize-package-data - Related to npm packages, included for consistency
83+
*
84+
* NOTE: Other external packages (debug, del, semver, etc.) don't have
85+
* tsconfig mappings, so they naturally resolve from node_modules without
86+
* needing to be listed here.
87+
*/
88+
const packagesWithPathMappings = [
89+
'cacache',
90+
'make-fetch-happen',
91+
'fast-sort',
92+
'pacote',
93+
'libnpmexec',
94+
'libnpmpack',
95+
'npm-package-arg',
96+
'normalize-package-data',
97+
]
98+
99+
return {
100+
name: 'force-node-modules',
101+
setup(build) {
102+
for (const pkg of packagesWithPathMappings) {
103+
build.onResolve({ filter: new RegExp(`^${pkg}$`) }, args => {
104+
// Only intercept if not already in node_modules
105+
if (!args.importer.includes('node_modules')) {
106+
try {
107+
return { path: requireResolve.resolve(pkg), external: false }
108+
} catch {
109+
// Package not found, let esbuild handle the error
110+
return null
111+
}
112+
}
113+
return null
114+
})
115+
}
116+
},
117+
}
118+
}
119+
24120
/**
25121
* Create esbuild plugin to stub modules using files from stubs/ directory.
26122
*
@@ -134,7 +230,7 @@ export function getEsbuildConfig(entryPoint, outfile, packageOpts = {}) {
134230
'@socketsecurity/registry',
135231
...(packageOpts.external || []),
136232
],
137-
plugins: [createStubPlugin()],
233+
plugins: [createForceNodeModulesPlugin(), createStubPlugin()],
138234
minify: true,
139235
sourcemap: false,
140236
metafile: true,

0 commit comments

Comments
 (0)