Modules

How Velyr loads ES modules, how specifiers resolve, and what is not wired into the CLI yet.

Velyr is ESM-first (ADR-006): ES modules are the canonical format, and CommonJS is meant to live in an optional compatibility layer. This page separates what velyr run does today from what exists only as library code or is planned.

What velyr run does today#

velyr run <file> picks the loader from the file extension:

ExtensionBehaviorStatus
.js, .mjsRun as an ES module (run_module_file)Available
anything else (.cjs, .ts, ...)Run as a classic script (run_file), no module syntax, no transformAvailable

Module entries support static import, dynamic import(), top-level await, and import cycles following ESM semantics. These are covered by CLI tests in crates/cli/tests/esm_m3.rs.

main.mjs
import { greet } from "./greet.mjs";

const { default: config } = await import("./config.mjs");
console.log(greet(config.name));
bash
velyr run main.mjs

How specifiers resolve#

Resolution lives in runtime-modules (FsResolver, plus StrategyResolver for bare specifiers). Every module gets a canonical ID: the file:// URL of its real path, so different spellings and symlinks of one file converge to a single module instance.

SpecifierExampleResultStatus
Relative./a.js, ../lib/b.jsResolved against the importing module's directoryAvailable
Absolute path/srv/app/a.jsUsed as given, then canonicalizedAvailable
file: URLfile:///srv/app/a.jsConverted to a pathAvailable
Bare packagelodash-es, @scope/pkg/subnode_modules walk with package.json exports, main or index.js, and self-referenceAvailable
Package imports#internalResolved from the nearest package.jsonAvailable
node:node:fsError: Node compatibility is not linkedPlanned
http: / https:https://example.com/m.jsError: not supported yetPlanned

Bare specifiers go through NpmResolver from compat-npm. It matches the conditions velyr, import and default. The lookup is covered by crates/cli/tests/npm_resolve.rs.

Permissions and loading#

  • The loader (FsLoader) opens only the canonical path of a resolved module and reads it as UTF-8 text.
  • By default velyr run grants import access to the project root: the directory of the nearest velyr.json, or the entry file's directory when there is none. An import outside that root fails with VELYR_PERMISSION_DENIED unless you pass a matching --allow-read. See Permissions.
  • VirtualLoader serves modules from memory. It is for tests and embedders, not reachable from the CLI.

Known limits#

  • The source cache never invalidates: a changed file is served stale until the module host is rebuilt.
  • Resolution checks that a target exists before consulting permissions, so the existence of files outside the grant can be observed through NotFound versus PermissionDenied.
  • A bare specifier that resolves to a CommonJS file (.cjs, or .js in a package without "type": "module") is rejected with an error saying CommonJS needs Node compatibility.
  • The imports key of velyr.json is parsed and validated, and the resolver implements import maps (exact keys, then the longest key ending in /). The CLI does not pass the map to the module host yet, so entries have no effect today. See Dependencies configuration.

TypeScript and JSX#

Experimental

The tooling-typescript crate implements the design from ADR-012. It parses with Oxc, strips types, prints JavaScript with a source map, and caches results on disk. It does not type-check, and it assumes isolatedModules semantics. Syntax errors are reported as file:line:column.

It plugs into the module host as a SourceTransform (ModuleHost::with_transform). The CLI does not register it, so:

  • velyr run app.ts is treated as a classic script and fails on the first type annotation.
  • Importing a .ts file from a .js module is not supported through the CLI.
  • .tsx is recognized by the transform's media type, but JSX configuration (compilerOptions.jsx, jsxImportSource) is parsed in velyr.json and not applied, and the planned tooling-jsx crate does not exist yet.

Until the transform is wired in, run TypeScript through a separate build step.

CommonJS#

Experimental

The compat-commonjs crate provides a require loader (JSON, .js and .cjs, cycles with partial exports, import() from CommonJS) and a transform that lets ESM import a CommonJS file, with default and statically detected named exports. It is covered by engine-level tests only; the CLI does not install it. require() of an ES module is not supported and throws ERR_REQUIRE_ESM.

npm packages#

Experimental

Importing an already-installed, ESM-only package from node_modules works through velyr run, as described above. Installing packages does not: there is no velyr install command. The compat-npm crate contains the building blocks (registry client with integrity checks, semver, dependency resolution, content-addressed cache, lockfile model) but they are not exposed by the CLI. See Node and npm compatibility.

Summary#

CapabilityStatusWhere
ESM entry, static and dynamic import, top-level awaitAvailablecrates/cli/src/run.rs, crates/runtime-core/src/esm.rs
Relative, absolute and file: specifiersAvailablecrates/runtime-modules/src/resolve.rs
Bare specifiers from node_modules (ESM packages)Availablecrates/compat-npm/src/resolve/mod.rs
velyr.json import mapExperimentalresolver implements it; CLI does not pass it
TypeScript strippingExperimentalcrates/tooling-typescript
CommonJS require and ESM interopExperimentalcrates/compat-commonjs
node: modules, remote (https:) importsPlanneddocs/compatibility/node-npm.md