Skip to content

Commit a192f78

Browse files
committed
build(externals): restore stub machinery + fix subpath output ext
Restores the full STUB_MAP + `createStubPlugin` + `[importerPattern, stubFilename]` scoped-stub tuple form that was inadvertently wiped from `scripts/build-externals/esbuild-config.mts` by the 5.19.1 release commit (f30d3af), reducing the stub set from 11+ entries to a single encoding stub. Reinstates stubs for `@npmcli/{git,metavuln-calculator,query,node-gyp,run-script}`, `@sigstore/*`, `@tufjs/*`, `postcss-selector-parser`, `proggy`, `sigstore`, `tuf-js`, pacote non-registry fetchers, arborist internals (audit-report, yarn-lock, isolated-reifier, query-selector-all, printable), cacache verify, and debug's browser entry — all conservative stubs gated on code paths we never reach from socket-lib consumers. Drops the now-dead `zod/v4/locales` stub (zod is no longer bundled after the previous commit). Fixes `orchestrator.mts` subpath bundling to append `.js` to the output filename when the subpath omits it, so `@sinclair/typebox/value` can be requested under the name its exports map actually declares while still landing at `dist/external/@sinclair/typebox/value.js` at runtime.
1 parent c1deeeb commit a192f78

3 files changed

Lines changed: 133 additions & 46 deletions

File tree

scripts/build-externals/esbuild-config.mts

Lines changed: 126 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,121 @@
33
*/
44

55
import { readFileSync } from 'node:fs'
6+
import { createRequire } from 'node:module'
67
import path from 'node:path'
78
import { fileURLToPath } from 'node:url'
89

910
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1011
const 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$': [/pacote[\\/]lib[\\/]/, 'pacote-fetcher-throw.cjs'],
72+
'^\\./file\\.js$': [/pacote[\\/]lib[\\/]/, 'pacote-fetcher-throw.cjs'],
73+
'^\\./git\\.js$': [/pacote[\\/]lib[\\/]/, 'pacote-fetcher-throw.cjs'],
74+
'^\\./remote\\.js$': [/pacote[\\/]lib[\\/]/, '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+
/@npmcli[\\/]arborist[\\/]lib[\\/]arborist[\\/]/,
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+
/@npmcli[\\/]arborist[\\/]lib[\\/]/,
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+
/@npmcli[\\/]arborist[\\/]lib[\\/]arborist[\\/]/,
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+
/@npmcli[\\/]arborist[\\/]lib[\\/]/,
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+
/@npmcli[\\/]arborist[\\/]lib[\\/]/,
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$': [/cacache[\\/]lib[\\/]/, '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$': [/debug[\\/]src[\\/]/, '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.

scripts/build-externals/orchestrator.mts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,13 @@ async function bundleAllPackages(options = {}) {
146146
// Bundle subpath exports (e.g., @npmcli/package-json/lib/read-package)
147147
if (subpaths) {
148148
for (const subpath of subpaths) {
149-
const outputPath = path.join(distExternalDir, scope, subpath)
149+
// Output file always ends in .js. Subpath may already include it
150+
// (e.g. '@npmcli/package-json/lib/read-package.js' — the package's
151+
// own exports map uses that literal path) or omit it (e.g.
152+
// '@sinclair/typebox/value' — exports map uses './value', so the
153+
// subpath can't include .js or resolve will fail).
154+
const outFilename = subpath.endsWith('.js') ? subpath : `${subpath}.js`
155+
const outputPath = path.join(distExternalDir, scope, outFilename)
150156
const packageName = `${scope}/${subpath}`
151157
// Ensure parent directory exists
152158
await ensureDir(path.dirname(outputPath))

scripts/build-externals/stubs/zod-locales.cjs

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)