Building and loading package artifacts
A package contributes to Oda through the artifacts its build generates: a record of what the package exposes, the schemas that validate its contracts at runtime, and, for packages that declare Remote methods, the client side of those calls. This page follows that pipeline for package authors: what the build generates and checks, how Oda finds and loads a contribution, and how the runtime registers it, with the messages each step can raise. For the methods themselves, see Remote methods and streams; for the service and event catalog and the manifest fields a package declares, see Plugin artifacts and the catalog.
What the build reads and writes
Generation runs at build time only. It never runs inside a live Oda: Oda loads what a build produced, and it does not generate artifact files itself.
- Two projects per package. The build analyses the package's host and client TypeScript projects, one for each side the package contributes.
- A runtime file and a declaration per side. For each side, the build writes a runtime artifact and a matching declaration into the package's output folder.
- The client side of Remote calls. A package that declares Remote methods also gets the generated client projections of those calls, with their declarations; a later build clears those files once the package stops declaring Remote methods.
- Check mode and write mode. Check mode only reports and fails; it never changes your sources. In write mode the build inserts a missing public type annotation for you and then re-checks the edited projects from scratch, so anything it cannot repair still fails.
- What it will not read. Only concrete export targets are analysed: a package whose exports use a wildcard pattern is skipped entirely.
What the build checks
The build validates the package manifest against what it generated, and fails when the two disagree:
- An export entry for every generated file. Each artifact's entry must name its declaration under
types and its runtime file under default, exactly as written. A missing or wrong entry fails with must export <subpath> as {"types":...,"default":...}.
- The published file list. Both generated files must appear among the manifest's published files, or the build fails with
package files must include <file>.
- Remote artifacts without Remote methods. A package that still publishes them fails with
publishes Remote artifacts but has no Remote methods; remove the leftovers or restore the methods.
- Sources that cannot be kept. A public member without an explicit type annotation is refused in check mode; a reference to another package's private declaration fails; a namespace re-export that crosses between the host and client sides fails.
- Types outside the schema subset. A generic schema declaration cannot be emitted, and computed roots such as a conditional or mapped type fail with the construct named, instead of a weaker type.
How Oda finds and loads a contribution
Oda scans the packages in its setup and registers the contributions it finds.
- The entry is the opt-in. A package contributes through its host entry; a package without the entry is skipped silently.
- One verdict for the life of the process. Oda works out once whether a package contributes and keeps that answer, so a package that gains its entry while Oda runs stays skipped until the next start; Hot reload covers what applies live.
- Nested packages are named explicitly. A package behind another entry is not discovered on its own: name it in the setup. A named entry that cannot be resolved, or that lacks its export, fails loudly and names the package.
- A malformed artifact fails the load loudly. Among packages that are already mounted, a malformed artifact fails the load with one counted report,
<n> contributor(s) failed to register:, and each failing package brings its own message. A failure later in the process is logged for that package, and unrelated packages keep registering.
- Stale imports are dropped. An artifact whose import settles after its package has been unloaded is discarded rather than registered late.
- The host side is the one discovered. Automatic discovery covers the host side only; the client side has no equivalent automatic registration in the current build.
How a contribution is registered
The runtime keeps what each contributing package carries: its record of what it exposes, its schemas and the declarations of its calls. Registration is one atomic batch per contribution.
- Validated first, committed together. The package's identity and side, its schemas and its call declarations are validated, then committed in one step. A duplicate identity (the same package and side, a schema name the package already contributes, an invocation id or an endpoint) rejects the whole batch before anything is committed; an earlier registration stays intact.
- A schema name belongs to the package, not to the side. Schema identity carries the package and the name, so contributing the same schema name from both sides of one package is a duplicate that rejects the batch.
- Schemas are built lazily. Registering an artifact does not create its schemas: a schema is created on its first read, once for the running Oda, and later reads reuse the same instance.
- A declaration is fixed once seen. How a value crosses the boundary is declared once and fixed for the rest of the run: registering the same name again with a changed declaration is refused, even after the contribution that carried it is withdrawn.
- Change listeners are contained. Subscribers are told when a contribution registers or is withdrawn; a listener that fails is logged, and later listeners still run.
- Registrations follow their package. Each registration is withdrawn exactly when the package that made it unloads.
- A failed read says which case it is. A read that does not find a schema distinguishes a malformed name, a package registered without a schema by that name, and a package with no registered contribution at all.
Questions, answered
What does the build read from my package?
The package's host and client TypeScript projects, one for each side the package contributes. The build reads them while the package is built; a running Oda never analyses your sources.
Which files does the build add to my package?
For each side, a runtime artifact and a matching declaration file in the package's output folder. A package that declares Remote methods also gets the client projections of those calls, with their declarations; a later build removes those files once the methods are gone.
Why does a package contribute nothing, with no error?
A contribution is opt-in: a package without its host entry is skipped silently, and Oda keeps that verdict for the life of the process. Add the entry and start Oda again; Hot reload lists what applies live.
Is a schema created when my package registers?
No. Registration stores the schema's factory; the schema itself is created on its first read, once for the running Oda, and every later read reuses that instance.
Can both sides of my package contribute the same schema name?
No. A schema is identified by its package and its name, without the side, so the second contribution of the same name is a duplicate, and a duplicate rejects the whole batch it arrives in.
Why did one duplicate reject my whole registration?
Registration validates the whole contribution before committing anything, so a duplicate (the same package and side, a schema name the package already contributes, an invocation id or an endpoint) rejects the batch before any of it is committed. Nothing of the refused batch lands, and an earlier registration stays intact.
Can a wire declaration change inside one run?
No. How a value crosses the boundary is declared once and fixed for the rest of the run: registering the same name again with a changed declaration is refused, even after the contribution that carried it was withdrawn. A new run sees the new declaration.
What is validated before a contribution is accepted?
The artifact must be a complete, well-formed contribution: it must be owned by the package that declares it, it must be the host side, its schemas must carry factories, and its call records must be well formed. Anything missing is refused at registration rather than half-registered.
Which load failures are loud, and which are only logged?
A malformed artifact among packages that are already mounted fails the load loudly, with one counted report and a message per failing package. A failure later in the process is logged for that package, and unrelated packages keep registering.
Why can a registered schema not be found by name?
A read that misses says which case it is: the name is not well formed (the expected shape is <package>#<name>), the package is registered but contributes no schema by that name, or nothing is registered under that package.