@ic-reactor/vite-plugin 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,226 @@
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, ViteDevServer } from "vite"
28
+ import fs from "fs"
29
+ import path from "path"
30
+ import {
31
+ generateDeclarations,
32
+ generateReactorFile,
33
+ type CanisterConfig,
34
+ } from "@ic-reactor/codegen"
35
+
36
+ const ICP_LOCAL_IDS_PATH = ".icp/cache/mappings/local.ids.json"
37
+ const IC_ROOT_KEY_HEX =
38
+ "308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c050302010361008b52b4994f94c7ce4be1c1542d7c81dc79fea17d49efe8fa42e8566373581d4b969c4a59e96a0ef51b711fe5027ec01601182519d0a788f4bfe388e593b97cd1d7e44904de79422430bca686ac8c21305b3397b5ba4d7037d17877312fb7ee34"
39
+
40
+ // ═══════════════════════════════════════════════════════════════════════════
41
+ // TYPES
42
+ // ═══════════════════════════════════════════════════════════════════════════
43
+
44
+ export interface IcReactorPluginOptions {
45
+ /** List of canisters to generate hooks for */
46
+ canisters: (CanisterConfig & { name: string; advanced?: boolean })[]
47
+ /** Base output directory (default: ./src/canisters) */
48
+ outDir?: string
49
+ /**
50
+ * Path to import ClientManager from (relative to generated file).
51
+ * Default: "../../lib/client"
52
+ */
53
+ clientManagerPath?: string
54
+ /**
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
+ */
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
65
+ }
66
+
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)
72
+
73
+ try {
74
+ return JSON.parse(fs.readFileSync(idsPath, "utf-8"))
75
+ } catch {
76
+ return null
77
+ }
78
+ }
79
+
80
+ function buildIcEnvCookie(canisterIds: Record<string, string>): string {
81
+ const envParts = [`ic_root_key=${IC_ROOT_KEY_HEX}`]
82
+
83
+ for (const [name, id] of Object.entries(canisterIds)) {
84
+ envParts.push(`PUBLIC_CANISTER_ID:${name}=${id}`)
85
+ }
86
+
87
+ return encodeURIComponent(envParts.join("&"))
88
+ }
89
+
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
+ // ═══════════════════════════════════════════════════════════════════════════
136
+ // VITE PLUGIN
137
+ // ═══════════════════════════════════════════════════════════════════════════
138
+
139
+ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
140
+ const baseOutDir = options.outDir ?? "./src/canisters"
141
+
142
+ return {
143
+ name: "ic-reactor-plugin",
144
+
145
+ configureServer(server) {
146
+ if (options.autoInjectIcEnv ?? true) {
147
+ setupIcEnvMiddleware(server)
148
+ }
149
+ },
150
+
151
+ async buildStart() {
152
+ for (const canister of options.canisters) {
153
+ const outDir = canister.outDir ?? path.join(baseOutDir, canister.name)
154
+
155
+ console.log(
156
+ `[ic-reactor] Generating hooks for ${canister.name} from ${canister.didFile}`
157
+ )
158
+
159
+ // Step 1: Generate declarations via @icp-sdk/bindgen
160
+ const result = await generateDeclarations({
161
+ didFile: canister.didFile,
162
+ outDir,
163
+ canisterName: canister.name,
164
+ })
165
+
166
+ if (!result.success) {
167
+ console.error(
168
+ `[ic-reactor] Failed to generate declarations: ${result.error}`
169
+ )
170
+ continue
171
+ }
172
+
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
192
+ const reactorContent = generateReactorFile({
193
+ canisterName: canister.name,
194
+ canisterConfig: canister,
195
+ globalClientManagerPath: options.clientManagerPath,
196
+ hasDeclarations: true,
197
+ advanced: useAdvanced,
198
+ didContent,
199
+ })
200
+
201
+ const reactorPath = path.join(outDir, "index.ts")
202
+ fs.mkdirSync(outDir, { recursive: true })
203
+ fs.writeFileSync(reactorPath, reactorContent)
204
+
205
+ console.log(`[ic-reactor] Reactor hooks generated at ${reactorPath}`)
206
+ }
207
+ },
208
+
209
+ handleHotUpdate({ file, server }) {
210
+ // Watch for .did file changes and regenerate
211
+ if (file.endsWith(".did")) {
212
+ const canister = options.canisters.find(
213
+ (c) => path.resolve(c.didFile) === file
214
+ )
215
+ if (canister) {
216
+ console.log(
217
+ `[ic-reactor] Detected change in ${file}, regenerating...`
218
+ )
219
+ server.restart()
220
+ }
221
+ }
222
+ },
223
+ }
224
+ }
225
+
226
+ export default icReactorPlugin