@ic-reactor/vite-plugin 0.12.0 → 0.13.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 +26 -2
- package/dist/index.cjs +187 -63
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +187 -63
- package/llms.txt +8 -1
- package/package.json +6 -5
- package/src/env.ts +64 -16
- package/src/index.ts +310 -59
- package/src/index.test.ts +0 -349
package/dist/index.js
CHANGED
|
@@ -7,44 +7,59 @@ import {
|
|
|
7
7
|
// src/env.ts
|
|
8
8
|
import { execFileSync } from "child_process";
|
|
9
9
|
function getIcEnvironmentInfo(canisterNames) {
|
|
10
|
-
const
|
|
10
|
+
const networkName = process.env.ICP_ENVIRONMENT || "local";
|
|
11
|
+
const diagnostics = [];
|
|
11
12
|
try {
|
|
12
13
|
const networkStatus = JSON.parse(
|
|
13
|
-
execFileSync("icp", ["network", "status", "-e",
|
|
14
|
+
execFileSync("icp", ["network", "status", "-e", networkName, "--json"], {
|
|
14
15
|
encoding: "utf-8",
|
|
15
|
-
|
|
16
|
-
//
|
|
16
|
+
// stderr is piped rather than ignored so a failure can explain itself.
|
|
17
|
+
// Piping still keeps it off the terminal — it only reaches the user if
|
|
18
|
+
// the caller decides to print the diagnostics we collect below.
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
17
20
|
})
|
|
18
21
|
);
|
|
19
22
|
const rootKey = networkStatus.root_key;
|
|
20
23
|
const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
|
|
21
24
|
if (!proxyTarget) {
|
|
22
|
-
|
|
25
|
+
diagnostics.push(
|
|
26
|
+
`\`icp network status -e ${networkName}\` reported no api_url, gateway_url or port`
|
|
27
|
+
);
|
|
28
|
+
return { environment: null, diagnostics };
|
|
23
29
|
}
|
|
24
30
|
const canisterIds = {};
|
|
25
31
|
for (const name of canisterNames) {
|
|
26
32
|
try {
|
|
27
33
|
const canisterId = execFileSync(
|
|
28
34
|
"icp",
|
|
29
|
-
["canister", "status", name, "-e",
|
|
30
|
-
{ encoding: "utf-8", stdio: ["ignore", "pipe", "
|
|
35
|
+
["canister", "status", name, "-e", networkName, "-i"],
|
|
36
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
|
|
31
37
|
).trim();
|
|
32
38
|
if (canisterId) {
|
|
33
39
|
canisterIds[name] = canisterId;
|
|
34
40
|
}
|
|
35
|
-
} catch {
|
|
41
|
+
} catch (error) {
|
|
42
|
+
diagnostics.push(
|
|
43
|
+
`\`icp canister status ${name} -e ${networkName}\` failed: ${describeExecError(error)}`
|
|
44
|
+
);
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
|
-
const internetIdentityProvider = !canisterIds.internet_identity && isLocalhostGateway(proxyTarget) &&
|
|
47
|
+
const internetIdentityProvider = !canisterIds.internet_identity && isLocalhostGateway(proxyTarget) && networkName !== "ic" ? localInternetIdentityProvider(proxyTarget) : void 0;
|
|
39
48
|
return {
|
|
40
|
-
environment
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
environment: {
|
|
50
|
+
environment: networkName,
|
|
51
|
+
rootKey,
|
|
52
|
+
proxyTarget,
|
|
53
|
+
canisterIds,
|
|
54
|
+
internetIdentityProvider
|
|
55
|
+
},
|
|
56
|
+
diagnostics
|
|
45
57
|
};
|
|
46
58
|
} catch (error) {
|
|
47
|
-
|
|
59
|
+
diagnostics.push(
|
|
60
|
+
`\`icp network status -e ${networkName}\` failed: ${describeExecError(error)}`
|
|
61
|
+
);
|
|
62
|
+
return { environment: null, diagnostics };
|
|
48
63
|
}
|
|
49
64
|
}
|
|
50
65
|
function buildIcEnvCookie(canisterIds, rootKey, internetIdentityProvider) {
|
|
@@ -57,6 +72,15 @@ function buildIcEnvCookie(canisterIds, rootKey, internetIdentityProvider) {
|
|
|
57
72
|
}
|
|
58
73
|
return encodeURIComponent(parts.join("&"));
|
|
59
74
|
}
|
|
75
|
+
function describeExecError(error) {
|
|
76
|
+
const stderr = error?.stderr;
|
|
77
|
+
const text = typeof stderr === "string" ? stderr : stderr?.toString("utf-8");
|
|
78
|
+
const trimmed = text?.trim();
|
|
79
|
+
if (trimmed) {
|
|
80
|
+
return trimmed;
|
|
81
|
+
}
|
|
82
|
+
return error instanceof Error ? error.message : String(error);
|
|
83
|
+
}
|
|
60
84
|
function isLocalhostGateway(proxyTarget) {
|
|
61
85
|
try {
|
|
62
86
|
const { hostname } = new URL(proxyTarget);
|
|
@@ -72,40 +96,103 @@ function localInternetIdentityProvider(proxyTarget) {
|
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
// src/index.ts
|
|
99
|
+
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
100
|
+
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
75
101
|
function icReactor(options) {
|
|
76
102
|
const {
|
|
77
103
|
canisters,
|
|
78
104
|
outDir = "src/declarations",
|
|
79
105
|
clientManagerPath = "../../clients",
|
|
80
106
|
target = "react",
|
|
81
|
-
injectEnvironment = true
|
|
107
|
+
injectEnvironment = true,
|
|
108
|
+
failOnError
|
|
82
109
|
} = options;
|
|
83
110
|
const globalConfig = {
|
|
84
111
|
outDir,
|
|
85
112
|
clientManagerPath,
|
|
86
113
|
target
|
|
87
114
|
};
|
|
88
|
-
|
|
115
|
+
let projectRoot = process.cwd();
|
|
116
|
+
let command = "build";
|
|
117
|
+
let devServer = null;
|
|
89
118
|
const resolveDidPath = (didFile) => path.normalize(
|
|
90
119
|
path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
|
|
91
120
|
);
|
|
92
121
|
const configuredCanisterIds = Object.fromEntries(
|
|
93
122
|
canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
|
|
94
123
|
);
|
|
124
|
+
let pendingFailure = null;
|
|
125
|
+
const reportFailure = (server, message, cause) => {
|
|
126
|
+
console.error(`[ic-reactor] ${message}`);
|
|
127
|
+
const err = {
|
|
128
|
+
message: `[ic-reactor] ${message}`,
|
|
129
|
+
stack: cause instanceof Error && cause.stack ? cause.stack : "",
|
|
130
|
+
plugin: PLUGIN_NAME
|
|
131
|
+
};
|
|
132
|
+
pendingFailure = { message: err.message, stack: err.stack };
|
|
133
|
+
server?.ws.send({ type: "error", err });
|
|
134
|
+
};
|
|
135
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
136
|
+
const rerunQueued = /* @__PURE__ */ new Set();
|
|
137
|
+
const regenerate = (canisterConfig, server) => {
|
|
138
|
+
const { name } = canisterConfig;
|
|
139
|
+
const running = inFlight.get(name);
|
|
140
|
+
if (running) {
|
|
141
|
+
rerunQueued.add(name);
|
|
142
|
+
return running;
|
|
143
|
+
}
|
|
144
|
+
const run = runCanisterPipeline({
|
|
145
|
+
canisterConfig,
|
|
146
|
+
projectRoot,
|
|
147
|
+
globalConfig
|
|
148
|
+
}).then((result) => {
|
|
149
|
+
if (result.success) {
|
|
150
|
+
pendingFailure = null;
|
|
151
|
+
server.ws.send({ type: "full-reload" });
|
|
152
|
+
} else {
|
|
153
|
+
reportFailure(
|
|
154
|
+
server,
|
|
155
|
+
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}).catch((error) => {
|
|
159
|
+
reportFailure(
|
|
160
|
+
server,
|
|
161
|
+
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
162
|
+
error
|
|
163
|
+
);
|
|
164
|
+
}).finally(() => {
|
|
165
|
+
inFlight.delete(name);
|
|
166
|
+
if (rerunQueued.delete(name)) {
|
|
167
|
+
void regenerate(canisterConfig, server);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
inFlight.set(name, run);
|
|
171
|
+
return run;
|
|
172
|
+
};
|
|
95
173
|
const plugin = {
|
|
96
|
-
name:
|
|
174
|
+
name: PLUGIN_NAME,
|
|
97
175
|
enforce: "pre",
|
|
98
176
|
// Run before other plugins
|
|
99
|
-
config(_config, { command }) {
|
|
100
|
-
|
|
177
|
+
config(_config, { command: viteCommand }) {
|
|
178
|
+
command = viteCommand;
|
|
179
|
+
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
101
180
|
return {};
|
|
102
181
|
}
|
|
103
182
|
const canisterNames = canisters.map((c) => c.name).filter((n) => !!n);
|
|
104
183
|
if (!canisterNames.includes("internet_identity")) {
|
|
105
184
|
canisterNames.push("internet_identity");
|
|
106
185
|
}
|
|
107
|
-
const icEnv = getIcEnvironmentInfo(canisterNames);
|
|
186
|
+
const { environment: icEnv, diagnostics } = getIcEnvironmentInfo(canisterNames);
|
|
108
187
|
if (!icEnv) {
|
|
188
|
+
if (canisters.length > 0) {
|
|
189
|
+
console.warn(
|
|
190
|
+
`[ic-reactor] Could not detect the local IC environment, falling back to ${DEFAULT_LOCAL_REPLICA}. Canister IDs and the root key will not be injected \u2014 is the local replica running? Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
for (const diagnostic of diagnostics) {
|
|
194
|
+
debugLog(diagnostic);
|
|
195
|
+
}
|
|
109
196
|
const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
|
|
110
197
|
{},
|
|
111
198
|
void 0,
|
|
@@ -118,13 +205,26 @@ function icReactor(options) {
|
|
|
118
205
|
} : void 0,
|
|
119
206
|
proxy: {
|
|
120
207
|
"/api": {
|
|
121
|
-
target:
|
|
208
|
+
target: DEFAULT_LOCAL_REPLICA,
|
|
122
209
|
changeOrigin: true
|
|
123
210
|
}
|
|
124
211
|
}
|
|
125
212
|
}
|
|
126
213
|
};
|
|
127
214
|
}
|
|
215
|
+
const missingCanisterIds = canisters.map((canister) => canister.name).filter((name) => !!name).filter(
|
|
216
|
+
(name) => !icEnv.canisterIds[name] && !configuredCanisterIds[name]
|
|
217
|
+
);
|
|
218
|
+
if (missingCanisterIds.length > 0) {
|
|
219
|
+
const names = missingCanisterIds.map((name) => `"${name}"`).join(", ");
|
|
220
|
+
const it = missingCanisterIds.length === 1 ? "it" : "them";
|
|
221
|
+
console.warn(
|
|
222
|
+
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${names}. Deploy ${it} (\`icp deploy\`) \u2014 until then the injected ic_env carries no PUBLIC_CANISTER_ID for ${it} and the app will see an undefined canister id. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
for (const diagnostic of diagnostics) {
|
|
226
|
+
debugLog(diagnostic);
|
|
227
|
+
}
|
|
128
228
|
const cookieValue = buildIcEnvCookie(
|
|
129
229
|
{
|
|
130
230
|
...icEnv.canisterIds,
|
|
@@ -147,7 +247,19 @@ function icReactor(options) {
|
|
|
147
247
|
}
|
|
148
248
|
};
|
|
149
249
|
},
|
|
250
|
+
configResolved(config) {
|
|
251
|
+
projectRoot = config.root;
|
|
252
|
+
command = config.command;
|
|
253
|
+
},
|
|
150
254
|
configureServer(server) {
|
|
255
|
+
devServer = server;
|
|
256
|
+
server.ws.on?.("connection", () => {
|
|
257
|
+
if (!pendingFailure) return;
|
|
258
|
+
server.ws.send({
|
|
259
|
+
type: "error",
|
|
260
|
+
err: { ...pendingFailure, plugin: PLUGIN_NAME }
|
|
261
|
+
});
|
|
262
|
+
});
|
|
151
263
|
const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
|
|
152
264
|
server.watcher.add(didFiles);
|
|
153
265
|
},
|
|
@@ -155,55 +267,67 @@ function icReactor(options) {
|
|
|
155
267
|
console.log(
|
|
156
268
|
`[ic-reactor] Generating canister bindings for ${canisters.length} canisters...`
|
|
157
269
|
);
|
|
158
|
-
await Promise.allSettled(
|
|
159
|
-
canisters.map(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
canisterConfig,
|
|
163
|
-
projectRoot,
|
|
164
|
-
globalConfig
|
|
165
|
-
});
|
|
166
|
-
if (!result.success) {
|
|
167
|
-
console.error(
|
|
168
|
-
`[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
} catch (err) {
|
|
172
|
-
console.error(
|
|
173
|
-
`[ic-reactor] Error generating ${canisterConfig.name}:`,
|
|
174
|
-
err
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
})
|
|
178
|
-
);
|
|
179
|
-
},
|
|
180
|
-
handleHotUpdate({ file, server }) {
|
|
181
|
-
if (file.endsWith(".did")) {
|
|
182
|
-
const affectedCanister = canisters.find((c) => {
|
|
183
|
-
const configPath = resolveDidPath(c.didFile);
|
|
184
|
-
return configPath === path.normalize(file);
|
|
185
|
-
});
|
|
186
|
-
if (affectedCanister) {
|
|
187
|
-
console.log(
|
|
188
|
-
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
189
|
-
);
|
|
190
|
-
runCanisterPipeline({
|
|
191
|
-
canisterConfig: affectedCanister,
|
|
270
|
+
const outcomes = await Promise.allSettled(
|
|
271
|
+
canisters.map(
|
|
272
|
+
(canisterConfig) => runCanisterPipeline({
|
|
273
|
+
canisterConfig,
|
|
192
274
|
projectRoot,
|
|
193
275
|
globalConfig
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
});
|
|
276
|
+
})
|
|
277
|
+
)
|
|
278
|
+
);
|
|
279
|
+
const failures = outcomes.flatMap((outcome, index) => {
|
|
280
|
+
const name = canisters[index]?.name ?? `canister #${index}`;
|
|
281
|
+
if (outcome.status === "rejected") {
|
|
282
|
+
return [`${name}: ${describeError(outcome.reason)}`];
|
|
201
283
|
}
|
|
284
|
+
if (!outcome.value.success) {
|
|
285
|
+
return [`${name}: ${outcome.value.error ?? "unknown error"}`];
|
|
286
|
+
}
|
|
287
|
+
return [];
|
|
288
|
+
});
|
|
289
|
+
if (failures.length === 0) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const message = `Failed to generate ${failures.length} of ${canisters.length} canisters:
|
|
293
|
+
` + failures.map((failure) => ` - ${failure}`).join("\n");
|
|
294
|
+
if (failOnError ?? command === "build") {
|
|
295
|
+
this.error(`[ic-reactor] ${message}`);
|
|
202
296
|
}
|
|
297
|
+
reportFailure(devServer, message);
|
|
298
|
+
},
|
|
299
|
+
handleHotUpdate({ file, server }) {
|
|
300
|
+
if (!file.endsWith(".did")) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const changedPath = path.normalize(file);
|
|
304
|
+
const affectedCanister = canisters.find(
|
|
305
|
+
// Check if changed file matches configured didFile
|
|
306
|
+
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
307
|
+
);
|
|
308
|
+
if (!affectedCanister) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
console.log(
|
|
312
|
+
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
313
|
+
);
|
|
314
|
+
return regenerate(affectedCanister, server);
|
|
203
315
|
}
|
|
204
316
|
};
|
|
205
317
|
return plugin;
|
|
206
318
|
}
|
|
319
|
+
function describeError(error) {
|
|
320
|
+
return error instanceof Error ? error.message : String(error);
|
|
321
|
+
}
|
|
322
|
+
function debugLog(message) {
|
|
323
|
+
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|
|
324
|
+
const scope = entry.trim();
|
|
325
|
+
return scope === "*" || scope === "ic-reactor" || scope.startsWith("ic-reactor:");
|
|
326
|
+
});
|
|
327
|
+
if (enabled) {
|
|
328
|
+
console.debug(`[ic-reactor:debug] ${message}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
207
331
|
export {
|
|
208
332
|
icReactor
|
|
209
333
|
};
|
package/llms.txt
CHANGED
|
@@ -24,11 +24,18 @@ icReactor({
|
|
|
24
24
|
- Runs codegen on startup and `.did` changes
|
|
25
25
|
- Regenerates declarations + `index.generated.ts`
|
|
26
26
|
- Preserves user-facing `index.ts` wrapper after initial creation
|
|
27
|
+
- Relative paths (`didFile`, `outDir`) resolve against Vite's `config.root`
|
|
28
|
+
- A failed canister fails `vite build`; in `vite dev` it goes to the terminal
|
|
29
|
+
and the browser error overlay instead
|
|
27
30
|
|
|
28
31
|
## Key Options
|
|
29
32
|
|
|
30
|
-
- `canisters`, `outDir`, `clientManagerPath`, `target`,
|
|
33
|
+
- Plugin level: `canisters`, `outDir`, `clientManagerPath`, `target`,
|
|
34
|
+
`injectEnvironment`, `failOnError`
|
|
35
|
+
- Per canister (inside `canisters[]`): `name`, `didFile`, `outDir`,
|
|
36
|
+
`clientManagerPath`, `target`, `mode`, `canisterId`
|
|
31
37
|
- `injectEnvironment` for local `ic_env` cookie + `/api` proxy behavior
|
|
38
|
+
- `failOnError` overrides the default (`true` for build, `false` for dev)
|
|
32
39
|
|
|
33
40
|
## Best Practices
|
|
34
41
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Vite plugin for zero-config IC reactor generation from Candid files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist",
|
|
24
24
|
"src",
|
|
25
|
+
"!src/**/*.test.ts",
|
|
26
|
+
"!src/**/__snapshots__",
|
|
25
27
|
"README.md",
|
|
26
28
|
"llms.txt"
|
|
27
29
|
],
|
|
@@ -48,20 +50,19 @@
|
|
|
48
50
|
},
|
|
49
51
|
"homepage": "https://ic-reactor.b3pay.net/v3/packages/vite-plugin",
|
|
50
52
|
"dependencies": {
|
|
51
|
-
"@ic-reactor/codegen": "0.
|
|
53
|
+
"@ic-reactor/codegen": "0.13.0"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
54
56
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
57
|
-
"@
|
|
58
|
-
"@types/node": "^26.1.1",
|
|
59
|
+
"@types/node": "^26.2.0",
|
|
59
60
|
"tsup": "^8.5.1",
|
|
60
61
|
"typescript": "^6.0.3",
|
|
61
62
|
"vitest": "^4.1.10"
|
|
62
63
|
},
|
|
63
64
|
"scripts": {
|
|
64
|
-
"build": "tsup src/index.ts --format esm,cjs --dts --tsconfig tsconfig.json",
|
|
65
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --tsconfig tsconfig.json",
|
|
65
66
|
"dev": "tsup src/index.ts --format esm,cjs --dts --watch --tsconfig tsconfig.json",
|
|
66
67
|
"test": "vitest run",
|
|
67
68
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json"
|
package/src/env.ts
CHANGED
|
@@ -15,19 +15,35 @@ export interface IcEnvironment {
|
|
|
15
15
|
internetIdentityProvider?: string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface IcEnvironmentDetection {
|
|
19
|
+
/** The detected environment, or `null` when `icp` could not answer. */
|
|
20
|
+
environment: IcEnvironment | null
|
|
21
|
+
/**
|
|
22
|
+
* Why detection came back empty or partial — the `icp` stderr where we have
|
|
23
|
+
* it. Detection failing is routine on a machine with no local replica, so
|
|
24
|
+
* these are handed back for the caller to log at its own level rather than
|
|
25
|
+
* printed here.
|
|
26
|
+
*/
|
|
27
|
+
diagnostics: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
31
|
* Detect the IC environment using the `icp` CLI.
|
|
20
32
|
*/
|
|
21
33
|
export function getIcEnvironmentInfo(
|
|
22
34
|
canisterNames: string[]
|
|
23
|
-
):
|
|
24
|
-
const
|
|
35
|
+
): IcEnvironmentDetection {
|
|
36
|
+
const networkName = process.env.ICP_ENVIRONMENT || "local"
|
|
37
|
+
const diagnostics: string[] = []
|
|
25
38
|
|
|
26
39
|
try {
|
|
27
40
|
const networkStatus = JSON.parse(
|
|
28
|
-
execFileSync("icp", ["network", "status", "-e",
|
|
41
|
+
execFileSync("icp", ["network", "status", "-e", networkName, "--json"], {
|
|
29
42
|
encoding: "utf-8",
|
|
30
|
-
|
|
43
|
+
// stderr is piped rather than ignored so a failure can explain itself.
|
|
44
|
+
// Piping still keeps it off the terminal — it only reaches the user if
|
|
45
|
+
// the caller decides to print the diagnostics we collect below.
|
|
46
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
31
47
|
})
|
|
32
48
|
)
|
|
33
49
|
|
|
@@ -40,7 +56,10 @@ export function getIcEnvironmentInfo(
|
|
|
40
56
|
: undefined)
|
|
41
57
|
|
|
42
58
|
if (!proxyTarget) {
|
|
43
|
-
|
|
59
|
+
diagnostics.push(
|
|
60
|
+
`\`icp network status -e ${networkName}\` reported no api_url, gateway_url or port`
|
|
61
|
+
)
|
|
62
|
+
return { environment: null, diagnostics }
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
const canisterIds: Record<string, string> = {}
|
|
@@ -49,35 +68,45 @@ export function getIcEnvironmentInfo(
|
|
|
49
68
|
try {
|
|
50
69
|
const canisterId = execFileSync(
|
|
51
70
|
"icp",
|
|
52
|
-
["canister", "status", name, "-e",
|
|
53
|
-
{ encoding: "utf-8", stdio: ["ignore", "pipe", "
|
|
71
|
+
["canister", "status", name, "-e", networkName, "-i"],
|
|
72
|
+
{ encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
|
|
54
73
|
).trim()
|
|
55
74
|
|
|
56
75
|
if (canisterId) {
|
|
57
76
|
canisterIds[name] = canisterId
|
|
58
77
|
}
|
|
59
|
-
} catch {
|
|
60
|
-
// Canister might not exist or be deployed yet
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Canister might not exist or be deployed yet — expected for
|
|
80
|
+
// internet_identity, which is added to the lookup list unconditionally.
|
|
81
|
+
diagnostics.push(
|
|
82
|
+
`\`icp canister status ${name} -e ${networkName}\` failed: ${describeExecError(error)}`
|
|
83
|
+
)
|
|
61
84
|
}
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
const internetIdentityProvider =
|
|
65
88
|
!canisterIds.internet_identity &&
|
|
66
89
|
isLocalhostGateway(proxyTarget) &&
|
|
67
|
-
|
|
90
|
+
networkName !== "ic"
|
|
68
91
|
? localInternetIdentityProvider(proxyTarget)
|
|
69
92
|
: undefined
|
|
70
93
|
|
|
71
94
|
return {
|
|
72
|
-
environment
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
environment: {
|
|
96
|
+
environment: networkName,
|
|
97
|
+
rootKey,
|
|
98
|
+
proxyTarget,
|
|
99
|
+
canisterIds,
|
|
100
|
+
internetIdentityProvider,
|
|
101
|
+
},
|
|
102
|
+
diagnostics,
|
|
77
103
|
}
|
|
78
104
|
} catch (error) {
|
|
79
105
|
// CLI not found or failed
|
|
80
|
-
|
|
106
|
+
diagnostics.push(
|
|
107
|
+
`\`icp network status -e ${networkName}\` failed: ${describeExecError(error)}`
|
|
108
|
+
)
|
|
109
|
+
return { environment: null, diagnostics }
|
|
81
110
|
}
|
|
82
111
|
}
|
|
83
112
|
|
|
@@ -103,6 +132,25 @@ export function buildIcEnvCookie(
|
|
|
103
132
|
return encodeURIComponent(parts.join("&"))
|
|
104
133
|
}
|
|
105
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Turn whatever `execFileSync` threw into one readable line.
|
|
137
|
+
*
|
|
138
|
+
* The interesting part is almost always the captured stderr — the thrown
|
|
139
|
+
* Error's own message is just "Command failed: icp ..." — but stderr is absent
|
|
140
|
+
* when the binary itself is missing (ENOENT), so fall back to the message.
|
|
141
|
+
*/
|
|
142
|
+
function describeExecError(error: unknown): string {
|
|
143
|
+
const stderr = (error as { stderr?: Buffer | string } | undefined)?.stderr
|
|
144
|
+
const text = typeof stderr === "string" ? stderr : stderr?.toString("utf-8")
|
|
145
|
+
const trimmed = text?.trim()
|
|
146
|
+
|
|
147
|
+
if (trimmed) {
|
|
148
|
+
return trimmed
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return error instanceof Error ? error.message : String(error)
|
|
152
|
+
}
|
|
153
|
+
|
|
106
154
|
function isLocalhostGateway(proxyTarget: string): boolean {
|
|
107
155
|
try {
|
|
108
156
|
const { hostname } = new URL(proxyTarget)
|