@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/README.md +28 -147
- package/dist/index.cjs +114 -66
- package/dist/index.d.cts +15 -19
- package/dist/index.d.ts +15 -19
- package/dist/index.js +116 -67
- package/package.json +7 -5
- package/src/index.test.ts +273 -0
- package/src/index.ts +148 -107
package/src/index.ts
CHANGED
|
@@ -24,61 +24,84 @@
|
|
|
24
24
|
* ```
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import type { Plugin
|
|
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
|
-
|
|
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:
|
|
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: "../../
|
|
54
|
+
* Default: "../../clients"
|
|
52
55
|
*/
|
|
53
56
|
clientManagerPath?: string
|
|
54
57
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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(
|
|
81
|
-
|
|
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
|
-
|
|
146
|
-
if (options.
|
|
147
|
-
|
|
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 ${
|
|
218
|
+
`[ic-reactor] Generating hooks for ${canister.name} from ${didFile}`
|
|
157
219
|
)
|
|
158
220
|
|
|
159
|
-
// Step 1: Generate declarations via @
|
|
221
|
+
// Step 1: Generate declarations via @ic-reactor/codegen
|
|
160
222
|
const result = await generateDeclarations({
|
|
161
|
-
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|