@ic-reactor/vite-plugin 0.4.0 → 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 +68 -87
- package/dist/index.d.cts +18 -36
- package/dist/index.d.ts +18 -36
- package/dist/index.js +68 -89
- package/package.json +2 -2
- package/src/env.ts +80 -0
- package/src/index.test.ts +21 -98
- package/src/index.ts +96 -184
package/src/index.ts
CHANGED
|
@@ -1,134 +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)
|
|
72
|
+
if (!canisterNames.includes("internet_identity")) {
|
|
73
|
+
canisterNames.push("internet_identity")
|
|
74
|
+
}
|
|
75
|
+
|
|
129
76
|
const icEnv = getIcEnvironmentInfo(canisterNames)
|
|
130
77
|
|
|
131
78
|
if (!icEnv) {
|
|
79
|
+
// Fallback: just proxy /api to default local replica
|
|
132
80
|
return {
|
|
133
81
|
server: {
|
|
134
82
|
proxy: {
|
|
@@ -159,107 +107,71 @@ export function icReactorPlugin(options: IcReactorPluginOptions): Plugin {
|
|
|
159
107
|
},
|
|
160
108
|
|
|
161
109
|
async buildStart() {
|
|
162
|
-
//
|
|
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
|
-
}
|
|
110
|
+
// ── Code Generation ──────────────────────────────────────────────────
|
|
175
111
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
112
|
+
const projectRoot = process.cwd()
|
|
113
|
+
console.log(
|
|
114
|
+
`[ic-reactor] Generating hooks for ${canisters.length} canisters...`
|
|
115
|
+
)
|
|
179
116
|
|
|
180
|
-
|
|
181
|
-
|
|
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.
|
|
182
122
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"icp",
|
|
189
|
-
[
|
|
190
|
-
"canister",
|
|
191
|
-
"metadata",
|
|
192
|
-
canister.name,
|
|
193
|
-
"candid:service",
|
|
194
|
-
"-e",
|
|
195
|
-
environment,
|
|
196
|
-
],
|
|
197
|
-
{ encoding: "utf-8" }
|
|
198
|
-
).trim()
|
|
123
|
+
const result = await runCanisterPipeline({
|
|
124
|
+
canisterConfig,
|
|
125
|
+
projectRoot,
|
|
126
|
+
globalConfig,
|
|
127
|
+
})
|
|
199
128
|
|
|
200
|
-
|
|
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) {
|
|
129
|
+
if (!result.success) {
|
|
210
130
|
console.error(
|
|
211
|
-
`[ic-reactor] Failed to
|
|
131
|
+
`[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
|
|
212
132
|
)
|
|
213
|
-
|
|
133
|
+
} else {
|
|
134
|
+
// Optional: log success
|
|
135
|
+
// console.log(`[ic-reactor] Generated ${canisterConfig.name} hooks`)
|
|
214
136
|
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
console.log(
|
|
218
|
-
`[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
// Step 1: Generate declarations via @ic-reactor/codegen
|
|
222
|
-
const result = await generateDeclarations({
|
|
223
|
-
didFile: didFile,
|
|
224
|
-
outDir,
|
|
225
|
-
canisterName: canister.name,
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
if (!result.success) {
|
|
137
|
+
} catch (err) {
|
|
229
138
|
console.error(
|
|
230
|
-
`[ic-reactor]
|
|
139
|
+
`[ic-reactor] Error generating ${canisterConfig.name}:`,
|
|
140
|
+
err
|
|
231
141
|
)
|
|
232
|
-
continue
|
|
233
142
|
}
|
|
234
|
-
|
|
235
|
-
// Step 2: Generate the reactor file using shared codegen
|
|
236
|
-
const reactorContent = generateReactorFile({
|
|
237
|
-
canisterName: canister.name,
|
|
238
|
-
didFile: didFile,
|
|
239
|
-
clientManagerPath:
|
|
240
|
-
canister.clientManagerPath ?? options.clientManagerPath,
|
|
241
|
-
})
|
|
242
|
-
|
|
243
|
-
const reactorPath = path.join(outDir, "index.ts")
|
|
244
|
-
fs.mkdirSync(outDir, { recursive: true })
|
|
245
|
-
fs.writeFileSync(reactorPath, reactorContent)
|
|
246
|
-
|
|
247
|
-
console.log(`[ic-reactor] Reactor hooks generated at ${reactorPath}`)
|
|
248
143
|
}
|
|
249
144
|
},
|
|
250
145
|
|
|
251
146
|
handleHotUpdate({ file, server }) {
|
|
252
|
-
//
|
|
147
|
+
// ── Hot Reload on .did changes ───────────────────────────────────────
|
|
253
148
|
if (file.endsWith(".did")) {
|
|
254
|
-
const
|
|
255
|
-
if
|
|
256
|
-
|
|
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
|
|
257
154
|
})
|
|
258
|
-
|
|
155
|
+
|
|
156
|
+
if (affectedCanister) {
|
|
259
157
|
console.log(
|
|
260
|
-
`[ic-reactor]
|
|
158
|
+
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
261
159
|
)
|
|
262
|
-
|
|
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
|
+
})
|
|
263
175
|
}
|
|
264
176
|
}
|
|
265
177
|
},
|