When a plugin package joins Oda, a build step turns its source into the artifacts the runtime loads: a record of what the package exposes, the schemas that validate its contracts, and the client side of its Remote methods. This page follows that pipeline for plugin developers: what the build generates and checks, how the runtime registers the result, and how the service and event catalog stays in step with the code.
What the build generates
Build time only. Generation runs at build time and never while Oda runs: Oda loads the files a build produced, and it does not generate them itself.
Two projects per package. The build analyses the package's host and client TypeScript projects, one for each side the package contributes.
Two modes. A check mode verifies and fails on a problem, and never changes your sources; a write mode repairs what it can, then re-checks the result from scratch.
Opt-in entries. A package contributes through its manifest, with one export entry for each side it contributes:
Export entry
What it publishes
A host entry
The host side: the package's record of what it exposes, and its runtime schemas.
What lands beside the package. For each side, the build writes a runtime file and a matching declaration into the package's output folder, and a later build clears the leftover Remote files once the package stops declaring Remote methods. The wire those Remote calls speak is on The Oda Remote API.
Freshness. A freshness check compares the committed artifacts with their sources; when one no longer matches, the check fails and tells you to regenerate it and commit the result.
What fails the build
The build checks the manifest against what it generated, and fails when the two disagree:
An export entry that does not point at the generated files. Each entry must name its declaration file under types and its runtime file under default: must export <subpath> as {"types":...,"default":...}.
A generated file left out of the manifest. Both generated files must appear in the package's published file list: package files must include <file>.
Remote artifacts without Remote methods. A package that publishes them fails with publishes Remote artifacts but has no Remote methods; remove the leftovers or restore the methods.
Errors in the sources. Syntax or semantic errors fail the build.
A public member without an explicit type annotation. Check mode refuses it; write mode inserts the annotation and re-checks.
A construct the schema emitter cannot represent. A generic schema declaration, or a computed root such as a conditional or mapped type, fails naming the construct, instead of a weakened type.
A reference that cannot be kept losslessly. A namespace re-export crossing between the host and client sides is refused, as is a reference to another package's private declaration or a declaration merge that cannot be kept.
Only concrete export targets are read. A package whose exports use a wildcard pattern is not analysed at all.
How the runtime registers a package
The entry is the opt-in. A mounted package contributes through its host entry; a package without that 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 keeps being skipped until the next start; Hot reload covers what applies live.
Nested packages are named. A package nested behind another entry is not discovered by itself: name it explicitly in the setup. A named entry that cannot be resolved, or that lacks its export entry, fails the load loudly and names the package.
A malformed artifact fails 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 contributor brings its own message. A failure later in the process is logged for that package, and unrelated packages keep registering.
The host side is the one discovered. Automatic registration covers the host side only in the current build; the client side has no equivalent automatic discovery.
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.
What is validated. The artifact must be a complete, well-formed contribution: owned by the package that declares it, the host side, with factories for its schemas and well-formed call records. Anything missing is refused at registration rather than half-registered.
A batch is all or nothing. The package identity, its schemas and its call descriptors are validated first, 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 wire declaration is fixed once seen. How a value crosses between the sides is declared once: registering the same name again with a changed wire declaration is refused for the rest of the run, even after the contribution that carried it is withdrawn.
A failed read says which case it is. A read that does not find a schema distinguishes a malformed name (the expected shape is <package>#<name>), a package registered without a schema by that name, and a package with no registered contribution at all.
Registrations follow their plugin. Each registration is withdrawn exactly when the plugin that made it unloads; an artifact whose import settles after its plugin is gone is discarded.
A failing listener is contained. Subscribers are told when a contribution registers or is withdrawn; a listener that throws is logged, and later listeners still run.
Where packages come from. Which bundles a profile runs, and adding one from a command line, are on Oda profiles and launchers; switching and installing bundles from inside the app is on Plugins and bundles.
The catalog of services and events
The catalog is generated from the sources: it lists the services and the events a plugin can listen to, with their signatures, and the inspection tools read it, so their answers describe what this build actually exposes (Inspect and manage extensions). The same catalog is written into developer pages as generated regions.
Annotate every service and event. Write the summary prose, and add the tags the catalog needs: @mode on an event, @param <name> for every parameter, and @returns (return type: <type>) on a method that returns a value.
The gates fail closed. A missing annotation fails with a finding that names its service or event and its source file and line, and the check fails with the total count: is missing an @mode tag. Add '@mode emit|bail|waterfall|parallel|serial' to its JSDoc, is missing @param <name>., or is missing @returns (return type: <type>).
Every referenced type needs a home. A signature may only reference types the catalog can link: a type with a documentation page, a built-in type, or an explicit exemption. A reference with no home fails the gate and names the type.
Regions are generated. In a document, the catalog text sits between BEGIN GENERATED and END GENERATED comment markers. Do not edit between them: edit the source, regenerate, and commit the result.
Freshness. A freshness check fails when a committed region no longer matches its sources.
What a package manifest declares
All of these fields are optional: omitting one leaves it undeclared, and readers do not infer a default.
Icon. A relative path to an SVG, PNG, JPEG or WebP file of at most 256 KiB, kept inside the package folder. Oda reads it for display as an image, never as inline markup.
Manifest format version. The declared format is 1, independent of the package version and of the chat log format.
Engines range. The compatible Oda versions as a SemVer range, including an exact version, beside the usual Node.js and npm ranges. Declaring it does not reject an incompatible host by itself: compatibility is declarative today.
Questions, answered
What keeps the catalog in step with the code?
Generation does. The catalog is produced from the sources, and a freshness check compares the committed result with them; when the two drift apart, the check fails and tells you to regenerate and commit. The gates fail closed in between: a missing annotation, or a type with no home, stops the generation instead of shipping a catalog that does not match the code.
May I edit the text between the generated markers?
No. Everything between the BEGIN GENERATED and END GENERATED markers is produced from the sources; an edit there is lost on the next regeneration, and a committed region that no longer matches its sources fails the freshness check. Edit the source and regenerate.
Why did the catalog gate refuse a type my service uses?
The referenced type has no home in the catalog: no documentation page owns it, it is not one of the built-in types, and no exemption covers it. The failure names the type; give it a home, or an exemption, and regenerate.
What does the counted register failure mean?
A malformed artifact failed the load for packages that were already mounted. The report counts the contributors that could not register (<n> contributor(s) failed to register:), and each failing contributor brings its own message. Repair or remove the package and start Oda again; a failure later in the process is logged for that package, and unrelated packages keep registering.
Why does my nested plugin not register?
Oda does not discover a package that sits behind another entry on its own. Name the package explicitly in the setup; a named entry that cannot be resolved, or that lacks its export entry, fails the load loudly and names the package.
Which entries must a contributing package declare?
One export entry for each side it contributes: a host entry, a client entry and, when it declares Remote methods, a remote entry. Each entry names its declaration file under types and its runtime file under default, and the build fails on an entry that does not point exactly at the generated files.
What does write mode change in my sources?
It inserts the public type annotations that check mode would refuse, then re-checks the edited projects from scratch, so anything it cannot repair still fails. Check mode only reports; it never changes your sources.
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.
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.
What icon may a package declare?
A relative path to an SVG, PNG, JPEG or WebP file of at most 256 KiB, kept inside the package folder. Oda shows it as an image, never as inline markup.
When does a registration go away?
When the plugin that made it unloads: each registration is withdrawn exactly then. An artifact whose import settles after its plugin is gone is discarded rather than registered late.