Modules
Ende uses
ECMAScript modules
as its primary module system, the official standard for JavaScript. You share
code between files with standard import and export statements and run it
directly, with no bundler or build step. This is the same module system browsers
use, so the code you write stays portable across environments.
Ende runs ECMAScript modules only: CommonJS (require and module.exports) is
not supported.
Importing modules
In this example the add function is imported from a local calc.ts module.
export function add(a: number, b: number): number {
return a + b;
}// imports the `calc.ts` module next to this file
import { add } from "./calc.ts";
console.log(add(1, 2)); // 3You can run this example by calling ende run main.ts in the directory that
contains both main.ts and calc.ts.
With ECMAScript modules, local import specifiers must always include the full file extension. It cannot be omitted.
// WRONG: missing file extension
import { add } from "./calc";
// CORRECT: includes file extension
import { add } from "./calc.ts";Dynamic imports
The import statements above are static: the specifier is a string literal and
the module loads before your code runs. You can also load a module on demand
with the import() function, which returns a promise for the module's
namespace:
const { add } = await import("./calc.ts");
console.log(add(1, 2)); // 3Because import() runs at runtime, the specifier can be computed and the module
is only fetched and evaluated when the call executes. This is useful for loading
code conditionally or keeping a rarely used feature out of the startup path:
if (Ende.args.includes("--greet")) {
const { greet } = await import("./greet.ts");
greet();
}
A dynamic import() whose specifier is a string literal is part of the static
module graph and loads without extra permissions. One whose specifier is
computed at runtime is checked against the permission system when it runs: local
paths need --allow-read and remote URLs need --allow-import. See
Security for details.
Import metadata
Inside any module, import.meta exposes information about the current module
and helpers for resolving paths relative to it:
console.log(import.meta.url); // file:///path/to/main.ts
console.log(import.meta.main); // true if this is the entry module
console.log(import.meta.filename); // /path/to/main.ts (local modules only)
console.log(import.meta.dirname); // /path/to (local modules only)
console.log(import.meta.resolve("./data.json")); // resolves a specifier to a URLimport.meta.main is a common way to make a file work both as an entry point
and as an importable library:
export function createServer() {/* ... */}
// Only start the server when run directly, not when imported.
if (import.meta.main) {
createServer();
}filename and dirname are undefined for remote modules. For a full
walkthrough, see the
module metadata tutorial.
Import attributes
Ende supports the with { type: "json" } import attribute syntax for importing
JSON files:
import data from "./data.json" with { type: "json" };
console.log(data.property); // Access JSON data as an object
It's possible to import text and bytes modules too.
import text from "./log.txt" with { type: "text" };
console.log(typeof text === "string");
// true
console.log(text);
// Hello from a text file
text imports
Stable; no flag needed.
import bytes from "./image.png" with { type: "bytes" };
console.log(bytes instanceof Uint8Array);
// true
console.log(bytes);
// Uint8Array(12) [
// 72, 101, 108, 108, 111,
// 44, 32, 68, 101, 110,
// 111, 33
// ]
bytes imports
Still experimental. Enable with the --unstable-raw-imports CLI flag or the
unstable.raw-import option in
ende.json.
A stylesheet can be imported with with { type: "css" }. The import evaluates
to a
CSSStyleSheet,
matching what browsers ship. This is mainly useful for running unmodified
browser module graphs in Ende, such as server-side rendering or testing web
components, where a CSS import would otherwise stop the module graph from
loading:
import sheet from "./styles.css" with { type: "css" };
console.log(sheet instanceof CSSStyleSheet);
// true
Dynamic imports work the same way:
const { default: sheet } = await import("./styles.css", {
with: { type: "css" }
});
Static imports and dynamic imports with a statically analyzable specifier are
loaded as part of the module graph and need no permission. Only a dynamic import
whose specifier can't be analyzed, such as import(base + "styles.css"), reads
the file at runtime and therefore requires read permission (--allow-read).
Ende implements the small slice of the CSSStyleSheet interface that browser
module graphs rely on:
cssRulesreturns the sheet's top-level rules. Unlike the browser, this is a frozen array ofCSSRule(not a liveCSSRuleList), and a fresh array is created on each access.CSSRule.cssTextis the verbatim text of one top-level rule.replace(text)andreplaceSync(text)swap the sheet's contents. As with constructed stylesheets in the browser, top-level@importrules are dropped.- The
new CSSStyleSheet()constructor is available, but itsoptionsargument (media,disabled,baseURL) is not supported.
import sheet from "./styles.css" with { type: "css" };
for (const rule of sheet.cssRules) {
console.log(rule.cssText);
}
sheet.replaceSync("body { color: red; }");
Because Ende has no DOM, a sheet can't be adopted anywhere; the implementation
is backed by the raw CSS text rather than a full CSS object model. cssRules
uses a naive top-level rule split, so the live mutation methods insertRule and
deleteRule are not implemented.
css imports
Still experimental. Enable with the --unstable-raw-imports CLI flag or the
unstable.raw-import option in
ende.json.
Deferred module evaluation
The
TC39 Deferred Module Evaluation proposal
is supported. The import defer syntax loads a module (including its
dependencies) but does not execute its top-level code until you first read a
property from the namespace:
import defer * as expensive from "./expensive.ts";
console.log("startup is fast: expensive.ts has not run yet");
// Touching any property triggers synchronous evaluation
console.log(expensive.value);Use it to defer the cost of modules that are only needed conditionally; for example, error-path code in a CLI tool or feature flags whose implementation is heavy to initialize.
The proposal is still at TC39 Stage 3, so the syntax is considered experimental
and may change. Standard import remains the right default.
WebAssembly modules
Ende supports importing Wasm modules directly:
import { add } from "./add.wasm";
console.log(add(1, 2));
The named exports mirror the Wasm module's exports: functions, memories, and
tables come through as their JavaScript objects, and a global export resolves
to the value it holds rather than the WebAssembly.Global wrapper, matching the
WebAssembly ES module integration.
To learn more, visit WebAssembly section
Data URL imports
Ende supports importing of data URLs, which allows you to import content that isn't in a separate file. This is useful for testing, prototyping, or when you need to programmatically generate modules.
You can create modules on the fly using the data: URL scheme:
// Import a simple JavaScript module from a data URL
import * as module from "data:application/javascript;base64,ZXhwb3J0IGNvbnN0IG1lc3NhZ2UgPSAiSGVsbG8gZnJvbSBkYXRhIFVSTCI7";
console.log(module.message); // Outputs: Hello from data URL
// You can also use the non-base64 format
const plainModule = await import(
"data:application/javascript,export function greet() { return 'Hi there!'; }"
);
console.log(plainModule.greet()); // Outputs: Hi there!
// A simpler example with text content
const textModule = await import(
"data:text/plain,export default 'This is plain text'"
);
console.log(textModule.default); // Outputs: This is plain text
The data URL format follows this pattern:
data:[<media type>][;base64],<data>
For JavaScript modules, use application/javascript as the media type.
TypeScript is also supported with application/typescript. This feature is
particularly useful for testing modules in isolation and creating mock modules
during tests.
Importing third party modules and libraries
When working with third-party modules in Ende, use the same import syntax as
you do for local code. Third party modules are typically imported from a remote
registry and start with jsr:.
import { camelCase } from "jsr:@luca/[email protected]";Ende recommends JSR, the modern JavaScript registry, for third party modules. There, you'll find plenty of well documented ES modules for your projects, including the Ende Standard Library.
Managing third party modules and libraries
Typing out the module name with the full version specifier can become tedious
when importing them in multiple files. You can centralize management of remote
modules with an imports field in your ende.json file. We call this imports
field the import map, which is based on the Import Maps Standard.
{
"imports": {
"@luca/cases": "jsr:@luca/cases@^1.0.0"
}
}With remapped specifiers, the code looks cleaner:
import { camelCase } from "@luca/cases";The remapped name can be any valid specifier. It's a very powerful feature in Ende that can remap anything. Learn more in the configuration dependencies section.
Differentiating between imports or importMap in ende.json and --import-map option
The Import Maps Standard requires two entries for each module: one for the
module specifier and another for the specifier with a trailing /. This is
because the standard allows only one entry per module specifier, and the
trailing / indicates that the specifier refers to a directory. For example,
when using the --import-map import_map.json option, the import_map.json file
must include both entries for each module (note the use of jsr:/@ende/async
instead of jsr:@ende/async):
{
"imports": {
"@ende/async": "jsr:@ende/async@^2026.10.15",
"@ende/async/": "jsr:/@ende/async@^1.0.0/"
}
}An import_map.json file referenced by the importMap field in ende.json
behaves exactly the same as using the --import-map option, with the same
requirements for including both entries for each module as shown above.
In contrast, ende.json extends the import maps standard. When you use the
imports field in ende.json, you only need to specify the module specifier
without the trailing /:
{
"imports": {
"@ende/async": "jsr:@ende/async@^2026.10.15"
}
}Managing dependencies
Now that you understand how Ende resolves and imports modules, see the
Dependency management guide for the day-to-day tasks:
adding and removing packages with ende add / ende remove, pinning versions,
overriding and vendoring dependencies, lockfiles and integrity checking, supply
chain management, publishing your own modules, and using private registries.
Updating versions from the command line
You don't have to edit version numbers in ende.json by hand. To move
dependencies to newer versions, run
ende outdated to see what's behind, then
ende outdated --update to bump them:
ende outdated # list dependencies with newer versions available
ende outdated --update # update them in ende.json
To increment your own package's version field between releases, use
ende bump-version:
ende bump-version patch # 1.4.6 -> 1.4.7 (also: minor, major, or a prerelease)