UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

208 lines (153 loc) • 9.19 kB
--- name: extends-config description: How the authored config's extends chains work (bos.app.ts preferred; legacy bos.config.json supported), deep merge semantics, resolved config lifecycle, env-specific extends, and canonical field ordering. Use when debugging extends inheritance, configuring per-environment parents, understanding what dev writes vs publish writes, or reasoning about config merging. metadata: sources: "packages/everything-dev/src/merge.ts,packages/everything-dev/src/config.ts,packages/everything-dev/src/shared-deps.ts,packages/everything-dev/src/types.ts" --- > **Config form:** the authored config is `bos.app.ts` (canonical, preferred when both exist). A legacy `bos.config.json` is still supported for older children. Where this doc says `bos.config.json` for the *local authored file*, read "the authored config". The published artifact on FastKV keeps the key name `bos.config.json`. # extends & Config Merging ## extends Field The `extends` field in the authored config (`bos.app.ts`, or legacy `bos.config.json`) specifies a parent config to inherit from. Supports two forms: ### String (all environments use same parent) ```json { "extends": "bos://dev.everything.near/everything.dev" } ``` ### Object (per-environment parent) ```json { "extends": { "development": "bos://dev.everything.near/everything.dev", "production": "bos://dev.everything.near/everything.dev", "staging": "bos://staging.everything.near/everything.dev" } } ``` Fallback chain: requested env → `production` → first defined value. ## Deep Merge Semantics Uses `defu` (with `createDefu` for custom merge rules): | Field type | Merge behavior | |-----------|---------------| | Scalars (account, domain, repository) | Child overrides parent; parent inherited when child omits | | `app.api.shared`, `app.auth.shared`, `plugins.*.shared` dep entries | Deep merged — child overrides specific keys, parent deps preserved | | `plugins` | Deep merged — child overrides per-key, parent plugins preserved unless removed | | `secrets` arrays | Unioned (deduplicated) | | `routes` arrays | Child replaces parent | | `variables` | Deep merged per-key | ### Null Sentinel Removal Set a plugin to `null` to explicitly remove an inherited plugin: ```json { "plugins": { "template": null } } ``` ## Fixed-Core Tenant Mode For shared-host tenant setups, `extends` is both the config inheritance mechanism and the lineage edge between runtimes. Example: ```json { "extends": "bos://pingpayio.near/pingpay.io", "account": "pizza.pingpayio.near", "domain": "pizza.com" } ``` Use this mental model: - `extends` says which runtime this one remixes or inherits from - `account` is the tenant namespace root for this runtime when it is served as a base app - `domain` is the public ingress for this runtime - a runtime can be a child in lineage and still become a new tenant root on its own domain Tenant permissions come from the DB, not env vars. The host fetches the tenant bindings map from the API's `GET /tenants/bindings` endpoint (cached for 30s), and the tenant record's allow flags gate what may be overridden and whether SSR is permitted. Design target for request mapping: - `pizza.com` -> base runtime `bos://pizza.pingpayio.near/pizza.com` - `chicago.pizza.com` -> `bos://chicago.pizza.pingpayio.near/pizza.com` Current implementation note: - the host still applies a single request-scoped tenant overlay on top of one process-wide base runtime - nested label routing and account-relative tenant derivation are the intended direction, but not the complete runtime behavior today ### What the tenant may override today In fixed-core mode, the host keeps the server core from the active base runtime and only applies request-scoped UI-facing overrides from the child config: - `app.ui` - existing `plugins.<id>.ui` - existing `plugins.<id>.sidebar` The tenant config must extend the base BOS runtime. Tenant API/auth overrides and dynamic new plugin IDs are not part of this mode. ### SSR behavior Tenant SSR is gated separately from inheritance: - a tenant may SSR only when its record has `allow_ssr` set - tenants without `allow_ssr` fall back to client rendering ## Resolved Config: `.bos/bos.resolved-config.json` **Generated by**: `bos dev`, `bos build`, `syncResolvedSharedDeps()` **Gitignored**: Yes (inside `.bos/`) When `bos dev` or `bos build` runs: 1. The full extends chain is resolved in memory 2. The merged config is written to `.bos/bos.resolved-config.json` 3. **The authored config is NOT modified** during dev Structure: ```json { "_resolved": { "env": "development", "resolvedAt": "2026-05-11T...", "extendsChain": ["bos://dev.everything.near/everything.dev"] }, "account": "me.near", "domain": "my.dev", "app": { ... }, "plugins": { ... } } ``` ### Build configs read resolved config first All build configs (ui/rsbuild.config.ts, host/rsbuild.config.ts, api/rspack.config.js, plugins/*/rspack.config.js) try `.bos/bos.resolved-config.json` first, falling back to the authored config. The `_resolved` metadata is stripped before use. ### When the authored config IS written | Command | Writes the authored config? | Why | |---------|------------------------|-----| | `bos dev` | No | Uses resolved config | | `bos build` | No | Uses resolved config | | `bos publish --deploy` | Yes | Snapshot moment — pins production URLs + versions | | `bos plugin publish` | Yes | Records production URL + integrity | | `bos plugin add/remove` | Yes | Changes project's own plugin list | | `bos sync` | Yes | Merges template updates into local config | ### Remote host mode (bos->catalog) When host is remote, `syncResolvedSharedDeps()` reads versions from the authored config and writes them into `package.json` catalog. No resolved config is written — the remote host reads the published config directly. ### `_resolved.resolvedAt` The `resolvedAt` timestamp in `.bos/bos.resolved-config.json` is metadata for debugging only. It is not used for staleness detection — the CLI always re-resolves on `bos dev` / `bos build`. ## Circular Extends Detection If the extends chain contains a cycle (e.g., A→B→A), the resolver detects it during config loading and throws an error listing the cycle path. This prevents infinite recursion during merge. Detection occurs in `resolveExtendsRef()` → `mergeBosConfigWithExtends()`. The extends chain is tracked as a set of visited BOS refs; a duplicate visit triggers the error. ## Canonical Field Ordering `BOS_CONFIG_ORDER` enforces consistent key order: 1. `extends` — always first 2. `account` 3. `domain` 4. `title` 5. `description` 6. `testnet` 7. `staging` 8. `repository` 9. `ci` 10. `app` 11. `plugins` Unknown keys go after known keys. `rebuildOrderedConfig()` is applied before every write. ## API From `packages/everything-dev/src/config.ts`: - `writeResolvedConfig(configDir, config, env, extendsChain?)` — writes `.bos/bos.resolved-config.json` - `loadResolvedConfig(configDir)` — reads resolved config, returns `BosConfig | null` - `resolveBosConfigPath(configDir)` — returns resolved config path if exists, else the authored config - `readBosConfigForBuild(configDir)` — reads resolved config stripping `_resolved`, falls back to the authored config From `packages/everything-dev/src/merge.ts`: - `mergeBosConfigWithExtends(parent, child)` — deep merge for extends chain - `mergeBosConfigWithTemplate(local, template)` — merge for sync (local wins) - `resolveExtendsRef(extendsField, env)` — resolve string|object extends for a given env - `rebuildOrderedConfig(config)` — enforce canonical ordering - `BOS_CONFIG_ORDER` — ordered field names ## The tasks you will actually be given **"Remove a plugin I inherited."** Set it to `null` in the authored config's `plugins` — the null sentinel deletes it from the merged result. Deleting the entry entirely just inherits the parent's. **"Different parent for staging."** Object-form `extends` keyed by env (`development` / `staging` / `production`); the fallback chain is requested env → `production` → first defined value. **"Why doesn't the runtime see my config edit?"** Dev reads `.bos/bos.resolved-config.json` (regenerated on every `bos dev`); production reads FastKV. The authored config is only the publish input — `bos publish` is the write. ## What comes back when it refuses | word | do | |---|---| | `CircularExtendsError` — "Circular extends detected: A -> B -> A" | stop — the extends chain loops; restructure so no runtime extends itself transitively | | `Circular dependency detected among: … dependsOn` | build-order cycle in the build train — fix the `dependsOn` declarations in the authored config | | inherited plugin still present after "removing" it | deep merge keeps parent plugins unless nulled — you need the explicit `null` sentinel, keyed exactly as the parent's plugin key | | your section is missing from `.bos/bos.resolved-config.json` | ordering never drops keys — if it's absent, the authored config or its parent didn't define it; check the `extends` chain with `bos config --full` |