@ic-reactor/vite-plugin 0.15.1 → 4.0.0-beta.2

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,9 +33,8 @@ __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);
37
- var import_node_path = __toESM(require("path"), 1);
38
- var import_codegen = require("@ic-reactor/codegen");
36
+ var import_node_fs2 = __toESM(require("fs"), 1);
37
+ var import_node_path2 = __toESM(require("path"), 1);
39
38
 
40
39
  // src/env.ts
41
40
  var import_child_process = require("child_process");
@@ -268,149 +267,549 @@ function icEnvMiddleware(environment) {
268
267
  };
269
268
  }
270
269
 
270
+ // src/generate.ts
271
+ var import_node_child_process = require("child_process");
272
+ var import_node_fs = __toESM(require("fs"), 1);
273
+ var import_node_module = require("module");
274
+ var import_node_path = __toESM(require("path"), 1);
275
+ var CANDID_CORE_CLI_VERSION = "0.2.0";
276
+ var REPORT_SCHEMA_VERSION = 1;
277
+ var GENERATE_TIMEOUT_MS = 6e4;
278
+ var MAX_STREAMED_LINES = 200;
279
+ var MAX_LINE_CHARS = 4096;
280
+ var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
281
+ function resolveCliBin(root) {
282
+ const requireFromApp = (0, import_node_module.createRequire)(import_node_path.default.join(root, "package.json"));
283
+ let manifestPath;
284
+ try {
285
+ manifestPath = requireFromApp.resolve("@candid-core/cli/package.json");
286
+ } catch {
287
+ throw new Error(
288
+ `cannot find @candid-core/cli from ${root}. Install the generator that pairs with this plugin next to @candid-core/schema: npm install --save-dev --save-exact @candid-core/cli@${CANDID_CORE_CLI_VERSION}`
289
+ );
290
+ }
291
+ const manifest = JSON.parse(import_node_fs.default.readFileSync(manifestPath, "utf-8"));
292
+ const bin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.["candid-core-cli"];
293
+ if (!bin) {
294
+ throw new Error(
295
+ `${manifestPath} declares no candid-core-cli bin, so it is not the generator this plugin runs`
296
+ );
297
+ }
298
+ return import_node_path.default.resolve(import_node_path.default.dirname(manifestPath), bin);
299
+ }
300
+ var keep = (text, chunk) => text.length >= MAX_OUTPUT_BYTES ? text : text + chunk;
301
+ function runProcess(bin, args, cwd, timeoutMs, signal, onStderrLine) {
302
+ return new Promise((resolve) => {
303
+ let stdout = "";
304
+ let stderr = "";
305
+ let timedOut = false;
306
+ let settled = false;
307
+ const stopped = {
308
+ code: null,
309
+ signal: null,
310
+ problem: "was stopped",
311
+ stopped: true
312
+ };
313
+ if (signal?.aborted) {
314
+ resolve({ stdout, stderr, ...stopped });
315
+ return;
316
+ }
317
+ let unfinished = "";
318
+ let cut = false;
319
+ let passed = 0;
320
+ const pass = (text) => {
321
+ if (!text.trim() || passed > MAX_STREAMED_LINES) return;
322
+ passed++;
323
+ onStderrLine?.(
324
+ passed > MAX_STREAMED_LINES ? `more than ${MAX_STREAMED_LINES} lines on stderr: the rest is not logged` : text.trimEnd()
325
+ );
326
+ };
327
+ const passUnfinished = () => {
328
+ pass(
329
+ cut ? `${unfinished} [line cut at ${MAX_LINE_CHARS} characters]` : unfinished
330
+ );
331
+ unfinished = "";
332
+ cut = false;
333
+ };
334
+ const settle = (result) => {
335
+ if (settled) return;
336
+ settled = true;
337
+ passUnfinished();
338
+ clearTimeout(timer);
339
+ signal?.removeEventListener("abort", stop);
340
+ resolve({ stdout, stderr, ...result });
341
+ };
342
+ const stop = () => {
343
+ unfinished = "";
344
+ cut = false;
345
+ child.kill("SIGKILL");
346
+ settle(stopped);
347
+ };
348
+ const child = (0, import_node_child_process.spawn)(process.execPath, [bin, ...args], {
349
+ cwd,
350
+ stdio: ["ignore", "pipe", "pipe"],
351
+ windowsHide: true
352
+ });
353
+ const timer = setTimeout(() => {
354
+ timedOut = true;
355
+ child.kill("SIGKILL");
356
+ }, timeoutMs);
357
+ signal?.addEventListener("abort", stop, { once: true });
358
+ child.stdout.setEncoding("utf-8");
359
+ child.stderr.setEncoding("utf-8");
360
+ child.stdout.on("data", (chunk) => stdout = keep(stdout, chunk));
361
+ child.stderr.on("data", (chunk) => {
362
+ stderr = keep(stderr, chunk);
363
+ if (settled || !onStderrLine || passed > MAX_STREAMED_LINES) return;
364
+ const pieces = chunk.split(/\r?\n/);
365
+ pieces.forEach((piece, index) => {
366
+ const room = MAX_LINE_CHARS - unfinished.length;
367
+ if (piece.length > room) cut = true;
368
+ unfinished += piece.slice(0, room);
369
+ if (index < pieces.length - 1) passUnfinished();
370
+ });
371
+ });
372
+ child.on(
373
+ "error",
374
+ (error) => settle({
375
+ code: null,
376
+ signal: null,
377
+ problem: `could not start: ${error.message}`,
378
+ unstartable: true
379
+ })
380
+ );
381
+ child.on(
382
+ "close",
383
+ (code, signal2) => settle({
384
+ code,
385
+ signal: signal2,
386
+ problem: timedOut ? `did not finish within ${timeoutMs / 1e3}s and was killed` : void 0
387
+ })
388
+ );
389
+ });
390
+ }
391
+ function describeProcess(run, reason) {
392
+ const ending = run.problem ?? (run.signal ? `was killed by ${run.signal}` : `exited with code ${run.code}`);
393
+ const lines = [`candid-core-cli ${ending}`];
394
+ if (reason) lines.push(reason);
395
+ const stderr = run.stderr.trim();
396
+ if (stderr) lines.push(stderr);
397
+ return lines.join("\n");
398
+ }
399
+ function parseReport(stdout, count) {
400
+ let report;
401
+ try {
402
+ report = JSON.parse(stdout);
403
+ } catch {
404
+ return void 0;
405
+ }
406
+ const { schemaVersion, entries } = report ?? {};
407
+ if (schemaVersion !== REPORT_SCHEMA_VERSION) {
408
+ throw new Error(
409
+ `its report has schemaVersion ${String(schemaVersion)}, and this plugin reads ${REPORT_SCHEMA_VERSION}; install @candid-core/cli@${CANDID_CORE_CLI_VERSION}`
410
+ );
411
+ }
412
+ if (!Array.isArray(entries) || entries.length !== count) {
413
+ throw new Error(`its report does not have one entry per .did (${count})`);
414
+ }
415
+ return entries.map((entry) => {
416
+ const { status, module: module2, omitted, diagnostics } = entry;
417
+ return {
418
+ status: String(status),
419
+ module: typeof module2 === "string" ? module2 : void 0,
420
+ omitted: Array.isArray(omitted) ? omitted : [],
421
+ diagnostics: Array.isArray(diagnostics) ? diagnostics : []
422
+ };
423
+ });
424
+ }
425
+ function formatDiagnostic(diagnostic) {
426
+ const { code, message, notes } = diagnostic ?? {};
427
+ if (typeof message !== "string") return JSON.stringify(diagnostic);
428
+ const lead = typeof code === "string" ? `${code}: ${message}` : message;
429
+ const extra = Array.isArray(notes) ? notes.map((note) => ` ${note}`) : [];
430
+ return [lead, ...extra].join("\n");
431
+ }
432
+ async function runGroup(context, outDir, units) {
433
+ const run = await runProcess(
434
+ context.cli,
435
+ ["gen", ...units.map((unit) => unit.didFile), "-o", outDir, "--json"],
436
+ context.root,
437
+ context.timeoutMs,
438
+ context.signal,
439
+ (line) => context.onStderr?.(
440
+ line,
441
+ units.map((unit) => unit.didFile)
442
+ )
443
+ );
444
+ let entries;
445
+ let reason;
446
+ if (!run.problem) {
447
+ try {
448
+ entries = parseReport(run.stdout, units.length);
449
+ if (!entries && run.code === 0) reason = "it printed no report";
450
+ } catch (error) {
451
+ reason = error instanceof Error ? error.message : String(error);
452
+ }
453
+ }
454
+ if (!entries) {
455
+ if (units.length > 1 && !run.unstartable && !run.stopped) {
456
+ const results = await Promise.all(
457
+ units.map((unit) => runGroup(context, outDir, [unit]))
458
+ );
459
+ return { outcomes: results.flatMap((result) => result.outcomes) };
460
+ }
461
+ const failure = describeProcess(run, reason);
462
+ return {
463
+ outcomes: units.map(({ names }) => ({
464
+ names,
465
+ status: "failed",
466
+ omitted: [],
467
+ failure
468
+ }))
469
+ };
470
+ }
471
+ return {
472
+ outcomes: units.map((unit, index) => {
473
+ const entry = entries[index];
474
+ if (entry.status === "failed") {
475
+ return {
476
+ names: unit.names,
477
+ status: "failed",
478
+ omitted: [],
479
+ failure: entry.diagnostics.map(formatDiagnostic).join("\n") || "candid-core-cli reported the entry as failed"
480
+ };
481
+ }
482
+ return {
483
+ names: unit.names,
484
+ module: entry.module,
485
+ status: entry.status === "unchanged" ? "unchanged" : "written",
486
+ omitted: entry.omitted
487
+ };
488
+ })
489
+ };
490
+ }
491
+ function quoteNames(names) {
492
+ return names.map((name) => `"${name}"`).join(", ");
493
+ }
494
+ function foldsCase(dir) {
495
+ const swapCase = (name) => name.replace(
496
+ /[a-z]/gi,
497
+ (letter) => letter === letter.toLowerCase() ? letter.toUpperCase() : letter.toLowerCase()
498
+ );
499
+ const probe = (entry) => {
500
+ const swapped = import_node_path.default.join(
501
+ import_node_path.default.dirname(entry),
502
+ swapCase(import_node_path.default.basename(entry))
503
+ );
504
+ if (swapped === entry) return void 0;
505
+ try {
506
+ const found = import_node_fs.default.lstatSync(entry, { bigint: true });
507
+ if (found.ino === 0n) return void 0;
508
+ try {
509
+ const other = import_node_fs.default.lstatSync(swapped, { bigint: true });
510
+ return other.dev === found.dev && other.ino === found.ino;
511
+ } catch (error) {
512
+ return error.code === "ENOENT" ? false : void 0;
513
+ }
514
+ } catch {
515
+ return void 0;
516
+ }
517
+ };
518
+ for (let at = dir; ; at = import_node_path.default.dirname(at)) {
519
+ let names = [];
520
+ try {
521
+ names = import_node_fs.default.readdirSync(at);
522
+ } catch {
523
+ }
524
+ const named = names.find((name) => swapCase(name) !== name);
525
+ const verdict = (named === void 0 ? void 0 : probe(import_node_path.default.join(at, named))) ?? probe(at);
526
+ if (verdict !== void 0) return verdict;
527
+ if (import_node_path.default.dirname(at) === at) return void 0;
528
+ }
529
+ }
530
+ async function generate(request) {
531
+ const { root } = request;
532
+ const units = /* @__PURE__ */ new Map();
533
+ const claimed = /* @__PURE__ */ new Map();
534
+ const refused = /* @__PURE__ */ new Map();
535
+ const folding = /* @__PURE__ */ new Map();
536
+ const targetOf = (unit) => {
537
+ let folds = folding.get(unit.outDir);
538
+ if (folds === void 0) {
539
+ folds = (request.foldsCase ?? foldsCase)(unit.outDir) ?? false;
540
+ folding.set(unit.outDir, folds);
541
+ }
542
+ return folds ? moduleOf(unit).toLowerCase() : moduleOf(unit);
543
+ };
544
+ for (const canister of request.canisters) {
545
+ const key = JSON.stringify([canister.didFile, canister.outDir]);
546
+ const same = units.get(key);
547
+ if (same) {
548
+ same.names.push(canister.name);
549
+ continue;
550
+ }
551
+ const unit = {
552
+ names: [canister.name],
553
+ didFile: canister.didFile,
554
+ outDir: canister.outDir
555
+ };
556
+ units.set(key, unit);
557
+ const target = targetOf(unit);
558
+ const owner = claimed.get(target);
559
+ if (owner) refused.set(unit, owner);
560
+ else claimed.set(target, unit);
561
+ }
562
+ const outcomes = [...refused].map(([unit, owner]) => ({
563
+ names: unit.names,
564
+ status: "failed",
565
+ omitted: [],
566
+ failure: `would write ${import_node_path.default.relative(root, moduleOf(unit))}, which ${quoteNames(owner.names)} already writes` + (moduleOf(unit) === moduleOf(owner) ? ". " : ` as ${import_node_path.default.relative(root, moduleOf(owner))}: the filesystem does not tell the two names apart. `) + `The generator names a module after its .did file: rename one file or give one canister its own outDir`
567
+ }));
568
+ const groups = /* @__PURE__ */ new Map();
569
+ for (const unit of units.values()) {
570
+ if (refused.has(unit)) continue;
571
+ groups.set(unit.outDir, [...groups.get(unit.outDir) ?? [], unit]);
572
+ }
573
+ const results = await Promise.all(
574
+ [...groups].map(([outDir, members]) => runGroup(request, outDir, members))
575
+ );
576
+ return {
577
+ outcomes: [...outcomes, ...results.flatMap((result) => result.outcomes)]
578
+ };
579
+ }
580
+ function moduleOf(unit) {
581
+ return import_node_path.default.join(
582
+ unit.outDir,
583
+ import_node_path.default.basename(unit.didFile, import_node_path.default.extname(unit.didFile)) + ".ts"
584
+ );
585
+ }
586
+
271
587
  // src/index.ts
272
588
  var PLUGIN_NAME = "ic-reactor-plugin";
273
- function icReactor(options) {
274
- const {
275
- canisters,
276
- outDir = "src/declarations",
277
- clientManagerPath = "../../clients",
278
- target = "react",
279
- injectEnvironment = true,
280
- failOnError
281
- } = options;
282
- const globalConfig = {
283
- outDir,
284
- clientManagerPath,
285
- target
286
- };
589
+ var DEFAULT_OUT_DIR = "src/canisters";
590
+ var GUIDE_LINE = "ic-reactor: agent guide at node_modules/@ic-reactor/core/llms.txt";
591
+ var consoleLog = {
592
+ info: (message) => console.log(message),
593
+ warn: (message) => console.warn(message),
594
+ error: (message) => console.error(message)
595
+ };
596
+ function icReactor(options = {}) {
597
+ const { canisters = {}, injectEnvironment = true, failOnError } = options;
598
+ const names = Object.keys(canisters);
599
+ const configuredCanisterIds = Object.fromEntries(
600
+ names.flatMap((name) => {
601
+ const { canisterId } = canisters[name];
602
+ return canisterId ? [[name, canisterId]] : [];
603
+ })
604
+ );
605
+ const generated = names.flatMap((name) => {
606
+ const { didFile, outDir = DEFAULT_OUT_DIR } = canisters[name];
607
+ return didFile === void 0 ? [] : [{ name, didFile, outDir }];
608
+ });
287
609
  let projectRoot = process.cwd();
288
610
  let command = "build";
611
+ let log = consoleLog;
612
+ let announced = false;
289
613
  let localEnvironment;
290
614
  const apiProxyOptions = /* @__PURE__ */ new Set();
291
615
  let devServer = null;
292
- const resolveDidPath = (didFile) => import_node_path.default.normalize(
293
- import_node_path.default.isAbsolute(didFile) ? didFile : import_node_path.default.resolve(projectRoot, didFile)
294
- );
616
+ const didPath = (canister) => import_node_path2.default.resolve(projectRoot, canister.didFile);
617
+ const relativeToRoot = (file) => import_node_path2.default.relative(projectRoot, file) || ".";
295
618
  const generatedFrom = /* @__PURE__ */ new Map();
296
- const readDidSource = (canister) => {
619
+ const unfixed = /* @__PURE__ */ new Map();
620
+ let tail = Promise.resolve();
621
+ const serially = (job) => {
622
+ const result = tail.then(job);
623
+ tail = result.catch(() => void 0);
624
+ return result;
625
+ };
626
+ const queued = /* @__PURE__ */ new Set();
627
+ let stopper = new AbortController();
628
+ const readDid = (canister) => {
297
629
  try {
298
- return import_node_fs.default.readFileSync(resolveDidPath(canister.didFile), "utf-8");
630
+ return import_node_fs2.default.readFileSync(didPath(canister), "utf-8");
299
631
  } catch {
300
632
  return void 0;
301
633
  }
302
634
  };
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));
635
+ const generateNow = async (wanted, force, signal) => {
636
+ if (signal.aborted) return void 0;
637
+ try {
638
+ const sources = new Map(
639
+ wanted.map((canister) => [canister.name, readDid(canister)])
640
+ );
641
+ const stale = wanted.filter(
642
+ ({ name }) => force || sources.get(name) === void 0 || generatedFrom.get(name) !== sources.get(name)
643
+ );
644
+ if (stale.length === 0) return [];
645
+ let cli;
646
+ try {
647
+ cli = resolveCliBin(projectRoot);
648
+ } catch (error) {
649
+ return stale.map((canister) => ({ canister, message: describe(error) }));
650
+ }
651
+ const result = await generate({
652
+ cli,
653
+ root: projectRoot,
654
+ timeoutMs: GENERATE_TIMEOUT_MS,
655
+ signal,
656
+ // Shown as it arrives: a run that takes long, or is killed at the
657
+ // timeout, would otherwise say nothing until it is over.
658
+ onStderr: (line, didFiles) => {
659
+ if (signal.aborted) return;
660
+ log.warn(
661
+ `ic-reactor: candid-core-cli (${didFiles.map((file) => relativeToRoot(file)).join(", ")}): ${line}`
662
+ );
663
+ },
664
+ canisters: stale.map((canister) => ({
665
+ name: canister.name,
666
+ didFile: didPath(canister),
667
+ outDir: import_node_path2.default.resolve(projectRoot, canister.outDir)
668
+ }))
669
+ });
670
+ if (signal.aborted) return void 0;
671
+ const failures = [];
672
+ for (const outcome of result.outcomes) {
673
+ const members = stale.filter(({ name }) => outcome.names.includes(name));
674
+ const names2 = members.map(({ name }) => name).join(", ");
675
+ if (outcome.status === "failed") {
676
+ for (const canister of members) {
677
+ generatedFrom.delete(canister.name);
678
+ failures.push({ canister, message: outcome.failure ?? "" });
679
+ }
680
+ continue;
681
+ }
682
+ for (const { name } of members) {
683
+ const source = sources.get(name);
684
+ if (source !== void 0) generatedFrom.set(name, source);
685
+ }
686
+ if (outcome.status === "written" && outcome.module) {
687
+ log.info(
688
+ `ic-reactor: generated ${names2} into ${relativeToRoot(outcome.module)}`
689
+ );
690
+ }
691
+ for (const { kind, name: what, reason, via } of outcome.omitted) {
692
+ log.warn(
693
+ `ic-reactor: ${names2}: omitted ${kind} ${what} (${reason}${via ? ` via ${via}` : ""})`
694
+ );
695
+ }
696
+ }
697
+ return failures;
698
+ } catch (error) {
699
+ if (signal.aborted) return void 0;
700
+ return wanted.map((canister) => ({ canister, message: describe(error) }));
701
+ }
311
702
  };
312
- const configuredCanisterIds = Object.fromEntries(
313
- canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
314
- );
315
- const pendingFailures = /* @__PURE__ */ new Map();
316
- const reportFailure = (server, message, cause) => {
317
- console.error(`[ic-reactor] ${message}`);
318
- const err = {
319
- message: `[ic-reactor] ${message}`,
320
- stack: cause instanceof Error && cause.stack ? cause.stack : "",
321
- plugin: PLUGIN_NAME
322
- };
323
- server?.ws.send({ type: "error", err });
324
- return { message: err.message, stack: err.stack };
703
+ const describeFailures = (failures) => {
704
+ const byMessage = /* @__PURE__ */ new Map();
705
+ for (const { canister, message } of failures) {
706
+ const label = `${canister.name} (${relativeToRoot(didPath(canister))})`;
707
+ byMessage.set(message, [...byMessage.get(message) ?? [], label]);
708
+ }
709
+ return `ic-reactor: could not generate ${failures.length} of ${generated.length} canisters:
710
+ ` + [...byMessage].map(
711
+ ([message, labels]) => ` - ${labels.join(", ")}: ${message.replace(/\n/g, "\n ")}`
712
+ ).join("\n");
325
713
  };
326
- const inFlight = /* @__PURE__ */ new Map();
327
- const rerunQueued = /* @__PURE__ */ new Set();
328
- const regenerate = (canisterConfig, server) => {
329
- const { name } = canisterConfig;
330
- const running = inFlight.get(canisterConfig);
331
- if (running) {
332
- rerunQueued.add(canisterConfig);
333
- return running;
714
+ const publish = (attempted, failures) => {
715
+ let wasFailing = false;
716
+ for (const { name } of attempted) {
717
+ if (unfixed.delete(name)) wasFailing = true;
334
718
  }
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();
719
+ for (const failure of failures) unfixed.set(failure.canister.name, failure);
720
+ if (failures.length > 0) {
721
+ log.error(describeFailures(failures));
722
+ showOverlay();
723
+ } else if (wasFailing && unfixed.size > 0) {
724
+ showOverlay();
725
+ } else if (wasFailing) {
726
+ devServer?.ws.send({ type: "full-reload" });
342
727
  }
343
- const run = (0, import_codegen.runCanisterPipeline)({
344
- canisterConfig,
345
- projectRoot,
346
- globalConfig
347
- }).then((result) => {
348
- reportWarnings(name, result.warnings);
349
- if (result.success) {
350
- pendingFailures.delete(canisterConfig);
351
- server.ws.send({ type: "full-reload" });
352
- } else {
353
- pendingFailures.set(
354
- canisterConfig,
355
- reportFailure(
356
- server,
357
- `Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
358
- )
359
- );
728
+ };
729
+ const showOverlay = (client) => {
730
+ if (unfixed.size === 0) return;
731
+ const payload = {
732
+ type: "error",
733
+ err: {
734
+ message: describeFailures([...unfixed.values()]),
735
+ stack: "",
736
+ plugin: PLUGIN_NAME
360
737
  }
361
- }).catch((error) => {
362
- pendingFailures.set(
363
- canisterConfig,
364
- reportFailure(
365
- server,
366
- `Regeneration failed for ${name}: ${describeError(error)}`,
367
- error
368
- )
369
- );
370
- }).finally(() => {
371
- inFlight.delete(canisterConfig);
372
- if (rerunQueued.delete(canisterConfig)) {
373
- void regenerate(canisterConfig, server);
738
+ };
739
+ if (typeof client?.send === "function") {
740
+ try {
741
+ client.send(JSON.stringify(payload));
742
+ } catch {
374
743
  }
375
- });
376
- inFlight.set(canisterConfig, run);
377
- return run;
378
- };
379
- const regenerateForDid = (file, server) => {
380
- if (!file.endsWith(".did")) {
381
744
  return;
382
745
  }
383
- const changedPath = import_node_path.default.normalize(file);
384
- const affectedCanisters = canisters.filter(
385
- (canister) => resolveDidPath(canister.didFile) === changedPath
746
+ devServer?.ws.send(payload);
747
+ };
748
+ const onDidSaved = (file) => {
749
+ const saved = import_node_path2.default.normalize(file);
750
+ const affected = generated.filter((canister) => didPath(canister) === saved);
751
+ if (affected.length === 0 || queued.has(saved)) return;
752
+ queued.add(saved);
753
+ log.info(
754
+ `ic-reactor: ${relativeToRoot(saved)} changed, regenerating ${affected.map(({ name }) => name).join(", ")}`
386
755
  );
387
- if (affectedCanisters.length === 0) {
388
- return;
389
- }
390
- console.log(
391
- `[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
756
+ const { signal } = stopper;
757
+ void serially(async () => {
758
+ queued.delete(saved);
759
+ const failures = await generateNow(affected, true, signal);
760
+ if (failures) publish(affected, failures);
761
+ });
762
+ };
763
+ const onDidRemoved = (file) => {
764
+ const removed = import_node_path2.default.normalize(file);
765
+ const affected = generated.filter(
766
+ (canister) => didPath(canister) === removed
392
767
  );
393
- for (const canister of affectedCanisters) {
394
- void regenerate(canister, server);
768
+ if (affected.length === 0) return;
769
+ const { signal } = stopper;
770
+ void serially(async () => {
771
+ try {
772
+ if (signal.aborted || import_node_fs2.default.existsSync(removed)) return;
773
+ for (const { name } of affected) generatedFrom.delete(name);
774
+ publish(
775
+ affected,
776
+ affected.map((canister) => ({
777
+ canister,
778
+ message: "the .did file was deleted; its module is not regenerated until the file comes back"
779
+ }))
780
+ );
781
+ } catch (error) {
782
+ log.error(`ic-reactor: ${describe(error)}`);
783
+ }
784
+ });
785
+ };
786
+ const announceGuide = (userConfig) => {
787
+ if (announced) return;
788
+ announced = true;
789
+ if (userConfig.customLogger) {
790
+ userConfig.customLogger.info(GUIDE_LINE);
791
+ } else if (!["silent", "error", "warn"].includes(userConfig.logLevel ?? "")) {
792
+ console.log(GUIDE_LINE);
395
793
  }
396
794
  };
397
795
  const plugin = {
398
796
  name: PLUGIN_NAME,
399
797
  enforce: "pre",
400
798
  // Run before other plugins
401
- async config(userConfig, { command: viteCommand }) {
799
+ async config(userConfig, { command: viteCommand, mode }) {
800
+ announceGuide(userConfig);
402
801
  command = viteCommand;
403
- if (viteCommand !== "serve" || !injectEnvironment) {
802
+ if (viteCommand !== "serve" || mode === "test" || !injectEnvironment) {
404
803
  return {};
405
804
  }
406
805
  const ownsApiProxy = !userConfig.server?.proxy?.["/api"];
407
806
  const environment = createLocalEnvironment({
408
- canisterNames: canisters.map((canister) => canister.name).filter((name) => !!name),
807
+ canisterNames: names,
409
808
  configuredCanisterIds,
410
809
  // `configResolved` has not run yet, so resolve the root the way Vite
411
810
  // will. icp finds the project from the directory it starts in, and
412
811
  // 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()),
812
+ projectRoot: import_node_path2.default.resolve(userConfig.root ?? process.cwd()),
414
813
  onDiagnostic: debugLog,
415
814
  onUpdate: (previous, next) => {
416
815
  for (const proxyOptions of apiProxyOptions) {
@@ -423,7 +822,7 @@ function icReactor(options) {
423
822
  });
424
823
  localEnvironment = environment;
425
824
  const state = await environment.detect();
426
- warnAboutIncompleteDetection(state, canisters.length > 0, ownsApiProxy);
825
+ warnAboutIncompleteDetection(state, names.length > 0, ownsApiProxy);
427
826
  return {
428
827
  server: {
429
828
  // The cookie is not a static `server.headers` entry: the middleware
@@ -445,29 +844,19 @@ function icReactor(options) {
445
844
  configResolved(config) {
446
845
  projectRoot = config.root;
447
846
  command = config.command;
847
+ log = config.logger;
448
848
  },
449
849
  configureServer(server) {
450
850
  devServer = server;
451
851
  if (localEnvironment) {
452
852
  server.middlewares.use(icEnvMiddleware(localEnvironment));
453
853
  }
454
- server.ws.on?.("connection", () => {
455
- if (pendingFailures.size === 0) return;
456
- const failures = [...pendingFailures.values()];
457
- server.ws.send({
458
- type: "error",
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
- }
464
- });
465
- });
466
- const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
467
- server.watcher.add(didFiles);
468
- const onDidEvent = (file) => regenerateForDid(file, server);
469
- server.watcher.on("change", onDidEvent);
470
- server.watcher.on("add", onDidEvent);
854
+ if (generated.length === 0) return;
855
+ server.ws.on?.("connection", showOverlay);
856
+ server.watcher.add(generated.map(didPath));
857
+ server.watcher.on("change", onDidSaved);
858
+ server.watcher.on("add", onDidSaved);
859
+ server.watcher.on("unlink", onDidRemoved);
471
860
  },
472
861
  // `vite preview` resolves the config with the `serve` command too, and
473
862
  // used to inherit the cookie from `server.headers`.
@@ -477,90 +866,28 @@ function icReactor(options) {
477
866
  }
478
867
  },
479
868
  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]
869
+ if (generated.length === 0) return;
870
+ for (const canister of generated) this.addWatchFile(didPath(canister));
871
+ const { signal } = stopper;
872
+ const failures = await serially(
873
+ () => generateNow(generated, false, signal)
488
874
  );
489
- if (pending.length === 0) {
490
- return;
491
- }
492
- console.log(
493
- `[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
494
- );
495
- const outcomes = await Promise.allSettled(
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)({
502
- canisterConfig,
503
- projectRoot,
504
- globalConfig
505
- });
506
- })
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
- });
520
- const failures = outcomes.flatMap((outcome, index) => {
521
- const canister = pending[index];
522
- const name = canister?.name ?? `canister #${index}`;
523
- if (outcome.status === "rejected") {
524
- const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
525
- return [{ canister, detail }];
526
- }
527
- if (!outcome.value.success) {
528
- return [
529
- {
530
- canister,
531
- detail: `${name}: ${outcome.value.error ?? "unknown error"}`
532
- }
533
- ];
534
- }
535
- return [];
536
- });
537
- if (failures.length === 0) {
538
- return;
539
- }
540
- const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
541
- ` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
542
- if (failOnError ?? command === "build") {
543
- this.error(`[ic-reactor] ${message}`);
544
- }
545
- reportFailure(devServer, message);
546
- for (const { canister, detail } of failures) {
547
- pendingFailures.set(canister, {
548
- message: `[ic-reactor] Failed to generate ${detail}`,
549
- stack: ""
550
- });
875
+ if (!failures) return;
876
+ if (failures.length > 0 && (failOnError ?? command === "build")) {
877
+ this.error(describeFailures(failures));
551
878
  }
879
+ publish(generated, failures);
880
+ },
881
+ // The end of a build, and the close of a dev server, which Vite reports
882
+ // here once for each of its environments.
883
+ closeBundle() {
884
+ stopper.abort();
885
+ stopper = new AbortController();
552
886
  }
553
887
  };
554
888
  return plugin;
555
889
  }
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
- }
563
- function describeError(error) {
890
+ function describe(error) {
564
891
  return error instanceof Error ? error.message : String(error);
565
892
  }
566
893
  function apiProxy(userConfig, target, configure) {
@@ -578,7 +905,7 @@ function apiProxy(userConfig, target, configure) {
578
905
  }
579
906
  };
580
907
  }
581
- function quoteNames(names) {
908
+ function quoteNames2(names) {
582
909
  return names.map((name) => `"${name}"`).join(", ");
583
910
  }
584
911
  function warnAboutIncompleteDetection(state, hasCanisters, ownsApiProxy) {
@@ -594,7 +921,7 @@ function warnAboutIncompleteDetection(state, hasCanisters, ownsApiProxy) {
594
921
  if (missing.length > 0) {
595
922
  const it = missing.length === 1 ? "it" : "them";
596
923
  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.`
924
+ `[ic-reactor] The local replica is running, but no canister ID could be resolved for ${quoteNames2(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
925
  );
599
926
  }
600
927
  }
@@ -609,7 +936,7 @@ function reportDetectionProgress(previous, next, followsApiProxy) {
609
936
  );
610
937
  if (next.environment && resolved.length > 0) {
611
938
  console.log(
612
- `[ic-reactor] The ic_env cookie now carries the canister ID${resolved.length === 1 ? "" : "s"} for ${quoteNames(resolved)}.`
939
+ `[ic-reactor] The ic_env cookie now carries the canister ID${resolved.length === 1 ? "" : "s"} for ${quoteNames2(resolved)}.`
613
940
  );
614
941
  }
615
942
  if (next.complete && !previous.complete) {