@ic-reactor/vite-plugin 0.2.0 → 0.3.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
@@ -24,15 +24,11 @@ Automatically generate type-safe React hooks for your Internet Computer canister
24
24
  ## Installation
25
25
 
26
26
  ```bash
27
- # With npm
28
- npm install -D @ic-reactor/vite-plugin
29
-
30
27
  # With pnpm
31
28
  pnpm add -D @ic-reactor/vite-plugin
32
29
 
33
30
  # Required peer dependencies
34
- npm install @ic-reactor/react @tanstack/react-query @icp-sdk/core
35
- npm install -D @icp-sdk/bindgen
31
+ pnpm add @ic-reactor/react @tanstack/react-query @icp-sdk/core
36
32
  ```
37
33
 
38
34
  ## Quick Start
@@ -62,52 +58,30 @@ export default defineConfig({
62
58
 
63
59
  ### 2. Create Your ClientManager
64
60
 
65
- The plugin expects you to have a `ClientManager` exported from a file. By default, it looks for `./src/lib/client.ts`:
61
+ The plugin expects you to have a `ClientManager` exported from a file. By default, it looks for `./src/lib/clients.ts`:
66
62
 
67
63
  ```typescript
68
- // src/lib/client.ts
64
+ // src/lib/clients.ts
69
65
  import { ClientManager } from "@ic-reactor/react"
70
66
  import { QueryClient } from "@tanstack/react-query"
71
67
 
72
68
  export const queryClient = new QueryClient()
73
-
74
- export const clientManager = new ClientManager({
75
- queryClient,
76
- withCanisterEnv: true, // Enable environment-based canister ID resolution
77
- })
69
+ export const clientManager = new ClientManager({ queryClient })
78
70
  ```
79
71
 
80
72
  ### 3. Use Generated Hooks
81
73
 
82
- The plugin generates hooks in `./src/canisters/<name>/index.ts`:
74
+ The plugin generates an `index.ts` file in your canister folder (default: `./src/lib/canisters/<name>/index.ts`):
83
75
 
84
76
  ```tsx
85
- // Generated at: ./src/canisters/backend/index.ts
86
- import {
87
- backendReactor,
88
- useBackendQuery,
89
- useBackendMutation,
90
- useBackendSuspenseQuery,
91
- } from "./canisters/backend"
77
+ import { useActorQuery, useActorMutation } from "./canisters/backend"
92
78
 
93
79
  function MyComponent() {
94
- // Query data
95
- const { data, isPending } = useBackendQuery({
80
+ const { data, isPending } = useActorQuery({
96
81
  functionName: "get_message",
97
82
  })
98
83
 
99
- // Mutate data
100
- const { mutate } = useBackendMutation({
101
- functionName: "set_message",
102
- onSuccess: () => console.log("Message updated!"),
103
- })
104
-
105
- return (
106
- <div>
107
- <p>{isPending ? "Loading..." : data}</p>
108
- <button onClick={() => mutate(["Hello IC!"])}>Update</button>
109
- </div>
110
- )
84
+ return <p>{isPending ? "Loading..." : data}</p>
111
85
  }
112
86
  ```
113
87
 
@@ -115,123 +89,30 @@ function MyComponent() {
115
89
 
116
90
  ### Plugin Options
117
91
 
118
- ```typescript
119
- interface IcReactorPluginOptions {
120
- /** List of canisters to generate hooks for */
121
- canisters: CanisterConfig[]
122
- /** Base output directory (default: "./src/canisters") */
123
- outDir?: string
124
- /** Path to import ClientManager from (default: "../../lib/client") */
125
- clientManagerPath?: string
126
- }
127
-
128
- interface CanisterConfig {
129
- /** Name of the canister (used for variable naming) */
130
- name: string
131
- /** Path to the .did file */
132
- didFile: string
133
- /** Output directory (default: ./src/canisters/<name>) */
134
- outDir?: string
135
- /** Use DisplayReactor for React-friendly types (default: true) */
136
- useDisplayReactor?: boolean
137
- /** Path to import ClientManager from (relative to generated file) */
138
- clientManagerPath?: string
139
- }
140
- ```
141
-
142
- ### Example: Multiple Canisters
143
-
144
- ```typescript
145
- // vite.config.ts
146
- import { icReactorPlugin } from "@ic-reactor/vite-plugin"
147
-
148
- export default defineConfig({
149
- plugins: [
150
- icReactorPlugin({
151
- clientManagerPath: "@/lib/client", // Global default
152
- canisters: [
153
- {
154
- name: "backend",
155
- didFile: "./backend/backend.did",
156
- },
157
- {
158
- name: "ledger",
159
- didFile: "./ledger/ledger.did",
160
- useDisplayReactor: true, // BigInt → string, etc.
161
- },
162
- {
163
- name: "nft",
164
- didFile: "./nft/nft.did",
165
- outDir: "./src/services/nft", // Custom output
166
- clientManagerPath: "@/lib/nft-client", // Custom client
167
- },
168
- ],
169
- }),
170
- ],
171
- })
172
- ```
173
-
174
- ## Advanced Plugin
175
-
176
- For more granular control, use `icReactorAdvancedPlugin` which generates individual hooks per method:
92
+ | Option | Type | Description | Default |
93
+ | :------------------ | :----------------- | :--------------------------------------------------- | :---------------------- |
94
+ | `canisters` | `CanisterConfig[]` | List of canisters to generate hooks for. | `[]` |
95
+ | `outDir` | `string` | Base output directory for generated files. | `"./src/lib/canisters"` |
96
+ | `injectEnvironment` | `boolean` | Whether to inject canister IDs into `ic_env` cookie. | `true` |
97
+ | `clientManagerPath` | `string` | Path to a custom `ClientManager` instance. | `"../../clients"` |
177
98
 
178
- ```typescript
179
- import { icReactorAdvancedPlugin } from "@ic-reactor/vite-plugin"
99
+ ### Canister Config
180
100
 
181
- export default defineConfig({
182
- plugins: [
183
- icReactorAdvancedPlugin({
184
- canisters: [
185
- {
186
- name: "backend",
187
- didFile: "./backend/backend.did",
188
- },
189
- ],
190
- }),
191
- ],
192
- })
193
- ```
194
-
195
- This generates method-specific hooks:
196
-
197
- ```tsx
198
- import {
199
- useGetMessageQuery,
200
- useSetMessageMutation,
201
- getMessageQuery, // Static query for no-arg methods
202
- } from "./canisters/backend"
203
-
204
- function MyComponent() {
205
- // Method-specific hook
206
- const { data } = useGetMessageQuery([], { staleTime: 5000 })
207
-
208
- // Static query usage
209
- const { data: cached } = getMessageQuery.useQuery()
210
-
211
- const { mutate } = useSetMessageMutation()
212
-
213
- return <div>{data}</div>
214
- }
215
- ```
216
-
217
- ## Generated File Structure
218
-
219
- ```
220
- src/
221
- ├── canisters/
222
- │ └── backend/
223
- │ ├── index.ts # Reactor + hooks
224
- │ └── declarations/
225
- │ └── backend.did.ts # Type declarations
226
- ├── lib/
227
- │ └── client.ts # Your ClientManager (not generated)
228
- ```
101
+ | Option | Type | Description | Required |
102
+ | :------------------ | :-------- | :-------------------------------------------------- | :------- |
103
+ | `name` | `string` | Name of the canister (used in generated code). | Yes |
104
+ | `didFile` | `string` | Path to the `.did` file. | Yes |
105
+ | `outDir` | `string` | Override output directory for this canister. | No |
106
+ | `useDisplayReactor` | `boolean` | Use `DisplayReactor` instead of standard `Reactor`. | `true` |
107
+ | `clientManagerPath` | `string` | Override client manager path for this canister. | No |
229
108
 
230
109
  ## How It Works
231
110
 
232
- 1. **Build Start**: The plugin reads your `.did` files and uses `@icp-sdk/bindgen` to generate TypeScript declarations
233
- 2. **Code Generation**: Creates a reactor instance and typed hooks for each canister
234
- 3. **Hot Reload**: Watches for `.did` file changes and regenerates hooks automatically
111
+ 1. **Build Start**: The plugin reads your `.did` files and generates TypeScript declarations (`.js` and `.d.ts`).
112
+ 2. **Code Generation**: Creates a reactor instance and typed hooks (using `createActorHooks`) for each canister in `index.ts`.
113
+ 3. **Hot Reload**: Watches for `.did` file changes and regenerates everything automatically.
114
+ 4. **Local Proxy**: Configures a Vite proxy to redirect `/api` calls to your local replica.
115
+ 5. **Environment Detection**: Automatically injects canister IDs from `icp-cli` or `dfx` cache into your session.
235
116
 
236
117
  ## DisplayReactor vs Reactor
237
118
 
@@ -299,7 +180,7 @@ If you need to disable this behavior:
299
180
  ```typescript
300
181
  icReactorPlugin({
301
182
  canisters: [...],
302
- autoInjectIcEnv: false,
183
+ injectEnvironment: false,
303
184
  })
304
185
  ```
305
186
 
package/dist/index.cjs CHANGED
@@ -30,75 +30,141 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- default: () => index_default,
34
33
  icReactorPlugin: () => icReactorPlugin
35
34
  });
36
35
  module.exports = __toCommonJS(index_exports);
37
36
  var import_fs = __toESM(require("fs"), 1);
38
37
  var import_path = __toESM(require("path"), 1);
38
+ var import_child_process = require("child_process");
39
39
  var import_codegen = require("@ic-reactor/codegen");
40
- var ICP_LOCAL_IDS_PATH = ".icp/cache/mappings/local.ids.json";
41
- var IC_ROOT_KEY_HEX = "308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361008b52b4994f94c7ce4be1c1542d7c81dc79fea17d49efe8fa42e8566373581d4b969c4a59e96a0ef51b711fe5027ec01601182519d0a788f4bfe388e593b97cd1d7e44904de79422430bca686ac8c21305b3397b5ba4d7037d17877312fb7ee34";
42
- function loadLocalCanisterIds(rootDir) {
43
- const idsPath = import_path.default.resolve(rootDir, ICP_LOCAL_IDS_PATH);
40
+ function getIcEnvironmentInfo(canisterNames) {
41
+ const environment = process.env.ICP_ENVIRONMENT || "local";
44
42
  try {
45
- return JSON.parse(import_fs.default.readFileSync(idsPath, "utf-8"));
43
+ const networkStatus = JSON.parse(
44
+ (0, import_child_process.execFileSync)("icp", ["network", "status", "-e", environment, "--json"], {
45
+ encoding: "utf-8"
46
+ })
47
+ );
48
+ const rootKey = networkStatus.root_key;
49
+ const proxyTarget = `http://127.0.0.1:${networkStatus.port}`;
50
+ const canisterIds = {};
51
+ for (const name of canisterNames) {
52
+ try {
53
+ const canisterId = (0, import_child_process.execFileSync)(
54
+ "icp",
55
+ ["canister", "status", name, "-e", environment, "-i"],
56
+ {
57
+ encoding: "utf-8"
58
+ }
59
+ ).trim();
60
+ canisterIds[name] = canisterId;
61
+ } catch {
62
+ }
63
+ }
64
+ return { environment, rootKey, proxyTarget, canisterIds };
46
65
  } catch {
47
66
  return null;
48
67
  }
49
68
  }
50
- function buildIcEnvCookie(canisterIds) {
51
- const envParts = [`ic_root_key=${IC_ROOT_KEY_HEX}`];
69
+ function buildIcEnvCookie(canisterIds, rootKey) {
70
+ const envParts = [`ic_root_key=${rootKey}`];
52
71
  for (const [name, id] of Object.entries(canisterIds)) {
53
72
  envParts.push(`PUBLIC_CANISTER_ID:${name}=${id}`);
54
73
  }
55
74
  return encodeURIComponent(envParts.join("&"));
56
75
  }
57
- function addOrReplaceSetCookie(existing, cookie) {
58
- const cookieEntries = typeof existing === "string" ? [existing] : Array.isArray(existing) ? existing.filter((value) => typeof value === "string") : [];
59
- const nonIcEnvCookies = cookieEntries.filter(
60
- (entry) => !entry.trim().startsWith("ic_env=")
61
- );
62
- return [...nonIcEnvCookies, cookie];
63
- }
64
- function setupIcEnvMiddleware(server) {
65
- const rootDir = server.config.root || process.cwd();
66
- const idsPath = import_path.default.resolve(rootDir, ICP_LOCAL_IDS_PATH);
67
- let hasLoggedHint = false;
68
- server.middlewares.use((req, res, next) => {
69
- const canisterIds = loadLocalCanisterIds(rootDir);
70
- if (!canisterIds) {
71
- if (!hasLoggedHint) {
72
- server.config.logger.info(
73
- `[ic-reactor] icp-cli local IDs not found at ${idsPath}. Run \`icp deploy\` to enable automatic ic_env cookie injection.`
74
- );
75
- hasLoggedHint = true;
76
- }
77
- return next();
78
- }
79
- const cookie = `ic_env=${buildIcEnvCookie(canisterIds)}; Path=/; SameSite=Lax;`;
80
- const current = res.getHeader("Set-Cookie");
81
- res.setHeader("Set-Cookie", addOrReplaceSetCookie(current, cookie));
82
- next();
83
- });
84
- }
85
76
  function icReactorPlugin(options) {
86
- const baseOutDir = options.outDir ?? "./src/canisters";
77
+ const baseOutDir = options.outDir ?? "./src/lib/canisters";
87
78
  return {
88
79
  name: "ic-reactor-plugin",
89
- configureServer(server) {
90
- if (options.autoInjectIcEnv ?? true) {
91
- setupIcEnvMiddleware(server);
80
+ config(_config, { command }) {
81
+ if (command !== "serve" || !(options.injectEnvironment ?? true)) {
82
+ return {};
92
83
  }
84
+ const canisterNames = options.canisters.map((c) => c.name);
85
+ const icEnv = getIcEnvironmentInfo(canisterNames);
86
+ if (!icEnv) {
87
+ return {
88
+ server: {
89
+ proxy: {
90
+ "/api": {
91
+ target: "http://127.0.0.1:4943",
92
+ changeOrigin: true
93
+ }
94
+ }
95
+ }
96
+ };
97
+ }
98
+ const cookieValue = buildIcEnvCookie(icEnv.canisterIds, icEnv.rootKey);
99
+ return {
100
+ server: {
101
+ headers: {
102
+ "Set-Cookie": `ic_env=${cookieValue}; Path=/; SameSite=Lax;`
103
+ },
104
+ proxy: {
105
+ "/api": {
106
+ target: icEnv.proxyTarget,
107
+ changeOrigin: true
108
+ }
109
+ }
110
+ }
111
+ };
93
112
  },
94
113
  async buildStart() {
114
+ const defaultClientPath = import_path.default.resolve(
115
+ process.cwd(),
116
+ "src/lib/clients.ts"
117
+ );
118
+ if (!import_fs.default.existsSync(defaultClientPath)) {
119
+ console.log(
120
+ `[ic-reactor] Default client manager not found. Creating at ${defaultClientPath}`
121
+ );
122
+ const clientContent = (0, import_codegen.generateClientFile)();
123
+ import_fs.default.mkdirSync(import_path.default.dirname(defaultClientPath), { recursive: true });
124
+ import_fs.default.writeFileSync(defaultClientPath, clientContent);
125
+ }
95
126
  for (const canister of options.canisters) {
127
+ let didFile = canister.didFile;
96
128
  const outDir = canister.outDir ?? import_path.default.join(baseOutDir, canister.name);
129
+ if (!didFile) {
130
+ const environment = process.env.ICP_ENVIRONMENT || "local";
131
+ console.log(
132
+ `[ic-reactor] didFile not specified for "${canister.name}". Attempting to download from canister...`
133
+ );
134
+ try {
135
+ const candidContent = (0, import_child_process.execFileSync)(
136
+ "icp",
137
+ [
138
+ "canister",
139
+ "metadata",
140
+ canister.name,
141
+ "candid:service",
142
+ "-e",
143
+ environment
144
+ ],
145
+ { encoding: "utf-8" }
146
+ ).trim();
147
+ const declarationsDir = import_path.default.join(outDir, "declarations");
148
+ if (!import_fs.default.existsSync(declarationsDir)) {
149
+ import_fs.default.mkdirSync(declarationsDir, { recursive: true });
150
+ }
151
+ didFile = import_path.default.join(declarationsDir, `${canister.name}.did`);
152
+ import_fs.default.writeFileSync(didFile, candidContent);
153
+ console.log(
154
+ `[ic-reactor] Candid downloaded and saved to ${didFile}`
155
+ );
156
+ } catch (error) {
157
+ console.error(
158
+ `[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
159
+ );
160
+ continue;
161
+ }
162
+ }
97
163
  console.log(
98
- `[ic-reactor] Generating hooks for ${canister.name} from ${canister.didFile}`
164
+ `[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
99
165
  );
100
166
  const result = await (0, import_codegen.generateDeclarations)({
101
- didFile: canister.didFile,
167
+ didFile,
102
168
  outDir,
103
169
  canisterName: canister.name
104
170
  });
@@ -108,28 +174,10 @@ function icReactorPlugin(options) {
108
174
  );
109
175
  continue;
110
176
  }
111
- console.log(
112
- `[ic-reactor] Declarations generated at ${result.declarationsDir}`
113
- );
114
- const useAdvanced = canister.advanced ?? options.advanced ?? false;
115
- let didContent;
116
- if (useAdvanced) {
117
- try {
118
- didContent = import_fs.default.readFileSync(canister.didFile, "utf-8");
119
- } catch (e) {
120
- console.warn(
121
- `[ic-reactor] Could not read DID file at ${canister.didFile}, skipping advanced hook generation for ${canister.name}.`
122
- );
123
- continue;
124
- }
125
- }
126
177
  const reactorContent = (0, import_codegen.generateReactorFile)({
127
178
  canisterName: canister.name,
128
- canisterConfig: canister,
129
- globalClientManagerPath: options.clientManagerPath,
130
- hasDeclarations: true,
131
- advanced: useAdvanced,
132
- didContent
179
+ didFile,
180
+ clientManagerPath: canister.clientManagerPath ?? options.clientManagerPath
133
181
  });
134
182
  const reactorPath = import_path.default.join(outDir, "index.ts");
135
183
  import_fs.default.mkdirSync(outDir, { recursive: true });
@@ -139,9 +187,10 @@ function icReactorPlugin(options) {
139
187
  },
140
188
  handleHotUpdate({ file, server }) {
141
189
  if (file.endsWith(".did")) {
142
- const canister = options.canisters.find(
143
- (c) => import_path.default.resolve(c.didFile) === file
144
- );
190
+ const canister = options.canisters.find((c) => {
191
+ if (!c.didFile) return false;
192
+ return import_path.default.resolve(c.didFile) === file;
193
+ });
145
194
  if (canister) {
146
195
  console.log(
147
196
  `[ic-reactor] Detected change in ${file}, regenerating...`
@@ -152,7 +201,6 @@ function icReactorPlugin(options) {
152
201
  }
153
202
  };
154
203
  }
155
- var index_default = icReactorPlugin;
156
204
  // Annotate the CommonJS export names for ESM import in node:
157
205
  0 && (module.exports = {
158
206
  icReactorPlugin
package/dist/index.d.cts CHANGED
@@ -1,6 +1,4 @@
1
1
  import { Plugin } from 'vite';
2
- import { CanisterConfig } from '@ic-reactor/codegen';
3
- export { CanisterConfig } from '@ic-reactor/codegen';
4
2
 
5
3
  /**
6
4
  * IC-Reactor Vite Plugin
@@ -28,32 +26,30 @@ export { CanisterConfig } from '@ic-reactor/codegen';
28
26
  * ```
29
27
  */
30
28
 
29
+ interface CanisterConfig {
30
+ name: string;
31
+ outDir?: string;
32
+ didFile?: string;
33
+ clientManagerPath?: string;
34
+ }
31
35
  interface IcReactorPluginOptions {
32
36
  /** List of canisters to generate hooks for */
33
- canisters: (CanisterConfig & {
34
- name: string;
35
- advanced?: boolean;
36
- })[];
37
- /** Base output directory (default: ./src/canisters) */
37
+ canisters: CanisterConfig[];
38
+ /** Base output directory (default: ./src/lib/canisters) */
38
39
  outDir?: string;
39
40
  /**
40
41
  * Path to import ClientManager from (relative to generated file).
41
- * Default: "../../lib/client"
42
+ * Default: "../../clients"
42
43
  */
43
44
  clientManagerPath?: string;
44
45
  /**
45
- * Generate advanced per-method hooks with createQuery/createMutation
46
- * instead of generic actor hooks (default: false).
47
- * Can be overridden per-canister by setting `advanced` on the individual canister entry.
46
+ * Automatically inject the IC environment (canister IDs and root key)
47
+ * into the browser using an `ic_env` cookie. (default: true)
48
+ *
49
+ * This is useful for local development with `icp`.
48
50
  */
49
- advanced?: boolean;
50
- /**
51
- * Automatically set the `ic_env` cookie in Vite dev server from
52
- * `.icp/cache/mappings/local.ids.json` (default: true).
53
- */
54
- autoInjectIcEnv?: boolean;
51
+ injectEnvironment?: boolean;
55
52
  }
56
-
57
53
  declare function icReactorPlugin(options: IcReactorPluginOptions): Plugin;
58
54
 
59
- export { type IcReactorPluginOptions, icReactorPlugin as default, icReactorPlugin };
55
+ export { type CanisterConfig, type IcReactorPluginOptions, icReactorPlugin };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,4 @@
1
1
  import { Plugin } from 'vite';
2
- import { CanisterConfig } from '@ic-reactor/codegen';
3
- export { CanisterConfig } from '@ic-reactor/codegen';
4
2
 
5
3
  /**
6
4
  * IC-Reactor Vite Plugin
@@ -28,32 +26,30 @@ export { CanisterConfig } from '@ic-reactor/codegen';
28
26
  * ```
29
27
  */
30
28
 
29
+ interface CanisterConfig {
30
+ name: string;
31
+ outDir?: string;
32
+ didFile?: string;
33
+ clientManagerPath?: string;
34
+ }
31
35
  interface IcReactorPluginOptions {
32
36
  /** List of canisters to generate hooks for */
33
- canisters: (CanisterConfig & {
34
- name: string;
35
- advanced?: boolean;
36
- })[];
37
- /** Base output directory (default: ./src/canisters) */
37
+ canisters: CanisterConfig[];
38
+ /** Base output directory (default: ./src/lib/canisters) */
38
39
  outDir?: string;
39
40
  /**
40
41
  * Path to import ClientManager from (relative to generated file).
41
- * Default: "../../lib/client"
42
+ * Default: "../../clients"
42
43
  */
43
44
  clientManagerPath?: string;
44
45
  /**
45
- * Generate advanced per-method hooks with createQuery/createMutation
46
- * instead of generic actor hooks (default: false).
47
- * Can be overridden per-canister by setting `advanced` on the individual canister entry.
46
+ * Automatically inject the IC environment (canister IDs and root key)
47
+ * into the browser using an `ic_env` cookie. (default: true)
48
+ *
49
+ * This is useful for local development with `icp`.
48
50
  */
49
- advanced?: boolean;
50
- /**
51
- * Automatically set the `ic_env` cookie in Vite dev server from
52
- * `.icp/cache/mappings/local.ids.json` (default: true).
53
- */
54
- autoInjectIcEnv?: boolean;
51
+ injectEnvironment?: boolean;
55
52
  }
56
-
57
53
  declare function icReactorPlugin(options: IcReactorPluginOptions): Plugin;
58
54
 
59
- export { type IcReactorPluginOptions, icReactorPlugin as default, icReactorPlugin };
55
+ export { type CanisterConfig, type IcReactorPluginOptions, icReactorPlugin };