Polyglot
Languages Web (HTML / CSS / JS) modules
Web (HTML / CSS / JS) § modules

Tooling and bundlers

JavaScript admits ES Modules — the standardised module system since ES2015, supported natively in browsers and in Node.js. Each module is its own file with import and export declarations; modules are singletons (loaded once per origin) and execute strict by default. The principal forms: named exports (export const x = ...), default exports (export default ...), re-exports (export { x } from "./other.js"), dynamic imports (await import(...)). The conventional contemporary discipline: ES modules everywhere, with bundlers (Vite, esbuild, webpack) for substantial production. Older patterns (CommonJS in Node, AMD, IIFE in browsers) are legacy. Packages are managed via npm (or substantial alternatives — pnpm, yarn, bun); imports may resolve relative paths, package names, or import maps. The combination — ES modules as the principal mechanism, the substantial bundler ecosystem, npm as the substantial package registry — is the substance of JavaScript modularity.

Named exports

// math.js
export const PI = 3.14159;

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

export class Calculator {
    // ...
}

// Or batch:
const PI = 3.14159;
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
export { PI, add, subtract };
// main.js
import { add, subtract, PI } from "./math.js";

console.log(add(2, 3));
console.log(PI);

Renaming imports

import { add as plus, subtract as minus } from "./math.js";

console.log(plus(2, 3));

Renaming exports

function _add(a, b) { return a + b; }
export { _add as add };

Importing all

import * as math from "./math.js";

console.log(math.add(2, 3));
console.log(math.PI);

Default exports

// user.js
export default class User {
    constructor(name) { this.name = name; }
}

// Or:
const config = { host: "localhost", port: 8080 };
export default config;

// Or:
export default function (a, b) { return a + b; }   // anonymous
// main.js
import User from "./user.js";                      // any name OK
import config from "./config.js";
import sum from "./sum.js";

const u = new User("Alice");

The default export admits any name on import — substantial convention is to match the file name.

Mixed default + named

// api.js
export default function fetchData() { ... }
export const API_URL = "/api";
export class HttpError extends Error { ... }

// main.js
import fetchData, { API_URL, HttpError } from "./api.js";

Re-exports

// index.js (barrel file)
export { add, subtract } from "./math.js";
export { multiply, divide } from "./more-math.js";
export { default as User } from "./user.js";

// Re-export all named:
export * from "./math.js";

// Re-export with rename:
export { add as plus } from "./math.js";

// Re-export default as named:
export { default as User } from "./user.js";

The barrel pattern admits substantial single import path:

// Without barrel:
import { add } from "./utils/math.js";
import { User } from "./models/user.js";
import { fetchData } from "./api/data.js";

// With barrel:
import { add, User, fetchData } from "./utils/index.js";

(Note: barrels admit substantial bundle size if not tree-shaken.)

Dynamic imports

async function loadFeature() {
    const { default: Feature } = await import("./feature.js");
    new Feature().init();
}

// Conditional:
if (window.matchMedia("(min-width: 768px)").matches) {
    const { setupDesktop } = await import("./desktop.js");
    setupDesktop();
}

// On demand:
button.addEventListener("click", async () => {
    const { showDialog } = await import("./dialog.js");
    showDialog();
});

The import(...) admits runtime loading — substantial code-splitting and substantial conditional loading.

Dynamic import with default

const module = await import("./feature.js");
const Feature = module.default;
const { utility } = module;

// Or destructure inline:
const { default: Feature, utility } = await import("./feature.js");

Top-level await

ES modules admit await at the top level:

// config.js
const r = await fetch("/config.json");
export const config = await r.json();

// main.js
import { config } from "./config.js";              // waits for config to load

The pattern admits substantial async initialisation; treat as side-effecting.

import.meta

Module metadata:

console.log(import.meta.url);                      // "https://example.com/main.js"

// Resolve relative to module:
const url = new URL("./assets/icon.svg", import.meta.url);

// Modern (some runtimes):
const path = import.meta.resolve("./other.js");

HTML and modules

<script type="module" src="/main.js"></script>

<!-- Inline: -->
<script type="module">
    import { setup } from "/setup.js";
    setup();
</script>

<!-- Fallback for non-module browsers (rare in 2026): -->
<script nomodule src="/fallback.js"></script>

The principal facts about <script type="module">:

  • Defer by default — runs after document parsed.
  • Strict by default.
  • Cross-origin requires CORS.
  • Once per URL — second <script> with same src does not re-execute.

Import maps

For bare specifiers in browsers:

<script type="importmap">
{
    "imports": {
        "lodash": "https://esm.sh/lodash@4",
        "react": "https://esm.sh/react@18",
        "./utils/": "/src/utils/"
    }
}
</script>

<script type="module">
    import _ from "lodash";
    import React from "react";
    import { helper } from "./utils/helper.js";
</script>

Import maps admit substantial bundler-free development for substantial small projects.

Package managers

The principal package manager: npm (Node Package Manager). Substantial alternatives: pnpm (efficient disk usage, strict), yarn (alternative), bun (fast).

# Initialise:
npm init -y

# Install:
npm install lodash
npm install --save-dev typescript
npm install -g eslint                              # global

# Update:
npm update
npm outdated

# Remove:
npm uninstall lodash

# Run scripts:
npm run dev
npm run build
npm test

# Audit:
npm audit
npm audit fix

package.json:

{
    "name": "my-app",
    "version": "1.0.0",
    "type": "module",
    "main": "src/index.js",
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "test": "vitest"
    },
    "dependencies": {
        "lodash": "^4.17.21",
        "react": "^18.0.0"
    },
    "devDependencies": {
        "vite": "^5.0.0",
        "typescript": "^5.0.0"
    }
}

The "type": "module" admits ES modules in .js files — without it, .js is treated as CommonJS in Node.

CommonJS (legacy in Node)

// math.js (CommonJS)
function add(a, b) { return a + b; }
module.exports = { add };
// Or:
module.exports.add = add;
exports.add = add;

// main.js (CommonJS)
const { add } = require("./math.js");
const math = require("./math.js");

In a Node project with "type": "module", .cjs files admit CommonJS; .mjs admits ES modules; .js follows type.

For interop:

// In ES modules, importing CommonJS:
import lodash from "lodash";                       // gets module.exports

// In CommonJS, importing ES modules:
const { add } = await import("./math.js");          // dynamic only

Bundlers

In production, the conventional contemporary discipline uses a bundler:

BundlerNotes
ViteModern; ESM-first dev server, Rollup for build. The principal contemporary choice for new apps.
esbuildSubstantial fast; underpins many tools.
RollupLibrary-focused; substantial tree-shaking.
webpackMature; substantial plugin ecosystem.
ParcelZero-config.
bunAll-in-one runtime + bundler.
TurbopackNext.js’s bundler.
RspackRust-based webpack-compatible.

A typical Vite project:

npm create vite@latest my-app -- --template vanilla
cd my-app
npm install
npm run dev

vite.config.js:

import { defineConfig } from "vite";

export default defineConfig({
    server: { port: 3000 },
    build: {
        outDir: "dist",
        rollupOptions: {
            input: {
                main: "index.html",
                admin: "admin.html"
            }
        }
    }
});

Tree-shaking

Bundlers eliminate unused exports — admit substantial bundle size:

// utils.js
export function used() { /* ... */ }
export function unused() { /* ... */ }

// main.js
import { used } from "./utils.js";
used();
// `unused` is dropped from the bundle (if no side effects)

For substantial tree-shaking:

  • ES modules — required (CommonJS does not admit static analysis).
  • No side-effects in module top level — or marked in package.json:
    { "sideEffects": false }

Code splitting

// Static: each `<script type="module">` is one chunk.
// Dynamic: `await import(...)` admits a separate chunk.
const { default: Heavy } = await import("./heavy.js");

Bundlers split:

  • Per route (in frameworks).
  • Per dynamic import.
  • Vendor chunks (bundled separately).

Conditional exports

package.json exports:

{
    "name": "my-lib",
    "exports": {
        ".": {
            "import": "./dist/index.mjs",
            "require": "./dist/index.cjs",
            "types": "./dist/index.d.ts"
        },
        "./utils": "./dist/utils.mjs",
        "./styles.css": "./dist/styles.css",
        "./package.json": "./package.json"
    }
}
import lib from "my-lib";                          // resolves "."
import utils from "my-lib/utils";                  // resolves "./utils"
import "my-lib/styles.css";

The mechanism admits substantial control over what consumers see.

TypeScript and modules

// In tsconfig.json:
{
    "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "Bundler",            // or "Node16"
        "esModuleInterop": true
    }
}

// math.ts:
export function add(a: number, b: number): number {
    return a + b;
}

export type Result = { ok: true; value: number } | { ok: false; error: string };

// main.ts:
import { add, type Result } from "./math.js";     // .js even though source is .ts

// `import type` (no runtime import):
import type { Result } from "./math.js";

Common patterns

Barrel file

// components/index.js
export { default as Button } from "./Button.js";
export { default as Input } from "./Input.js";
export { default as Card } from "./Card.js";

// usage:
import { Button, Input, Card } from "./components/index.js";

Side-effect import

import "./styles.css";                             // CSS injected
import "./polyfill.js";                            // runs immediately

Lazy route

const routes = {
    "/": () => import("./pages/Home.js"),
    "/about": () => import("./pages/About.js"),
    "/admin": () => import("./pages/Admin.js")
};

async function navigate(path) {
    const loader = routes[path];
    const { default: Page } = await loader();
    render(Page);
}

Worker import

const worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module" });

The new URL("./worker.js", import.meta.url) form admits substantial bundler-friendly worker references.

Asset import

With Vite/webpack, asset imports return URLs:

import imageUrl from "./image.png";
import iconUrl from "./icon.svg?url";
import iconHtml from "./icon.svg?raw";

document.querySelector("img").src = imageUrl;

Re-export with rename

// public-api.js
export { _internalThing as PublicThing } from "./internal.js";

Module factory

// db.js
let _db;

export async function getDb() {
    if (!_db) {
        const { openDB } = await import("idb");
        _db = await openDB("app", 1, { upgrade(db) { /* ... */ } });
    }
    return _db;
}

Module-level singleton

// auth.js
class AuthService {
    constructor() { ... }
    login(creds) { ... }
}

export const auth = new AuthService();             // singleton

Plugin pattern

// app.js
const plugins = [];

export function use(plugin) {
    plugins.push(plugin);
    plugin.install?.();
}

export async function start() {
    for (const p of plugins) {
        await p.start?.();
    }
}

// elsewhere:
import { use, start } from "./app.js";
import logger from "./plugins/logger.js";
import telemetry from "./plugins/telemetry.js";

use(logger);
use(telemetry);
await start();

Configuration loading

// config.js
const r = await fetch("/config.json");
const config = await r.json();
export default config;

// main.js
import config from "./config.js";                  // top-level await ensures loaded

Conditional import

let analytics;

if (import.meta.env.PROD) {
    analytics = await import("./prod-analytics.js");
} else {
    analytics = await import("./dev-analytics.js");
}

Re-export and freeze

// constants.js
export const STATUS = Object.freeze({
    ACTIVE: "active",
    INACTIVE: "inactive"
});

// elsewhere:
import { STATUS } from "./constants.js";
console.log(STATUS.ACTIVE);

A note on monorepos

For substantial projects:

ToolNotes
npm workspacesBuilt-in; substantial.
pnpm workspacesSubstantial efficient; substantial discipline.
TurborepoSubstantial caching, parallel builds.
NxSubstantial enterprise; substantial tooling.

package.json:

{
    "workspaces": ["packages/*"]
}
npm install                                        # installs all
npm run build --workspaces                         # all packages
npm run test -w pkg-a                              # one package

A note on registries

Public packages are typically on npm registry (registry.npmjs.org). Substantial alternatives:

  • jsr.io — modern; URL-based imports; first-class TS.
  • deno.land/x — Deno’s substantial registry.
  • GitHub Packages — for private/scoped packages.
  • Verdaccio — self-hosted private registry.

A note on the conventional discipline

The contemporary modules advice:

  • Use ES modules"type": "module" and .js/.mjs/.ts.
  • Use named exports over defaults — substantial more discoverable.
  • Use a bundler (Vite is the conventional contemporary choice) for substantial production.
  • Use TypeScript for substantial type-safe modules.
  • Use barrels carefully — admit substantial bundle bloat if not tree-shaken.
  • Use dynamic imports for substantial code splitting.
  • Use top-level await sparingly — substantial blocks loading.
  • Use import.meta.url for substantial asset/worker references.
  • Pin versions with lockfiles (package-lock.json, pnpm-lock.yaml).
  • Audit dependenciesnpm audit, Snyk, Socket.
  • Use exports in package.json for substantial public API control.
  • Use a monorepo tool (npm workspaces, pnpm, Turborepo) for substantial multi-package.

The combination — ES modules as the principal mechanism, named/default/re-exports, dynamic imports for substantial splitting, the bundler ecosystem (Vite, esbuild, etc.), the npm ecosystem with substantial alternatives, the substantial monorepo tooling — is the substance of JavaScript modularity. The discipline produces well-organised, tree-shakeable, deployable applications.