Skip to content
LogoLogo

Platform globals in VibeLang

Node.js reaches a .vibe file only through Effect's services, which a layer provides, or by escaping to TypeScript: a hatch, a module block, or a .ts file. So Node.js's globals, such as process, Buffer, require, and __dirname, and the web's globals that do I/O or read the clock or randomness, such as fetch, setTimeout, crypto, performance, and localStorage, are not in scope in a .vibe file, and a .vibe file does not import a Node.js built-in module, such as node:fs or fs, as a value. Each use is EK4402, or EK4403 for a clock or randomness read, and the error names what replaces it. A type-only import, as in import type { Server } from "node:http", stays allowed, and .ts files keep every global.

ECMAScript's built-ins stay: Array, Map, Set, JSON, Math, Promise, Symbol, Date, Intl, and the rest; Date.now(), new Date(), and Math.random() are EK4403. The web's pure helpers stay too, because they touch nothing outside the program: URL, URLSearchParams, TextEncoder, TextDecoder, structuredClone, AbortController, AbortSignal, Blob, FormData, Headers, Request, Response, Event, EventTarget, atob, btoa, and the web streams.

Replacements

Each row says what replaces a global or one of its members: an Effect API first, then VibeLang's experimental service where Effect lacks one, then the escape to TypeScript, with the reason. EK4402 reads the same table.

GlobalUse instead
BroadcastChannelescape to TypeScript for now: Effect has no service for messages between browser tabs; a Worker carries messages to a worker.
Buffernew TextEncoder().encode(text)
Buffer.allocnew Uint8Array(size)
Buffer.concatnew Uint8Array([...a, ...b])
Buffer.fromnew TextEncoder().encode(text)
Bunescape to TypeScript for now: A runtime's own API belongs to its platform package's layers, such as @effect/platform-bun's, or to a .ts file.
Denoescape to TypeScript for now: A runtime's own API belongs to its platform package's layers, such as @effect/platform-bun's, or to a .ts file.
EventSourceHttpClient.get(url)
SharedWorkerWorker and WorkerRunner
WebSocketSocket.makeWebSocket(url)
WorkerWorker and WorkerRunner
XMLHttpRequestHttpClient.get(url)
__dirnamePath.dirname(Path.fromFileUrl(new URL(import.meta.url)))
__filenamePath.fromFileUrl(new URL(import.meta.url))
addEventListenerStream.fromEventListener(target, "message")
alertescape to TypeScript for now: Effect has no service for the browser's dialogs; a terminal program asks with Terminal.readLine().
cachesescape to TypeScript for now: Effect has no service for the browser's Cache Storage; KeyValueStore stores strings by key.
cancelAnimationFrameescape to TypeScript for now: Effect has no service for the browser's animation and idle callbacks; schedule work with Schedule, or call them in a .ts file.
cancelIdleCallbackescape to TypeScript for now: Effect has no service for the browser's animation and idle callbacks; schedule work with Schedule, or call them in a .ts file.
clearImmediatefiber.interrupt()
clearIntervalfiber.interrupt()
clearTimeoutfiber.interrupt()
confirmescape to TypeScript for now: Effect has no service for the browser's dialogs; a terminal program asks with Terminal.readLine().
consoleConsole.log(message)
console.debugConsole.debug(message)
console.errorConsole.error(message)
console.infoConsole.info(message)
console.logConsole.log(message)
console.tableConsole.table(rows)
console.traceConsole.trace(message)
console.warnConsole.warn(message)
cryptoCrypto.randomBytes(size)
crypto.createCipherivescape to TypeScript for now: Effect's Crypto has random values and digests, and VibeLang's experimental Crypto adds HMAC and signatures; neither has ciphers or key generation.
crypto.createDecipherivescape to TypeScript for now: Effect's Crypto has random values and digests, and VibeLang's experimental Crypto adds HMAC and signatures; neither has ciphers or key generation.
crypto.createHashCrypto.digest("SHA-256", bytes)
crypto.createHmacExperimental.Crypto.hmac("SHA-256", key, data)
crypto.createSignExperimental.Crypto.sign("Ed25519", privateKey, data)
crypto.createVerifyExperimental.Crypto.verify("Ed25519", publicKey, signature, data)
crypto.generateKeyPairescape to TypeScript for now: Effect's Crypto has random values and digests, and VibeLang's experimental Crypto adds HMAC and signatures; neither has ciphers or key generation.
crypto.getRandomValuesCrypto.randomBytes(size)
crypto.hashCrypto.digest("SHA-256", bytes)
crypto.randomBytesCrypto.randomBytes(size)
crypto.randomIntCrypto.randomIntBetween(min, max)
crypto.randomUUIDCrypto.randomUUIDv4()
crypto.signExperimental.Crypto.sign("Ed25519", privateKey, data)
crypto.subtleExperimental.Crypto.hmac("SHA-256", key, data)
crypto.subtle.digestCrypto.digest("SHA-256", bytes)
crypto.subtle.signExperimental.Crypto.sign("Ed25519", privateKey, data)
crypto.subtle.verifyExperimental.Crypto.verify("Ed25519", publicKey, signature, data)
crypto.verifyExperimental.Crypto.verify("Ed25519", publicKey, signature, data)
crypto.webcryptoExperimental.Crypto.hmac("SHA-256", key, data)
dispatchEventStream.fromEventListener(target, "message")
documentescape to TypeScript for now: Effect has no service for the DOM; UI code belongs in a .ts or .tsx file, such as a React component's.
document.addEventListenerBrowserStream.fromEventListenerDocument("visibilitychange")
exportsexport const name = value
fetchHttpClient.get(url)
globalglobalThis
historyescape to TypeScript for now: Effect has no service for the DOM; UI code belongs in a .ts or .tsx file, such as a React component's.
importScriptsWorker and WorkerRunner
indexedDBKeyValueStore.get(key)
localStorageKeyValueStore.get(key)
locationescape to TypeScript for now: Effect has no service for the DOM; UI code belongs in a .ts or .tsx file, such as a React component's.
moduleexport const name = value
navigatorescape to TypeScript for now: Effect has no service for the browser's navigator; navigator.clipboard, geolocation, permissions, and locks have services.
navigator.clipboardClipboard.readString()
navigator.geolocationGeolocation.getCurrentPosition()
navigator.hardwareConcurrencyExperimental.Process.availableParallelism()
navigator.locksExperimental.Locks.hold(name)
navigator.permissionsPermissions.query("geolocation")
performanceClock.currentTimeMillis()
performance.nowClock.monotonicTimeNanos()
performance.timeOriginExperimental.Process.timeOrigin()
processConfig.String("NAME")()
process.archExperimental.Process.arch()
process.argvStdio.args()
process.cwdPath.resolve()
process.envConfig.String("NAME")()
process.execPathExperimental.Process.execPath()
process.exitmain's result: return or fail from main
process.exitCodemain's result: return or fail from main
process.hrtimeClock.monotonicTimeNanos()
process.killExperimental.Process.kill(pid, { killSignal })
process.memoryUsageExperimental.Process.memoryUsage()
process.nextTickyieldNow()
process.offescape to TypeScript for now: NodeRuntime.runMain handles SIGINT and SIGTERM; Effect has no service for other process events.
process.onescape to TypeScript for now: NodeRuntime.runMain handles SIGINT and SIGTERM; Effect has no service for other process events.
process.onceescape to TypeScript for now: NodeRuntime.runMain handles SIGINT and SIGTERM; Effect has no service for other process events.
process.pidExperimental.Process.pid()
process.platformExperimental.Process.platform()
process.removeListenerescape to TypeScript for now: NodeRuntime.runMain handles SIGINT and SIGTERM; Effect has no service for other process events.
process.stderrConsole.error(text)
process.stdinTerminal.readLine()
process.stdoutConsole.log(text)
process.uptimeescape to TypeScript for now: Clock reads the time, not the process's age: keep a Clock.monotonicTimeNanos() reading from the start.
process.versionExperimental.Process.versions()
process.versionsExperimental.Process.versions()
promptescape to TypeScript for now: Effect has no service for the browser's dialogs; a terminal program asks with Terminal.readLine().
queueMicrotaskyieldNow()
removeEventListenerStream.fromEventListener(target, "message")
requestAnimationFrameescape to TypeScript for now: Effect has no service for the browser's animation and idle callbacks; schedule work with Schedule, or call them in a .ts file.
requestIdleCallbackescape to TypeScript for now: Effect has no service for the browser's animation and idle callbacks; schedule work with Schedule, or call them in a .ts file.
requireimport { name } from "./module.ts"
require.resolveimport.meta.resolve(specifier)
selfglobalThis
sessionStorageKeyValueStore.get(key)
setImmediateyieldNow()
setIntervalrepeat(program, Schedule.spaced("1 second"))
setTimeoutsleep("1 second")
windowescape to TypeScript for now: Effect has no service for the DOM; UI code belongs in a .ts or .tsx file, such as a React component's.
window.addEventListenerBrowserStream.fromEventListenerWindow("resize")

The replacements at work

Each call above, in code that compiles. A function that calls a service needs its layer in main, such as NodeServices.layer from @effect/platform-node:

function replacements(
  path: string, text: string, url: string, key: string, specifier: string, message: string,
  size: number, min: number, max: number, bytes: Uint8Array, a: Uint8Array, b: Uint8Array,
  rows: ReadonlyArray<unknown>, target: EventTarget, program: void!!,
) {
  const name = Config.String("NAME")()
  const args = Stdio.args()
  const directory = Path.resolve()
  const here = Path.dirname(Path.fromFileUrl(new URL(import.meta.url)))
  const file = Path.fromFileUrl(new URL(import.meta.url))
  const resolved = import.meta.resolve(specifier)
  const line = Terminal.readLine()
  Console.log(message)
  Console.log(text)
  Console.error(message)
  Console.error(text)
  Console.warn(message)
  Console.info(message)
  Console.debug(message)
  Console.trace(message)
  Console.table(rows)
  const random = Crypto.randomBytes(size)
  const id = Crypto.randomUUIDv4()
  const roll = Crypto.randomIntBetween(min, max)
  const digest = Crypto.digest("SHA-256", bytes)
  const encoded = new TextEncoder().encode(text)
  const empty = new Uint8Array(size)
  const joined = new Uint8Array([...a, ...b])
  const response = HttpClient.get(url)
  const socket = Socket.makeWebSocket(url)
  const stored = KeyValueStore.get(key)
  const events = Stream.fromEventListener(target, "message")
  const millis = Clock.currentTimeMillis()
  const nanos = Clock.monotonicTimeNanos()
  sleep("1 second")
  yieldNow()
  const fiber = fork repeat(program, Schedule.spaced("1 second"))
  fiber.interrupt()
  const global = globalThis
  return { name, args, directory, here, file, resolved, line, random, id, roll, digest, encoded, empty, joined, response, socket, stored, events, millis, nanos, global }
}

In the browser, @effect/platform-browser supplies these services and their layers:

import { BrowserStream, Clipboard, Geolocation, Permissions } from "@effect/platform-browser"
 
function browser() {
  const text = Clipboard.readString()
  const position = Geolocation.getCurrentPosition()
  const status = Permissions.query("geolocation")
  const resizes = BrowserStream.fromEventListenerWindow("resize")
  const visibility = BrowserStream.fromEventListenerDocument("visibilitychange")
  return { text, position, status, resizes, visibility }
}

VibeLang's experimental services fill what Effect lacks, provided by NodeServices.layer from vibelang-runtime/experimental/platform-node or Bun's, and Locks by BrowserServices.layer from vibelang-runtime/experimental/platform-browser; Experimental platform APIs has the details:

function experimental(path: string, pid: number, killSignal: ChildProcess.Signal, name: string, key: Redacted<Uint8Array>, privateKey: Redacted<Uint8Array>, publicKey: Uint8Array, signature: Uint8Array, data: Uint8Array) {
  const id = Experimental.Process.pid()
  const platform = Experimental.Process.platform()
  const arch = Experimental.Process.arch()
  const executable = Experimental.Process.execPath()
  const versions = Experimental.Process.versions()
  const memory = Experimental.Process.memoryUsage()
  const host = Experimental.Process.hostname()
  const cores = Experimental.Process.availableParallelism()
  const origin = Experimental.Process.timeOrigin()
  Experimental.Process.kill(pid, { killSignal })
  const running = Experimental.Process.isRunning(pid)
  Experimental.FileSystem.access(path, { executable: true })
  const file = Experimental.FileSystem.open(path, { nonBlocking: true })
  const mac = Experimental.Crypto.hmac("SHA-256", key, data)
  const signed = Experimental.Crypto.sign("Ed25519", privateKey, data)
  const valid = Experimental.Crypto.verify("Ed25519", publicKey, signature, data)
  const lock = Experimental.Locks.hold(name)
  return { id, platform, arch, executable, versions, memory, host, cores, origin, running, file, mac, signed, valid, lock }
}

Modules load with declarations, not at run time. A .vibe file imports what require loaded:

// module.ts
export const name = "Ada"
import { name } from "./module.ts"
 
function greeting() {
  return `Hello, ${name}`
}

and exports what CommonJS's module.exports and exports held:

const value = 42
export const name = value

Escaping to TypeScript

Where a row says to escape, write the TypeScript in a module block, whose body keeps every global, and run its function in a hatch:

module dom {
  export function title(): string {
    return document.title
  }
}
import { title } from dom
 
function pageTitle() {
  return sync(() => title())
}

A .vibe file that needs a Node.js value, such as the createServer that NodeHttpServer.layer takes, imports it in a module block too:

// fragment
// The snippet checker installs no @types/node, which declares node:http.
module node {
  import { createServer } from "node:http"
  export const server = createServer
}
import { server } from node

Limits of static checks

EK4402 and EK4403 are static, lint-strength checks, not a sandbox or a security boundary. They recognize direct uses, but aliases such as const g = globalThis; g.process.pid, as any, and reflection through Reflect.get can bypass them. Use process isolation when running untrusted code.

Intl.DateTimeFormat().format() with no argument reads the current clock without EK4403. Read time through Clock.currentTimeMillis() and pass that value to the formatter when you need controlled time, including in tests.