Module-Format Mods
This document specifies the entry.format: "module" evaluation mode, the rules that govern ES-module mod entries across all conformant runtimes, and the authoring conventions that make a TypeScript mod compile to a module the runtime can load.
A mod entry is either a classic script (format: "script", the default) or an ES module (format: "module"). Both modes evaluate a single self-contained entry source in the same sandbox realm. Module mode adds top-level export syntax and automatic export harvesting; it does not add module loading.
Evaluation Model
Section titled “Evaluation Model”When a mod’s entry block declares format: "module":
- The entry source is evaluated as an ES module, one-shot, at
load_mod/loadMod/LoadModtime — the same call site that evaluates a classic script in script mode. - Evaluation happens in the same sandbox realm and global as a script-mode mod. Bindings, hooks,
console, thexriptglobal, the exports surface, and the fragment API are installed onglobalThisbefore the module is evaluated. A module sees the identical ambient environment a script sees: the host bindings as globals/namespaces,hooks,console, andxript. - Top-level code runs exactly once. Side-effecting registration (
hooks.fragment.update(...),xript.exports.register(...)) is legal inside a module and behaves identically to script mode. - Top-level
awaitis permitted where the runtime’s module evaluator supports it. A module that never settles its top-levelawaitis a load-time error, mirroring the existing “workflow promise never resolved” handling. - A failed module instantiation or evaluation (syntax error, top-level throw, unresolved import) surfaces as the same load-time error the script path uses today — never a silent no-op.
Module-vs-script is a manifest fact (entry.format) read by the loader; the host calls the same load entry point in both modes. The single entry script (entry.script) is the v1 baseline: exactly one module source is evaluated. Multi-module / array entry is out of scope for module mode in v1.
Runtime evaluators
Section titled “Runtime evaluators”- rust (
xript-runtime): rquickjsModulecompile + eval; drive pending jobs to settle, then catch. - js (
@xriptjs/runtime): only the async sandbox (createSandboxAsync) supports module evaluation. The sync sandbox (createSandboxSync) must reject a module-format entry with aModuleUnsupportedError(“module-format mods require the async sandbox”) rather than silently evaluating it as a script. - node (
@xriptjs/runtime-node):node:vmSourceTextModulewith a deny-all link callback, thenmodule.evaluate(). - csharp (
Xript.Runtime): JintEngine.Modules.Add+Engine.Modules.Import, with a module loader that rejects external specifiers.
Top-Level Exports Become Host-Invokable Exports
Section titled “Top-Level Exports Become Host-Invokable Exports”After a module evaluates, the runtime reads its top-level named function exports and registers each into the same export registry that xript.exports.register feeds. A function exported as transcribe becomes invokable via the unchanged invoke_export('transcribe', args) / invokeExport / InvokeExport path. No xript.exports.register call is required in module mode.
- Script mode keeps using
xript.exports.register. Both paths coexist and merge into one registry. - Collision rule: if a top-level export and an explicit
register()call share a name,register()wins — it is an explicit imperative act and runs after the export binding is established. - Only function-valued named exports are harvested. Non-function named exports (
export const VERSION = "1.0") are ignored for invocation purposes (they are data, not callables) and do not error. - The default export is not harvested — exports are addressed by name and a default export has no stable invocation name.
- Capability gating is unchanged:
entry.exports[name].capabilitygatesinvoke_exportby name via the host-side export-capability map, regardless of whether the export came from a module binding or aregister()call. Audit emission is identical for both origins.
entry.exports in the manifest documents and capability-gates these exports. It need not enumerate them for invocation to work; the runtime is authoritative for what actually registered. It SHOULD still enumerate them for typed authoring and docs.
Imports Are Default-Deny; Approved Libraries Lift It
Section titled “Imports Are Default-Deny; Approved Libraries Lift It”A module-format mod is a self-contained entry module whose imports are denied by default. Every import specifier is rejected at link/instantiation time, before any top-level code runs, unless the specifier names a library the host has approved (see Approved Libraries). That covers bare specifiers ("fs", "lodash"), absolute, URL, and (in v1) relative alike. This preserves security guarantee #1 (no sandbox escape) and the eval ban on dynamic import().
- Static
import x from "..."of an unapproved specifier fails at link time. - Dynamic
import("...")fails at call time regardless of approval (already covered by the no-eval / dynamic-import ban; module mode does not re-enable it, and the library lift applies only to static imports). - The rejection is a load-time error with a stable, cross-runtime identity: error name
ImportDeniedError, with.specifierset, and a message of the form:import of "<specifier>" is not permitted; xript mods cannot import external modules (see security guarantee: no sandbox escape).
Relative intra-mod imports are denied in v1: the single-entry self-contained module is the baseline.
Approved Libraries
Section titled “Approved Libraries”The host manifest’s top-level libraries map is a curated allow-list of importable libraries — the capability model applied to modules. Default-deny is intact: the host curates which libraries exist, and a capability gates which mods may import each.
// host manifest{ "libraries": { "@example/doc": { "description": "Shared markdown + doc rendering.", "capability": "lib.doc", "version": "^1.0.0" } }}// host wiring — the host ships the implementation; xript provides the link pathcreateRuntime(manifest, { hostBindings, libraries: { "@example/doc": docModuleSource } });// mod entry — a real static import, linked in-sandboximport { renderMarkdown } from "@example/doc";export function render(md) { return renderMarkdown(md); }Semantics
Section titled “Semantics”- Resolution order at link time: the specifier must (1) be declared in the resolved host manifest’s
libraries, (2) have source registered by the host at runtime construction, and (3) pass the capability gate — the granted set must satisfy the library’scapabilityunder the v0.7 subsumption rules (lib⊇lib.doc). A miss at any step is a load-time error: undeclared →ImportDeniedError; declared-but-unregistered →LibraryUnavailableError(the host forgot to supply the source — a host bug, named as such); capability denied →CapabilityDeniedErrornaming the specifier and the missing capability. An ungated library (nocapability) skips step (3). - In-sandbox execution: an approved library evaluates inside the sandbox, at the importing mod’s own privilege. It sees the same ambient environment the mod sees and crosses no marshalling boundary — imports are full-fidelity object/function references, not JSON. Approving a library grants the mod no new power; it is the host vouching “this is sandbox-safe shared code I choose to offer.”
- One instance per runtime: a library module is instantiated once and shared by everything that imports it in that runtime, standard ES module semantics.
- Import-clean rule: an approved library must be a self-contained, pre-bundled ES module with no imports of its own. Library source is run through the same default-deny loader — a library importing an unapproved specifier fails exactly as a mod would, and runtimes MUST reject a registered library whose source contains static
import/export … from/ dynamicimport(forms at registration time, using the same conservative-detector posture as the CommonJS guard (false positives in strings/comments are accepted). This stops the import-deny from being laundered through a library’s dependency tree. - CommonJS guard applies: registered library source is subject to the same
CommonJSDetectedErrordetector as mod entries. - Capability declaration integrity: a library’s
capabilityscope must be declared in the manifest’scapabilitiesmap, the same rule that governs binding and slot gates.
Runtime requirements
Section titled “Runtime requirements”Libraries link at module-entry evaluation, so they ride the module path end to end. In the universal JS runtime that means the async sandbox: initXriptAsync + loadModAsync. The sync sandbox rejects module-format entries outright (ModuleUnsupportedError), and there is no other import site — a host on the sync path adopts the async runtime first, then adds libraries. The Node, Rust, and C# runtimes evaluate modules on their standard load paths, so no equivalent split exists there.
What stays host-side
Section titled “What stays host-side”The library lift is for pure compute — markdown rendering, date math, validation, formatting: code that needs no privilege beyond the sandbox. Anything touching host state, the filesystem, or the network stays a host binding (host-side execution, JSON boundary, per-function capability). The two columns are complementary, not competing; a host typically offers both.
CommonJS Is Never Supported
Section titled “CommonJS Is Never Supported”CommonJS is not a supported module format in any mode. Runtimes and the validator must detect CommonJS artifacts in a mod entry and fail loudly with a fix-it message, rather than producing a mod whose exports silently never register (the failure mode when a misconfigured tsconfig emits CJS: require/module are undefined in the sandbox and the top-level code throws an opaque ReferenceError, or an exports.foo = ... assignment mutates a stray global and registers nothing).
Canonical detector
Section titled “Canonical detector”The detector is conservative — it favors over-rejection over a silently-broken mod. It flags an entry source when any of the following appear at any position:
require(— the CommonJS require call form.module.exports— the CommonJS module-exports assignment or reference.exports.<ident> =orexports[— a top-level CommonJS named-export assignment.
The reference match is the union of these case-sensitive patterns:
/\brequire\s*\(//\bmodule\s*\.\s*exports\b//\bexports\s*\.\s*[A-Za-z_$][\w$]*\s*=//\bexports\s*\[/
False positives inside string literals or comments are accepted: a mod flagged as CJS that was actually fine is a one-line author fix, whereas a CJS mod that loads silently broken is the exact bug this guard kills. Runtimes MUST NOT build a full tokenizer to avoid over-rejection.
The check runs before module/script evaluation, in both script and module mode (a script-mode entry compiled to CJS is equally broken).
Error identity
Section titled “Error identity”Error name CommonJSDetectedError, with .artifact set to the matched form (require(), module.exports, or exports.x), and a message that points at the ESM/script-mode fix and the authoring guide:
CommonJS artifacts detected in mod entry (found: <artifact>). xript mods must be authoredas ES modules (entry.format: "module", top-level export) or as classic scripts usingxript.exports.register — never CommonJS. Fix your tsconfig to emit ESM (module: "esnext",moduleResolution: "bundler"/"nodenext") or remove the require()/module.exports usage.See https://xript.dev/spec/modules/.Two enforcement homes
Section titled “Two enforcement homes”Both ship, by design:
- Validate-time (early, author/CI-facing): when the entry source is reachable, the validator raises a hard error with keyword
commonjs-detectedon/entry. This is the primary fix-it surface — it catches a misconfiguredtsconfigat author time. - Runtime load-time (late, host-facing): the runtime runs the same detector before evaluation as defense-in-depth, so a misconfigured
tsconfigcan never silently break — even for a mod the validator never saw.
Authoring Mods in TypeScript
Section titled “Authoring Mods in TypeScript”A TypeScript mod must compile to an ES module the runtime can evaluate. The rules:
- Compile to ESM. Set
module: "ESNext"(or"NodeNext") andmoduleResolution: "Bundler"(or"NodeNext") intsconfig.json.module: "Node16"with a default package can emit CJS-shaped output — the exact footgun the CommonJS guard exists to catch. - Use top-level
exports for invokable functions.export function transcribe(...)is registered automatically; noxript.exports.registercall is needed. Declare each export in the mod manifest’sentry.exportsfor typed authoring, docs, and capability gating. - Import only approved libraries. Imports are default-deny; the only specifiers that link are the ones in the host’s
librariesallow-list (gated by their declared capability). For everything else, use host bindings (available as ambient globals) instead of pulling in packages. - Never CommonJS. No
require(...), nomodule.exports, noexports.x = .... These fail loudly at both validate and load time. - Type against the ambient surface. Generate an ambient declaration file with
xript typegen --ambientfrom the mod (and optionally the host) manifest, and reference it fromtsconfigtypesor a triple-slash directive. This types thexriptglobal, the host bindings,hooks, and the mod’s ownExports.
A minimal module-mode mod entry:
/// <reference path="./xript-env.d.ts" />
export function transcribe(audioUrl: string): string { log("transcribing " + audioUrl); return "transcript of " + audioUrl;}
hooks.fragment.update("transcript-panel", (bindings, fragment) => { log("fragment updated with: " + JSON.stringify(bindings));});Related Documents
Section titled “Related Documents”- Security: module syntax is allowed, but imports are denied and CommonJS is loudly rejected — guarantees #1 (no sandbox escape) and #4 (no eval) are strengthened, not weakened.
- Bindings: defines the runtime error vocabulary, including
ImportDeniedErrorandCommonJSDetectedError. - Fragments: the inert-fragment model that module-mode mods contribute to.