@ic-reactor/vite-plugin 0.13.1 → 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.d.ts CHANGED
@@ -13,7 +13,9 @@ import { CanisterConfig, CodegenTarget } from '@ic-reactor/codegen';
13
13
  interface IcReactorPluginOptions {
14
14
  /**
15
15
  * Canister configurations.
16
- * `name` is required for each canister.
16
+ * `name` is required for each canister. Set `factories: true` on an entry to
17
+ * also generate `index.factories.generated.ts`, a query or mutation object
18
+ * per method bound to the generated reactor; see `CanisterConfig.factories`.
17
19
  */
18
20
  canisters: CanisterConfig[];
19
21
  /**
@@ -32,7 +34,17 @@ interface IcReactorPluginOptions {
32
34
  */
33
35
  target?: CodegenTarget;
34
36
  /**
35
- * Automatically inject `ic_env` cookie for local development?
37
+ * Inject the local IC environment under `vite dev` and `vite preview`: set
38
+ * the `ic_env` cookie on each response and proxy `/api` to the network the
39
+ * `icp` CLI reports.
40
+ *
41
+ * Until `icp` reports a network and every configured canister has an ID
42
+ * (a configured `canisterId` counts), each page load asks `icp` again, so a
43
+ * deploy after the server started needs only a reload. Once detection is
44
+ * complete, page loads run no `icp` command, and a redeploy into a fresh
45
+ * network needs a restart. An `/api` proxy that the Vite config or another
46
+ * plugin sets is left alone.
47
+ *
36
48
  * Default: true
37
49
  */
38
50
  injectEnvironment?: boolean;
package/dist/index.js CHANGED
@@ -1,23 +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) {
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
- encoding: "utf-8",
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"]
20
- })
40
+ await runIcp(
41
+ ["network", "status", "-e", networkName, "--json"],
42
+ projectRoot
43
+ )
21
44
  );
22
45
  const rootKey = networkStatus.root_key;
23
46
  const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
@@ -30,11 +53,10 @@ function getIcEnvironmentInfo(canisterNames) {
30
53
  const canisterIds = {};
31
54
  for (const name of canisterNames) {
32
55
  try {
33
- const canisterId = execFileSync(
34
- "icp",
56
+ const canisterId = (await runIcp(
35
57
  ["canister", "status", name, "-e", networkName, "-i"],
36
- { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }
37
- ).trim();
58
+ projectRoot
59
+ )).trim();
38
60
  if (canisterId) {
39
61
  canisterIds[name] = canisterId;
40
62
  }
@@ -95,9 +117,129 @@ function localInternetIdentityProvider(proxyTarget) {
95
117
  return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
96
118
  }
97
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
+
98
241
  // src/index.ts
99
242
  var PLUGIN_NAME = "ic-reactor-plugin";
100
- var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
101
243
  function icReactor(options) {
102
244
  const {
103
245
  canisters,
@@ -114,14 +256,33 @@ function icReactor(options) {
114
256
  };
115
257
  let projectRoot = process.cwd();
116
258
  let command = "build";
259
+ let localEnvironment;
260
+ const apiProxyOptions = /* @__PURE__ */ new Set();
117
261
  let devServer = null;
118
262
  const resolveDidPath = (didFile) => path.normalize(
119
263
  path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
120
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
+ };
121
282
  const configuredCanisterIds = Object.fromEntries(
122
283
  canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
123
284
  );
124
- let pendingFailure = null;
285
+ const pendingFailures = /* @__PURE__ */ new Map();
125
286
  const reportFailure = (server, message, cause) => {
126
287
  console.error(`[ic-reactor] ${message}`);
127
288
  const err = {
@@ -129,121 +290,125 @@ function icReactor(options) {
129
290
  stack: cause instanceof Error && cause.stack ? cause.stack : "",
130
291
  plugin: PLUGIN_NAME
131
292
  };
132
- pendingFailure = { message: err.message, stack: err.stack };
133
293
  server?.ws.send({ type: "error", err });
294
+ return { message: err.message, stack: err.stack };
134
295
  };
135
296
  const inFlight = /* @__PURE__ */ new Map();
136
297
  const rerunQueued = /* @__PURE__ */ new Set();
137
298
  const regenerate = (canisterConfig, server) => {
138
299
  const { name } = canisterConfig;
139
- const running = inFlight.get(name);
300
+ const running = inFlight.get(canisterConfig);
140
301
  if (running) {
141
- rerunQueued.add(name);
302
+ rerunQueued.add(canisterConfig);
142
303
  return running;
143
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
+ }
144
313
  const run = runCanisterPipeline({
145
314
  canisterConfig,
146
315
  projectRoot,
147
316
  globalConfig
148
317
  }).then((result) => {
318
+ reportWarnings(name, result.warnings);
149
319
  if (result.success) {
150
- pendingFailure = null;
320
+ pendingFailures.delete(canisterConfig);
151
321
  server.ws.send({ type: "full-reload" });
152
322
  } else {
153
- reportFailure(
154
- server,
155
- `Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
323
+ pendingFailures.set(
324
+ canisterConfig,
325
+ reportFailure(
326
+ server,
327
+ `Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
328
+ )
156
329
  );
157
330
  }
158
331
  }).catch((error) => {
159
- reportFailure(
160
- server,
161
- `Regeneration failed for ${name}: ${describeError(error)}`,
162
- error
332
+ pendingFailures.set(
333
+ canisterConfig,
334
+ reportFailure(
335
+ server,
336
+ `Regeneration failed for ${name}: ${describeError(error)}`,
337
+ error
338
+ )
163
339
  );
164
340
  }).finally(() => {
165
- inFlight.delete(name);
166
- if (rerunQueued.delete(name)) {
341
+ inFlight.delete(canisterConfig);
342
+ if (rerunQueued.delete(canisterConfig)) {
167
343
  void regenerate(canisterConfig, server);
168
344
  }
169
345
  });
170
- inFlight.set(name, run);
346
+ inFlight.set(canisterConfig, run);
171
347
  return run;
172
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
+ };
173
367
  const plugin = {
174
368
  name: PLUGIN_NAME,
175
369
  enforce: "pre",
176
370
  // Run before other plugins
177
- config(_config, { command: viteCommand }) {
371
+ async config(userConfig, { command: viteCommand }) {
178
372
  command = viteCommand;
179
373
  if (viteCommand !== "serve" || !injectEnvironment) {
180
374
  return {};
181
375
  }
182
- const canisterNames = canisters.map((c) => c.name).filter((n) => !!n);
183
- if (!canisterNames.includes("internet_identity")) {
184
- canisterNames.push("internet_identity");
185
- }
186
- const { environment: icEnv, diagnostics } = getIcEnvironmentInfo(canisterNames);
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
- }
196
- const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
197
- {},
198
- void 0,
199
- "http://id.ai.localhost:8000/authorize"
200
- ) : void 0;
201
- return {
202
- server: {
203
- headers: envOnlyCookie ? {
204
- "Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
205
- } : void 0,
206
- proxy: {
207
- "/api": {
208
- target: DEFAULT_LOCAL_REPLICA,
209
- changeOrigin: true
210
- }
211
- }
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;
212
388
  }
213
- };
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
- }
228
- const cookieValue = buildIcEnvCookie(
229
- {
230
- ...icEnv.canisterIds,
231
- ...configuredCanisterIds
232
- },
233
- icEnv.rootKey,
234
- icEnv.internetIdentityProvider
235
- );
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);
236
397
  return {
237
398
  server: {
238
- headers: {
239
- "Set-Cookie": `ic_env=${cookieValue}; Path=/; SameSite=Lax;`
240
- },
241
- proxy: {
242
- "/api": {
243
- target: icEnv.proxyTarget,
244
- changeOrigin: true
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;
245
408
  }
246
- }
409
+ apiProxyOptions.add(proxyOptions);
410
+ proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
411
+ })
247
412
  }
248
413
  };
249
414
  },
@@ -253,72 +418,176 @@ function icReactor(options) {
253
418
  },
254
419
  configureServer(server) {
255
420
  devServer = server;
421
+ if (localEnvironment) {
422
+ server.middlewares.use(icEnvMiddleware(localEnvironment));
423
+ }
256
424
  server.ws.on?.("connection", () => {
257
- if (!pendingFailure) return;
425
+ if (pendingFailures.size === 0) return;
426
+ const failures = [...pendingFailures.values()];
258
427
  server.ws.send({
259
428
  type: "error",
260
- err: { ...pendingFailure, plugin: PLUGIN_NAME }
429
+ err: {
430
+ message: failures.map((failure) => failure.message).join("\n"),
431
+ stack: failures.map((failure) => failure.stack).filter(Boolean).join("\n"),
432
+ plugin: PLUGIN_NAME
433
+ }
261
434
  });
262
435
  });
263
436
  const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
264
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
+ }
265
448
  },
266
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
+ }
267
462
  console.log(
268
- `[ic-reactor] Generating canister bindings for ${canisters.length} canisters...`
463
+ `[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
269
464
  );
270
465
  const outcomes = await Promise.allSettled(
271
- canisters.map(
272
- (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({
273
472
  canisterConfig,
274
473
  projectRoot,
275
474
  globalConfig
276
- })
277
- )
475
+ });
476
+ })
278
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
+ });
279
490
  const failures = outcomes.flatMap((outcome, index) => {
280
- const name = canisters[index]?.name ?? `canister #${index}`;
491
+ const canister = pending[index];
492
+ const name = canister?.name ?? `canister #${index}`;
281
493
  if (outcome.status === "rejected") {
282
- return [`${name}: ${describeError(outcome.reason)}`];
494
+ const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
495
+ return [{ canister, detail }];
283
496
  }
284
497
  if (!outcome.value.success) {
285
- return [`${name}: ${outcome.value.error ?? "unknown error"}`];
498
+ return [
499
+ {
500
+ canister,
501
+ detail: `${name}: ${outcome.value.error ?? "unknown error"}`
502
+ }
503
+ ];
286
504
  }
287
505
  return [];
288
506
  });
289
507
  if (failures.length === 0) {
290
508
  return;
291
509
  }
292
- const message = `Failed to generate ${failures.length} of ${canisters.length} canisters:
293
- ` + failures.map((failure) => ` - ${failure}`).join("\n");
510
+ const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
511
+ ` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
294
512
  if (failOnError ?? command === "build") {
295
513
  this.error(`[ic-reactor] ${message}`);
296
514
  }
297
515
  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;
516
+ for (const { canister, detail } of failures) {
517
+ pendingFailures.set(canister, {
518
+ message: `[ic-reactor] Failed to generate ${detail}`,
519
+ stack: ""
520
+ });
310
521
  }
311
- console.log(
312
- `[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
313
- );
314
- return regenerate(affectedCanister, server);
315
522
  }
316
523
  };
317
524
  return plugin;
318
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
+ }
319
533
  function describeError(error) {
320
534
  return error instanceof Error ? error.message : String(error);
321
535
  }
536
+ function apiProxy(userConfig, target, configure) {
537
+ if (userConfig.server?.proxy?.["/api"]) {
538
+ debugLog(
539
+ `The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
540
+ );
541
+ return void 0;
542
+ }
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
+ }
590
+ }
322
591
  function debugLog(message) {
323
592
  const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
324
593
  const scope = entry.trim();