name: build description: Esposter rolldown build conventions — shared rolldown configs, external list rules, bundle sizes, azure-functions special case. Apply when adding packages, editing rolldown configs, or optimizing bundle sizes.
Build Conventions (Rolldown)
Shared Rolldown Configs
Located in packages/configuration/src/. All library packages import one of:
| Config | Platform | Use for |
|---|---|---|
rolldownConfigurationBrowser |
browser | db-schema, parse-tmx, shared |
rolldownConfigurationNode |
node | db, azure-mock, infra, db-mock |
rolldownConfigurationIsomorphic |
browser + node polyfills | xml2js |
All extend rolldownConfigurationBrowser. Node adds platform: "node". Isomorphic adds @rolldown/plugin-node-polyfills. Use { external } shorthand when no extra entries needed; spread [...external, "extra"] only if the package requires additional externals.
The base browser config enables tsgo: true in the dts() call (uses @typescript/native-preview for fast DTS generation; already in catalog — do not remove).
Global External List
Defined in packages/configuration/src/external/external.ts, exported as external. Used by rolldownConfigurationBrowser (extended by all rolldown configs) and by viteConfiguration.
// packages/configuration/src/external/external.ts
export const external: (RegExp | string)[] = [
// Workspace packages — never bundle sibling packages
/@esposter\//u,
"azure-mock",
"parse-tmx",
"vue-phaserjs",
// @esposter/azure-mock
"@azure/core-http-compat",
// ... (grouped by owning @esposter package, alphabetical package order, alphabetical entries within)
];
Key rules
/@esposter\//ucovers all@esposter/*workspace packages — never add individual@esposter/foostrings.- Non-
@esposter/workspace packages must be listed explicitly (azure-mock,parse-tmx,vue-phaserjs— not covered by the regex). - The external list is a build superset, not a per-package peer-dependency checklist — a package declares only the externalized packages it directly imports at runtime or exposes through its generated
.d.tssurface. - Do not duplicate transitive peers — the package that directly imports a dependency owns the contract. If
azure-mockimports@esposter/db-schemawhich importszod,zodisdb-schema's peer, notazure-mock's. dependenciesget bundled;peerDependenciesare externalized. When a package directly imports a non-workspace package that should not be bundled, put it inpeerDependenciesand ensure it's covered by the shared external list. Exceptions:@esposter/app(root consumer, not a library) and@esposter/azure-functions(overrides external list to bundle almost everything).- Vite builds:
viteConfigurationlives inpackages/configuration/src/viteConfiguration.ts.packages/configuration/vite.config.jsimports it from source; consumers likevue-phaserjsimport it from@esposter/configuration.
Ordering convention
Group by owning @esposter package; sections in alphabetical package-name order; entries alphabetical within each section. Section header comment is the bare package name (// @esposter/db). One exception: a final "Vue framework" group for always-consumer-provided deps not owned by a single package (@vueuse/core, pinia, vue).
Dependency declaration convention
dependencies: direct runtime imports to bundle or auto-install for consumers. Workspace packages imported at runtime usually go here even though the external list keeps their code out of the bundle.peerDependencies: direct runtime or declaration-surface imports that are externalized and must be supplied by the consumer — framework/runtime singletons (vue,pinia), SDKs mirrored in public APIs, Drizzle/Pulumi runtimes, package-plugin ecosystems.devDependencies: build, lint, test, codegen, typecheck tools; test-only packages; packages used only by source types that don't appear in generated declarations.- If a package only needs a dependency because an imported workspace package needs it, don't redeclare it as a peer — let the directly importing workspace package own it.
Auditing external vs peerDependencies alignment
Run from repo root to find any dependencies entry that should be a peerDependency:
// node -e "..." or save as a script
const fs = require("fs"),
path = require("path");
const externalStrings = [
"@azure/data-tables",
"@azure/core-http-compat",
"@azure/core-rest-pipeline",
"@azure/eventgrid",
"@azure/search-documents",
"@azure/storage-blob",
"@azure/storage-queue",
"@azure/web-pubsub",
"@rolldown/plugin-node-polyfills",
"@electric-sql/pglite",
"@pulumi/azure-native",
"@pulumi/pulumi",
"@vitejs/plugin-vue",
"@vueuse/core",
"pinia",
"rolldown",
"rolldown-plugin-dts",
"vue",
"phaser",
"vite",
"vite-plugin-mkcert",
"zod",
];
const externalPatterns = [
/^drizzle-kit/,
/^drizzle-orm/,
/^phaser4-rex-plugins/,
/^unplugin-auto-import/,
/^unplugin-dts/,
];
const skip = new Set(["@esposter/app", "@esposter/azure-functions"]); // intentional exceptions
const pkgsDir = "packages";
for (const dir of fs.readdirSync(pkgsDir)) {
const p = path.join(pkgsDir, dir, "package.json");
if (!fs.existsSync(p)) continue;
const pkg = JSON.parse(fs.readFileSync(p, "utf8"));
if (skip.has(pkg.name)) continue;
const peers = new Set(Object.keys(pkg.peerDependencies || {}));
for (const dep of Object.keys(pkg.dependencies || {})) {
const isExt = externalStrings.includes(dep) || externalPatterns.some((r) => r.test(dep));
if (isExt && !peers.has(dep)) console.log(`${pkg.name}: ${dep} should be peerDependency`);
}
}
Azure Functions Special Case
packages/azure-functions/rolldown.config.ts uses a targeted external list instead of the full global one:
external: [...externalVueFramework, "@azure/functions"],
externalVueFramework(vue,@vueuse/core,pinia) — peer deps of@esposter/shared/@esposter/vue-phaserjs; azure-functions doesn't use Vue so rolldown tree-shakes these out safely.- Everything else is bundled — the runtime provides only
@azure/functions; all other deps (Azure SDKs, drizzle-orm, zod, postgres,@esposter/*) must be in the bundle. - Do not spread the full
externallist here — its/@esposter\//uwould externalize@esposter/db,@esposter/shared, etc., breaking runtime.
Bundle Sizes (Baselines)
| Package | Size | Notes |
|---|---|---|
@esposter/azure-functions |
~4.23MB | Intentionally self-contained |
@esposter/azure-mock |
~35.8KB | Azure SDKs external |
@esposter/configuration |
~4KB | Build toolchain external |
@esposter/db |
~1.09MB | Azure SDKs external |
@esposter/db-mock |
~886B | Re-exports |
@esposter/db-schema |
~64.4KB | Schema only |
@esposter/infra |
~121KB | Pulumi resources external |
@esposter/parse-tmx |
~815KB | xml2js external |
@esposter/shared |
~384KB | Vue/Zod external |
@esposter/xml2js |
~1.12MB | Bundles sax + xmlbuilder2 |
vue-phaserjs |
~34KB | Phaser/parse-tmx external |
Adding a New Package
- Create
rolldown.config.tsimporting the appropriate base config. - Add to
package.json:"build": "pnpm export:gen && rolldown --config rolldown.config.ts". - For any direct dep the consumer should provide, add it to
peerDependenciesAND verify it's covered by the global external list (or add it). Don't add transitive-only peers from imported workspace packages. - Add
src/index.test.tsbundle size snapshot (see testing skill). - Add
test/coveragescripts +vitest,@vitest/coverage-v8,@types/nodetodevDependencies. - Run
pnpm ifrom the workspace root after editingpackage.json.
Dependency Installs
- Use plain
pnpm ifrom the repo root when package manifests change. - Follow
architecture/monorepo-tooling.mdfor install safety rules. - If
pnpm ineeds network access, request approval for plainpnpm irather than changing pnpm store settings.
Workspace Graph
- Use
pnpm depcruise:graphto generatedependency-graph.svg. - It should pipe dependency-cruiser DOT output directly into
graphviz-cli; avoid keeping intermediateMODULES.dot,MODULES.mmd, or generated Markdown wrapper files unless explicitly requested.