@ic-reactor/vite-plugin 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -20
- package/dist/index.cjs +350 -113
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +354 -115
- package/llms.txt +134 -33
- package/package.json +2 -2
- package/src/dev-environment.ts +268 -0
- package/src/env.ts +54 -20
- package/src/index.ts +415 -172
package/dist/index.cjs
CHANGED
|
@@ -33,24 +33,44 @@ __export(index_exports, {
|
|
|
33
33
|
icReactor: () => icReactor
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
36
37
|
var import_node_path = __toESM(require("path"), 1);
|
|
37
38
|
var import_codegen = require("@ic-reactor/codegen");
|
|
38
39
|
|
|
39
40
|
// src/env.ts
|
|
40
41
|
var import_child_process = require("child_process");
|
|
41
|
-
|
|
42
|
+
var ICP_TIMEOUT_MS = 1e4;
|
|
43
|
+
function runIcp(args, cwd) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const child = (0, import_child_process.execFile)(
|
|
46
|
+
"icp",
|
|
47
|
+
args,
|
|
48
|
+
{
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: "utf-8",
|
|
51
|
+
timeout: ICP_TIMEOUT_MS,
|
|
52
|
+
windowsHide: true
|
|
53
|
+
},
|
|
54
|
+
(error, stdout, stderr) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
reject(Object.assign(error, { stderr }));
|
|
57
|
+
} else {
|
|
58
|
+
resolve(stdout);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
child.stdin?.end();
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
async function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
|
|
42
66
|
const networkName = process.env.ICP_ENVIRONMENT || "local";
|
|
43
67
|
const diagnostics = [];
|
|
44
68
|
try {
|
|
45
69
|
const networkStatus = JSON.parse(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Piping still keeps it off the terminal — it only reaches the user if
|
|
51
|
-
// the caller decides to print the diagnostics we collect below.
|
|
52
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
53
|
-
})
|
|
70
|
+
await runIcp(
|
|
71
|
+
["network", "status", "-e", networkName, "--json"],
|
|
72
|
+
projectRoot
|
|
73
|
+
)
|
|
54
74
|
);
|
|
55
75
|
const rootKey = networkStatus.root_key;
|
|
56
76
|
const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
|
|
@@ -63,15 +83,10 @@ function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
|
|
|
63
83
|
const canisterIds = {};
|
|
64
84
|
for (const name of canisterNames) {
|
|
65
85
|
try {
|
|
66
|
-
const canisterId = (
|
|
67
|
-
"icp",
|
|
86
|
+
const canisterId = (await runIcp(
|
|
68
87
|
["canister", "status", name, "-e", networkName, "-i"],
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
encoding: "utf-8",
|
|
72
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
73
|
-
}
|
|
74
|
-
).trim();
|
|
88
|
+
projectRoot
|
|
89
|
+
)).trim();
|
|
75
90
|
if (canisterId) {
|
|
76
91
|
canisterIds[name] = canisterId;
|
|
77
92
|
}
|
|
@@ -132,9 +147,129 @@ function localInternetIdentityProvider(proxyTarget) {
|
|
|
132
147
|
return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
|
|
133
148
|
}
|
|
134
149
|
|
|
150
|
+
// src/dev-environment.ts
|
|
151
|
+
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
152
|
+
var DEFAULT_INTERNET_IDENTITY_PROVIDER = "http://id.ai.localhost:8000/authorize";
|
|
153
|
+
function createLocalEnvironment(options) {
|
|
154
|
+
const {
|
|
155
|
+
canisterNames,
|
|
156
|
+
configuredCanisterIds,
|
|
157
|
+
projectRoot,
|
|
158
|
+
onDiagnostic,
|
|
159
|
+
onUpdate
|
|
160
|
+
} = options;
|
|
161
|
+
const requiredNames = [...new Set(canisterNames)];
|
|
162
|
+
const lookupNames = requiredNames.includes("internet_identity") ? requiredNames : [...requiredNames, "internet_identity"];
|
|
163
|
+
let state;
|
|
164
|
+
let running;
|
|
165
|
+
const toState = (environment) => {
|
|
166
|
+
const canisterIds = {
|
|
167
|
+
...environment?.canisterIds,
|
|
168
|
+
...configuredCanisterIds
|
|
169
|
+
};
|
|
170
|
+
const missingCanisterIds = requiredNames.filter(
|
|
171
|
+
(name) => !canisterIds[name]
|
|
172
|
+
);
|
|
173
|
+
let cookie;
|
|
174
|
+
if (environment) {
|
|
175
|
+
cookie = buildIcEnvCookie(
|
|
176
|
+
canisterIds,
|
|
177
|
+
environment.rootKey,
|
|
178
|
+
environment.internetIdentityProvider
|
|
179
|
+
);
|
|
180
|
+
} else if (requiredNames.length === 0) {
|
|
181
|
+
cookie = buildIcEnvCookie(
|
|
182
|
+
{},
|
|
183
|
+
void 0,
|
|
184
|
+
DEFAULT_INTERNET_IDENTITY_PROVIDER
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
environment,
|
|
189
|
+
cookie,
|
|
190
|
+
proxyTarget: environment?.proxyTarget ?? DEFAULT_LOCAL_REPLICA,
|
|
191
|
+
missingCanisterIds,
|
|
192
|
+
complete: environment !== null && missingCanisterIds.length === 0
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
const run = async () => {
|
|
196
|
+
const previous = state;
|
|
197
|
+
try {
|
|
198
|
+
const { environment, diagnostics } = await getIcEnvironmentInfo(
|
|
199
|
+
lookupNames,
|
|
200
|
+
projectRoot
|
|
201
|
+
);
|
|
202
|
+
diagnostics.forEach(onDiagnostic);
|
|
203
|
+
state = toState(
|
|
204
|
+
mergeDetections(previous?.environment ?? null, environment)
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
onDiagnostic(
|
|
208
|
+
`Detecting the local IC environment failed: ${error instanceof Error ? error.message : String(error)}`
|
|
209
|
+
);
|
|
210
|
+
state = previous ?? toState(null);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
onUpdate?.(previous, state);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
onDiagnostic(
|
|
216
|
+
`Applying the detected IC environment failed: ${error instanceof Error ? error.message : String(error)}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return state;
|
|
220
|
+
};
|
|
221
|
+
return {
|
|
222
|
+
get state() {
|
|
223
|
+
return state;
|
|
224
|
+
},
|
|
225
|
+
detect() {
|
|
226
|
+
running ?? (running = run().finally(() => {
|
|
227
|
+
running = void 0;
|
|
228
|
+
}));
|
|
229
|
+
return running;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function mergeDetections(previous, next) {
|
|
234
|
+
if (!next) return previous;
|
|
235
|
+
if (!previous || previous.rootKey !== next.rootKey) return next;
|
|
236
|
+
const canisterIds = { ...previous.canisterIds, ...next.canisterIds };
|
|
237
|
+
return {
|
|
238
|
+
...next,
|
|
239
|
+
canisterIds,
|
|
240
|
+
// The built-in provider stands in only for a project that has no
|
|
241
|
+
// internet_identity canister, as getIcEnvironmentInfo decides it.
|
|
242
|
+
internetIdentityProvider: canisterIds.internet_identity ? void 0 : next.internetIdentityProvider
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function isDocumentRequest(req) {
|
|
246
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
247
|
+
const { accept } = req.headers;
|
|
248
|
+
return typeof accept === "string" && accept.includes("text/html");
|
|
249
|
+
}
|
|
250
|
+
function setIcEnvCookie(res, value) {
|
|
251
|
+
const existing = res.getHeader("Set-Cookie");
|
|
252
|
+
const others = (existing === void 0 ? [] : Array.isArray(existing) ? existing : [String(existing)]).filter((cookie2) => !cookie2.startsWith("ic_env="));
|
|
253
|
+
const cookie = `ic_env=${value}; Path=/; SameSite=Lax;`;
|
|
254
|
+
res.setHeader("Set-Cookie", others.length > 0 ? [...others, cookie] : cookie);
|
|
255
|
+
}
|
|
256
|
+
function icEnvMiddleware(environment) {
|
|
257
|
+
return (req, res, next) => {
|
|
258
|
+
const respond = (state2) => {
|
|
259
|
+
if (state2?.cookie !== void 0) setIcEnvCookie(res, state2.cookie);
|
|
260
|
+
next();
|
|
261
|
+
};
|
|
262
|
+
const { state } = environment;
|
|
263
|
+
if (state?.complete || !isDocumentRequest(req)) {
|
|
264
|
+
respond(state);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
void environment.detect().then(respond);
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
135
271
|
// src/index.ts
|
|
136
272
|
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
137
|
-
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
138
273
|
function icReactor(options) {
|
|
139
274
|
const {
|
|
140
275
|
canisters,
|
|
@@ -151,10 +286,29 @@ function icReactor(options) {
|
|
|
151
286
|
};
|
|
152
287
|
let projectRoot = process.cwd();
|
|
153
288
|
let command = "build";
|
|
289
|
+
let localEnvironment;
|
|
290
|
+
const apiProxyOptions = /* @__PURE__ */ new Set();
|
|
154
291
|
let devServer = null;
|
|
155
292
|
const resolveDidPath = (didFile) => import_node_path.default.normalize(
|
|
156
293
|
import_node_path.default.isAbsolute(didFile) ? didFile : import_node_path.default.resolve(projectRoot, didFile)
|
|
157
294
|
);
|
|
295
|
+
const generatedFrom = /* @__PURE__ */ new Map();
|
|
296
|
+
const readDidSource = (canister) => {
|
|
297
|
+
try {
|
|
298
|
+
return import_node_fs.default.readFileSync(resolveDidPath(canister.didFile), "utf-8");
|
|
299
|
+
} catch {
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
const describeEntry = (canister) => `canisters[${canisters.indexOf(canister)}] (${JSON.stringify(canister.name)})`;
|
|
304
|
+
const sharedOutDirError = (canister) => {
|
|
305
|
+
const first = (0, import_codegen.findSharedOutDirs)(
|
|
306
|
+
canisters.map((entry) => [entry, entry]),
|
|
307
|
+
outDir,
|
|
308
|
+
projectRoot
|
|
309
|
+
).get(canister);
|
|
310
|
+
return first === void 0 ? void 0 : (0, import_codegen.sharedOutDirMessage)(describeEntry(canister), describeEntry(first));
|
|
311
|
+
};
|
|
158
312
|
const configuredCanisterIds = Object.fromEntries(
|
|
159
313
|
canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
|
|
160
314
|
);
|
|
@@ -173,22 +327,31 @@ function icReactor(options) {
|
|
|
173
327
|
const rerunQueued = /* @__PURE__ */ new Set();
|
|
174
328
|
const regenerate = (canisterConfig, server) => {
|
|
175
329
|
const { name } = canisterConfig;
|
|
176
|
-
const running = inFlight.get(
|
|
330
|
+
const running = inFlight.get(canisterConfig);
|
|
177
331
|
if (running) {
|
|
178
|
-
rerunQueued.add(
|
|
332
|
+
rerunQueued.add(canisterConfig);
|
|
179
333
|
return running;
|
|
180
334
|
}
|
|
335
|
+
const sharedError = sharedOutDirError(canisterConfig);
|
|
336
|
+
if (sharedError !== void 0) {
|
|
337
|
+
pendingFailures.set(
|
|
338
|
+
canisterConfig,
|
|
339
|
+
reportFailure(server, `Regeneration failed for ${sharedError}`)
|
|
340
|
+
);
|
|
341
|
+
return Promise.resolve();
|
|
342
|
+
}
|
|
181
343
|
const run = (0, import_codegen.runCanisterPipeline)({
|
|
182
344
|
canisterConfig,
|
|
183
345
|
projectRoot,
|
|
184
346
|
globalConfig
|
|
185
347
|
}).then((result) => {
|
|
348
|
+
reportWarnings(name, result.warnings);
|
|
186
349
|
if (result.success) {
|
|
187
|
-
pendingFailures.delete(
|
|
350
|
+
pendingFailures.delete(canisterConfig);
|
|
188
351
|
server.ws.send({ type: "full-reload" });
|
|
189
352
|
} else {
|
|
190
353
|
pendingFailures.set(
|
|
191
|
-
|
|
354
|
+
canisterConfig,
|
|
192
355
|
reportFailure(
|
|
193
356
|
server,
|
|
194
357
|
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
@@ -197,7 +360,7 @@ function icReactor(options) {
|
|
|
197
360
|
}
|
|
198
361
|
}).catch((error) => {
|
|
199
362
|
pendingFailures.set(
|
|
200
|
-
|
|
363
|
+
canisterConfig,
|
|
201
364
|
reportFailure(
|
|
202
365
|
server,
|
|
203
366
|
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
@@ -205,81 +368,77 @@ function icReactor(options) {
|
|
|
205
368
|
)
|
|
206
369
|
);
|
|
207
370
|
}).finally(() => {
|
|
208
|
-
inFlight.delete(
|
|
209
|
-
if (rerunQueued.delete(
|
|
371
|
+
inFlight.delete(canisterConfig);
|
|
372
|
+
if (rerunQueued.delete(canisterConfig)) {
|
|
210
373
|
void regenerate(canisterConfig, server);
|
|
211
374
|
}
|
|
212
375
|
});
|
|
213
|
-
inFlight.set(
|
|
376
|
+
inFlight.set(canisterConfig, run);
|
|
214
377
|
return run;
|
|
215
378
|
};
|
|
379
|
+
const regenerateForDid = (file, server) => {
|
|
380
|
+
if (!file.endsWith(".did")) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const changedPath = import_node_path.default.normalize(file);
|
|
384
|
+
const affectedCanisters = canisters.filter(
|
|
385
|
+
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
386
|
+
);
|
|
387
|
+
if (affectedCanisters.length === 0) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
console.log(
|
|
391
|
+
`[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
|
|
392
|
+
);
|
|
393
|
+
for (const canister of affectedCanisters) {
|
|
394
|
+
void regenerate(canister, server);
|
|
395
|
+
}
|
|
396
|
+
};
|
|
216
397
|
const plugin = {
|
|
217
398
|
name: PLUGIN_NAME,
|
|
218
399
|
enforce: "pre",
|
|
219
400
|
// Run before other plugins
|
|
220
|
-
config(userConfig, { command: viteCommand }) {
|
|
401
|
+
async config(userConfig, { command: viteCommand }) {
|
|
221
402
|
command = viteCommand;
|
|
222
403
|
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
223
404
|
return {};
|
|
224
405
|
}
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
canisterNames.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
for (const diagnostic of diagnostics) {
|
|
240
|
-
debugLog(diagnostic);
|
|
241
|
-
}
|
|
242
|
-
const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
|
|
243
|
-
{},
|
|
244
|
-
void 0,
|
|
245
|
-
"http://id.ai.localhost:8000/authorize"
|
|
246
|
-
) : void 0;
|
|
247
|
-
return {
|
|
248
|
-
server: {
|
|
249
|
-
headers: envOnlyCookie ? {
|
|
250
|
-
"Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
|
|
251
|
-
} : void 0,
|
|
252
|
-
proxy: apiProxy(userConfig, DEFAULT_LOCAL_REPLICA)
|
|
406
|
+
const ownsApiProxy = !userConfig.server?.proxy?.["/api"];
|
|
407
|
+
const environment = createLocalEnvironment({
|
|
408
|
+
canisterNames: canisters.map((canister) => canister.name).filter((name) => !!name),
|
|
409
|
+
configuredCanisterIds,
|
|
410
|
+
// `configResolved` has not run yet, so resolve the root the way Vite
|
|
411
|
+
// will. icp finds the project from the directory it starts in, and
|
|
412
|
+
// with `vite apps/web` or a `root` option that is not the process cwd.
|
|
413
|
+
projectRoot: import_node_path.default.resolve(userConfig.root ?? process.cwd()),
|
|
414
|
+
onDiagnostic: debugLog,
|
|
415
|
+
onUpdate: (previous, next) => {
|
|
416
|
+
for (const proxyOptions of apiProxyOptions) {
|
|
417
|
+
proxyOptions.target = next.proxyTarget;
|
|
253
418
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
console.warn(
|
|
263
|
-
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${names}. Deploy ${it} (\`icp deploy\`) \u2014 until then the injected ic_env carries no PUBLIC_CANISTER_ID for ${it} and the app will see an undefined canister id. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
for (const diagnostic of diagnostics) {
|
|
267
|
-
debugLog(diagnostic);
|
|
268
|
-
}
|
|
269
|
-
const cookieValue = buildIcEnvCookie(
|
|
270
|
-
{
|
|
271
|
-
...icEnv.canisterIds,
|
|
272
|
-
...configuredCanisterIds
|
|
273
|
-
},
|
|
274
|
-
icEnv.rootKey,
|
|
275
|
-
icEnv.internetIdentityProvider
|
|
276
|
-
);
|
|
419
|
+
if (previous) {
|
|
420
|
+
reportDetectionProgress(previous, next, apiProxyOptions.size > 0);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
localEnvironment = environment;
|
|
425
|
+
const state = await environment.detect();
|
|
426
|
+
warnAboutIncompleteDetection(state, canisters.length > 0, ownsApiProxy);
|
|
277
427
|
return {
|
|
278
428
|
server: {
|
|
279
|
-
headers:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
proxy: apiProxy(userConfig,
|
|
429
|
+
// The cookie is not a static `server.headers` entry: the middleware
|
|
430
|
+
// that configureServer adds sets it per response, from the latest
|
|
431
|
+
// detection. See dev-environment.ts.
|
|
432
|
+
proxy: apiProxy(userConfig, state.proxyTarget, (proxyOptions) => {
|
|
433
|
+
if (proxyOptions.target !== state.proxyTarget) {
|
|
434
|
+
debugLog(
|
|
435
|
+
"Another plugin changed the target of the /api proxy, so the plugin leaves that proxy alone."
|
|
436
|
+
);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
apiProxyOptions.add(proxyOptions);
|
|
440
|
+
proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
|
|
441
|
+
})
|
|
283
442
|
}
|
|
284
443
|
};
|
|
285
444
|
},
|
|
@@ -289,6 +448,9 @@ function icReactor(options) {
|
|
|
289
448
|
},
|
|
290
449
|
configureServer(server) {
|
|
291
450
|
devServer = server;
|
|
451
|
+
if (localEnvironment) {
|
|
452
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
453
|
+
}
|
|
292
454
|
server.ws.on?.("connection", () => {
|
|
293
455
|
if (pendingFailures.size === 0) return;
|
|
294
456
|
const failures = [...pendingFailures.values()];
|
|
@@ -303,29 +465,69 @@ function icReactor(options) {
|
|
|
303
465
|
});
|
|
304
466
|
const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
|
|
305
467
|
server.watcher.add(didFiles);
|
|
468
|
+
const onDidEvent = (file) => regenerateForDid(file, server);
|
|
469
|
+
server.watcher.on("change", onDidEvent);
|
|
470
|
+
server.watcher.on("add", onDidEvent);
|
|
471
|
+
},
|
|
472
|
+
// `vite preview` resolves the config with the `serve` command too, and
|
|
473
|
+
// used to inherit the cookie from `server.headers`.
|
|
474
|
+
configurePreviewServer(server) {
|
|
475
|
+
if (localEnvironment) {
|
|
476
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
477
|
+
}
|
|
306
478
|
},
|
|
307
479
|
async buildStart() {
|
|
480
|
+
for (const canister of canisters) {
|
|
481
|
+
if (typeof canister.didFile === "string") {
|
|
482
|
+
this.addWatchFile(resolveDidPath(canister.didFile));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
const sources = canisters.map(readDidSource);
|
|
486
|
+
const pending = canisters.filter(
|
|
487
|
+
(canister, index) => !this.meta.watchMode || sources[index] === void 0 || generatedFrom.get(canister) !== sources[index]
|
|
488
|
+
);
|
|
489
|
+
if (pending.length === 0) {
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
308
492
|
console.log(
|
|
309
|
-
`[ic-reactor] Generating canister bindings for ${
|
|
493
|
+
`[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
|
|
310
494
|
);
|
|
311
495
|
const outcomes = await Promise.allSettled(
|
|
312
|
-
|
|
313
|
-
|
|
496
|
+
pending.map((canisterConfig) => {
|
|
497
|
+
const sharedError = sharedOutDirError(canisterConfig);
|
|
498
|
+
if (sharedError !== void 0) {
|
|
499
|
+
return Promise.reject(new SharedOutDirError(sharedError));
|
|
500
|
+
}
|
|
501
|
+
return (0, import_codegen.runCanisterPipeline)({
|
|
314
502
|
canisterConfig,
|
|
315
503
|
projectRoot,
|
|
316
504
|
globalConfig
|
|
317
|
-
})
|
|
318
|
-
)
|
|
505
|
+
});
|
|
506
|
+
})
|
|
319
507
|
);
|
|
508
|
+
outcomes.forEach((outcome, index) => {
|
|
509
|
+
const canister = pending[index];
|
|
510
|
+
const source = sources[canisters.indexOf(canister)];
|
|
511
|
+
if (outcome.status === "fulfilled") {
|
|
512
|
+
reportWarnings(canister.name, outcome.value.warnings);
|
|
513
|
+
}
|
|
514
|
+
if (outcome.status === "fulfilled" && outcome.value.success && source !== void 0) {
|
|
515
|
+
generatedFrom.set(canister, source);
|
|
516
|
+
} else {
|
|
517
|
+
generatedFrom.delete(canister);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
320
520
|
const failures = outcomes.flatMap((outcome, index) => {
|
|
321
|
-
const
|
|
521
|
+
const canister = pending[index];
|
|
522
|
+
const name = canister?.name ?? `canister #${index}`;
|
|
322
523
|
if (outcome.status === "rejected") {
|
|
323
|
-
|
|
524
|
+
const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
|
|
525
|
+
return [{ canister, detail }];
|
|
324
526
|
}
|
|
325
527
|
if (!outcome.value.success) {
|
|
326
528
|
return [
|
|
327
529
|
{
|
|
328
|
-
|
|
530
|
+
canister,
|
|
329
531
|
detail: `${name}: ${outcome.value.error ?? "unknown error"}`
|
|
330
532
|
}
|
|
331
533
|
];
|
|
@@ -335,51 +537,86 @@ function icReactor(options) {
|
|
|
335
537
|
if (failures.length === 0) {
|
|
336
538
|
return;
|
|
337
539
|
}
|
|
338
|
-
const message = `Failed to generate ${failures.length} of ${
|
|
540
|
+
const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
|
|
339
541
|
` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
|
|
340
542
|
if (failOnError ?? command === "build") {
|
|
341
543
|
this.error(`[ic-reactor] ${message}`);
|
|
342
544
|
}
|
|
343
545
|
reportFailure(devServer, message);
|
|
344
|
-
for (const {
|
|
345
|
-
pendingFailures.set(
|
|
546
|
+
for (const { canister, detail } of failures) {
|
|
547
|
+
pendingFailures.set(canister, {
|
|
346
548
|
message: `[ic-reactor] Failed to generate ${detail}`,
|
|
347
549
|
stack: ""
|
|
348
550
|
});
|
|
349
551
|
}
|
|
350
|
-
},
|
|
351
|
-
handleHotUpdate({ file, server }) {
|
|
352
|
-
if (!file.endsWith(".did")) {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
const changedPath = import_node_path.default.normalize(file);
|
|
356
|
-
const affectedCanisters = canisters.filter(
|
|
357
|
-
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
358
|
-
);
|
|
359
|
-
if (affectedCanisters.length === 0) {
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
console.log(
|
|
363
|
-
`[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
|
|
364
|
-
);
|
|
365
|
-
return Promise.all(
|
|
366
|
-
affectedCanisters.map((canister) => regenerate(canister, server))
|
|
367
|
-
).then(() => void 0);
|
|
368
552
|
}
|
|
369
553
|
};
|
|
370
554
|
return plugin;
|
|
371
555
|
}
|
|
556
|
+
var SharedOutDirError = class extends Error {
|
|
557
|
+
};
|
|
558
|
+
function reportWarnings(name, warnings) {
|
|
559
|
+
for (const warning of warnings ?? []) {
|
|
560
|
+
console.warn(`[ic-reactor] ${name}: ${warning}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
372
563
|
function describeError(error) {
|
|
373
564
|
return error instanceof Error ? error.message : String(error);
|
|
374
565
|
}
|
|
375
|
-
function apiProxy(userConfig, target) {
|
|
566
|
+
function apiProxy(userConfig, target, configure) {
|
|
376
567
|
if (userConfig.server?.proxy?.["/api"]) {
|
|
377
568
|
debugLog(
|
|
378
569
|
`The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
|
|
379
570
|
);
|
|
380
571
|
return void 0;
|
|
381
572
|
}
|
|
382
|
-
return {
|
|
573
|
+
return {
|
|
574
|
+
"/api": {
|
|
575
|
+
target,
|
|
576
|
+
changeOrigin: true,
|
|
577
|
+
configure: (_proxy, options) => configure(options)
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function quoteNames(names) {
|
|
582
|
+
return names.map((name) => `"${name}"`).join(", ");
|
|
583
|
+
}
|
|
584
|
+
function warnAboutIncompleteDetection(state, hasCanisters, ownsApiProxy) {
|
|
585
|
+
if (!state.environment) {
|
|
586
|
+
if (!hasCanisters) return;
|
|
587
|
+
const proxyNote = ownsApiProxy ? ` and /api goes to ${state.proxyTarget} for now` : "";
|
|
588
|
+
console.warn(
|
|
589
|
+
`[ic-reactor] Could not detect the local IC environment, so no ic_env cookie is set${proxyNote}. Is the local network running? The plugin asks \`icp\` again on each page load until it answers${ownsApiProxy ? ", then sends /api to the network it reports" : ""}: start the network (\`icp network start\`) and reload the page. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
590
|
+
);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const missing = state.missingCanisterIds;
|
|
594
|
+
if (missing.length > 0) {
|
|
595
|
+
const it = missing.length === 1 ? "it" : "them";
|
|
596
|
+
console.warn(
|
|
597
|
+
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${quoteNames(missing)}. Until one is, the ic_env cookie carries no PUBLIC_CANISTER_ID for ${it} and the app will see an undefined canister id. Deploy ${it} (\`icp deploy\`) and reload the page: the plugin asks \`icp\` again on each page load until every configured canister has an ID. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function reportDetectionProgress(previous, next, followsApiProxy) {
|
|
602
|
+
if (!previous.environment && next.environment) {
|
|
603
|
+
console.log(
|
|
604
|
+
`[ic-reactor] Detected the local IC network: the ic_env cookie now carries its root key` + (followsApiProxy ? ` and /api goes to ${next.proxyTarget}.` : ".")
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const resolved = previous.missingCanisterIds.filter(
|
|
608
|
+
(name) => !next.missingCanisterIds.includes(name)
|
|
609
|
+
);
|
|
610
|
+
if (next.environment && resolved.length > 0) {
|
|
611
|
+
console.log(
|
|
612
|
+
`[ic-reactor] The ic_env cookie now carries the canister ID${resolved.length === 1 ? "" : "s"} for ${quoteNames(resolved)}.`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (next.complete && !previous.complete) {
|
|
616
|
+
console.log(
|
|
617
|
+
"[ic-reactor] Every configured canister has an ID, so page loads no longer run `icp`. Restart the dev server after redeploying into a fresh network."
|
|
618
|
+
);
|
|
619
|
+
}
|
|
383
620
|
}
|
|
384
621
|
function debugLog(message) {
|
|
385
622
|
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|
package/dist/index.d.cts
CHANGED
|
@@ -13,7 +13,9 @@ import { CanisterConfig, CodegenTarget } from '@ic-reactor/codegen';
|
|
|
13
13
|
interface IcReactorPluginOptions {
|
|
14
14
|
/**
|
|
15
15
|
* Canister configurations.
|
|
16
|
-
* `name` is required for each canister.
|
|
16
|
+
* `name` is required for each canister. Set `factories: true` on an entry to
|
|
17
|
+
* also generate `index.factories.generated.ts`, a query or mutation object
|
|
18
|
+
* per method bound to the generated reactor; see `CanisterConfig.factories`.
|
|
17
19
|
*/
|
|
18
20
|
canisters: CanisterConfig[];
|
|
19
21
|
/**
|
|
@@ -32,7 +34,17 @@ interface IcReactorPluginOptions {
|
|
|
32
34
|
*/
|
|
33
35
|
target?: CodegenTarget;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
37
|
+
* Inject the local IC environment under `vite dev` and `vite preview`: set
|
|
38
|
+
* the `ic_env` cookie on each response and proxy `/api` to the network the
|
|
39
|
+
* `icp` CLI reports.
|
|
40
|
+
*
|
|
41
|
+
* Until `icp` reports a network and every configured canister has an ID
|
|
42
|
+
* (a configured `canisterId` counts), each page load asks `icp` again, so a
|
|
43
|
+
* deploy after the server started needs only a reload. Once detection is
|
|
44
|
+
* complete, page loads run no `icp` command, and a redeploy into a fresh
|
|
45
|
+
* network needs a restart. An `/api` proxy that the Vite config or another
|
|
46
|
+
* plugin sets is left alone.
|
|
47
|
+
*
|
|
36
48
|
* Default: true
|
|
37
49
|
*/
|
|
38
50
|
injectEnvironment?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ import { CanisterConfig, CodegenTarget } from '@ic-reactor/codegen';
|
|
|
13
13
|
interface IcReactorPluginOptions {
|
|
14
14
|
/**
|
|
15
15
|
* Canister configurations.
|
|
16
|
-
* `name` is required for each canister.
|
|
16
|
+
* `name` is required for each canister. Set `factories: true` on an entry to
|
|
17
|
+
* also generate `index.factories.generated.ts`, a query or mutation object
|
|
18
|
+
* per method bound to the generated reactor; see `CanisterConfig.factories`.
|
|
17
19
|
*/
|
|
18
20
|
canisters: CanisterConfig[];
|
|
19
21
|
/**
|
|
@@ -32,7 +34,17 @@ interface IcReactorPluginOptions {
|
|
|
32
34
|
*/
|
|
33
35
|
target?: CodegenTarget;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
37
|
+
* Inject the local IC environment under `vite dev` and `vite preview`: set
|
|
38
|
+
* the `ic_env` cookie on each response and proxy `/api` to the network the
|
|
39
|
+
* `icp` CLI reports.
|
|
40
|
+
*
|
|
41
|
+
* Until `icp` reports a network and every configured canister has an ID
|
|
42
|
+
* (a configured `canisterId` counts), each page load asks `icp` again, so a
|
|
43
|
+
* deploy after the server started needs only a reload. Once detection is
|
|
44
|
+
* complete, page loads run no `icp` command, and a redeploy into a fresh
|
|
45
|
+
* network needs a restart. An `/api` proxy that the Vite config or another
|
|
46
|
+
* plugin sets is left alone.
|
|
47
|
+
*
|
|
36
48
|
* Default: true
|
|
37
49
|
*/
|
|
38
50
|
injectEnvironment?: boolean;
|