# The Vite plugin

`@ic-reactor/vite-plugin` is for an app built on a module that
`candid-core-cli gen` generates from a `.did` file. It does two things, and it
exports only `icReactor` and the type `IcReactorPluginOptions`:

- **Generation.** It runs `candid-core-cli gen` on each configured `.did` file
  when a build or the dev server starts, and again when that file changes. The
  module is the generator's, as it wrote it: the plugin adds no wrapper files,
  hooks or reactors.
- **Environment.** Under `vite dev` and `vite preview` it sets the `ic_env`
  cookie and proxies `/api` to the local IC network, so the app finds its
  canister ids and the replica's root key without configuration.

## Install

The plugin runs the `@candid-core/cli` that your app installs, and that CLI has
to pair with the `@candid-core/schema` runtime the generated modules import.
Both are pinned to one exact release, as candid-core asks of every release
before 1.0, and so is the plugin's peer on the CLI:

```sh
npm install --save-exact @candid-core/schema@0.3.0
npm install --save-dev --save-exact @candid-core/cli@0.2.0
npm install --save-dev @ic-reactor/vite-plugin@beta
```

The plugin imports neither package, and no `@ic-reactor` runtime. Its peers are
the CLI and Vite 4.2 up to 8. See [@ic-reactor/vite-plugin](https://ic-reactor.b3pay.net/v4/packages/vite-plugin/).

## Quick start

```ts
// vite.config.ts
import { icReactor } from "@ic-reactor/vite-plugin"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [
    icReactor({
      canisters: {
        icrc1: { didFile: "did/icrc1.did" },
      },
    }),
  ],
})
```

On `vite dev` and `vite build` this writes `src/canisters/icrc1.ts`, the
generated module (it exports `actor` and the type `Actor`), and
`src/canisters/icrc1.envelope.json` beside it. The generator names its output
after the `.did` file, not after the key in `canisters`. The app imports it:

```ts
import { createClient } from "@ic-reactor/core"
import { actor, type Actor } from "./canisters/icrc1"

const client = createClient({ network: "ic", identity: "anonymous" })
const ledger = client.canister<Actor>(actor, {
  id: "ryjl3-tyaaa-aaaaa-aaaba-cai",
})
```

Without an `outDir` the plugin writes to `src/canisters`. To match the
`./generated/...` imports of [Getting started](https://ic-reactor.b3pay.net/v4/guides/getting-started/), set
`outDir: "src/generated"`.

The first line the plugin logs says where an agent reads how to use the
library: `ic-reactor: agent guide at node_modules/@ic-reactor/core/llms.txt`.

## Options

| Option              | Default                   | Meaning                                                                                      |
| ------------------- | ------------------------- | -------------------------------------------------------------------------------------------- |
| `canisters`         | `{}`                      | The app's canisters, by their name in the `icp` project                                      |
| `injectEnvironment` | `true`                    | Set the cookie and the `/api` proxy under `vite dev` and `vite preview`                      |
| `failOnError`       | build `true`, dev `false` | Abort the Vite run when a canister fails to generate. See [Failures](#when-generation-fails) |

Each entry of `canisters` takes:

| Field        | Default           | Meaning                                                                                |
| ------------ | ----------------- | -------------------------------------------------------------------------------------- |
| `didFile`    | none              | The canister's Candid file, relative to the Vite root. Without it nothing is generated |
| `outDir`     | `"src/canisters"` | Where the generator writes, relative to the Vite root                                  |
| `canisterId` | none              | A fixed id for the cookie, which wins over the one `icp` reports                       |

The key of an entry is the canister's name in the `icp` project, and it is the
name the cookie carries its id under. A canister without a `didFile` generates
nothing and is only named in the cookie.

### Canisters that share an interface

An ICP ledger and a ckBTC ledger can both use `icrc1.did`. They are two
entries, so that the cookie carries each id, and one generated module:

```ts
// vite.config.ts
import { icReactor } from "@ic-reactor/vite-plugin"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [
    icReactor({
      canisters: {
        icp_ledger: { didFile: "did/icrc1.did" },
        ckbtc_ledger: { didFile: "did/icrc1.did" },
      },
    }),
  ],
})
```

The generator is given `icrc1.did` once, writes `src/canisters/icrc1.ts` once,
and a save of the file regenerates it once.

Different `.did` files that name the same module cannot share an `outDir`:
`a/ledger.did` and `b/ledger.did` both mean `ledger.ts`, and the second is
refused with a message naming both. Give one an `outDir` of its own. Names that
differ only in case (`Ledger.did`, `ledger.did`) collide only where the
filesystem of the `outDir` ignores case, as macOS and Windows do by default.

## Generation

The generator is WebAssembly, so the plugin does not load it. It runs the bin
script of the `@candid-core/cli` installed for your app (resolved from the Vite
root, so a monorepo's hoisted copy is found) with the running Node binary, in a
child process and not through a shell. A trap, a crash or a runaway loop on a
bad `.did` ends that process, and the plugin reports it. The dev server is not
affected. The command it runs is
`gen <didFiles...> -o <outDir> --json`.

- Canisters that write into one `outDir` share one process, and each `outDir`
  has its own. A process that dies without a report is run again, one canister
  at a time, so the failure lands on the canister that caused it.
- A process that runs longer than 60 seconds is killed. A hang therefore costs
  up to two timeouts (120 seconds) before the others are generated.
- What the generator writes to stderr is logged as a warning, a line at a time.
  Past 200 lines from one process the rest is not logged.
- A declaration the generator cannot represent is left out of the module, and
  the plugin logs each one as a warning, for example
  `ic-reactor: ledger: omitted declaration Bad (reserved_field_name)`.
- Editing a `.did` regenerates only the canisters that name it. Under
  `vite build --watch`, a canister is regenerated only when its `.did` text
  changed.
- Deleting a `.did` under `vite dev` fails the canisters that name it at once,
  because the module it generated is still on disk and would look current.
  Putting the file back regenerates them.

### When generation fails

A failed canister costs that canister, and nothing else:

- Under `vite build`, the build fails with the generator's diagnostics (or its
  stderr, if it crashed) in the error message. A build that exits 0 would ship
  the bindings left over from the last good run.
- Under `vite dev`, the failure is logged and shown in the browser's error
  overlay, and the server keeps serving. Fixing the last broken canister clears
  the overlay. Vite's client clears an error overlay whenever it applies a hot
  update, so a canister that is still broken can drop out of the overlay after
  you save another file. Its error stays in the terminal log, and a full
  reload shows the overlay again.
- `failOnError` overrides either default.

### In CI

`candid-core-cli gen --check` compares the generated files with the ones on
disk, writes nothing, and exits 1 on any difference. Run it with the arguments
the plugin uses, to fail a pipeline when the committed output is stale:

```json
{
  "scripts": {
    "gen:check": "candid-core-cli gen did/icrc1.did -o src/canisters --check"
  }
}
```

## The local environment

When `injectEnvironment` is on, under `vite dev` and `vite preview`, the plugin:

1. asks `icp` for the local network's status (`icp network status`);
2. resolves canister ids: the keys of `canisters`, and `internet_identity`,
   which is added if it is not listed;
3. sets the `ic_env` cookie on each response;
4. proxies `/api` to the local replica.

A `canisterId` in the plugin config overrides the detected id for that
canister. Set the `ICP_ENVIRONMENT` environment variable to use a network other
than `"local"`.

### The cookie

The cookie is `ic_env`, set with `Path=/; SameSite=Lax`. Its value is URI-encoded
text of this form:

```text
ic_root_key=<hex key>&PUBLIC_CANISTER_ID:<name>=<id>&...&INTERNET_IDENTITY_PROVIDER=<url>
```

It carries the replica's root key, one `PUBLIC_CANISTER_ID:<name>` for each
canister, and the local Internet Identity's url when there is one.

### Detection

- If detection fails, the plugin falls back to proxying `/api` to
  `http://127.0.0.1:4943` and sets no cookie, or with no canisters configured,
  one that names only the built-in Internet Identity. It warns when that
  happens with canisters configured, and when a configured canister has no id.
  Run with `DEBUG=ic-reactor` to see the `icp` output behind the warning.
- Detection is complete once `icp` reports the network and every configured
  canister has an id. Until then the plugin asks `icp` again on each page load,
  so start `vite dev` first, run `icp network start` and `icp deploy`, and
  reload the page.
- Once detection is complete, page loads run no more `icp` commands. Deploying
  into a fresh network, with new ids and a new root key, needs a restart of the
  dev server.
- A configured canister you never deploy locally keeps detection incomplete.
  Set its `canisterId` and it counts as resolved. If you never run a local
  network, set `injectEnvironment: false`.
- If your Vite config or another plugin sets `server.proxy["/api"]`, the plugin
  leaves it alone.

### How the app reads it

The app reads the cookie through `network: "env"` and calls canisters by name:

```ts
import { createClient } from "@ic-reactor/core"
import { actor, type Actor } from "./canisters/backend"

const client = createClient({ network: "env", identity: "anonymous" })
const backend = client.canister<Actor>(actor, { name: "backend" })
```

On a local page the client trusts the cookie, so `{ name: "backend" }` finds the
id that `icp deploy` gave the backend. Nothing in the code names a host, a port
or an id. Deployed on a mainnet domain, `"env"` still sends through the page's
origin, but the client no longer believes the cookie, so a `{ name }` target
does not resolve there: give the canister an `{ id }`, or write
`allowEnvConfig: true` when every subdomain of the page's domain is trusted.
See [The client](https://ic-reactor.b3pay.net/v4/guides/client/#the-ic_env-cookie) for when the cookie is
trusted. The [Vite wallet example](https://ic-reactor.b3pay.net/v4/examples/vite-wallet/) is built this way
for a local network.

## Tests

Vitest runs the plugin in mode `test`. There it injects no environment and
never runs `icp`, whatever `injectEnvironment` says. Generation is not an
environment concern and still runs, so the modules a test imports exist. To
test canister code, see [Testing](https://ic-reactor.b3pay.net/v4/guides/testing/).