Package
This guide covers the clientx build pipeline, the dist/ output layout, and the difference between single-resource and composed (dependency closure) builds.
1. clientx build
CLI entry points:
node scripts/clientx.mjs build # default: client+userx+msgx+agix+conn+e2ee
node scripts/clientx.mjs build --resources sitex # only sitex (closure = sitex)
node scripts/clientx.mjs build --resources hello,client # custom resource + dependencies
node scripts/clientx.mjs build --no-clean # keep dist (default: clean build for determinism)
node scripts/clientx.mjs --help # help
--resources is the list of resource ids you want to enable. The builder resolves the transitive dependency closure (resolve-closure.mjs), includes dependencies, and builds them in topological order.
2. Build pipeline
- shared-vendor →
dist/assets/shared-vendor.js: bundles thereactfamily (react / react-dom / react-router / react-query / zustand / sonner / next-themes) into a single instance. - vendor-i18n →
dist/assets/vendor-i18n.js: bundlesi18next+react-i18nextseparately (i18next’s default export conflicts with React). - vendor-packages →
dist/vendor/{runtime,common}/**: emits the two@clientxpackages withpreserveModulesfor precise importmap subpath resolution. - build-resource →
dist/resource/<id>/**: builds each resource in the closure, dependencies first. Resource code keeps bare imports (react/@clientx/.../@/resource/<dep>/...) resolved by the importmap. - build-app →
dist/index.html+dist/resource/{manifest,importmap}.json: builds core-app, writes the closure manifest and importmap, and inlines the importmap intoindex.html’s<head>.
3. Output layout
dist/
├── index.html # core-app entry (inline <script type="importmap">)
├── assets/
│ ├── shared-vendor.js # single React family instance
│ ├── vendor-i18n.js # single i18n family instance
│ └── ... # remaining core-app chunks
├── vendor/
│ ├── runtime/** # @clientx/runtime modules
│ └── common/** # @clientx/common modules
├── resource/
│ ├── manifest.json # closure: { resources: [{ id, entry, deps }] }
│ ├── importmap.json # bare specifier → URL mapping
│ ├── <id>/register.js # resource entry (loaded dynamically)
│ ├── <id>/** # resource modules
│ └── <id>/i18n/zh.json # locale bundle
└── downloads/** # only when portal is in the closure
manifest.json is the single source of truth for runtime loading (see the Assemble guide):
{
"resources": [
{ "id": "sitex", "entry": "/resource/sitex/register.js", "deps": [] },
{ "id": "portal", "entry": "/resource/portal/register.js", "deps": ["sitex"] }
]
}
Note: deps in the manifest are resource ids (resolved at build time), not the kind in Resource.dependsOn (such as site). portal declares dependsOn: ['site'] as a kind-level constraint; the built closure dependency is derived from source imports (@/resource/sitex/...) and is ["sitex"].
importmap.json is the key to the single-instance guarantee: the react family maps to shared-vendor.js, every @clientx/runtime|common subpath maps to vendor/<pkg>/<sub>.js, and cross-resource subpaths map to /resource/<dep>/<sub>.js.
4. Single resource vs composition
Single resource
node scripts/clientx.mjs build --resources sitex
Closure = sitex (plus the @clientx packages it imports, which go into vendor/). dist/resource/ contains only sitex/. Useful for a single pluggable unit or to verify that a resource stands alone.
node scripts/clientx.mjs build --resources hello,client
The custom hello resource depends on client, so the closure is [client, hello] (dependency first). A missing dependency fails loudly: build-app verifies that resources statically required by the app frame are in the closure.
Composition (default app / website)
node scripts/clientx.mjs build # default app: client+userx+msgx+agix+conn+e2ee
node scripts/clientx.mjs build --resources client,sitex,portal # website: app shell + site framework + site
Key points:
- Resources stand alone:
--resources portalproduces[sitex, portal](sitex is portal’s declared dependency) and does not requireclient. The marketing home, downloads and changelog only need sitex’s content model and block registry. - The full website needs client: build-app statically depends on
@/resource/client/*(before the app frame is fully decoupled), so a full site build must includeclient. - Optional resources stay out of the default build:
sitex/portalare not inDEFAULT; if you do not list them, they are not bundled, declared or downloaded.
5. Download artifacts
scripts/p3/package-downloads.mjs writes distributable artifacts to resource/portal/downloads/ (copied into dist/downloads/ at build time and served from /downloads/):
node scripts/p3/package-downloads.mjs
clientx-core-prebuild.tar.gz— run the defaultclientx buildonce, then package the wholedist/.clientx-core-source.tar.gz— toolchain source.resource-<id>-source.tar.gz— built-in resource source (auto-discovered fromsrc/resource/directories containingregister.ts).resource-hello-example.tar.gz— source generated byclientx create hello --deps client.downloads-manifest.json— file/size data for the download cards.downloads-meta.json— per-artifact title/description (SEO) used by the cards.
6. Next steps
- Assemble guide: how the core runtime loads from manifest and how
clientx runworks. - Case study: the full
helloresource from source to package.