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

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