@ic-reactor/vite-plugin 0.13.1 → 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 +97 -13
- package/dist/index.cjs +385 -118
- package/dist/index.d.cts +14 -2
- package/dist/index.d.ts +14 -2
- package/dist/index.js +389 -120
- package/llms.txt +134 -33
- package/package.json +4 -4
- package/src/dev-environment.ts +268 -0
- package/src/env.ts +60 -16
- package/src/index.ts +476 -178
package/dist/index.cjs
CHANGED
|
@@ -33,23 +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
|
-
// the caller decides to print the diagnostics we collect below.
|
|
51
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
52
|
-
})
|
|
70
|
+
await runIcp(
|
|
71
|
+
["network", "status", "-e", networkName, "--json"],
|
|
72
|
+
projectRoot
|
|
73
|
+
)
|
|
53
74
|
);
|
|
54
75
|
const rootKey = networkStatus.root_key;
|
|
55
76
|
const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
|
|
@@ -62,11 +83,10 @@ function getIcEnvironmentInfo(canisterNames) {
|
|
|
62
83
|
const canisterIds = {};
|
|
63
84
|
for (const name of canisterNames) {
|
|
64
85
|
try {
|
|
65
|
-
const canisterId = (
|
|
66
|
-
"icp",
|
|
86
|
+
const canisterId = (await runIcp(
|
|
67
87
|
["canister", "status", name, "-e", networkName, "-i"],
|
|
68
|
-
|
|
69
|
-
).trim();
|
|
88
|
+
projectRoot
|
|
89
|
+
)).trim();
|
|
70
90
|
if (canisterId) {
|
|
71
91
|
canisterIds[name] = canisterId;
|
|
72
92
|
}
|
|
@@ -127,9 +147,129 @@ function localInternetIdentityProvider(proxyTarget) {
|
|
|
127
147
|
return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
|
|
128
148
|
}
|
|
129
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
|
+
|
|
130
271
|
// src/index.ts
|
|
131
272
|
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
132
|
-
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
133
273
|
function icReactor(options) {
|
|
134
274
|
const {
|
|
135
275
|
canisters,
|
|
@@ -146,14 +286,33 @@ function icReactor(options) {
|
|
|
146
286
|
};
|
|
147
287
|
let projectRoot = process.cwd();
|
|
148
288
|
let command = "build";
|
|
289
|
+
let localEnvironment;
|
|
290
|
+
const apiProxyOptions = /* @__PURE__ */ new Set();
|
|
149
291
|
let devServer = null;
|
|
150
292
|
const resolveDidPath = (didFile) => import_node_path.default.normalize(
|
|
151
293
|
import_node_path.default.isAbsolute(didFile) ? didFile : import_node_path.default.resolve(projectRoot, didFile)
|
|
152
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
|
+
};
|
|
153
312
|
const configuredCanisterIds = Object.fromEntries(
|
|
154
313
|
canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
|
|
155
314
|
);
|
|
156
|
-
|
|
315
|
+
const pendingFailures = /* @__PURE__ */ new Map();
|
|
157
316
|
const reportFailure = (server, message, cause) => {
|
|
158
317
|
console.error(`[ic-reactor] ${message}`);
|
|
159
318
|
const err = {
|
|
@@ -161,121 +320,125 @@ function icReactor(options) {
|
|
|
161
320
|
stack: cause instanceof Error && cause.stack ? cause.stack : "",
|
|
162
321
|
plugin: PLUGIN_NAME
|
|
163
322
|
};
|
|
164
|
-
pendingFailure = { message: err.message, stack: err.stack };
|
|
165
323
|
server?.ws.send({ type: "error", err });
|
|
324
|
+
return { message: err.message, stack: err.stack };
|
|
166
325
|
};
|
|
167
326
|
const inFlight = /* @__PURE__ */ new Map();
|
|
168
327
|
const rerunQueued = /* @__PURE__ */ new Set();
|
|
169
328
|
const regenerate = (canisterConfig, server) => {
|
|
170
329
|
const { name } = canisterConfig;
|
|
171
|
-
const running = inFlight.get(
|
|
330
|
+
const running = inFlight.get(canisterConfig);
|
|
172
331
|
if (running) {
|
|
173
|
-
rerunQueued.add(
|
|
332
|
+
rerunQueued.add(canisterConfig);
|
|
174
333
|
return running;
|
|
175
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
|
+
}
|
|
176
343
|
const run = (0, import_codegen.runCanisterPipeline)({
|
|
177
344
|
canisterConfig,
|
|
178
345
|
projectRoot,
|
|
179
346
|
globalConfig
|
|
180
347
|
}).then((result) => {
|
|
348
|
+
reportWarnings(name, result.warnings);
|
|
181
349
|
if (result.success) {
|
|
182
|
-
|
|
350
|
+
pendingFailures.delete(canisterConfig);
|
|
183
351
|
server.ws.send({ type: "full-reload" });
|
|
184
352
|
} else {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
353
|
+
pendingFailures.set(
|
|
354
|
+
canisterConfig,
|
|
355
|
+
reportFailure(
|
|
356
|
+
server,
|
|
357
|
+
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
358
|
+
)
|
|
188
359
|
);
|
|
189
360
|
}
|
|
190
361
|
}).catch((error) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
362
|
+
pendingFailures.set(
|
|
363
|
+
canisterConfig,
|
|
364
|
+
reportFailure(
|
|
365
|
+
server,
|
|
366
|
+
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
367
|
+
error
|
|
368
|
+
)
|
|
195
369
|
);
|
|
196
370
|
}).finally(() => {
|
|
197
|
-
inFlight.delete(
|
|
198
|
-
if (rerunQueued.delete(
|
|
371
|
+
inFlight.delete(canisterConfig);
|
|
372
|
+
if (rerunQueued.delete(canisterConfig)) {
|
|
199
373
|
void regenerate(canisterConfig, server);
|
|
200
374
|
}
|
|
201
375
|
});
|
|
202
|
-
inFlight.set(
|
|
376
|
+
inFlight.set(canisterConfig, run);
|
|
203
377
|
return run;
|
|
204
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
|
+
};
|
|
205
397
|
const plugin = {
|
|
206
398
|
name: PLUGIN_NAME,
|
|
207
399
|
enforce: "pre",
|
|
208
400
|
// Run before other plugins
|
|
209
|
-
config(
|
|
401
|
+
async config(userConfig, { command: viteCommand }) {
|
|
210
402
|
command = viteCommand;
|
|
211
403
|
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
212
404
|
return {};
|
|
213
405
|
}
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
canisterNames.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
debugLog(diagnostic);
|
|
227
|
-
}
|
|
228
|
-
const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
|
|
229
|
-
{},
|
|
230
|
-
void 0,
|
|
231
|
-
"http://id.ai.localhost:8000/authorize"
|
|
232
|
-
) : void 0;
|
|
233
|
-
return {
|
|
234
|
-
server: {
|
|
235
|
-
headers: envOnlyCookie ? {
|
|
236
|
-
"Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
|
|
237
|
-
} : void 0,
|
|
238
|
-
proxy: {
|
|
239
|
-
"/api": {
|
|
240
|
-
target: DEFAULT_LOCAL_REPLICA,
|
|
241
|
-
changeOrigin: true
|
|
242
|
-
}
|
|
243
|
-
}
|
|
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;
|
|
244
418
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.warn(
|
|
254
|
-
`[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.`
|
|
255
|
-
);
|
|
256
|
-
}
|
|
257
|
-
for (const diagnostic of diagnostics) {
|
|
258
|
-
debugLog(diagnostic);
|
|
259
|
-
}
|
|
260
|
-
const cookieValue = buildIcEnvCookie(
|
|
261
|
-
{
|
|
262
|
-
...icEnv.canisterIds,
|
|
263
|
-
...configuredCanisterIds
|
|
264
|
-
},
|
|
265
|
-
icEnv.rootKey,
|
|
266
|
-
icEnv.internetIdentityProvider
|
|
267
|
-
);
|
|
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);
|
|
268
427
|
return {
|
|
269
428
|
server: {
|
|
270
|
-
headers:
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
proxy: {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
277
438
|
}
|
|
278
|
-
|
|
439
|
+
apiProxyOptions.add(proxyOptions);
|
|
440
|
+
proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
|
|
441
|
+
})
|
|
279
442
|
}
|
|
280
443
|
};
|
|
281
444
|
},
|
|
@@ -285,72 +448,176 @@ function icReactor(options) {
|
|
|
285
448
|
},
|
|
286
449
|
configureServer(server) {
|
|
287
450
|
devServer = server;
|
|
451
|
+
if (localEnvironment) {
|
|
452
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
453
|
+
}
|
|
288
454
|
server.ws.on?.("connection", () => {
|
|
289
|
-
if (
|
|
455
|
+
if (pendingFailures.size === 0) return;
|
|
456
|
+
const failures = [...pendingFailures.values()];
|
|
290
457
|
server.ws.send({
|
|
291
458
|
type: "error",
|
|
292
|
-
err: {
|
|
459
|
+
err: {
|
|
460
|
+
message: failures.map((failure) => failure.message).join("\n"),
|
|
461
|
+
stack: failures.map((failure) => failure.stack).filter(Boolean).join("\n"),
|
|
462
|
+
plugin: PLUGIN_NAME
|
|
463
|
+
}
|
|
293
464
|
});
|
|
294
465
|
});
|
|
295
466
|
const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
|
|
296
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
|
+
}
|
|
297
478
|
},
|
|
298
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
|
+
}
|
|
299
492
|
console.log(
|
|
300
|
-
`[ic-reactor] Generating canister bindings for ${
|
|
493
|
+
`[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
|
|
301
494
|
);
|
|
302
495
|
const outcomes = await Promise.allSettled(
|
|
303
|
-
|
|
304
|
-
|
|
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)({
|
|
305
502
|
canisterConfig,
|
|
306
503
|
projectRoot,
|
|
307
504
|
globalConfig
|
|
308
|
-
})
|
|
309
|
-
)
|
|
505
|
+
});
|
|
506
|
+
})
|
|
310
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
|
+
});
|
|
311
520
|
const failures = outcomes.flatMap((outcome, index) => {
|
|
312
|
-
const
|
|
521
|
+
const canister = pending[index];
|
|
522
|
+
const name = canister?.name ?? `canister #${index}`;
|
|
313
523
|
if (outcome.status === "rejected") {
|
|
314
|
-
|
|
524
|
+
const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
|
|
525
|
+
return [{ canister, detail }];
|
|
315
526
|
}
|
|
316
527
|
if (!outcome.value.success) {
|
|
317
|
-
return [
|
|
528
|
+
return [
|
|
529
|
+
{
|
|
530
|
+
canister,
|
|
531
|
+
detail: `${name}: ${outcome.value.error ?? "unknown error"}`
|
|
532
|
+
}
|
|
533
|
+
];
|
|
318
534
|
}
|
|
319
535
|
return [];
|
|
320
536
|
});
|
|
321
537
|
if (failures.length === 0) {
|
|
322
538
|
return;
|
|
323
539
|
}
|
|
324
|
-
const message = `Failed to generate ${failures.length} of ${
|
|
325
|
-
` + failures.map((
|
|
540
|
+
const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
|
|
541
|
+
` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
|
|
326
542
|
if (failOnError ?? command === "build") {
|
|
327
543
|
this.error(`[ic-reactor] ${message}`);
|
|
328
544
|
}
|
|
329
545
|
reportFailure(devServer, message);
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const changedPath = import_node_path.default.normalize(file);
|
|
336
|
-
const affectedCanister = canisters.find(
|
|
337
|
-
// Check if changed file matches configured didFile
|
|
338
|
-
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
339
|
-
);
|
|
340
|
-
if (!affectedCanister) {
|
|
341
|
-
return;
|
|
546
|
+
for (const { canister, detail } of failures) {
|
|
547
|
+
pendingFailures.set(canister, {
|
|
548
|
+
message: `[ic-reactor] Failed to generate ${detail}`,
|
|
549
|
+
stack: ""
|
|
550
|
+
});
|
|
342
551
|
}
|
|
343
|
-
console.log(
|
|
344
|
-
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
345
|
-
);
|
|
346
|
-
return regenerate(affectedCanister, server);
|
|
347
552
|
}
|
|
348
553
|
};
|
|
349
554
|
return plugin;
|
|
350
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
|
+
}
|
|
351
563
|
function describeError(error) {
|
|
352
564
|
return error instanceof Error ? error.message : String(error);
|
|
353
565
|
}
|
|
566
|
+
function apiProxy(userConfig, target, configure) {
|
|
567
|
+
if (userConfig.server?.proxy?.["/api"]) {
|
|
568
|
+
debugLog(
|
|
569
|
+
`The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
|
|
570
|
+
);
|
|
571
|
+
return void 0;
|
|
572
|
+
}
|
|
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
|
+
}
|
|
620
|
+
}
|
|
354
621
|
function debugLog(message) {
|
|
355
622
|
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|
|
356
623
|
const scope = entry.trim();
|
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;
|