@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/src/index.ts CHANGED
@@ -1,134 +1,82 @@
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
- import type { Plugin } from "vite"
28
- import fs from "fs"
29
- import path from "path"
30
- import { execFileSync } from "child_process"
10
+ import type { Plugin, UserConfig } from "vite"
11
+ import fs from "node:fs"
12
+ import path from "node:path"
31
13
  import {
32
- generateDeclarations,
33
- generateReactorFile,
34
- generateClientFile,
14
+ runCanisterPipeline,
15
+ type CanisterConfig,
16
+ type CodegenConfig,
35
17
  } 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
- }
18
+ import { getIcEnvironmentInfo, buildIcEnvCookie } from "./env.js"
46
19
 
47
20
  export interface IcReactorPluginOptions {
48
- /** List of canisters to generate hooks for */
21
+ /**
22
+ * Canister configurations.
23
+ * `name` is required for each canister.
24
+ */
49
25
  canisters: CanisterConfig[]
50
- /** Base output directory (default: ./src/lib/canisters) */
26
+ /**
27
+ * Default output directory (relative to project root).
28
+ * Default: "src/declarations"
29
+ */
51
30
  outDir?: string
52
31
  /**
53
- * Path to import ClientManager from (relative to generated file).
32
+ * Default client manager import path.
54
33
  * Default: "../../clients"
55
34
  */
56
35
  clientManagerPath?: string
57
36
  /**
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`.
37
+ * Automatically inject `ic_env` cookie for local development?
38
+ * Default: true
62
39
  */
63
40
  injectEnvironment?: boolean
64
41
  }
65
42
 
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
43
  export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
118
- const baseOutDir = options.outDir ?? "./src/lib/canisters"
44
+ const {
45
+ canisters,
46
+ outDir = "src/declarations",
47
+ clientManagerPath = "../../clients",
48
+ injectEnvironment = true,
49
+ } = options
50
+
51
+ // Construct a partial CodegenConfig to pass to the pipeline
52
+ const globalConfig: Pick<CodegenConfig, "outDir" | "clientManagerPath"> = {
53
+ outDir,
54
+ clientManagerPath,
55
+ }
119
56
 
120
57
  return {
121
58
  name: "ic-reactor-plugin",
59
+ enforce: "pre", // Run before other plugins
122
60
 
123
- config(_config, { command }) {
124
- if (command !== "serve" || !(options.injectEnvironment ?? true)) {
61
+ config(config, { command }) {
62
+ if (command !== "serve" || !injectEnvironment) {
125
63
  return {}
126
64
  }
127
65
 
128
- const canisterNames = options.canisters.map((c) => c.name)
66
+ // ── Local Development Proxy & Cookies ────────────────────────────────
67
+
68
+ // Always include internet_identity if not present (common need)
69
+ const canisterNames = canisters
70
+ .map((c) => c.name)
71
+ .filter((n): n is string => !!n)
72
+ if (!canisterNames.includes("internet_identity")) {
73
+ canisterNames.push("internet_identity")
74
+ }
75
+
129
76
  const icEnv = getIcEnvironmentInfo(canisterNames)
130
77
 
131
78
  if (!icEnv) {
79
+ // Fallback: just proxy /api to default local replica
132
80
  return {
133
81
  server: {
134
82
  proxy: {
@@ -159,107 +107,71 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
159
107
  },
160
108
 
161
109
  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
- }
110
+ // ── Code Generation ──────────────────────────────────────────────────
175
111
 
176
- for (const canister of options.canisters) {
177
- let didFile = canister.didFile
178
- const outDir = canister.outDir ?? path.join(baseOutDir, canister.name)
112
+ const projectRoot = process.cwd()
113
+ console.log(
114
+ `[ic-reactor] Generating hooks for ${canisters.length} canisters...`
115
+ )
179
116
 
180
- if (!didFile) {
181
- const environment = process.env.ICP_ENVIRONMENT || "local"
117
+ for (const canisterConfig of canisters) {
118
+ try {
119
+ // If .did file is missing, we might want to attempt pulling it?
120
+ // For now, pipeline fails if missing. The old plugin logic to "download"
121
+ // is omitted for simplicity unless requested, to keep "codegen" pure.
182
122
 
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()
123
+ const result = await runCanisterPipeline({
124
+ canisterConfig,
125
+ projectRoot,
126
+ globalConfig,
127
+ })
199
128
 
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) {
129
+ if (!result.success) {
210
130
  console.error(
211
- `[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
131
+ `[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
212
132
  )
213
- continue
133
+ } else {
134
+ // Optional: log success
135
+ // console.log(`[ic-reactor] Generated ${canisterConfig.name} hooks`)
214
136
  }
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) {
137
+ } catch (err) {
229
138
  console.error(
230
- `[ic-reactor] Failed to generate declarations: ${result.error}`
139
+ `[ic-reactor] Error generating ${canisterConfig.name}:`,
140
+ err
231
141
  )
232
- continue
233
142
  }
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
143
  }
249
144
  },
250
145
 
251
146
  handleHotUpdate({ file, server }) {
252
- // Watch for .did file changes and regenerate
147
+ // ── Hot Reload on .did changes ───────────────────────────────────────
253
148
  if (file.endsWith(".did")) {
254
- const canister = options.canisters.find((c) => {
255
- if (!c.didFile) return false
256
- return path.resolve(c.didFile) === file
149
+ const affectedCanister = canisters.find((c) => {
150
+ // Check if changed file matches configured didFile
151
+ // Cast is safe because didFile is required in CanisterConfig
152
+ const configPath = path.resolve(process.cwd(), c.didFile)
153
+ return configPath === file
257
154
  })
258
- if (canister) {
155
+
156
+ if (affectedCanister) {
259
157
  console.log(
260
- `[ic-reactor] Detected change in ${file}, regenerating...`
158
+ `[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
261
159
  )
262
- server.restart()
160
+
161
+ // Re-run pipeline for this canister
162
+ const projectRoot = process.cwd()
163
+ runCanisterPipeline({
164
+ canisterConfig: affectedCanister,
165
+ projectRoot,
166
+ globalConfig,
167
+ }).then((result: import("@ic-reactor/codegen").PipelineResult) => {
168
+ if (result.success) {
169
+ // Reload page to reflect new types/hooks
170
+ server.ws.send({ type: "full-reload" })
171
+ } else {
172
+ console.error(`[ic-reactor] Regeneration failed: ${result.error}`)
173
+ }
174
+ })
263
175
  }
264
176
  }
265
177
  },