@ic-reactor/codegen
@ic-reactor/codegen contains the shared generation pipeline used by
@ic-reactor/cli and
@ic-reactor/vite-plugin. It is the advanced,
programmatic entry point for generating declarations, reactor files, and client
boilerplate from Candid definitions.
Installation
Section titled “Installation”pnpm add -D @ic-reactor/codegenMain Entry Point
Section titled “Main Entry Point”The primary API is runCanisterPipeline, which coordinates parsing, naming,
declaration generation, and reactor file generation.
import { runCanisterPipeline } from "@ic-reactor/codegen"
await runCanisterPipeline({ canisterConfig: { name: "backend", didFile: "./backend/backend.did", mode: "DisplayReactor", // Written into index.generated.ts. Without it the generated reactor throws // on import anywhere but a local replica with an ic_env cookie. canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai", }, projectRoot: process.cwd(), globalConfig: { outDir: "src/declarations", clientManagerPath: "../../clients", target: "react", },})Set factories: true in canisterConfig to also generate
index.factories.generated.ts; see
Query and Mutation Factories.
runCanisterPipeline resolves with { success, files, error?, warnings? }.
warnings lists what the run could not fix for you, such as an index.ts of
your own that does not re-export the factories.
What It Exposes
Section titled “What It Exposes”runCanisterPipelinefor full end-to-end generation.- Naming helpers like
toPascalCase,getReactorName, andgetServiceTypeName. findSharedOutDirsandsharedOutDirMessage, which the CLI and the Vite plugin run across every configured entry to refuse one that would generate into the directory of an earlier entry, such as two entries with the samenameunder the globaloutDir.getFactoryExportNames, which gives the export name of each method’s generated factory.- Lower-level generators when you need declarations, reactor files, factories files, or client files separately.
For each canister, the pipeline writes into <outDir>/<name>/:
declarations/<did-basename>.js: the IDL factory moduledeclarations/<did-basename>.d.ts: the TypeScript service typesdeclarations/<did-basename>.did: a copy of the source.didindex.generated.ts: managed implementation, always regeneratedindex.factories.generated.ts: withfactories: true, a managed query or mutation object per method, always regenerated. See Query and Mutation Factories.index.ts: stable entry wrapper, preserved unless it still matches the default wrapper or an older generated scaffold that can be migrated — a migrated file is copied toindex.ts.bakfirst, and one carrying any export the old generator never wrote is left alone
The declaration filenames come from the .did file’s basename, not the
canister name. Nothing is written until every file has been generated, so a
.did that fails to parse leaves the previous declarations in place instead of
deleting them. After every run, declarations/ holds only the files that run
generated. A file whose content has not changed is not rewritten, so
regenerating from unchanged input leaves every file untouched and wakes no file
watcher.
If Prettier resolves from the project root, the pipeline formats the .js,
.d.ts, index.generated.ts, index.factories.generated.ts and the
index.ts wrapper it writes with it,
using the config Prettier finds for each file’s final path. Output you commit
after running Prettier then comes out byte-identical when you regenerate.
Without Prettier, or if formatting fails, the declarations hold the Candid
parser’s output followed by a newline. The .did copy is a byte copy of the
source and is never formatted.
With target: "react", index.generated.ts also exports the hooks bound
to the generated reactor — use<Pascal>Query, use<Pascal>SuspenseQuery,
use<Pascal>InfiniteQuery, use<Pascal>SuspenseInfiniteQuery,
use<Pascal>Mutation, and use<Pascal>Method. Set factories: true to also
generate a createQuery or createMutation object per method, which works
both in components and outside React.
Query and Mutation Factories
Section titled “Query and Mutation Factories”Set factories: true on a canister to generate index.factories.generated.ts
next to index.generated.ts. It holds one object per method of the service,
bound to the generated reactor and chosen by the method’s annotation in the
.did:
Method in the .did |
Factory | Export name |
|---|---|---|
query or composite_query, no arguments |
createQuery |
<method>Query |
query or composite_query, with arguments |
createQueryFactory |
<method>Query |
update (no annotation) or oneway |
createMutation |
<method>Mutation |
An update method is never generated as a query, so a refetch, a window focus or a remount can never re-run a call that changes state.
For this service:
service : { get_message : () -> (text) query; get_user : (principal) -> (opt text) query; set_message : (text) -> ();}the generated file holds, comments aside:
// index.factories.generated.ts (managed, do not edit)import { createMutation, createQuery, createQueryFactory,} from "@ic-reactor/react"import { backendReactor } from "./index.generated"
export const getMessageQuery = createQuery(backendReactor, { functionName: "get_message",})
export const getUserQuery = createQueryFactory(backendReactor, { functionName: "get_user",})
export const setMessageMutation = createMutation(backendReactor, { functionName: "set_message",})The generated index.ts wrapper re-exports it next to index.generated.ts,
so the factories import from the canister’s entry point:
import { getMessageQuery, getUserQuery, setMessageMutation,} from "./declarations/backend"
function Message() { const { data } = getMessageQuery.useQuery() const { mutate } = setMessageMutation.useMutation({ invalidateQueries: [getMessageQuery.getQueryKey()], }) // ...}
// Outside React: loaders, actions, scriptsawait getUserQuery(["aaaaa-aa"]).fetch()await setMessageMutation.execute(["hello"])await getMessageQuery.invalidate()The factories are typed by the reactor class mode picks, so a
DisplayReactor canister reads and sends a nat as a string and a Reactor
canister as a bigint. For the three @ic-reactor/candid classes each call
also passes the service, the transform and the method name as type arguments,
as index.generated.ts does for createActorHooks, so TypeScript never has to
infer the service from a subclass of Reactor.
Each call in the file is annotated /* @__PURE__ */: creating a factory has no
side effects, and the annotation tells the bundler so. Vite, Rollup, esbuild
and webpack then leave out every factory the app does not import, although the
index.ts wrapper re-exports them all, and an app that imports only a bound
hook from the canister’s entry point bundles none of them.
Export names
Section titled “Export names”Every Candid method name, however it is spelled, gets a valid identifier that no other export of the canister uses:
<method>is the method name in camelCase, as the reactor’s name is derived from the canister name:get_messagegivesgetMessageQuery,icrc1_transfergivesicrc1TransferMutation,"get-thing"givesgetThingQuery. The suffix keeps names such asdeleteorifclear of reserved words.- A name that would begin with a digit gets a leading
_:"2fa_status"gives_2faStatusQuery. - Methods take their names in the order Candid lists them, which is by name. A
name that an earlier method took, or that
index.generated.tsalready exports, gets_appended until it is free, the way the declarations rename a Candid type named like a TypeScript type. SogetThingandget_thinggivegetThingQueryandgetThingQuery_, and a query methoduse_backendin canisterbackendgivesuseBackendQuery_, sinceuseBackendQueryis the bound hook.
Since names are taken in that order, a method added later can take a name an
existing method had: adding "get-thing" to a service that has getThing
gives the new method getThingQuery, because it sorts first, and moves
getThing to getThingQuery_. If both take the same arguments, code that
imported getThingQuery still compiles and now calls the new method. The doc
comment above each generated export names the method it calls.
getFactoryExportNames(canisterName, methods) returns the names for a list of
methods, such as parseDid(source).service.methods from
@ic-reactor/parser.
Customizing
Section titled “Customizing”Pass per-call options to the hooks, such as getMessageQuery.useQuery({ select, staleTime }) or setMessageMutation.useMutation({ invalidateQueries, onSuccess }). For a factory with its own config, such as a
refetchInterval or an invalidateQueries that execute() also runs, define
it in index.ts. An export index.ts declares itself takes precedence over
the same name from export *, so it can also replace a generated one:
// index.ts (yours)import { createMutation } from "@ic-reactor/react"import { backendReactor } from "./index.generated"import { getMessageQuery } from "./index.factories.generated"
export * from "./index.generated"export * from "./index.factories.generated"
export const setMessageMutation = createMutation(backendReactor, { functionName: "set_message", invalidateQueries: [getMessageQuery.getQueryKey()],})Lifecycle
Section titled “Lifecycle”- The file is regenerated on every run, and left untouched when its content has not changed.
- Switching
factoriesoff removes the file, if it still carries codegen’s header, and puts back the wrapper without the re-export if nobody edited it. - An
index.tsyou have edited is left alone. When it does not re-export the factories, or re-exports them after they were switched off, the run succeeds and reports a warning, which the CLI and the Vite plugin print, andrunCanisterPipelinereturns inwarnings. generateReactor: false(the CLI’s--bindgen-only) leaves the file as it is. The CLI’s--cleanremoves it with the rest of a canister’s stale output directory.- The factories come from
@ic-reactor/react, sofactories: truewithtarget: "core"is a config error, and the run fails before it writes anything. With a core target, call the reactor’sfetchQuery()andcallMethod()instead.
Reactor Modes
Section titled “Reactor Modes”Generated output can target any of these reactor classes:
DisplayReactorReactorCandidReactorCandidDisplayReactorMetadataDisplayReactor
The three Candid modes always import from @ic-reactor/candid, regardless of
runtime target, so that package must be installed when you pick one. Only
Reactor and DisplayReactor switch import source based on the target.
Runtime Target
Section titled “Runtime Target”react(default): generates the reactor plus bound React hooks, and withfactories: truethe query and mutation factoriescore: generates only the typed reactor exports with no React dependency