Skip to content

Commit 567e8cd

Browse files
committed
perf(external): cherry-pick imports to reduce bundle sizes
Optimize external package bundles by importing only what we actually use instead of full barrel imports. This reduces bundle size by only including the specific functions/classes we need. **Cherry-picked imports:** - **pacote**: Only `get()` from lib/fetcher.js to implement extract() (avoids bundling manifest, packument, tarball, resolve, etc.) - **cacache**: Only get, put, rm.{entry,all}, ls.stream, tmp.withTmp (avoids bundling verify, index operations, etc.) 440KB → 432KB (2% reduction) - **make-fetch-happen**: Only .defaults() method from lib/index.js (avoids bundling full fetch implementation) - **@npmcli/arborist**: Direct import from lib/arborist/index.js (avoids bundling Node, Link, Edge, Shrinkwrap classes) These optimizations maintain full functionality while reducing bundle overhead from unused code paths. perf(external): cherry-pick more lib imports Continue optimizing external package bundles by importing from lib/ directories instead of package roots. This bypasses unnecessary wrapper code and package.json resolution overhead. **Optimizations:** - **npm-package-arg**: Import from lib/npa.js directly (exports npa, npa.resolve, npa.toPurl, npa.Result) - **normalize-package-data**: Import from lib/normalize.js (exports normalize function and normalize.fixer) - **libnpmpack**: Import from lib/index.js (single pack function, already uses pacote/arborist internally) While bundle sizes remain similar (these packages pull in heavy dependencies), the direct lib imports avoid unnecessary wrappers and improve tree-shaking potential. perf(external): bundle npm packages for deduplication Create two bundled modules for npm-related packages to achieve significant size reduction through dependency deduplication. **npm-core bundle** (npm-package-arg, normalize-package-data, semver): - Before: 380KB total (160KB + 152KB + 68KB) - After: 224KB - Savings: 156KB (41% reduction) **npm-pack bundle** (pacote, libnpmpack, cacache, make-fetch-happen): - Before: ~5.4MB total (1.8MB + 2.4MB + 432KB + 792KB) - After: 2.4MB - Savings: ~3MB (56% reduction) **Total savings: ~3.2MB (57% reduction for npm packages)** Individual package files now re-export from bundles, maintaining the same API while leveraging shared dependencies. This hybrid approach keeps other external packages unbundled for optimal lazy loading. Changes: - Add src/external/npm-core.js and npm-pack.js bundles - Add type definitions for both bundles - Update individual package files to re-export from bundles - Update build config to bundle new modules fix(external): copy non-bundled re-export wrappers Fix build to properly handle npm packages that re-export from bundles. Individual package files are now thin wrappers (4KB each) that pull from the consolidated npm-core and npm-pack bundles. **Changes:** - Add copy logic for `bundle: false` packages in orchestrator - Set `bundle: false` for npm packages that re-export from bundles - Individual files are now ~150 byte source files copied as-is **Verified bundle sizes:** - npm-core.js: 224KB (npm-package-arg, normalize-package-data, semver) - npm-pack.js: 2.4MB (pacote, libnpmpack, cacache, make-fetch-happen) - Re-export wrappers: 4KB each (thin source copies) - Total: 5.8MB → 2.6MB saved (55% reduction) All 5,375 tests passing, including external bundle validation tests. fix(build): resolve npm bundle subpath imports correctly Update esbuild plugin to handle subpath imports like npm-package-arg/lib/npa.js instead of just bare package names. This fixes npm-core and npm-pack bundles to actually bundle dependencies instead of copying source files. Changed regex from ^pkg$ (exact match) to ^pkg(/|$) (match with or without subpaths). Results: - npm-core.js: 220KB bundled (npmPackageArg, normalizePackageData, semver) - npm-pack.js: 2.4MB bundled (pacote, libnpmpack, cacache, makeFetchHappen) - Individual re-export files: ~140B each (thin wrappers) refactor(build): include arborist in npm-pack bundle Move @npmcli/arborist into npm-pack bundle for better deduplication since it depends on pacote, cacache, and other npm packages. Changes: - Add Arborist to npm-pack.js bundle (alphabetically sorted) - Update @npmcli/arborist.js to re-export from npm-pack - Add bundle: false config for arborist to copy wrapper instead of bundling - Update orchestrator to handle bundle: false for scoped packages Results: - @npmcli/arborist.js: 2.6MB → 143B (thin wrapper) - npm-pack.js: 2.4MB → 3.1MB (includes arborist + dependencies) - Total size reduction: 2.6MB + 2.4MB = 5.0MB → 3.1MB + 143B = 3.1MB (38% smaller)
1 parent 3f57d39 commit 567e8cd

File tree

15 files changed

+160
-22
lines changed

15 files changed

+160
-22
lines changed

scripts/build-externals/config.mjs

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,27 @@
55

66
// Define which packages need bundling (ones that are actual npm packages).
77
export const externalPackages = [
8-
// NPM internals
9-
{ name: 'cacache', bundle: true },
10-
{ name: 'pacote', bundle: true },
11-
{ name: 'make-fetch-happen', bundle: true },
8+
// NPM bundles - grouped for better deduplication
9+
// npm-core: npm-package-arg, normalize-package-data, semver
10+
{ name: 'npm-core', bundle: true },
11+
// npm-pack: arborist, cacache, libnpmpack, make-fetch-happen, pacote
12+
{ name: 'npm-pack', bundle: true },
13+
// NPM internals - individual packages now just re-export from bundles (no bundling needed)
14+
{ name: 'cacache', bundle: false },
15+
{ name: 'pacote', bundle: false },
16+
{ name: 'make-fetch-happen', bundle: false },
1217
{ name: 'libnpmexec', bundle: true },
13-
{ name: 'libnpmpack', bundle: true },
14-
{ name: 'npm-package-arg', bundle: true },
15-
{ name: 'normalize-package-data', bundle: true },
18+
{ name: 'libnpmpack', bundle: false },
19+
{ name: 'npm-package-arg', bundle: false },
20+
{ name: 'normalize-package-data', bundle: false },
21+
{ name: 'semver', bundle: false },
1622
// Utilities
1723
{ name: 'debug', bundle: true },
1824
{ name: 'del', bundle: true },
1925
{ name: 'fast-glob', bundle: true },
2026
{ name: 'fast-sort', bundle: true },
2127
{ name: 'get-east-asian-width', bundle: true },
2228
{ name: 'picomatch', bundle: true },
23-
{ name: 'semver', bundle: true },
2429
{ name: 'spdx-correct', bundle: true },
2530
{ name: 'spdx-expression-parse', bundle: true },
2631
{ name: 'streaming-iterables', bundle: true },
@@ -36,7 +41,13 @@ export const externalPackages = [
3641
export const scopedPackages = [
3742
{
3843
scope: '@npmcli',
39-
packages: ['arborist', 'package-json', 'promise-spawn'],
44+
// arborist re-exports from npm-pack bundle (no separate bundling needed)
45+
name: 'arborist',
46+
bundle: false,
47+
},
48+
{
49+
scope: '@npmcli',
50+
packages: ['package-json', 'promise-spawn'],
4051
bundle: true,
4152
subpaths: ['package-json/lib/read-package.js', 'package-json/lib/sort.js'],
4253
},

scripts/build-externals/esbuild-config.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,15 @@ function createForceNodeModulesPlugin() {
100100
name: 'force-node-modules',
101101
setup(build) {
102102
for (const pkg of packagesWithPathMappings) {
103-
build.onResolve({ filter: new RegExp(`^${pkg}$`) }, args => {
103+
// Match both bare package name and subpath imports (e.g., pkg/lib/foo.js)
104+
build.onResolve({ filter: new RegExp(`^${pkg}(/|$)`) }, args => {
104105
// Only intercept if not already in node_modules
105106
if (!args.importer.includes('node_modules')) {
106107
try {
107-
return { path: requireResolve.resolve(pkg), external: false }
108+
return {
109+
path: requireResolve.resolve(args.path),
110+
external: false,
111+
}
108112
} catch {
109113
// Package not found, let esbuild handle the error
110114
return null

scripts/build-externals/orchestrator.mjs

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Orchestrates bundling and reporting.
44
*/
55

6+
import { promises as fs } from 'node:fs'
67
import path from 'node:path'
78
import { fileURLToPath } from 'node:url'
89

@@ -26,7 +27,7 @@ async function bundleAllPackages(options = {}) {
2627
let bundledCount = 0
2728
let totalSize = 0
2829

29-
// Bundle each external package.
30+
// Bundle each external package or copy non-bundled files.
3031
for (const { bundle, name } of externalPackages) {
3132
if (bundle) {
3233
const outputPath = path.join(distExternalDir, `${name}.js`)
@@ -38,18 +39,40 @@ async function bundleAllPackages(options = {}) {
3839
bundledCount++
3940
totalSize += size
4041
}
42+
} else {
43+
// Copy non-bundled file as-is (thin re-export wrapper)
44+
const srcPath = path.join(rootDir, 'src', 'external', `${name}.js`)
45+
const destPath = path.join(distExternalDir, `${name}.js`)
46+
await fs.copyFile(srcPath, destPath)
4147
}
4248
}
4349

4450
// Bundle scoped packages.
45-
for (const { name, optional, packages, scope, subpaths } of scopedPackages) {
51+
for (const {
52+
bundle,
53+
name,
54+
optional,
55+
packages,
56+
scope,
57+
subpaths,
58+
} of scopedPackages) {
4659
const scopeDir = path.join(distExternalDir, scope)
4760
await ensureDir(scopeDir)
4861

4962
if (name) {
5063
// Single package in scope.
5164
const outputPath = path.join(scopeDir, `${name}.js`)
52-
if (optional) {
65+
if (bundle === false) {
66+
// Copy non-bundled file as-is (thin re-export wrapper)
67+
const srcPath = path.join(
68+
rootDir,
69+
'src',
70+
'external',
71+
scope,
72+
`${name}.js`,
73+
)
74+
await fs.copyFile(srcPath, outputPath)
75+
} else if (optional) {
5376
try {
5477
const size = await bundlePackage(`${scope}/${name}`, outputPath, {
5578
quiet,

src/external/@npmcli/arborist.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict'
22

3-
const Arborist = require('@npmcli/arborist')
3+
// Re-export from npm-pack bundle for better deduplication
4+
const { Arborist } = require('../npm-pack')
45
module.exports = Arborist

src/external/cacache.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
'use strict'
22

3-
module.exports = require('cacache')
3+
// Re-export from npm-pack bundle for better deduplication
4+
const { cacache } = require('./npm-pack')
5+
module.exports = cacache

src/external/libnpmpack.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
'use strict'
22

3-
module.exports = require('libnpmpack')
3+
// Re-export from npm-pack bundle for better deduplication
4+
const { libnpmpack } = require('./npm-pack')
5+
module.exports = libnpmpack

src/external/make-fetch-happen.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
'use strict'
22

3-
module.exports = require('make-fetch-happen')
3+
// Re-export from npm-pack bundle for better deduplication
4+
const { makeFetchHappen } = require('./npm-pack')
5+
module.exports = makeFetchHappen
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
'use strict'
22

3-
module.exports = require('normalize-package-data')
3+
// Re-export from npm-core bundle for better deduplication
4+
const { normalizePackageData } = require('./npm-core')
5+
module.exports = normalizePackageData

src/external/npm-core.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type npmPackageArg from 'npm-package-arg'
2+
import type normalizePackageData from 'normalize-package-data'
3+
import type * as semver from 'semver'
4+
5+
export interface NpmCore {
6+
npmPackageArg: typeof npmPackageArg
7+
normalizePackageData: typeof normalizePackageData
8+
semver: typeof semver
9+
}
10+
11+
declare const npmCore: NpmCore
12+
export = npmCore

src/external/npm-core.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
'use strict'
2+
3+
// npm-core: Bundle npm-package-arg, normalize-package-data, and semver together
4+
// These packages share dependencies and are commonly used together for package spec parsing
5+
6+
const npmPackageArg = require('npm-package-arg/lib/npa.js')
7+
const normalizePackageData = require('normalize-package-data/lib/normalize.js')
8+
const semver = require('semver')
9+
10+
module.exports = {
11+
npmPackageArg,
12+
normalizePackageData,
13+
semver,
14+
}

0 commit comments

Comments
 (0)