@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/README.md +32 -114
- package/dist/index.cjs +67 -88
- package/dist/index.d.cts +19 -38
- package/dist/index.d.ts +19 -38
- package/dist/index.js +67 -90
- package/package.json +3 -4
- package/src/env.ts +80 -0
- package/src/index.test.ts +21 -99
- package/src/index.ts +94 -184
package/src/index.ts
CHANGED
|
@@ -1,137 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* @ic-reactor/vite-plugin
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
29
|
-
import path from "path"
|
|
30
|
-
import { execFileSync } from "child_process"
|
|
11
|
+
import path from "node:path"
|
|
31
12
|
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Canister configurations.
|
|
22
|
+
* `name` is required for each canister.
|
|
23
|
+
*/
|
|
49
24
|
canisters: CanisterConfig[]
|
|
50
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Default output directory (relative to project root).
|
|
27
|
+
* Default: "src/declarations"
|
|
28
|
+
*/
|
|
51
29
|
outDir?: string
|
|
52
30
|
/**
|
|
53
|
-
*
|
|
31
|
+
* Default client manager import path.
|
|
54
32
|
* Default: "../../clients"
|
|
55
33
|
*/
|
|
56
34
|
clientManagerPath?: string
|
|
57
35
|
/**
|
|
58
|
-
* Automatically inject
|
|
59
|
-
*
|
|
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
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
56
|
+
const plugin: Plugin = {
|
|
121
57
|
name: "ic-reactor-plugin",
|
|
58
|
+
enforce: "pre", // Run before other plugins
|
|
122
59
|
|
|
123
|
-
config(
|
|
124
|
-
if (command !== "serve" || !
|
|
60
|
+
config(config, { command }) {
|
|
61
|
+
if (command !== "serve" || !injectEnvironment) {
|
|
125
62
|
return {}
|
|
126
63
|
}
|
|
127
64
|
|
|
128
|
-
|
|
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
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
122
|
+
const result = await runCanisterPipeline({
|
|
123
|
+
canisterConfig,
|
|
124
|
+
projectRoot,
|
|
125
|
+
globalConfig,
|
|
126
|
+
})
|
|
185
127
|
|
|
186
|
-
|
|
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
|
|
130
|
+
`[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
|
|
215
131
|
)
|
|
216
|
-
|
|
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]
|
|
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
|
-
//
|
|
146
|
+
// ── Hot Reload on .did changes ───────────────────────────────────────
|
|
256
147
|
if (file.endsWith(".did")) {
|
|
257
|
-
const
|
|
258
|
-
if
|
|
259
|
-
|
|
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
|
-
|
|
154
|
+
|
|
155
|
+
if (affectedCanister) {
|
|
262
156
|
console.log(
|
|
263
|
-
`[ic-reactor]
|
|
157
|
+
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
264
158
|
)
|
|
265
|
-
|
|
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
|
}
|