@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.js
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import path from "path";
|
|
3
4
|
import {
|
|
4
|
-
|
|
5
|
+
findSharedOutDirs,
|
|
6
|
+
runCanisterPipeline,
|
|
7
|
+
sharedOutDirMessage
|
|
5
8
|
} from "@ic-reactor/codegen";
|
|
6
9
|
|
|
7
10
|
// src/env.ts
|
|
8
|
-
import {
|
|
9
|
-
|
|
11
|
+
import { execFile } from "child_process";
|
|
12
|
+
var ICP_TIMEOUT_MS = 1e4;
|
|
13
|
+
function runIcp(args, cwd) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const child = execFile(
|
|
16
|
+
"icp",
|
|
17
|
+
args,
|
|
18
|
+
{
|
|
19
|
+
cwd,
|
|
20
|
+
encoding: "utf-8",
|
|
21
|
+
timeout: ICP_TIMEOUT_MS,
|
|
22
|
+
windowsHide: true
|
|
23
|
+
},
|
|
24
|
+
(error, stdout, stderr) => {
|
|
25
|
+
if (error) {
|
|
26
|
+
reject(Object.assign(error, { stderr }));
|
|
27
|
+
} else {
|
|
28
|
+
resolve(stdout);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
child.stdin?.end();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
|
|
10
36
|
const networkName = process.env.ICP_ENVIRONMENT || "local";
|
|
11
37
|
const diagnostics = [];
|
|
12
38
|
try {
|
|
13
39
|
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
|
-
})
|
|
40
|
+
await runIcp(
|
|
41
|
+
["network", "status", "-e", networkName, "--json"],
|
|
42
|
+
projectRoot
|
|
43
|
+
)
|
|
22
44
|
);
|
|
23
45
|
const rootKey = networkStatus.root_key;
|
|
24
46
|
const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
|
|
@@ -31,15 +53,10 @@ function getIcEnvironmentInfo(canisterNames, projectRoot = process.cwd()) {
|
|
|
31
53
|
const canisterIds = {};
|
|
32
54
|
for (const name of canisterNames) {
|
|
33
55
|
try {
|
|
34
|
-
const canisterId =
|
|
35
|
-
"icp",
|
|
56
|
+
const canisterId = (await runIcp(
|
|
36
57
|
["canister", "status", name, "-e", networkName, "-i"],
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
encoding: "utf-8",
|
|
40
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
41
|
-
}
|
|
42
|
-
).trim();
|
|
58
|
+
projectRoot
|
|
59
|
+
)).trim();
|
|
43
60
|
if (canisterId) {
|
|
44
61
|
canisterIds[name] = canisterId;
|
|
45
62
|
}
|
|
@@ -100,9 +117,129 @@ function localInternetIdentityProvider(proxyTarget) {
|
|
|
100
117
|
return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
|
|
101
118
|
}
|
|
102
119
|
|
|
120
|
+
// src/dev-environment.ts
|
|
121
|
+
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
122
|
+
var DEFAULT_INTERNET_IDENTITY_PROVIDER = "http://id.ai.localhost:8000/authorize";
|
|
123
|
+
function createLocalEnvironment(options) {
|
|
124
|
+
const {
|
|
125
|
+
canisterNames,
|
|
126
|
+
configuredCanisterIds,
|
|
127
|
+
projectRoot,
|
|
128
|
+
onDiagnostic,
|
|
129
|
+
onUpdate
|
|
130
|
+
} = options;
|
|
131
|
+
const requiredNames = [...new Set(canisterNames)];
|
|
132
|
+
const lookupNames = requiredNames.includes("internet_identity") ? requiredNames : [...requiredNames, "internet_identity"];
|
|
133
|
+
let state;
|
|
134
|
+
let running;
|
|
135
|
+
const toState = (environment) => {
|
|
136
|
+
const canisterIds = {
|
|
137
|
+
...environment?.canisterIds,
|
|
138
|
+
...configuredCanisterIds
|
|
139
|
+
};
|
|
140
|
+
const missingCanisterIds = requiredNames.filter(
|
|
141
|
+
(name) => !canisterIds[name]
|
|
142
|
+
);
|
|
143
|
+
let cookie;
|
|
144
|
+
if (environment) {
|
|
145
|
+
cookie = buildIcEnvCookie(
|
|
146
|
+
canisterIds,
|
|
147
|
+
environment.rootKey,
|
|
148
|
+
environment.internetIdentityProvider
|
|
149
|
+
);
|
|
150
|
+
} else if (requiredNames.length === 0) {
|
|
151
|
+
cookie = buildIcEnvCookie(
|
|
152
|
+
{},
|
|
153
|
+
void 0,
|
|
154
|
+
DEFAULT_INTERNET_IDENTITY_PROVIDER
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
environment,
|
|
159
|
+
cookie,
|
|
160
|
+
proxyTarget: environment?.proxyTarget ?? DEFAULT_LOCAL_REPLICA,
|
|
161
|
+
missingCanisterIds,
|
|
162
|
+
complete: environment !== null && missingCanisterIds.length === 0
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const run = async () => {
|
|
166
|
+
const previous = state;
|
|
167
|
+
try {
|
|
168
|
+
const { environment, diagnostics } = await getIcEnvironmentInfo(
|
|
169
|
+
lookupNames,
|
|
170
|
+
projectRoot
|
|
171
|
+
);
|
|
172
|
+
diagnostics.forEach(onDiagnostic);
|
|
173
|
+
state = toState(
|
|
174
|
+
mergeDetections(previous?.environment ?? null, environment)
|
|
175
|
+
);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
onDiagnostic(
|
|
178
|
+
`Detecting the local IC environment failed: ${error instanceof Error ? error.message : String(error)}`
|
|
179
|
+
);
|
|
180
|
+
state = previous ?? toState(null);
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
onUpdate?.(previous, state);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
onDiagnostic(
|
|
186
|
+
`Applying the detected IC environment failed: ${error instanceof Error ? error.message : String(error)}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return state;
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
get state() {
|
|
193
|
+
return state;
|
|
194
|
+
},
|
|
195
|
+
detect() {
|
|
196
|
+
running ?? (running = run().finally(() => {
|
|
197
|
+
running = void 0;
|
|
198
|
+
}));
|
|
199
|
+
return running;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function mergeDetections(previous, next) {
|
|
204
|
+
if (!next) return previous;
|
|
205
|
+
if (!previous || previous.rootKey !== next.rootKey) return next;
|
|
206
|
+
const canisterIds = { ...previous.canisterIds, ...next.canisterIds };
|
|
207
|
+
return {
|
|
208
|
+
...next,
|
|
209
|
+
canisterIds,
|
|
210
|
+
// The built-in provider stands in only for a project that has no
|
|
211
|
+
// internet_identity canister, as getIcEnvironmentInfo decides it.
|
|
212
|
+
internetIdentityProvider: canisterIds.internet_identity ? void 0 : next.internetIdentityProvider
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function isDocumentRequest(req) {
|
|
216
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false;
|
|
217
|
+
const { accept } = req.headers;
|
|
218
|
+
return typeof accept === "string" && accept.includes("text/html");
|
|
219
|
+
}
|
|
220
|
+
function setIcEnvCookie(res, value) {
|
|
221
|
+
const existing = res.getHeader("Set-Cookie");
|
|
222
|
+
const others = (existing === void 0 ? [] : Array.isArray(existing) ? existing : [String(existing)]).filter((cookie2) => !cookie2.startsWith("ic_env="));
|
|
223
|
+
const cookie = `ic_env=${value}; Path=/; SameSite=Lax;`;
|
|
224
|
+
res.setHeader("Set-Cookie", others.length > 0 ? [...others, cookie] : cookie);
|
|
225
|
+
}
|
|
226
|
+
function icEnvMiddleware(environment) {
|
|
227
|
+
return (req, res, next) => {
|
|
228
|
+
const respond = (state2) => {
|
|
229
|
+
if (state2?.cookie !== void 0) setIcEnvCookie(res, state2.cookie);
|
|
230
|
+
next();
|
|
231
|
+
};
|
|
232
|
+
const { state } = environment;
|
|
233
|
+
if (state?.complete || !isDocumentRequest(req)) {
|
|
234
|
+
respond(state);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
void environment.detect().then(respond);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
103
241
|
// src/index.ts
|
|
104
242
|
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
105
|
-
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
106
243
|
function icReactor(options) {
|
|
107
244
|
const {
|
|
108
245
|
canisters,
|
|
@@ -119,10 +256,29 @@ function icReactor(options) {
|
|
|
119
256
|
};
|
|
120
257
|
let projectRoot = process.cwd();
|
|
121
258
|
let command = "build";
|
|
259
|
+
let localEnvironment;
|
|
260
|
+
const apiProxyOptions = /* @__PURE__ */ new Set();
|
|
122
261
|
let devServer = null;
|
|
123
262
|
const resolveDidPath = (didFile) => path.normalize(
|
|
124
263
|
path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
|
|
125
264
|
);
|
|
265
|
+
const generatedFrom = /* @__PURE__ */ new Map();
|
|
266
|
+
const readDidSource = (canister) => {
|
|
267
|
+
try {
|
|
268
|
+
return fs.readFileSync(resolveDidPath(canister.didFile), "utf-8");
|
|
269
|
+
} catch {
|
|
270
|
+
return void 0;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const describeEntry = (canister) => `canisters[${canisters.indexOf(canister)}] (${JSON.stringify(canister.name)})`;
|
|
274
|
+
const sharedOutDirError = (canister) => {
|
|
275
|
+
const first = findSharedOutDirs(
|
|
276
|
+
canisters.map((entry) => [entry, entry]),
|
|
277
|
+
outDir,
|
|
278
|
+
projectRoot
|
|
279
|
+
).get(canister);
|
|
280
|
+
return first === void 0 ? void 0 : sharedOutDirMessage(describeEntry(canister), describeEntry(first));
|
|
281
|
+
};
|
|
126
282
|
const configuredCanisterIds = Object.fromEntries(
|
|
127
283
|
canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
|
|
128
284
|
);
|
|
@@ -141,22 +297,31 @@ function icReactor(options) {
|
|
|
141
297
|
const rerunQueued = /* @__PURE__ */ new Set();
|
|
142
298
|
const regenerate = (canisterConfig, server) => {
|
|
143
299
|
const { name } = canisterConfig;
|
|
144
|
-
const running = inFlight.get(
|
|
300
|
+
const running = inFlight.get(canisterConfig);
|
|
145
301
|
if (running) {
|
|
146
|
-
rerunQueued.add(
|
|
302
|
+
rerunQueued.add(canisterConfig);
|
|
147
303
|
return running;
|
|
148
304
|
}
|
|
305
|
+
const sharedError = sharedOutDirError(canisterConfig);
|
|
306
|
+
if (sharedError !== void 0) {
|
|
307
|
+
pendingFailures.set(
|
|
308
|
+
canisterConfig,
|
|
309
|
+
reportFailure(server, `Regeneration failed for ${sharedError}`)
|
|
310
|
+
);
|
|
311
|
+
return Promise.resolve();
|
|
312
|
+
}
|
|
149
313
|
const run = runCanisterPipeline({
|
|
150
314
|
canisterConfig,
|
|
151
315
|
projectRoot,
|
|
152
316
|
globalConfig
|
|
153
317
|
}).then((result) => {
|
|
318
|
+
reportWarnings(name, result.warnings);
|
|
154
319
|
if (result.success) {
|
|
155
|
-
pendingFailures.delete(
|
|
320
|
+
pendingFailures.delete(canisterConfig);
|
|
156
321
|
server.ws.send({ type: "full-reload" });
|
|
157
322
|
} else {
|
|
158
323
|
pendingFailures.set(
|
|
159
|
-
|
|
324
|
+
canisterConfig,
|
|
160
325
|
reportFailure(
|
|
161
326
|
server,
|
|
162
327
|
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
@@ -165,7 +330,7 @@ function icReactor(options) {
|
|
|
165
330
|
}
|
|
166
331
|
}).catch((error) => {
|
|
167
332
|
pendingFailures.set(
|
|
168
|
-
|
|
333
|
+
canisterConfig,
|
|
169
334
|
reportFailure(
|
|
170
335
|
server,
|
|
171
336
|
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
@@ -173,81 +338,77 @@ function icReactor(options) {
|
|
|
173
338
|
)
|
|
174
339
|
);
|
|
175
340
|
}).finally(() => {
|
|
176
|
-
inFlight.delete(
|
|
177
|
-
if (rerunQueued.delete(
|
|
341
|
+
inFlight.delete(canisterConfig);
|
|
342
|
+
if (rerunQueued.delete(canisterConfig)) {
|
|
178
343
|
void regenerate(canisterConfig, server);
|
|
179
344
|
}
|
|
180
345
|
});
|
|
181
|
-
inFlight.set(
|
|
346
|
+
inFlight.set(canisterConfig, run);
|
|
182
347
|
return run;
|
|
183
348
|
};
|
|
349
|
+
const regenerateForDid = (file, server) => {
|
|
350
|
+
if (!file.endsWith(".did")) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const changedPath = path.normalize(file);
|
|
354
|
+
const affectedCanisters = canisters.filter(
|
|
355
|
+
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
356
|
+
);
|
|
357
|
+
if (affectedCanisters.length === 0) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
console.log(
|
|
361
|
+
`[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
|
|
362
|
+
);
|
|
363
|
+
for (const canister of affectedCanisters) {
|
|
364
|
+
void regenerate(canister, server);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
184
367
|
const plugin = {
|
|
185
368
|
name: PLUGIN_NAME,
|
|
186
369
|
enforce: "pre",
|
|
187
370
|
// Run before other plugins
|
|
188
|
-
config(userConfig, { command: viteCommand }) {
|
|
371
|
+
async config(userConfig, { command: viteCommand }) {
|
|
189
372
|
command = viteCommand;
|
|
190
373
|
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
191
374
|
return {};
|
|
192
375
|
}
|
|
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)
|
|
376
|
+
const ownsApiProxy = !userConfig.server?.proxy?.["/api"];
|
|
377
|
+
const environment = createLocalEnvironment({
|
|
378
|
+
canisterNames: canisters.map((canister) => canister.name).filter((name) => !!name),
|
|
379
|
+
configuredCanisterIds,
|
|
380
|
+
// `configResolved` has not run yet, so resolve the root the way Vite
|
|
381
|
+
// will. icp finds the project from the directory it starts in, and
|
|
382
|
+
// with `vite apps/web` or a `root` option that is not the process cwd.
|
|
383
|
+
projectRoot: path.resolve(userConfig.root ?? process.cwd()),
|
|
384
|
+
onDiagnostic: debugLog,
|
|
385
|
+
onUpdate: (previous, next) => {
|
|
386
|
+
for (const proxyOptions of apiProxyOptions) {
|
|
387
|
+
proxyOptions.target = next.proxyTarget;
|
|
221
388
|
}
|
|
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
|
-
);
|
|
389
|
+
if (previous) {
|
|
390
|
+
reportDetectionProgress(previous, next, apiProxyOptions.size > 0);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
localEnvironment = environment;
|
|
395
|
+
const state = await environment.detect();
|
|
396
|
+
warnAboutIncompleteDetection(state, canisters.length > 0, ownsApiProxy);
|
|
245
397
|
return {
|
|
246
398
|
server: {
|
|
247
|
-
headers:
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
proxy: apiProxy(userConfig,
|
|
399
|
+
// The cookie is not a static `server.headers` entry: the middleware
|
|
400
|
+
// that configureServer adds sets it per response, from the latest
|
|
401
|
+
// detection. See dev-environment.ts.
|
|
402
|
+
proxy: apiProxy(userConfig, state.proxyTarget, (proxyOptions) => {
|
|
403
|
+
if (proxyOptions.target !== state.proxyTarget) {
|
|
404
|
+
debugLog(
|
|
405
|
+
"Another plugin changed the target of the /api proxy, so the plugin leaves that proxy alone."
|
|
406
|
+
);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
apiProxyOptions.add(proxyOptions);
|
|
410
|
+
proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
|
|
411
|
+
})
|
|
251
412
|
}
|
|
252
413
|
};
|
|
253
414
|
},
|
|
@@ -257,6 +418,9 @@ function icReactor(options) {
|
|
|
257
418
|
},
|
|
258
419
|
configureServer(server) {
|
|
259
420
|
devServer = server;
|
|
421
|
+
if (localEnvironment) {
|
|
422
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
423
|
+
}
|
|
260
424
|
server.ws.on?.("connection", () => {
|
|
261
425
|
if (pendingFailures.size === 0) return;
|
|
262
426
|
const failures = [...pendingFailures.values()];
|
|
@@ -271,29 +435,69 @@ function icReactor(options) {
|
|
|
271
435
|
});
|
|
272
436
|
const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
|
|
273
437
|
server.watcher.add(didFiles);
|
|
438
|
+
const onDidEvent = (file) => regenerateForDid(file, server);
|
|
439
|
+
server.watcher.on("change", onDidEvent);
|
|
440
|
+
server.watcher.on("add", onDidEvent);
|
|
441
|
+
},
|
|
442
|
+
// `vite preview` resolves the config with the `serve` command too, and
|
|
443
|
+
// used to inherit the cookie from `server.headers`.
|
|
444
|
+
configurePreviewServer(server) {
|
|
445
|
+
if (localEnvironment) {
|
|
446
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
447
|
+
}
|
|
274
448
|
},
|
|
275
449
|
async buildStart() {
|
|
450
|
+
for (const canister of canisters) {
|
|
451
|
+
if (typeof canister.didFile === "string") {
|
|
452
|
+
this.addWatchFile(resolveDidPath(canister.didFile));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const sources = canisters.map(readDidSource);
|
|
456
|
+
const pending = canisters.filter(
|
|
457
|
+
(canister, index) => !this.meta.watchMode || sources[index] === void 0 || generatedFrom.get(canister) !== sources[index]
|
|
458
|
+
);
|
|
459
|
+
if (pending.length === 0) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
276
462
|
console.log(
|
|
277
|
-
`[ic-reactor] Generating canister bindings for ${
|
|
463
|
+
`[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
|
|
278
464
|
);
|
|
279
465
|
const outcomes = await Promise.allSettled(
|
|
280
|
-
|
|
281
|
-
(canisterConfig)
|
|
466
|
+
pending.map((canisterConfig) => {
|
|
467
|
+
const sharedError = sharedOutDirError(canisterConfig);
|
|
468
|
+
if (sharedError !== void 0) {
|
|
469
|
+
return Promise.reject(new SharedOutDirError(sharedError));
|
|
470
|
+
}
|
|
471
|
+
return runCanisterPipeline({
|
|
282
472
|
canisterConfig,
|
|
283
473
|
projectRoot,
|
|
284
474
|
globalConfig
|
|
285
|
-
})
|
|
286
|
-
)
|
|
475
|
+
});
|
|
476
|
+
})
|
|
287
477
|
);
|
|
478
|
+
outcomes.forEach((outcome, index) => {
|
|
479
|
+
const canister = pending[index];
|
|
480
|
+
const source = sources[canisters.indexOf(canister)];
|
|
481
|
+
if (outcome.status === "fulfilled") {
|
|
482
|
+
reportWarnings(canister.name, outcome.value.warnings);
|
|
483
|
+
}
|
|
484
|
+
if (outcome.status === "fulfilled" && outcome.value.success && source !== void 0) {
|
|
485
|
+
generatedFrom.set(canister, source);
|
|
486
|
+
} else {
|
|
487
|
+
generatedFrom.delete(canister);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
288
490
|
const failures = outcomes.flatMap((outcome, index) => {
|
|
289
|
-
const
|
|
491
|
+
const canister = pending[index];
|
|
492
|
+
const name = canister?.name ?? `canister #${index}`;
|
|
290
493
|
if (outcome.status === "rejected") {
|
|
291
|
-
|
|
494
|
+
const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
|
|
495
|
+
return [{ canister, detail }];
|
|
292
496
|
}
|
|
293
497
|
if (!outcome.value.success) {
|
|
294
498
|
return [
|
|
295
499
|
{
|
|
296
|
-
|
|
500
|
+
canister,
|
|
297
501
|
detail: `${name}: ${outcome.value.error ?? "unknown error"}`
|
|
298
502
|
}
|
|
299
503
|
];
|
|
@@ -303,51 +507,86 @@ function icReactor(options) {
|
|
|
303
507
|
if (failures.length === 0) {
|
|
304
508
|
return;
|
|
305
509
|
}
|
|
306
|
-
const message = `Failed to generate ${failures.length} of ${
|
|
510
|
+
const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
|
|
307
511
|
` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
|
|
308
512
|
if (failOnError ?? command === "build") {
|
|
309
513
|
this.error(`[ic-reactor] ${message}`);
|
|
310
514
|
}
|
|
311
515
|
reportFailure(devServer, message);
|
|
312
|
-
for (const {
|
|
313
|
-
pendingFailures.set(
|
|
516
|
+
for (const { canister, detail } of failures) {
|
|
517
|
+
pendingFailures.set(canister, {
|
|
314
518
|
message: `[ic-reactor] Failed to generate ${detail}`,
|
|
315
519
|
stack: ""
|
|
316
520
|
});
|
|
317
521
|
}
|
|
318
|
-
},
|
|
319
|
-
handleHotUpdate({ file, server }) {
|
|
320
|
-
if (!file.endsWith(".did")) {
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
const changedPath = path.normalize(file);
|
|
324
|
-
const affectedCanisters = canisters.filter(
|
|
325
|
-
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
326
|
-
);
|
|
327
|
-
if (affectedCanisters.length === 0) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
console.log(
|
|
331
|
-
`[ic-reactor] .did file changed: ${affectedCanisters.map((canister) => canister.name).join(", ")}. Regenerating...`
|
|
332
|
-
);
|
|
333
|
-
return Promise.all(
|
|
334
|
-
affectedCanisters.map((canister) => regenerate(canister, server))
|
|
335
|
-
).then(() => void 0);
|
|
336
522
|
}
|
|
337
523
|
};
|
|
338
524
|
return plugin;
|
|
339
525
|
}
|
|
526
|
+
var SharedOutDirError = class extends Error {
|
|
527
|
+
};
|
|
528
|
+
function reportWarnings(name, warnings) {
|
|
529
|
+
for (const warning of warnings ?? []) {
|
|
530
|
+
console.warn(`[ic-reactor] ${name}: ${warning}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
340
533
|
function describeError(error) {
|
|
341
534
|
return error instanceof Error ? error.message : String(error);
|
|
342
535
|
}
|
|
343
|
-
function apiProxy(userConfig, target) {
|
|
536
|
+
function apiProxy(userConfig, target, configure) {
|
|
344
537
|
if (userConfig.server?.proxy?.["/api"]) {
|
|
345
538
|
debugLog(
|
|
346
539
|
`The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
|
|
347
540
|
);
|
|
348
541
|
return void 0;
|
|
349
542
|
}
|
|
350
|
-
return {
|
|
543
|
+
return {
|
|
544
|
+
"/api": {
|
|
545
|
+
target,
|
|
546
|
+
changeOrigin: true,
|
|
547
|
+
configure: (_proxy, options) => configure(options)
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function quoteNames(names) {
|
|
552
|
+
return names.map((name) => `"${name}"`).join(", ");
|
|
553
|
+
}
|
|
554
|
+
function warnAboutIncompleteDetection(state, hasCanisters, ownsApiProxy) {
|
|
555
|
+
if (!state.environment) {
|
|
556
|
+
if (!hasCanisters) return;
|
|
557
|
+
const proxyNote = ownsApiProxy ? ` and /api goes to ${state.proxyTarget} for now` : "";
|
|
558
|
+
console.warn(
|
|
559
|
+
`[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.`
|
|
560
|
+
);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const missing = state.missingCanisterIds;
|
|
564
|
+
if (missing.length > 0) {
|
|
565
|
+
const it = missing.length === 1 ? "it" : "them";
|
|
566
|
+
console.warn(
|
|
567
|
+
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${quoteNames(missing)}. Until one is, the ic_env cookie carries no PUBLIC_CANISTER_ID for ${it} and the app will see an undefined canister id. Deploy ${it} (\`icp deploy\`) and reload the page: the plugin asks \`icp\` again on each page load until every configured canister has an ID. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function reportDetectionProgress(previous, next, followsApiProxy) {
|
|
572
|
+
if (!previous.environment && next.environment) {
|
|
573
|
+
console.log(
|
|
574
|
+
`[ic-reactor] Detected the local IC network: the ic_env cookie now carries its root key` + (followsApiProxy ? ` and /api goes to ${next.proxyTarget}.` : ".")
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const resolved = previous.missingCanisterIds.filter(
|
|
578
|
+
(name) => !next.missingCanisterIds.includes(name)
|
|
579
|
+
);
|
|
580
|
+
if (next.environment && resolved.length > 0) {
|
|
581
|
+
console.log(
|
|
582
|
+
`[ic-reactor] The ic_env cookie now carries the canister ID${resolved.length === 1 ? "" : "s"} for ${quoteNames(resolved)}.`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
if (next.complete && !previous.complete) {
|
|
586
|
+
console.log(
|
|
587
|
+
"[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."
|
|
588
|
+
);
|
|
589
|
+
}
|
|
351
590
|
}
|
|
352
591
|
function debugLog(message) {
|
|
353
592
|
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|