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