ES modules
Cloudpack uses modern ECMAScript modules (ESM) for its output and provides an import map for path resolution in the browser.
This page outlines the history of module formats including ESM and CommonJS (CJS), and explains Cloudpack's choice of ES modules.
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.
Related reading
- More context on ESM vs CJS formats and history
- Deep dive about CJS/ESM interop problems
- TypeScript's module options and syntax reference
- Node's modules reference
ES modules in Cloudpack
WARNING
This section is a work in progress.
Cloudpack internally uses ES modules for its output in either library mode (per-package bundles) or optimized/production mode. This approach is key to the speed increases and minimal re-bundling in library mode.
In library mode, one ESM bundle is created for each entry point path that's defined in each package's exports map (omitting any unused paths if features.removeUnusedExports is enabled), with common chunks created automatically as needed.
To resolve import paths in the browser, Cloudpack provides an import map based on the dependency graph and known export paths. Each import path points to a URL under the bundle server, and hitting this URL will trigger bundling the package if needed or return the previously-bundled version. This allows us to re-bundle at a very granular level when the user makes a code change.