Skip to content

Package boundaries

Define business objects with @onerp/meta, execute their behavior on the server with @onerp/core, and access the backend with @onerp/client. The shared packages have no dependency on the NestJS backend or the React application framework.

Import Responsibility
@onerp/meta Fields, business-object definitions, rule and operation declarations, Meta, labels and permissions
@onerp/meta/records Record, update, filter, access and error contracts; applying, merging and copying updates; identifiers and number-series markers
@onerp/meta/serialization The meta wire shape (toMetaWire, parseMeta), codecs and hydration
@onerp/meta/values Decimal and Temporal values
@onerp/meta/format Locale-aware formatting, including use in server-rendered documents
@onerp/catalogs Country, currency and unit reference catalogs; unit dimensions
@onerp/client HTTP access from browsers, Node services and integration tools
import { defineBusinessObject, field } from "@onerp/meta";
import type { BusinessObject, Update } from "@onerp/meta/records";
import { Decimal } from "@onerp/meta/values";
import { currencyCatalog } from "@onerp/catalogs";

The root metadata entry point is for authoring the model. Rule execution and the rule and operation contracts (Determination, Validator, Action, Function) belong to @onerp/core, beside the decorators that register them. A new utility does not belong in metadata merely because both the browser and backend use it: it must describe the model, its contracts, its scalar values, or its serialization. Keep types with the concepts they describe; do not add a second representation just to cross an entry point.

Core depends on metadata; metadata must never import core. Catalogs are concrete reference data layered on the generic Catalog contract. Keep them out of the generic model’s root exports. Tenant-extension policy (custom names) belongs to the server’s MetaService, not to generic metadata definitions.

@onerp/core provides the NestJS server runtime: persistence, auth, HTTP, jobs, plugin discovery and server integrations. @onerp/react provides the React application framework and its forms, lists and data hooks. @onerp/ui contains UI primitives.

@onerp/mcp serves the model to any MCP host as a tool catalog on /api/mcp, under the same permissions the HTTP API enforces. Two further packages are optional and sit outside that spine. @onerp/agent is the chat agent — prompt, model loop and an in-process MCP client over that same catalog; it does nothing until a provider key is configured. @onerp/node-red depends only on @onerp/client, so it runs wherever Node-RED does rather than inside the server. Domain packages contain server code; browser code follows the existing metadata pipeline rather than importing a domain’s root at runtime.

Import Test support
@onerp/testing Server application and service harnesses with database/auth integration
@onerp/react/testing React rendering and application providers
@onerp/react/testing/in-memory In-memory record manager without React Testing Library
@onerp/meta/testing Lightweight metadata fixtures
@onerp/factory Schema-aware data factories with a host-supplied runtime

The factory package does not depend on the server test harness. Form completeness checks live in the React form runtime; they are not backend validation guarantees.

Whether a package is consumed as source or as build output is decided per package.

@onerp/react and @onerp/ui ship source: their exports point at src and they have no build script. Only a bundler ever loads them, so compiling them first would buy nothing but a second, staler copy.

Every other package ships compiled output from dist and has a build script, because a Node process loads it directly: the API server, the Node-RED nodes, or the end-to-end suite.

A compiled package that a browser app also reaches through Vite publishes its source beside that output, under the source export condition:

"exports": {
".": {
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.js"
}
}

@onerp/meta, @onerp/client and @onerp/catalogs publish it on every subpath they export. onerpAppConfig from @onerp/react/vite lists source first in resolve.conditions, so an application picks up an edit to those packages immediately instead of whichever dist was last built. Node skips a condition it does not know, so the API server and the Node-RED nodes keep loading dist.

Types still resolve through types, which is dist, and a package’s own e2e and scenario tests import it by its published name. Every type-check therefore still depends on build and ^build.

A package declares its entry points in exports and nothing else. main and types are the pre-exports spelling of the same thing, and no resolver here reads them: every tsconfig resolves as NodeNext or Bundler, both of which prefer exports, and Node does the same for an import. A package that ships source additionally carries no build. Nothing in this repository is published, so workspace dependencies are spelled workspace:* throughout.

@repo/vitest-config builds with a bare tsc rather than a tsconfig.build.json like its peers. That file exists to keep tests out of dist, and this package has none — a second config would be ceremony that changes no output.

@onerp/api carries a one-key .swcrc. It is a "type": "module" package like every other one here, but nest build’s SWC builder hardcodes module.type: "commonjs" and never reads the module the tsconfig already sets to NodeNext, so without that override the server would emit CommonJS into an ESM package. The Nest CLI merges the file over its own defaults rather than replacing them, which leaves the decorator metadata the DI container reads untouched. The CLI’s own isEsmProject helper already drives its webpack and rspack builders; once the SWC builder reads it too, the file can go.

Only @onerp/react and @onerp/ui declare sideEffects, both as ["**/*.css"]. Nothing else declares it, and that is deliberate.

@onerp/meta cannot. Three of its modules do work at import time: values/decimal.ts writes Big.DP, Big.RM, Big.NE and Big.PE, which is what fixes decimal precision and the plain-string wire format for every Decimal in the system; records/id.ts constructs the monotonic ULID generator as a singleton; and wire/meta-codec.ts assigns the recursive fieldSchema at module scope. A false here would let a bundler delete any of them silently.

@onerp/catalogs and @onerp/client could — no module in either of them carries a top-level statement — and it was measured to change nothing. Both app bundles come out byte-identical down to the content hash, because Rolldown’s own per-statement analysis already drops every unused module: countryCatalog and currencyCatalog are absent from apps/web today with no declaration at all.

The rule this leaves: sideEffects: false earns its place when a module carries a top-level statement a bundler must conservatively keep. A package written as plain ESM exports has none, so the declaration buys no bytes — and it is a claim that fails by deleting code, silently, the day someone adds a legitimate module-level statement.

@repo/typescript-config shares compiler settings. base.json holds common language/strictness settings, node.json selects Node module resolution, react.json selects browser libraries and bundler resolution, vite-app.json adds the Vite and Node types a browser application needs to type-check its own build config alongside its source, and nestjs.json adds backend decorator settings and the API application’s existing compiler policy. Packages that use web-platform APIs explicitly include the required libraries.

@repo/vitest-config/node is for plain TypeScript tests. The /nestjs preset adds the decorator transform and serializes expensive scenario/e2e files. React packages configure their own Node, jsdom and browser projects. Turbo runs tests per package, preserving package-level caching.