@ic-reactor/vite-plugin 0.2.0 → 0.3.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
@@ -24,61 +24,84 @@
24
24
  * ```
25
25
  */
26
26
 
27
- import type { Plugin, ViteDevServer } from "vite"
27
+ import type { Plugin } from "vite"
28
28
  import fs from "fs"
29
29
  import path from "path"
30
+ import { execFileSync } from "child_process"
30
31
  import {
31
32
  generateDeclarations,
32
33
  generateReactorFile,
33
- type CanisterConfig,
34
+ generateClientFile,
34
35
  } from "@ic-reactor/codegen"
35
36
 
36
- const ICP_LOCAL_IDS_PATH = ".icp/cache/mappings/local.ids.json"
37
- const IC_ROOT_KEY_HEX =
38
- "308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361008b52b4994f94c7ce4be1c1542d7c81dc79fea17d49efe8fa42e8566373581d4b969c4a59e96a0ef51b711fe5027ec01601182519d0a788f4bfe388e593b97cd1d7e44904de79422430bca686ac8c21305b3397b5ba4d7037d17877312fb7ee34"
39
-
40
37
  // ═══════════════════════════════════════════════════════════════════════════
41
38
  // TYPES
42
39
  // ═══════════════════════════════════════════════════════════════════════════
40
+ export interface CanisterConfig {
41
+ name: string
42
+ outDir?: string
43
+ didFile?: string
44
+ clientManagerPath?: string
45
+ }
43
46
 
44
47
  export interface IcReactorPluginOptions {
45
48
  /** List of canisters to generate hooks for */
46
- canisters: (CanisterConfig & { name: string; advanced?: boolean })[]
47
- /** Base output directory (default: ./src/canisters) */
49
+ canisters: CanisterConfig[]
50
+ /** Base output directory (default: ./src/lib/canisters) */
48
51
  outDir?: string
49
52
  /**
50
53
  * Path to import ClientManager from (relative to generated file).
51
- * Default: "../../lib/client"
54
+ * Default: "../../clients"
52
55
  */
53
56
  clientManagerPath?: string
54
57
  /**
55
- * Generate advanced per-method hooks with createQuery/createMutation
56
- * instead of generic actor hooks (default: false).
57
- * Can be overridden per-canister by setting `advanced` on the individual canister entry.
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`.
58
62
  */
59
- advanced?: boolean
60
- /**
61
- * Automatically set the `ic_env` cookie in Vite dev server from
62
- * `.icp/cache/mappings/local.ids.json` (default: true).
63
- */
64
- autoInjectIcEnv?: boolean
63
+ injectEnvironment?: boolean
65
64
  }
66
65
 
67
- // Re-export CanisterConfig for convenience
68
- export type { CanisterConfig }
69
-
70
- function loadLocalCanisterIds(rootDir: string): Record<string, string> | null {
71
- const idsPath = path.resolve(rootDir, ICP_LOCAL_IDS_PATH)
66
+ function getIcEnvironmentInfo(canisterNames: string[]) {
67
+ const environment = process.env.ICP_ENVIRONMENT || "local"
72
68
 
73
69
  try {
74
- return JSON.parse(fs.readFileSync(idsPath, "utf-8"))
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 }
75
95
  } catch {
76
96
  return null
77
97
  }
78
98
  }
79
99
 
80
- function buildIcEnvCookie(canisterIds: Record<string, string>): string {
81
- const envParts = [`ic_root_key=${IC_ROOT_KEY_HEX}`]
100
+ function buildIcEnvCookie(
101
+ canisterIds: Record<string, string>,
102
+ rootKey: string
103
+ ): string {
104
+ const envParts = [`ic_root_key=${rootKey}`]
82
105
 
83
106
  for (const [name, id] of Object.entries(canisterIds)) {
84
107
  envParts.push(`PUBLIC_CANISTER_ID:${name}=${id}`)
@@ -87,78 +110,117 @@ function buildIcEnvCookie(canisterIds: Record<string, string>): string {
87
110
  return encodeURIComponent(envParts.join("&"))
88
111
  }
89
112
 
90
- function addOrReplaceSetCookie(
91
- existing: string | string[] | number | undefined,
92
- cookie: string
93
- ): string[] {
94
- const cookieEntries =
95
- typeof existing === "string"
96
- ? [existing]
97
- : Array.isArray(existing)
98
- ? existing.filter((value): value is string => typeof value === "string")
99
- : []
100
-
101
- const nonIcEnvCookies = cookieEntries.filter(
102
- (entry) => !entry.trim().startsWith("ic_env=")
103
- )
104
-
105
- return [...nonIcEnvCookies, cookie]
106
- }
107
-
108
- function setupIcEnvMiddleware(server: ViteDevServer): void {
109
- const rootDir = server.config.root || process.cwd()
110
- const idsPath = path.resolve(rootDir, ICP_LOCAL_IDS_PATH)
111
- let hasLoggedHint = false
112
-
113
- server.middlewares.use((req, res, next) => {
114
- const canisterIds = loadLocalCanisterIds(rootDir)
115
-
116
- if (!canisterIds) {
117
- if (!hasLoggedHint) {
118
- server.config.logger.info(
119
- `[ic-reactor] icp-cli local IDs not found at ${idsPath}. Run \`icp deploy\` to enable automatic ic_env cookie injection.`
120
- )
121
- hasLoggedHint = true
122
- }
123
-
124
- return next()
125
- }
126
-
127
- const cookie = `ic_env=${buildIcEnvCookie(canisterIds)}; Path=/; SameSite=Lax;`
128
- const current = res.getHeader("Set-Cookie")
129
- res.setHeader("Set-Cookie", addOrReplaceSetCookie(current, cookie))
130
-
131
- next()
132
- })
133
- }
134
-
135
113
  // ═══════════════════════════════════════════════════════════════════════════
136
114
  // VITE PLUGIN
137
115
  // ═══════════════════════════════════════════════════════════════════════════
138
116
 
139
117
  export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
140
- const baseOutDir = options.outDir ?? "./src/canisters"
118
+ const baseOutDir = options.outDir ?? "./src/lib/canisters"
141
119
 
142
120
  return {
143
121
  name: "ic-reactor-plugin",
144
122
 
145
- configureServer(server) {
146
- if (options.autoInjectIcEnv ?? true) {
147
- setupIcEnvMiddleware(server)
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
+ },
148
158
  }
149
159
  },
150
160
 
151
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
+
152
176
  for (const canister of options.canisters) {
177
+ let didFile = canister.didFile
153
178
  const outDir = canister.outDir ?? path.join(baseOutDir, canister.name)
154
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
+
155
217
  console.log(
156
- `[ic-reactor] Generating hooks for ${canister.name} from ${canister.didFile}`
218
+ `[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
157
219
  )
158
220
 
159
- // Step 1: Generate declarations via @icp-sdk/bindgen
221
+ // Step 1: Generate declarations via @ic-reactor/codegen
160
222
  const result = await generateDeclarations({
161
- didFile: canister.didFile,
223
+ didFile: didFile,
162
224
  outDir,
163
225
  canisterName: canister.name,
164
226
  })
@@ -170,32 +232,12 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
170
232
  continue
171
233
  }
172
234
 
173
- console.log(
174
- `[ic-reactor] Declarations generated at ${result.declarationsDir}`
175
- )
176
-
177
- // Step 2: Determine advanced mode (can be set per-canister; falls back to plugin-level)
178
- const useAdvanced = canister.advanced ?? options.advanced ?? false
179
- let didContent: string | undefined
180
- if (useAdvanced) {
181
- try {
182
- didContent = fs.readFileSync(canister.didFile, "utf-8")
183
- } catch (e) {
184
- console.warn(
185
- `[ic-reactor] Could not read DID file at ${canister.didFile}, skipping advanced hook generation for ${canister.name}.`
186
- )
187
- continue
188
- }
189
- }
190
-
191
- // Step 3: Generate the reactor file using shared codegen
235
+ // Step 2: Generate the reactor file using shared codegen
192
236
  const reactorContent = generateReactorFile({
193
237
  canisterName: canister.name,
194
- canisterConfig: canister,
195
- globalClientManagerPath: options.clientManagerPath,
196
- hasDeclarations: true,
197
- advanced: useAdvanced,
198
- didContent,
238
+ didFile: didFile,
239
+ clientManagerPath:
240
+ canister.clientManagerPath ?? options.clientManagerPath,
199
241
  })
200
242
 
201
243
  const reactorPath = path.join(outDir, "index.ts")
@@ -209,9 +251,10 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
209
251
  handleHotUpdate({ file, server }) {
210
252
  // Watch for .did file changes and regenerate
211
253
  if (file.endsWith(".did")) {
212
- const canister = options.canisters.find(
213
- (c) => path.resolve(c.didFile) === file
214
- )
254
+ const canister = options.canisters.find((c) => {
255
+ if (!c.didFile) return false
256
+ return path.resolve(c.didFile) === file
257
+ })
215
258
  if (canister) {
216
259
  console.log(
217
260
  `[ic-reactor] Detected change in ${file}, regenerating...`
@@ -222,5 +265,3 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
222
265
  },
223
266
  }
224
267
  }
225
-
226
- export default icReactorPlugin