-
-
Save tlhunter/abf4590f85f5b1267c00a8be4317b73a to your computer and use it in GitHub Desktop.
Oden.js: JavaScript with zero concerns for ecosystem/browser compat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Oden.js: A controversial JavaScript environment with zero concerns for browser compatibility or existing library compatibility. | |
| // This is an idea for a fork of Node.js to bring it back to it's more "classic" roots. | |
| // It moves tons of globals into "internal modules", forcing files to be explicit about feature dependency. | |
| // This changes some fundamental JavaScript features and breaks most applications. | |
| // It would have to be implemented as a sort of "fresh start" runtime. | |
| // It might not technically be fair to refer to this as JavaScript due to the changes? | |
| // For now it's just an idea that you can paste into your REPL to experiment with. | |
| // Usage: `node --require ./oden.js` | |
| // The long-term goal is to compile a version of Node.js with these changes (and a modified V8) into an `oden` binary. | |
| // This current iteration is simply a pile of hacks which mutate globals and creates fake internal modules. | |
| // This is up-to-date as of Node.js v24.15.0. | |
| // More Info: https://thomashunter.name/posts/2026-04-24-server-side-js-p2-server-first-js | |
| // delete the automatic global modules when using the Node.js REPL as it's confusing | |
| setImmediate(() => { | |
| for (let mod of require('repl').builtinModules) { | |
| if (mod === 'console') continue; | |
| delete global[mod]; | |
| } | |
| // Really this should happen last and within the initial stack but unsure how to delete the automatic module REPL magic | |
| delete global; | |
| delete globalThis; // Undici needs this | |
| }); | |
| void Request; // needs to be accessed before deleting globalThis | |
| { | |
| // What if we remove some of the baggage that comes with an old language intended for browsers? | |
| // Did you know about these methods?! "foo".bold() === "<b>foo</b>" | |
| delete String.prototype.anchor; | |
| delete String.prototype.big; | |
| delete String.prototype.blink; | |
| delete String.prototype.bold; | |
| delete String.prototype.fixed; | |
| delete String.prototype.fontcolor; | |
| delete String.prototype.fontsize; | |
| delete String.prototype.italics; | |
| delete String.prototype.link; | |
| delete String.prototype.small; | |
| delete String.prototype.strike; | |
| delete String.prototype.sub; | |
| delete String.prototype.sup; | |
| } | |
| { | |
| // Is this truly needed? | |
| delete global.eval; | |
| } | |
| { | |
| // const process = require('process'); | |
| delete global.process; | |
| } | |
| { | |
| const timers = require('timers'); | |
| // const { setTimeout, ... } = require('timers'); | |
| delete global.setTimeout; | |
| delete global.clearTimeout; | |
| delete global.setImmediate; | |
| delete global.clearImmediate; | |
| delete global.setInterval; | |
| delete global.clearInterval; | |
| timers.queueMicrotask = global.queueMicrotask; | |
| delete global.queueMicrotask; | |
| timers.sleep = ms => new Promise((resolve) => timers.setTimeout(resolve, ms)); | |
| } | |
| { | |
| // These are all in require('perf_hooks') with the same name as the global | |
| delete global.performance; | |
| delete global.Performance; | |
| if (global.PerformanceEntry) delete global.PerformanceEntry; | |
| if (global.PerformanceMark) delete global.PerformanceMark; | |
| if (global.PerformanceMeasure) delete global.PerformanceMeasure; | |
| if (global.PerformanceObserver) delete global.PerformanceObserver; | |
| if (global.PerformanceObserverEntryList) delete global.PerformanceObserverEntryList; | |
| if (global.PerformanceResourceTiming) delete global.PerformanceResourceTiming; | |
| } | |
| { | |
| const events = require('events'); | |
| events.EventTarget = global.EventTarget; | |
| delete global.EventTarget; | |
| events.Event = global.Event; | |
| delete global.Event; | |
| if (global.CloseEvent) { | |
| events.CloseEvent = global.CloseEvent; | |
| delete global.CloseEvent; | |
| } | |
| if (global.CustomEvent) { | |
| events.CustomEvent = global.CustomEvent; | |
| delete global.CustomEvent; | |
| } | |
| } | |
| { | |
| const crypto = require('crypto'); | |
| if (global.Crypto) { | |
| crypto.Crypto = global.Crypto; | |
| delete global.Crypto; | |
| } | |
| if (global.CryptoKey) { | |
| crypto.CryptoKey = global.CryptoKey; | |
| delete global.CryptoKey; | |
| } | |
| if (global.SubtleCrypto) { | |
| crypto.SubtleCrypto = global.SubtleCrypto; | |
| delete global.SubtleCrypto; | |
| } | |
| } | |
| { | |
| const fs = require('fs'); | |
| if (global.File) { | |
| fs.File = global.File; | |
| delete global.File; | |
| } | |
| } | |
| { | |
| const http = require('http'); | |
| http.fetch = global.fetch; | |
| delete global.fetch; | |
| http.FormData = global.FormData; | |
| delete global.FormData; | |
| http.Request = global.Request; | |
| delete global.Request; | |
| http.Response = global.Response; | |
| delete global.Response; | |
| http.Headers = global.Headers; | |
| delete global.Headers; | |
| http.AbortController = global.AbortController; | |
| delete global.AbortController; | |
| http.AbortSignal = global.AbortSignal; | |
| delete global.AbortSignal; | |
| if (global.WebSocket) { | |
| // already available at require('http').WebSocket | |
| delete global.WebSocket; | |
| } | |
| } | |
| { | |
| const urlModule = require('url'); | |
| const _encodeURI = global.encodeURI; | |
| urlModule.encodeURI = global.encodeURI; | |
| delete global.encodeURI; | |
| String.prototype.encodeURI = function() { return _encodeURI(this); }; | |
| const _decodeURI = global.decodeURI; | |
| urlModule.decodeURI = global.decodeURI; | |
| delete global.decodeURI; | |
| String.prototype.decodeURI = function() { return _decodeURI(this); }; | |
| urlModule.encodeURIComponent = global.encodeURIComponent; | |
| delete global.encodeURIComponent; | |
| const _escape = global.escape; | |
| urlModule.escape = global.escape; | |
| String.prototype.escapeURI = function() { return _escape(this); }; | |
| delete global.escape; | |
| const _unescape = global.unescape; | |
| urlModule.unescape = global.unescape; | |
| String.prototype.unescapeURI = function() { return _unescape(this); }; | |
| delete global.unescape; | |
| const _decodeURIComponent = global.decodeURIComponent; | |
| urlModule.decodeURIComponent = global.decodeURIComponent; | |
| String.prototype.decodeURIComponent = function() { return _decodeURIComponent(this); }; | |
| delete global.decodeURIComponent; | |
| // const { URL, URLSearchParams, URLPattern } = require('url'); | |
| delete global.URL; // Undici needs this | |
| delete global.URLSearchParams; | |
| delete global.URLPattern; | |
| } | |
| { | |
| const stream = require('stream'); | |
| stream.CompressionStream = global.CompressionStream; | |
| delete global.CompressionStream; | |
| stream.DecompressionStream = global.DecompressionStream; | |
| delete global.DecompressionStream; | |
| stream.ReadableByteStreamController = global.ReadableByteStreamController; | |
| delete global.ReadableByteStreamController; | |
| stream.ReadableStream = global.ReadableStream; | |
| delete global.ReadableStream; | |
| stream.ReadableStreamBYOBReader = global.ReadableStreamBYOBReader; | |
| delete global.ReadableStreamBYOBReader; | |
| stream.ReadableStreamBYOBRequest = global.ReadableStreamBYOBRequest; | |
| delete global.ReadableStreamBYOBRequest; | |
| stream.ReadableStreamDefaultController = global.ReadableStreamDefaultController; | |
| delete global.ReadableStreamDefaultController; | |
| stream.ReadableStreamDefaultReader = global.ReadableStreamDefaultReader; | |
| delete global.ReadableStreamDefaultReader; | |
| stream.TransformStream = global.TransformStream; | |
| delete global.TransformStream; | |
| stream.TransformStreamDefaultController = global.TransformStreamDefaultController; | |
| delete global.TransformStreamDefaultController; | |
| stream.WritableStream = global.WritableStream; | |
| delete global.WritableStream; | |
| stream.WritableStreamDefaultController = global.WritableStreamDefaultController; | |
| delete global.WritableStreamDefaultController; | |
| stream.WritableStreamDefaultWriter = global.WritableStreamDefaultWriter; | |
| delete global.WritableStreamDefaultWriter; | |
| stream.ByteLengthQueuingStrategy = global.ByteLengthQueuingStrategy; | |
| delete global.ByteLengthQueuingStrategy; | |
| stream.CountQueuingStrategy = global.CountQueuingStrategy; | |
| delete global.CountQueuingStrategy; | |
| } | |
| { | |
| // What if Object#toString() was useful? | |
| Object.prototype.toString = function(...args) { return JSON.stringify(this, ...args); }; | |
| Set.prototype.toString = function() { return JSON.stringify(Array.from(this)); }; | |
| Map.prototype.toString = function() { return JSON.stringify(Object.fromEntries(this)); }; | |
| } | |
| { | |
| // ArrayBuffer, Int32Array & friends were added to JavaScript after Node.js was conceived | |
| // Node.js wouldn't have a Buffer if it were made today | |
| delete global.Buffer; | |
| } | |
| { | |
| const _btoa = global.btoa; | |
| const _atob = global.atob; | |
| String.prototype.toBase64 = function() { return _btoa(this); }; | |
| String.prototype.fromBase64 = function() { return _atob(this); }; | |
| delete global.btoa; | |
| delete global.atob; | |
| // I suppose it's starting to look like Ruby now... | |
| String.prototype.fromJson = function() { return JSON.parse(this); }; | |
| } | |
| { | |
| // This hack allows us to make "fake internal modules". | |
| // It's mostly useful for this single-file Oden example. | |
| // Of course, if this were a full runtime, we wouldn't need these hacks. | |
| const module = require('module'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| function addFakeGlobalModule(moduleName, newExports) { | |
| const pathToModule = path.join(__dirname, 'node_modules', moduleName, 'index.js'); | |
| const parent = path.dirname(pathToModule); | |
| fs.mkdirSync(parent, { recursive: true }); | |
| fs.writeFileSync(pathToModule, ''); | |
| const newModule = new module.Module(); | |
| newModule.path = moduleName; | |
| newModule.exports = newExports; | |
| newModule.filename = moduleName; | |
| newModule.loaded = true; | |
| require.cache[pathToModule] = newModule; | |
| newModule.id = pathToModule; | |
| newModule.path = parent; | |
| } | |
| { | |
| addFakeGlobalModule('errors', { | |
| Error, | |
| TypeError, | |
| SyntaxError, | |
| RangeError, | |
| ReferenceError, | |
| EvalError, // unneccesary w/o eval()? | |
| URIError, | |
| AggregateError, | |
| }); | |
| // delete global.Error; | |
| // delete global.TypeError; | |
| // delete global.SyntaxError; | |
| // delete global.RangeError; | |
| // delete global.ReferenceError; | |
| delete global.EvalError; | |
| delete global.URIError; | |
| delete global.AggregateError; | |
| delete global.DOMException; | |
| } | |
| { | |
| addFakeGlobalModule('math', Math); | |
| delete global.Math; | |
| } | |
| { | |
| addFakeGlobalModule('threads', { | |
| BroadcastChannel: global.BroadcastChannel, | |
| Atomics: global.Atomics, | |
| MessagePort: global.MessagePort, | |
| MessageEvent: global.MessageEvent, | |
| MessageChannel: global.MessageChannel, | |
| }); | |
| delete global.BroadcastChannel; | |
| delete global.Atomics; | |
| delete global.MessagePort; | |
| delete global.MessageEvent; | |
| delete global.MessageChannel; | |
| } | |
| { | |
| addFakeGlobalModule('atomics', { | |
| Atomics: global.Atomics, | |
| SharedArrayBuffer: global.SharedArrayBuffer, // redundant with array_buffer | |
| }); | |
| delete global.Atomics; | |
| // delete global.SharedArrayBuffer; // later | |
| } | |
| { | |
| addFakeGlobalModule('array_buffer', { | |
| ArrayBuffer: global.ArrayBuffer, | |
| SharedArrayBuffer: global.SharedArrayBuffer, // redundant with atomics | |
| DataView: global.DataView, | |
| Int8Array: global.Int8Array, | |
| Int16Array: global.Int16Array, | |
| Int32Array: global.Int32Array, | |
| BigInt64Array: global.BigInt64Array, | |
| Uint8Array: global.Uint8Array, | |
| Uint8ClampedArray: global.Uint8ClampedArray, | |
| Uint16Array: global.Uint16Array, | |
| Uint32Array: global.Uint32Array, | |
| BigUint64Array: global.BigUint64Array, | |
| Float32Array: global.Float32Array, | |
| Float64Array: global.Float64Array, | |
| TypedArray: global.Int8Array.prototype, // I have no idea why TypedArray isn't a global | |
| Blob: global.Blob, | |
| }); | |
| delete global.SharedArrayBuffer; | |
| delete global.ArrayBuffer; | |
| delete global.BigInt64Array; | |
| delete global.BigUint64Array; | |
| delete global.DataView; | |
| delete global.Float32Array; | |
| delete global.Float64Array; | |
| delete global.Int16Array; | |
| delete global.Int32Array; | |
| delete global.Int8Array; | |
| delete global.Uint16Array; | |
| delete global.Uint32Array; | |
| delete global.Uint8Array; | |
| delete global.Uint8ClampedArray; | |
| delete global.Blob; | |
| } | |
| { | |
| const _encoder = global.TextEncoder; | |
| addFakeGlobalModule('text_encoder', { | |
| TextEncoder: global.TextEncoder, | |
| TextEncoderStream: global.TextEncoderStream, | |
| TextDecoder: global.TextDecoder, | |
| TextDecoderStream: global.TextDecoderStream, | |
| }); | |
| delete global.TextEncoder; | |
| delete global.TextEncoderStream; | |
| delete global.TextDecoder; | |
| delete global.TextDecoderStream; | |
| String.prototype.toArrayBuffer = function() { return (new _encoder()).encode(this); }; | |
| } | |
| { | |
| addFakeGlobalModule('intl', global.Intl); | |
| delete global.Intl; | |
| } | |
| { | |
| // TODO: This might be going to far | |
| addFakeGlobalModule('reflection', { | |
| Reflect: global.Reflect, | |
| Proxy: global.Proxy, | |
| }); | |
| delete global.Reflect; | |
| delete global.Proxy; | |
| } | |
| if (global.navigator) { | |
| addFakeGlobalModule('navigator', { | |
| navigator: global.navigator, | |
| Navigator: global.Navigator, | |
| }); | |
| delete global.navigator; | |
| delete global.Navigator; | |
| } | |
| if (global.FinalizationRegistry) { | |
| addFakeGlobalModule('finalization_registry', global.FinalizationRegistry); | |
| delete global.FinalizationRegistry; | |
| } | |
| { | |
| // Produces a valid ISO8601 date in Zulu time only | |
| Date.prototype.toString = Date.prototype.toISOString; | |
| addFakeGlobalModule('datetime', { | |
| Date: global.Date, | |
| // Temporal: global.Temporal | |
| }); | |
| const Math = require('math'); | |
| // .toISOString -> 2024-11-07T16:42:23.840Z | |
| // .toISOTimezoneString -> 2024-11-07T09:42:23-07:00 | |
| // Produces a valid ISO8601 string with timezone offset | |
| Date.prototype.toISOTimezoneString = function() { | |
| const tzo = -this.getTimezoneOffset(); | |
| const dif = tzo >= 0 ? '+' : '-'; | |
| const pad = (num) => (num < 10 ? '0' : '') + num; | |
| return this.getFullYear() + | |
| '-' + pad(this.getMonth() + 1) + | |
| '-' + pad(this.getDate()) + | |
| 'T' + pad(this.getHours()) + | |
| ':' + pad(this.getMinutes()) + | |
| ':' + pad(this.getSeconds()) + | |
| dif + pad(Math.floor(Math.abs(tzo) / 60)) + | |
| ':' + pad(Math.abs(tzo) % 60); | |
| }; | |
| delete global.Date; | |
| } | |
| // This causes a bug with the internal Undici module | |
| // { | |
| // addFakeGlobalModule('wasm', global.WebAssembly); | |
| // | |
| // delete global.WebAssembly; | |
| // } | |
| // It throws immediately even if fetch is never used | |
| // TODO: Can we make typeof null === 'null'? | |
| } |
Author
Author
const { sleep } = require('timers');
const { random } = require('math');
const { Date } = require('datetime');
(async () => {
const result = '{"foo": "bar"}';
console.log('parsed', result.fromJson());
const encoded = result.toBase64();
await sleep(100);
console.log('encoded', encoded);
console.log('random', random());
console.log('now', (new Date()).toISOTimezoneString());
})();
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
At this point, pressing
<tab>in the REPL lists the following: