@ic-reactor/vite-plugin 0.4.0 → 0.5.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/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,51 @@ 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";
50
+ const {
51
+ canisters,
52
+ outDir = "src/declarations",
53
+ clientManagerPath = "../../clients",
54
+ injectEnvironment = true
55
+ } = options;
56
+ const globalConfig = {
57
+ outDir,
58
+ clientManagerPath
59
+ };
48
60
  return {
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);
69
+ if (!canisterNames.includes("internet_identity")) {
70
+ canisterNames.push("internet_identity");
71
+ }
55
72
  const icEnv = getIcEnvironmentInfo(canisterNames);
56
73
  if (!icEnv) {
57
74
  return {
@@ -81,91 +98,53 @@ function icReactorPlugin(options) {
81
98
  };
82
99
  },
83
100
  async buildStart() {
84
- const defaultClientPath = path.resolve(
85
- process.cwd(),
86
- "src/lib/clients.ts"
101
+ const projectRoot = process.cwd();
102
+ console.log(
103
+ `[ic-reactor] Generating hooks for ${canisters.length} canisters...`
87
104
  );
88
- if (!fs.existsSync(defaultClientPath)) {
89
- console.log(
90
- `[ic-reactor] Default client manager not found. Creating at ${defaultClientPath}`
91
- );
92
- const clientContent = generateClientFile();
93
- fs.mkdirSync(path.dirname(defaultClientPath), { recursive: true });
94
- fs.writeFileSync(defaultClientPath, clientContent);
95
- }
96
- for (const canister of options.canisters) {
97
- let didFile = canister.didFile;
98
- const outDir = canister.outDir ?? path.join(baseOutDir, canister.name);
99
- if (!didFile) {
100
- const environment = process.env.ICP_ENVIRONMENT || "local";
101
- console.log(
102
- `[ic-reactor] didFile not specified for "${canister.name}". Attempting to download from canister...`
103
- );
104
- try {
105
- const candidContent = execFileSync(
106
- "icp",
107
- [
108
- "canister",
109
- "metadata",
110
- canister.name,
111
- "candid:service",
112
- "-e",
113
- environment
114
- ],
115
- { encoding: "utf-8" }
116
- ).trim();
117
- const declarationsDir = path.join(outDir, "declarations");
118
- if (!fs.existsSync(declarationsDir)) {
119
- fs.mkdirSync(declarationsDir, { recursive: true });
120
- }
121
- didFile = path.join(declarationsDir, `${canister.name}.did`);
122
- fs.writeFileSync(didFile, candidContent);
123
- console.log(
124
- `[ic-reactor] Candid downloaded and saved to ${didFile}`
125
- );
126
- } 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) {
127
113
  console.error(
128
- `[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
114
+ `[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
129
115
  );
130
- continue;
116
+ } else {
131
117
  }
132
- }
133
- console.log(
134
- `[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
135
- );
136
- const result = await generateDeclarations({
137
- didFile,
138
- outDir,
139
- canisterName: canister.name
140
- });
141
- if (!result.success) {
118
+ } catch (err) {
142
119
  console.error(
143
- `[ic-reactor] Failed to generate declarations: ${result.error}`
120
+ `[ic-reactor] Error generating ${canisterConfig.name}:`,
121
+ err
144
122
  );
145
- continue;
146
123
  }
147
- const reactorContent = generateReactorFile({
148
- canisterName: canister.name,
149
- didFile,
150
- clientManagerPath: canister.clientManagerPath ?? options.clientManagerPath
151
- });
152
- const reactorPath = path.join(outDir, "index.ts");
153
- fs.mkdirSync(outDir, { recursive: true });
154
- fs.writeFileSync(reactorPath, reactorContent);
155
- console.log(`[ic-reactor] Reactor hooks generated at ${reactorPath}`);
156
124
  }
157
125
  },
158
126
  handleHotUpdate({ file, server }) {
159
127
  if (file.endsWith(".did")) {
160
- const canister = options.canisters.find((c) => {
161
- if (!c.didFile) return false;
162
- 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;
163
131
  });
164
- if (canister) {
132
+ if (affectedCanister) {
165
133
  console.log(
166
- `[ic-reactor] Detected change in ${file}, regenerating...`
134
+ `[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
167
135
  );
168
- 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
+ });
169
148
  }
170
149
  }
171
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ic-reactor/vite-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.5.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,7 +37,7 @@
37
37
  "url": "https://github.com/B3Pay/ic-reactor/issues"
38
38
  },
39
39
  "dependencies": {
40
- "@ic-reactor/codegen": "0.4.0"
40
+ "@ic-reactor/codegen": "0.5.0"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
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
@@ -3,17 +3,11 @@ import { icReactorPlugin, type IcReactorPluginOptions } from "./index"
3
3
  import fs from "fs"
4
4
  import path from "path"
5
5
  import { execFileSync } from "child_process"
6
- import {
7
- generateDeclarations,
8
- generateReactorFile,
9
- generateClientFile,
10
- } from "@ic-reactor/codegen"
6
+ import { runCanisterPipeline } from "@ic-reactor/codegen"
11
7
 
12
8
  // Mock internal dependencies
13
9
  vi.mock("@ic-reactor/codegen", () => ({
14
- generateDeclarations: vi.fn(),
15
- generateReactorFile: vi.fn(),
16
- generateClientFile: vi.fn(),
10
+ runCanisterPipeline: vi.fn(),
17
11
  }))
18
12
 
19
13
  // Mock child_process
@@ -54,16 +48,16 @@ describe("icReactorPlugin", () => {
54
48
  use: vi.fn(),
55
49
  },
56
50
  restart: vi.fn(),
51
+ ws: {
52
+ send: vi.fn(),
53
+ },
57
54
  }
58
55
 
59
56
  beforeEach(() => {
60
57
  vi.resetAllMocks()
61
- ;(generateDeclarations as any).mockResolvedValue({
58
+ ;(runCanisterPipeline as any).mockResolvedValue({
62
59
  success: true,
63
- declarationsDir: "/mock/declarations",
64
60
  })
65
- ;(generateReactorFile as any).mockReturnValue("export const reactor = {}")
66
- ;(generateClientFile as any).mockReturnValue("export const client = {}")
67
61
  })
68
62
 
69
63
  it("should return correct plugin structure", () => {
@@ -72,8 +66,6 @@ describe("icReactorPlugin", () => {
72
66
  expect(plugin.buildStart).toBeDefined()
73
67
  expect(plugin.handleHotUpdate).toBeDefined()
74
68
  expect((plugin as any).config).toBeDefined()
75
- // configureServer is no longer used for middleware
76
- expect(plugin.configureServer).toBeUndefined()
77
69
  })
78
70
 
79
71
  describe("config", () => {
@@ -136,82 +128,13 @@ describe("icReactorPlugin", () => {
136
128
  const plugin = icReactorPlugin(mockOptions)
137
129
  await (plugin.buildStart as any)()
138
130
 
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,
131
+ expect(runCanisterPipeline).toHaveBeenCalledWith({
132
+ canisterConfig: mockOptions.canisters[0],
133
+ projectRoot: expect.any(String),
134
+ globalConfig: {
135
+ outDir: "src/declarations",
136
+ clientManagerPath: "../../clients",
137
+ },
215
138
  })
216
139
  })
217
140
 
@@ -220,7 +143,7 @@ describe("icReactorPlugin", () => {
220
143
  .spyOn(console, "error")
221
144
  .mockImplementation(() => {})
222
145
 
223
- ;(generateDeclarations as any).mockResolvedValue({
146
+ ;(runCanisterPipeline as any).mockResolvedValue({
224
147
  success: false,
225
148
  error: "Failed to generate",
226
149
  })
@@ -229,16 +152,15 @@ describe("icReactorPlugin", () => {
229
152
  await (plugin.buildStart as any)()
230
153
 
231
154
  expect(consoleErrorSpy).toHaveBeenCalledWith(
232
- expect.stringContaining("Failed to generate declarations")
155
+ expect.stringContaining("Failed to generate test_canister")
233
156
  )
234
- expect(generateReactorFile).not.toHaveBeenCalled()
235
157
 
236
158
  consoleErrorSpy.mockRestore()
237
159
  })
238
160
  })
239
161
 
240
162
  describe("handleHotUpdate", () => {
241
- it("should restart server when .did file changes", () => {
163
+ it("should restart server when .did file changes", async () => {
242
164
  const plugin = icReactorPlugin(mockOptions)
243
165
  const ctx = {
244
166
  file: "/absolute/path/to/src/declarations/test.did",
@@ -248,14 +170,15 @@ describe("icReactorPlugin", () => {
248
170
  // Mock path.resolve to match the test case
249
171
  const originalResolve = path.resolve
250
172
  vi.spyOn(path, "resolve").mockImplementation((...args) => {
251
- if (args.some((a) => a.includes("test.did"))) {
173
+ if (args.some((a) => a && a.includes("test.did"))) {
252
174
  return "/absolute/path/to/src/declarations/test.did"
253
175
  }
254
176
  return originalResolve(...args)
255
177
  })
256
- ;(plugin.handleHotUpdate as any)(ctx)
257
178
 
258
- expect(mockServer.restart).toHaveBeenCalled()
179
+ await (plugin.handleHotUpdate as any)(ctx)
180
+
181
+ expect(mockServer.ws.send).toHaveBeenCalledWith({ type: "full-reload" })
259
182
  })
260
183
 
261
184
  it("should ignore other files", () => {
@@ -267,7 +190,7 @@ describe("icReactorPlugin", () => {
267
190
 
268
191
  ;(plugin.handleHotUpdate as any)(ctx)
269
192
 
270
- expect(mockServer.restart).not.toHaveBeenCalled()
193
+ expect(mockServer.ws.send).not.toHaveBeenCalled()
271
194
  })
272
195
  })
273
196
  })