Skip to content

Design: Bundling

Cloudpack bundles your application's dependencies into ES modules for fast development. This page explains the bundling approach, including operation modes, bundler selection, and workarounds for CommonJS packages.

Operation modes

Cloudpack can serve or bundle an application in two modes, each optimized for different use cases.

Library mode (default)

In library mode, each package entry point is bundled as an isolated ES module. This is the default mode for cloudpack start.

How it works

  • Each entry point of each package becomes a separate ESM bundle
  • An import map maps package names to bundle URLs
  • Bundles are created on-demand by the bundle server when the browser imports them
  • Previously bundled packages are served from cache (usually this is a local cache, but we also support a remote cache via cloudpack sync)

For a walkthrough of an earlier version of this concept, see this video.

Benefits

  • Only bundles what's needed for the current page
  • Maximum cache reuse between sessions and across machines
  • Fast incremental updates when code changes
  • Packages can be pre-bundled and shared via remote cache
  • Helps facilitate cross-repo testing via cloudpack link
  • Provides the foundation for future ambitions of improving the dependency update process (not currently implemented and not on our immediate roadmap)

Requirements

  • Works best with packages that have exports maps, so that valid package entry point files can be added to the import map ahead of time.
  • Deep imports to packages without exports maps may require running cloudpack init first to determine which entry point paths are imported and auto-generate exports maps.

Production/optimized mode

While library mode is ideal for development, downloading thousands of small bundles isn't optimal for end users.

Production mode creates optimized bundles for deployment, including bundling and minifying all code, tree shaking, and splitting out common chunks. (As of early 2026, a basic version of this is implemented with cloudpack bundle --mode production using Webpack or Rspack, but it's still a work in progress.)

Usage (run from your app's folder):

bash
# Bundle the app in production mode
cloudpack bundle --mode production

# Locally run the app in production mode
cloudpack start --mode production

--mode development is also available to bundle all dependencies but skip minification and optimization (like Webpack development mode).

Bundler abstraction

Cloudpack is bundler-agnostic. Instead of committing to one bundler, it chooses the best tool for each situation.

Design philosophy

  • Automatic selection - Cloudpack picks the optimal bundler based on the package contents and mode
  • Minimal user config required - Cloudpack's bundler integrations have built-in config to handle the most common scenarios
  • Unified interface - Each bundler integration takes standard Cloudpack options and translates them to bundler-specific config

WARNING

⚠️ Cloudpack intentionally ignores existing bundler config files. ⚠️

Its bundling approach is fundamentally different from a monolithic bundle, so many settings aren't relevant, and our built-in bundler configs handle the most common scenarios.

We recommend trying Cloudpack without any extra config first, then adding to your Cloudpack config and/or enabling built-in capabilities as needed. (Custom bundler capabilities provide an "escape hatch" for advanced scenarios, but please talk to the team first to make sure we haven't covered your scenario another way.)

Available bundlers

Cloudpack's available bundlers and default choices are detailed below.

If you need to change the bundler (either for a specific package or the default), see the bundler config docs.

BundlerDefault forSpeedCJSHMRProd
oriTS and ESM packagesFast!
rollupCJS packagesOK
rspackInternal packages if HMRGood
webpackProd; AMD; Node targetSlow

(CJS = packages with CommonJS; HMR = experimental hot module replacement; Prod = production mode)

Ori

Ori is a new bundler based on esbuild. It's developed by another team at Microsoft, and the code and docs (requires MS github login) are still internal. Ori is at least 30% faster than Rspack, so it's Cloudpack's default choice if possible when bundling in library mode.

The keys to Ori's speed:

  • Provides a native binary with esbuild's Go code, plus Go versions of commonly-used plugins such as CSS. This eliminates the IPC overhead between a native binary and JS plugins, which can be a major bottleneck with esbuild or Rspack in very large repos (the motivation for creating Ori).
  • Runs builds in a reused service process via a JS wrapper, avoiding process startup overhead for each package.

The major downside of Ori for Cloudpack is that its esbuild core doesn't handle CommonJS code well: specifically require() of other packages. For CommonJS, we use Rollup.

If you'd prefer not to use Ori, we recommend setting Rspack as the default bundler override.

Rollup

Rollup excels at producing clean ESM output and handling CommonJS. It also supports HMR. Performance is generally better than Webpack but somewhat worse than Rspack.

For packages with CommonJS entry points, Cloudpack generates an ESM stub to find the export names, then uses that as the entry point for Rollup.

Rspack

Rspack is a Rust-based Webpack-compatible bundler. It's significantly faster than Webpack while maintaining similar configuration and output, and it supports all module formats and Cloudpack features (such as production bundling).

Cloudpack only chooses Rspack by default when the experimental hot module replacement (HMR) feature is enabled, but it's a good alternative for packages relying on Webpack features, or as a default bundler override if you'd prefer not to use Ori.

Webpack

Webpack is the classic highly-capable bundler choice, but also the slowest bundler offered by Cloudpack. It's currently used by default for production bundling and for the rare case of packages that only distribute legacy AMD bundles (Rspack should also work for either of these).

A lot of legacy Webpack config isn't needed with Cloudpack, so please try these alternatives before setting a package to use Webpack:

  1. Bundle with Cloudpack default settings (it will work for many packages)
  2. Check Cloudpack config and built-in capabilities to see if any options cover your scenario
  3. Set PackageSettings.bundler for specific packages that don't work with Cloudpack's default choices
  4. Talk to the team to make sure we haven't covered your scenario another way
  5. If nothing else works, custom bundler capabilities provide an "escape hatch" to apply unavoidable legacy Webpack config. Even in this case, you should try to make the capability work with Rspack for improved performance.

Determining entry points

When available, Cloudpack uses exports maps to determine package entry points. See the link for more details on exports maps.

If a package doesn't have an exports map, Cloudpack will try to determine the entry points based on main and/or module, along with a series of heuristics. Some of these heuristics work during cloudpack start, but cloudpack init can do some more detailed analysis and save the results to cloudpack.generated.json (which should be checked in).

Import maps

An import map is a modern browser feature that tells the browser how to resolve module specifiers in ESM import statements. It goes inside a <script type="importmap"> tag and looks like this (details of the URL format):

json
{
  "imports": {
    // Mapping from package import path to bundle server URL
    "my-app/lib/index.js": "/my-app@1.0.0/h-pending/v0.1/bundled/lib/index.js",
    "react": "https://localhost:5500/react@18.0.0/h-abc123/v0/bundled/index.js",
    "react/jsx-runtime": "https://localhost:5500/react@18.0.0/h-abc123/v0/bundled/jsx-runtime.js",
    "old-library": "https://localhost:5500/old-library@1.0.0/h-def123/v0/bundled/so-old.js"
    // ...and many other packages
  },
  "scopes": {
    // old-library has a different version of react 😱
    "/old-library@1.0.0": {
      "react": "https://localhost:5500/react@16.0.0/h-def456/v0/bundled/index.js"
    }
  }
}

How Cloudpack uses import maps

  1. Cloudpack generates the import map from its internal resolve map of package versions and locations
  2. When the browser requests a page, Cloudpack injects the import map into a <script type="importmap">
  3. When a loaded JS file does import React from 'react', the browser looks up react in the import map to find which URL to request:
    • If the import is in a file under /old-library@1.0.0, it uses scopes['/old-library@1.0.0']['react']
    • Otherwise, it uses imports['react']
  4. Cloudpack's bundle server receives the request, either bundles the package or reads the file from the cache, and returns the bundled JS

ESM stubs

ES module stubs are a workaround for CommonJS packages that can't be statically analyzed.

The problem

CommonJS modules can declare exports in ways that are impossible to analyze without running the code:

js
// Static analysis can't determine these export names
module.exports[computePropertyName()] = value
if (process.env.PROD) {
  module.exports.foo = 'foo'
}
Object.assign(module.exports, require('./other'))

ESM bundles must declare all export names upfront. If a bundler can't detect the exports, it may only provide a default export, breaking code that imports named exports:

js
import { specificExport } from 'cjs-package' // Fails if not detected

The solution

Cloudpack's esm-stub-utilities package detects the actual export names and generates an ESM "stub" file:

js
// Original: node_modules/pkg/index.js
module.exports.default = { value: 'default' }
module.exports[computeName()] = 'computed' // Returns 'foo'
module.exports.bar = 'bar'

// Generated stub: index-stub.mjs
import moduleExport from './index.js'
const { foo, bar } = moduleExport
const defaultExport = moduleExport?.default?.default ?? moduleExport?.default
export default defaultExport
export { foo, bar }

The bundler then processes the stub file, producing a bundle with proper named exports.

Detection methods

Primary method: Run the code in a worker thread with a browser-like environment. This is the only way to achieve near-100% accuracy for dynamic exports, but is not ideal:

  • Higher performance overhead than parsing
  • Some packages may fail in the worker environment
  • Security considerations for untrusted packages

Alternative: Use PackageSettings.unsafeCjsExportNames to either provide hardcoded export names, or attempt detection from TS types included with the package. This also has its issues:

  • Hardcoded names may be unreliable across package updates
  • Types are fairly safe for packages written in TypeScript, but won't always be correct or complete for JS packages with manually-written types

We're considering other alternatives to implement in the future.

Troubleshooting