@ic-reactor/vite-plugin 0.1.0 → 0.3.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 +51 -169
- package/dist/index.cjs +134 -349
- package/dist/index.d.cts +13 -25
- package/dist/index.d.ts +13 -25
- package/dist/index.js +139 -347
- package/package.json +24 -18
- package/src/index.test.ts +273 -0
- package/src/index.ts +267 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest"
|
|
2
|
+
import { icReactorPlugin, type IcReactorPluginOptions } from "./index"
|
|
3
|
+
import fs from "fs"
|
|
4
|
+
import path from "path"
|
|
5
|
+
import { execFileSync } from "child_process"
|
|
6
|
+
import {
|
|
7
|
+
generateDeclarations,
|
|
8
|
+
generateReactorFile,
|
|
9
|
+
generateClientFile,
|
|
10
|
+
} from "@ic-reactor/codegen"
|
|
11
|
+
|
|
12
|
+
// Mock internal dependencies
|
|
13
|
+
vi.mock("@ic-reactor/codegen", () => ({
|
|
14
|
+
generateDeclarations: vi.fn(),
|
|
15
|
+
generateReactorFile: vi.fn(),
|
|
16
|
+
generateClientFile: vi.fn(),
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
// Mock child_process
|
|
20
|
+
vi.mock("child_process", () => ({
|
|
21
|
+
execFileSync: vi.fn(),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
// Mock fs
|
|
25
|
+
vi.mock("fs", () => ({
|
|
26
|
+
default: {
|
|
27
|
+
existsSync: vi.fn(() => true),
|
|
28
|
+
readFileSync: vi.fn(),
|
|
29
|
+
mkdirSync: vi.fn(),
|
|
30
|
+
writeFileSync: vi.fn(),
|
|
31
|
+
},
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
describe("icReactorPlugin", () => {
|
|
35
|
+
const mockOptions: IcReactorPluginOptions = {
|
|
36
|
+
canisters: [
|
|
37
|
+
{
|
|
38
|
+
name: "test_canister",
|
|
39
|
+
didFile: "src/declarations/test.did",
|
|
40
|
+
outDir: "src/declarations/test_canister",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
outDir: "src/declarations",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mockServer: any = {
|
|
47
|
+
config: {
|
|
48
|
+
root: "/mock/root",
|
|
49
|
+
logger: {
|
|
50
|
+
info: vi.fn(),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
middlewares: {
|
|
54
|
+
use: vi.fn(),
|
|
55
|
+
},
|
|
56
|
+
restart: vi.fn(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
vi.resetAllMocks()
|
|
61
|
+
;(generateDeclarations as any).mockResolvedValue({
|
|
62
|
+
success: true,
|
|
63
|
+
declarationsDir: "/mock/declarations",
|
|
64
|
+
})
|
|
65
|
+
;(generateReactorFile as any).mockReturnValue("export const reactor = {}")
|
|
66
|
+
;(generateClientFile as any).mockReturnValue("export const client = {}")
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("should return correct plugin structure", () => {
|
|
70
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
71
|
+
expect(plugin.name).toBe("ic-reactor-plugin")
|
|
72
|
+
expect(plugin.buildStart).toBeDefined()
|
|
73
|
+
expect(plugin.handleHotUpdate).toBeDefined()
|
|
74
|
+
expect((plugin as any).config).toBeDefined()
|
|
75
|
+
// configureServer is no longer used for middleware
|
|
76
|
+
expect(plugin.configureServer).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe("config", () => {
|
|
80
|
+
it("should set up API proxy and headers when icp-cli is available", () => {
|
|
81
|
+
;(execFileSync as any).mockImplementation(
|
|
82
|
+
(command: string, args: string[], options: any) => {
|
|
83
|
+
if (
|
|
84
|
+
command === "icp" &&
|
|
85
|
+
args.includes("network") &&
|
|
86
|
+
args.includes("status")
|
|
87
|
+
) {
|
|
88
|
+
return JSON.stringify({ root_key: "mock-root-key", port: 4943 })
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
command === "icp" &&
|
|
92
|
+
args.includes("canister") &&
|
|
93
|
+
args.includes("status")
|
|
94
|
+
) {
|
|
95
|
+
return "mock-canister-id"
|
|
96
|
+
}
|
|
97
|
+
return ""
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
102
|
+
const config = (plugin as any).config({}, { command: "serve" })
|
|
103
|
+
|
|
104
|
+
expect(config.server.headers["Set-Cookie"]).toContain("ic_env=")
|
|
105
|
+
expect(config.server.headers["Set-Cookie"]).toContain(
|
|
106
|
+
"PUBLIC_CANISTER_ID%3Atest_canister%3Dmock-canister-id"
|
|
107
|
+
)
|
|
108
|
+
expect(config.server.headers["Set-Cookie"]).toContain(
|
|
109
|
+
"ic_root_key%3Dmock-root-key"
|
|
110
|
+
)
|
|
111
|
+
expect(config.server.proxy["/api"].target).toBe("http://127.0.0.1:4943")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("should fallback to default proxy when icp-cli fails", () => {
|
|
115
|
+
;(execFileSync as any).mockImplementation(() => {
|
|
116
|
+
throw new Error("Command not found")
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
120
|
+
const config = (plugin as any).config({}, { command: "serve" })
|
|
121
|
+
|
|
122
|
+
expect(config.server.headers).toBeUndefined()
|
|
123
|
+
expect(config.server.proxy["/api"].target).toBe("http://127.0.0.1:4943")
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it("should return empty config for build command", () => {
|
|
127
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
128
|
+
const config = (plugin as any).config({}, { command: "build" })
|
|
129
|
+
|
|
130
|
+
expect(config).toEqual({})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe("buildStart", () => {
|
|
135
|
+
it("should generate declarations and reactor file", async () => {
|
|
136
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
137
|
+
await (plugin.buildStart as any)()
|
|
138
|
+
|
|
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,
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it("should handle generation errors", async () => {
|
|
219
|
+
const consoleErrorSpy = vi
|
|
220
|
+
.spyOn(console, "error")
|
|
221
|
+
.mockImplementation(() => {})
|
|
222
|
+
|
|
223
|
+
;(generateDeclarations as any).mockResolvedValue({
|
|
224
|
+
success: false,
|
|
225
|
+
error: "Failed to generate",
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
229
|
+
await (plugin.buildStart as any)()
|
|
230
|
+
|
|
231
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
232
|
+
expect.stringContaining("Failed to generate declarations")
|
|
233
|
+
)
|
|
234
|
+
expect(generateReactorFile).not.toHaveBeenCalled()
|
|
235
|
+
|
|
236
|
+
consoleErrorSpy.mockRestore()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe("handleHotUpdate", () => {
|
|
241
|
+
it("should restart server when .did file changes", () => {
|
|
242
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
243
|
+
const ctx = {
|
|
244
|
+
file: "/absolute/path/to/src/declarations/test.did",
|
|
245
|
+
server: mockServer,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Mock path.resolve to match the test case
|
|
249
|
+
const originalResolve = path.resolve
|
|
250
|
+
vi.spyOn(path, "resolve").mockImplementation((...args) => {
|
|
251
|
+
if (args.some((a) => a.includes("test.did"))) {
|
|
252
|
+
return "/absolute/path/to/src/declarations/test.did"
|
|
253
|
+
}
|
|
254
|
+
return originalResolve(...args)
|
|
255
|
+
})
|
|
256
|
+
;(plugin.handleHotUpdate as any)(ctx)
|
|
257
|
+
|
|
258
|
+
expect(mockServer.restart).toHaveBeenCalled()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it("should ignore other files", () => {
|
|
262
|
+
const plugin = icReactorPlugin(mockOptions)
|
|
263
|
+
const ctx = {
|
|
264
|
+
file: "/some/other/file.ts",
|
|
265
|
+
server: mockServer,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
;(plugin.handleHotUpdate as any)(ctx)
|
|
269
|
+
|
|
270
|
+
expect(mockServer.restart).not.toHaveBeenCalled()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
})
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IC-Reactor Vite Plugin
|
|
3
|
+
*
|
|
4
|
+
* A Vite plugin that generates ic-reactor hooks from Candid .did files.
|
|
5
|
+
* Uses @ic-reactor/codegen for all code generation logic.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { icReactorPlugin } from "@ic-reactor/vite-plugin"
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* plugins: [
|
|
13
|
+
* icReactorPlugin({
|
|
14
|
+
* canisters: [
|
|
15
|
+
* {
|
|
16
|
+
* name: "backend",
|
|
17
|
+
* didFile: "../backend/backend.did",
|
|
18
|
+
* clientManagerPath: "../lib/client"
|
|
19
|
+
* }
|
|
20
|
+
* ]
|
|
21
|
+
* })
|
|
22
|
+
* ]
|
|
23
|
+
* })
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { Plugin } from "vite"
|
|
28
|
+
import fs from "fs"
|
|
29
|
+
import path from "path"
|
|
30
|
+
import { execFileSync } from "child_process"
|
|
31
|
+
import {
|
|
32
|
+
generateDeclarations,
|
|
33
|
+
generateReactorFile,
|
|
34
|
+
generateClientFile,
|
|
35
|
+
} from "@ic-reactor/codegen"
|
|
36
|
+
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
// TYPES
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
+
export interface CanisterConfig {
|
|
41
|
+
name: string
|
|
42
|
+
outDir?: string
|
|
43
|
+
didFile?: string
|
|
44
|
+
clientManagerPath?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IcReactorPluginOptions {
|
|
48
|
+
/** List of canisters to generate hooks for */
|
|
49
|
+
canisters: CanisterConfig[]
|
|
50
|
+
/** Base output directory (default: ./src/lib/canisters) */
|
|
51
|
+
outDir?: string
|
|
52
|
+
/**
|
|
53
|
+
* Path to import ClientManager from (relative to generated file).
|
|
54
|
+
* Default: "../../clients"
|
|
55
|
+
*/
|
|
56
|
+
clientManagerPath?: string
|
|
57
|
+
/**
|
|
58
|
+
* Automatically inject the IC environment (canister IDs and root key)
|
|
59
|
+
* into the browser using an `ic_env` cookie. (default: true)
|
|
60
|
+
*
|
|
61
|
+
* This is useful for local development with `icp`.
|
|
62
|
+
*/
|
|
63
|
+
injectEnvironment?: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getIcEnvironmentInfo(canisterNames: string[]) {
|
|
67
|
+
const environment = process.env.ICP_ENVIRONMENT || "local"
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const networkStatus = JSON.parse(
|
|
71
|
+
execFileSync("icp", ["network", "status", "-e", environment, "--json"], {
|
|
72
|
+
encoding: "utf-8",
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
const rootKey = networkStatus.root_key
|
|
76
|
+
const proxyTarget = `http://127.0.0.1:${networkStatus.port}`
|
|
77
|
+
|
|
78
|
+
const canisterIds: Record<string, string> = {}
|
|
79
|
+
for (const name of canisterNames) {
|
|
80
|
+
try {
|
|
81
|
+
const canisterId = execFileSync(
|
|
82
|
+
"icp",
|
|
83
|
+
["canister", "status", name, "-e", environment, "-i"],
|
|
84
|
+
{
|
|
85
|
+
encoding: "utf-8",
|
|
86
|
+
}
|
|
87
|
+
).trim()
|
|
88
|
+
canisterIds[name] = canisterId
|
|
89
|
+
} catch {
|
|
90
|
+
// Skip if canister not found
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { environment, rootKey, proxyTarget, canisterIds }
|
|
95
|
+
} catch {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildIcEnvCookie(
|
|
101
|
+
canisterIds: Record<string, string>,
|
|
102
|
+
rootKey: string
|
|
103
|
+
): string {
|
|
104
|
+
const envParts = [`ic_root_key=${rootKey}`]
|
|
105
|
+
|
|
106
|
+
for (const [name, id] of Object.entries(canisterIds)) {
|
|
107
|
+
envParts.push(`PUBLIC_CANISTER_ID:${name}=${id}`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return encodeURIComponent(envParts.join("&"))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// VITE PLUGIN
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
|
|
118
|
+
const baseOutDir = options.outDir ?? "./src/lib/canisters"
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
name: "ic-reactor-plugin",
|
|
122
|
+
|
|
123
|
+
config(_config, { command }) {
|
|
124
|
+
if (command !== "serve" || !(options.injectEnvironment ?? true)) {
|
|
125
|
+
return {}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const canisterNames = options.canisters.map((c) => c.name)
|
|
129
|
+
const icEnv = getIcEnvironmentInfo(canisterNames)
|
|
130
|
+
|
|
131
|
+
if (!icEnv) {
|
|
132
|
+
return {
|
|
133
|
+
server: {
|
|
134
|
+
proxy: {
|
|
135
|
+
"/api": {
|
|
136
|
+
target: "http://127.0.0.1:4943",
|
|
137
|
+
changeOrigin: true,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const cookieValue = buildIcEnvCookie(icEnv.canisterIds, icEnv.rootKey)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
server: {
|
|
148
|
+
headers: {
|
|
149
|
+
"Set-Cookie": `ic_env=${cookieValue}; Path=/; SameSite=Lax;`,
|
|
150
|
+
},
|
|
151
|
+
proxy: {
|
|
152
|
+
"/api": {
|
|
153
|
+
target: icEnv.proxyTarget,
|
|
154
|
+
changeOrigin: true,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async buildStart() {
|
|
162
|
+
// Step 0: Ensure central client manager exists (default: src/lib/clients.ts)
|
|
163
|
+
const defaultClientPath = path.resolve(
|
|
164
|
+
process.cwd(),
|
|
165
|
+
"src/lib/clients.ts"
|
|
166
|
+
)
|
|
167
|
+
if (!fs.existsSync(defaultClientPath)) {
|
|
168
|
+
console.log(
|
|
169
|
+
`[ic-reactor] Default client manager not found. Creating at ${defaultClientPath}`
|
|
170
|
+
)
|
|
171
|
+
const clientContent = generateClientFile()
|
|
172
|
+
fs.mkdirSync(path.dirname(defaultClientPath), { recursive: true })
|
|
173
|
+
fs.writeFileSync(defaultClientPath, clientContent)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const canister of options.canisters) {
|
|
177
|
+
let didFile = canister.didFile
|
|
178
|
+
const outDir = canister.outDir ?? path.join(baseOutDir, canister.name)
|
|
179
|
+
|
|
180
|
+
if (!didFile) {
|
|
181
|
+
const environment = process.env.ICP_ENVIRONMENT || "local"
|
|
182
|
+
|
|
183
|
+
console.log(
|
|
184
|
+
`[ic-reactor] didFile not specified for "${canister.name}". Attempting to download from canister...`
|
|
185
|
+
)
|
|
186
|
+
try {
|
|
187
|
+
const candidContent = execFileSync(
|
|
188
|
+
"icp",
|
|
189
|
+
[
|
|
190
|
+
"canister",
|
|
191
|
+
"metadata",
|
|
192
|
+
canister.name,
|
|
193
|
+
"candid:service",
|
|
194
|
+
"-e",
|
|
195
|
+
environment,
|
|
196
|
+
],
|
|
197
|
+
{ encoding: "utf-8" }
|
|
198
|
+
).trim()
|
|
199
|
+
|
|
200
|
+
const declarationsDir = path.join(outDir, "declarations")
|
|
201
|
+
if (!fs.existsSync(declarationsDir)) {
|
|
202
|
+
fs.mkdirSync(declarationsDir, { recursive: true })
|
|
203
|
+
}
|
|
204
|
+
didFile = path.join(declarationsDir, `${canister.name}.did`)
|
|
205
|
+
fs.writeFileSync(didFile, candidContent)
|
|
206
|
+
console.log(
|
|
207
|
+
`[ic-reactor] Candid downloaded and saved to ${didFile}`
|
|
208
|
+
)
|
|
209
|
+
} catch (error) {
|
|
210
|
+
console.error(
|
|
211
|
+
`[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
|
|
212
|
+
)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log(
|
|
218
|
+
`[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
// Step 1: Generate declarations via @ic-reactor/codegen
|
|
222
|
+
const result = await generateDeclarations({
|
|
223
|
+
didFile: didFile,
|
|
224
|
+
outDir,
|
|
225
|
+
canisterName: canister.name,
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (!result.success) {
|
|
229
|
+
console.error(
|
|
230
|
+
`[ic-reactor] Failed to generate declarations: ${result.error}`
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Step 2: Generate the reactor file using shared codegen
|
|
236
|
+
const reactorContent = generateReactorFile({
|
|
237
|
+
canisterName: canister.name,
|
|
238
|
+
didFile: didFile,
|
|
239
|
+
clientManagerPath:
|
|
240
|
+
canister.clientManagerPath ?? options.clientManagerPath,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const reactorPath = path.join(outDir, "index.ts")
|
|
244
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
245
|
+
fs.writeFileSync(reactorPath, reactorContent)
|
|
246
|
+
|
|
247
|
+
console.log(`[ic-reactor] Reactor hooks generated at ${reactorPath}`)
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
handleHotUpdate({ file, server }) {
|
|
252
|
+
// Watch for .did file changes and regenerate
|
|
253
|
+
if (file.endsWith(".did")) {
|
|
254
|
+
const canister = options.canisters.find((c) => {
|
|
255
|
+
if (!c.didFile) return false
|
|
256
|
+
return path.resolve(c.didFile) === file
|
|
257
|
+
})
|
|
258
|
+
if (canister) {
|
|
259
|
+
console.log(
|
|
260
|
+
`[ic-reactor] Detected change in ${file}, regenerating...`
|
|
261
|
+
)
|
|
262
|
+
server.restart()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
}
|
|
267
|
+
}
|