Mutualized: the implementation no longer lives under
packages/front/fw/tools/build/bundler/— it was ported logic-verbatim to the repo-leveltools/fw-bundlerpackage (tools/fw-bundler/src/bundle/), consumed here as a devDependency (@awacloud/tool-fw-bundler) and invoked via itsfw-bundler bundlebin.
Purpose
Zero-dependency prebuild orchestrator based on Bun.build. Produces minified/unminified ESM bundles for the presets and side-bundles declared in fw.config.json, as well as sanity.min.js.
CLI usage
bun run prebuild # everything (presets + side-bundles + sanity)
bun run prebuild:presets # all presets (no side-bundles)
bun run prebuild:side-bundles # all side-bundles
bun run prebuild:minimal # a specific preset
bun run prebuild:core
bun run prebuild:site
bun run prebuild:site-interactive
bun run prebuild:spa
bun run prebuild:pwa
bun run prebuild:sanity # only sanity.min.js
Direct form:
fw-bundler bundle [target] [flags] # from the package root
target can be:
- A preset name (
minimal,core,site, …) - A side-bundle name (
crypto-basic,realtime, …) presets— all presets onlyside-bundles— all side-bundles onlyall(default) — everything
Flags
| Flag | Effect |
|---|---|
--dev |
ENV.DEV = true and preserves /* dev_only */ blocks. |
--no-log |
ENV.LOG = false — no main logger. |
--no-sanity-log |
Patches sanity/base.js with LOG_ATTEMPTS = false (silent in production). |
--sanity-only |
Builds only dist/build/sanity.min.js. |
--no-sanity |
Does not emit sanity.min.js. |
--config <path> |
Overrides the path to fw.config.json. |
--classic |
Forces emission of the classic variant on all targets in the run (union with the config's variants). |
--endpoint <name> |
Name of the global property attached by the classic variant (overrides config; default fw). |
--backend <name> |
Build backend: bun (default under bun), esbuild or rollup (Node-capable). Auto-selected if omitted. |
Build backends (bun by default, or Node)
The builder drives Bun.build/Bun.gzipSync via a selector (lib/bundler.js) with a single contract. Three backends:
| Backend | Runtime | Selection |
|---|---|---|
bun |
bun | default under bun |
esbuild |
Node (recommended) | --backend esbuild / FW_BUILD_BACKEND / auto if bun absent |
rollup |
Node (alternative, + terser) | --backend rollup |
To produce dist/build/* under pure Node (without bun):
bun run setup:e2e # installs esbuild (and rollup/terser) on demand
bun run prebuild:node # = node ../../../tools/fw-bundler/src/bundle/index.js --backend esbuild
# rollup alternative:
node ../../../tools/fw-bundler/src/bundle/index.js --backend rollup
Under Node, target
index.jsexplicitly (node .../bundle/index.js), not the directory — otherwise theisMainModuleguard does not executemain(). Under bun, the declaredfw-bundler bundlebin is enough. Details:../dev/runtime.md, backends esbuild / rollup.
Configuration
The configuration lives in fw.config.json. Single source of truth, consumed by the self-contained build and by the Vite plugin — no risk of drift.
Shape:
{
"defaults": {
"variants": ["pure"]
},
"presets": {
"minimal": {
"description": "...",
"modules": []
},
"core": {
"description": "...",
"modules": ["errors", "eventBus", "..."]
},
"site": {
"extends": "core",
"modules": ["dom", "events", "..."],
"variants": ["pure", "classic"]
}
},
"sideBundles": {
"realtime": {
"description": "...",
"modules": ["ws", "sse", "webrtc"]
}
}
}
Cascading via the extends field (cycle-detected). The preset inherits the parent's modules and merges them (deduplication + stable order).
Variants: pure or classic. By default, only pure is emitted. For a preset/side-bundle to also emit the classic variant, explicitly declare "variants": ["pure", "classic"] on the relevant entry — or globally via defaults.variants.
Endpoint: the classic variant attaches the framework to globalThis.<endpoint> (default fw). Configurable at three levels, from highest to lowest priority:
- CLI
--endpoint <name>(global run override); endpointper entry (presets.<name>.endpoint/sideBundles.<name>.endpoint);defaults.endpoint.
The name must be a valid JS identifier (dot-access in generated code). Example config:
{
"defaults": { "variants": ["pure"], "endpoint": "fw" },
"presets": {
"site": { "extends": "core", "modules": ["dom"], "variants": ["pure", "classic"], "endpoint": "awa" }
}
}
pure vs classic variants
| Variant | Effect |
|---|---|
| pure (default) | export default fw; — no side effects. The app imports the bundle as ESM. |
| classic | Additionally: if (!globalThis.<endpoint>) globalThis.<endpoint> = fw; — useful for <script type="module" src="..."> when you want to access the framework from non-module code. <endpoint> = fw by default, configurable (see above). |
Side-bundles follow the same logic: pure exports default _mods (the descriptor array) + install(runtime) + modules; classic self-installs onto globalThis.<endpoint>.runtime via registerAllDeep (same endpoint as the preset loaded upstream).
Outputs
Everything is written to dist/build/. Naming convention:
| Variant | Preset | Side-bundle |
|---|---|---|
pure |
fw.<preset>.pure.min.js (+ .pure.js) |
fw.pack.<name>.pure.min.js (+ .pure.js) |
classic |
fw.<preset>.min.js (+ .js) |
fw.pack.<name>.min.js (+ .js) |
Each bundle has a paired .meta.json with: description, modules listed, env, bytes (min/gz), sha256, builtAt.
Architecture
fw.config.json # source of truth — pure data (package root)
tools/fw-bundler/src/bundle/ # repo-level, mutualized — @awacloud/tool-fw-bundler
├── index.js # CLI orchestrator (dispatched via the `fw-bundler bundle` bin)
└── lib/
├── config.js # load fw.config.json + cascade extends + validate
├── entry-gen.js # generates the entry from scratch (preset + side-bundle)
├── validate-deps.js # invariants: deps↔dependencies, modules.js consistency, dep-string lookup
├── strip-dev.js # mirrors src/ with dev_only blocks removed
└── bundler.js # wrapper over Bun.build
fw.config.json lives at the package root (alongside package.json, like tsconfig.types.json) — it is cross-cutting editorial data, consumed by the bundler and the Vite plugin, not owned by a single tool.
The shared scanner tools/fw-bundler/src/modlib/scan-modules.js is the source of truth for module discovery: all tools (bundler, validate-deps, standalone) consume it, never src/core/modules.js directly (the latter serves the self-contained mode).
Pipeline
- Load
fw.config.json+ scansrc/(single walk). - Validate: source-tree invariants (3 checks from
validate-deps.js) + preset/side-bundle references. - Strip-dev (production only): mirror
src/→dist/_tmp/_src/with/* dev_only */blocks removed. - Sanity: emits
sanity.min.js. - Assets: copies
brotli_dict.binand other binary assets. - Presets: for each preset × variant, generates the entry from scratch and bundles.
- Side-bundles: same.
Customising
Adding a module to a preset
Edit fw.config.json, add the name to the preset's modules array. The name must exist in src/ (a module-shaped export const X = { name, factory, ... }). If the reference is invalid, the build fails at the loadConfig phase:
[config] preset "site" references unknown modules: foo, bar
Creating a preset
Add an entry in fw.config.json#presets, optionally with extends: "<parent>". Add a prebuild:<name> script in package.json (optional — fw-bundler bundle <name> already works).
Adding a side-bundle
Add an entry in fw.config.json#sideBundles: { "description": "...", "modules": [...] }. Variants inherit from defaults.variants unless explicitly overridden.
Editorial pruning
With the deps field (cf. docs/guide/module-pattern.md) and registerAllDeep, presets no longer need to list the transitive closure: only the top-level conceptual modules are needed. The bundler pulls in deps via each module's JS imports. You can freely shorten existing lists.
Technical constraints
- Zero npm dependencies — only
Bun.build,Bun.gzipSync,node:fs,node:path,node:crypto. - Browser target (
target: 'browser',format: 'esm'). - External source maps (
.map) for preset and pack bundles. sanity.min.jsis a classic script (no module syntax).
See also
standalone.md— self-contained ESM bundle for an individual fw module.precompilation.md— HTML →ParseResultJSON precompilation.aot.md— AOT HTML → imperative JS factory compilation.../guide/integration-bundlers.md— self-contained vs Vite/Webpack.../../integrations/vite/README.md— Vite plugin.