@ic-reactor/vite-plugin 0.14.0 → 0.15.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/dist/index.js CHANGED
@@ -1,24 +1,46 @@
1
1
  // src/index.ts
2
+ import fs from "fs";
2
3
  import path from "path";
3
4
  import {
4
- runCanisterPipeline
5
+ findSharedOutDirs,
6
+ runCanisterPipeline,
7
+ sharedOutDirMessage
5
8
  } from "@ic-reactor/codegen";
6
9
 
7
10
  // src/env.ts
8
- import { execFileSync } from "child_process";
9
- function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
11
+ import { execFile } from "child_process";
12
+ var ICP_TIMEOUT_MS = 1e4;
13
+ function runIcp(args, cwd) {
14
+ return new Promise((resolve, reject) => {
15
+ const child = execFile(
16
+ "icp",
17
+ args,
18
+ {
19
+ cwd,
20
+ encoding: "utf-8",
21
+ timeout: ICP_TIMEOUT_MS,
22
+ windowsHide: true
23
+ },
24
+ (error, stdout, stderr) => {
25
+ if (error) {
26
+ reject(Object.assign(error, { stderr }));
27
+ } else {
28
+ resolve(stdout);
29
+ }
30
+ }
31
+ );
32
+ child.stdin?.end();
33
+ });
34
+ }
35
+ async function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
10
36
  const networkName = process.env.ICP_ENVIRONMENT || "local";
11
37
  const diagnostics = [];
12
38
  try {
13
39
  const networkStatus = JSON.parse(
14
- execFileSync("icp", ["network", "status", "-e", networkName, "--json"], {
15
- cwd: projectRoot,
16
- encoding: "utf-8",
17
- // stderr is piped rather than ignored so a failure can explain itself.
18
- // Piping still keeps it off the terminal — it only reaches the user if
19
- // the caller decides to print the diagnostics we collect below.
20
- stdio: ["ignore", "pipe", "pipe"]
21
- })
40
+ await runIcp(
41
+ ["network", "status", "-e", networkName, "--json"],
42
+ projectRoot
43
+ )
22
44
  );
23
45
  const rootKey = networkStatus.root_key;
24
46
  const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
@@ -31,15 +53,10 @@ function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
31
53
  const canisterIds = {};
32
54
  for (const name of canisterNames) {
33
55
  try {
34
- const canisterId = execFileSync(
35
- "icp",
56
+ const canisterId = (await runIcp(
36
57
  ["canister", "status", name, "-e", networkName, "-i"],
37
- {
38
- cwd: projectRoot,
39
- encoding: "utf-8",
40
- stdio: ["ignore", "pipe", "pipe"]
41
- }
42
- ).trim();
58
+ projectRoot
59
+ )).trim();
43
60
  if (canisterId) {
44
61
  canisterIds[name] = canisterId;
45
62
  }
@@ -100,9 +117,129 @@ function localInternetIdentityProvider(proxyTarget) {
100
117
  return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
101
118
  }
102
119
 
120
+ // src/dev-environment.ts
121
+ var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
122
+ var DEFAULT_INTERNET_IDENTITY_PROVIDER = "http://id.ai.localhost:8000/authorize";
123
+ function createLocalEnvironment(options) {
124
+ const {
125
+ canisterNames,
126
+ configuredCanisterIds,
127
+ projectRoot,
128
+ onDiagnostic,
129
+ onUpdate
130
+ } = options;
131
+ const requiredNames = [...new Set(canisterNames)];
132
+ const lookupNames = requiredNames.includes("internet_identity") ? requiredNames : [...requiredNames, "internet_identity"];
133
+ let state;
134
+ let running;
135
+ const toState = (environment) => {
136
+ const canisterIds = {
137
+ ...environment?.canisterIds,
138
+ ...configuredCanisterIds
139
+ };
140
+ const missingCanisterIds = requiredNames.filter(
141
+ (name) => !canisterIds[name]
142
+ );
143
+ let cookie;
144
+ if (environment) {
145
+ cookie = buildIcEnvCookie(
146
+ canisterIds,
147
+ environment.rootKey,
148
+ environment.internetIdentityProvider
149
+ );
150
+ } else if (requiredNames.length === 0) {
151
+ cookie = buildIcEnvCookie(
152
+ {},
153
+ void 0,
154
+ DEFAULT_INTERNET_IDENTITY_PROVIDER
155
+ );
156
+ }
157
+ return {
158
+ environment,
159
+ cookie,
160
+ proxyTarget: environment?.proxyTarget ?? DEFAULT_LOCAL_REPLICA,
161
+ missingCanisterIds,
162
+ complete: environment !== null && missingCanisterIds.length === 0
163
+ };
164
+ };
165
+ const run = async () => {
166
+ const previous = state;
167
+ try {
168
+ const { environment, diagnostics } = await getIcEnvironmentInfo(
169
+ lookupNames,
170
+ projectRoot
171
+ );
172
+ diagnostics.forEach(onDiagnostic);
173
+ state = toState(
174
+ mergeDetections(previous?.environment ?? null, environment)
175
+ );
176
+ } catch (error) {
177
+ onDiagnostic(
178
+ `Detecting the local IC environment failed: ${error instanceof Error ? error.message : String(error)}`
179
+ );
180
+ state = previous ?? toState(null);
181
+ }
182
+ try {
183
+ onUpdate?.(previous, state);
184
+ } catch (error) {
185
+ onDiagnostic(
186
+ `Applying the detected IC environment failed: ${error instanceof Error ? error.message : String(error)}`
187
+ );
188
+ }
189
+ return state;
190
+ };
191
+ return {
192
+ get state() {
193
+ return state;
194
+ },
195
+ detect() {
196
+ running ?? (running = run().finally(() => {
197
+ running = void 0;
198
+ }));
199
+ return running;
200
+ }
201
+ };
202
+ }
203
+ function mergeDetections(previous, next) {
204
+ if (!next) return previous;
205
+ if (!previous || previous.rootKey !== next.rootKey) return next;
206
+ const canisterIds = { ...previous.canisterIds, ...next.canisterIds };
207
+ return {
208
+ ...next,
209
+ canisterIds,
210
+ // The built-in provider stands in only for a project that has no
211
+ // internet_identity canister, as getIcEnvironmentInfo decides it.
212
+ internetIdentityProvider: canisterIds.internet_identity ? void 0 : next.internetIdentityProvider
213
+ };
214
+ }
215
+ function isDocumentRequest(req) {
216
+ if (req.method !== "GET" && req.method !== "HEAD") return false;
217
+ const { accept } = req.headers;
218
+ return typeof accept === "string" && accept.includes("text/html");
219
+ }
220
+ function setIcEnvCookie(res, value) {
221
+ const existing = res.getHeader("Set-Cookie");
222
+ const others = (existing === void 0 ? [] : Array.isArray(existing) ? existing : [String(existing)]).filter((cookie2) => !cookie2.startsWith("ic_env="));
223
+ const cookie = `ic_env=${value}; Path=/; SameSite=Lax;`;
224
+ res.setHeader("Set-Cookie", others.length > 0 ? [...others, cookie] : cookie);
225
+ }
226
+ function icEnvMiddleware(environment) {
227
+ return (req, res, next) => {
228
+ const respond = (state2) => {
229
+ if (state2?.cookie !== void 0) setIcEnvCookie(res, state2.cookie);
230
+ next();
231
+ };
232
+ const { state } = environment;
233
+ if (state?.complete || !isDocumentRequest(req)) {
234
+ respond(state);
235
+ return;
236
+ }
237
+ void environment.detect().then(respond);
238
+ };
239
+ }
240
+
103
241
  // src/index.ts
104
242
  var PLUGIN_NAME = "ic-reactor-plugin";
105
- var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
106
243
  function icReactor(options) {
107
244
  const {
108
245
  canisters,
@@ -119,10 +256,29 @@ function icReactor(options) {
119
256
  };
120
257
  let projectRoot = process.cwd();
121
258
  let command = "build";
259
+ let localEnvironment;
260
+ const apiProxyOptions = /* @__PURE__ */ new Set();
122
261
  let devServer = null;
123
262
  const resolveDidPath = (didFile) => path.normalize(
124
263
  path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
125
264
  );
265
+ const generatedFrom = /* @__PURE__ */ new Map();
266
+ const readDidSource = (canister) => {
267
+ try {
268
+ return fs.readFileSync(resolveDidPath(canister.didFile), "utf-8");
269
+ } catch {
270
+ return void 0;
271
+ }
272
+ };
273
+ const describeEntry = (canister) => `canisters[${canisters.indexOf(canister)}] (${JSON.stringify(canister.name)})`;
274
+ const sharedOutDirError = (canister) => {
275
+ const first = findSharedOutDirs(
276
+ canisters.map((entry) => [entry, entry]),
277
+ outDir,
278
+ projectRoot
279
+ ).get(canister);
280
+ return first === void 0 ? void 0 : sharedOutDirMessage(describeEntry(canister), describeEntry(first));
281
+ };
126
282
  const configuredCanisterIds = Object.fromEntries(
127
283
  canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
128
284
  );
@@ -141,22 +297,31 @@ function icReactor(options) {
141
297
  const rerunQueued = /* @__PURE__ */ new Set();
142
298
  const regenerate = (canisterConfig, server) => {
143
299
  const { name } = canisterConfig;
144
- const running = inFlight.get(name);
300
+ const running = inFlight.get(canisterConfig);
145
301
  if (running) {
146
- rerunQueued.add(name);
302
+ rerunQueued.add(canisterConfig);
147
303
  return running;
148
304
  }
305
+ const sharedError = sharedOutDirError(canisterConfig);
306
+ if (sharedError !== void 0) {
307
+ pendingFailures.set(
308
+ canisterConfig,
309
+ reportFailure(server, `Regeneration failed for ${sharedError}`)
310
+ );
311
+ return Promise.resolve();
312
+ }
149
313
  const run = runCanisterPipeline({
150
314
  canisterConfig,
151
315
  projectRoot,
152
316
  globalConfig
153
317
  }).then((result) => {
318
+ reportWarnings(name, result.warnings);
154
319
  if (result.success) {
155
- pendingFailures.delete(name);
320
+ pendingFailures.delete(canisterConfig);
156
321
  server.ws.send({ type: "full-reload" });
157
322
  } else {
158
323
  pendingFailures.set(
159
- name,
324
+ canisterConfig,
160
325
  reportFailure(
161
326
  server,
162
327
  `Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
@@ -165,7 +330,7 @@ function icReactor(options) {
165
330
  }
166
331
  }).catch((error) => {
167
332
  pendingFailures.set(
168
- name,
333
+ canisterConfig,
169
334
  reportFailure(
170
335
  server,
171
336
  `Regeneration failed for ${name}: ${describeError(error)}`,
@@ -173,81 +338,77 @@ function icReactor(options) {
173
338
  )
174
339
  );
175
340
  }).finally(() => {
176
- inFlight.delete(name);
177
- if (rerunQueued.delete(name)) {
341
+ inFlight.delete(canisterConfig);
342
+ if (rerunQueued.delete(canisterConfig)) {
178
343
  void regenerate(canisterConfig, server);
179
344
  }
180
345
  });
181
- inFlight.set(name, run);
346
+ inFlight.set(canisterConfig, run);
182
347
  return run;
183
348
  };
349
+ const regenerateForDid = (file, server) => {
350
+ if (!file.endsWith(".did")) {
351
+ return;
352
+ }
353
+ const changedPath = path.normalize(file);
354
+ const affectedCanisters = canisters.filter(
355
+ (canister) => resolveDidPath(canister.didFile) === changedPath
356
+ );
357
+ if (affectedCanisters.length === 0) {
358
+ return;
359
+ }
360
+ console.log(
361
+ `[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
362
+ );
363
+ for (const canister of affectedCanisters) {
364
+ void regenerate(canister, server);
365
+ }
366
+ };
184
367
  const plugin = {
185
368
  name: PLUGIN_NAME,
186
369
  enforce: "pre",
187
370
  // Run before other plugins
188
- config(userConfig, { command: viteCommand }) {
371
+ async config(userConfig, { command: viteCommand }) {
189
372
  command = viteCommand;
190
373
  if (viteCommand !== "serve" || !injectEnvironment) {
191
374
  return {};
192
375
  }
193
- const canisterNames = canisters.map((c) => c.name).filter((n) => !!n);
194
- if (!canisterNames.includes("internet_identity")) {
195
- canisterNames.push("internet_identity");
196
- }
197
- const { environment: icEnv, diagnostics } = getIcEnvironmentInfo(
198
- canisterNames,
199
- path.resolve(userConfig.root ?? process.cwd())
200
- );
201
- if (!icEnv) {
202
- if (canisters.length > 0) {
203
- console.warn(
204
- `[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.`
205
- );
206
- }
207
- for (const diagnostic of diagnostics) {
208
- debugLog(diagnostic);
209
- }
210
- const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
211
- {},
212
- void 0,
213
- "http://id.ai.localhost:8000/authorize"
214
- ) : void 0;
215
- return {
216
- server: {
217
- headers: envOnlyCookie ? {
218
- "Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
219
- } : void 0,
220
- proxy: apiProxy(userConfig, DEFAULT_LOCAL_REPLICA)
376
+ const ownsApiProxy = !userConfig.server?.proxy?.["/api"];
377
+ const environment = createLocalEnvironment({
378
+ canisterNames: canisters.map((canister) => canister.name).filter((name) => !!name),
379
+ configuredCanisterIds,
380
+ // `configResolved` has not run yet, so resolve the root the way Vite
381
+ // will. icp finds the project from the directory it starts in, and
382
+ // with `vite apps/web` or a `root` option that is not the process cwd.
383
+ projectRoot: path.resolve(userConfig.root ?? process.cwd()),
384
+ onDiagnostic: debugLog,
385
+ onUpdate: (previous, next) => {
386
+ for (const proxyOptions of apiProxyOptions) {
387
+ proxyOptions.target = next.proxyTarget;
221
388
  }
222
- };
223
- }
224
- const missingCanisterIds = canisters.map((canister) => canister.name).filter((name) => !!name).filter(
225
- (name) => !icEnv.canisterIds[name] && !configuredCanisterIds[name]
226
- );
227
- if (missingCanisterIds.length > 0) {
228
- const names = missingCanisterIds.map((name) => `"${name}"`).join(", ");
229
- const it = missingCanisterIds.length === 1 ? "it" : "them";
230
- console.warn(
231
- `[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.`
232
- );
233
- }
234
- for (const diagnostic of diagnostics) {
235
- debugLog(diagnostic);
236
- }
237
- const cookieValue = buildIcEnvCookie(
238
- {
239
- ...icEnv.canisterIds,
240
- ...configuredCanisterIds
241
- },
242
- icEnv.rootKey,
243
- icEnv.internetIdentityProvider
244
- );
389
+ if (previous) {
390
+ reportDetectionProgress(previous, next, apiProxyOptions.size > 0);
391
+ }
392
+ }
393
+ });
394
+ localEnvironment = environment;
395
+ const state = await environment.detect();
396
+ warnAboutIncompleteDetection(state, canisters.length > 0, ownsApiProxy);
245
397
  return {
246
398
  server: {
247
- headers: {
248
- "Set-Cookie": `ic_env=${cookieValue}; Path=/; SameSite=Lax;`
249
- },
250
- proxy: apiProxy(userConfig, icEnv.proxyTarget)
399
+ // The cookie is not a static `server.headers` entry: the middleware
400
+ // that configureServer adds sets it per response, from the latest
401
+ // detection. See dev-environment.ts.
402
+ proxy: apiProxy(userConfig, state.proxyTarget, (proxyOptions) => {
403
+ if (proxyOptions.target !== state.proxyTarget) {
404
+ debugLog(
405
+ "Another plugin changed the target of the /api proxy, so the plugin leaves that proxy alone."
406
+ );
407
+ return;
408
+ }
409
+ apiProxyOptions.add(proxyOptions);
410
+ proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
411
+ })
251
412
  }
252
413
  };
253
414
  },
@@ -257,6 +418,9 @@ function icReactor(options) {
257
418
  },
258
419
  configureServer(server) {
259
420
  devServer = server;
421
+ if (localEnvironment) {
422
+ server.middlewares.use(icEnvMiddleware(localEnvironment));
423
+ }
260
424
  server.ws.on?.("connection", () => {
261
425
  if (pendingFailures.size === 0) return;
262
426
  const failures = [...pendingFailures.values()];
@@ -271,29 +435,69 @@ function icReactor(options) {
271
435
  });
272
436
  const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
273
437
  server.watcher.add(didFiles);
438
+ const onDidEvent = (file) => regenerateForDid(file, server);
439
+ server.watcher.on("change", onDidEvent);
440
+ server.watcher.on("add", onDidEvent);
441
+ },
442
+ // `vite preview` resolves the config with the `serve` command too, and
443
+ // used to inherit the cookie from `server.headers`.
444
+ configurePreviewServer(server) {
445
+ if (localEnvironment) {
446
+ server.middlewares.use(icEnvMiddleware(localEnvironment));
447
+ }
274
448
  },
275
449
  async buildStart() {
450
+ for (const canister of canisters) {
451
+ if (typeof canister.didFile === "string") {
452
+ this.addWatchFile(resolveDidPath(canister.didFile));
453
+ }
454
+ }
455
+ const sources = canisters.map(readDidSource);
456
+ const pending = canisters.filter(
457
+ (canister, index) => !this.meta.watchMode || sources[index] === void 0 || generatedFrom.get(canister) !== sources[index]
458
+ );
459
+ if (pending.length === 0) {
460
+ return;
461
+ }
276
462
  console.log(
277
- `[ic-reactor] Generating canister bindings for ${canisters.length} canisters...`
463
+ `[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
278
464
  );
279
465
  const outcomes = await Promise.allSettled(
280
- canisters.map(
281
- (canisterConfig) => runCanisterPipeline({
466
+ pending.map((canisterConfig) => {
467
+ const sharedError = sharedOutDirError(canisterConfig);
468
+ if (sharedError !== void 0) {
469
+ return Promise.reject(new SharedOutDirError(sharedError));
470
+ }
471
+ return runCanisterPipeline({
282
472
  canisterConfig,
283
473
  projectRoot,
284
474
  globalConfig
285
- })
286
- )
475
+ });
476
+ })
287
477
  );
478
+ outcomes.forEach((outcome, index) => {
479
+ const canister = pending[index];
480
+ const source = sources[canisters.indexOf(canister)];
481
+ if (outcome.status === "fulfilled") {
482
+ reportWarnings(canister.name, outcome.value.warnings);
483
+ }
484
+ if (outcome.status === "fulfilled" && outcome.value.success && source !== void 0) {
485
+ generatedFrom.set(canister, source);
486
+ } else {
487
+ generatedFrom.delete(canister);
488
+ }
489
+ });
288
490
  const failures = outcomes.flatMap((outcome, index) => {
289
- const name = canisters[index]?.name ?? `canister #${index}`;
491
+ const canister = pending[index];
492
+ const name = canister?.name ?? `canister #${index}`;
290
493
  if (outcome.status === "rejected") {
291
- return [{ name, detail: `${name}: ${describeError(outcome.reason)}` }];
494
+ const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
495
+ return [{ canister, detail }];
292
496
  }
293
497
  if (!outcome.value.success) {
294
498
  return [
295
499
  {
296
- name,
500
+ canister,
297
501
  detail: `${name}: ${outcome.value.error ?? "unknown error"}`
298
502
  }
299
503
  ];
@@ -303,51 +507,86 @@ function icReactor(options) {
303
507
  if (failures.length === 0) {
304
508
  return;
305
509
  }
306
- const message = `Failed to generate ${failures.length} of ${canisters.length} canisters:
510
+ const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
307
511
  ` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
308
512
  if (failOnError ?? command === "build") {
309
513
  this.error(`[ic-reactor] ${message}`);
310
514
  }
311
515
  reportFailure(devServer, message);
312
- for (const { name, detail } of failures) {
313
- pendingFailures.set(name, {
516
+ for (const { canister, detail } of failures) {
517
+ pendingFailures.set(canister, {
314
518
  message: `[ic-reactor] Failed to generate ${detail}`,
315
519
  stack: ""
316
520
  });
317
521
  }
318
- },
319
- handleHotUpdate({ file, server }) {
320
- if (!file.endsWith(".did")) {
321
- return;
322
- }
323
- const changedPath = path.normalize(file);
324
- const affectedCanisters = canisters.filter(
325
- (canister) => resolveDidPath(canister.didFile) === changedPath
326
- );
327
- if (affectedCanisters.length === 0) {
328
- return;
329
- }
330
- console.log(
331
- `[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
332
- );
333
- return Promise.all(
334
- affectedCanisters.map((canister) => regenerate(canister, server))
335
- ).then(() => void 0);
336
522
  }
337
523
  };
338
524
  return plugin;
339
525
  }
526
+ var SharedOutDirError = class extends Error {
527
+ };
528
+ function reportWarnings(name, warnings) {
529
+ for (const warning of warnings ?? []) {
530
+ console.warn(`[ic-reactor] ${name}: ${warning}`);
531
+ }
532
+ }
340
533
  function describeError(error) {
341
534
  return error instanceof Error ? error.message : String(error);
342
535
  }
343
- function apiProxy(userConfig, target) {
536
+ function apiProxy(userConfig, target, configure) {
344
537
  if (userConfig.server?.proxy?.["/api"]) {
345
538
  debugLog(
346
539
  `The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
347
540
  );
348
541
  return void 0;
349
542
  }
350
- return { "/api": { target, changeOrigin: true } };
543
+ return {
544
+ "/api": {
545
+ target,
546
+ changeOrigin: true,
547
+ configure: (_proxy, options) => configure(options)
548
+ }
549
+ };
550
+ }
551
+ function quoteNames(names) {
552
+ return names.map((name) => `"${name}"`).join(", ");
553
+ }
554
+ function warnAboutIncompleteDetection(state, hasCanisters, ownsApiProxy) {
555
+ if (!state.environment) {
556
+ if (!hasCanisters) return;
557
+ const proxyNote = ownsApiProxy ? ` and /api goes to ${state.proxyTarget} for now` : "";
558
+ console.warn(
559
+ `[ic-reactor] Could not detect the local IC environment, so no ic_env cookie is set${proxyNote}. Is the local network running? The plugin asks \`icp\` again on each page load until it answers${ownsApiProxy ? ", then sends /api to the network it reports" : ""}: start the network (\`icp network start\`) and reload the page. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
560
+ );
561
+ return;
562
+ }
563
+ const missing = state.missingCanisterIds;
564
+ if (missing.length > 0) {
565
+ const it = missing.length === 1 ? "it" : "them";
566
+ console.warn(
567
+ `[ic-reactor] The local replica is running, but no canister ID could be resolved for ${quoteNames(missing)}. Until one is, the ic_env cookie carries no PUBLIC_CANISTER_ID for ${it} and the app will see an undefined canister id. Deploy ${it} (\`icp deploy\`) and reload the page: the plugin asks \`icp\` again on each page load until every configured canister has an ID. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
568
+ );
569
+ }
570
+ }
571
+ function reportDetectionProgress(previous, next, followsApiProxy) {
572
+ if (!previous.environment && next.environment) {
573
+ console.log(
574
+ `[ic-reactor] Detected the local IC network: the ic_env cookie now carries its root key` + (followsApiProxy ? ` and /api goes to ${next.proxyTarget}.` : ".")
575
+ );
576
+ }
577
+ const resolved = previous.missingCanisterIds.filter(
578
+ (name) => !next.missingCanisterIds.includes(name)
579
+ );
580
+ if (next.environment && resolved.length > 0) {
581
+ console.log(
582
+ `[ic-reactor] The ic_env cookie now carries the canister ID${resolved.length === 1 ? "" : "s"} for ${quoteNames(resolved)}.`
583
+ );
584
+ }
585
+ if (next.complete && !previous.complete) {
586
+ console.log(
587
+ "[ic-reactor] Every configured canister has an ID, so page loads no longer run `icp`. Restart the dev server after redeploying into a fresh network."
588
+ );
589
+ }
351
590
  }
352
591
  function debugLog(message) {
353
592
  const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {