@ic-reactor/vite-plugin 0.15.1 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,33 @@
1
1
  # @ic-reactor/vite-plugin
2
2
 
3
- > **AI coding agents:** read [`llms.txt`](./llms.txt) in this package
4
- > (`node_modules/@ic-reactor/vite-plugin/llms.txt`) before writing code with it. It
5
- > is written for the installed version and lists the patterns to use and the
6
- > mistakes to avoid.
7
-
8
- Vite plugin for IC Reactor code generation. It runs the shared
9
- `@ic-reactor/codegen` pipeline, watches `.did` files, and can inject the
10
- `ic_env` cookie used by `ClientManager` during local development.
3
+ > **ic-reactor 4 is a prerelease.** `4.0.0-beta.1` is published under npm's
4
+ > `beta` dist-tag, and `latest` stays the 0.15 plugin of the 3.x line until 4.0
5
+ > GA. That plugin, which generates reactor bindings with `@ic-reactor/codegen`,
6
+ > is documented at https://ic-reactor.b3pay.net/v3/packages/vite-plugin.
7
+
8
+ A Vite plugin for an app built on a module that `candid-core-cli gen` generates
9
+ from a `.did` file. It does two things, and exports only `icReactor` and the
10
+ type `IcReactorPluginOptions`:
11
+
12
+ - **Generation.** It runs `candid-core-cli gen` on each configured `.did` file
13
+ when a build or the dev server starts, and again when that file changes. The
14
+ generated module is candid-core's, as the generator wrote it: the plugin
15
+ adds no wrapper files, hooks or reactors.
16
+ - **Environment.** Under `vite dev` and `vite preview` it sets the `ic_env`
17
+ cookie and proxies `/api` to the local IC network, so the app finds its
18
+ canister IDs and the replica's root key without configuration.
11
19
 
12
20
  ## Install
13
21
 
14
- ```bash
15
- pnpm add -D @ic-reactor/vite-plugin
16
- pnpm add @ic-reactor/react @tanstack/react-query @icp-sdk/core
22
+ The plugin runs the `@candid-core/cli` your app installs, and that CLI has to
23
+ pair with the `@candid-core/schema` runtime the generated modules import. Both
24
+ are pinned to one exact release while they are betas, and so is the plugin's
25
+ peer on the CLI:
26
+
27
+ ```sh
28
+ npm install --save-exact @candid-core/schema@0.3.0-beta.1
29
+ npm install --save-dev --save-exact @candid-core/cli@0.2.0-beta.1
30
+ npm install --save-dev @ic-reactor/vite-plugin@beta
17
31
  ```
18
32
 
19
33
  ## Quick Start
@@ -21,156 +35,140 @@ pnpm add @ic-reactor/react @tanstack/react-query @icp-sdk/core
21
35
  ```ts
22
36
  // vite.config.ts
23
37
  import { defineConfig } from "vite"
24
- import react from "@vitejs/plugin-react"
25
38
  import { icReactor } from "@ic-reactor/vite-plugin"
26
39
 
27
40
  export default defineConfig({
28
41
  plugins: [
29
- react(),
30
42
  icReactor({
31
- canisters: [{ name: "backend", didFile: "./backend/backend.did" }],
43
+ canisters: {
44
+ ledger: { didFile: "../backend/ledger.did" },
45
+ },
32
46
  }),
33
47
  ],
34
48
  })
35
49
  ```
36
50
 
37
- ```ts
38
- // src/clients.ts
39
- import { ClientManager } from "@ic-reactor/react"
40
- import { QueryClient } from "@tanstack/react-query"
41
-
42
- export const queryClient = new QueryClient()
43
- export const clientManager = new ClientManager({
44
- queryClient,
45
- })
46
- ```
47
-
48
- No opt-in flag is needed to pick up the plugin's environment in development:
49
- the plugin sets the `ic_env` cookie and `ClientManager` reads it automatically
50
- in the browser. That trust stops at the local replica — cookies are not
51
- origin-isolated, so on a custom domain, on mainnet, and on Codespaces or Gitpod
52
- (whose workspaces share a parent domain with every other user's) the cookie is
53
- ignored and a reactor with no `canisterId` throws. Set the per-canister `canisterId` in the
54
- plugin config to bake it into the generated output for those builds, or pass
55
- `allowEnvConfig: true` to `ClientManager` if you trust every subdomain of the
56
- domain you serve from.
57
-
58
- The plugin generates files under `src/declarations/<canister>/` by default —
59
- `declarations/<did-basename>.{js,d.ts,did}` plus a managed `index.generated.ts`
60
- and a stable `index.ts` wrapper. With `target: "react"`, `index.generated.ts`
61
- exports the reactor and six hooks named after the canister
62
- (`use<Canister>Query`, `use<Canister>SuspenseQuery`,
63
- `use<Canister>InfiniteQuery`, `use<Canister>SuspenseInfiniteQuery`,
64
- `use<Canister>Mutation`, `use<Canister>Method`).
65
-
66
- Set `factories: true` on a canister to also generate
67
- `index.factories.generated.ts`, a query or mutation object per method bound to
68
- the generated reactor: `createQuery` for a query method without arguments,
69
- `createQueryFactory` for one with arguments, and `createMutation` for an
70
- update or oneway method, named `<method>Query` or `<method>Mutation` with the
71
- method name in camelCase. The default `index.ts` wrapper re-exports it; an
72
- `index.ts` you have edited is left alone, and the plugin warns in the terminal
73
- until it re-exports the factories. It needs `target: "react"`. See
74
- https://ic-reactor.b3pay.net/v3/packages/codegen#query-and-mutation-factories
75
- for the naming rule for any method name.
76
-
77
- ```ts
78
- import { icReactor } from "@ic-reactor/vite-plugin"
51
+ On `vite dev` and `vite build` this writes `src/canisters/ledger.ts`, the
52
+ generated module (it exports `actor` and the type `Actor`), and
53
+ `src/canisters/ledger.envelope.json` next to it. The generator names its output
54
+ after the `.did` file, not after the key in `canisters`.
79
55
 
80
- icReactor({
81
- canisters: [
82
- { name: "backend", didFile: "./backend/backend.did", factories: true },
83
- ],
84
- })
85
- ```
56
+ The first line the plugin logs says where an agent reads how to use the
57
+ library: `ic-reactor: agent guide at node_modules/@ic-reactor/core/llms.txt`.
86
58
 
87
- ```tsx
88
- import { getMessageQuery, setMessageMutation } from "./declarations/backend"
59
+ ## Options
89
60
 
90
- // In a component
91
- const { data } = getMessageQuery.useQuery()
61
+ | Option | Default | Meaning |
62
+ | ------------------- | ------------------------- | --------------------------------------------------------------------------------- |
63
+ | `canisters` | `{}` | The app's canisters, by their name in the `icp` project; see below |
64
+ | `injectEnvironment` | `true` | Set the cookie and the `/api` proxy under `vite dev` and `vite preview` |
65
+ | `failOnError` | build `true`, dev `false` | Abort the Vite run when a canister fails to generate; see "When generation fails" |
92
66
 
93
- // Anywhere, outside React included
94
- await setMessageMutation.execute(["hello"])
95
- ```
67
+ Each entry of `canisters` takes:
96
68
 
97
- If Prettier resolves from Vite's `config.root`, the plugin formats the
98
- generated `.js`, `.d.ts`, `index.generated.ts`, `index.factories.generated.ts`
99
- and the `index.ts` wrapper it writes with it and your Prettier config, so a rebuild leaves formatted,
100
- committed output unchanged. Without Prettier the declarations hold the Candid
101
- parser's output followed by a newline, and a formatting error never fails the
102
- build.
69
+ | Field | Default | Meaning |
70
+ | ------------ | ----------------- | -------------------------------------------------------------------------------------- |
71
+ | `didFile` | none | The canister's Candid file, relative to the Vite root. Without it nothing is generated |
72
+ | `outDir` | `"src/canisters"` | Where the generator writes, relative to the Vite root |
73
+ | `canisterId` | none | A fixed ID for the cookie, which wins over the one `icp` reports |
103
74
 
104
- If you want non-React output, set `target: "core"` and install the matching
105
- runtime package instead of `@ic-reactor/react`.
106
-
107
- ## Options
75
+ Canisters that share one interface share one module. An ICP ledger and a ckBTC
76
+ ledger both on `icrc1.did` are two entries, so that the `ic_env` cookie carries
77
+ each ID, and one generated module:
108
78
 
109
79
  ```ts
80
+ // vite.config.ts
81
+ import { defineConfig } from "vite"
110
82
  import { icReactor } from "@ic-reactor/vite-plugin"
111
83
 
112
- icReactor({
113
- canisters: [
114
- {
115
- name: "backend",
116
- didFile: "./backend/backend.did",
117
- mode: "DisplayReactor",
118
- },
84
+ export default defineConfig({
85
+ plugins: [
86
+ icReactor({
87
+ canisters: {
88
+ icp_ledger: { didFile: "did/icrc1.did" },
89
+ ckbtc_ledger: { didFile: "did/icrc1.did" },
90
+ },
91
+ }),
119
92
  ],
120
- outDir: "src/declarations",
121
- clientManagerPath: "../../clients",
122
- target: "react",
123
- injectEnvironment: true,
124
- failOnError: true,
125
93
  })
126
94
  ```
127
95
 
128
- Relative paths — `didFile`, `outDir` — resolve against Vite's resolved
129
- `config.root`, not the directory vite was started from. If you set
130
- `root: "frontend"`, write the paths as the project itself sees them.
131
-
132
- Note `--config` alone does **not** change the root: `vite build --config
133
- apps/web/vite.config.ts` still leaves `root` at the directory vite was started
134
- from, so app-relative paths resolve against the monorepo root. Set `root` in the
135
- config file, or pass it positionally (`vite build apps/web --config …`), for
136
- those paths to mean what the app expects.
137
-
138
- `failOnError` decides what a failed canister does to the run. It defaults to
139
- `true` under `vite build` and `false` under `vite dev`: a build that quietly
140
- ships the bindings left over from the last successful run is worse than no
141
- build at all, while a dev server has to survive the broken intermediate states
142
- of a `.did` file being edited.
143
-
144
- ### Per-canister options
145
-
146
- - `name`
147
- - `didFile`
148
- - `outDir`
149
- - `clientManagerPath`
150
- - `target`
151
- - `mode`
152
- - `canisterId`
153
- - `factories`: also generate `index.factories.generated.ts` (default `false`)
154
-
155
- Each entry needs an output directory of its own. Two entries with the same
156
- `name` and no `outDir`, or with `outDir` values that reach one directory, would
157
- overwrite each other's output, so the plugin generates the first of them and
158
- fails the later one with the error the CLI reports. To generate one canister
159
- twice, say as a `DisplayReactor` and as a `Reactor`, give each entry its own
160
- `outDir`.
161
-
162
- Supported `mode` values:
163
-
164
- - `Reactor`
165
- - `DisplayReactor`
166
- - `CandidReactor`
167
- - `CandidDisplayReactor`
168
- - `MetadataDisplayReactor`
169
-
170
- Supported `target` values:
171
-
172
- - `react` (default): generates the reactor plus bound React hooks
173
- - `core`: generates only the typed reactor exports with no React dependency
96
+ The generator is given `icrc1.did` once, writes `src/canisters/icrc1.ts` once,
97
+ and a save of the file regenerates it once. (Naming a canister without a
98
+ `didFile` is the same thing when only the cookie is wanted.) Different `.did`
99
+ files that name the same module cannot share an `outDir`: `a/ledger.did` and
100
+ `b/ledger.did` both mean `ledger.ts`, and the second is refused with a message
101
+ naming both. Give one an `outDir` of its own. Names that differ only in case
102
+ (`Ledger.did` and `ledger.did`) collide only where the filesystem of the
103
+ `outDir` ignores case, as macOS and Windows do by default: the plugin checks
104
+ the filesystem and does not refuse them where it tells them apart.
105
+
106
+ ## Generation
107
+
108
+ The generator is WebAssembly, so the plugin does not load it: it runs the bin
109
+ script of the `@candid-core/cli` installed for your app (resolved from the Vite
110
+ root, so a monorepo's hoisted copy is found) with the running Node binary, in a
111
+ child process and not through a shell. A trap, a crash or a runaway loop on a
112
+ bad `.did` ends that process and the plugin reports it. The dev server is not
113
+ affected.
114
+
115
+ - Canisters that write into one `outDir` share one process, and each `outDir`
116
+ has its own. A process that dies without a report is run again, one
117
+ canister at a time and side by side, so the failure lands on the canister
118
+ that caused it.
119
+ - A process that runs longer than 60 seconds is killed, and counts as one that
120
+ died: when it was generating several canisters, each is run again alone and
121
+ only the one that hangs fails. A hang therefore costs up to two timeouts
122
+ (120 seconds) before the others are generated.
123
+ - What the generator writes to stderr is logged as a warning, a line at a time
124
+ and as it arrives, naming the `.did` files its process is generating. With
125
+ `--json` the generator reports in its JSON document, so what shows up here
126
+ is a crash's output or a warning from the runtime. When a process fails, its
127
+ stderr is also in the error message, so that text appears twice. Past 200
128
+ lines from one process the rest is not logged, so a runaway generator does
129
+ not flood the terminal.
130
+ - Closing the dev server (or restarting it, as a `vite.config` edit does) kills
131
+ a generator that is still running and drops the runs still waiting, so none
132
+ outlives the server it belonged to or runs beside the next one.
133
+ - A declaration the generator cannot represent is left out of the module, and
134
+ the plugin logs each one as a warning, for example
135
+ `ic-reactor: ledger: omitted declaration Bad (reserved_field_name)`.
136
+ - Editing a `.did` regenerates only the canisters that name it. Saves that
137
+ arrive while it runs collapse into one more run. `vite build --watch`
138
+ regenerates a canister only when its `.did` text changed.
139
+ - Deleting a `.did` under `vite dev` fails the canisters that name it at once,
140
+ in the log and the overlay: the module it generated is still on disk, and
141
+ would otherwise look current. Putting the file back regenerates them.
142
+
143
+ ### When generation fails
144
+
145
+ A failed canister costs that canister, and nothing else:
146
+
147
+ - Under `vite build` the build fails, with the generator's diagnostics (or its
148
+ stderr, if it crashed) in the error message. A build that exits 0 would ship
149
+ the bindings left over from the last good run.
150
+ - Under `vite dev` the failure is logged and shown in the browser's error
151
+ overlay, and the server keeps serving. The overlay lists the canisters that
152
+ are still broken: fixing one of several updates it, and fixing the last
153
+ clears it. Vite's client clears an error overlay whenever it applies a hot
154
+ update, as it does for Vite's own errors, so a canister that is still broken
155
+ can drop out of the browser's overlay after you save some other file. Its
156
+ error stays in the terminal log, and a full reload shows the overlay again.
157
+ - `failOnError` overrides either default.
158
+
159
+ ### In CI
160
+
161
+ `candid-core-cli gen --check` compares the generated files with the ones on
162
+ disk, writes nothing, and exits 1 on any difference. Run it with the same
163
+ arguments the plugin uses to fail a pipeline when committed output is stale:
164
+
165
+ ```json
166
+ {
167
+ "scripts": {
168
+ "gen:check": "candid-core-cli gen ../backend/ledger.did -o src/canisters --check"
169
+ }
170
+ }
171
+ ```
174
172
 
175
173
  ## Local Development Behavior
176
174
 
@@ -178,8 +176,8 @@ When `injectEnvironment` is enabled during `vite dev` or `vite preview`, the
178
176
  plugin:
179
177
 
180
178
  1. asks `icp` for the local network status
181
- 2. resolves canister IDs — `internet_identity` is added automatically if not
182
- already in your canister list
179
+ 2. resolves canister IDs: the keys of `canisters`, and `internet_identity`,
180
+ which is added automatically if not already listed
183
181
  3. sets the `ic_env` cookie on each response
184
182
  4. proxies `/api` to the local replica
185
183
 
@@ -213,38 +211,8 @@ If your Vite config or another plugin sets `server.proxy["/api"]`, the plugin
213
211
  leaves that entry alone, whether detection succeeds or not, and that proxy does
214
212
  not follow detection.
215
213
 
216
- ## File Regeneration
217
-
218
- On startup and on `.did` file changes, the plugin regenerates declarations and
219
- the managed `index.generated.ts` implementation, and `index.factories.generated.ts`
220
- for a canister that sets `factories: true`. The user-facing `index.ts`
221
- entry is created once, then preserved unless it still matches the default
222
- wrapper or a legacy generated scaffold that can be migrated automatically.
223
- When a watched `.did` file changes, the plugin sends a full browser reload so
224
- the new declarations are picked up.
225
-
226
- The plugin follows the dev server's file watcher itself, so a `.did` file that
227
- appears after the server started, or that a build tool deletes and writes
228
- again, is generated too, and saves regenerate even with `server.hmr: false`.
229
-
230
- Regeneration is serialized per canister — saves that land while a run is in
231
- flight collapse into a single rerun — so two rapid saves cannot interleave
232
- inside the pipeline's delete-then-write sequence.
233
- A regeneration that fails is reported to the terminal and to the browser error
234
- overlay rather than leaving the page on stale bindings.
235
-
236
- `vite build --watch` watches the configured `.did` files as well. Saving one
237
- starts a rebuild that regenerates that canister's bindings. A rebuild that any
238
- other file starts leaves the generated files alone.
239
-
240
- ## When To Use It
241
-
242
- - Vite apps with active `.did` iteration
243
- - teams that want zero extra codegen commands during development
244
- - projects that want the same output format as the CLI without manual steps
245
-
246
- ## See Also
214
+ ## Tests
247
215
 
248
- - Docs: https://ic-reactor.b3pay.net/v3/packages/vite-plugin
249
- - `@ic-reactor/codegen`: ../codegen/README.md
250
- - `@ic-reactor/cli`: ../cli/README.md
216
+ Vitest runs the plugin in mode `test`. There the plugin injects no environment
217
+ and never runs `icp`, whatever `injectEnvironment` says. Generation is not an
218
+ environment concern and still runs, so the modules a test imports exist.