@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.cjs CHANGED
@@ -33,24 +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, projectRoot = process.cwd()) {
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
- cwd: projectRoot,
48
- encoding: "utf-8",
49
- // stderr is piped rather than ignored so a failure can explain itself.
50
- // Piping still keeps it off the terminal — it only reaches the user if
51
- // the caller decides to print the diagnostics we collect below.
52
- stdio: ["ignore", "pipe", "pipe"]
53
- })
70
+ await runIcp(
71
+ ["network", "status", "-e", networkName, "--json"],
72
+ projectRoot
73
+ )
54
74
  );
55
75
  const rootKey = networkStatus.root_key;
56
76
  const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
@@ -63,15 +83,10 @@ function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
63
83
  const canisterIds = {};
64
84
  for (const name of canisterNames) {
65
85
  try {
66
- const canisterId = (0, import_child_process.execFileSync)(
67
- "icp",
86
+ const canisterId = (await runIcp(
68
87
  ["canister", "status", name, "-e", networkName, "-i"],
69
- {
70
- cwd: projectRoot,
71
- encoding: "utf-8",
72
- stdio: ["ignore", "pipe", "pipe"]
73
- }
74
- ).trim();
88
+ projectRoot
89
+ )).trim();
75
90
  if (canisterId) {
76
91
  canisterIds[name] = canisterId;
77
92
  }
@@ -132,9 +147,129 @@ function localInternetIdentityProvider(proxyTarget) {
132
147
  return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
133
148
  }
134
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
+
135
271
  // src/index.ts
136
272
  var PLUGIN_NAME = "ic-reactor-plugin";
137
- var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
138
273
  function icReactor(options) {
139
274
  const {
140
275
  canisters,
@@ -151,10 +286,29 @@ function icReactor(options) {
151
286
  };
152
287
  let projectRoot = process.cwd();
153
288
  let command = "build";
289
+ let localEnvironment;
290
+ const apiProxyOptions = /* @__PURE__ */ new Set();
154
291
  let devServer = null;
155
292
  const resolveDidPath = (didFile) => import_node_path.default.normalize(
156
293
  import_node_path.default.isAbsolute(didFile) ? didFile : import_node_path.default.resolve(projectRoot, didFile)
157
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
+ };
158
312
  const configuredCanisterIds = Object.fromEntries(
159
313
  canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
160
314
  );
@@ -173,22 +327,31 @@ function icReactor(options) {
173
327
  const rerunQueued = /* @__PURE__ */ new Set();
174
328
  const regenerate = (canisterConfig, server) => {
175
329
  const { name } = canisterConfig;
176
- const running = inFlight.get(name);
330
+ const running = inFlight.get(canisterConfig);
177
331
  if (running) {
178
- rerunQueued.add(name);
332
+ rerunQueued.add(canisterConfig);
179
333
  return running;
180
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
+ }
181
343
  const run = (0, import_codegen.runCanisterPipeline)({
182
344
  canisterConfig,
183
345
  projectRoot,
184
346
  globalConfig
185
347
  }).then((result) => {
348
+ reportWarnings(name, result.warnings);
186
349
  if (result.success) {
187
- pendingFailures.delete(name);
350
+ pendingFailures.delete(canisterConfig);
188
351
  server.ws.send({ type: "full-reload" });
189
352
  } else {
190
353
  pendingFailures.set(
191
- name,
354
+ canisterConfig,
192
355
  reportFailure(
193
356
  server,
194
357
  `Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
@@ -197,7 +360,7 @@ function icReactor(options) {
197
360
  }
198
361
  }).catch((error) => {
199
362
  pendingFailures.set(
200
- name,
363
+ canisterConfig,
201
364
  reportFailure(
202
365
  server,
203
366
  `Regeneration failed for ${name}: ${describeError(error)}`,
@@ -205,81 +368,77 @@ function icReactor(options) {
205
368
  )
206
369
  );
207
370
  }).finally(() => {
208
- inFlight.delete(name);
209
- if (rerunQueued.delete(name)) {
371
+ inFlight.delete(canisterConfig);
372
+ if (rerunQueued.delete(canisterConfig)) {
210
373
  void regenerate(canisterConfig, server);
211
374
  }
212
375
  });
213
- inFlight.set(name, run);
376
+ inFlight.set(canisterConfig, run);
214
377
  return run;
215
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
+ };
216
397
  const plugin = {
217
398
  name: PLUGIN_NAME,
218
399
  enforce: "pre",
219
400
  // Run before other plugins
220
- config(userConfig, { command: viteCommand }) {
401
+ async config(userConfig, { command: viteCommand }) {
221
402
  command = viteCommand;
222
403
  if (viteCommand !== "serve" || !injectEnvironment) {
223
404
  return {};
224
405
  }
225
- const canisterNames = canisters.map((c) => c.name).filter((n) => !!n);
226
- if (!canisterNames.includes("internet_identity")) {
227
- canisterNames.push("internet_identity");
228
- }
229
- const { environment: icEnv, diagnostics } = getIcEnvironmentInfo(
230
- canisterNames,
231
- import_node_path.default.resolve(userConfig.root ?? process.cwd())
232
- );
233
- if (!icEnv) {
234
- if (canisters.length > 0) {
235
- console.warn(
236
- `[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.`
237
- );
238
- }
239
- for (const diagnostic of diagnostics) {
240
- debugLog(diagnostic);
241
- }
242
- const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
243
- {},
244
- void 0,
245
- "http://id.ai.localhost:8000/authorize"
246
- ) : void 0;
247
- return {
248
- server: {
249
- headers: envOnlyCookie ? {
250
- "Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
251
- } : void 0,
252
- proxy: apiProxy(userConfig, DEFAULT_LOCAL_REPLICA)
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;
253
418
  }
254
- };
255
- }
256
- const missingCanisterIds = canisters.map((canister) => canister.name).filter((name) => !!name).filter(
257
- (name) => !icEnv.canisterIds[name] && !configuredCanisterIds[name]
258
- );
259
- if (missingCanisterIds.length > 0) {
260
- const names = missingCanisterIds.map((name) => `"${name}"`).join(", ");
261
- const it = missingCanisterIds.length === 1 ? "it" : "them";
262
- console.warn(
263
- `[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.`
264
- );
265
- }
266
- for (const diagnostic of diagnostics) {
267
- debugLog(diagnostic);
268
- }
269
- const cookieValue = buildIcEnvCookie(
270
- {
271
- ...icEnv.canisterIds,
272
- ...configuredCanisterIds
273
- },
274
- icEnv.rootKey,
275
- icEnv.internetIdentityProvider
276
- );
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);
277
427
  return {
278
428
  server: {
279
- headers: {
280
- "Set-Cookie": `ic_env=${cookieValue}; Path=/; SameSite=Lax;`
281
- },
282
- proxy: apiProxy(userConfig, icEnv.proxyTarget)
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;
438
+ }
439
+ apiProxyOptions.add(proxyOptions);
440
+ proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
441
+ })
283
442
  }
284
443
  };
285
444
  },
@@ -289,6 +448,9 @@ function icReactor(options) {
289
448
  },
290
449
  configureServer(server) {
291
450
  devServer = server;
451
+ if (localEnvironment) {
452
+ server.middlewares.use(icEnvMiddleware(localEnvironment));
453
+ }
292
454
  server.ws.on?.("connection", () => {
293
455
  if (pendingFailures.size === 0) return;
294
456
  const failures = [...pendingFailures.values()];
@@ -303,29 +465,69 @@ function icReactor(options) {
303
465
  });
304
466
  const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
305
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
+ }
306
478
  },
307
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
+ }
308
492
  console.log(
309
- `[ic-reactor] Generating canister bindings for ${canisters.length} canisters...`
493
+ `[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
310
494
  );
311
495
  const outcomes = await Promise.allSettled(
312
- canisters.map(
313
- (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)({
314
502
  canisterConfig,
315
503
  projectRoot,
316
504
  globalConfig
317
- })
318
- )
505
+ });
506
+ })
319
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
+ });
320
520
  const failures = outcomes.flatMap((outcome, index) => {
321
- const name = canisters[index]?.name ?? `canister #${index}`;
521
+ const canister = pending[index];
522
+ const name = canister?.name ?? `canister #${index}`;
322
523
  if (outcome.status === "rejected") {
323
- return [{ name, detail: `${name}: ${describeError(outcome.reason)}` }];
524
+ const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
525
+ return [{ canister, detail }];
324
526
  }
325
527
  if (!outcome.value.success) {
326
528
  return [
327
529
  {
328
- name,
530
+ canister,
329
531
  detail: `${name}: ${outcome.value.error ?? "unknown error"}`
330
532
  }
331
533
  ];
@@ -335,51 +537,86 @@ function icReactor(options) {
335
537
  if (failures.length === 0) {
336
538
  return;
337
539
  }
338
- const message = `Failed to generate ${failures.length} of ${canisters.length} canisters:
540
+ const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
339
541
  ` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
340
542
  if (failOnError ?? command === "build") {
341
543
  this.error(`[ic-reactor] ${message}`);
342
544
  }
343
545
  reportFailure(devServer, message);
344
- for (const { name, detail } of failures) {
345
- pendingFailures.set(name, {
546
+ for (const { canister, detail } of failures) {
547
+ pendingFailures.set(canister, {
346
548
  message: `[ic-reactor] Failed to generate ${detail}`,
347
549
  stack: ""
348
550
  });
349
551
  }
350
- },
351
- handleHotUpdate({ file, server }) {
352
- if (!file.endsWith(".did")) {
353
- return;
354
- }
355
- const changedPath = import_node_path.default.normalize(file);
356
- const affectedCanisters = canisters.filter(
357
- (canister) => resolveDidPath(canister.didFile) === changedPath
358
- );
359
- if (affectedCanisters.length === 0) {
360
- return;
361
- }
362
- console.log(
363
- `[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
364
- );
365
- return Promise.all(
366
- affectedCanisters.map((canister) => regenerate(canister, server))
367
- ).then(() => void 0);
368
552
  }
369
553
  };
370
554
  return plugin;
371
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
+ }
372
563
  function describeError(error) {
373
564
  return error instanceof Error ? error.message : String(error);
374
565
  }
375
- function apiProxy(userConfig, target) {
566
+ function apiProxy(userConfig, target, configure) {
376
567
  if (userConfig.server?.proxy?.["/api"]) {
377
568
  debugLog(
378
569
  `The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
379
570
  );
380
571
  return void 0;
381
572
  }
382
- return { "/api": { target, changeOrigin: true } };
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
+ }
383
620
  }
384
621
  function debugLog(message) {
385
622
  const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
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;
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;