Plugin artifacts and the catalog
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 modes. A check mode verifies and fails on a problem; 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. |
| A client entry | The client side of the package. |
| A remote entry | The client projections of the package's Remote methods. |
- What lands beside the package. For each side, the build writes a runtime file and a matching declaration into the package's output, and a later build clears the leftover Remote files once the package stops declaring Remote methods.
- 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.
- 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.
- 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.
- A batch is all or nothing. The package identity, its schemas and its call descriptors are validated together; a duplicate identity (the same package and side, schema name, invocation id or endpoint) rejects the whole batch before anything is committed.
- A wire declaration is fixed once seen. Registering the same name again with a changed wire declaration is refused during that process lifetime.
- 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. A change 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 source file and line:
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
- Icon. The field carries an image as a base64 data URL: SVG, PNG, JPEG or WebP, up to 256 KiB. Oda renders it 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 range, 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.
Does the client side register itself?
No. Automatic registration covers the host side only in the current build; the client side has no matching automatic discovery.
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.