@ic-reactor/vite-plugin 0.4.1 → 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,137 +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)
129
72
  if (!canisterNames.includes("internet_identity")) {
130
73
  canisterNames.push("internet_identity")
131
74
  }
75
+
132
76
  const icEnv = getIcEnvironmentInfo(canisterNames)
133
77
 
134
78
  if (!icEnv) {
79
+ // Fallback: just proxy /api to default local replica
135
80
  return {
136
81
  server: {
137
82
  proxy: {
@@ -162,107 +107,71 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
162
107
  },
163
108
 
164
109
  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"
169
- )
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
- }
110
+ // ── Code Generation ──────────────────────────────────────────────────
178
111
 
179
- for (const canister of options.canisters) {
180
- let didFile = canister.didFile
181
- 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
+ )
182
116
 
183
- if (!didFile) {
184
- 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.
185
122
 
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()
123
+ const result = await runCanisterPipeline({
124
+ canisterConfig,
125
+ projectRoot,
126
+ globalConfig,
127
+ })
202
128
 
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) {
129
+ if (!result.success) {
213
130
  console.error(
214
- `[ic-reactor] Failed to download candid for ${canister.name}: ${error}`
131
+ `[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
215
132
  )
216
- continue
133
+ } else {
134
+ // Optional: log success
135
+ // console.log(`[ic-reactor] Generated ${canisterConfig.name} hooks`)
217
136
  }
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) {
137
+ } catch (err) {
232
138
  console.error(
233
- `[ic-reactor] Failed to generate declarations: ${result.error}`
139
+ `[ic-reactor] Error generating ${canisterConfig.name}:`,
140
+ err
234
141
  )
235
- continue
236
142
  }
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
143
  }
252
144
  },
253
145
 
254
146
  handleHotUpdate({ file, server }) {
255
- // Watch for .did file changes and regenerate
147
+ // ── Hot Reload on .did changes ───────────────────────────────────────
256
148
  if (file.endsWith(".did")) {
257
- const canister = options.canisters.find((c) => {
258
- if (!c.didFile) return false
259
- 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
260
154
  })
261
- if (canister) {
155
+
156
+ if (affectedCanister) {
262
157
  console.log(
263
- `[ic-reactor] Detected change in ${file}, regenerating...`
158
+ `[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
264
159
  )
265
- 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
+ })
266
175
  }
267
176
  }
268
177
  },