@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/src/index.ts CHANGED
@@ -1,137 +1,81 @@
1
1
  /**
2
- * IC-Reactor Vite Plugin
2
+ * @ic-reactor/vite-plugin
3
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
- * ```
4
+ * Vite plugin that:
5
+ * 1. Generates hooks at build time (using @ic-reactor/codegen pipeline)
6
+ * 2. Injects `ic_env` cookie for local development (via proxy)
7
+ * 3. Hot-reloads when .did files change
25
8
  */
26
9
 
27
10
  import type { Plugin } from "vite"
28
- import fs from "fs"
29
- import path from "path"
30
- import { execFileSync } from "child_process"
11
+ import path from "node:path"
31
12
  import {
32
- generateDeclarations,
33
- generateReactorFile,
34
- generateClientFile,
13
+ runCanisterPipeline,
14
+ type CanisterConfig,
15
+ type CodegenConfig,
35
16
  } 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
- }
17
+ import { getIcEnvironmentInfo, buildIcEnvCookie } from "./env.js"
46
18
 
47
19
  export interface IcReactorPluginOptions {
48
- /** List of canisters to generate hooks for */
20
+ /**
21
+ * Canister configurations.
22
+ * `name` is required for each canister.
23
+ */
49
24
  canisters: CanisterConfig[]
50
- /** Base output directory (default: ./src/lib/canisters) */
25
+ /**
26
+ * Default output directory (relative to project root).
27
+ * Default: "src/declarations"
28
+ */
51
29
  outDir?: string
52
30
  /**
53
- * Path to import ClientManager from (relative to generated file).
31
+ * Default client manager import path.
54
32
  * Default: "../../clients"
55
33
  */
56
34
  clientManagerPath?: string
57
35
  /**
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`.
36
+ * Automatically inject `ic_env` cookie for local development?
37
+ * Default: true
62
38
  */
63
39
  injectEnvironment?: boolean
64
40
  }
65
41
 
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
42
+ export function icReactorPlugin(options: IcReactorPluginOptions): any {
43
+ const {
44
+ canisters,
45
+ outDir = "src/declarations",
46
+ clientManagerPath = "../../clients",
47
+ injectEnvironment = true,
48
+ } = options
49
+
50
+ // Construct a partial CodegenConfig to pass to the pipeline
51
+ const globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath"> = {
52
+ outDir,
53
+ clientManagerPath,
97
54
  }
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
55
 
120
- return {
56
+ const plugin: Plugin = {
121
57
  name: "ic-reactor-plugin",
58
+ enforce: "pre", // Run before other plugins
122
59
 
123
- config(_config, { command }) {
124
- if (command !== "serve" || !(options.injectEnvironment ?? true)) {
60
+ config(config, { command }) {
61
+ if (command !== "serve" || !injectEnvironment) {
125
62
  return {}
126
63
  }
127
64
 
128
- const canisterNames = options.canisters.map((c) => c.name)
65
+ // ── Local Development Proxy & Cookies ────────────────────────────────
66
+
67
+ // Always include internet_identity if not present (common need)
68
+ const canisterNames = canisters
69
+ .map((c) => c.name)
70
+ .filter((n): n is string => !!n)
129
71
  if (!canisterNames.includes("internet_identity")) {
130
72
  canisterNames.push("internet_identity")
131
73
  }
74
+
132
75
  const icEnv = getIcEnvironmentInfo(canisterNames)
133
76
 
134
77
  if (!icEnv) {
78
+ // Fallback: just proxy /api to default local replica
135
79
  return {
136
80
  server: {
137
81
  proxy: {
@@ -162,109 +106,75 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
162
106
  },
163
107
 
164
108
  async buildStart() {
165
- // Step 0: Ensure central client manager exists (default: src/lib/clients.ts)
166
- const defaultClientPath = path.resolve(
167
- process.cwd(),
168
- "src/lib/clients.ts"
109
+ // ── Code Generation ──────────────────────────────────────────────────
110
+
111
+ const projectRoot = process.cwd()
112
+ console.log(
113
+ `[ic-reactor] Generating hooks for ${canisters.length} canisters...`
169
114
  )
170
- if (!fs.existsSync(defaultClientPath)) {
171
- console.log(
172
- `[ic-reactor] Default client manager not found. Creating at ${defaultClientPath}`
173
- )
174
- const clientContent = generateClientFile()
175
- fs.mkdirSync(path.dirname(defaultClientPath), { recursive: true })
176
- fs.writeFileSync(defaultClientPath, clientContent)
177
- }
178
115
 
179
- for (const canister of options.canisters) {
180
- let didFile = canister.didFile
181
- const outDir = canister.outDir ?? path.join(baseOutDir, canister.name)
116
+ for (const canisterConfig of canisters) {
117
+ try {
118
+ // If .did file is missing, we might want to attempt pulling it?
119
+ // For now, pipeline fails if missing. The old plugin logic to "download"
120
+ // is omitted for simplicity unless requested, to keep "codegen" pure.
182
121
 
183
- if (!didFile) {
184
- const environment = process.env.ICP_ENVIRONMENT || "local"
122
+ const result = await runCanisterPipeline({
123
+ canisterConfig,
124
+ projectRoot,
125
+ globalConfig,
126
+ })
185
127
 
186
- console.log(
187
- `[ic-reactor] didFile not specified for "${canister.name}". Attempting to download from canister...`
188
- )
189
- try {
190
- const candidContent = execFileSync(
191
- "icp",
192
- [
193
- "canister",
194
- "metadata",
195
- canister.name,
196
- "candid:service",
197
- "-e",
198
- environment,
199
- ],
200
- { encoding: "utf-8" }
201
- ).trim()
202
-
203
- const declarationsDir = path.join(outDir, "declarations")
204
- if (!fs.existsSync(declarationsDir)) {
205
- fs.mkdirSync(declarationsDir, { recursive: true })
206
- }
207
- didFile = path.join(declarationsDir, `${canister.name}.did`)
208
- fs.writeFileSync(didFile, candidContent)
209
- console.log(
210
- `[ic-reactor] Candid downloaded and saved to ${didFile}`
211
- )
212
- } catch (error) {
128
+ if (!result.success) {
213
129
  console.error(
214
- `[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
130
+ `[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
215
131
  )
216
- continue
132
+ } else {
133
+ // Optional: log success
134
+ // console.log(`[ic-reactor] Generated ${canisterConfig.name} hooks`)
217
135
  }
218
- }
219
-
220
- console.log(
221
- `[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
222
- )
223
-
224
- // Step 1: Generate declarations via @ic-reactor/codegen
225
- const result = await generateDeclarations({
226
- didFile: didFile,
227
- outDir,
228
- canisterName: canister.name,
229
- })
230
-
231
- if (!result.success) {
136
+ } catch (err) {
232
137
  console.error(
233
- `[ic-reactor] Failed to generate declarations: ${result.error}`
138
+ `[ic-reactor] Error generating ${canisterConfig.name}:`,
139
+ err
234
140
  )
235
- continue
236
141
  }
237
-
238
- // Step 2: Generate the reactor file using shared codegen
239
- const reactorContent = generateReactorFile({
240
- canisterName: canister.name,
241
- didFile: didFile,
242
- clientManagerPath:
243
- canister.clientManagerPath ?? options.clientManagerPath,
244
- })
245
-
246
- const reactorPath = path.join(outDir, "index.ts")
247
- fs.mkdirSync(outDir, { recursive: true })
248
- fs.writeFileSync(reactorPath, reactorContent)
249
-
250
- console.log(`[ic-reactor] Reactor hooks generated at ${reactorPath}`)
251
142
  }
252
143
  },
253
144
 
254
145
  handleHotUpdate({ file, server }) {
255
- // Watch for .did file changes and regenerate
146
+ // ── Hot Reload on .did changes ───────────────────────────────────────
256
147
  if (file.endsWith(".did")) {
257
- const canister = options.canisters.find((c) => {
258
- if (!c.didFile) return false
259
- return path.resolve(c.didFile) === file
148
+ const affectedCanister = canisters.find((c) => {
149
+ // Check if changed file matches configured didFile
150
+ // Cast is safe because didFile is required in CanisterConfig
151
+ const configPath = path.resolve(process.cwd(), c.didFile)
152
+ return configPath === file
260
153
  })
261
- if (canister) {
154
+
155
+ if (affectedCanister) {
262
156
  console.log(
263
- `[ic-reactor] Detected change in ${file}, regenerating...`
157
+ `[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
264
158
  )
265
- server.restart()
159
+
160
+ // Re-run pipeline for this canister
161
+ const projectRoot = process.cwd()
162
+ runCanisterPipeline({
163
+ canisterConfig: affectedCanister,
164
+ projectRoot,
165
+ globalConfig,
166
+ }).then((result: import("@ic-reactor/codegen").PipelineResult) => {
167
+ if (result.success) {
168
+ // Reload page to reflect new types/hooks
169
+ server.ws.send({ type: "full-reload" })
170
+ } else {
171
+ console.error(`[ic-reactor] Regeneration failed: ${result.error}`)
172
+ }
173
+ })
266
174
  }
267
175
  }
268
176
  },
269
177
  }
178
+
179
+ return plugin
270
180
  }