ES modules
Cloudpack uses modern ECMAScript modules (ES modules or ESM) for its output and provides an import map for path resolution in the browser.
The first section of this page assumes you're already familiar with ES modules and dives straight into why and how they're used in Cloudpack.
If you're not familiar with ES modules and how they compare to other formats such as CommonJS (CJS), the second section provides details about different module formats and their history. Briefly:
- CJS is the format historically used by Node, with
require()for imports andmodule.exportsfor exports. It does not work in browsers. - ESM is the modern standard for both browsers and Node, and uses
importandexportkeywords. (ECMAScript is the language specification for JavaScript, and the module specification was added in 2015.) - TypeScript uses the
importandexportkeywords, but the final module format is abstracted away by the compiler and/or bundler.
ES modules in Cloudpack
Cloudpack uses ES modules as its output format. This choice is fundamental to its speed and caching strategy.
Why ES modules?
ES modules provide several advantages for Cloudpack's approach:
- Fast re-bundles - Only re-bundle the changed package, not the world
- Native browser support - Modern browsers can load ES modules directly via
<script type="module">, eliminating the need for a module runtime - Import maps - Import maps (
<script type="importmap">) translateimportpaths directly to URLs without an extra transform step - Granular browser caching - Each module can be cached independently in the browser, enabling fine-grained cache invalidation
- Static analysis - All imports and exports must be declared upfront, enabling tree shaking and predictable bundling
Library mode output
In library mode, Cloudpack creates one ESM bundle per entry point per package:
node_modules/react/
├── package.json (exports: { ".": "./index.js", "./jsx-runtime": "./jsx-runtime.js" })
└── ...
↓ Cloudpack bundles ↓
~/.cloudpack/react-18.0.0-v0-abc123/index.js
~/.cloudpack/react-18.0.0-v0-abc123/bundled/jsx-runtime.jsEach entry point defined in the package's exports map becomes a separate bundle. If features.removeUnusedExports is enabled, unused entry points are omitted. Common code shared between entry points is automatically extracted into shared chunks to avoid duplication.
During cloudpack start, Cloudpack generates an import map with a mapping from package import paths to bundle server URLs, which the browser uses to translate imports like import React from 'react' into URLs.
Handling non-ESM packages
Many npm packages still publish CommonJS, not ES modules. Cloudpack handles this through:
- Bundling - Bundlers like Rollup can convert many CJS patterns to ESM
- ESM stubs - Cloudpack generates a wrapper file with the CJS module's exported names, since it's not always possible to determine these through static analysis
See Bundling: ESM stubs for details on how this works.
Best practices
To get the best experience with Cloudpack:
- Prefer ESM - When publishing packages, include ESM output (
"type": "module") - Use exports maps and avoid deep imports - Define entry points in your
package.jsonexports field, and avoid importing from arbitrary internal paths - Follow other exporting best practices
- Use type imports - Use
import typefor TypeScript types to avoid unnecessary bundling - Use recommended tsconfig settings - Use TypeScript settings that help enforce good practices
For troubleshooting issues with migrating a repo to use ES modules, see ES module issues.
Module formats and history
ECMAScript is the language specification for all versions of JavaScript (as used by both browsers and Node), but "ES modules" specifically refers to the module format introduced in the 2015 version of the specification.
Before the ES module spec was finalized, Node, browsers, and TypeScript had taken different approaches to implementing or simulating modules. ES modules are usable today in both browsers and Node, but it took nearly a decade to reach that point (and there are still some nuances), so some of the previous formats are still widely used and worth understanding.
Node: CommonJS modules
In Node.js, modules were initially implemented using the CommonJS (CJS) format. require() is used for imports, and exports are defined by assigning to module.exports (or simply to exports).
// File: func.js
const foo = require('foo')
const { bar } = require('./bar')
function func() {
console.log(foo, bar)
}
// could also assign an object or primitive, or assign to module.exports.whatever
module.exports = func
// File: index.js
const func = require('./func')
func()require() resolves the given string to a path on the local filesystem: either using file locations for relative paths, or searching up through installed node_modules for package names. Neither of these approaches work for the browser, since there's no local filesystem.
An unfortunate feature of CJS modules (from Cloudpack's standpoint) is that module imports and exports can be declared in a massive variety of ways, including with conditional runtime logic:
// Please don't do this
const foo = process.env.FOO ? require('foo1') : require('foo2')
if (process.env.WHATEVER) {
module.exports.foo = 'foo'
} else {
module.exports.bar = require('bar')
}
module.exports[foo()] = 'umm'Browsers: no native modules
Originally, browsers didn't support JS modules at all, just synchronously-loaded <script> tags which ran in the global context. Various formats were invented to emulate modules, such as asynchronous module definition (AMD) (note that "today" in the link refers to 2015ish) and universal module definition (UMD). Generally you can recognize these by references to define().
AMD and UMD patterns are still seen in 2025, most often as bundle output generated by tools such as webpack, but you should not use them directly in new code!
CJS modules are sometimes used as an intermediate format for code that's primarily intended to run in the browser, but will be bundled (such as by Webpack) before running.
Introducing ES modules
The 2015 version of the ECMAScript specification (also called ES2015 or ES6) introduced proper support for modules, which use the import and export keywords:
// File: func.js
import foo from 'foo'
import { bar } from './bar'
export default function func() {
console.log(foo, bar)
}
// File: index.js
import func from './func.js'
func()In Node, imported paths are resolved on the filesystem using a similar algorithm as with CJS. In the browser, an entry point ES module is loaded with <script type="module">, and the new feature of import maps facilitates import path resolution without a filesystem.
Unlike in CJS modules, all export names must be statically defined, and standard imports are hoisted to the top of the file. This makes things much easier for a bundler to analyze.
Although ES modules are "the future," it's taken the better part of a decade for native support in either browsers or Node to become widespread enough that the format is directly usable (and there are still some quirks and nuances). So it's still very common to see CommonJS for Node, and various legacy formats for browser bundle output.
TypeScript
TypeScript uses the import/export keywords with syntax similar to ESM, and its module setting allows transpilation to a variety of output script/module formats.
Unfortunately, TS initially implemented the import/export keywords before the ES2015 spec was finalized, so there are a few differences in the original approach that continue to cause headaches. You can choose between the original approach and the spec-compliant approach with the esModuleInterop flag. For more details about issues and migration, see the ES module issues docs.