Skip to content

Commit 3cf08f8

Browse files
committed
perf: lazy-load heavy external imports across modules
Defer loading of heavy external sub-bundles until first use via lazy require() getters with /*@__PURE__*/ and /*@__NO_SIDE_EFFECTS__*/ annotations for tree-shaking. sorts.ts: semver (2.5 MB via npm-pack) and fastSort (8 KB) versions.ts: semver (2.5 MB via npm-pack) archives.ts: adm-zip (102 KB) and tar-fs (105 KB) globs.ts: fast-glob and picomatch (260 KB via pico-pack) fs.ts: del (260 KB via pico-pack) spawn.ts: @npmcli/promise-spawn (17 KB) strings.ts: get-east-asian-width (10 KB) Impact: consumers importing lightweight exports (isObject, httpJson, localeCompare, defaultIgnore, readJsonSync, stripAnsi, spawnSync) no longer pay for heavy externals at module init time. Total deferred: ~494 KB unique, ~2.8 MB including npm-pack chain.
1 parent ab2e353 commit 3cf08f8

File tree

7 files changed

+147
-19
lines changed

7 files changed

+147
-19
lines changed

src/archives.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,26 @@ import { pipeline } from 'node:stream/promises'
88
import { createGunzip } from 'node:zlib'
99
import process from 'node:process'
1010

11-
import AdmZip from './external/adm-zip.js'
12-
import tarFs from './external/tar-fs.js'
11+
import type AdmZipType from './external/adm-zip.js'
12+
import type tarFsType from './external/tar-fs.js'
13+
14+
let _AdmZip: typeof AdmZipType | undefined
15+
/*@__NO_SIDE_EFFECTS__*/
16+
function getAdmZip() {
17+
if (_AdmZip === undefined) {
18+
_AdmZip = /*@__PURE__*/ require('./external/adm-zip.js')
19+
}
20+
return _AdmZip!
21+
}
22+
23+
let _tarFs: typeof tarFsType | undefined
24+
/*@__NO_SIDE_EFFECTS__*/
25+
function getTarFs() {
26+
if (_tarFs === undefined) {
27+
_tarFs = /*@__PURE__*/ require('./external/tar-fs.js')
28+
}
29+
return _tarFs!
30+
}
1331

1432
import { safeMkdir } from './fs.js'
1533
import { normalizePath } from './paths/normalize.js'
@@ -142,6 +160,7 @@ export async function extractTar(
142160

143161
let destroyScheduled = false
144162

163+
const tarFs = getTarFs()
145164
const extractStream = tarFs.extract(normalizedOutputDir, {
146165
map: (header: { name: string; size?: number; type?: string }) => {
147166
// Skip if destroy already scheduled
@@ -267,6 +286,7 @@ export async function extractTarGz(
267286

268287
let destroyScheduled = false
269288

289+
const tarFs = getTarFs()
270290
const extractStream = tarFs.extract(normalizedOutputDir, {
271291
map: (header: { name: string; size?: number; type?: string }) => {
272292
// Skip if destroy already scheduled
@@ -387,6 +407,7 @@ export async function extractZip(
387407
const normalizedOutputDir = normalizePath(outputDir)
388408
await safeMkdir(normalizedOutputDir)
389409

410+
const AdmZip = getAdmZip()
390411
const zip = new AdmZip(archivePath)
391412
const path = getPath()
392413

src/fs.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,21 @@ import type {
1919
import { getAbortSignal } from './constants/process'
2020

2121
import { isArray } from './arrays'
22-
import { deleteAsync, deleteSync } from './external/del'
22+
import type {
23+
deleteAsync as deleteAsyncType,
24+
deleteSync as deleteSyncType,
25+
} from './external/del'
26+
27+
let _del:
28+
| { deleteAsync: typeof deleteAsyncType; deleteSync: typeof deleteSyncType }
29+
| undefined
30+
/*@__NO_SIDE_EFFECTS__*/
31+
function getDel() {
32+
if (_del === undefined) {
33+
_del = /*@__PURE__*/ require('./external/del')
34+
}
35+
return _del!
36+
}
2337
import { pRetry } from './promises'
2438
import { defaultIgnore, getGlobMatcher } from './globs'
2539
import type { JsonReviver } from './json/types'
@@ -1235,7 +1249,7 @@ export async function safeDelete(
12351249
filepath: PathLike | PathLike[],
12361250
options?: RemoveOptions | undefined,
12371251
) {
1238-
// deleteAsync is imported at the top
1252+
// deleteAsync is lazily loaded via getDel()
12391253
const opts = { __proto__: null, ...options } as RemoveOptions
12401254
const patterns = isArray(filepath)
12411255
? filepath.map(pathLikeToString)
@@ -1276,9 +1290,10 @@ export async function safeDelete(
12761290
const retryDelay = opts.retryDelay ?? defaultRemoveOptions.retryDelay
12771291

12781292
/* c8 ignore start - External del call */
1293+
const del = getDel()
12791294
await pRetry(
12801295
async () => {
1281-
await deleteAsync(patterns, {
1296+
await del.deleteAsync(patterns, {
12821297
dryRun: false,
12831298
force: shouldForce,
12841299
onlyFiles: false,
@@ -1328,7 +1343,7 @@ export function safeDeleteSync(
13281343
filepath: PathLike | PathLike[],
13291344
options?: RemoveOptions | undefined,
13301345
) {
1331-
// deleteSync is imported at the top
1346+
// deleteSync is lazily loaded via getDel()
13321347
const opts = { __proto__: null, ...options } as RemoveOptions
13331348
const patterns = isArray(filepath)
13341349
? filepath.map(pathLikeToString)
@@ -1369,11 +1384,12 @@ export function safeDeleteSync(
13691384
const retryDelay = opts.retryDelay ?? defaultRemoveOptions.retryDelay
13701385

13711386
/* c8 ignore start - External del call */
1387+
const del = getDel()
13721388
let lastError: Error | undefined
13731389
let delay = retryDelay
13741390
for (let attempt = 0; attempt <= maxRetries; attempt++) {
13751391
try {
1376-
deleteSync(patterns, {
1392+
del.deleteSync(patterns, {
13771393
dryRun: false,
13781394
force: shouldForce,
13791395
onlyFiles: false,

src/globs.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@
33
* Provides file filtering and glob matcher functions for npm-like behavior.
44
*/
55

6-
// IMPORTANT: Do not use destructuring here - use direct assignment instead.
7-
import * as fastGlob from './external/fast-glob.js'
8-
import picomatch from './external/picomatch.js'
6+
import type * as fastGlobType from './external/fast-glob.js'
7+
import type picomatchType from './external/picomatch.js'
8+
9+
let _fastGlob: typeof fastGlobType | undefined
10+
/*@__NO_SIDE_EFFECTS__*/
11+
function getFastGlob() {
12+
if (_fastGlob === undefined) {
13+
_fastGlob = /*@__PURE__*/ require('./external/fast-glob.js')
14+
}
15+
return _fastGlob!
16+
}
17+
18+
let _picomatch: typeof picomatchType | undefined
19+
/*@__NO_SIDE_EFFECTS__*/
20+
function getPicomatch() {
21+
if (_picomatch === undefined) {
22+
_picomatch = /*@__PURE__*/ require('./external/picomatch.js')
23+
}
24+
return _picomatch!
25+
}
926

1027
import { objectFreeze as ObjectFreeze } from './objects'
1128
import {
@@ -112,6 +129,7 @@ export function globStreamLicenses(
112129
ignore.push(LICENSE_ORIGINAL_GLOB_RECURSIVE)
113130
}
114131
/* c8 ignore start - External fast-glob call */
132+
const fastGlob = getFastGlob()
115133
return fastGlob.globStream(
116134
[recursive ? LICENSE_GLOB_RECURSIVE : LICENSE_GLOB],
117135
{
@@ -188,7 +206,8 @@ export function getGlobMatcher(
188206
...(negativePatterns.length > 0 ? { ignore: negativePatterns } : {}),
189207
}
190208

191-
/* c8 ignore next 4 - External picomatch call */
209+
/* c8 ignore next 5 - External picomatch call */
210+
const picomatch = getPicomatch()
192211
matcher = picomatch(
193212
positivePatterns.length > 0 ? positivePatterns : patterns,
194213
matchOptions,
@@ -209,6 +228,7 @@ export function glob(
209228
options?: FastGlobOptions,
210229
): Promise<string[]> {
211230
/* c8 ignore next - External fast-glob call */
231+
const fastGlob = getFastGlob()
212232
return fastGlob.glob(patterns, options as import('fast-glob').Options)
213233
}
214234

@@ -222,5 +242,6 @@ export function globSync(
222242
options?: FastGlobOptions,
223243
): string[] {
224244
/* c8 ignore next - External fast-glob call */
245+
const fastGlob = getFastGlob()
225246
return fastGlob.globSync(patterns, options as import('fast-glob').Options)
226247
}

src/sorts.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,32 @@
33
* Provides various comparison utilities for arrays and collections.
44
*/
55

6-
import * as fastSort from './external/fast-sort.js'
7-
import * as semver from './external/semver.js'
6+
import type * as fastSortType from './external/fast-sort.js'
7+
import type * as semverType from './external/semver.js'
8+
9+
let _semver: typeof semverType | undefined
10+
function getSemver() {
11+
if (_semver === undefined) {
12+
_semver = require('./external/semver.js')
13+
}
14+
return _semver!
15+
}
16+
17+
let _fastSort: typeof fastSortType | undefined
18+
function getFastSort() {
19+
if (_fastSort === undefined) {
20+
_fastSort = require('./external/fast-sort.js')
21+
}
22+
return _fastSort!
23+
}
824

925
/**
1026
* Compare semantic versions.
1127
*/
1228
/*@__NO_SIDE_EFFECTS__*/
1329
export function compareSemver(a: string, b: string): number {
1430
/* c8 ignore next 2 - External semver calls */
31+
const semver = getSemver()
1532
const validA: string | null = semver.valid(a)
1633
const validB: string | null = semver.valid(b)
1734

@@ -89,7 +106,8 @@ export function naturalSorter<T>(
89106
arrayToSort: T[],
90107
): ReturnType<FastSortFunction> {
91108
if (_naturalSorter === undefined) {
92-
/* c8 ignore next 3 - External fast-sort call */
109+
/* c8 ignore next 4 - External fast-sort call */
110+
const fastSort = getFastSort()
93111
_naturalSorter = fastSort.createNewSortInstance({
94112
comparer: naturalCompare,
95113
}) as FastSortFunction

src/spawn.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,16 @@ import process from 'node:process'
3030
import { getAbortSignal } from './constants/process'
3131
import { stackWithCauses } from './errors'
3232

33-
import npmCliPromiseSpawn from './external/@npmcli/promise-spawn'
33+
import type npmCliPromiseSpawnType from './external/@npmcli/promise-spawn'
34+
35+
let _npmCliPromiseSpawn: typeof npmCliPromiseSpawnType | undefined
36+
/*@__NO_SIDE_EFFECTS__*/
37+
function getNpmCliPromiseSpawn() {
38+
if (_npmCliPromiseSpawn === undefined) {
39+
_npmCliPromiseSpawn = /*@__PURE__*/ require('./external/@npmcli/promise-spawn')
40+
}
41+
return _npmCliPromiseSpawn!
42+
}
3443

3544
let _path: typeof import('node:path') | undefined
3645
/**
@@ -738,7 +747,7 @@ export function spawn(
738747
if (shouldStopSpinner) {
739748
spinnerInstance.stop()
740749
}
741-
// npmCliPromiseSpawn is imported at the top
750+
// npmCliPromiseSpawn is lazily loaded via getNpmCliPromiseSpawn()
742751
// Use __proto__: null to prevent prototype pollution when passing to
743752
// third-party code, Node.js built-ins, or JavaScript built-in methods.
744753
// https://github.com/npm/promise-spawn
@@ -769,10 +778,11 @@ export function spawn(
769778
gid: spawnOptions.gid,
770779
} as unknown as PromiseSpawnOptions
771780
/* c8 ignore start - External npmCliPromiseSpawn call */
781+
const npmCliPromiseSpawn = getNpmCliPromiseSpawn()
772782
const spawnPromise = npmCliPromiseSpawn(
773783
actualCmd,
774784
args ? [...args] : [],
775-
promiseSpawnOpts as Parameters<typeof npmCliPromiseSpawn>[2],
785+
promiseSpawnOpts as Parameters<typeof npmCliPromiseSpawnType>[2],
776786
extra,
777787
)
778788
/* c8 ignore stop */

src/strings.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@
44
*/
55

66
import { ansiRegex, stripAnsi } from './ansi'
7-
import { eastAsianWidth } from './external/get-east-asian-width'
7+
import type { eastAsianWidth as eastAsianWidthType } from './external/get-east-asian-width'
8+
9+
let _eastAsianWidth: typeof eastAsianWidthType | undefined
10+
/*@__NO_SIDE_EFFECTS__*/
11+
function getEastAsianWidth() {
12+
if (_eastAsianWidth === undefined) {
13+
_eastAsianWidth = /*@__PURE__*/ (
14+
require('./external/get-east-asian-width') as {
15+
eastAsianWidth: typeof eastAsianWidthType
16+
}
17+
).eastAsianWidth
18+
}
19+
return _eastAsianWidth!
20+
}
821
// Import get-east-asian-width from external wrapper.
922
// This library implements Unicode Standard Annex #11 (East Asian Width).
1023
// https://www.unicode.org/reports/tr11/
@@ -668,6 +681,7 @@ export function stringWidth(text: string): number {
668681
// - Most terminal emulators default to narrow for ambiguous characters
669682
// - Consistent with string-width's default behavior
670683
const eastAsianWidthOptions = { ambiguousAsWide: false }
684+
const eastAsianWidth = getEastAsianWidth()
671685

672686
// KEY IMPROVEMENT #3: Comprehensive Width Calculation
673687
//

0 commit comments

Comments
 (0)