Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions docs/case-studies/issue-50/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Case Study: Issue #50 - Do Not Hardcode Builtins

## Overview

**Issue**: [#50 - do not hardcode builtins](https://github.com/link-foundation/use-m/issues/50)
**Date**: December 2025
**Status**: Implementation in progress

## Problem Statement

The `use-m` library maintains a hardcoded list of Node.js built-in modules in the `supportedBuiltins` object. This approach has several drawbacks:

1. **Maintenance burden**: New Node.js versions introduce new built-in modules that require manual updates
2. **Code size**: The hardcoded list adds to the bundle size that needs to be downloaded
3. **Version drift**: The list may become outdated as Node.js evolves
4. **Incomplete coverage**: Not all built-in modules are included in the hardcoded list

## Timeline of Events

### Initial Report
- User `klntsky` opened issue #50 pointing out that built-in modules can be obtained dynamically using `module.builtinModules`
- Provided code snippet demonstrating how to access the list

### Requirements Clarified
- Maintainer `konard` requested a case study analysis to:
- Reconstruct the timeline/sequence of events
- Find root causes of the problem
- Propose possible solutions

## Root Cause Analysis

### Current Implementation (Before Fix)

The `use.mjs` file (and corresponding `use.cjs`, `use.js`) contains a `supportedBuiltins` object with hardcoded entries:

```javascript
const supportedBuiltins = {
'console': { browser: ..., node: ... },
'crypto': { browser: ..., node: ... },
'fs': { browser: null, node: ... },
// ... approximately 25 more hardcoded entries
};
```

### Why This Is Problematic

1. **Node.js has ~70+ built-in modules** (as shown in issue description)
2. **Current hardcoded list covers only ~25 modules**
3. **New modules** like `node:sqlite`, `node:test`, `node:sea` are not supported
4. **Subpath modules** like `path/posix`, `path/win32`, `util/types` may be missed

## Solution Analysis

### Node.js API: `module.builtinModules`

Node.js provides a built-in API to dynamically retrieve all available modules:

```javascript
// ESM
import { builtinModules } from 'node:module';

// CommonJS
const { builtinModules } = require('node:module');
```

As of Node.js v23.5.0+, this list includes prefix-only modules.

### Node.js API: `module.isBuiltin()`

Additionally, Node.js v18.6.0+ provides a helper function:

```javascript
import { isBuiltin } from 'node:module';
isBuiltin('node:fs'); // true
isBuiltin('fs'); // true
isBuiltin('wss'); // false
```

## Proposed Solution

### Strategy: Hybrid Approach

Since `use-m` supports both browser and Node.js environments, and some built-in modules require special browser polyfills or wrappers:

1. **Keep special handling** for modules that need browser polyfills (`console`, `crypto`, `url`, `performance`)
2. **Use `isBuiltin()`** to dynamically detect all other Node.js built-in modules
3. **Fall back to generic import** for modules without special browser handling

### Implementation Changes

1. **Add dynamic builtin detection** using `module.isBuiltin()` or checking against `builtinModules`
2. **Reduce hardcoded list** to only modules that need special browser handling
3. **Update all three files**: `use.mjs`, `use.cjs`, `use.js`

### Benefits

- **Smaller code size**: Removes ~200+ lines of hardcoded module definitions
- **Future-proof**: Automatically supports new built-in modules
- **Better coverage**: Supports all 70+ built-in modules immediately
- **Easier maintenance**: No need to update list when Node.js adds new modules

## References

- [Node.js module.builtinModules API](https://nodejs.org/api/module.html)
- [GitHub Issue #50](https://github.com/link-foundation/use-m/issues/50)
- [npm package: builtin-modules](https://github.com/sindresorhus/builtin-modules) - static alternative

## Files Modified

- `use.mjs` - Primary implementation
- `use.cjs` - CommonJS version
- `use.js` - Universal/browser version
- `tests/builtins.test.mjs` - Tests for dynamic builtin detection
- `tests/builtins.test.cjs` - CommonJS version of tests
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions tests/dynamic-builtins.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const { use } = require('../use.cjs');
const { describe, test, expect } = require('../test-adapter.cjs');
const moduleName = '[cjs module]';

describe(`${moduleName} Dynamic built-in module detection`, () => {
// Test modules that are now dynamically detected (not in specialBuiltins with explicit node handler)
test(`${moduleName} tty module should work (dynamically detected)`, async () => {
const tty = await use('tty');

expect(tty).toBeDefined();
expect(typeof tty.isatty).toBe('function');
});

test(`${moduleName} cluster module should work (dynamically detected)`, async () => {
const cluster = await use('cluster');

expect(cluster).toBeDefined();
expect(typeof cluster.isMaster === 'boolean' || typeof cluster.isPrimary === 'boolean').toBe(true);
});

test(`${moduleName} readline module should work (dynamically detected)`, async () => {
const readline = await use('readline');

expect(readline).toBeDefined();
expect(typeof readline.createInterface).toBe('function');
});

test(`${moduleName} string_decoder module should work (dynamically detected)`, async () => {
const stringDecoder = await use('string_decoder');

expect(stringDecoder).toBeDefined();
expect(typeof stringDecoder.StringDecoder).toBe('function');
});

test(`${moduleName} timers module should work (dynamically detected)`, async () => {
const timers = await use('timers');

expect(timers).toBeDefined();
expect(typeof timers.setTimeout).toBe('function');
expect(typeof timers.setInterval).toBe('function');
});

test(`${moduleName} worker_threads module should work (dynamically detected)`, async () => {
const workerThreads = await use('worker_threads');

expect(workerThreads).toBeDefined();
expect(typeof workerThreads.Worker).toBe('function');
expect(typeof workerThreads.isMainThread).toBe('boolean');
});

test(`${moduleName} vm module should work (dynamically detected)`, async () => {
const vm = await use('vm');

expect(vm).toBeDefined();
expect(typeof vm.createContext).toBe('function');
expect(typeof vm.runInContext).toBe('function');
});

test(`${moduleName} punycode module should work (dynamically detected)`, async () => {
const punycode = await use('punycode');

expect(punycode).toBeDefined();
expect(typeof punycode.encode).toBe('function');
expect(typeof punycode.decode).toBe('function');
});

// Test with node: prefix
test(`${moduleName} node:tty should work with prefix`, async () => {
const tty = await use('node:tty');

expect(tty).toBeDefined();
expect(typeof tty.isatty).toBe('function');
});

// Test subpath modules that are dynamically detected
test(`${moduleName} dns/promises should still work (has special handling)`, async () => {
const dnsPromises = await use('dns/promises');

expect(dnsPromises).toBeDefined();
expect(typeof dnsPromises.lookup).toBe('function');
});

test(`${moduleName} stream/promises should still work (has special handling for older node)`, async () => {
const streamPromises = await use('stream/promises');

expect(streamPromises).toBeDefined();
expect(typeof streamPromises.pipeline).toBe('function');
});
});
89 changes: 89 additions & 0 deletions tests/dynamic-builtins.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { use } from '../use.mjs';
import { describe, test, expect } from '../test-adapter.mjs';
const moduleName = `[${import.meta.url.split('.').pop()} module]`;

describe(`${moduleName} Dynamic built-in module detection`, () => {
// Test modules that are now dynamically detected (not in specialBuiltins with explicit node handler)
test(`${moduleName} tty module should work (dynamically detected)`, async () => {
const tty = await use('tty');

expect(tty).toBeDefined();
expect(typeof tty.isatty).toBe('function');
});

test(`${moduleName} cluster module should work (dynamically detected)`, async () => {
const cluster = await use('cluster');

expect(cluster).toBeDefined();
expect(typeof cluster.isMaster === 'boolean' || typeof cluster.isPrimary === 'boolean').toBe(true);
});

test(`${moduleName} readline module should work (dynamically detected)`, async () => {
const readline = await use('readline');

expect(readline).toBeDefined();
expect(typeof readline.createInterface).toBe('function');
});

test(`${moduleName} string_decoder module should work (dynamically detected)`, async () => {
const stringDecoder = await use('string_decoder');

expect(stringDecoder).toBeDefined();
expect(typeof stringDecoder.StringDecoder).toBe('function');
});

test(`${moduleName} timers module should work (dynamically detected)`, async () => {
const timers = await use('timers');

expect(timers).toBeDefined();
expect(typeof timers.setTimeout).toBe('function');
expect(typeof timers.setInterval).toBe('function');
});

test(`${moduleName} worker_threads module should work (dynamically detected)`, async () => {
const workerThreads = await use('worker_threads');

expect(workerThreads).toBeDefined();
expect(typeof workerThreads.Worker).toBe('function');
expect(typeof workerThreads.isMainThread).toBe('boolean');
});

test(`${moduleName} vm module should work (dynamically detected)`, async () => {
const vm = await use('vm');

expect(vm).toBeDefined();
expect(typeof vm.createContext).toBe('function');
expect(typeof vm.runInContext).toBe('function');
});

test(`${moduleName} punycode module should work (dynamically detected)`, async () => {
const punycode = await use('punycode');

expect(punycode).toBeDefined();
expect(typeof punycode.encode).toBe('function');
expect(typeof punycode.decode).toBe('function');
});

// Test with node: prefix
test(`${moduleName} node:tty should work with prefix`, async () => {
const tty = await use('node:tty');

expect(tty).toBeDefined();
expect(typeof tty.isatty).toBe('function');
});

// Test subpath modules that are dynamically detected
test(`${moduleName} dns/promises should still work (has special handling)`, async () => {
const dnsPromises = await use('dns/promises');

expect(dnsPromises).toBeDefined();
expect(typeof dnsPromises.lookup).toBe('function');
});

test(`${moduleName} stream/promises should still work (has special handling for older node)`, async () => {
const streamPromises = await use('stream/promises');

expect(streamPromises).toBeDefined();
expect(typeof streamPromises.pipeline).toBe('function');
});
});
Loading
Loading