From 5b553fa2fa4cd77f0d1fc4b62e7c62f8d0ba4c09 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Fri, 22 May 2026 15:24:26 +0200 Subject: [PATCH 1/3] ci(internal): migrate local pipeline + non-release CI from dfx to icp-cli Switch `npm run replica`/`deploy-local` and the corresponding ci.yml steps to icp-cli + pocket-ic; mops-test.yml now relies on `[toolchain].pocket-ic` in mops.toml instead of dfx. Production deploys (release.yml, deploy-staging, deploy-ic) and `dfx generate` for declarations stay on dfx. LANG-1311 Co-authored-by: Cursor --- .github/workflows/ci.yml | 9 ++++ .github/workflows/mops-test.yml | 2 - .gitignore | 1 + AGENTS.md | 5 +- frontend/vite.config.ts | 36 +++++++++++---- icp.yaml | 81 +++++++++++++++++++++++++++++++++ mops.toml | 1 + package.json | 4 +- 8 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 icp.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82dbaa20..81eb403b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,13 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "npm" + # dfx is still needed for `dfx generate` (decl:cli) and the + # production deploy paths (deploy-staging/deploy-ic, release.yml). + # Local network + deploy now go through icp-cli (see deploy-local + # and replica scripts in package.json). - uses: dfinity/setup-dfx@e50c04f104ee4285ec010f10609483cf41e4d365 # main + - name: Install icp-cli + run: npm install -g @icp-sdk/icp-cli @icp-sdk/ic-wasm - uses: ./.github/actions/cache-rust-wasm @@ -68,6 +74,9 @@ jobs: - name: Deploy backend run: npm run deploy-local + - name: Generate declarations + run: npm run decl + - name: Build frontend run: npm run build-frontend diff --git a/.github/workflows/mops-test.yml b/.github/workflows/mops-test.yml index 00ab0506..1a80656d 100644 --- a/.github/workflows/mops-test.yml +++ b/.github/workflows/mops-test.yml @@ -36,8 +36,6 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "npm" - - uses: dfinity/setup-dfx@e50c04f104ee4285ec010f10609483cf41e4d365 # main - - run: dfx cache install - name: Cache mops packages uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: diff --git a/.gitignore b/.gitignore index f7764cfa..b14bcc98 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ target/ dist/ .dfx/ +.icp/cache/ .mops/ .migrations-*/ mops.lock diff --git a/AGENTS.md b/AGENTS.md index 3b0b2edf..cf38b510 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ npm run lint # ESLint npm run fix # Prettier + ESLint fix npm run check # TypeScript check for CLI + Frontend (parallel) npm test # mops test (Motoko) + CLI Jest tests -npm start # Start local dfx replica + deploy + all frontends +npm start # Start local icp replica + deploy + all frontends ``` ### CLI (`cd cli/`) @@ -70,6 +70,7 @@ Svelte 5 + Vite 8, queries the main canister. Staging canister: `ogp6e-diaaa-aaa ## Key constraints +- **Local pipeline uses icp-cli**: `npm run replica` and `npm run deploy-local` use `icp` (config in `icp.yaml`). `dfx.json` is still kept around — production deploys (`deploy-staging`, `deploy-ic`, `release.yml`) and `npm run decl:cli` (`dfx generate`) still go through dfx. - **dfx version**: pinned in `dfx.json` via `dfxvm`. Do not run `dfxvm update/install/default` to change it. -- **Declarations must be regenerated** after backend changes: `npm run decl` (requires local dfx running). +- **Declarations must be regenerated** after backend changes: `npm run decl` (uses `dfx generate`, no replica needed). - **API version** in `cli/mops.ts` (`apiVersion`) and `backend/main/main-canister.mo` (`API_VERSION`) must match. diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4a919abc..0059dd49 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -15,17 +15,33 @@ interface CanisterIds { let canisterIds: CanisterIds = {}; try { - canisterIds = JSON.parse( - fs - .readFileSync( - network === "local" - ? "../.dfx/local/canister_ids.json" - : "../canister_ids.json", - ) - .toString(), - ); + if (network === "local") { + // icp-cli writes a flat { name: id } map per environment; dfx writes + // { name: { network: id } }. Try icp first, fall back to dfx. + try { + const icpIds = JSON.parse( + fs.readFileSync("../.icp/cache/mappings/local.ids.json").toString(), + ) as Record; + canisterIds = Object.fromEntries( + Object.entries(icpIds).map(([name, id]) => [ + name, + { local: id } as { [k in Network]: string }, + ]), + ); + } catch { + canisterIds = JSON.parse( + fs.readFileSync("../.dfx/local/canister_ids.json").toString(), + ); + } + } else { + canisterIds = JSON.parse( + fs.readFileSync("../canister_ids.json").toString(), + ); + } } catch (e) { - console.error("\n⚠️ Before starting the dev server run: dfx deploy\n\n"); + console.error( + "\n⚠️ Before starting the dev server run: npm run deploy-local\n\n", + ); } // Generate canister ids, required by the generated canister code in .dfx/local/declarations/* diff --git a/icp.yaml b/icp.yaml new file mode 100644 index 00000000..7ae78032 --- /dev/null +++ b/icp.yaml @@ -0,0 +1,81 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dfinity/icp-cli/main/docs/schemas/icp-yaml-schema.json +# +# Mirrors dfx.json. dfx.json is still kept around for the production deploy +# path (release.yml, deploy-staging, deploy-ic) and for mops users who haven't +# migrated yet. + +canisters: + - name: main + recipe: + type: "@dfinity/motoko@v4.1.0" + configuration: + main: backend/main/main-canister.mo + compress: true + shrink: true + - name: bench + recipe: + type: "@dfinity/motoko@v4.1.0" + configuration: + main: cli/commands/bench/bench-canister.mo + - name: assets + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + version: "0.29.1" + dir: frontend/dist + build: + - npm run build-frontend + - name: docs + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + version: "0.29.1" + dir: docs/build + build: + - npm run build-docs + - name: blog + recipe: + type: "@dfinity/asset-canister@v2.1.0" + configuration: + version: "0.29.1" + dir: blog/build + build: + - npm run build-blog + # Raw build/sync (not the recipe) because the asset-canister recipe template + # only supports a single `dir`, but dfx.json ships two sources for `cli`: + # `cli-releases/` (install.sh, releases.json, tags/, versions/ — served at + # cli.mops.one) and `cli-releases/frontend/dist/` (the SPA). + - name: cli + build: + steps: + - type: script + commands: + - npm run build-cli-releases + - type: pre-built + url: https://github.com/dfinity/sdk/releases/download/0.29.1/assetstorage.wasm.gz + sync: + steps: + - type: assets + dirs: + - cli-releases + - cli-releases/frontend/dist + # Static-only canister: no build step, just serves + # play-frontend/.well-known/ic-domains. + - name: play-frontend + build: + steps: + - type: pre-built + url: https://github.com/dfinity/sdk/releases/download/0.29.1/assetstorage.wasm.gz + sync: + steps: + - type: assets + dir: play-frontend + +# Override the default local network port (8000) to bind on dfx's 4943 so +# vite's /api proxy keeps working without changes to the proxy config. +networks: + - name: local + mode: managed + gateway: + bind: 127.0.0.1 + port: 4943 diff --git a/mops.toml b/mops.toml index 22371d1a..010312fe 100644 --- a/mops.toml +++ b/mops.toml @@ -21,3 +21,4 @@ prng = "0.0.8" moc = "0.14.14" wasmtime = "34.0.1" lintoko = "0.7.0" +pocket-ic = "12.0.0" diff --git a/package.json b/package.json index 48a812ed..5a79f270 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start-docs": "cd docs && npm run start", "start-blog": "cd blog && npm run start", "start-cli-releases": "cd cli-releases && npm run start", - "replica": "dfx stop && dfx start --clean --background", + "replica": "icp network stop -e local 2>/dev/null; rm -rf .icp/cache/networks && icp network start --background -e local", "decl": "npm run decl:cli && npm run decl:frontend", "lint": "eslint .", "fix": "prettier --write . && eslint --fix .", @@ -17,7 +17,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "deploy": "dfx deploy --no-wallet --identity ${IDENTITY:-default} --network ${DFX_NETWORK:-local}", - "deploy-local": "NODE_ENV=development dfx deploy --identity default --no-wallet main", + "deploy-local": "NODE_ENV=development icp deploy -y -e local main", "deploy-staging": "NODE_ENV=production IDENTITY=mops DFX_NETWORK=staging npm run deploy", "deploy-ic": "NODE_ENV=production IDENTITY=mops DFX_NETWORK=ic npm run deploy", "deploy:main": "NODE_ENV=development IDENTITY=default DFX_NETWORK=local npm run deploy main", From 5ce420679d678fc463dfbe91f4032729102b5bb0 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Fri, 22 May 2026 15:26:56 +0200 Subject: [PATCH 2/3] Revert mops.toml pocket-ic + mops-test.yml dfx removal mops 1.0.0 (tested by setup-mops.yml and mops-test.yml matrix) only supports pocket-ic 4.0.0. Adding pocket-ic = 12.0.0 to root mops.toml breaks 'mops test' for old CLIs in those matrices. Keeps icp.yaml + ci.yml/package.json/vite migration; mops-test.yml stays on dfx for now. Co-authored-by: Cursor --- .github/workflows/mops-test.yml | 2 + cli/specs/canister-imports.md | 265 ++++++++++++++++++++++++++++++++ mops.toml | 1 - 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 cli/specs/canister-imports.md diff --git a/.github/workflows/mops-test.yml b/.github/workflows/mops-test.yml index 1a80656d..00ab0506 100644 --- a/.github/workflows/mops-test.yml +++ b/.github/workflows/mops-test.yml @@ -36,6 +36,8 @@ jobs: with: node-version: ${{ matrix.node-version }} cache: "npm" + - uses: dfinity/setup-dfx@e50c04f104ee4285ec010f10609483cf41e4d365 # main + - run: dfx cache install - name: Cache mops packages uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: diff --git a/cli/specs/canister-imports.md b/cli/specs/canister-imports.md new file mode 100644 index 00000000..b7effad3 --- /dev/null +++ b/cli/specs/canister-imports.md @@ -0,0 +1,265 @@ +# Spec: `canister-imports` + +Status: draft +Owner: mops CLI +Tracking: feature spec, no implementation yet + +## Motivation + +Motoko 1.5 introduces two moc flags for resolving cross-canister `import` statements: + +- `--actor-env-alias ` — late binding; the + deployed canister reads the env-var key from its own canister environment + variables at instantiation time to resolve the principal. +- `--actor-id-alias ` — compile-time pinning; + the principal is baked into the wasm. + +Both are required for `mops check` and `mops build`, otherwise +`import "canister:foo"` fails to type-check with `M0011`. + +Today, the only way to express these in `mops.toml` is to drop the raw flags +into `[moc].args` or `[canisters.NAME].args`: + +```toml +[canisters.test] +args = ["--actor-env-alias", "greet", "PUBLIC_CANISTER_ID:greeter", "did/greet.did"] +``` + +This is brittle, hard to read, easy to typo, and offers no validation. Users +deploying with icp-cli additionally need to align the env-var key with +icp-cli's `PUBLIC_CANISTER_ID:` convention by hand. + +This feature adds first-class config for cross-canister imports. + +## Scope (v1) + +Supports only `--actor-env-alias` for canisters that follow the icp-cli +deployer convention `PUBLIC_CANISTER_ID:`. `--actor-id-alias` and +custom env-var keys are out of scope and tracked under "future extensions" +below — they remain accessible via `[moc].args` in the meantime. + +## Schema + +```toml +[canisters..canister-imports.env] + = "" +``` + +- `` — the canister that performs the `import` (must be declared in + `[canisters.]`). +- `` — the source-code alias from `import Foo "canister:"`. + Used both as the alias passed to moc and as the suffix of the auto-generated + env-var key. +- `` — path to a `.did` file describing the imported actor's + interface, resolved relative to `mops.toml`. + +The section path `canister-imports.env` is the v1 extension point; future +flag flavors (e.g. `canister-imports.id`) slot in as siblings without +breaking this schema. + +## Resolution + +Each entry under `canister-imports.env` expands to a single moc invocation +fragment: + +``` +--actor-env-alias PUBLIC_CANISTER_ID: +``` + +The env-var key is always derived from the alias; it is not configurable in +v1. + +### Example + +Source code: + +```motoko +// src/test/Test.mo +import Greet "canister:greet"; + +persistent actor Test { + public func call() : async Text { + await Greet.greet("World"); + }; +}; +``` + +Configuration: + +```toml +[canisters.greet] +main = "src/greet/Greet.mo" +candid = "did/greet.did" + +[canisters.test] +main = "src/test/Test.mo" + +[canisters.test.canister-imports.env] +greet = "did/greet.did" +``` + +Expansion (appended to every moc invocation that compiles or type-checks +`Test.mo`): + +``` +--actor-env-alias greet PUBLIC_CANISTER_ID:greet did/greet.did +``` + +## Affected commands + +The expanded moc args must be threaded through every command that invokes +moc on a canister that declares `canister-imports`: + +- `mops check` +- `mops check-stable` +- `mops build` +- `mops test` (when test files belong to a canister that declares imports) +- `mops bench` (same as test) + +A single shared helper builds the args from a canister config, called from +each command. Without the flag, type-checking fails with `M0011` and builds +do not produce wasm. + +## Validation + +- The candid path must exist on disk; missing → clear error naming the + binding and the path. +- Aliases are unique within a canister's `canister-imports.env` section. + Duplicate keys are a TOML error and are surfaced as such. +- Unknown subsections under `canister-imports` (anything other than `env` + in v1) are an error, not silently ignored. Forward-compat: when `id` + arrives, the validator's allowed-keys list is extended. + +## Toolchain requirement + +`--actor-env-alias` requires moc 1.5 or later. When `canister-imports.env` +is non-empty and the resolved moc is older, mops emits an error during +command setup before invoking moc: + +``` +Canister `` declares [canisters..canister-imports.env] which +requires moc >= 1.5 (resolved: 0.14.14). +Set [toolchain] moc = "1.5.x" or higher in mops.toml. +``` + +## Interaction with existing flag escape hatches + +`[moc].args` and `[canisters.NAME].args` continue to accept raw moc flags. +If the user supplies a literal `--actor-env-alias` or `--actor-id-alias` +in `args` while also declaring `canister-imports`, mops emits a warning at +build time (matching the existing `managedFlags` pattern in `build.ts`). +This stays advisory, not fatal — users may have legitimate reasons to +mix the structured form with raw overrides during migration. + +## Trade-offs and explicit non-goals + +### Alias-and-canister-name divergence + +The shorthand always derives the env-var key from the alias. This means +shorthand only produces correct runtime behaviour when the alias the user +chose in source code matches the canister name written in the deployer's +config (`icp.yaml` or equivalent), because that name is what the deployer +uses when populating `PUBLIC_CANISTER_ID:`. + +If a user names their canister `greeter` in `icp.yaml` but imports it as +`canister:greet` in source code, the shorthand defaults to +`PUBLIC_CANISTER_ID:greet`, which the deployer will not set. The +canister will fail at runtime when the import is resolved. + +The supported answer in v1: pick consistent names. The future answer: an +explicit form that lets the user spell out the env-var key (see "future +extensions"). + +### No cross-section candid lookup + +Earlier drafts considered ` = ""` and pulling +the candid path from `[canisters.].candid`. We rejected this +because: + +- It introduces action-at-a-distance: editing one canister's `candid` field + silently changes another canister's build behaviour. +- It conflates "the canister I'm building" with "the canister whose + interface I'm consuming" — those can diverge during migrations. +- It does not generalise to external canisters that aren't declared in + `[canisters]` at all. + +The candid path is always written explicitly. Two canisters that share an +interface file just reference the same path string — duplication of a path +is not duplication of meaning. + +### No deployer-convention configuration + +The `PUBLIC_CANISTER_ID:` prefix is hardcoded for v1. We deliberately do +not expose `[canister-imports] env-prefix = "..."` or similar. If a +project needs a different convention (different deployer, custom keys), +it falls back to `[moc].args` until v2 introduces an explicit-env form. + +### No auto-generation of `.did` files + +If `[canisters.].candid` is unset and the project has never been +built, no canonical `.did` exists at any user-visible path. Users either: + +- run `mops build ` once and copy `.mops/.build/.did` to a + stable location, then commit it and reference it from `canister-imports`, + or +- author the `.did` by hand. + +mops does not write to user-visible paths as a side effect of +`canister-imports`. Auto-generation is a separate, larger discussion. + +## Future extensions (not in v1) + +These slot into the same parent section without breaking v1 syntax. + +### Compile-time pinning (`canister-imports.id`) + +```toml +[canisters.test.canister-imports.id.nns] +principal = "rrkah-fqaaa-aaaaa-aaaaq-cai" +candid = "did/nns.did" +``` + +Emits `--actor-id-alias nns rrkah-fqaaa-aaaaa-aaaaq-cai did/nns.did`. +Always a table (principal + candid are both required). No string +shorthand applies. + +### Explicit env-var key + +For deployers that don't use `PUBLIC_CANISTER_ID:` or for cases where +alias and canister name diverge: + +```toml +[canisters.test.canister-imports.env.greet] +key = "PUBLIC_CANISTER_ID:greeter" +candid = "did/greet.did" +``` + +The string shorthand and the table form coexist in the same section, +mirroring the existing `[canisters]` pattern in `mops.toml` +(`Record`). + +## Documentation surface + +When implemented, this feature touches: + +- `docs/docs/09-mops.toml.md` — new `canister-imports` reference section + with the example above. +- `cli/CHANGELOG.md` — entry under `## Next`. +- `.agents/skills/mops-cli/SKILL.md` — short note on cross-canister + imports for agents authoring `mops.toml`. + +## Open questions + +1. **Strict moc version error vs. warning.** Currently specced as a hard + error when moc < 1.5 and `canister-imports.env` is non-empty. Worth + double-checking once the feature is implemented in case some workflows + want to soft-degrade. +2. **`icp.yaml` cross-check.** Optional polish: when `icp.yaml` is present + and a binding's defaulted env-var key references an alias that does not + appear as a canister name in `icp.yaml`, emit a build-time warning. Not + required for v1; prevents silent runtime failures. +3. **Snapshot scope for tests.** Suggested fixture: + `cli/tests/build/canister-imports/` covering the happy path plus one + error case (missing candid file). Following the project's snapshot + strategy: full CLI output snapshot for the happy path, targeted + `toMatch` for the error case. diff --git a/mops.toml b/mops.toml index 010312fe..22371d1a 100644 --- a/mops.toml +++ b/mops.toml @@ -21,4 +21,3 @@ prng = "0.0.8" moc = "0.14.14" wasmtime = "34.0.1" lintoko = "0.7.0" -pocket-ic = "12.0.0" From 5a9448ec783bffd99ea3e5bc7f8da68a6eccb8d4 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Fri, 22 May 2026 15:27:17 +0200 Subject: [PATCH 3/3] Drop unrelated canister-imports.md from this branch Co-authored-by: Cursor --- cli/specs/canister-imports.md | 265 ---------------------------------- 1 file changed, 265 deletions(-) delete mode 100644 cli/specs/canister-imports.md diff --git a/cli/specs/canister-imports.md b/cli/specs/canister-imports.md deleted file mode 100644 index b7effad3..00000000 --- a/cli/specs/canister-imports.md +++ /dev/null @@ -1,265 +0,0 @@ -# Spec: `canister-imports` - -Status: draft -Owner: mops CLI -Tracking: feature spec, no implementation yet - -## Motivation - -Motoko 1.5 introduces two moc flags for resolving cross-canister `import` statements: - -- `--actor-env-alias ` — late binding; the - deployed canister reads the env-var key from its own canister environment - variables at instantiation time to resolve the principal. -- `--actor-id-alias ` — compile-time pinning; - the principal is baked into the wasm. - -Both are required for `mops check` and `mops build`, otherwise -`import "canister:foo"` fails to type-check with `M0011`. - -Today, the only way to express these in `mops.toml` is to drop the raw flags -into `[moc].args` or `[canisters.NAME].args`: - -```toml -[canisters.test] -args = ["--actor-env-alias", "greet", "PUBLIC_CANISTER_ID:greeter", "did/greet.did"] -``` - -This is brittle, hard to read, easy to typo, and offers no validation. Users -deploying with icp-cli additionally need to align the env-var key with -icp-cli's `PUBLIC_CANISTER_ID:` convention by hand. - -This feature adds first-class config for cross-canister imports. - -## Scope (v1) - -Supports only `--actor-env-alias` for canisters that follow the icp-cli -deployer convention `PUBLIC_CANISTER_ID:`. `--actor-id-alias` and -custom env-var keys are out of scope and tracked under "future extensions" -below — they remain accessible via `[moc].args` in the meantime. - -## Schema - -```toml -[canisters..canister-imports.env] - = "" -``` - -- `` — the canister that performs the `import` (must be declared in - `[canisters.]`). -- `` — the source-code alias from `import Foo "canister:"`. - Used both as the alias passed to moc and as the suffix of the auto-generated - env-var key. -- `` — path to a `.did` file describing the imported actor's - interface, resolved relative to `mops.toml`. - -The section path `canister-imports.env` is the v1 extension point; future -flag flavors (e.g. `canister-imports.id`) slot in as siblings without -breaking this schema. - -## Resolution - -Each entry under `canister-imports.env` expands to a single moc invocation -fragment: - -``` ---actor-env-alias PUBLIC_CANISTER_ID: -``` - -The env-var key is always derived from the alias; it is not configurable in -v1. - -### Example - -Source code: - -```motoko -// src/test/Test.mo -import Greet "canister:greet"; - -persistent actor Test { - public func call() : async Text { - await Greet.greet("World"); - }; -}; -``` - -Configuration: - -```toml -[canisters.greet] -main = "src/greet/Greet.mo" -candid = "did/greet.did" - -[canisters.test] -main = "src/test/Test.mo" - -[canisters.test.canister-imports.env] -greet = "did/greet.did" -``` - -Expansion (appended to every moc invocation that compiles or type-checks -`Test.mo`): - -``` ---actor-env-alias greet PUBLIC_CANISTER_ID:greet did/greet.did -``` - -## Affected commands - -The expanded moc args must be threaded through every command that invokes -moc on a canister that declares `canister-imports`: - -- `mops check` -- `mops check-stable` -- `mops build` -- `mops test` (when test files belong to a canister that declares imports) -- `mops bench` (same as test) - -A single shared helper builds the args from a canister config, called from -each command. Without the flag, type-checking fails with `M0011` and builds -do not produce wasm. - -## Validation - -- The candid path must exist on disk; missing → clear error naming the - binding and the path. -- Aliases are unique within a canister's `canister-imports.env` section. - Duplicate keys are a TOML error and are surfaced as such. -- Unknown subsections under `canister-imports` (anything other than `env` - in v1) are an error, not silently ignored. Forward-compat: when `id` - arrives, the validator's allowed-keys list is extended. - -## Toolchain requirement - -`--actor-env-alias` requires moc 1.5 or later. When `canister-imports.env` -is non-empty and the resolved moc is older, mops emits an error during -command setup before invoking moc: - -``` -Canister `` declares [canisters..canister-imports.env] which -requires moc >= 1.5 (resolved: 0.14.14). -Set [toolchain] moc = "1.5.x" or higher in mops.toml. -``` - -## Interaction with existing flag escape hatches - -`[moc].args` and `[canisters.NAME].args` continue to accept raw moc flags. -If the user supplies a literal `--actor-env-alias` or `--actor-id-alias` -in `args` while also declaring `canister-imports`, mops emits a warning at -build time (matching the existing `managedFlags` pattern in `build.ts`). -This stays advisory, not fatal — users may have legitimate reasons to -mix the structured form with raw overrides during migration. - -## Trade-offs and explicit non-goals - -### Alias-and-canister-name divergence - -The shorthand always derives the env-var key from the alias. This means -shorthand only produces correct runtime behaviour when the alias the user -chose in source code matches the canister name written in the deployer's -config (`icp.yaml` or equivalent), because that name is what the deployer -uses when populating `PUBLIC_CANISTER_ID:`. - -If a user names their canister `greeter` in `icp.yaml` but imports it as -`canister:greet` in source code, the shorthand defaults to -`PUBLIC_CANISTER_ID:greet`, which the deployer will not set. The -canister will fail at runtime when the import is resolved. - -The supported answer in v1: pick consistent names. The future answer: an -explicit form that lets the user spell out the env-var key (see "future -extensions"). - -### No cross-section candid lookup - -Earlier drafts considered ` = ""` and pulling -the candid path from `[canisters.].candid`. We rejected this -because: - -- It introduces action-at-a-distance: editing one canister's `candid` field - silently changes another canister's build behaviour. -- It conflates "the canister I'm building" with "the canister whose - interface I'm consuming" — those can diverge during migrations. -- It does not generalise to external canisters that aren't declared in - `[canisters]` at all. - -The candid path is always written explicitly. Two canisters that share an -interface file just reference the same path string — duplication of a path -is not duplication of meaning. - -### No deployer-convention configuration - -The `PUBLIC_CANISTER_ID:` prefix is hardcoded for v1. We deliberately do -not expose `[canister-imports] env-prefix = "..."` or similar. If a -project needs a different convention (different deployer, custom keys), -it falls back to `[moc].args` until v2 introduces an explicit-env form. - -### No auto-generation of `.did` files - -If `[canisters.].candid` is unset and the project has never been -built, no canonical `.did` exists at any user-visible path. Users either: - -- run `mops build ` once and copy `.mops/.build/.did` to a - stable location, then commit it and reference it from `canister-imports`, - or -- author the `.did` by hand. - -mops does not write to user-visible paths as a side effect of -`canister-imports`. Auto-generation is a separate, larger discussion. - -## Future extensions (not in v1) - -These slot into the same parent section without breaking v1 syntax. - -### Compile-time pinning (`canister-imports.id`) - -```toml -[canisters.test.canister-imports.id.nns] -principal = "rrkah-fqaaa-aaaaa-aaaaq-cai" -candid = "did/nns.did" -``` - -Emits `--actor-id-alias nns rrkah-fqaaa-aaaaa-aaaaq-cai did/nns.did`. -Always a table (principal + candid are both required). No string -shorthand applies. - -### Explicit env-var key - -For deployers that don't use `PUBLIC_CANISTER_ID:` or for cases where -alias and canister name diverge: - -```toml -[canisters.test.canister-imports.env.greet] -key = "PUBLIC_CANISTER_ID:greeter" -candid = "did/greet.did" -``` - -The string shorthand and the table form coexist in the same section, -mirroring the existing `[canisters]` pattern in `mops.toml` -(`Record`). - -## Documentation surface - -When implemented, this feature touches: - -- `docs/docs/09-mops.toml.md` — new `canister-imports` reference section - with the example above. -- `cli/CHANGELOG.md` — entry under `## Next`. -- `.agents/skills/mops-cli/SKILL.md` — short note on cross-canister - imports for agents authoring `mops.toml`. - -## Open questions - -1. **Strict moc version error vs. warning.** Currently specced as a hard - error when moc < 1.5 and `canister-imports.env` is non-empty. Worth - double-checking once the feature is implemented in case some workflows - want to soft-degrade. -2. **`icp.yaml` cross-check.** Optional polish: when `icp.yaml` is present - and a binding's defaulted env-var key references an alias that does not - appear as a canister name in `icp.yaml`, emit a build-time warning. Not - required for v1; prevents silent runtime failures. -3. **Snapshot scope for tests.** Suggested fixture: - `cli/tests/build/canister-imports/` covering the happy path plus one - error case (missing candid file). Following the project's snapshot - strategy: full CLI output snapshot for the happy path, targeted - `toMatch` for the error case.