@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.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;
|
package/dist/index.js
CHANGED
|
@@ -1,23 +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
|
-
// the caller decides to print the diagnostics we collect below.
|
|
19
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
20
|
-
})
|
|
40
|
+
await runIcp(
|
|
41
|
+
["network", "status", "-e", networkName, "--json"],
|
|
42
|
+
projectRoot
|
|
43
|
+
)
|
|
21
44
|
);
|
|
22
45
|
const rootKey = networkStatus.root_key;
|
|
23
46
|
const proxyTarget = networkStatus.api_url || networkStatus.gateway_url || (networkStatus.port ? `http://127.0.0.1:${networkStatus.port}` : void 0);
|
|
@@ -30,11 +53,10 @@ function getIcEnvironmentInfo(canisterNames) {
|
|
|
30
53
|
const canisterIds = {};
|
|
31
54
|
for (const name of canisterNames) {
|
|
32
55
|
try {
|
|
33
|
-
const canisterId =
|
|
34
|
-
"icp",
|
|
56
|
+
const canisterId = (await runIcp(
|
|
35
57
|
["canister", "status", name, "-e", networkName, "-i"],
|
|
36
|
-
|
|
37
|
-
).trim();
|
|
58
|
+
projectRoot
|
|
59
|
+
)).trim();
|
|
38
60
|
if (canisterId) {
|
|
39
61
|
canisterIds[name] = canisterId;
|
|
40
62
|
}
|
|
@@ -95,9 +117,129 @@ function localInternetIdentityProvider(proxyTarget) {
|
|
|
95
117
|
return `${protocol}//id.ai.localhost${portSuffix}/authorize`;
|
|
96
118
|
}
|
|
97
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
|
+
|
|
98
241
|
// src/index.ts
|
|
99
242
|
var PLUGIN_NAME = "ic-reactor-plugin";
|
|
100
|
-
var DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943";
|
|
101
243
|
function icReactor(options) {
|
|
102
244
|
const {
|
|
103
245
|
canisters,
|
|
@@ -114,14 +256,33 @@ function icReactor(options) {
|
|
|
114
256
|
};
|
|
115
257
|
let projectRoot = process.cwd();
|
|
116
258
|
let command = "build";
|
|
259
|
+
let localEnvironment;
|
|
260
|
+
const apiProxyOptions = /* @__PURE__ */ new Set();
|
|
117
261
|
let devServer = null;
|
|
118
262
|
const resolveDidPath = (didFile) => path.normalize(
|
|
119
263
|
path.isAbsolute(didFile) ? didFile : path.resolve(projectRoot, didFile)
|
|
120
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
|
+
};
|
|
121
282
|
const configuredCanisterIds = Object.fromEntries(
|
|
122
283
|
canisters.filter((canister) => !!canister.canisterId).map((canister) => [canister.name, canister.canisterId])
|
|
123
284
|
);
|
|
124
|
-
|
|
285
|
+
const pendingFailures = /* @__PURE__ */ new Map();
|
|
125
286
|
const reportFailure = (server, message, cause) => {
|
|
126
287
|
console.error(`[ic-reactor] ${message}`);
|
|
127
288
|
const err = {
|
|
@@ -129,121 +290,125 @@ function icReactor(options) {
|
|
|
129
290
|
stack: cause instanceof Error && cause.stack ? cause.stack : "",
|
|
130
291
|
plugin: PLUGIN_NAME
|
|
131
292
|
};
|
|
132
|
-
pendingFailure = { message: err.message, stack: err.stack };
|
|
133
293
|
server?.ws.send({ type: "error", err });
|
|
294
|
+
return { message: err.message, stack: err.stack };
|
|
134
295
|
};
|
|
135
296
|
const inFlight = /* @__PURE__ */ new Map();
|
|
136
297
|
const rerunQueued = /* @__PURE__ */ new Set();
|
|
137
298
|
const regenerate = (canisterConfig, server) => {
|
|
138
299
|
const { name } = canisterConfig;
|
|
139
|
-
const running = inFlight.get(
|
|
300
|
+
const running = inFlight.get(canisterConfig);
|
|
140
301
|
if (running) {
|
|
141
|
-
rerunQueued.add(
|
|
302
|
+
rerunQueued.add(canisterConfig);
|
|
142
303
|
return running;
|
|
143
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
|
+
}
|
|
144
313
|
const run = runCanisterPipeline({
|
|
145
314
|
canisterConfig,
|
|
146
315
|
projectRoot,
|
|
147
316
|
globalConfig
|
|
148
317
|
}).then((result) => {
|
|
318
|
+
reportWarnings(name, result.warnings);
|
|
149
319
|
if (result.success) {
|
|
150
|
-
|
|
320
|
+
pendingFailures.delete(canisterConfig);
|
|
151
321
|
server.ws.send({ type: "full-reload" });
|
|
152
322
|
} else {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
323
|
+
pendingFailures.set(
|
|
324
|
+
canisterConfig,
|
|
325
|
+
reportFailure(
|
|
326
|
+
server,
|
|
327
|
+
`Regeneration failed for ${name}: ${result.error ?? "unknown error"}`
|
|
328
|
+
)
|
|
156
329
|
);
|
|
157
330
|
}
|
|
158
331
|
}).catch((error) => {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
332
|
+
pendingFailures.set(
|
|
333
|
+
canisterConfig,
|
|
334
|
+
reportFailure(
|
|
335
|
+
server,
|
|
336
|
+
`Regeneration failed for ${name}: ${describeError(error)}`,
|
|
337
|
+
error
|
|
338
|
+
)
|
|
163
339
|
);
|
|
164
340
|
}).finally(() => {
|
|
165
|
-
inFlight.delete(
|
|
166
|
-
if (rerunQueued.delete(
|
|
341
|
+
inFlight.delete(canisterConfig);
|
|
342
|
+
if (rerunQueued.delete(canisterConfig)) {
|
|
167
343
|
void regenerate(canisterConfig, server);
|
|
168
344
|
}
|
|
169
345
|
});
|
|
170
|
-
inFlight.set(
|
|
346
|
+
inFlight.set(canisterConfig, run);
|
|
171
347
|
return run;
|
|
172
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
|
+
};
|
|
173
367
|
const plugin = {
|
|
174
368
|
name: PLUGIN_NAME,
|
|
175
369
|
enforce: "pre",
|
|
176
370
|
// Run before other plugins
|
|
177
|
-
config(
|
|
371
|
+
async config(userConfig, { command: viteCommand }) {
|
|
178
372
|
command = viteCommand;
|
|
179
373
|
if (viteCommand !== "serve" || !injectEnvironment) {
|
|
180
374
|
return {};
|
|
181
375
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
canisterNames.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
debugLog(diagnostic);
|
|
195
|
-
}
|
|
196
|
-
const envOnlyCookie = canisters.length === 0 ? buildIcEnvCookie(
|
|
197
|
-
{},
|
|
198
|
-
void 0,
|
|
199
|
-
"http://id.ai.localhost:8000/authorize"
|
|
200
|
-
) : void 0;
|
|
201
|
-
return {
|
|
202
|
-
server: {
|
|
203
|
-
headers: envOnlyCookie ? {
|
|
204
|
-
"Set-Cookie": `ic_env=${envOnlyCookie}; Path=/; SameSite=Lax;`
|
|
205
|
-
} : void 0,
|
|
206
|
-
proxy: {
|
|
207
|
-
"/api": {
|
|
208
|
-
target: DEFAULT_LOCAL_REPLICA,
|
|
209
|
-
changeOrigin: true
|
|
210
|
-
}
|
|
211
|
-
}
|
|
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;
|
|
212
388
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.warn(
|
|
222
|
-
`[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.`
|
|
223
|
-
);
|
|
224
|
-
}
|
|
225
|
-
for (const diagnostic of diagnostics) {
|
|
226
|
-
debugLog(diagnostic);
|
|
227
|
-
}
|
|
228
|
-
const cookieValue = buildIcEnvCookie(
|
|
229
|
-
{
|
|
230
|
-
...icEnv.canisterIds,
|
|
231
|
-
...configuredCanisterIds
|
|
232
|
-
},
|
|
233
|
-
icEnv.rootKey,
|
|
234
|
-
icEnv.internetIdentityProvider
|
|
235
|
-
);
|
|
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);
|
|
236
397
|
return {
|
|
237
398
|
server: {
|
|
238
|
-
headers:
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
proxy: {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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;
|
|
245
408
|
}
|
|
246
|
-
|
|
409
|
+
apiProxyOptions.add(proxyOptions);
|
|
410
|
+
proxyOptions.target = environment.state?.proxyTarget ?? state.proxyTarget;
|
|
411
|
+
})
|
|
247
412
|
}
|
|
248
413
|
};
|
|
249
414
|
},
|
|
@@ -253,72 +418,176 @@ function icReactor(options) {
|
|
|
253
418
|
},
|
|
254
419
|
configureServer(server) {
|
|
255
420
|
devServer = server;
|
|
421
|
+
if (localEnvironment) {
|
|
422
|
+
server.middlewares.use(icEnvMiddleware(localEnvironment));
|
|
423
|
+
}
|
|
256
424
|
server.ws.on?.("connection", () => {
|
|
257
|
-
if (
|
|
425
|
+
if (pendingFailures.size === 0) return;
|
|
426
|
+
const failures = [...pendingFailures.values()];
|
|
258
427
|
server.ws.send({
|
|
259
428
|
type: "error",
|
|
260
|
-
err: {
|
|
429
|
+
err: {
|
|
430
|
+
message: failures.map((failure) => failure.message).join("\n"),
|
|
431
|
+
stack: failures.map((failure) => failure.stack).filter(Boolean).join("\n"),
|
|
432
|
+
plugin: PLUGIN_NAME
|
|
433
|
+
}
|
|
261
434
|
});
|
|
262
435
|
});
|
|
263
436
|
const didFiles = canisters.map((c) => resolveDidPath(c.didFile));
|
|
264
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
|
+
}
|
|
265
448
|
},
|
|
266
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
|
+
}
|
|
267
462
|
console.log(
|
|
268
|
-
`[ic-reactor] Generating canister bindings for ${
|
|
463
|
+
`[ic-reactor] Generating canister bindings for ${pending.length} canisters...`
|
|
269
464
|
);
|
|
270
465
|
const outcomes = await Promise.allSettled(
|
|
271
|
-
|
|
272
|
-
(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({
|
|
273
472
|
canisterConfig,
|
|
274
473
|
projectRoot,
|
|
275
474
|
globalConfig
|
|
276
|
-
})
|
|
277
|
-
)
|
|
475
|
+
});
|
|
476
|
+
})
|
|
278
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
|
+
});
|
|
279
490
|
const failures = outcomes.flatMap((outcome, index) => {
|
|
280
|
-
const
|
|
491
|
+
const canister = pending[index];
|
|
492
|
+
const name = canister?.name ?? `canister #${index}`;
|
|
281
493
|
if (outcome.status === "rejected") {
|
|
282
|
-
|
|
494
|
+
const detail = outcome.reason instanceof SharedOutDirError ? outcome.reason.message : `${name}: ${describeError(outcome.reason)}`;
|
|
495
|
+
return [{ canister, detail }];
|
|
283
496
|
}
|
|
284
497
|
if (!outcome.value.success) {
|
|
285
|
-
return [
|
|
498
|
+
return [
|
|
499
|
+
{
|
|
500
|
+
canister,
|
|
501
|
+
detail: `${name}: ${outcome.value.error ?? "unknown error"}`
|
|
502
|
+
}
|
|
503
|
+
];
|
|
286
504
|
}
|
|
287
505
|
return [];
|
|
288
506
|
});
|
|
289
507
|
if (failures.length === 0) {
|
|
290
508
|
return;
|
|
291
509
|
}
|
|
292
|
-
const message = `Failed to generate ${failures.length} of ${
|
|
293
|
-
` + failures.map((
|
|
510
|
+
const message = `Failed to generate ${failures.length} of ${pending.length} canisters:
|
|
511
|
+
` + failures.map(({ detail }) => ` - ${detail}`).join("\n");
|
|
294
512
|
if (failOnError ?? command === "build") {
|
|
295
513
|
this.error(`[ic-reactor] ${message}`);
|
|
296
514
|
}
|
|
297
515
|
reportFailure(devServer, message);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const changedPath = path.normalize(file);
|
|
304
|
-
const affectedCanister = canisters.find(
|
|
305
|
-
// Check if changed file matches configured didFile
|
|
306
|
-
(canister) => resolveDidPath(canister.didFile) === changedPath
|
|
307
|
-
);
|
|
308
|
-
if (!affectedCanister) {
|
|
309
|
-
return;
|
|
516
|
+
for (const { canister, detail } of failures) {
|
|
517
|
+
pendingFailures.set(canister, {
|
|
518
|
+
message: `[ic-reactor] Failed to generate ${detail}`,
|
|
519
|
+
stack: ""
|
|
520
|
+
});
|
|
310
521
|
}
|
|
311
|
-
console.log(
|
|
312
|
-
`[ic-reactor] .did file changed: ${affectedCanister.name}. Regenerating...`
|
|
313
|
-
);
|
|
314
|
-
return regenerate(affectedCanister, server);
|
|
315
522
|
}
|
|
316
523
|
};
|
|
317
524
|
return plugin;
|
|
318
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
|
+
}
|
|
319
533
|
function describeError(error) {
|
|
320
534
|
return error instanceof Error ? error.message : String(error);
|
|
321
535
|
}
|
|
536
|
+
function apiProxy(userConfig, target, configure) {
|
|
537
|
+
if (userConfig.server?.proxy?.["/api"]) {
|
|
538
|
+
debugLog(
|
|
539
|
+
`The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
|
|
540
|
+
);
|
|
541
|
+
return void 0;
|
|
542
|
+
}
|
|
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
|
+
}
|
|
590
|
+
}
|
|
322
591
|
function debugLog(message) {
|
|
323
592
|
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|
|
324
593
|
const scope = entry.trim();
|