Skip to content

Commit 24d445a

Browse files
committed
feat(constants): add IS_NODE / IS_BROWSER / IS_WORKER runtime constants
src/constants/runtime.ts — browser-safe runtime detection via process.versions.node; no Node-only imports, safe for browser bundlers. src/node/module.ts — IS_NODE guard on getNodeModule()/isNodeBuiltin() so they return undefined/false in browser contexts instead of calling require('node:module'). Without this, browser bundlers follow the static require() through lib's pre-built CJS dist, producing UNRESOLVED_IMPORT warnings that crash Chrome extension SW registration (status code 15). test/unit/node/module.test.mts — handle optional getNodeModule() return.
1 parent 8e5db59 commit 24d445a

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

src/constants/runtime.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @file Runtime environment detection constants. All checks use only
3+
* `typeof`-safe global probes so this module is safe to import in browser,
4+
* Node.js, Deno, Bun, and bundled contexts alike.
5+
*/
6+
7+
/**
8+
* True when running inside a Node.js process. Detected via
9+
* `process.versions.node` — present in Node, absent in browsers and Deno/Bun
10+
* which expose a different `process.versions` shape (or no `process` at all).
11+
*/
12+
export const IS_NODE =
13+
typeof process !== 'undefined' &&
14+
typeof process.versions !== 'undefined' &&
15+
typeof process.versions.node === 'string'
16+
17+
/**
18+
* True when running in a browser context (window + document both defined).
19+
* Note: Chrome extensions have `window` in popup contexts but not in service
20+
* workers — check `IS_SERVICE_WORKER` for that case.
21+
*/
22+
export const IS_BROWSER =
23+
typeof window !== 'undefined' && typeof document !== 'undefined'
24+
25+
/**
26+
* True when running inside a Web Worker / Chrome MV3 service worker.
27+
* `self` is defined without `window` in worker contexts.
28+
*/
29+
export const IS_WORKER =
30+
typeof self !== 'undefined' &&
31+
typeof window === 'undefined' &&
32+
typeof document === 'undefined'

src/node/module.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@
66
// eslint-disable-next-line n/prefer-node-protocol
77
import type * as NodeModule from 'node:module'
88

9+
import { IS_NODE } from '../constants/runtime'
10+
911
let cachedModule: typeof NodeModule | undefined
1012

11-
export function getNodeModule(): typeof NodeModule {
13+
export function getNodeModule(): typeof NodeModule | undefined {
14+
// Skip in browser / non-Node runtimes — `node:module` is not available
15+
// there and the static require() call would throw at runtime or produce an
16+
// UNRESOLVED_IMPORT warning when a browser bundler walks the call.
17+
if (!IS_NODE) {
18+
return undefined
19+
}
1220
return (cachedModule ??=
1321
/*@__PURE__*/ require('node:module') as typeof NodeModule)
1422
}
@@ -18,10 +26,16 @@ export function getNodeModule(): typeof NodeModule {
1826
* resolves the binding; subsequent calls dispatch through the cached function
1927
* reference. Safe to detach — `isBuiltin` is `this`-free.
2028
*
29+
* Returns `false` in browser / non-CJS environments where `require` is
30+
* undefined — no `node:` modules are built-in there.
31+
*
2132
* Single source of truth for "is this a Node builtin?" probes across socket-lib
2233
* (used by the smol-binding loaders to gate `require('node:smol-*')`).
2334
*/
2435
let cachedIsBuiltin: ((name: string) => boolean) | undefined
2536
export function isNodeBuiltin(name: string): boolean {
26-
return (cachedIsBuiltin ??= getNodeModule().isBuiltin)(name)
37+
if (!IS_NODE) {
38+
return false
39+
}
40+
return (cachedIsBuiltin ??= getNodeModule()!.isBuiltin)(name)
2741
}

test/unit/node/module.test.mts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { getNodeModule, isNodeBuiltin } from '../../../src/node/module'
88

99
describe('node/module', () => {
1010
describe('getNodeModule', () => {
11-
it('returns the node:module module', () => {
11+
it('returns the node:module module in Node.js', () => {
1212
const mod = getNodeModule()
13-
expect(typeof mod.isBuiltin).toBe('function')
14-
expect(typeof mod.createRequire).toBe('function')
13+
// In Node.js (where this test runs) `require` exists so mod is defined.
14+
expect(mod).toBeDefined()
15+
expect(typeof mod!.isBuiltin).toBe('function')
16+
expect(typeof mod!.createRequire).toBe('function')
1517
})
1618

1719
it('is idempotent across repeated calls', () => {

0 commit comments

Comments
 (0)