@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/llms.txt
CHANGED
|
@@ -1,55 +1,156 @@
|
|
|
1
|
-
# @ic-reactor/vite-plugin
|
|
1
|
+
# @ic-reactor/vite-plugin
|
|
2
2
|
|
|
3
|
-
Vite
|
|
3
|
+
> Vite plugin that generates typed declarations, a reactor with bound React
|
|
4
|
+
> hooks, and optionally one query or mutation object per method from `.did`
|
|
5
|
+
> files, regenerates on change, and gives `vite dev` the local canister ids
|
|
6
|
+
> and an `/api` proxy to the local replica.
|
|
7
|
+
|
|
8
|
+
Applies to `@ic-reactor/vite-plugin` 0.15.1.
|
|
9
|
+
|
|
10
|
+
Read this file from `node_modules/@ic-reactor/vite-plugin/llms.txt` when
|
|
11
|
+
writing code against the installed version. The complete guide for every
|
|
12
|
+
package is https://ic-reactor.b3pay.net/llms-full.txt, and behaviour changes
|
|
13
|
+
are in https://github.com/B3Pay/ic-reactor/blob/main/CHANGELOG.md.
|
|
4
14
|
|
|
5
15
|
## Install
|
|
6
16
|
|
|
7
17
|
```bash
|
|
8
18
|
pnpm add -D @ic-reactor/vite-plugin
|
|
9
|
-
pnpm add @ic-reactor/react @tanstack/react-query
|
|
19
|
+
pnpm add @ic-reactor/react @icp-sdk/core @tanstack/react-query
|
|
10
20
|
```
|
|
11
21
|
|
|
12
|
-
##
|
|
22
|
+
## Setup and Usage
|
|
13
23
|
|
|
14
24
|
```ts
|
|
25
|
+
// vite.config.ts
|
|
26
|
+
import { defineConfig, loadEnv } from "vite"
|
|
15
27
|
import { icReactor } from "@ic-reactor/vite-plugin"
|
|
16
28
|
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
export default defineConfig(({ mode }) => {
|
|
30
|
+
const env = loadEnv(mode, process.cwd(), "")
|
|
31
|
+
return {
|
|
32
|
+
plugins: [
|
|
33
|
+
icReactor({
|
|
34
|
+
// outDir: "src/declarations" and clientManagerPath: "../../clients"
|
|
35
|
+
// by default, so src/clients.ts must export `clientManager`
|
|
36
|
+
canisters: [
|
|
37
|
+
{
|
|
38
|
+
name: "backend",
|
|
39
|
+
didFile: "./backend/backend.did",
|
|
40
|
+
factories: true, // also a query or mutation object per method
|
|
41
|
+
// Used under `vite dev` too, in place of the local id
|
|
42
|
+
canisterId: env.CANISTER_ID_BACKEND,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
}
|
|
19
48
|
})
|
|
20
49
|
```
|
|
21
50
|
|
|
22
|
-
|
|
51
|
+
```tsx
|
|
52
|
+
// src/App.tsx: the generated entry is src/declarations/backend/index.ts
|
|
53
|
+
import { getPostsQuery, useBackendMutation } from "./declarations/backend"
|
|
54
|
+
|
|
55
|
+
export function Posts() {
|
|
56
|
+
const { data: posts } = getPostsQuery.useQuery()
|
|
57
|
+
const create = useBackendMutation({
|
|
58
|
+
functionName: "create_post",
|
|
59
|
+
invalidateQueries: [getPostsQuery],
|
|
60
|
+
})
|
|
61
|
+
return (
|
|
62
|
+
<button onClick={() => create.mutate(["Hello"])}>
|
|
63
|
+
{posts?.length ?? 0} posts
|
|
64
|
+
</button>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`src/clients.ts` is yours:
|
|
70
|
+
`export const clientManager = new ClientManager({ queryClient })`, with
|
|
71
|
+
`ClientManager` from `@ic-reactor/react` and a `QueryClient` from
|
|
72
|
+
`@tanstack/react-query` (set `retry: reactorRetry` in its query defaults).
|
|
73
|
+
For Internet Identity sign-in, build
|
|
74
|
+
`new AuthenticationManager({ clientManager })` in the same module and export
|
|
75
|
+
`useAuth` from `createAuthHooks(authentication)`; every generated reactor on
|
|
76
|
+
that `clientManager` then calls as the signed-in user.
|
|
77
|
+
|
|
78
|
+
## When to Use What
|
|
79
|
+
|
|
80
|
+
| Need | Use |
|
|
81
|
+
| ------------------------------------------------- | ------------------------------------------------------ |
|
|
82
|
+
| Vite app, `.did` files in the repo | this plugin |
|
|
83
|
+
| No Vite, or generation as a CI step | `@ic-reactor/cli` |
|
|
84
|
+
| Query and mutation objects per method | `factories: true` on the canister entry |
|
|
85
|
+
| Raw Candid values instead of display values | `mode: "Reactor"` on the canister entry |
|
|
86
|
+
| No React | `target: "core"` |
|
|
87
|
+
| Local canister ids and `/api` proxy in `vite dev` | `injectEnvironment` (on by default) |
|
|
88
|
+
| A project without a local network | `injectEnvironment: false`, so page loads run no `icp` |
|
|
89
|
+
|
|
90
|
+
## Options
|
|
91
|
+
|
|
92
|
+
- Plugin level: `canisters` (required), `outDir`, `clientManagerPath`,
|
|
93
|
+
`target`, `injectEnvironment`, `failOnError`.
|
|
94
|
+
- Per canister: `name` and `didFile` (required), `outDir`,
|
|
95
|
+
`clientManagerPath`, `target`, `mode`, `canisterId`, `factories`. Relative
|
|
96
|
+
paths resolve against Vite's `root`.
|
|
97
|
+
- `failOnError` defaults to `true` under `vite build` and `false` under
|
|
98
|
+
`vite dev`, where a failure goes to the terminal and the browser overlay.
|
|
99
|
+
- Each entry needs its own output directory: a later entry with the same
|
|
100
|
+
`name` and `outDir` as an earlier one fails.
|
|
101
|
+
|
|
102
|
+
## Generated Files
|
|
23
103
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
104
|
+
Per canister, under `<outDir>/<name>/`: `declarations/` (`idlFactory` and
|
|
105
|
+
`_SERVICE`), `index.generated.ts` (`<name>Reactor` and six hooks
|
|
106
|
+
`use<Name>Query`, `use<Name>Mutation`, ...), `index.factories.generated.ts`
|
|
107
|
+
with `factories: true` (`<method>Query`, `<method>Mutation`), and `index.ts`,
|
|
108
|
+
which is created once and then yours. Generation runs on startup and on every
|
|
109
|
+
`.did` change. An edited `index.ts` gets a terminal warning until it
|
|
110
|
+
re-exports `./index.factories.generated`; a warning never fails the build.
|
|
30
111
|
|
|
31
|
-
##
|
|
112
|
+
## Local Environment (`injectEnvironment`)
|
|
32
113
|
|
|
33
|
-
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
114
|
+
- Under `vite dev` and `vite preview` the plugin sets the `ic_env` cookie
|
|
115
|
+
(root key, Internet Identity provider, `PUBLIC_CANISTER_ID:<name>`) on each
|
|
116
|
+
response and proxies `/api` to the network `icp` reports.
|
|
117
|
+
- While `icp` reports no network or a configured canister has no id, each page
|
|
118
|
+
load asks `icp` again, so deploying after `vite dev` started needs only a
|
|
119
|
+
reload. Once every canister resolves, page loads run no `icp` command, and a
|
|
120
|
+
redeploy into a fresh network needs a restart.
|
|
121
|
+
- An `/api` proxy set by the Vite config or another plugin is left alone.
|
|
122
|
+
- `ClientManager` trusts the cookie only on loopback and `localhost` hosts. A
|
|
123
|
+
dev build served over a tunnel, Codespaces, Gitpod or a custom domain needs
|
|
124
|
+
`allowEnvConfig: true` on the `ClientManager`; without it a generated
|
|
125
|
+
reactor with no `canisterId` throws instead of reading one from the cookie.
|
|
126
|
+
- The cookie does not exist in production: set `canisterId` on every entry
|
|
127
|
+
whose build is served anywhere else. A set `canisterId` is written into the
|
|
128
|
+
generated reactor and wins over the local id under `vite dev` too, so take
|
|
129
|
+
it from the environment (`loadEnv`, as above) when the local and production
|
|
130
|
+
ids differ, rather than hard-coding the production id.
|
|
131
|
+
- Vitest runs `vite.config.ts` unless a `vitest.config.ts` replaces it, so
|
|
132
|
+
the plugin generates again when tests run, in mode `test`. Set the
|
|
133
|
+
`canisterId` variable for that mode too (for example in `.env.test`), or
|
|
134
|
+
the regenerated reactor has no id and importing it in a test throws
|
|
135
|
+
`canisterId is required`.
|
|
46
136
|
|
|
47
|
-
##
|
|
137
|
+
## Do Not
|
|
48
138
|
|
|
49
|
-
-
|
|
50
|
-
|
|
139
|
+
- Edit `index.generated.ts`, `index.factories.generated.ts` or
|
|
140
|
+
`declarations/`; edit the `.did` or the plugin options.
|
|
141
|
+
- Hand-write per-method `createQuery` / `createMutation` modules; set
|
|
142
|
+
`factories: true` and customize in `index.ts`.
|
|
143
|
+
- Rely on the `ic_env` cookie outside local development.
|
|
144
|
+
- Point two entries at one output directory.
|
|
145
|
+
- Import generated modules into a React Server Component, or use them per
|
|
146
|
+
request on a server: they are module scope and import hooks.
|
|
51
147
|
|
|
52
|
-
##
|
|
148
|
+
## Docs
|
|
53
149
|
|
|
54
|
-
-
|
|
55
|
-
-
|
|
150
|
+
- Vite plugin: https://ic-reactor.b3pay.net/v3/packages/vite-plugin.md
|
|
151
|
+
- Quick start: https://ic-reactor.b3pay.net/v3/getting-started/quick-start.md
|
|
152
|
+
- Local development: https://ic-reactor.b3pay.net/v3/getting-started/local-development.md
|
|
153
|
+
- Codegen (factory naming rules): https://ic-reactor.b3pay.net/v3/packages/codegen.md
|
|
154
|
+
- Vite plugin example: https://ic-reactor.b3pay.net/v3/examples/vite-plugin.md
|
|
155
|
+
- Index of all docs: https://ic-reactor.b3pay.net/llms.txt
|
|
156
|
+
- Full guide: https://ic-reactor.b3pay.net/llms-full.txt
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ic-reactor/vite-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "Vite plugin for zero-config IC reactor generation from Candid files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
"homepage": "https://ic-reactor.b3pay.net/v3/packages/vite-plugin",
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@ic-reactor/codegen": "0.
|
|
53
|
+
"@ic-reactor/codegen": "0.15.1"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local IC environment that `vite dev` and `vite preview` inject, kept up
|
|
3
|
+
* to date while detection is incomplete.
|
|
4
|
+
*
|
|
5
|
+
* The dev server usually starts before the canisters are deployed, and
|
|
6
|
+
* sometimes before the local network is up. Detection used to run once, in the
|
|
7
|
+
* `config` hook, and its answer was fixed into `server.headers` and
|
|
8
|
+
* `server.proxy` for the server's lifetime. A canister deployed afterwards
|
|
9
|
+
* never reached the `ic_env` cookie, and a network started afterwards never
|
|
10
|
+
* received `/api`, until the dev server was restarted.
|
|
11
|
+
*
|
|
12
|
+
* Detection now runs again for each page load while it is incomplete, that is
|
|
13
|
+
* while `icp` reports no network or a configured canister has no ID. Once
|
|
14
|
+
* every configured canister has an ID on a detected network it stops, and page
|
|
15
|
+
* loads run no `icp` command. A redeploy into a fresh network, with new IDs and
|
|
16
|
+
* a new root key, still needs a restart.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { IncomingMessage, ServerResponse } from "node:http"
|
|
20
|
+
import {
|
|
21
|
+
buildIcEnvCookie,
|
|
22
|
+
getIcEnvironmentInfo,
|
|
23
|
+
type IcEnvironment,
|
|
24
|
+
} from "./env.js"
|
|
25
|
+
|
|
26
|
+
/** Where `/api` goes while `icp` reports no network. */
|
|
27
|
+
export const DEFAULT_LOCAL_REPLICA = "http://127.0.0.1:4943"
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The Internet Identity provider the cookie names when no canister is
|
|
31
|
+
* configured and `icp` reports no network: icp-cli's built-in one, on its
|
|
32
|
+
* default port.
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_INTERNET_IDENTITY_PROVIDER =
|
|
35
|
+
"http://id.ai.localhost:8000/authorize"
|
|
36
|
+
|
|
37
|
+
/** What the dev server injects, from the latest detection. */
|
|
38
|
+
export interface LocalEnvironmentState {
|
|
39
|
+
/** What `icp` reported, or `null` while it has reported no network. */
|
|
40
|
+
environment: IcEnvironment | null
|
|
41
|
+
/** The `ic_env` cookie's value, or `undefined` when no cookie is set. */
|
|
42
|
+
cookie: string | undefined
|
|
43
|
+
/** Where the plugin's `/api` proxy sends requests. */
|
|
44
|
+
proxyTarget: string
|
|
45
|
+
/** Configured canisters with neither a detected nor a configured ID. */
|
|
46
|
+
missingCanisterIds: string[]
|
|
47
|
+
/**
|
|
48
|
+
* `icp` reported a network and every configured canister has an ID, so
|
|
49
|
+
* detection does not run again.
|
|
50
|
+
*/
|
|
51
|
+
complete: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LocalEnvironmentOptions {
|
|
55
|
+
/** The configured canisters' names. */
|
|
56
|
+
canisterNames: string[]
|
|
57
|
+
/** IDs set in the plugin config. They win over detected ones. */
|
|
58
|
+
configuredCanisterIds: Record<string, string>
|
|
59
|
+
/** The directory `icp` runs in. */
|
|
60
|
+
projectRoot: string
|
|
61
|
+
/** Receives each line that explains why an `icp` command failed. */
|
|
62
|
+
onDiagnostic: (message: string) => void
|
|
63
|
+
/**
|
|
64
|
+
* Called after each detection with the state before it, `undefined` for the
|
|
65
|
+
* first, and the state after it.
|
|
66
|
+
*/
|
|
67
|
+
onUpdate?: (
|
|
68
|
+
previous: LocalEnvironmentState | undefined,
|
|
69
|
+
next: LocalEnvironmentState
|
|
70
|
+
) => void
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LocalEnvironment {
|
|
74
|
+
/** The latest detection's result, once one has finished. */
|
|
75
|
+
readonly state: LocalEnvironmentState | undefined
|
|
76
|
+
/**
|
|
77
|
+
* Ask `icp` again and resolve with the new state. A call while a detection
|
|
78
|
+
* runs shares it. Never rejects.
|
|
79
|
+
*/
|
|
80
|
+
detect(): Promise<LocalEnvironmentState>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createLocalEnvironment(
|
|
84
|
+
options: LocalEnvironmentOptions
|
|
85
|
+
): LocalEnvironment {
|
|
86
|
+
const {
|
|
87
|
+
canisterNames,
|
|
88
|
+
configuredCanisterIds,
|
|
89
|
+
projectRoot,
|
|
90
|
+
onDiagnostic,
|
|
91
|
+
onUpdate,
|
|
92
|
+
} = options
|
|
93
|
+
|
|
94
|
+
// Two entries can share a name. internet_identity is looked up as well, but
|
|
95
|
+
// it is routinely not deployed, so it is not needed for detection to be
|
|
96
|
+
// complete.
|
|
97
|
+
const requiredNames = [...new Set(canisterNames)]
|
|
98
|
+
const lookupNames = requiredNames.includes("internet_identity")
|
|
99
|
+
? requiredNames
|
|
100
|
+
: [...requiredNames, "internet_identity"]
|
|
101
|
+
|
|
102
|
+
let state: LocalEnvironmentState | undefined
|
|
103
|
+
let running: Promise<LocalEnvironmentState> | undefined
|
|
104
|
+
|
|
105
|
+
const toState = (
|
|
106
|
+
environment: IcEnvironment | null
|
|
107
|
+
): LocalEnvironmentState => {
|
|
108
|
+
const canisterIds = {
|
|
109
|
+
...environment?.canisterIds,
|
|
110
|
+
...configuredCanisterIds,
|
|
111
|
+
}
|
|
112
|
+
const missingCanisterIds = requiredNames.filter(
|
|
113
|
+
(name) => !canisterIds[name]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
let cookie: string | undefined
|
|
117
|
+
if (environment) {
|
|
118
|
+
cookie = buildIcEnvCookie(
|
|
119
|
+
canisterIds,
|
|
120
|
+
environment.rootKey,
|
|
121
|
+
environment.internetIdentityProvider
|
|
122
|
+
)
|
|
123
|
+
} else if (requiredNames.length === 0) {
|
|
124
|
+
// Env-only mode: there are no canister IDs to carry, but the app can
|
|
125
|
+
// still sign in with icp-cli's built-in Internet Identity.
|
|
126
|
+
cookie = buildIcEnvCookie(
|
|
127
|
+
{},
|
|
128
|
+
undefined,
|
|
129
|
+
DEFAULT_INTERNET_IDENTITY_PROVIDER
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
environment,
|
|
135
|
+
cookie,
|
|
136
|
+
proxyTarget: environment?.proxyTarget ?? DEFAULT_LOCAL_REPLICA,
|
|
137
|
+
missingCanisterIds,
|
|
138
|
+
complete: environment !== null && missingCanisterIds.length === 0,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const run = async (): Promise<LocalEnvironmentState> => {
|
|
143
|
+
const previous = state
|
|
144
|
+
try {
|
|
145
|
+
const { environment, diagnostics } = await getIcEnvironmentInfo(
|
|
146
|
+
lookupNames,
|
|
147
|
+
projectRoot
|
|
148
|
+
)
|
|
149
|
+
diagnostics.forEach(onDiagnostic)
|
|
150
|
+
state = toState(
|
|
151
|
+
mergeDetections(previous?.environment ?? null, environment)
|
|
152
|
+
)
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// getIcEnvironmentInfo reports failures in its result. This is for
|
|
155
|
+
// anything else, which must not fail the page request that waits here.
|
|
156
|
+
onDiagnostic(
|
|
157
|
+
`Detecting the local IC environment failed: ${
|
|
158
|
+
error instanceof Error ? error.message : String(error)
|
|
159
|
+
}`
|
|
160
|
+
)
|
|
161
|
+
state = previous ?? toState(null)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
onUpdate?.(previous, state)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
onDiagnostic(
|
|
168
|
+
`Applying the detected IC environment failed: ${
|
|
169
|
+
error instanceof Error ? error.message : String(error)
|
|
170
|
+
}`
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
return state
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
get state() {
|
|
178
|
+
return state
|
|
179
|
+
},
|
|
180
|
+
detect() {
|
|
181
|
+
running ??= run().finally(() => {
|
|
182
|
+
running = undefined
|
|
183
|
+
})
|
|
184
|
+
return running
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Combine a detection with the one before it.
|
|
191
|
+
*
|
|
192
|
+
* `icp` can fail for a moment, during a deploy say. A detection that finds no
|
|
193
|
+
* network keeps the last one that did, and on the same network, which the
|
|
194
|
+
* root key identifies, a canister keeps the ID it had. A cookie that works is
|
|
195
|
+
* not taken away by a command that failed once.
|
|
196
|
+
*/
|
|
197
|
+
function mergeDetections(
|
|
198
|
+
previous: IcEnvironment | null,
|
|
199
|
+
next: IcEnvironment | null
|
|
200
|
+
): IcEnvironment | null {
|
|
201
|
+
if (!next) return previous
|
|
202
|
+
if (!previous || previous.rootKey !== next.rootKey) return next
|
|
203
|
+
const canisterIds = { ...previous.canisterIds, ...next.canisterIds }
|
|
204
|
+
return {
|
|
205
|
+
...next,
|
|
206
|
+
canisterIds,
|
|
207
|
+
// The built-in provider stands in only for a project that has no
|
|
208
|
+
// internet_identity canister, as getIcEnvironmentInfo decides it.
|
|
209
|
+
internetIdentityProvider: canisterIds.internet_identity
|
|
210
|
+
? undefined
|
|
211
|
+
: next.internetIdentityProvider,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Whether `req` loads a page, which is when the app reads the `ic_env` cookie.
|
|
217
|
+
*
|
|
218
|
+
* Browsers put `text/html` in the Accept header of a navigation, and not in
|
|
219
|
+
* that of a script, a stylesheet, a `fetch` call or an HMR request.
|
|
220
|
+
*/
|
|
221
|
+
export function isDocumentRequest(req: IncomingMessage): boolean {
|
|
222
|
+
if (req.method !== "GET" && req.method !== "HEAD") return false
|
|
223
|
+
const { accept } = req.headers
|
|
224
|
+
return typeof accept === "string" && accept.includes("text/html")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Add the `ic_env` cookie to `res`, keeping any other cookie set on it. */
|
|
228
|
+
function setIcEnvCookie(res: ServerResponse, value: string): void {
|
|
229
|
+
const existing = res.getHeader("Set-Cookie")
|
|
230
|
+
const others = (
|
|
231
|
+
existing === undefined
|
|
232
|
+
? []
|
|
233
|
+
: Array.isArray(existing)
|
|
234
|
+
? existing
|
|
235
|
+
: [String(existing)]
|
|
236
|
+
).filter((cookie) => !cookie.startsWith("ic_env="))
|
|
237
|
+
const cookie = `ic_env=${value}; Path=/; SameSite=Lax;`
|
|
238
|
+
res.setHeader("Set-Cookie", others.length > 0 ? [...others, cookie] : cookie)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Connect middleware that sets the `ic_env` cookie on each response.
|
|
243
|
+
*
|
|
244
|
+
* For a page load while detection is incomplete, it first asks `icp` again and
|
|
245
|
+
* waits for the answer, so the page that loads after a deploy already carries
|
|
246
|
+
* the new ID. Other requests, and every request once detection is complete,
|
|
247
|
+
* get the cookie from the latest detection without running `icp`.
|
|
248
|
+
*/
|
|
249
|
+
export function icEnvMiddleware(environment: LocalEnvironment) {
|
|
250
|
+
return (
|
|
251
|
+
req: IncomingMessage,
|
|
252
|
+
res: ServerResponse,
|
|
253
|
+
next: (error?: unknown) => void
|
|
254
|
+
): void => {
|
|
255
|
+
const respond = (state: LocalEnvironmentState | undefined) => {
|
|
256
|
+
if (state?.cookie !== undefined) setIcEnvCookie(res, state.cookie)
|
|
257
|
+
next()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const { state } = environment
|
|
261
|
+
if (state?.complete || !isDocumentRequest(req)) {
|
|
262
|
+
respond(state)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
void environment.detect().then(respond)
|
|
267
|
+
}
|
|
268
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and building the `ic_env` cookie for the browser.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { execFile } from "child_process"
|
|
9
9
|
|
|
10
10
|
export interface IcEnvironment {
|
|
11
11
|
environment: string
|
|
@@ -27,6 +27,45 @@ export interface IcEnvironmentDetection {
|
|
|
27
27
|
diagnostics: string[]
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* How long one `icp` command may run. The dev server runs detection while a
|
|
32
|
+
* page request waits for it, so a command that never exits must not hold the
|
|
33
|
+
* page forever.
|
|
34
|
+
*/
|
|
35
|
+
const ICP_TIMEOUT_MS = 10_000
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run `icp` with `args` in `cwd` and resolve with its stdout.
|
|
39
|
+
*
|
|
40
|
+
* Asynchronous, so a page request that waits for detection does not block the
|
|
41
|
+
* dev server's other requests. stderr is captured rather than shown, so a
|
|
42
|
+
* failure can explain itself: it only reaches the terminal if the caller prints
|
|
43
|
+
* the diagnostics. stdin is closed, so a command that asks a question fails
|
|
44
|
+
* instead of waiting for an answer.
|
|
45
|
+
*/
|
|
46
|
+
function runIcp(args: string[], cwd: string): Promise<string> {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
const child = execFile(
|
|
49
|
+
"icp",
|
|
50
|
+
args,
|
|
51
|
+
{
|
|
52
|
+
cwd,
|
|
53
|
+
encoding: "utf-8",
|
|
54
|
+
timeout: ICP_TIMEOUT_MS,
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
},
|
|
57
|
+
(error, stdout, stderr) => {
|
|
58
|
+
if (error) {
|
|
59
|
+
reject(Object.assign(error, { stderr }))
|
|
60
|
+
} else {
|
|
61
|
+
resolve(stdout)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
child.stdin?.end()
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
30
69
|
/**
|
|
31
70
|
* Detect the IC environment using the `icp` CLI.
|
|
32
71
|
*
|
|
@@ -34,23 +73,19 @@ export interface IcEnvironmentDetection {
|
|
|
34
73
|
* looking for `icp.yaml` there and in each parent directory, so this has to be
|
|
35
74
|
* the app's root and not wherever the process happened to start.
|
|
36
75
|
*/
|
|
37
|
-
export function getIcEnvironmentInfo(
|
|
76
|
+
export async function getIcEnvironmentInfo(
|
|
38
77
|
canisterNames: string[],
|
|
39
78
|
projectRoot: string = process.cwd()
|
|
40
|
-
): IcEnvironmentDetection {
|
|
79
|
+
): Promise<IcEnvironmentDetection> {
|
|
41
80
|
const networkName = process.env.ICP_ENVIRONMENT || "local"
|
|
42
81
|
const diagnostics: string[] = []
|
|
43
82
|
|
|
44
83
|
try {
|
|
45
84
|
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
|
-
})
|
|
85
|
+
await runIcp(
|
|
86
|
+
["network", "status", "-e", networkName, "--json"],
|
|
87
|
+
projectRoot
|
|
88
|
+
)
|
|
54
89
|
)
|
|
55
90
|
|
|
56
91
|
const rootKey = networkStatus.root_key
|
|
@@ -70,16 +105,15 @@ export function getIcEnvironmentInfo(
|
|
|
70
105
|
|
|
71
106
|
const canisterIds: Record<string, string> = {}
|
|
72
107
|
|
|
108
|
+
// One at a time, as icp is run from a terminal. Each command reads the
|
|
109
|
+
// project's state, and these are not known to be safe to run at once.
|
|
73
110
|
for (const name of canisterNames) {
|
|
74
111
|
try {
|
|
75
|
-
const canisterId =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
encoding: "utf-8",
|
|
81
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
82
|
-
}
|
|
112
|
+
const canisterId = (
|
|
113
|
+
await runIcp(
|
|
114
|
+
["canister", "status", name, "-e", networkName, "-i"],
|
|
115
|
+
projectRoot
|
|
116
|
+
)
|
|
83
117
|
).trim()
|
|
84
118
|
|
|
85
119
|
if (canisterId) {
|
|
@@ -143,7 +177,7 @@ export function buildIcEnvCookie(
|
|
|
143
177
|
}
|
|
144
178
|
|
|
145
179
|
/**
|
|
146
|
-
* Turn whatever `
|
|
180
|
+
* Turn whatever `icp` failed with into one readable line.
|
|
147
181
|
*
|
|
148
182
|
* The interesting part is almost always the captured stderr — the thrown
|
|
149
183
|
* Error's own message is just "Command failed: icp ..." — but stderr is absent
|