Skip to content

Commit 85bd730

Browse files
authored
feat: comprehensive icp-cli skill improvements for dfx migration (#76)
1 parent 84a6aa9 commit 85bd730

1 file changed

Lines changed: 210 additions & 25 deletions

File tree

skills/icp-cli/SKILL.md

Lines changed: 210 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,28 @@ The `icp` command-line tool builds and deploys applications on the Internet Comp
8181
- npm run build
8282
```
8383

84+
10. **Expecting `output_env_file` or `.env` with canister IDs.** dfx writes canister IDs to a `.env` file (`CANISTER_ID_BACKEND=...`) via `output_env_file`. icp-cli does not generate `.env` files. Instead, it injects canister IDs as environment variables (`PUBLIC_CANISTER_ID:<name>`) directly into canisters during `icp deploy`. Frontends read these from the `ic_env` cookie set by the asset canister. Remove `output_env_file` from your config and any code that reads `CANISTER_ID_*` from `.env` — use the `ic_env` cookie instead (see Canister Environment Variables below).
85+
86+
11. **Expecting `dfx generate` for TypeScript bindings.** icp-cli does not have a `dfx generate` equivalent. Use `@icp-sdk/bindgen` (a Vite plugin) to generate TypeScript bindings from `.did` files at build time. The `.did` file must exist on disk — either commit it to the repo, or generate it with `icp build` first (recipes auto-generate it when `candid` is not specified). See Binding Generation below.
87+
88+
12. **Misunderstanding Candid file generation with recipes.** When using the Rust or Motoko recipe:
89+
- If `candid` is **specified**: the file must already exist (checked in or manually created). The recipe uses it as-is and does **not** generate one.
90+
- If `candid` is **omitted**: the recipe auto-generates the `.did` file from the compiled WASM (via `candid-extractor` for Rust, `moc` for Motoko). The generated file is placed in the build cache, not at a predictable project path.
91+
92+
For projects that need a `.did` file on disk (e.g., for `@icp-sdk/bindgen`), the recommended pattern is: generate the `.did` file once, commit it, and specify `candid` in the recipe config. To generate it manually:
93+
94+
**Rust** — build the WASM first, then extract the Candid interface:
95+
```bash
96+
cargo install candid-extractor # one-time setup
97+
icp build backend
98+
candid-extractor target/wasm32-unknown-unknown/release/backend.wasm > backend/backend.did
99+
```
100+
101+
**Motoko** — use `moc` directly with the `--idl` flag:
102+
```bash
103+
$(mops toolchain bin moc) --idl $(mops sources) -o backend/backend.did backend/app.mo
104+
```
105+
84106
## How It Works
85107

86108
### Project Creation
@@ -188,6 +210,12 @@ For multi-canister projects, list all canisters in the same `canisters` array. i
188210

189211
### Custom build steps (no recipe)
190212

213+
When not using a recipe, only `name`, `build`, `sync`, `settings`, and `init_args` are valid canister-level fields. There are no `wasm`, `candid`, or `metadata` fields — handle these in the build script instead:
214+
215+
- **WASM output**: copy the final WASM to `$ICP_WASM_OUTPUT_PATH`
216+
- **Candid metadata**: use `ic-wasm` to embed `candid:service` metadata
217+
- **Candid file**: the `.did` file is referenced only in the `ic-wasm` command, not as a YAML field
218+
191219
```yaml
192220
canisters:
193221
- name: backend
@@ -197,10 +225,9 @@ canisters:
197225
commands:
198226
- cargo build --target wasm32-unknown-unknown --release
199227
- cp target/wasm32-unknown-unknown/release/backend.wasm "$ICP_WASM_OUTPUT_PATH"
228+
- ic-wasm "$ICP_WASM_OUTPUT_PATH" -o "$ICP_WASM_OUTPUT_PATH" metadata candid:service -f backend/backend.did -v public --keep-name-section
200229
```
201230

202-
`ICP_WASM_OUTPUT_PATH` is an environment variable that tells your build script where to place the final WASM file.
203-
204231
### Available recipes
205232

206233
| Recipe | Purpose |
@@ -212,8 +239,170 @@ canisters:
212239

213240
Use `icp project show` to see the effective configuration after recipe expansion.
214241

242+
### Canister Environment Variables
243+
244+
icp-cli automatically injects all canister IDs as environment variables during `icp deploy`. Variables are formatted as `PUBLIC_CANISTER_ID:<canister-name>` and injected into every canister in the environment.
245+
246+
**Frontend → Backend** (reading canister IDs in JavaScript):
247+
248+
Asset canisters expose injected variables through a cookie named `ic_env`, set on all HTML responses. Use `@icp-sdk/core` to read it:
249+
```js
250+
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
251+
252+
const canisterEnv = safeGetCanisterEnv();
253+
const backendId = canisterEnv?.["PUBLIC_CANISTER_ID:backend"];
254+
```
255+
256+
**Backend → Backend** (reading canister IDs in canister code):
257+
- Rust: `ic_cdk::api::env_var_value("PUBLIC_CANISTER_ID:other_canister")`
258+
- Motoko (motoko-core v2.1.0+):
259+
```motoko
260+
import Runtime "mo:core/Runtime";
261+
let otherId = Runtime.envVar("PUBLIC_CANISTER_ID:other_canister");
262+
```
263+
264+
Note: variables are only updated for canisters being deployed. When adding a new canister, run `icp deploy` (without specifying a canister name) to update all canisters with the complete ID set.
265+
266+
### Binding Generation
267+
268+
icp-cli does not have a built-in `dfx generate` command. Use `@icp-sdk/bindgen` to generate TypeScript bindings from `.did` files.
269+
270+
**Vite plugin** (recommended for Vite-based frontend projects):
271+
```js
272+
// vite.config.js
273+
import { icpBindgen } from "@icp-sdk/bindgen/plugins/vite";
274+
275+
export default defineConfig({
276+
plugins: [
277+
// Add one icpBindgen() call per canister the frontend needs to access
278+
icpBindgen({
279+
didFile: "../backend/backend.did",
280+
outDir: "./src/bindings/backend",
281+
}),
282+
icpBindgen({
283+
didFile: "../other/other.did",
284+
outDir: "./src/bindings/other",
285+
}),
286+
],
287+
});
288+
```
289+
290+
Each `icpBindgen()` instance generates a `createActor` function in its `outDir`. Add `**/src/bindings/` to `.gitignore`.
291+
292+
**Creating actors from bindings** — connect the generated bindings with the `ic_env` cookie:
293+
```js
294+
// src/actor.js
295+
import { safeGetCanisterEnv } from "@icp-sdk/core/agent/canister-env";
296+
import { createActor } from "./bindings/backend";
297+
// For additional canisters: import { createActor as createOther } from "./bindings/other";
298+
299+
const canisterEnv = safeGetCanisterEnv();
300+
const agentOptions = {
301+
host: window.location.origin,
302+
rootKey: canisterEnv?.IC_ROOT_KEY,
303+
};
304+
305+
export const backend = createActor(
306+
canisterEnv?.["PUBLIC_CANISTER_ID:backend"],
307+
{ agentOptions }
308+
);
309+
// Repeat for each canister: createOther(canisterEnv?.["PUBLIC_CANISTER_ID:other"], { agentOptions })
310+
```
311+
312+
**Non-Vite frontends** — use the `@icp-sdk/bindgen` CLI to generate bindings manually:
313+
```bash
314+
npx @icp-sdk/bindgen --did ../backend/backend.did --out ./src/bindings/backend
315+
```
316+
317+
**Requirements:**
318+
- The `.did` file must exist on disk. If using a recipe with `candid` specified, the file must be committed. If `candid` is omitted, run `icp build` first to auto-generate it.
319+
- `@icp-sdk/bindgen` generates code that depends on `@icp-sdk/core`. Projects using `@dfinity/agent` must upgrade to `@icp-sdk/core` + `@icp-sdk/bindgen`. This is not optional — there is no way to generate TypeScript bindings with icp-cli while staying on `@dfinity/agent`.
320+
321+
### Dev Server Configuration (Vite)
322+
323+
In development, the Vite dev server must simulate the `ic_env` cookie that the asset canister provides in production. Query the local network for the root key, canister IDs, and API URL:
324+
325+
```js
326+
// vite.config.js
327+
import { execSync } from "child_process";
328+
329+
const environment = process.env.ICP_ENVIRONMENT || "local";
330+
// List all backend canisters the frontend needs to access
331+
const CANISTER_NAMES = ["backend", "other"];
332+
333+
function getCanisterId(name) {
334+
// `-i` makes the command return only the identity of the canister
335+
return execSync(`icp canister status ${name} -e ${environment} -i`, {
336+
encoding: "utf-8", stdio: "pipe",
337+
}).trim();
338+
}
339+
340+
function getDevServerConfig() {
341+
const networkStatus = JSON.parse(
342+
execSync(`icp network status -e ${environment} --json`, {
343+
encoding: "utf-8",
344+
})
345+
);
346+
const canisterParams = CANISTER_NAMES
347+
.map((name) => `PUBLIC_CANISTER_ID:${name}=${getCanisterId(name)}`)
348+
.join("&");
349+
return {
350+
headers: {
351+
"Set-Cookie": `ic_env=${encodeURIComponent(
352+
`${canisterParams}&ic_root_key=${networkStatus.root_key}`
353+
)}; SameSite=Lax;`,
354+
},
355+
proxy: {
356+
"/api": { target: networkStatus.api_url, changeOrigin: true },
357+
},
358+
};
359+
}
360+
```
361+
362+
Key differences from dfx:
363+
- The proxy target and root key come from `icp network status --json` (no hardcoded ports)
364+
- Canister IDs come from `icp canister status <name> -e <env> -i` (no `.env` file)
365+
- The `ic_env` cookie replaces dfx's `CANISTER_ID_*` environment variables
366+
- `ICP_ENVIRONMENT` lets the dev server target any environment (local, staging, ic)
367+
215368
## dfx → icp Migration
216369

370+
### Local network port change
371+
372+
dfx serves the local network on port `4943`. icp-cli uses port `8000`. When migrating, search the project for hardcoded references to `4943` (or `localhost:4943`) and update them to `8000`. Better yet, use `icp network status --json` to get the `api_url` dynamically (see Dev Server Configuration above). Common locations to check:
373+
- Vite/webpack proxy configs (e.g., `vite.config.ts`)
374+
- README documentation
375+
- Test fixtures and scripts
376+
377+
### Remove `.env` file and `output_env_file`
378+
379+
dfx generates a `.env` file with `CANISTER_ID_*` variables via `output_env_file` in `dfx.json`. icp-cli does not use `.env` files for canister IDs — remove `output_env_file` from config and delete any dfx-generated `.env` file. Also remove dfx-specific environment variables from `.env` files (e.g., `DFX_NETWORK`, `NETWORK`).
380+
381+
Replace code that reads `process.env.CANISTER_ID_*` with the `ic_env` cookie pattern (see Canister Environment Variables above).
382+
383+
### Frontend package migration
384+
385+
Since `@icp-sdk/bindgen` generates code that depends on `@icp-sdk/core`, projects with TypeScript bindings **must** upgrade from `@dfinity/*` packages. This is not optional — `dfx generate` does not exist in icp-cli, and `@icp-sdk/bindgen` is the only supported way to generate bindings.
386+
387+
| Remove | Replace with |
388+
|--------|-------------|
389+
| `@dfinity/agent` | `@icp-sdk/core` |
390+
| `@dfinity/candid` | `@icp-sdk/core` |
391+
| `@dfinity/principal` | `@icp-sdk/core` |
392+
| `dfx generate` (declarations) | `@icp-sdk/bindgen` (Vite plugin or CLI) |
393+
| `vite-plugin-environment` | Not needed — use `ic_env` cookie |
394+
| `src/declarations/` (generated by dfx) | `src/bindings/` (generated by `@icp-sdk/bindgen`) |
395+
396+
Steps:
397+
1. `npm uninstall @dfinity/agent @dfinity/candid @dfinity/principal vite-plugin-environment`
398+
2. `npm install @icp-sdk/core @icp-sdk/bindgen`
399+
3. Delete `src/declarations/` (dfx-generated bindings)
400+
4. Add `**/src/bindings/` to `.gitignore`
401+
5. Commit the `.did` file(s) used by bindgen
402+
6. Add `icpBindgen()` to `vite.config.js` (see Binding Generation above)
403+
7. Replace actor setup code: use `safeGetCanisterEnv` from `@icp-sdk/core` + `createActor` from generated bindings (see Creating actors from bindings above)
404+
8. Remove `process.env.CANISTER_ID_*` references — use the `ic_env` cookie instead
405+
217406
### Command mapping
218407

219408
| Task | dfx | icp |
@@ -249,6 +438,8 @@ Use `icp project show` to see the effective configuration after recipe expansion
249438
| `"main": "X"` | `recipe.configuration.main: X` |
250439
| `"source": ["dist"]` | `recipe.configuration.dir: dist` |
251440
| `"dependencies": [...]` | Not needed — use Canister Environment Variables |
441+
| `"output_env_file": ".env"` | Not needed — use `ic_env` cookie |
442+
| `dfx generate` | `@icp-sdk/bindgen` Vite plugin |
252443
| `--network ic` | `-e ic` |
253444

254445
### Identity migration
@@ -266,7 +457,7 @@ icp identity principal --identity my-identity
266457

267458
### Canister ID migration
268459

269-
If you have existing mainnet canisters managed by dfx, create the mapping file:
460+
If you have existing mainnet canisters managed by dfx, migrate the IDs from `canister_ids.json` to icp-cli's mapping file:
270461

271462
```bash
272463
# Get IDs from dfx
@@ -282,30 +473,24 @@ cat > .icp/data/mappings/ic.ids.json << 'EOF'
282473
}
283474
EOF
284475

476+
# Delete the dfx canister ID file — icp-cli uses .icp/data/mappings/ instead
477+
rm -f canister_ids.json
478+
285479
# Commit to version control
286480
git add .icp/data/
287481
```
288482

289-
## Verify It Works
483+
## Post-Migration Verification
290484

291-
```bash
292-
# 1. Create and deploy a project locally
293-
icp new my-test --subfolder hello-world \
294-
--define backend_type=motoko \
295-
--define frontend_type=react \
296-
--define network_type=Default && cd my-test
297-
icp network start -d
298-
icp deploy
299-
# Expected: Canisters deployed successfully
300-
301-
# 2. Call the backend
302-
icp canister call backend greet '("World")'
303-
# Expected: ("Hello, World!")
304-
305-
# 3. Check effective configuration (recipe expansion)
306-
icp project show
307-
# Expected: Expanded recipe configuration
308-
309-
# 4. Stop local network
310-
icp network stop
311-
```
485+
After migrating a project from dfx to icp-cli, verify the following:
486+
487+
1. **Deleted files**: `dfx.json` and `canister_ids.json` no longer exist
488+
2. **Created files**: `icp.yaml` exists. `.icp/data/mappings/ic.ids.json` exists and is committed (if project has mainnet canisters)
489+
3. **`.gitignore`**: contains `.icp/cache/`, does not contain `.dfx`
490+
4. **No stale port references**: search the codebase for `4943` — there should be zero matches
491+
5. **No dfx env patterns**: search for `output_env_file`, `CANISTER_ID_`, `DFX_NETWORK` — there should be zero matches in config and source files
492+
6. **Frontend packages** (if project has TypeScript bindings): `@dfinity/agent` is not in `package.json`, `@icp-sdk/core` and `@icp-sdk/bindgen` are. `src/declarations/` is deleted, `src/bindings/` is in `.gitignore`
493+
7. **Candid files**: `.did` files used by `@icp-sdk/bindgen` are committed
494+
8. **Build succeeds**: `icp build` completes without errors
495+
9. **Config is correct**: `icp project show` displays the expected expanded configuration
496+
10. **README**: references `icp` commands (not `dfx`), says "local network" (not "replica"), shows correct port

0 commit comments

Comments
 (0)