33 */
44
55import { readFileSync } from 'node:fs'
6+ import { createRequire } from 'node:module'
67import path from 'node:path'
78import { fileURLToPath } from 'node:url'
89
910const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) )
1011const stubsDir = path . join ( __dirname , 'stubs' )
1112
13+ const requireResolve = createRequire ( import . meta. url )
14+
1215/**
1316 * Stub configuration - maps module patterns to stub files.
1417 * Only includes conservative stubs that are safe to use.
18+ *
19+ * SAFETY NOTE for the Arborist-reachable stubs below:
20+ * We use Arborist via `safeIdealTree` (buildIdealTree + reify in
21+ * packageLockOnly mode) and `safeReify` only. We never call
22+ * `arb.audit()` (→ metavuln-calculator → sigstore/tuf) nor
23+ * `arb.query(...)` (→ @npmcli/query → postcss-selector-parser).
24+ * If a future caller needs those code paths, drop the corresponding
25+ * entry from STUB_MAP.
1526 */
16- const STUB_MAP = {
27+ /**
28+ * Each entry may be a bare stub filename (matches against args.path only)
29+ * or a tuple `[importerPattern, stubFilename]` to require args.importer
30+ * to also match (used to scope relative-path stubs to a specific package).
31+ */
32+ const STUB_MAP : Record < string , string | [ RegExp , string ] > = {
33+ // Git-based package specs (`git://`, `github:`, `gitlab:`). We only
34+ // pass registry specs (`name@version`); pacote/lib/git.js and
35+ // @npmcli /git are unreachable.
36+ '^@npmcli/git$' : 'empty.cjs' ,
37+ // Vulnerability calculator — arb.audit() path only.
38+ '^@npmcli/metavuln-calculator$' : 'empty.cjs' ,
39+ // Arborist CSS-selector query API — unused.
40+ '^@npmcli/query$' : 'empty.cjs' ,
41+ // node-gyp detection — Arborist calls isNodeGypPackage(path) during
42+ // rebuild to decide whether to synthesize an install script for native
43+ // rebuilds. That synthesized script only runs when !ignoreScripts,
44+ // and we always pass ignoreScripts: true, so the detection return
45+ // value is consumed but never acted on. Stub returns falsy =>
46+ // isGyp=false => branch skipped.
47+ '^@npmcli/node-gyp$' : 'npmcli-node-gyp.cjs' ,
48+ // Lifecycle scripts — we always pass ignoreScripts: true, so every
49+ // runScript(...) call site in arborist/reify.js and arborist/rebuild.js
50+ // is guarded out.
51+ '^@npmcli/run-script$' : 'empty.cjs' ,
52+ // Sigstore attestation — reachable only via arb.audit(), unused.
53+ '^@sigstore/(bundle|core|protobuf-specs|sign|tuf|verify)$' : 'empty.cjs' ,
54+ // TUF root-of-trust — Sigstore-only dependency.
55+ '^@tufjs/(canonical-json|models)$' : 'empty.cjs' ,
1756 // Character encoding - we only use UTF-8.
1857 '^(encoding|iconv-lite)$' : 'encoding.cjs' ,
58+ '^postcss-selector-parser$' : 'empty.cjs' ,
59+ // Progress tracker — we pass progress: false. Replace with an
60+ // EventEmitter-based no-op that preserves the `new Tracker(...)`
61+ // + `on('done')` contract Arborist uses.
62+ '^proggy$' : 'proggy.cjs' ,
63+ '^sigstore$' : 'empty.cjs' ,
64+ '^tuf-js$' : 'empty.cjs' ,
65+ // Pacote non-registry fetchers — eagerly required at the top of
66+ // pacote/lib/fetcher.js but only instantiated when the parsed spec
67+ // type matches. We only pass registry specs (name@version/range/tag)
68+ // → RegistryFetcher is the only one that ever fires. Scope each
69+ // stub to imports coming from inside pacote/lib so unrelated ./dir
70+ // etc. imports elsewhere aren't caught.
71+ '^\\./dir\\.js$' : [ / p a c o t e [ \\ / ] l i b [ \\ / ] / , 'pacote-fetcher-throw.cjs' ] ,
72+ '^\\./file\\.js$' : [ / p a c o t e [ \\ / ] l i b [ \\ / ] / , 'pacote-fetcher-throw.cjs' ] ,
73+ '^\\./git\\.js$' : [ / p a c o t e [ \\ / ] l i b [ \\ / ] / , 'pacote-fetcher-throw.cjs' ] ,
74+ '^\\./remote\\.js$' : [ / p a c o t e [ \\ / ] l i b [ \\ / ] / , 'pacote-fetcher-throw.cjs' ] ,
75+ // Arborist AuditReport — load() is gated on options.audit !== false
76+ // and we always pass audit: false. The require is eager but the
77+ // class is never instantiated.
78+ '^\\.\\./audit-report\\.js$' : [
79+ / @ n p m c l i [ \\ / ] a r b o r i s t [ \\ / ] l i b [ \\ / ] a r b o r i s t [ \\ / ] / ,
80+ 'arborist-audit-report.cjs' ,
81+ ] ,
82+ // Arborist YarnLock — instantiated only when a yarn.lock file is
83+ // present in the install dir. We operate in scratch tmp dirs (pin
84+ // flow) or Socket cache dirs (install flow), neither of which has
85+ // a yarn.lock.
86+ '^\\./yarn-lock\\.js$' : [
87+ / @ n p m c l i [ \\ / ] a r b o r i s t [ \\ / ] l i b [ \\ / ] / ,
88+ 'arborist-yarn-lock.cjs' ,
89+ ] ,
90+ // Arborist IsolatedReifier mixin — only adds methods used when
91+ // options.installStrategy === 'linked'. We never pass that flag.
92+ // Identity mixin preserves the class composition chain.
93+ '^\\./isolated-reifier\\.js$' : [
94+ / @ n p m c l i [ \\ / ] a r b o r i s t [ \\ / ] l i b [ \\ / ] a r b o r i s t [ \\ / ] / ,
95+ 'arborist-isolated-reifier.cjs' ,
96+ ] ,
97+ // Arborist querySelectorAll — arb.query(selector) API, unused.
98+ // Fixes the @npmcli /query + postcss-selector-parser stub by
99+ // preventing the call site from being reachable.
100+ '^\\./query-selector-all\\.js$' : [
101+ / @ n p m c l i [ \\ / ] a r b o r i s t [ \\ / ] l i b [ \\ / ] / ,
102+ 'arborist-query-selector-all.cjs' ,
103+ ] ,
104+ // Arborist printable-tree — Node.prototype.toJSON() helper. Arborist
105+ // never JSON.stringify's a tree itself; the helper only matters for
106+ // debug dumps callers might do. We don't.
107+ '^\\./printable\\.js$' : [
108+ / @ n p m c l i [ \\ / ] a r b o r i s t [ \\ / ] l i b [ \\ / ] / ,
109+ 'arborist-printable.cjs' ,
110+ ] ,
111+ // cacache.verify — the `npm cache verify` helper. Exported from
112+ // cacache/lib/index.js but no code in our bundle chain calls it.
113+ '^\\./verify\\.js$' : [ / c a c a c h e [ \\ / ] l i b [ \\ / ] / , 'empty.cjs' ] ,
114+ // debug's browser entry — debug/src/index.js conditionally requires
115+ // it via `typeof process === 'undefined' || process.browser === true`.
116+ // We run in Node and set process.browser=false, so the branch is
117+ // dead. Esbuild still bundles the eager require path.
118+ '^\\./browser\\.js$' : [ / d e b u g [ \\ / ] s r c [ \\ / ] / , 'empty.cjs' ] ,
19119}
20120
21- // Import createRequire at top level
22- import { createRequire } from 'node:module'
23-
24- const requireResolve = createRequire ( import . meta. url )
25-
26121/**
27122 * Create esbuild plugin to force npm packages to resolve from node_modules.
28123 * This prevents tsconfig.json path mappings from creating circular dependencies.
@@ -145,27 +240,38 @@ function createForceNodeModulesPlugin() {
145240
146241/**
147242 * Create esbuild plugin to stub modules using files from stubs/ directory.
148- *
149- * @param {Record<string, string> } stubMap - Map of regex patterns to stub filenames
150- * @returns {import('esbuild').Plugin }
243+ * stubMap keys are regex patterns; values are stub filenames.
151244 */
152- function createStubPlugin ( stubMap = STUB_MAP ) {
245+ function createStubPlugin (
246+ stubMap : Record < string , string | [ RegExp , string ] > = STUB_MAP ,
247+ ) {
153248 // Pre-compile regex patterns and load stub contents
154- const stubs = Object . entries ( stubMap ) . map ( ( [ pattern , filename ] ) => ( {
155- filter : new RegExp ( pattern ) ,
156- contents : readFileSync ( path . join ( stubsDir , filename ) , 'utf8' ) ,
157- stubFile : filename ,
158- } ) )
249+ const stubs = Object . entries ( stubMap ) . map ( ( [ pattern , value ] ) => {
250+ const [ importerFilter , filename ] = Array . isArray ( value )
251+ ? value
252+ : [ undefined , value ]
253+ return {
254+ filter : new RegExp ( pattern ) ,
255+ importerFilter,
256+ contents : readFileSync ( path . join ( stubsDir , filename ) , 'utf8' ) ,
257+ stubFile : filename ,
258+ }
259+ } )
159260
160261 return {
161262 name : 'stub-modules' ,
162263 setup ( build ) {
163- for ( const { contents, filter, stubFile } of stubs ) {
264+ for ( const { contents, filter, importerFilter , stubFile } of stubs ) {
164265 // Resolve: mark modules as stubbed
165- build . onResolve ( { filter } , args => ( {
166- path : args . path ,
167- namespace : `stub:${ stubFile } ` ,
168- } ) )
266+ build . onResolve ( { filter } , args => {
267+ if ( importerFilter && ! importerFilter . test ( args . importer ) ) {
268+ return undefined
269+ }
270+ return {
271+ path : args . path ,
272+ namespace : `stub:${ stubFile } ` ,
273+ }
274+ } )
169275
170276 // Load: return stub file contents
171277 build . onLoad ( { filter : / .* / , namespace : `stub:${ stubFile } ` } , ( ) => ( {
@@ -191,17 +297,6 @@ export function getPackageSpecificOptions(packageName) {
191297 opts . define = {
192298 'process.versions.node' : '"18.0.0"' ,
193299 }
194- } else if ( packageName === 'zod' ) {
195- // Zod has localization files we don't need.
196- opts . external = [ ...( opts . external || [ ] ) , './locales/*' ]
197- } else if ( packageName === 'external-pack' ) {
198- // Inquirer packages have heavy dependencies we can exclude.
199- opts . external = [ ...( opts . external || [ ] ) , 'rxjs/operators' ]
200- } else if ( packageName . startsWith ( '@inquirer/' ) ) {
201- // @inquirer packages export default only - unwrap for CJS compatibility.
202- opts . footer = {
203- js : 'if (module.exports && module.exports.default && Object.keys(module.exports).length === 1) { module.exports = module.exports.default; }' ,
204- }
205300 } else if ( packageName === '@socketregistry/packageurl-js' ) {
206301 // packageurl-js imports from socket-lib, creating a circular dependency.
207302 // Mark socket-lib imports as external to avoid bundling issues.
0 commit comments