Skip to content

Commit ef6cdf3

Browse files
committed
refactor: convert Node.js module imports to lazy-loading pattern to avoid Webpack bundling issues
Replace direct imports of Node.js built-in modules (fs, path, os, crypto, child_process, readline, async_hooks) with lazy-loading helper functions that use non-'node:' prefixed require() calls. This prevents Webpack from attempting to bundle Node.js built-ins when this library is used in browser contexts. Pattern used: - Add private getter functions (getFs(), getPath(), etc.) - Use non-'node:' prefixed require() to avoid Webpack errors - Add /*@__NO_SIDE_EFFECTS__*/ and /*@__PURE__*/ annotations for tree-shaking - Update all usage sites to call helpers instead of direct module references Files converted: - src/cover/code.ts (fs, path) - src/dlx/cache.ts (crypto) - src/dlx/manifest.ts (fs, path) - src/env/rewire.ts (async_hooks) - src/http-request.ts (fs - createWriteStream) - src/ipc.ts (crypto, fs, path) - src/packages/isolation.ts (fs, path) - src/packages/licenses.ts (path) - src/paths/socket.ts (os, path) - src/process-lock.ts (fs) - src/releases/github.ts (fs, path) - src/releases/socket-btm.ts (fs) - src/spawn.ts (path) - src/stdio/mask.ts (child_process, readline) - src/themes/context.ts (async_hooks) Note: src/stdio/stdout.ts was not converted as it only imports the WriteStream type, which doesn't require runtime code.
1 parent 9d20102 commit ef6cdf3

13 files changed

Lines changed: 338 additions & 118 deletions

File tree

src/cover/code.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
* @fileoverview Code coverage utilities for parsing v8 coverage data.
33
*/
44

5-
import { promises as fs } from 'fs'
6-
75
import { readJson } from '../fs'
86
import { isObjectObject } from '../objects'
97
import { spawn } from '../spawn'
@@ -16,7 +14,21 @@ import type {
1614
V8FileCoverage,
1715
} from './types'
1816

17+
let _fs: typeof import('node:fs') | undefined
1918
let _path: typeof import('node:path') | undefined
19+
/**
20+
* Lazily load the fs module to avoid Webpack errors.
21+
* @private
22+
*/
23+
/*@__NO_SIDE_EFFECTS__*/
24+
function getFs() {
25+
if (_fs === undefined) {
26+
// Use non-'node:' prefixed require to avoid Webpack errors.
27+
28+
_fs = /*@__PURE__*/ require('fs')
29+
}
30+
return _fs as typeof import('node:fs')
31+
}
2032
/**
2133
* Lazily load the path module to avoid Webpack errors.
2234
* @private
@@ -30,7 +42,6 @@ function getPath() {
3042
}
3143
return _path as typeof import('node:path')
3244
}
33-
3445
/**
3546
* Get code coverage metrics from v8 coverage-final.json.
3647
*
@@ -55,7 +66,8 @@ export async function getCodeCoverage(
5566
}
5667

5768
// Check if coverage file exists.
58-
const coverageExists = await fs
69+
const fs = getFs()
70+
const coverageExists = await fs.promises
5971
.access(coveragePath)
6072
.then(() => true)
6173
.catch(() => false)
@@ -157,7 +169,6 @@ export async function getCodeCoverage(
157169
statements: calculateMetric(totals.statements),
158170
}
159171
}
160-
161172
/**
162173
* Calculate coverage metric with percentage.
163174
*/

src/dlx/cache.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
/** @fileoverview Cache key generation utilities for DLX package installations. */
22

3-
import { createHash } from 'node:crypto'
3+
let _crypto: typeof import('node:crypto') | undefined
4+
/**
5+
* Lazily load the crypto module to avoid Webpack errors.
6+
* @private
7+
*/
8+
/*@__NO_SIDE_EFFECTS__*/
9+
function getCrypto() {
10+
if (_crypto === undefined) {
11+
// Use non-'node:' prefixed require to avoid Webpack errors.
12+
13+
_crypto = /*@__PURE__*/ require('crypto')
14+
}
15+
return _crypto as typeof import('node:crypto')
16+
}
417

518
/**
619
* Generate a cache directory name using npm/npx approach.
@@ -27,5 +40,6 @@ import { createHash } from 'node:crypto'
2740
* npx hashes the package spec (name@version), not just name
2841
*/
2942
export function generateCacheKey(spec: string): string {
30-
return createHash('sha512').update(spec).digest('hex').substring(0, 16)
43+
const crypto = getCrypto()
44+
return crypto.createHash('sha512').update(spec).digest('hex').substring(0, 16)
3145
}

src/dlx/manifest.ts

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,30 @@
2525
* - Rate limiting registry requests
2626
*/
2727

28-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'
29-
import path from 'path'
30-
28+
let _fs: typeof import('node:fs') | undefined
29+
let _path: typeof import('node:path') | undefined
30+
/**
31+
* Lazily load the fs module to avoid Webpack errors.
32+
* @private
33+
*/
34+
/*@__NO_SIDE_EFFECTS__*/
35+
function getFs() {
36+
if (_fs === undefined) {
37+
_fs = /*@__PURE__*/ require('fs')
38+
}
39+
return _fs as typeof import('node:fs')
40+
}
41+
/**
42+
* Lazily load the path module to avoid Webpack errors.
43+
* @private
44+
*/
45+
/*@__NO_SIDE_EFFECTS__*/
46+
function getPath() {
47+
if (_path === undefined) {
48+
_path = /*@__PURE__*/ require('path')
49+
}
50+
return _path as typeof import('node:path')
51+
}
3152
import { readFileUtf8Sync, safeMkdirSync } from '../fs'
3253
import { getDefaultLogger } from '../logger'
3354
import { getSocketDlxDir } from '../paths/socket'
@@ -52,7 +73,6 @@ export interface PackageDetails {
5273
latest_known: string
5374
}
5475
}
55-
5676
/**
5777
* Details for binary download entries.
5878
*/
@@ -67,7 +87,6 @@ export interface BinaryDetails {
6787
url: string
6888
}
6989
}
70-
7190
export type ChecksumAlgorithm = 'sha256' | 'sha512'
7291

7392
/**
@@ -80,7 +99,6 @@ export interface ManifestEntry {
8099
timestamp: number
81100
details: PackageDetails | BinaryDetails
82101
}
83-
84102
/**
85103
* Type guard for package entries.
86104
*/
@@ -89,7 +107,6 @@ export function isPackageEntry(
89107
): entry is ManifestEntry & { details: PackageDetails } {
90108
return entry.type === 'package'
91109
}
92-
93110
/**
94111
* Type guard for binary entries.
95112
*/
@@ -98,7 +115,6 @@ export function isBinaryEntry(
98115
): entry is ManifestEntry & { details: BinaryDetails } {
99116
return entry.type === 'binary'
100117
}
101-
102118
/**
103119
* Legacy store record format (deprecated, for migration).
104120
*/
@@ -107,14 +123,12 @@ export interface StoreRecord {
107123
timestampNotification: number
108124
version: string
109125
}
110-
111126
export interface DlxManifestOptions {
112127
/**
113128
* Custom manifest file path (defaults to ~/.socket/_dlx/.dlx-manifest.json).
114129
*/
115130
manifestPath?: string
116131
}
117-
118132
/**
119133
* DLX manifest storage manager with atomic operations.
120134
* Supports both legacy format (package name keys) and new unified manifest format (spec keys).
@@ -125,7 +139,8 @@ export class DlxManifest {
125139

126140
constructor(options: DlxManifestOptions = {}) {
127141
this.manifestPath =
128-
options.manifestPath ?? path.join(getSocketDlxDir(), MANIFEST_FILE_NAME)
142+
options.manifestPath ??
143+
getPath().join(getSocketDlxDir(), MANIFEST_FILE_NAME)
129144
this.lockPath = `${this.manifestPath}.lock`
130145
}
131146

@@ -134,7 +149,7 @@ export class DlxManifest {
134149
*/
135150
private readManifest(): Record<string, ManifestEntry | StoreRecord> {
136151
try {
137-
if (!existsSync(this.manifestPath)) {
152+
if (!getFs().existsSync(this.manifestPath)) {
138153
return Object.create(null)
139154
}
140155

@@ -240,7 +255,7 @@ export class DlxManifest {
240255
data: Record<string, ManifestEntry | StoreRecord>,
241256
): Promise<void> {
242257
// Ensure directory exists.
243-
const manifestDir = path.dirname(this.manifestPath)
258+
const manifestDir = getPath().dirname(this.manifestPath)
244259
try {
245260
safeMkdirSync(manifestDir, { recursive: true })
246261
} catch (error) {
@@ -254,22 +269,22 @@ export class DlxManifest {
254269
const tempPath = `${this.manifestPath}.tmp`
255270

256271
try {
257-
writeFileSync(tempPath, content, 'utf8')
258-
writeFileSync(this.manifestPath, content, 'utf8')
272+
getFs().writeFileSync(tempPath, content, 'utf8')
273+
getFs().writeFileSync(this.manifestPath, content, 'utf8')
259274

260275
// Clean up temp file.
261276
try {
262-
if (existsSync(tempPath)) {
263-
unlinkSync(tempPath)
277+
if (getFs().existsSync(tempPath)) {
278+
getFs().unlinkSync(tempPath)
264279
}
265280
} catch {
266281
// Cleanup failed, not critical.
267282
}
268283
} catch (error) {
269284
// Clean up temp file on error.
270285
try {
271-
if (existsSync(tempPath)) {
272-
unlinkSync(tempPath)
286+
if (getFs().existsSync(tempPath)) {
287+
getFs().unlinkSync(tempPath)
273288
}
274289
} catch {
275290
// Best effort cleanup.
@@ -288,8 +303,8 @@ export class DlxManifest {
288303

289304
// Read existing data.
290305
try {
291-
if (existsSync(this.manifestPath)) {
292-
const content = readFileSync(this.manifestPath, 'utf8')
306+
if (getFs().existsSync(this.manifestPath)) {
307+
const content = getFs().readFileSync(this.manifestPath, 'utf8')
293308
if (content.trim()) {
294309
data = JSON.parse(content) as Record<string, StoreRecord>
295310
}
@@ -304,7 +319,7 @@ export class DlxManifest {
304319
data[name] = record
305320

306321
// Ensure directory exists.
307-
const manifestDir = path.dirname(this.manifestPath)
322+
const manifestDir = getPath().dirname(this.manifestPath)
308323
try {
309324
safeMkdirSync(manifestDir, { recursive: true })
310325
} catch (error) {
@@ -318,22 +333,22 @@ export class DlxManifest {
318333
const tempPath = `${this.manifestPath}.tmp`
319334

320335
try {
321-
writeFileSync(tempPath, content, 'utf8')
322-
writeFileSync(this.manifestPath, content, 'utf8')
336+
getFs().writeFileSync(tempPath, content, 'utf8')
337+
getFs().writeFileSync(this.manifestPath, content, 'utf8')
323338

324339
// Clean up temp file.
325340
try {
326-
if (existsSync(tempPath)) {
327-
unlinkSync(tempPath)
341+
if (getFs().existsSync(tempPath)) {
342+
getFs().unlinkSync(tempPath)
328343
}
329344
} catch {
330345
// Cleanup failed, not critical.
331346
}
332347
} catch (error) {
333348
// Clean up temp file on error.
334349
try {
335-
if (existsSync(tempPath)) {
336-
unlinkSync(tempPath)
350+
if (getFs().existsSync(tempPath)) {
351+
getFs().unlinkSync(tempPath)
337352
}
338353
} catch {
339354
// Best effort cleanup.
@@ -349,11 +364,11 @@ export class DlxManifest {
349364
async clear(name: string): Promise<void> {
350365
await processLock.withLock(this.lockPath, async () => {
351366
try {
352-
if (!existsSync(this.manifestPath)) {
367+
if (!getFs().existsSync(this.manifestPath)) {
353368
return
354369
}
355370

356-
const content = readFileSync(this.manifestPath, 'utf8')
371+
const content = getFs().readFileSync(this.manifestPath, 'utf8')
357372
if (!content.trim()) {
358373
return
359374
}
@@ -362,7 +377,7 @@ export class DlxManifest {
362377
delete data[name]
363378

364379
const updatedContent = JSON.stringify(data, null, 2)
365-
writeFileSync(this.manifestPath, updatedContent, 'utf8')
380+
getFs().writeFileSync(this.manifestPath, updatedContent, 'utf8')
366381
} catch (error) {
367382
logger.warn(
368383
`Failed to clear cache for ${name}: ${error instanceof Error ? error.message : String(error)}`,
@@ -377,8 +392,8 @@ export class DlxManifest {
377392
async clearAll(): Promise<void> {
378393
await processLock.withLock(this.lockPath, async () => {
379394
try {
380-
if (existsSync(this.manifestPath)) {
381-
unlinkSync(this.manifestPath)
395+
if (getFs().existsSync(this.manifestPath)) {
396+
getFs().unlinkSync(this.manifestPath)
382397
}
383398
} catch (error) {
384399
logger.warn(
@@ -405,7 +420,7 @@ export class DlxManifest {
405420
*/
406421
getAllPackages(): string[] {
407422
try {
408-
if (!existsSync(this.manifestPath)) {
423+
if (!getFs().existsSync(this.manifestPath)) {
409424
return []
410425
}
411426

@@ -429,6 +444,5 @@ export class DlxManifest {
429444
}
430445
}
431446
}
432-
433447
// Export singleton instance using default manifest location.
434448
export const dlxManifest = new DlxManifest()

src/env/rewire.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,28 @@
99
* - Thread-safe for concurrent test execution
1010
*/
1111

12-
import { AsyncLocalStorage } from 'async_hooks'
12+
let _async_hooks: typeof import('node:async_hooks') | undefined
13+
/**
14+
* Lazily load the async_hooks module to avoid Webpack errors.
15+
* @private
16+
*/
17+
/*@__NO_SIDE_EFFECTS__*/
18+
function getAsyncHooks() {
19+
if (_async_hooks === undefined) {
20+
// Use non-'node:' prefixed require to avoid Webpack errors.
21+
22+
_async_hooks = /*@__PURE__*/ require('async_hooks')
23+
}
24+
return _async_hooks as typeof import('node:async_hooks')
25+
}
1326

1427
import { envAsBoolean } from './helpers'
1528

1629
type EnvOverrides = Map<string, string | undefined>
1730

1831
// Isolated execution context storage for nested overrides (withEnv/withEnvSync)
1932
// AsyncLocalStorage creates isolated contexts that don't leak between concurrent code
33+
const { AsyncLocalStorage } = getAsyncHooks()
2034
const isolatedOverridesStorage = new AsyncLocalStorage<EnvOverrides>()
2135

2236
// Shared test hook overrides (setEnv/clearEnv/resetEnv in beforeEach/afterEach)

src/http-request.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@
1414
* - Zero dependencies on external HTTP libraries.
1515
*/
1616

17-
import { createWriteStream } from 'fs'
17+
let _fs: typeof import('node:fs') | undefined
18+
19+
/**
20+
* Lazily load the fs module to avoid Webpack errors.
21+
* @private
22+
*/
23+
/*@__NO_SIDE_EFFECTS__*/
24+
function getFs() {
25+
if (_fs === undefined) {
26+
_fs = /*@__PURE__*/ require('fs')
27+
}
28+
return _fs as typeof import('node:fs')
29+
}
1830

1931
import type { IncomingMessage } from 'http'
2032

@@ -880,6 +892,8 @@ async function httpDownloadAttempt(
880892
timeout,
881893
}
882894

895+
const { createWriteStream } = getFs()
896+
883897
let fileStream: ReturnType<typeof createWriteStream> | undefined
884898
let streamClosed = false
885899

0 commit comments

Comments
 (0)