@ic-reactor/vite-plugin 0.7.1 → 0.8.0

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,65 +1,35 @@
1
1
  # @ic-reactor/vite-plugin
2
2
 
3
- <div align="center">
4
- <strong>Zero-config Vite plugin for auto-generating IC reactor hooks from Candid files.</strong>
5
- <br><br>
6
-
7
- [![npm version](https://img.shields.io/npm/v/@ic-reactor/vite-plugin.svg)](https://www.npmjs.com/package/@ic-reactor/vite-plugin)
8
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
10
- </div>
3
+ Vite plugin for IC Reactor code generation. It runs the shared
4
+ `@ic-reactor/codegen` pipeline, watches `.did` files, and can inject the
5
+ `ic_env` cookie used by `ClientManager` during local development.
11
6
 
12
- ---
13
-
14
- Automatically generate type-safe React hooks for your Internet Computer canisters. This plugin watches your `.did` files and generates ready-to-use hooks with full TypeScript support using the shared `@ic-reactor/codegen` pipeline.
15
-
16
- ## Features
17
-
18
- - ⚡ **Zero Config** — Point to your `.did` file and get hooks instantly
19
- - 🔄 **Hot Reload** — Automatically regenerates hooks and types when `.did` files change
20
- - 📦 **TypeScript Declarations** — Full built-in type safety
21
- - 🌍 **Auto Environment** — Automatically detects local replica and injects `ic_env` cookie
22
-
23
- ## Installation
7
+ ## Install
24
8
 
25
9
  ```bash
26
- # With pnpm
27
10
  pnpm add -D @ic-reactor/vite-plugin
28
-
29
- # Required peer dependencies
30
11
  pnpm add @ic-reactor/react @tanstack/react-query @icp-sdk/core
31
12
  ```
32
13
 
33
14
  ## Quick Start
34
15
 
35
- ### 1. Configure the Plugin
36
-
37
- ```typescript
16
+ ```ts
38
17
  // vite.config.ts
39
18
  import { defineConfig } from "vite"
40
19
  import react from "@vitejs/plugin-react"
41
- import { icReactorPlugin } from "@ic-reactor/vite-plugin"
20
+ import { icReactor } from "@ic-reactor/vite-plugin"
42
21
 
43
22
  export default defineConfig({
44
23
  plugins: [
45
24
  react(),
46
- icReactorPlugin({
47
- canisters: [
48
- {
49
- name: "backend",
50
- didFile: "./backend/backend.did",
51
- },
52
- ],
25
+ icReactor({
26
+ canisters: [{ name: "backend", didFile: "./backend/backend.did" }],
53
27
  }),
54
28
  ],
55
29
  })
56
30
  ```
57
31
 
58
- ### 2. Create Your ClientManager
59
-
60
- The plugin looks for a client manager to import. By default, it expects it at `../../clients` relative to the generated files.
61
-
62
- ```typescript
32
+ ```ts
63
33
  // src/clients.ts
64
34
  import { ClientManager } from "@ic-reactor/react"
65
35
  import { QueryClient } from "@tanstack/react-query"
@@ -67,57 +37,75 @@ import { QueryClient } from "@tanstack/react-query"
67
37
  export const queryClient = new QueryClient()
68
38
  export const clientManager = new ClientManager({
69
39
  queryClient,
70
- withCanisterEnv: true, // Important for cookie injection support
40
+ withCanisterEnv: true,
71
41
  })
72
42
  ```
73
43
 
74
- ### 3. Use Generated Hooks
44
+ The plugin generates files under `src/declarations/<canister>/` by default.
45
+
46
+ ## Options
75
47
 
76
- The plugin generates headers in `src/declarations/<name>/index.ts` by default.
48
+ ```ts
49
+ icReactor({
50
+ canisters: [
51
+ {
52
+ name: "backend",
53
+ didFile: "./backend/backend.did",
54
+ mode: "DisplayReactor",
55
+ },
56
+ ],
57
+ outDir: "src/declarations",
58
+ clientManagerPath: "../../clients",
59
+ injectEnvironment: true,
60
+ })
61
+ ```
77
62
 
78
- ```tsx
79
- import { useBackendQuery } from "./declarations/backend"
63
+ ### Per-canister options
80
64
 
81
- function MyComponent() {
82
- const { data, isPending } = useBackendQuery({
83
- functionName: "get_message",
84
- })
65
+ - `name`
66
+ - `didFile`
67
+ - `outDir`
68
+ - `clientManagerPath`
69
+ - `mode`
70
+ - `canisterId`
85
71
 
86
- return <p>{isPending ? "Loading..." : data}</p>
87
- }
88
- ```
72
+ Supported `mode` values:
89
73
 
90
- ## Configuration
74
+ - `Reactor`
75
+ - `DisplayReactor`
76
+ - `CandidReactor`
77
+ - `CandidDisplayReactor`
78
+ - `MetadataDisplayReactor`
91
79
 
92
- ### Plugin Options
80
+ ## Local Development Behavior
93
81
 
94
- | Option | Type | Description | Default |
95
- | :------------------ | :----------------- | :-------------------------------------------------- | :------------------- |
96
- | `canisters` | `CanisterConfig[]` | List of canisters to generate hooks for (required). | - |
97
- | `outDir` | `string` | Base output directory for generated files. | `"src/declarations"` |
98
- | `clientManagerPath` | `string` | Path to client manager import. | `"../../clients"` |
99
- | `injectEnvironment` | `boolean` | Inject `ic_env` cookie for local development. | `true` |
82
+ When `injectEnvironment` is enabled during `vite dev`, the plugin:
100
83
 
101
- ### Canister Config
84
+ 1. asks `icp-cli` for the local network status
85
+ 2. resolves configured canister IDs
86
+ 3. sets the `ic_env` cookie
87
+ 4. proxies `/api` to the local replica
102
88
 
103
- | Option | Type | Description | Required |
104
- | :------------------ | :------- | :---------------------------------------------- | :------- |
105
- | `name` | `string` | Name of the canister (used for variable names). | Yes |
106
- | `didFile` | `string` | Path to the `.did` file. | Yes |
107
- | `outDir` | `string` | Override output directory for this canister. | No |
108
- | `clientManagerPath` | `string` | Override client manager path. | No |
109
- | `canisterId` | `string` | Optional fixed canister ID. | No |
89
+ If environment detection fails, the plugin still falls back to proxying `/api`
90
+ to `http://127.0.0.1:4943`, but it will not inject canister metadata.
110
91
 
111
- ## Local Development (Environment Injection)
92
+ ## File Regeneration
112
93
 
113
- When running `vite dev`, the plugin automatically handles local canister environment connection:
94
+ On startup and on `.did` file changes, the plugin regenerates declarations and
95
+ the managed `index.generated.ts` implementation. The user-facing `index.ts`
96
+ entry is created once, then preserved unless it still matches the default
97
+ wrapper or a legacy generated scaffold that can be migrated automatically.
98
+ When a watched `.did` file changes, the plugin sends a full browser reload so
99
+ the new declarations are picked up.
114
100
 
115
- 1. Detects your local environment (using `icp` or `dfx` CLI).
116
- 2. Sets an `ic_env` cookie containing the root key and canister IDs.
117
- 3. Sets up a proxy for `/api` to your local replica.
101
+ ## When To Use It
118
102
 
119
- This means you **don't** need complex `vite.config.ts` proxy rules or manual `.env` file management for local addresses — it just works.
103
+ - Vite apps with active `.did` iteration
104
+ - teams that want zero extra codegen commands during development
105
+ - projects that want the same output format as the CLI without manual steps
120
106
 
121
- ## License
107
+ ## See Also
122
108
 
123
- MIT © [Behrad Deylami](https://github.com/b3hr4d)
109
+ - Docs: https://ic-reactor.b3pay.net/v3/packages/vite-plugin
110
+ - `@ic-reactor/codegen`: ../codegen/README.md
111
+ - `@ic-reactor/cli`: ../cli/README.md
package/dist/index.cjs CHANGED
@@ -30,7 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- icReactorPlugin: () => icReactorPlugin
33
+ icReactor: () => icReactor
34
34
  });
35
35
  module.exports = __toCommonJS(index_exports);
36
36
  var import_node_path = __toESM(require("path"), 1);
@@ -78,7 +78,7 @@ function buildIcEnvCookie(canisterIds, rootKey) {
78
78
  }
79
79
 
80
80
  // src/index.ts
81
- function icReactorPlugin(options) {
81
+ function icReactor(options) {
82
82
  const {
83
83
  canisters,
84
84
  outDir = "src/declarations",
@@ -93,6 +93,9 @@ function icReactorPlugin(options) {
93
93
  const resolveDidPath = (didFile) => import_node_path.default.normalize(
94
94
  import_node_path.default.isAbsolute(didFile) ? didFile : import_node_path.default.resolve(projectRoot, didFile)
95
95
  );
96
+ const configuredCanisterIds = Object.fromEntries(
97
+ canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
98
+ );
96
99
  const plugin = {
97
100
  name: "ic-reactor-plugin",
98
101
  enforce: "pre",
@@ -118,7 +121,13 @@ function icReactorPlugin(options) {
118
121
  }
119
122
  };
120
123
  }
121
- const cookieValue = buildIcEnvCookie(icEnv.canisterIds, icEnv.rootKey);
124
+ const cookieValue = buildIcEnvCookie(
125
+ {
126
+ ...icEnv.canisterIds,
127
+ ...configuredCanisterIds
128
+ },
129
+ icEnv.rootKey
130
+ );
122
131
  return {
123
132
  server: {
124
133
  headers: {
@@ -191,5 +200,5 @@ function icReactorPlugin(options) {
191
200
  }
192
201
  // Annotate the CommonJS export names for ESM import in node:
193
202
  0 && (module.exports = {
194
- icReactorPlugin
203
+ icReactor
195
204
  });
package/dist/index.d.cts CHANGED
@@ -31,6 +31,6 @@ interface IcReactorPluginOptions {
31
31
  */
32
32
  injectEnvironment?: boolean;
33
33
  }
34
- declare function icReactorPlugin(options: IcReactorPluginOptions): any;
34
+ declare function icReactor(options: IcReactorPluginOptions): any;
35
35
 
36
- export { type IcReactorPluginOptions, icReactorPlugin };
36
+ export { type IcReactorPluginOptions, icReactor };
package/dist/index.d.ts CHANGED
@@ -31,6 +31,6 @@ interface IcReactorPluginOptions {
31
31
  */
32
32
  injectEnvironment?: boolean;
33
33
  }
34
- declare function icReactorPlugin(options: IcReactorPluginOptions): any;
34
+ declare function icReactor(options: IcReactorPluginOptions): any;
35
35
 
36
- export { type IcReactorPluginOptions, icReactorPlugin };
36
+ export { type IcReactorPluginOptions, icReactor };
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@ function buildIcEnvCookie(canisterIds, rootKey) {
46
46
  }
47
47
 
48
48
  // src/index.ts
49
- function icReactorPlugin(options) {
49
+ function icReactor(options) {
50
50
  const {
51
51
  canisters,
52
52
  outDir = "src/declarations",
@@ -61,6 +61,9 @@ function icReactorPlugin(options) {
61
61
  const resolveDidPath = (didFile) => path.normalize(
62
62
  path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
63
63
  );
64
+ const configuredCanisterIds = Object.fromEntries(
65
+ canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
66
+ );
64
67
  const plugin = {
65
68
  name: "ic-reactor-plugin",
66
69
  enforce: "pre",
@@ -86,7 +89,13 @@ function icReactorPlugin(options) {
86
89
  }
87
90
  };
88
91
  }
89
- const cookieValue = buildIcEnvCookie(icEnv.canisterIds, icEnv.rootKey);
92
+ const cookieValue = buildIcEnvCookie(
93
+ {
94
+ ...icEnv.canisterIds,
95
+ ...configuredCanisterIds
96
+ },
97
+ icEnv.rootKey
98
+ );
90
99
  return {
91
100
  server: {
92
101
  headers: {
@@ -158,5 +167,5 @@ function icReactorPlugin(options) {
158
167
  return plugin;
159
168
  }
160
169
  export {
161
- icReactorPlugin
170
+ icReactor
162
171
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/vite-plugin",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Vite plugin for zero-config IC reactor generation from Candid files",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -37,14 +37,14 @@
37
37
  "url": "https://github.com/B3Pay/ic-reactor/issues"
38
38
  },
39
39
  "dependencies": {
40
- "@ic-reactor/codegen": "0.7.1"
40
+ "@ic-reactor/codegen": "0.8.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
44
44
  },
45
45
  "devDependencies": {
46
- "@icp-sdk/bindgen": "^0.2.1",
47
- "@types/node": "^25.2.3",
46
+ "@icp-sdk/bindgen": "^0.2.2",
47
+ "@types/node": "^25.3.5",
48
48
  "tsup": "^8.5.1",
49
49
  "typescript": "^5.9.3",
50
50
  "vitest": "^4.0.18"
package/src/index.test.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest"
2
- import { icReactorPlugin, type IcReactorPluginOptions } from "./index"
2
+ import * as vitePluginModule from "./index"
3
+ import type { IcReactorPluginOptions } from "./index"
3
4
  import path from "path"
4
5
  import { execFileSync } from "child_process"
5
6
  import { runCanisterPipeline } from "@ic-reactor/codegen"
6
7
 
8
+ const createVitePlugin =
9
+ (vitePluginModule as any).icReactor ??
10
+ (vitePluginModule as any).icReactorPlugin
11
+
7
12
  // Mock internal dependencies
8
13
  vi.mock("@ic-reactor/codegen", () => ({
9
14
  runCanisterPipeline: vi.fn(),
@@ -24,7 +29,7 @@ vi.mock("fs", () => ({
24
29
  },
25
30
  }))
26
31
 
27
- describe("icReactorPlugin", () => {
32
+ describe("icReactor", () => {
28
33
  const mockOptions: IcReactorPluginOptions = {
29
34
  canisters: [
30
35
  {
@@ -63,7 +68,7 @@ describe("icReactorPlugin", () => {
63
68
  })
64
69
 
65
70
  it("should return correct plugin structure", () => {
66
- const plugin = icReactorPlugin(mockOptions)
71
+ const plugin = createVitePlugin(mockOptions)
67
72
  expect(plugin.name).toBe("ic-reactor-plugin")
68
73
  expect(plugin.buildStart).toBeDefined()
69
74
  expect(plugin.handleHotUpdate).toBeDefined()
@@ -92,7 +97,7 @@ describe("icReactorPlugin", () => {
92
97
  }
93
98
  )
94
99
 
95
- const plugin = icReactorPlugin(mockOptions)
100
+ const plugin = createVitePlugin(mockOptions)
96
101
  const config = (plugin as any).config({}, { command: "serve" })
97
102
 
98
103
  expect(config.server.headers["Set-Cookie"]).toContain("ic_env=")
@@ -105,12 +110,52 @@ describe("icReactorPlugin", () => {
105
110
  expect(config.server.proxy["/api"].target).toBe("http://127.0.0.1:4943")
106
111
  })
107
112
 
113
+ it("should prefer configured canisterId over CLI-discovered env values", () => {
114
+ ;(execFileSync as any).mockImplementation(
115
+ (command: string, args: string[], options: any) => {
116
+ if (
117
+ command === "icp" &&
118
+ args.includes("network") &&
119
+ args.includes("status")
120
+ ) {
121
+ return JSON.stringify({ root_key: "mock-root-key", port: 4943 })
122
+ }
123
+ if (
124
+ command === "icp" &&
125
+ args.includes("canister") &&
126
+ args.includes("status")
127
+ ) {
128
+ return "local-cli-canister-id"
129
+ }
130
+ return ""
131
+ }
132
+ )
133
+
134
+ const plugin = createVitePlugin({
135
+ ...mockOptions,
136
+ canisters: [
137
+ {
138
+ ...mockOptions.canisters[0],
139
+ canisterId: "yq4ns-hyaaa-aaaap-akbna-cai",
140
+ },
141
+ ],
142
+ })
143
+ const config = (plugin as any).config({}, { command: "serve" })
144
+
145
+ expect(config.server.headers["Set-Cookie"]).toContain(
146
+ "PUBLIC_CANISTER_ID%3Atest_canister%3Dyq4ns-hyaaa-aaaap-akbna-cai"
147
+ )
148
+ expect(config.server.headers["Set-Cookie"]).not.toContain(
149
+ "PUBLIC_CANISTER_ID%3Atest_canister%3Dlocal-cli-canister-id"
150
+ )
151
+ })
152
+
108
153
  it("should fallback to default proxy when icp-cli fails", () => {
109
154
  ;(execFileSync as any).mockImplementation(() => {
110
155
  throw new Error("Command not found")
111
156
  })
112
157
 
113
- const plugin = icReactorPlugin(mockOptions)
158
+ const plugin = createVitePlugin(mockOptions)
114
159
  const config = (plugin as any).config({}, { command: "serve" })
115
160
 
116
161
  expect(config.server.headers).toBeUndefined()
@@ -118,7 +163,7 @@ describe("icReactorPlugin", () => {
118
163
  })
119
164
 
120
165
  it("should return empty config for build command", () => {
121
- const plugin = icReactorPlugin(mockOptions)
166
+ const plugin = createVitePlugin(mockOptions)
122
167
  const config = (plugin as any).config({}, { command: "build" })
123
168
 
124
169
  expect(config).toEqual({})
@@ -127,7 +172,7 @@ describe("icReactorPlugin", () => {
127
172
 
128
173
  describe("buildStart", () => {
129
174
  it("should generate declarations and reactor file", async () => {
130
- const plugin = icReactorPlugin(mockOptions)
175
+ const plugin = createVitePlugin(mockOptions)
131
176
  await (plugin.buildStart as any)()
132
177
 
133
178
  expect(runCanisterPipeline).toHaveBeenCalledWith({
@@ -150,7 +195,7 @@ describe("icReactorPlugin", () => {
150
195
  error: "Failed to generate",
151
196
  })
152
197
 
153
- const plugin = icReactorPlugin(mockOptions)
198
+ const plugin = createVitePlugin(mockOptions)
154
199
  await (plugin.buildStart as any)()
155
200
 
156
201
  expect(consoleErrorSpy).toHaveBeenCalledWith(
@@ -159,11 +204,37 @@ describe("icReactorPlugin", () => {
159
204
 
160
205
  consoleErrorSpy.mockRestore()
161
206
  })
207
+
208
+ it("should pass canister mode through to codegen via canister config", async () => {
209
+ const plugin = createVitePlugin({
210
+ canisters: [
211
+ {
212
+ ...mockOptions.canisters[0],
213
+ mode: "Reactor",
214
+ },
215
+ ],
216
+ outDir: mockOptions.outDir,
217
+ })
218
+
219
+ await (plugin.buildStart as any)()
220
+
221
+ expect(runCanisterPipeline).toHaveBeenCalledWith({
222
+ canisterConfig: {
223
+ ...mockOptions.canisters[0],
224
+ mode: "Reactor",
225
+ },
226
+ projectRoot: expect.any(String),
227
+ globalConfig: {
228
+ outDir: "src/declarations",
229
+ clientManagerPath: "../../clients",
230
+ },
231
+ })
232
+ })
162
233
  })
163
234
 
164
235
  describe("handleHotUpdate", () => {
165
236
  it("should register configured .did files with the watcher", () => {
166
- const plugin = icReactorPlugin(mockOptions)
237
+ const plugin = createVitePlugin(mockOptions)
167
238
  ;(plugin.configureServer as any)(mockServer)
168
239
 
169
240
  expect(mockServer.watcher.add).toHaveBeenCalledWith([
@@ -172,7 +243,7 @@ describe("icReactorPlugin", () => {
172
243
  })
173
244
 
174
245
  it("should restart server when .did file changes", async () => {
175
- const plugin = icReactorPlugin(mockOptions)
246
+ const plugin = createVitePlugin(mockOptions)
176
247
  const ctx = {
177
248
  file: "/absolute/path/to/src/declarations/test.did",
178
249
  server: mockServer,
@@ -193,7 +264,7 @@ describe("icReactorPlugin", () => {
193
264
  })
194
265
 
195
266
  it("should ignore other files", () => {
196
- const plugin = icReactorPlugin(mockOptions)
267
+ const plugin = createVitePlugin(mockOptions)
197
268
  const ctx = {
198
269
  file: "/some/other/file.ts",
199
270
  server: mockServer,
package/src/index.ts CHANGED
@@ -39,7 +39,7 @@ export interface IcReactorPluginOptions {
39
39
  injectEnvironment?: boolean
40
40
  }
41
41
 
42
- export function icReactorPlugin(options: IcReactorPluginOptions): any {
42
+ export function icReactor(options: IcReactorPluginOptions): any {
43
43
  const {
44
44
  canisters,
45
45
  outDir = "src/declarations",
@@ -57,6 +57,11 @@ export function icReactorPlugin(options: IcReactorPluginOptions): any {
57
57
  path.normalize(
58
58
  path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
59
59
  )
60
+ const configuredCanisterIds = Object.fromEntries(
61
+ canisters
62
+ .filter((canister) => !!canister.canisterId)
63
+ .map((canister) => [canister.name, canister.canisterId as string])
64
+ )
60
65
 
61
66
  const plugin: Plugin = {
62
67
  name: "ic-reactor-plugin",
@@ -93,7 +98,13 @@ export function icReactorPlugin(options: IcReactorPluginOptions): any {
93
98
  }
94
99
  }
95
100
 
96
- const cookieValue = buildIcEnvCookie(icEnv.canisterIds, icEnv.rootKey)
101
+ const cookieValue = buildIcEnvCookie(
102
+ {
103
+ ...icEnv.canisterIds,
104
+ ...configuredCanisterIds,
105
+ },
106
+ icEnv.rootKey
107
+ )
97
108
 
98
109
  return {
99
110
  server: {