@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/README.md +32 -114
- package/dist/index.cjs +65 -87
- package/dist/index.d.cts +18 -36
- package/dist/index.d.ts +18 -36
- package/dist/index.js +65 -89
- package/package.json +2 -2
- package/src/env.ts +80 -0
- package/src/index.test.ts +21 -98
- package/src/index.ts +93 -184
package/src/index.ts
CHANGED
|
@@ -1,137 +1,82 @@
|
|
|
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
|
-
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Canister configurations.
|
|
23
|
+
* `name` is required for each canister.
|
|
24
|
+
*/
|
|
49
25
|
canisters: CanisterConfig[]
|
|
50
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Default output directory (relative to project root).
|
|
28
|
+
* Default: "src/declarations"
|
|
29
|
+
*/
|
|
51
30
|
outDir?: string
|
|
52
31
|
/**
|
|
53
|
-
*
|
|
32
|
+
* Default client manager import path.
|
|
54
33
|
* Default: "../../clients"
|
|
55
34
|
*/
|
|
56
35
|
clientManagerPath?: string
|
|
57
36
|
/**
|
|
58
|
-
* Automatically inject
|
|
59
|
-
*
|
|
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
|
|
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(
|
|
124
|
-
if (command !== "serve" || !
|
|
61
|
+
config(config, { command }) {
|
|
62
|
+
if (command !== "serve" || !injectEnvironment) {
|
|
125
63
|
return {}
|
|
126
64
|
}
|
|
127
65
|
|
|
128
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
112
|
+
const projectRoot = process.cwd()
|
|
113
|
+
console.log(
|
|
114
|
+
`[ic-reactor] Generating hooks for ${canisters.length} canisters...`
|
|
115
|
+
)
|
|
182
116
|
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
131
|
+
`[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
|
|
215
132
|
)
|
|
216
|
-
|
|
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]
|
|
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
|
-
//
|
|
147
|
+
// ── Hot Reload on .did changes ───────────────────────────────────────
|
|
256
148
|
if (file.endsWith(".did")) {
|
|
257
|
-
const
|
|
258
|
-
if
|
|
259
|
-
|
|
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
|
-
|
|
155
|
+
|
|
156
|
+
if (affectedCanister) {
|
|
262
157
|
console.log(
|
|
263
|
-
`[ic-reactor]
|
|
158
|
+
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
264
159
|
)
|
|
265
|
-
|
|
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
|
},
|