@ic-reactor/vite-plugin 0.14.0 → 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 +173 -130
- package/dist/index.cjs +782 -218
- package/dist/index.d.cts +99 -29
- package/dist/index.d.ts +99 -29
- package/dist/index.js +783 -221
- package/package.json +6 -8
- package/src/dev-environment.ts +268 -0
- package/src/env.ts +54 -20
- package/src/generate.ts +594 -0
- package/src/index.ts +665 -396
- package/llms.txt +0 -55
package/dist/index.js
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
runCanisterPipeline
|
|
5
|
-
} from "@ic-reactor/codegen";
|
|
2
|
+
import fs2 from "fs";
|
|
3
|
+
import path2 from "path";
|
|
6
4
|
|
|
7
5
|
// src/env.ts
|
|
8
|
-
import {
|
|
9
|
-
|
|
6
|
+
import { execFile } from "child_process";
|
|
7
|
+
var ICP_TIMEOUT_MS = 1e4;
|
|
8
|
+
function runIcp(args, cwd) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = execFile(
|
|
11
|
+
"icp",
|
|
12
|
+
args,
|
|
13
|
+
{
|
|
14
|
+
cwd,
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
timeout: ICP_TIMEOUT_MS,
|
|
17
|
+
windowsHide: true
|
|
18
|
+
},
|
|
19
|
+
(error, stdout, stderr) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
reject(Object.assign(error, { stderr }));
|
|
22
|
+
} else {
|
|
23
|
+
resolve(stdout);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
child.stdin?.end();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
|
|
10
31
|
const networkName = process.env.ICP_ENVIRONMENT || "local";
|
|
11
32
|
const diagnostics = [];
|
|
12
33
|
try {
|
|
13
34
|
const networkStatus = JSON.parse(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// Piping still keeps it off the terminal — it only reaches the user if
|
|
19
|
-
// the caller decides to print the diagnostics we collect below.
|
|
20
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
21
|
-
})
|
|
35
|
+
await runIcp(
|
|
36
|
+
["network", "status", "-e", networkName, "--json"],
|
|
37
|
+
projectRoot
|
|
38
|
+
)
|
|
22
39
|
);
|
|
23
40
|
const rootKey = networkStatus.root_key;
|
|
24
41
|
const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
|
|
@@ -31,15 +48,10 @@ function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
|
|
|
31
48
|
const canisterIds = {};
|
|
32
49
|
for (const name of canisterNames) {
|
|
33
50
|
try {
|
|
34
|
-
const canisterId =
|
|
35
|
-
"icp",
|
|
51
|
+
const canisterId = (await runIcp(
|
|
36
52
|
["canister", "status", name, "-e", networkName, "-i"],
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
encoding: "utf-8",
|
|
40
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
41
|
-
}
|
|
42
|
-
).trim();
|
|
53
|
+
projectRoot
|
|
54
|
+
)).trim();
|
|
43
55
|
if (canisterId) {
|
|
44
56
|
canisterIds[name] = canisterId;
|
|
45
57
|
}
|
|
@@ -100,254 +112,804 @@ function localInternetIdentityProvider(proxyTarget) {
|
|
|
100
112
|
return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
|
|
101
113
|
}
|
|
102
114
|
|
|
103
|
-
// src/
|
|
104
|
-
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
115
|
+
// src/dev-environment.ts
|
|
105
116
|
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
106
|
-
|
|
117
|
+
var DEFAULT_INTERNET_IDENTITY_PROVIDER = "http://id.ai.localhost:8000/authorize";
|
|
118
|
+
function createLocalEnvironment(options) {
|
|
107
119
|
const {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
failOnError
|
|
120
|
+
canisterNames,
|
|
121
|
+
configuredCanisterIds,
|
|
122
|
+
projectRoot,
|
|
123
|
+
onDiagnostic,
|
|
124
|
+
onUpdate
|
|
114
125
|
} = options;
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
126
|
+
const requiredNames = [...new Set(canisterNames)];
|
|
127
|
+
const lookupNames = requiredNames.includes("internet_identity") ? requiredNames : [...requiredNames, "internet_identity"];
|
|
128
|
+
let state;
|
|
129
|
+
let running;
|
|
130
|
+
const toState = (environment) => {
|
|
131
|
+
const canisterIds = {
|
|
132
|
+
...environment?.canisterIds,
|
|
133
|
+
...configuredCanisterIds
|
|
134
|
+
};
|
|
135
|
+
const missingCanisterIds = requiredNames.filter(
|
|
136
|
+
(name) => !canisterIds[name]
|
|
137
|
+
);
|
|
138
|
+
let cookie;
|
|
139
|
+
if (environment) {
|
|
140
|
+
cookie = buildIcEnvCookie(
|
|
141
|
+
canisterIds,
|
|
142
|
+
environment.rootKey,
|
|
143
|
+
environment.internetIdentityProvider
|
|
144
|
+
);
|
|
145
|
+
} else if (requiredNames.length === 0) {
|
|
146
|
+
cookie = buildIcEnvCookie(
|
|
147
|
+
{},
|
|
148
|
+
void 0,
|
|
149
|
+
DEFAULT_INTERNET_IDENTITY_PROVIDER
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
environment,
|
|
154
|
+
cookie,
|
|
155
|
+
proxyTarget: environment?.proxyTarget ?? DEFAULT_LOCAL_REPLICA,
|
|
156
|
+
missingCanisterIds,
|
|
157
|
+
complete: environment !== null && missingCanisterIds.length === 0
|
|
158
|
+
};
|
|
119
159
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
160
|
+
const run = async () => {
|
|
161
|
+
const previous = state;
|
|
162
|
+
try {
|
|
163
|
+
const { environment, diagnostics } = await getIcEnvironmentInfo(
|
|
164
|
+
lookupNames,
|
|
165
|
+
projectRoot
|
|
166
|
+
);
|
|
167
|
+
diagnostics.forEach(onDiagnostic);
|
|
168
|
+
state = toState(
|
|
169
|
+
mergeDetections(previous?.environment ?? null, environment)
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
onDiagnostic(
|
|
173
|
+
`Detecting the local IC environment failed: ${error instanceof Error ? error.message : String(error)}`
|
|
174
|
+
);
|
|
175
|
+
state = previous ?? toState(null);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
onUpdate?.(previous, state);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
onDiagnostic(
|
|
181
|
+
`Applying the detected IC environment failed: ${error instanceof Error ? error.message : String(error)}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return state;
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
get state() {
|
|
188
|
+
return state;
|
|
189
|
+
},
|
|
190
|
+
detect() {
|
|
191
|
+
running ?? (running = run().finally(() => {
|
|
192
|
+
running = void 0;
|
|
193
|
+
}));
|
|
194
|
+
return running;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function mergeDetections(previous, next) {
|
|
199
|
+
if (!next) return previous;
|
|
200
|
+
if (!previous || previous.rootKey !== next.rootKey) return next;
|
|
201
|
+
const canisterIds = { ...previous.canisterIds, ...next.canisterIds };
|
|
202
|
+
return {
|
|
203
|
+
...next,
|
|
204
|
+
canisterIds,
|
|
205
|
+
// The built-in provider stands in only for a project that has no
|
|
206
|
+
// internet_identity canister, as getIcEnvironmentInfo decides it.
|
|
207
|
+
internetIdentityProvider: canisterIds.internet_identity ? void 0 : next.internetIdentityProvider
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function isDocumentRequest(req) {
|
|
211
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
212
|
+
const { accept } = req.headers;
|
|
213
|
+
return typeof accept === "string" && accept.includes("text/html");
|
|
214
|
+
}
|
|
215
|
+
function setIcEnvCookie(res, value) {
|
|
216
|
+
const existing = res.getHeader("Set-Cookie");
|
|
217
|
+
const others = (existing === void 0 ? [] : Array.isArray(existing) ? existing : [String(existing)]).filter((cookie2) => !cookie2.startsWith("ic_env="));
|
|
218
|
+
const cookie = `ic_env=${value}; Path=/; SameSite=Lax;`;
|
|
219
|
+
res.setHeader("Set-Cookie", others.length > 0 ? [...others, cookie] : cookie);
|
|
220
|
+
}
|
|
221
|
+
function icEnvMiddleware(environment) {
|
|
222
|
+
return (req, res, next) => {
|
|
223
|
+
const respond = (state2) => {
|
|
224
|
+
if (state2?.cookie !== void 0) setIcEnvCookie(res, state2.cookie);
|
|
225
|
+
next();
|
|
226
|
+
};
|
|
227
|
+
const { state } = environment;
|
|
228
|
+
if (state?.complete || !isDocumentRequest(req)) {
|
|
229
|
+
respond(state);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
void environment.detect().then(respond);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
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
|
+
)
|
|
125
409
|
);
|
|
126
|
-
|
|
127
|
-
|
|
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()
|
|
128
464
|
);
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
136
521
|
};
|
|
137
|
-
|
|
138
|
-
|
|
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)]
|
|
139
544
|
};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
|
|
553
|
+
// src/index.ts
|
|
554
|
+
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
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
|
+
});
|
|
575
|
+
let projectRoot = process.cwd();
|
|
576
|
+
let command = "build";
|
|
577
|
+
let log = consoleLog;
|
|
578
|
+
let announced = false;
|
|
579
|
+
let localEnvironment;
|
|
580
|
+
const apiProxyOptions = /* @__PURE__ */ new Set();
|
|
581
|
+
let devServer = null;
|
|
582
|
+
const didPath = (canister) => path2.resolve(projectRoot, canister.didFile);
|
|
583
|
+
const relativeToRoot = (file) => path2.relative(projectRoot, file) || ".";
|
|
584
|
+
const generatedFrom = /* @__PURE__ */ new Map();
|
|
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) => {
|
|
595
|
+
try {
|
|
596
|
+
return fs2.readFileSync(didPath(canister), "utf-8");
|
|
597
|
+
} catch {
|
|
598
|
+
return void 0;
|
|
148
599
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
pendingFailures.delete(name);
|
|
156
|
-
server.ws.send({ type: "full-reload" });
|
|
157
|
-
} else {
|
|
158
|
-
pendingFailures.set(
|
|
159
|
-
name,
|
|
160
|
-
reportFailure(
|
|
161
|
-
server,
|
|
162
|
-
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
163
|
-
)
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
}).catch((error) => {
|
|
167
|
-
pendingFailures.set(
|
|
168
|
-
name,
|
|
169
|
-
reportFailure(
|
|
170
|
-
server,
|
|
171
|
-
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
172
|
-
error
|
|
173
|
-
)
|
|
600
|
+
};
|
|
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)])
|
|
174
606
|
);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
668
|
+
};
|
|
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");
|
|
679
|
+
};
|
|
680
|
+
const publish = (attempted, failures) => {
|
|
681
|
+
let wasFailing = false;
|
|
682
|
+
for (const { name } of attempted) {
|
|
683
|
+
if (unfixed.delete(name)) wasFailing = true;
|
|
684
|
+
}
|
|
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" });
|
|
693
|
+
}
|
|
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
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
if (typeof client?.send === "function") {
|
|
706
|
+
try {
|
|
707
|
+
client.send(JSON.stringify(payload));
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
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(", ")}`
|
|
721
|
+
);
|
|
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
|
|
733
|
+
);
|
|
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)}`);
|
|
179
749
|
}
|
|
180
750
|
});
|
|
181
|
-
|
|
182
|
-
|
|
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);
|
|
759
|
+
}
|
|
183
760
|
};
|
|
184
761
|
const plugin = {
|
|
185
762
|
name: PLUGIN_NAME,
|
|
186
763
|
enforce: "pre",
|
|
187
764
|
// Run before other plugins
|
|
188
|
-
config(userConfig, { command: viteCommand }) {
|
|
765
|
+
async config(userConfig, { command: viteCommand, mode }) {
|
|
766
|
+
announceGuide(userConfig);
|
|
189
767
|
command = viteCommand;
|
|
190
|
-
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
768
|
+
if (viteCommand !== "serve" || mode === "test" || !injectEnvironment) {
|
|
191
769
|
return {};
|
|
192
770
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
canisterNames
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
for (const diagnostic of diagnostics) {
|
|
208
|
-
debugLog(diagnostic);
|
|
209
|
-
}
|
|
210
|
-
const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
|
|
211
|
-
{},
|
|
212
|
-
void 0,
|
|
213
|
-
"http://id.ai.localhost:8000/authorize"
|
|
214
|
-
) : void 0;
|
|
215
|
-
return {
|
|
216
|
-
server: {
|
|
217
|
-
headers: envOnlyCookie ? {
|
|
218
|
-
"Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
|
|
219
|
-
} : void 0,
|
|
220
|
-
proxy: apiProxy(userConfig, DEFAULT_LOCAL_REPLICA)
|
|
771
|
+
const ownsApiProxy = !userConfig.server?.proxy?.["/api"];
|
|
772
|
+
const environment = createLocalEnvironment({
|
|
773
|
+
canisterNames: names,
|
|
774
|
+
configuredCanisterIds,
|
|
775
|
+
// `configResolved` has not run yet, so resolve the root the way Vite
|
|
776
|
+
// will. icp finds the project from the directory it starts in, and
|
|
777
|
+
// with `vite apps/web` or a `root` option that is not the process cwd.
|
|
778
|
+
projectRoot: path2.resolve(userConfig.root ?? process.cwd()),
|
|
779
|
+
onDiagnostic: debugLog,
|
|
780
|
+
onUpdate: (previous, next) => {
|
|
781
|
+
for (const proxyOptions of apiProxyOptions) {
|
|
782
|
+
proxyOptions.target = next.proxyTarget;
|
|
221
783
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
console.warn(
|
|
231
|
-
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${names}. Deploy ${it} (\`icp deploy\`) \u2014 until then the injected ic_env carries no PUBLIC_CANISTER_ID for ${it} and the app will see an undefined canister id. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
for (const diagnostic of diagnostics) {
|
|
235
|
-
debugLog(diagnostic);
|
|
236
|
-
}
|
|
237
|
-
const cookieValue = buildIcEnvCookie(
|
|
238
|
-
{
|
|
239
|
-
...icEnv.canisterIds,
|
|
240
|
-
...configuredCanisterIds
|
|
241
|
-
},
|
|
242
|
-
icEnv.rootKey,
|
|
243
|
-
icEnv.internetIdentityProvider
|
|
244
|
-
);
|
|
784
|
+
if (previous) {
|
|
785
|
+
reportDetectionProgress(previous, next, apiProxyOptions.size > 0);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
localEnvironment = environment;
|
|
790
|
+
const state = await environment.detect();
|
|
791
|
+
warnAboutIncompleteDetection(state, names.length > 0, ownsApiProxy);
|
|
245
792
|
return {
|
|
246
793
|
server: {
|
|
247
|
-
headers:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
proxy: apiProxy(userConfig,
|
|
794
|
+
// The cookie is not a static `server.headers` entry: the middleware
|
|
795
|
+
// that configureServer adds sets it per response, from the latest
|
|
796
|
+
// detection. See dev-environment.ts.
|
|
797
|
+
proxy: apiProxy(userConfig, state.proxyTarget, (proxyOptions) => {
|
|
798
|
+
if (proxyOptions.target !== state.proxyTarget) {
|
|
799
|
+
debugLog(
|
|
800
|
+
"Another plugin changed the target of the /api proxy, so the plugin leaves that proxy alone."
|
|
801
|
+
);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
apiProxyOptions.add(proxyOptions);
|
|
805
|
+
proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
|
|
806
|
+
})
|
|
251
807
|
}
|
|
252
808
|
};
|
|
253
809
|
},
|
|
254
810
|
configResolved(config) {
|
|
255
811
|
projectRoot = config.root;
|
|
256
812
|
command = config.command;
|
|
813
|
+
log = config.logger;
|
|
257
814
|
},
|
|
258
815
|
configureServer(server) {
|
|
259
816
|
devServer = server;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const failures = [...pendingFailures.values()];
|
|
263
|
-
server.ws.send({
|
|
264
|
-
type: "error",
|
|
265
|
-
err: {
|
|
266
|
-
message: failures.map((failure) => failure.message).join("\n"),
|
|
267
|
-
stack: failures.map((failure) => failure.stack).filter(Boolean).join("\n"),
|
|
268
|
-
plugin: PLUGIN_NAME
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
|
|
273
|
-
server.watcher.add(didFiles);
|
|
274
|
-
},
|
|
275
|
-
async buildStart() {
|
|
276
|
-
console.log(
|
|
277
|
-
`[ic-reactor] Generating canister bindings for ${canisters.length} canisters...`
|
|
278
|
-
);
|
|
279
|
-
const outcomes = await Promise.allSettled(
|
|
280
|
-
canisters.map(
|
|
281
|
-
(canisterConfig) => runCanisterPipeline({
|
|
282
|
-
canisterConfig,
|
|
283
|
-
projectRoot,
|
|
284
|
-
globalConfig
|
|
285
|
-
})
|
|
286
|
-
)
|
|
287
|
-
);
|
|
288
|
-
const failures = outcomes.flatMap((outcome, index) => {
|
|
289
|
-
const name = canisters[index]?.name ?? `canister #${index}`;
|
|
290
|
-
if (outcome.status === "rejected") {
|
|
291
|
-
return [{ name, detail: `${name}: ${describeError(outcome.reason)}` }];
|
|
292
|
-
}
|
|
293
|
-
if (!outcome.value.success) {
|
|
294
|
-
return [
|
|
295
|
-
{
|
|
296
|
-
name,
|
|
297
|
-
detail: `${name}: ${outcome.value.error ?? "unknown error"}`
|
|
298
|
-
}
|
|
299
|
-
];
|
|
300
|
-
}
|
|
301
|
-
return [];
|
|
302
|
-
});
|
|
303
|
-
if (failures.length === 0) {
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
const message = `Failed to generate ${failures.length} of ${canisters.length} canisters:
|
|
307
|
-
` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
|
|
308
|
-
if (failOnError ?? command === "build") {
|
|
309
|
-
this.error(`[ic-reactor] ${message}`);
|
|
310
|
-
}
|
|
311
|
-
reportFailure(devServer, message);
|
|
312
|
-
for (const { name, detail } of failures) {
|
|
313
|
-
pendingFailures.set(name, {
|
|
314
|
-
message: `[ic-reactor] Failed to generate ${detail}`,
|
|
315
|
-
stack: ""
|
|
316
|
-
});
|
|
817
|
+
if (localEnvironment) {
|
|
818
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
317
819
|
}
|
|
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);
|
|
318
826
|
},
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
827
|
+
// `vite preview` resolves the config with the `serve` command too, and
|
|
828
|
+
// used to inherit the cookie from `server.headers`.
|
|
829
|
+
configurePreviewServer(server) {
|
|
830
|
+
if (localEnvironment) {
|
|
831
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
322
832
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
833
|
+
},
|
|
834
|
+
async buildStart() {
|
|
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)
|
|
326
840
|
);
|
|
327
|
-
if (
|
|
328
|
-
|
|
841
|
+
if (!failures) return;
|
|
842
|
+
if (failures.length > 0 && (failOnError ?? command === "build")) {
|
|
843
|
+
this.error(describeFailures(failures));
|
|
329
844
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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();
|
|
336
852
|
}
|
|
337
853
|
};
|
|
338
854
|
return plugin;
|
|
339
855
|
}
|
|
340
|
-
function
|
|
856
|
+
function describe(error) {
|
|
341
857
|
return error instanceof Error ? error.message : String(error);
|
|
342
858
|
}
|
|
343
|
-
function apiProxy(userConfig, target) {
|
|
859
|
+
function apiProxy(userConfig, target, configure) {
|
|
344
860
|
if (userConfig.server?.proxy?.["/api"]) {
|
|
345
861
|
debugLog(
|
|
346
862
|
`The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
|
|
347
863
|
);
|
|
348
864
|
return void 0;
|
|
349
865
|
}
|
|
350
|
-
return {
|
|
866
|
+
return {
|
|
867
|
+
"/api": {
|
|
868
|
+
target,
|
|
869
|
+
changeOrigin: true,
|
|
870
|
+
configure: (_proxy, options) => configure(options)
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function quoteNames2(names) {
|
|
875
|
+
return names.map((name) => `"${name}"`).join(", ");
|
|
876
|
+
}
|
|
877
|
+
function warnAboutIncompleteDetection(state, hasCanisters, ownsApiProxy) {
|
|
878
|
+
if (!state.environment) {
|
|
879
|
+
if (!hasCanisters) return;
|
|
880
|
+
const proxyNote = ownsApiProxy ? ` and /api goes to ${state.proxyTarget} for now` : "";
|
|
881
|
+
console.warn(
|
|
882
|
+
`[ic-reactor] Could not detect the local IC environment, so no ic_env cookie is set${proxyNote}. Is the local network running? The plugin asks \`icp\` again on each page load until it answers${ownsApiProxy ? ", then sends /api to the network it reports" : ""}: start the network (\`icp network start\`) and reload the page. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
883
|
+
);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const missing = state.missingCanisterIds;
|
|
887
|
+
if (missing.length > 0) {
|
|
888
|
+
const it = missing.length === 1 ? "it" : "them";
|
|
889
|
+
console.warn(
|
|
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.`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function reportDetectionProgress(previous, next, followsApiProxy) {
|
|
895
|
+
if (!previous.environment && next.environment) {
|
|
896
|
+
console.log(
|
|
897
|
+
`[ic-reactor] Detected the local IC network: the ic_env cookie now carries its root key` + (followsApiProxy ? ` and /api goes to ${next.proxyTarget}.` : ".")
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
const resolved = previous.missingCanisterIds.filter(
|
|
901
|
+
(name) => !next.missingCanisterIds.includes(name)
|
|
902
|
+
);
|
|
903
|
+
if (next.environment && resolved.length > 0) {
|
|
904
|
+
console.log(
|
|
905
|
+
`[ic-reactor] The ic_env cookie now carries the canister ID${resolved.length === 1 ? "" : "s"} for ${quoteNames2(resolved)}.`
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
if (next.complete && !previous.complete) {
|
|
909
|
+
console.log(
|
|
910
|
+
"[ic-reactor] Every configured canister has an ID, so page loads no longer run `icp`. Restart the dev server after redeploying into a fresh network."
|
|
911
|
+
);
|
|
912
|
+
}
|
|
351
913
|
}
|
|
352
914
|
function debugLog(message) {
|
|
353
915
|
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|