Skip to content

Commit d3aba85

Browse files
committed
feat(build): add stub system for external dependencies
Add organized stub infrastructure to reduce external bundle sizes: - Create scripts/build-externals/stubs/ directory - Add utility stubs (empty.cjs, noop.cjs, throw.cjs) - Add active stubs (encoding.cjs, debug.cjs) - Integrate stubs into esbuild config via createStubPlugin() - Document stub system with philosophy and usage Conservative approach: Only stub provably unused dependencies. Active stubs save ~18KB: - encoding/iconv-lite: ~9KB (UTF-8 only) - debug: ~9KB (already compiled out)
1 parent 39aa25e commit d3aba85

File tree

7 files changed

+193
-17
lines changed

7 files changed

+193
-17
lines changed

scripts/build-externals/esbuild-config.mjs

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,59 @@
22
* @fileoverview esbuild configuration for external package bundling.
33
*/
44

5+
import { readFileSync } from 'node:fs'
6+
import path from 'node:path'
7+
import { fileURLToPath } from 'node:url'
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
10+
const stubsDir = path.join(__dirname, 'stubs')
11+
12+
/**
13+
* Stub configuration - maps module patterns to stub files.
14+
* Only includes conservative stubs that are safe to use.
15+
*/
16+
const STUB_MAP = {
17+
// Character encoding - we only use UTF-8
18+
'^(encoding|iconv-lite)$': 'encoding.cjs',
19+
20+
// Debug logging - already disabled via process.env.DEBUG = undefined
21+
'^debug$': 'debug.cjs',
22+
}
23+
24+
/**
25+
* Create esbuild plugin to stub modules using files from stubs/ directory.
26+
*
27+
* @param {Record<string, string>} stubMap - Map of regex patterns to stub filenames
28+
* @returns {import('esbuild').Plugin}
29+
*/
30+
function createStubPlugin(stubMap = STUB_MAP) {
31+
// Pre-compile regex patterns and load stub contents
32+
const stubs = Object.entries(stubMap).map(([pattern, filename]) => ({
33+
filter: new RegExp(pattern),
34+
contents: readFileSync(path.join(stubsDir, filename), 'utf8'),
35+
stubFile: filename,
36+
}))
37+
38+
return {
39+
name: 'stub-modules',
40+
setup(build) {
41+
for (const { contents, filter, stubFile } of stubs) {
42+
// Resolve: mark modules as stubbed
43+
build.onResolve({ filter }, args => ({
44+
path: args.path,
45+
namespace: `stub:${stubFile}`,
46+
}))
47+
48+
// Load: return stub file contents
49+
build.onLoad({ filter: /.*/, namespace: `stub:${stubFile}` }, () => ({
50+
contents,
51+
loader: 'js',
52+
}))
53+
}
54+
},
55+
}
56+
}
57+
558
/**
659
* Get package-specific esbuild options.
760
*
@@ -81,23 +134,7 @@ export function getEsbuildConfig(entryPoint, outfile, packageOpts = {}) {
81134
'@socketsecurity/registry',
82135
...(packageOpts.external || []),
83136
],
84-
plugins: [
85-
{
86-
name: 'stub-encoding',
87-
setup(build) {
88-
// Stub out encoding and iconv-lite packages.
89-
build.onResolve({ filter: /^(encoding|iconv-lite)$/ }, args => ({
90-
path: args.path,
91-
namespace: 'stub-encoding',
92-
}))
93-
94-
build.onLoad({ filter: /.*/, namespace: 'stub-encoding' }, () => ({
95-
contents: 'module.exports = {};',
96-
loader: 'js',
97-
}))
98-
},
99-
},
100-
],
137+
plugins: [createStubPlugin()],
101138
minify: true,
102139
sourcemap: false,
103140
metafile: true,
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# External Dependency Stubs
2+
3+
This directory contains stub modules used during the external package bundling process to replace unused dependencies and reduce bundle size.
4+
5+
**Philosophy:** Be conservative. Only stub dependencies that are provably unused or already disabled.
6+
7+
## How It Works
8+
9+
The build-externals system bundles external npm dependencies (like pacote, cacache, make-fetch-happen) into standalone modules in `dist/external/`. During bundling, esbuild uses the stubs in this directory to replace dependencies we don't need.
10+
11+
The stub configuration lives in `../esbuild-config.mjs`, which maps module patterns to stub files:
12+
13+
```javascript
14+
const STUB_MAP = {
15+
'^(encoding|iconv-lite)$': 'encoding.cjs',
16+
'^debug$': 'debug.cjs',
17+
}
18+
```
19+
20+
When esbuild encounters `require('encoding')` during bundling, it replaces it with the contents of `encoding.cjs` instead of bundling the entire encoding package.
21+
22+
## Stub Types
23+
24+
This directory provides both active stubs (currently in use) and utility stubs (available for future use):
25+
26+
### Utility Stubs (Available for Use)
27+
28+
**`empty.cjs`** - Empty object for unused modules
29+
- Exports: `{}`
30+
- Use case: Dependencies referenced but never executed
31+
32+
**`noop.cjs`** - No-op function for optional features
33+
- Exports: Function that does nothing
34+
- Use case: Logging, debugging, optional callbacks
35+
36+
**`throw.cjs`** - Error-throwing for unexpected usage
37+
- Exports: Function that throws descriptive error
38+
- Use case: Code paths that should never execute
39+
40+
### Active Stubs (Currently in Use)
41+
42+
**`encoding.cjs`** - Character encoding stub
43+
- Replaces: `encoding`, `iconv-lite`
44+
- Reason: We only use UTF-8, don't need legacy encoding support
45+
- Size impact: ~9KB saved (pacote, make-fetch-happen)
46+
47+
**`debug.cjs`** - Debug logging stub
48+
- Replaces: `debug` module
49+
- Reason: Already compiled out via `process.env.DEBUG = undefined`
50+
- Size impact: ~9KB saved
51+
52+
## Adding New Stubs
53+
54+
**Before adding a stub:**
55+
1. Verify the dependency is truly unused via code analysis
56+
2. Check if it's already disabled via esbuild `define` constants
57+
3. Consider the risk - conservative only!
58+
59+
**To add a stub:**
60+
1. Create stub file in this directory
61+
2. Document what it replaces and why it's safe
62+
3. Add entry to `STUB_MAP` in `../esbuild-config.mjs`
63+
4. Test: `pnpm build && pnpm test`
64+
5. Verify size savings: `du -sh dist/external`
65+
66+
## Testing Stubs
67+
68+
After adding stubs, verify:
69+
1. Build succeeds: `pnpm build`
70+
2. Tests pass: `pnpm test`
71+
3. No runtime errors in dependent packages
72+
4. Bundle size decreased as expected
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Debug stub - stubs out debug logging.
3+
*
4+
* Many npm packages include debug() calls for verbose logging.
5+
* In production, these are disabled via process.env.DEBUG.
6+
* This stub removes the debug module entirely.
7+
*
8+
* Used by: Various npm packages
9+
* Savings: ~9KB + removes debug dependency checks
10+
*/
11+
'use strict'
12+
13+
// Return a no-op function that accepts any arguments
14+
function debug() {
15+
return function noop() {}
16+
}
17+
18+
// Common debug properties
19+
debug.enabled = false
20+
debug.names = []
21+
debug.skips = []
22+
debug.formatters = {}
23+
24+
module.exports = debug
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Empty stub - provides no functionality.
3+
* Used for dependencies that are never actually called in our code paths.
4+
*/
5+
'use strict'
6+
7+
module.exports = {}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* Encoding/iconv-lite stub.
3+
*
4+
* These packages provide character encoding conversion (e.g., UTF-8 to Latin1).
5+
* We only work with UTF-8, so we stub them out to save ~100KB.
6+
*
7+
* Used by: make-fetch-happen, pacote (for legacy content-encoding)
8+
*/
9+
'use strict'
10+
11+
module.exports = {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* No-op stub - provides functions that do nothing.
3+
* Used for optional features we don't need (logging, debugging, etc).
4+
*/
5+
'use strict'
6+
7+
const noop = () => {}
8+
9+
module.exports = noop
10+
module.exports.default = noop
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Throw stub - errors if called.
3+
* Used for dependencies that should never be reached in production.
4+
* Helps catch bugs if accidentally called.
5+
*/
6+
'use strict'
7+
8+
function throwStub(moduleName) {
9+
throw new Error(
10+
`Module '${moduleName}' is stubbed and should not be called. ` +
11+
'This is likely a bundling error or unexpected code path.',
12+
)
13+
}
14+
15+
module.exports = throwStub

0 commit comments

Comments
 (0)