@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/README.md +141 -173
- package/dist/index.cjs +534 -207
- package/dist/index.d.cts +88 -30
- package/dist/index.d.ts +88 -30
- package/dist/index.js +534 -211
- package/package.json +6 -8
- package/src/generate.ts +594 -0
- package/src/index.ts +456 -430
- package/llms.txt +0 -156
package/dist/index.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
} = options;
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
263
|
-
|
|
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
|
|
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
|
|
596
|
+
return fs2.readFileSync(didPath(canister), "utf-8");
|
|
269
597
|
} catch {
|
|
270
598
|
return void 0;
|
|
271
599
|
}
|
|
272
600
|
};
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
306
|
-
if (
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
);
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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 (
|
|
460
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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 ${
|
|
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) {
|