@ic-reactor/vite-plugin 0.4.1 → 0.5.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/dist/index.js CHANGED
@@ -1,18 +1,19 @@
1
1
  // src/index.ts
2
- import fs from "fs";
3
2
  import path from "path";
4
- import { execFileSync } from "child_process";
5
3
  import {
6
- generateDeclarations,
7
- generateReactorFile,
8
- generateClientFile
4
+ runCanisterPipeline
9
5
  } from "@ic-reactor/codegen";
6
+
7
+ // src/env.ts
8
+ import { execFileSync } from "child_process";
10
9
  function getIcEnvironmentInfo(canisterNames) {
11
10
  const environment = process.env.ICP_ENVIRONMENT || "local";
12
11
  try {
13
12
  const networkStatus = JSON.parse(
14
13
  execFileSync("icp", ["network", "status", "-e", environment, "--json"], {
15
- encoding: "utf-8"
14
+ encoding: "utf-8",
15
+ stdio: ["ignore", "pipe", "ignore"]
16
+ // suppress stderr
16
17
  })
17
18
  );
18
19
  const rootKey = networkStatus.root_key;
@@ -23,35 +24,48 @@ function getIcEnvironmentInfo(canisterNames) {
23
24
  const canisterId = execFileSync(
24
25
  "icp",
25
26
  ["canister", "status", name, "-e", environment, "-i"],
26
- {
27
- encoding: "utf-8"
28
- }
27
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
29
28
  ).trim();
30
- canisterIds[name] = canisterId;
29
+ if (canisterId) {
30
+ canisterIds[name] = canisterId;
31
+ }
31
32
  } catch {
32
33
  }
33
34
  }
34
35
  return { environment, rootKey, proxyTarget, canisterIds };
35
- } catch {
36
+ } catch (error) {
36
37
  return null;
37
38
  }
38
39
  }
39
40
  function buildIcEnvCookie(canisterIds, rootKey) {
40
- const envParts = [`ic_root_key=${rootKey}`];
41
+ const parts = [`ic_root_key=${rootKey}`];
41
42
  for (const [name, id] of Object.entries(canisterIds)) {
42
- envParts.push(`PUBLIC_CANISTER_ID:${name}=${id}`);
43
+ parts.push(`PUBLIC_CANISTER_ID:${name}=${id}`);
43
44
  }
44
- return encodeURIComponent(envParts.join("&"));
45
+ return encodeURIComponent(parts.join("&"));
45
46
  }
47
+
48
+ // src/index.ts
46
49
  function icReactorPlugin(options) {
47
- const baseOutDir = options.outDir ?? "./src/lib/canisters";
48
- return {
50
+ const {
51
+ canisters,
52
+ outDir = "src/declarations",
53
+ clientManagerPath = "../../clients",
54
+ injectEnvironment = true
55
+ } = options;
56
+ const globalConfig = {
57
+ outDir,
58
+ clientManagerPath
59
+ };
60
+ const plugin = {
49
61
  name: "ic-reactor-plugin",
50
- config(_config, { command }) {
51
- if (command !== "serve" || !(options.injectEnvironment ?? true)) {
62
+ enforce: "pre",
63
+ // Run before other plugins
64
+ config(config, { command }) {
65
+ if (command !== "serve" || !injectEnvironment) {
52
66
  return {};
53
67
  }
54
- const canisterNames = options.canisters.map((c) => c.name);
68
+ const canisterNames = canisters.map((c) => c.name).filter((n) => !!n);
55
69
  if (!canisterNames.includes("internet_identity")) {
56
70
  canisterNames.push("internet_identity");
57
71
  }
@@ -84,95 +98,58 @@ function icReactorPlugin(options) {
84
98
  };
85
99
  },
86
100
  async buildStart() {
87
- const defaultClientPath = path.resolve(
88
- process.cwd(),
89
- "src/lib/clients.ts"
101
+ const projectRoot = process.cwd();
102
+ console.log(
103
+ `[ic-reactor] Generating hooks for ${canisters.length} canisters...`
90
104
  );
91
- if (!fs.existsSync(defaultClientPath)) {
92
- console.log(
93
- `[ic-reactor] Default client manager not found. Creating at ${defaultClientPath}`
94
- );
95
- const clientContent = generateClientFile();
96
- fs.mkdirSync(path.dirname(defaultClientPath), { recursive: true });
97
- fs.writeFileSync(defaultClientPath, clientContent);
98
- }
99
- for (const canister of options.canisters) {
100
- let didFile = canister.didFile;
101
- const outDir = canister.outDir ?? path.join(baseOutDir, canister.name);
102
- if (!didFile) {
103
- const environment = process.env.ICP_ENVIRONMENT || "local";
104
- console.log(
105
- `[ic-reactor] didFile not specified for "${canister.name}". Attempting to download from canister...`
106
- );
107
- try {
108
- const candidContent = execFileSync(
109
- "icp",
110
- [
111
- "canister",
112
- "metadata",
113
- canister.name,
114
- "candid:service",
115
- "-e",
116
- environment
117
- ],
118
- { encoding: "utf-8" }
119
- ).trim();
120
- const declarationsDir = path.join(outDir, "declarations");
121
- if (!fs.existsSync(declarationsDir)) {
122
- fs.mkdirSync(declarationsDir, { recursive: true });
123
- }
124
- didFile = path.join(declarationsDir, `${canister.name}.did`);
125
- fs.writeFileSync(didFile, candidContent);
126
- console.log(
127
- `[ic-reactor] Candid downloaded and saved to ${didFile}`
128
- );
129
- } catch (error) {
105
+ for (const canisterConfig of canisters) {
106
+ try {
107
+ const result = await runCanisterPipeline({
108
+ canisterConfig,
109
+ projectRoot,
110
+ globalConfig
111
+ });
112
+ if (!result.success) {
130
113
  console.error(
131
- `[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
114
+ `[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
132
115
  );
133
- continue;
116
+ } else {
134
117
  }
135
- }
136
- console.log(
137
- `[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
138
- );
139
- const result = await generateDeclarations({
140
- didFile,
141
- outDir,
142
- canisterName: canister.name
143
- });
144
- if (!result.success) {
118
+ } catch (err) {
145
119
  console.error(
146
- `[ic-reactor] Failed to generate declarations: ${result.error}`
120
+ `[ic-reactor] Error generating ${canisterConfig.name}:`,
121
+ err
147
122
  );
148
- continue;
149
123
  }
150
- const reactorContent = generateReactorFile({
151
- canisterName: canister.name,
152
- didFile,
153
- clientManagerPath: canister.clientManagerPath ?? options.clientManagerPath
154
- });
155
- const reactorPath = path.join(outDir, "index.ts");
156
- fs.mkdirSync(outDir, { recursive: true });
157
- fs.writeFileSync(reactorPath, reactorContent);
158
- console.log(`[ic-reactor] Reactor hooks generated at ${reactorPath}`);
159
124
  }
160
125
  },
161
126
  handleHotUpdate({ file, server }) {
162
127
  if (file.endsWith(".did")) {
163
- const canister = options.canisters.find((c) => {
164
- if (!c.didFile) return false;
165
- return path.resolve(c.didFile) === file;
128
+ const affectedCanister = canisters.find((c) => {
129
+ const configPath = path.resolve(process.cwd(), c.didFile);
130
+ return configPath === file;
166
131
  });
167
- if (canister) {
132
+ if (affectedCanister) {
168
133
  console.log(
169
- `[ic-reactor] Detected change in ${file}, regenerating...`
134
+ `[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
170
135
  );
171
- server.restart();
136
+ const projectRoot = process.cwd();
137
+ runCanisterPipeline({
138
+ canisterConfig: affectedCanister,
139
+ projectRoot,
140
+ globalConfig
141
+ }).then((result) => {
142
+ if (result.success) {
143
+ server.ws.send({ type: "full-reload" });
144
+ } else {
145
+ console.error(`[ic-reactor] Regeneration failed: ${result.error}`);
146
+ }
147
+ });
172
148
  }
173
149
  }
174
150
  }
175
151
  };
152
+ return plugin;
176
153
  }
177
154
  export {
178
155
  icReactorPlugin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/vite-plugin",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
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,17 +37,16 @@
37
37
  "url": "https://github.com/B3Pay/ic-reactor/issues"
38
38
  },
39
39
  "dependencies": {
40
- "@ic-reactor/codegen": "0.4.1"
40
+ "@ic-reactor/codegen": "0.5.1"
41
41
  },
42
42
  "peerDependencies": {
43
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
43
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@icp-sdk/bindgen": "^0.2.1",
47
47
  "@types/node": "^25.2.3",
48
48
  "tsup": "^8.5.1",
49
49
  "typescript": "^5.9.3",
50
- "vite": "^7.3.1",
51
50
  "vitest": "^4.0.18"
52
51
  },
53
52
  "scripts": {
package/src/env.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Environment Injection Utilities
3
+ *
4
+ * Handles detecting the local IC environment (via `dfx` or `icp` CLI)
5
+ * and building the `ic_env` cookie for the browser.
6
+ */
7
+
8
+ import { execFileSync } from "child_process"
9
+
10
+ export interface IcEnvironment {
11
+ environment: string
12
+ rootKey: string
13
+ proxyTarget: string
14
+ canisterIds: Record<string, string>
15
+ }
16
+
17
+ /**
18
+ * Detect the IC environment using the `icp` or `dfx` CLI.
19
+ */
20
+ export function getIcEnvironmentInfo(
21
+ canisterNames: string[]
22
+ ): IcEnvironment | null {
23
+ const environment = process.env.ICP_ENVIRONMENT || "local"
24
+
25
+ // We try `icp` first, but could fallback to `dfx` logic if needed.
26
+ // For now, assuming `icp` CLI is available based on previous code.
27
+
28
+ try {
29
+ const networkStatus = JSON.parse(
30
+ execFileSync("icp", ["network", "status", "-e", environment, "--json"], {
31
+ encoding: "utf-8",
32
+ stdio: ["ignore", "pipe", "ignore"], // suppress stderr
33
+ })
34
+ )
35
+
36
+ const rootKey = networkStatus.root_key
37
+ // Default to localhost:4943 if port strictly needed, but `icp` gives us the port
38
+ const proxyTarget = `http://127.0.0.1:${networkStatus.port}`
39
+
40
+ const canisterIds: Record<string, string> = {}
41
+
42
+ for (const name of canisterNames) {
43
+ try {
44
+ const canisterId = execFileSync(
45
+ "icp",
46
+ ["canister", "status", name, "-e", environment, "-i"],
47
+ { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }
48
+ ).trim()
49
+
50
+ if (canisterId) {
51
+ canisterIds[name] = canisterId
52
+ }
53
+ } catch {
54
+ // Canister might not exist or be deployed yet
55
+ }
56
+ }
57
+
58
+ return { environment, rootKey, proxyTarget, canisterIds }
59
+ } catch (error) {
60
+ // CLI not found or failed
61
+ return null
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Build the `ic_env` cookie string.
67
+ * Format: `ic_root_key=<key>&PUBLIC_CANISTER_ID:<name>=<id>&...`
68
+ */
69
+ export function buildIcEnvCookie(
70
+ canisterIds: Record<string, string>,
71
+ rootKey: string
72
+ ): string {
73
+ const parts = [`ic_root_key=${rootKey}`]
74
+
75
+ for (const [name, id] of Object.entries(canisterIds)) {
76
+ parts.push(`PUBLIC_CANISTER_ID:${name}=${id}`)
77
+ }
78
+
79
+ return encodeURIComponent(parts.join("&"))
80
+ }
package/src/index.test.ts CHANGED
@@ -1,19 +1,12 @@
1
1
  import { describe, it, expect, vi, beforeEach } from "vitest"
2
2
  import { icReactorPlugin, type IcReactorPluginOptions } from "./index"
3
- import fs from "fs"
4
3
  import path from "path"
5
4
  import { execFileSync } from "child_process"
6
- import {
7
- generateDeclarations,
8
- generateReactorFile,
9
- generateClientFile,
10
- } from "@ic-reactor/codegen"
5
+ import { runCanisterPipeline } from "@ic-reactor/codegen"
11
6
 
12
7
  // Mock internal dependencies
13
8
  vi.mock("@ic-reactor/codegen", () => ({
14
- generateDeclarations: vi.fn(),
15
- generateReactorFile: vi.fn(),
16
- generateClientFile: vi.fn(),
9
+ runCanisterPipeline: vi.fn(),
17
10
  }))
18
11
 
19
12
  // Mock child_process
@@ -54,16 +47,16 @@ describe("icReactorPlugin", () => {
54
47
  use: vi.fn(),
55
48
  },
56
49
  restart: vi.fn(),
50
+ ws: {
51
+ send: vi.fn(),
52
+ },
57
53
  }
58
54
 
59
55
  beforeEach(() => {
60
56
  vi.resetAllMocks()
61
- ;(generateDeclarations as any).mockResolvedValue({
57
+ ;(runCanisterPipeline as any).mockResolvedValue({
62
58
  success: true,
63
- declarationsDir: "/mock/declarations",
64
59
  })
65
- ;(generateReactorFile as any).mockReturnValue("export const reactor = {}")
66
- ;(generateClientFile as any).mockReturnValue("export const client = {}")
67
60
  })
68
61
 
69
62
  it("should return correct plugin structure", () => {
@@ -72,8 +65,6 @@ describe("icReactorPlugin", () => {
72
65
  expect(plugin.buildStart).toBeDefined()
73
66
  expect(plugin.handleHotUpdate).toBeDefined()
74
67
  expect((plugin as any).config).toBeDefined()
75
- // configureServer is no longer used for middleware
76
- expect(plugin.configureServer).toBeUndefined()
77
68
  })
78
69
 
79
70
  describe("config", () => {
@@ -136,82 +127,13 @@ describe("icReactorPlugin", () => {
136
127
  const plugin = icReactorPlugin(mockOptions)
137
128
  await (plugin.buildStart as any)()
138
129
 
139
- expect(generateDeclarations).toHaveBeenCalledWith({
140
- didFile: "src/declarations/test.did",
141
- outDir: "src/declarations/test_canister",
142
- canisterName: "test_canister",
143
- })
144
-
145
- expect(generateReactorFile).toHaveBeenCalledWith({
146
- canisterName: "test_canister",
147
- didFile: "src/declarations/test.did",
148
- clientManagerPath: undefined,
149
- })
150
-
151
- expect(fs.writeFileSync).toHaveBeenCalledWith(
152
- path.join("src/declarations/test_canister", "index.ts"),
153
- "export const reactor = {}"
154
- )
155
- })
156
-
157
- it("should download candid if not specified", async () => {
158
- const optionsWithMissingDid: IcReactorPluginOptions = {
159
- canisters: [
160
- {
161
- name: "missing_did",
162
- outDir: "src/declarations/missing_did",
163
- },
164
- ],
165
- outDir: "src/declarations",
166
- }
167
-
168
- ;(execFileSync as any).mockImplementation(
169
- (command: string, args: string[]) => {
170
- if (command === "icp" && args.includes("metadata")) {
171
- return "service : { greet: (text) -> (text) query }"
172
- }
173
- return ""
174
- }
175
- )
176
-
177
- const plugin = icReactorPlugin(optionsWithMissingDid)
178
- await (plugin.buildStart as any)()
179
-
180
- expect(execFileSync).toHaveBeenCalledWith(
181
- "icp",
182
- expect.arrayContaining([
183
- "canister",
184
- "metadata",
185
- "missing_did",
186
- "candid:service",
187
- ]),
188
- expect.any(Object)
189
- )
190
-
191
- expect(fs.writeFileSync).toHaveBeenCalledWith(
192
- path.join(
193
- "src/declarations/missing_did/declarations",
194
- "missing_did.did"
195
- ),
196
- "service : { greet: (text) -> (text) query }"
197
- )
198
-
199
- expect(generateDeclarations).toHaveBeenCalledWith({
200
- didFile: path.join(
201
- "src/declarations/missing_did/declarations",
202
- "missing_did.did"
203
- ),
204
- outDir: "src/declarations/missing_did",
205
- canisterName: "missing_did",
206
- })
207
-
208
- expect(generateReactorFile).toHaveBeenCalledWith({
209
- canisterName: "missing_did",
210
- didFile: path.join(
211
- "src/declarations/missing_did/declarations",
212
- "missing_did.did"
213
- ),
214
- clientManagerPath: undefined,
130
+ expect(runCanisterPipeline).toHaveBeenCalledWith({
131
+ canisterConfig: mockOptions.canisters[0],
132
+ projectRoot: expect.any(String),
133
+ globalConfig: {
134
+ outDir: "src/declarations",
135
+ clientManagerPath: "../../clients",
136
+ },
215
137
  })
216
138
  })
217
139
 
@@ -220,7 +142,7 @@ describe("icReactorPlugin", () => {
220
142
  .spyOn(console, "error")
221
143
  .mockImplementation(() => {})
222
144
 
223
- ;(generateDeclarations as any).mockResolvedValue({
145
+ ;(runCanisterPipeline as any).mockResolvedValue({
224
146
  success: false,
225
147
  error: "Failed to generate",
226
148
  })
@@ -229,16 +151,15 @@ describe("icReactorPlugin", () => {
229
151
  await (plugin.buildStart as any)()
230
152
 
231
153
  expect(consoleErrorSpy).toHaveBeenCalledWith(
232
- expect.stringContaining("Failed to generate declarations")
154
+ expect.stringContaining("Failed to generate test_canister")
233
155
  )
234
- expect(generateReactorFile).not.toHaveBeenCalled()
235
156
 
236
157
  consoleErrorSpy.mockRestore()
237
158
  })
238
159
  })
239
160
 
240
161
  describe("handleHotUpdate", () => {
241
- it("should restart server when .did file changes", () => {
162
+ it("should restart server when .did file changes", async () => {
242
163
  const plugin = icReactorPlugin(mockOptions)
243
164
  const ctx = {
244
165
  file: "/absolute/path/to/src/declarations/test.did",
@@ -248,14 +169,15 @@ describe("icReactorPlugin", () => {
248
169
  // Mock path.resolve to match the test case
249
170
  const originalResolve = path.resolve
250
171
  vi.spyOn(path, "resolve").mockImplementation((...args) => {
251
- if (args.some((a) => a.includes("test.did"))) {
172
+ if (args.some((a) => a && a.includes("test.did"))) {
252
173
  return "/absolute/path/to/src/declarations/test.did"
253
174
  }
254
175
  return originalResolve(...args)
255
176
  })
256
- ;(plugin.handleHotUpdate as any)(ctx)
257
177
 
258
- expect(mockServer.restart).toHaveBeenCalled()
178
+ await (plugin.handleHotUpdate as any)(ctx)
179
+
180
+ expect(mockServer.ws.send).toHaveBeenCalledWith({ type: "full-reload" })
259
181
  })
260
182
 
261
183
  it("should ignore other files", () => {
@@ -267,7 +189,7 @@ describe("icReactorPlugin", () => {
267
189
 
268
190
  ;(plugin.handleHotUpdate as any)(ctx)
269
191
 
270
- expect(mockServer.restart).not.toHaveBeenCalled()
192
+ expect(mockServer.ws.send).not.toHaveBeenCalled()
271
193
  })
272
194
  })
273
195
  })