Skip to content

Instantly share code, notes, and snippets.

@tlhunter

tlhunter/oden.js Secret

Last active April 25, 2026 20:10
Show Gist options
  • Select an option

  • Save tlhunter/abf4590f85f5b1267c00a8be4317b73a to your computer and use it in GitHub Desktop.

Select an option

Save tlhunter/abf4590f85f5b1267c00a8be4317b73a to your computer and use it in GitHub Desktop.
Oden.js: JavaScript with zero concerns for ecosystem/browser compat
// 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'?
}
@tlhunter
Copy link
Copy Markdown
Author

tlhunter commented Feb 27, 2023

At this point, pressing <tab> in the REPL lists the following:

Array
BigInt
Boolean
Date
Error
FinalizationRegistry
Function
Infinity
JSON
Map
NaN
Number
Object
Promise
RangeError
ReferenceError
RegExp
Set
String
Symbol
SyntaxError
TypeError
WeakMap
WeakRef
WeakSet
_
_error
isFinite
isNaN
parseFloat
parseInt
require
structuredClone
undefined

__proto__
hasOwnProperty
isPrototypeOf
propertyIsEnumerable
toLocaleString
toString
valueOf

constructor

@tlhunter
Copy link
Copy Markdown
Author

tlhunter commented Nov 7, 2024

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