From 3cf08f8437cfbd5c92010f2c7aa76cde05306d42 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 28 Mar 2026 17:34:38 -0400 Subject: [PATCH] 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. --- src/archives.ts | 25 +++++++++++++++++++++++-- src/fs.ts | 26 +++++++++++++++++++++----- src/globs.ts | 29 +++++++++++++++++++++++++---- src/sorts.ts | 24 +++++++++++++++++++++--- src/spawn.ts | 16 +++++++++++++--- src/strings.ts | 16 +++++++++++++++- src/versions.ts | 30 +++++++++++++++++++++++++++++- 7 files changed, 147 insertions(+), 19 deletions(-) diff --git a/src/archives.ts b/src/archives.ts index be29860..11dffae 100644 --- a/src/archives.ts +++ b/src/archives.ts @@ -8,8 +8,26 @@ import { pipeline } from 'node:stream/promises' import { createGunzip } from 'node:zlib' import process from 'node:process' -import AdmZip from './external/adm-zip.js' -import tarFs from './external/tar-fs.js' +import type AdmZipType from './external/adm-zip.js' +import type tarFsType from './external/tar-fs.js' + +let _AdmZip: typeof AdmZipType | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getAdmZip() { + if (_AdmZip === undefined) { + _AdmZip = /*@__PURE__*/ require('./external/adm-zip.js') + } + return _AdmZip! +} + +let _tarFs: typeof tarFsType | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getTarFs() { + if (_tarFs === undefined) { + _tarFs = /*@__PURE__*/ require('./external/tar-fs.js') + } + return _tarFs! +} import { safeMkdir } from './fs.js' import { normalizePath } from './paths/normalize.js' @@ -142,6 +160,7 @@ export async function extractTar( let destroyScheduled = false + const tarFs = getTarFs() const extractStream = tarFs.extract(normalizedOutputDir, { map: (header: { name: string; size?: number; type?: string }) => { // Skip if destroy already scheduled @@ -267,6 +286,7 @@ export async function extractTarGz( let destroyScheduled = false + const tarFs = getTarFs() const extractStream = tarFs.extract(normalizedOutputDir, { map: (header: { name: string; size?: number; type?: string }) => { // Skip if destroy already scheduled @@ -387,6 +407,7 @@ export async function extractZip( const normalizedOutputDir = normalizePath(outputDir) await safeMkdir(normalizedOutputDir) + const AdmZip = getAdmZip() const zip = new AdmZip(archivePath) const path = getPath() diff --git a/src/fs.ts b/src/fs.ts index 33d6ea0..98f2281 100644 --- a/src/fs.ts +++ b/src/fs.ts @@ -19,7 +19,21 @@ import type { import { getAbortSignal } from './constants/process' import { isArray } from './arrays' -import { deleteAsync, deleteSync } from './external/del' +import type { + deleteAsync as deleteAsyncType, + deleteSync as deleteSyncType, +} from './external/del' + +let _del: + | { deleteAsync: typeof deleteAsyncType; deleteSync: typeof deleteSyncType } + | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getDel() { + if (_del === undefined) { + _del = /*@__PURE__*/ require('./external/del') + } + return _del! +} import { pRetry } from './promises' import { defaultIgnore, getGlobMatcher } from './globs' import type { JsonReviver } from './json/types' @@ -1235,7 +1249,7 @@ export async function safeDelete( filepath: PathLike | PathLike[], options?: RemoveOptions | undefined, ) { - // deleteAsync is imported at the top + // deleteAsync is lazily loaded via getDel() const opts = { __proto__: null, ...options } as RemoveOptions const patterns = isArray(filepath) ? filepath.map(pathLikeToString) @@ -1276,9 +1290,10 @@ export async function safeDelete( const retryDelay = opts.retryDelay ?? defaultRemoveOptions.retryDelay /* c8 ignore start - External del call */ + const del = getDel() await pRetry( async () => { - await deleteAsync(patterns, { + await del.deleteAsync(patterns, { dryRun: false, force: shouldForce, onlyFiles: false, @@ -1328,7 +1343,7 @@ export function safeDeleteSync( filepath: PathLike | PathLike[], options?: RemoveOptions | undefined, ) { - // deleteSync is imported at the top + // deleteSync is lazily loaded via getDel() const opts = { __proto__: null, ...options } as RemoveOptions const patterns = isArray(filepath) ? filepath.map(pathLikeToString) @@ -1369,11 +1384,12 @@ export function safeDeleteSync( const retryDelay = opts.retryDelay ?? defaultRemoveOptions.retryDelay /* c8 ignore start - External del call */ + const del = getDel() let lastError: Error | undefined let delay = retryDelay for (let attempt = 0; attempt <= maxRetries; attempt++) { try { - deleteSync(patterns, { + del.deleteSync(patterns, { dryRun: false, force: shouldForce, onlyFiles: false, diff --git a/src/globs.ts b/src/globs.ts index 120880d..8e77765 100644 --- a/src/globs.ts +++ b/src/globs.ts @@ -3,9 +3,26 @@ * Provides file filtering and glob matcher functions for npm-like behavior. */ -// IMPORTANT: Do not use destructuring here - use direct assignment instead. -import * as fastGlob from './external/fast-glob.js' -import picomatch from './external/picomatch.js' +import type * as fastGlobType from './external/fast-glob.js' +import type picomatchType from './external/picomatch.js' + +let _fastGlob: typeof fastGlobType | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getFastGlob() { + if (_fastGlob === undefined) { + _fastGlob = /*@__PURE__*/ require('./external/fast-glob.js') + } + return _fastGlob! +} + +let _picomatch: typeof picomatchType | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getPicomatch() { + if (_picomatch === undefined) { + _picomatch = /*@__PURE__*/ require('./external/picomatch.js') + } + return _picomatch! +} import { objectFreeze as ObjectFreeze } from './objects' import { @@ -112,6 +129,7 @@ export function globStreamLicenses( ignore.push(LICENSE_ORIGINAL_GLOB_RECURSIVE) } /* c8 ignore start - External fast-glob call */ + const fastGlob = getFastGlob() return fastGlob.globStream( [recursive ? LICENSE_GLOB_RECURSIVE : LICENSE_GLOB], { @@ -188,7 +206,8 @@ export function getGlobMatcher( ...(negativePatterns.length > 0 ? { ignore: negativePatterns } : {}), } - /* c8 ignore next 4 - External picomatch call */ + /* c8 ignore next 5 - External picomatch call */ + const picomatch = getPicomatch() matcher = picomatch( positivePatterns.length > 0 ? positivePatterns : patterns, matchOptions, @@ -209,6 +228,7 @@ export function glob( options?: FastGlobOptions, ): Promise { /* c8 ignore next - External fast-glob call */ + const fastGlob = getFastGlob() return fastGlob.glob(patterns, options as import('fast-glob').Options) } @@ -222,5 +242,6 @@ export function globSync( options?: FastGlobOptions, ): string[] { /* c8 ignore next - External fast-glob call */ + const fastGlob = getFastGlob() return fastGlob.globSync(patterns, options as import('fast-glob').Options) } diff --git a/src/sorts.ts b/src/sorts.ts index 20f3c19..7c19096 100644 --- a/src/sorts.ts +++ b/src/sorts.ts @@ -3,8 +3,24 @@ * Provides various comparison utilities for arrays and collections. */ -import * as fastSort from './external/fast-sort.js' -import * as semver from './external/semver.js' +import type * as fastSortType from './external/fast-sort.js' +import type * as semverType from './external/semver.js' + +let _semver: typeof semverType | undefined +function getSemver() { + if (_semver === undefined) { + _semver = require('./external/semver.js') + } + return _semver! +} + +let _fastSort: typeof fastSortType | undefined +function getFastSort() { + if (_fastSort === undefined) { + _fastSort = require('./external/fast-sort.js') + } + return _fastSort! +} /** * Compare semantic versions. @@ -12,6 +28,7 @@ import * as semver from './external/semver.js' /*@__NO_SIDE_EFFECTS__*/ export function compareSemver(a: string, b: string): number { /* c8 ignore next 2 - External semver calls */ + const semver = getSemver() const validA: string | null = semver.valid(a) const validB: string | null = semver.valid(b) @@ -89,7 +106,8 @@ export function naturalSorter( arrayToSort: T[], ): ReturnType { if (_naturalSorter === undefined) { - /* c8 ignore next 3 - External fast-sort call */ + /* c8 ignore next 4 - External fast-sort call */ + const fastSort = getFastSort() _naturalSorter = fastSort.createNewSortInstance({ comparer: naturalCompare, }) as FastSortFunction diff --git a/src/spawn.ts b/src/spawn.ts index 681b6fe..5903919 100644 --- a/src/spawn.ts +++ b/src/spawn.ts @@ -30,7 +30,16 @@ import process from 'node:process' import { getAbortSignal } from './constants/process' import { stackWithCauses } from './errors' -import npmCliPromiseSpawn from './external/@npmcli/promise-spawn' +import type npmCliPromiseSpawnType from './external/@npmcli/promise-spawn' + +let _npmCliPromiseSpawn: typeof npmCliPromiseSpawnType | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getNpmCliPromiseSpawn() { + if (_npmCliPromiseSpawn === undefined) { + _npmCliPromiseSpawn = /*@__PURE__*/ require('./external/@npmcli/promise-spawn') + } + return _npmCliPromiseSpawn! +} let _path: typeof import('node:path') | undefined /** @@ -738,7 +747,7 @@ export function spawn( if (shouldStopSpinner) { spinnerInstance.stop() } - // npmCliPromiseSpawn is imported at the top + // npmCliPromiseSpawn is lazily loaded via getNpmCliPromiseSpawn() // Use __proto__: null to prevent prototype pollution when passing to // third-party code, Node.js built-ins, or JavaScript built-in methods. // https://github.com/npm/promise-spawn @@ -769,10 +778,11 @@ export function spawn( gid: spawnOptions.gid, } as unknown as PromiseSpawnOptions /* c8 ignore start - External npmCliPromiseSpawn call */ + const npmCliPromiseSpawn = getNpmCliPromiseSpawn() const spawnPromise = npmCliPromiseSpawn( actualCmd, args ? [...args] : [], - promiseSpawnOpts as Parameters[2], + promiseSpawnOpts as Parameters[2], extra, ) /* c8 ignore stop */ diff --git a/src/strings.ts b/src/strings.ts index b6206a7..13390fe 100644 --- a/src/strings.ts +++ b/src/strings.ts @@ -4,7 +4,20 @@ */ import { ansiRegex, stripAnsi } from './ansi' -import { eastAsianWidth } from './external/get-east-asian-width' +import type { eastAsianWidth as eastAsianWidthType } from './external/get-east-asian-width' + +let _eastAsianWidth: typeof eastAsianWidthType | undefined +/*@__NO_SIDE_EFFECTS__*/ +function getEastAsianWidth() { + if (_eastAsianWidth === undefined) { + _eastAsianWidth = /*@__PURE__*/ ( + require('./external/get-east-asian-width') as { + eastAsianWidth: typeof eastAsianWidthType + } + ).eastAsianWidth + } + return _eastAsianWidth! +} // Import get-east-asian-width from external wrapper. // This library implements Unicode Standard Annex #11 (East Asian Width). // https://www.unicode.org/reports/tr11/ @@ -668,6 +681,7 @@ export function stringWidth(text: string): number { // - Most terminal emulators default to narrow for ambiguous characters // - Consistent with string-width's default behavior const eastAsianWidthOptions = { ambiguousAsWide: false } + const eastAsianWidth = getEastAsianWidth() // KEY IMPROVEMENT #3: Comprehensive Width Calculation // diff --git a/src/versions.ts b/src/versions.ts index 3c30c85..e90bced 100644 --- a/src/versions.ts +++ b/src/versions.ts @@ -1,12 +1,21 @@ /** @fileoverview Version comparison and validation utilities for Socket ecosystem. */ -import * as semver from './external/semver.js' +import type * as semverType from './external/semver.js' + +let _semver: typeof semverType | undefined +function getSemver() { + if (_semver === undefined) { + _semver = require('./external/semver.js') + } + return _semver! +} /** * Coerce a version string to valid semver format. */ export function coerceVersion(version: string): string | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() const coerced = semver.coerce(version) return coerced?.version } @@ -21,6 +30,7 @@ export function compareVersions( ): -1 | 0 | 1 | undefined { try { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.compare(v1, v2) } catch { return undefined @@ -32,6 +42,7 @@ export function compareVersions( */ export function filterVersions(versions: string[], range: string): string[] { /* c8 ignore next - External semver call */ + const semver = getSemver() return versions.filter(v => semver.satisfies(v, range)) } @@ -40,6 +51,7 @@ export function filterVersions(versions: string[], range: string): string[] { */ export function getMajorVersion(version: string): number | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() const parsed = semver.parse(version) return parsed?.major } @@ -49,6 +61,7 @@ export function getMajorVersion(version: string): number | undefined { */ export function getMinorVersion(version: string): number | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() const parsed = semver.parse(version) return parsed?.minor } @@ -58,6 +71,7 @@ export function getMinorVersion(version: string): number | undefined { */ export function getPatchVersion(version: string): number | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() const parsed = semver.parse(version) return parsed?.patch } @@ -78,6 +92,7 @@ export function incrementVersion( identifier?: string | undefined, ): string | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.inc(version, release, identifier) || undefined } @@ -86,6 +101,7 @@ export function incrementVersion( */ export function isEqual(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.eq(version1, version2) } @@ -94,6 +110,7 @@ export function isEqual(version1: string, version2: string): boolean { */ export function isGreaterThan(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.gt(version1, version2) } @@ -105,6 +122,7 @@ export function isGreaterThanOrEqual( version2: string, ): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.gte(version1, version2) } @@ -113,6 +131,7 @@ export function isGreaterThanOrEqual( */ export function isLessThan(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.lt(version1, version2) } @@ -121,6 +140,7 @@ export function isLessThan(version1: string, version2: string): boolean { */ export function isLessThanOrEqual(version1: string, version2: string): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.lte(version1, version2) } @@ -129,6 +149,7 @@ export function isLessThanOrEqual(version1: string, version2: string): boolean { */ export function isValidVersion(version: string): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.valid(version) !== null } @@ -137,6 +158,7 @@ export function isValidVersion(version: string): boolean { */ export function maxVersion(versions: string[]): string | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.maxSatisfying(versions, '*') || undefined } @@ -145,6 +167,7 @@ export function maxVersion(versions: string[]): string | undefined { */ export function minVersion(versions: string[]): string | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.minSatisfying(versions, '*') || undefined } @@ -161,6 +184,7 @@ export function parseVersion(version: string): } | undefined { /* c8 ignore next - External semver call */ + const semver = getSemver() const parsed = semver.parse(version) if (!parsed) { return undefined @@ -179,6 +203,7 @@ export function parseVersion(version: string): */ export function satisfiesVersion(version: string, range: string): boolean { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.satisfies(version, range) } @@ -187,6 +212,7 @@ export function satisfiesVersion(version: string, range: string): boolean { */ export function sortVersions(versions: string[]): string[] { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.sort([...versions]) } @@ -195,6 +221,7 @@ export function sortVersions(versions: string[]): string[] { */ export function sortVersionsDesc(versions: string[]): string[] { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.rsort([...versions]) } @@ -216,6 +243,7 @@ export function versionDiff( | undefined { try { /* c8 ignore next - External semver call */ + const semver = getSemver() return semver.diff(version1, version2) || undefined } catch { return undefined