@ic-reactor/vite-plugin 0.12.1 → 0.13.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 +34 -4
- 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 +16 -2
- package/package.json +7 -6
- package/src/env.ts +64 -16
- package/src/index.ts +310 -59
- package/src/index.test.ts +0 -349
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. Hot-reloads when .did files change
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import type { Plugin } from "vite"
|
|
10
|
+
import type { Plugin, ResolvedConfig, ViteDevServer } from "vite"
|
|
11
11
|
import path from "node:path"
|
|
12
12
|
import {
|
|
13
13
|
runCanisterPipeline,
|
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
} from "@ic-reactor/codegen"
|
|
18
18
|
import { getIcEnvironmentInfo, buildIcEnvCookie } from "./env.js"
|
|
19
19
|
|
|
20
|
+
const PLUGIN_NAME = "ic-reactor-plugin"
|
|
21
|
+
const DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943"
|
|
22
|
+
|
|
20
23
|
export interface IcReactorPluginOptions {
|
|
21
24
|
/**
|
|
22
25
|
* Canister configurations.
|
|
@@ -24,7 +27,7 @@ export interface IcReactorPluginOptions {
|
|
|
24
27
|
*/
|
|
25
28
|
canisters: CanisterConfig[]
|
|
26
29
|
/**
|
|
27
|
-
* Default output directory (relative to project root).
|
|
30
|
+
* Default output directory (relative to the Vite project root).
|
|
28
31
|
* Default: "src/declarations"
|
|
29
32
|
*/
|
|
30
33
|
outDir?: string
|
|
@@ -43,15 +46,26 @@ export interface IcReactorPluginOptions {
|
|
|
43
46
|
* Default: true
|
|
44
47
|
*/
|
|
45
48
|
injectEnvironment?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Abort the Vite run when a canister fails to generate.
|
|
51
|
+
*
|
|
52
|
+
* Default: `true` under `vite build`, `false` under `vite dev`. A build that
|
|
53
|
+
* silently ships the bindings left over from the last successful run is worse
|
|
54
|
+
* than no build at all, while a dev server has to survive the broken
|
|
55
|
+
* intermediate states of a `.did` file being edited — there the failure is
|
|
56
|
+
* reported to the terminal and the browser error overlay instead.
|
|
57
|
+
*/
|
|
58
|
+
failOnError?: boolean
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
export function icReactor(options: IcReactorPluginOptions):
|
|
61
|
+
export function icReactor(options: IcReactorPluginOptions): Plugin {
|
|
49
62
|
const {
|
|
50
63
|
canisters,
|
|
51
64
|
outDir = "src/declarations",
|
|
52
65
|
clientManagerPath = "../../clients",
|
|
53
66
|
target = "react",
|
|
54
67
|
injectEnvironment = true,
|
|
68
|
+
failOnError,
|
|
55
69
|
} = options
|
|
56
70
|
|
|
57
71
|
// Construct a partial CodegenConfig to pass to the pipeline
|
|
@@ -63,7 +77,27 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
63
77
|
clientManagerPath,
|
|
64
78
|
target,
|
|
65
79
|
}
|
|
66
|
-
|
|
80
|
+
|
|
81
|
+
// Vite resolves relative project paths against the resolved `config.root`,
|
|
82
|
+
// which only equals the process cwd when vite happens to be started from the
|
|
83
|
+
// project directory — not for `root: "frontend"`, and not when the root is
|
|
84
|
+
// passed positionally (`vite build apps/web`). Note `--config` on its own does
|
|
85
|
+
// NOT move the root; it only selects the config file.
|
|
86
|
+
// `configResolved` overwrites this before any hook that resolves a path runs;
|
|
87
|
+
// the cwd is only the pre-resolution default, which is also Vite's own
|
|
88
|
+
// default root.
|
|
89
|
+
let projectRoot = process.cwd()
|
|
90
|
+
|
|
91
|
+
// `vite build` and `vite dev` want opposite failure behaviour, so remember
|
|
92
|
+
// which one we are in. Both `config` and `configResolved` carry the command;
|
|
93
|
+
// build is the safer default for the case where neither has run.
|
|
94
|
+
let command: ResolvedConfig["command"] = "build"
|
|
95
|
+
|
|
96
|
+
// Set once the dev server exists, so a `buildStart` failure in dev can reach
|
|
97
|
+
// the browser overlay too — in dev, `configureServer` runs before Vite calls
|
|
98
|
+
// `buildStart` on the plugin container.
|
|
99
|
+
let devServer: ViteDevServer | null = null
|
|
100
|
+
|
|
67
101
|
const resolveDidPath = (didFile: string) =>
|
|
68
102
|
path.normalize(
|
|
69
103
|
path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
|
|
@@ -74,12 +108,121 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
74
108
|
.map((canister) => [canister.name, canister.canisterId as string])
|
|
75
109
|
)
|
|
76
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Report a generation failure everywhere a developer might be looking.
|
|
113
|
+
*
|
|
114
|
+
* Terminal output scrolls away behind request logs and HMR chatter, so in dev
|
|
115
|
+
* the browser error overlay is the signal that actually gets noticed.
|
|
116
|
+
*/
|
|
117
|
+
/**
|
|
118
|
+
* The last failure, kept so a browser that was not connected when it happened
|
|
119
|
+
* still gets the overlay.
|
|
120
|
+
*
|
|
121
|
+
* Vite awaits the plugin container's `buildStart` before the HTTP server
|
|
122
|
+
* starts listening, so a generation failure during `vite dev` startup is
|
|
123
|
+
* broadcast when there are no WebSocket clients at all and the payload is
|
|
124
|
+
* simply dropped. The terminal shows it; the overlay never appears — for
|
|
125
|
+
* precisely the failures a developer is most likely to hit.
|
|
126
|
+
*/
|
|
127
|
+
let pendingFailure: { message: string; stack: string } | null = null
|
|
128
|
+
|
|
129
|
+
const reportFailure = (
|
|
130
|
+
server: ViteDevServer | null,
|
|
131
|
+
message: string,
|
|
132
|
+
cause?: unknown
|
|
133
|
+
) => {
|
|
134
|
+
console.error(`[ic-reactor] ${message}`)
|
|
135
|
+
|
|
136
|
+
const err = {
|
|
137
|
+
message: `[ic-reactor] ${message}`,
|
|
138
|
+
stack: cause instanceof Error && cause.stack ? cause.stack : "",
|
|
139
|
+
plugin: PLUGIN_NAME,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pendingFailure = { message: err.message, stack: err.stack }
|
|
143
|
+
server?.ws.send({ type: "error", err })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Keep at most one regeneration per canister in flight and collapse every save
|
|
147
|
+
// that arrives meanwhile into a single trailing rerun, so the last saved
|
|
148
|
+
// `.did` still wins without piling up concurrent runs.
|
|
149
|
+
//
|
|
150
|
+
// Scope, precisely: this covers the watcher path only. `buildStart` calls the
|
|
151
|
+
// pipeline directly and does not register here, so a save landing during the
|
|
152
|
+
// initial generation can still run concurrently with it. That is deliberate
|
|
153
|
+
// rather than an oversight -- since @ic-reactor/codegen generates into a
|
|
154
|
+
// staging directory and swaps atomically, concurrent runs for one canister no
|
|
155
|
+
// longer interleave inside a delete-then-write sequence; the loser is simply
|
|
156
|
+
// overwritten. What this buys is ordering and wasted work, not integrity.
|
|
157
|
+
//
|
|
158
|
+
// Note the coalesced promise resolves when the RUNNING pass finishes, not the
|
|
159
|
+
// trailing rerun, so `handleHotUpdate` can return before the newest `.did` has
|
|
160
|
+
// been written. The trailing run sends its own full-reload, so the browser
|
|
161
|
+
// still converges.
|
|
162
|
+
const inFlight = new Map<string, Promise<void>>()
|
|
163
|
+
const rerunQueued = new Set<string>()
|
|
164
|
+
|
|
165
|
+
const regenerate = (
|
|
166
|
+
canisterConfig: CanisterConfig,
|
|
167
|
+
server: ViteDevServer
|
|
168
|
+
): Promise<void> => {
|
|
169
|
+
const { name } = canisterConfig
|
|
170
|
+
const running = inFlight.get(name)
|
|
171
|
+
|
|
172
|
+
if (running) {
|
|
173
|
+
rerunQueued.add(name)
|
|
174
|
+
return running
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const run = runCanisterPipeline({
|
|
178
|
+
canisterConfig,
|
|
179
|
+
projectRoot,
|
|
180
|
+
globalConfig,
|
|
181
|
+
})
|
|
182
|
+
.then((result) => {
|
|
183
|
+
if (result.success) {
|
|
184
|
+
// A later connection must not be handed a failure that has since been
|
|
185
|
+
// fixed.
|
|
186
|
+
pendingFailure = null
|
|
187
|
+
// Reload page to reflect new types/hooks
|
|
188
|
+
server.ws.send({ type: "full-reload" })
|
|
189
|
+
} else {
|
|
190
|
+
reportFailure(
|
|
191
|
+
server,
|
|
192
|
+
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
// A throw from the pipeline (a malformed `.did` makes the parser throw
|
|
197
|
+
// rather than return a failed result) would otherwise be an unhandled
|
|
198
|
+
// rejection: invisible in the browser and, depending on the Node version,
|
|
199
|
+
// fatal to the dev server.
|
|
200
|
+
.catch((error: unknown) => {
|
|
201
|
+
reportFailure(
|
|
202
|
+
server,
|
|
203
|
+
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
204
|
+
error
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
.finally(() => {
|
|
208
|
+
inFlight.delete(name)
|
|
209
|
+
if (rerunQueued.delete(name)) {
|
|
210
|
+
void regenerate(canisterConfig, server)
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
inFlight.set(name, run)
|
|
215
|
+
return run
|
|
216
|
+
}
|
|
217
|
+
|
|
77
218
|
const plugin: Plugin = {
|
|
78
|
-
name:
|
|
219
|
+
name: PLUGIN_NAME,
|
|
79
220
|
enforce: "pre", // Run before other plugins
|
|
80
221
|
|
|
81
|
-
config(_config, { command }) {
|
|
82
|
-
|
|
222
|
+
config(_config, { command: viteCommand }) {
|
|
223
|
+
command = viteCommand
|
|
224
|
+
|
|
225
|
+
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
83
226
|
return {}
|
|
84
227
|
}
|
|
85
228
|
|
|
@@ -93,9 +236,26 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
93
236
|
canisterNames.push("internet_identity")
|
|
94
237
|
}
|
|
95
238
|
|
|
96
|
-
const icEnv =
|
|
239
|
+
const { environment: icEnv, diagnostics } =
|
|
240
|
+
getIcEnvironmentInfo(canisterNames)
|
|
97
241
|
|
|
98
242
|
if (!icEnv) {
|
|
243
|
+
// Failing detection used to be indistinguishable from success: no
|
|
244
|
+
// cookie was set, no warning was printed, and the app only broke much
|
|
245
|
+
// later on an undefined canister id. Stay quiet in env-only mode
|
|
246
|
+
// (no canisters configured), where there is nothing to inject anyway.
|
|
247
|
+
if (canisters.length > 0) {
|
|
248
|
+
console.warn(
|
|
249
|
+
`[ic-reactor] Could not detect the local IC environment, falling back to ${DEFAULT_LOCAL_REPLICA}. ` +
|
|
250
|
+
`Canister IDs and the root key will not be injected — is the local replica running? ` +
|
|
251
|
+
`Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (const diagnostic of diagnostics) {
|
|
256
|
+
debugLog(diagnostic)
|
|
257
|
+
}
|
|
258
|
+
|
|
99
259
|
const envOnlyCookie =
|
|
100
260
|
canisters.length === 0
|
|
101
261
|
? buildIcEnvCookie(
|
|
@@ -116,7 +276,7 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
116
276
|
: undefined,
|
|
117
277
|
proxy: {
|
|
118
278
|
"/api": {
|
|
119
|
-
target:
|
|
279
|
+
target: DEFAULT_LOCAL_REPLICA,
|
|
120
280
|
changeOrigin: true,
|
|
121
281
|
},
|
|
122
282
|
},
|
|
@@ -124,6 +284,42 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
124
284
|
}
|
|
125
285
|
}
|
|
126
286
|
|
|
287
|
+
// The replica can be UP -- `icp network status` succeeds, so icEnv is
|
|
288
|
+
// truthy and the check above never fires -- while a configured canister
|
|
289
|
+
// has never been deployed. Every `icp canister status <name>` then fails
|
|
290
|
+
// and that id is simply absent, so the cookie goes out carrying a root key
|
|
291
|
+
// and no PUBLIC_CANISTER_ID for it. That is the same "indistinguishable
|
|
292
|
+
// from success until the app breaks on an undefined canister id" failure
|
|
293
|
+
// the branch above exists to prevent, and it is the more common one.
|
|
294
|
+
//
|
|
295
|
+
// Only configured canisters are reported: `internet_identity` is appended
|
|
296
|
+
// to canisterNames for convenience and is routinely not deployed.
|
|
297
|
+
// An explicitly configured `canisterId` counts as resolved: the cookie
|
|
298
|
+
// below merges configuredCanisterIds over the detected ones, so the app
|
|
299
|
+
// does receive a valid PUBLIC_CANISTER_ID. Warning on those told the user
|
|
300
|
+
// to deploy a canister whose id they had already supplied.
|
|
301
|
+
const missingCanisterIds = canisters
|
|
302
|
+
.map((canister) => canister.name)
|
|
303
|
+
.filter((name): name is string => !!name)
|
|
304
|
+
.filter(
|
|
305
|
+
(name) => !icEnv.canisterIds[name] && !configuredCanisterIds[name]
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if (missingCanisterIds.length > 0) {
|
|
309
|
+
const names = missingCanisterIds.map((name) => `"${name}"`).join(", ")
|
|
310
|
+
const it = missingCanisterIds.length === 1 ? "it" : "them"
|
|
311
|
+
console.warn(
|
|
312
|
+
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${names}. ` +
|
|
313
|
+
`Deploy ${it} (\`icp deploy\`) — until then the injected ic_env carries no PUBLIC_CANISTER_ID ` +
|
|
314
|
+
`for ${it} and the app will see an undefined canister id. ` +
|
|
315
|
+
`Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
316
|
+
)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const diagnostic of diagnostics) {
|
|
320
|
+
debugLog(diagnostic)
|
|
321
|
+
}
|
|
322
|
+
|
|
127
323
|
const cookieValue = buildIcEnvCookie(
|
|
128
324
|
{
|
|
129
325
|
...icEnv.canisterIds,
|
|
@@ -148,7 +344,30 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
148
344
|
}
|
|
149
345
|
},
|
|
150
346
|
|
|
347
|
+
configResolved(config) {
|
|
348
|
+
// Everything the plugin resolves — `didFile`, `outDir`,
|
|
349
|
+
// `clientManagerPath` — is documented as relative to the project root, so
|
|
350
|
+
// it has to be Vite's resolved root and not wherever the process started.
|
|
351
|
+
projectRoot = config.root
|
|
352
|
+
command = config.command
|
|
353
|
+
},
|
|
354
|
+
|
|
151
355
|
configureServer(server) {
|
|
356
|
+
devServer = server
|
|
357
|
+
|
|
358
|
+
// Replay a startup failure to the first client that connects — see
|
|
359
|
+
// pendingFailure. Cleared once generation succeeds.
|
|
360
|
+
// Guarded: the peer range spans several Vite majors and `ws.on` is not
|
|
361
|
+
// present on every one of them. Losing the replay is acceptable; throwing
|
|
362
|
+
// out of configureServer is not.
|
|
363
|
+
server.ws.on?.("connection", () => {
|
|
364
|
+
if (!pendingFailure) return
|
|
365
|
+
server.ws.send({
|
|
366
|
+
type: "error",
|
|
367
|
+
err: { ...pendingFailure, plugin: PLUGIN_NAME },
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
152
371
|
// Explicitly watch configured DID files so HMR works even when they are not in the module graph.
|
|
153
372
|
const didFiles = canisters.map((c) => resolveDidPath(c.didFile))
|
|
154
373
|
server.watcher.add(didFiles)
|
|
@@ -161,66 +380,98 @@ export function icReactor(options: IcReactorPluginOptions): any {
|
|
|
161
380
|
`[ic-reactor] Generating canister bindings for ${canisters.length} canisters...`
|
|
162
381
|
)
|
|
163
382
|
|
|
164
|
-
await Promise.allSettled(
|
|
165
|
-
canisters.map(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
canisterConfig,
|
|
173
|
-
projectRoot,
|
|
174
|
-
globalConfig,
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
if (!result.success) {
|
|
178
|
-
console.error(
|
|
179
|
-
`[ic-reactor] Failed to generate ${canisterConfig.name}: ${result.error}`
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
} catch (err) {
|
|
183
|
-
console.error(
|
|
184
|
-
`[ic-reactor] Error generating ${canisterConfig.name}:`,
|
|
185
|
-
err
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
})
|
|
383
|
+
const outcomes = await Promise.allSettled(
|
|
384
|
+
canisters.map((canisterConfig) =>
|
|
385
|
+
runCanisterPipeline({
|
|
386
|
+
canisterConfig,
|
|
387
|
+
projectRoot,
|
|
388
|
+
globalConfig,
|
|
389
|
+
})
|
|
390
|
+
)
|
|
189
391
|
)
|
|
392
|
+
|
|
393
|
+
// Collect every failure before reporting one: a canister failing must not
|
|
394
|
+
// hide what the others did, and the error should name all of them so a CI
|
|
395
|
+
// log shows the whole picture in one go.
|
|
396
|
+
const failures = outcomes.flatMap((outcome, index) => {
|
|
397
|
+
const name = canisters[index]?.name ?? `canister #${index}`
|
|
398
|
+
|
|
399
|
+
if (outcome.status === "rejected") {
|
|
400
|
+
return [`${name}: ${describeError(outcome.reason)}`]
|
|
401
|
+
}
|
|
402
|
+
if (!outcome.value.success) {
|
|
403
|
+
return [`${name}: ${outcome.value.error ?? "unknown error"}`]
|
|
404
|
+
}
|
|
405
|
+
return []
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
if (failures.length === 0) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const message =
|
|
413
|
+
`Failed to generate ${failures.length} of ${canisters.length} canisters:\n` +
|
|
414
|
+
failures.map((failure) => ` - ${failure}`).join("\n")
|
|
415
|
+
|
|
416
|
+
// Previously every failure here was a `console.error` and nothing more,
|
|
417
|
+
// so `vite build` exited 0 and CI shipped whatever stale bindings were
|
|
418
|
+
// still on disk — bindings that no longer match the deployed canister.
|
|
419
|
+
if (failOnError ?? command === "build") {
|
|
420
|
+
this.error(`[ic-reactor] ${message}`)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
reportFailure(devServer, message)
|
|
190
424
|
},
|
|
191
425
|
|
|
192
426
|
handleHotUpdate({ file, server }) {
|
|
193
427
|
// ── Hot Reload on .did changes ───────────────────────────────────────
|
|
194
|
-
if (file.endsWith(".did")) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Cast is safe because didFile is required in CanisterConfig
|
|
198
|
-
const configPath = resolveDidPath(c.didFile)
|
|
199
|
-
return configPath === path.normalize(file)
|
|
200
|
-
})
|
|
428
|
+
if (!file.endsWith(".did")) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
201
431
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
432
|
+
const changedPath = path.normalize(file)
|
|
433
|
+
const affectedCanister = canisters.find(
|
|
434
|
+
// Check if changed file matches configured didFile
|
|
435
|
+
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
436
|
+
)
|
|
206
437
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
canisterConfig: affectedCanister,
|
|
210
|
-
projectRoot,
|
|
211
|
-
globalConfig,
|
|
212
|
-
}).then((result: import("@ic-reactor/codegen").PipelineResult) => {
|
|
213
|
-
if (result.success) {
|
|
214
|
-
// Reload page to reflect new types/hooks
|
|
215
|
-
server.ws.send({ type: "full-reload" })
|
|
216
|
-
} else {
|
|
217
|
-
console.error(`[ic-reactor] Regeneration failed: ${result.error}`)
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
}
|
|
438
|
+
if (!affectedCanister) {
|
|
439
|
+
return
|
|
221
440
|
}
|
|
441
|
+
|
|
442
|
+
console.log(
|
|
443
|
+
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
// Returned so Vite waits for the write to finish before applying the
|
|
447
|
+
// update; it resolves to `undefined`, which leaves the affected module
|
|
448
|
+
// list untouched.
|
|
449
|
+
return regenerate(affectedCanister, server)
|
|
222
450
|
},
|
|
223
451
|
}
|
|
224
452
|
|
|
225
453
|
return plugin
|
|
226
454
|
}
|
|
455
|
+
|
|
456
|
+
/** One readable line for whatever the pipeline threw. */
|
|
457
|
+
function describeError(error: unknown): string {
|
|
458
|
+
return error instanceof Error ? error.message : String(error)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* `icp` failing to answer is routine — no local replica, or a canister that has
|
|
463
|
+
* never been deployed — so its stderr is noise until someone is actually
|
|
464
|
+
* debugging. Gate it the way Vite gates its own debug output.
|
|
465
|
+
*/
|
|
466
|
+
function debugLog(message: string): void {
|
|
467
|
+
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|
|
468
|
+
const scope = entry.trim()
|
|
469
|
+
return (
|
|
470
|
+
scope === "*" || scope === "ic-reactor" || scope.startsWith("ic-reactor:")
|
|
471
|
+
)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
if (enabled) {
|
|
475
|
+
console.debug(`[ic-reactor:debug] ${message}`)
|
|
476
|
+
}
|
|
477
|
+
}
|