@ic-reactor/mcp 0.0.0-stage → 0.0.0
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/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
- package/.github/actions/setup-rust-wasm/action.yml +25 -0
- package/.github/dependabot.yml +107 -0
- package/.github/workflows/auth-latest.yml +67 -0
- package/.github/workflows/b3pay-site.yml +49 -0
- package/.github/workflows/ci.yml +430 -0
- package/.github/workflows/codeql.yml +41 -0
- package/.github/workflows/dependency-review.yml +68 -0
- package/.github/workflows/docs-check.yml +98 -0
- package/.github/workflows/docs.yml +265 -0
- package/.github/workflows/e2e.yml +90 -0
- package/.github/workflows/examples-published.yml +97 -0
- package/.github/workflows/release.yml +326 -0
- package/.husky/pre-commit +12 -0
- package/.prettierignore +109 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +103 -0
- package/CHANGELOG.md +3268 -0
- package/CLAUDE.md +93 -0
- package/CNAME +1 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/CONTRIBUTING.md +433 -0
- package/LICENSE +21 -0
- package/README.md +2 -2
- package/SECURITY.md +74 -0
- package/b3pay-module.json +194 -0
- package/docs/.remarkrc.json +10 -0
- package/docs/README.md +64 -0
- package/docs/astro.config.mjs +205 -0
- package/docs/favicon.svg +15 -0
- package/docs/package.json +39 -0
- package/docs/public/favicon.svg +15 -0
- package/docs/public/ic-reactor-social-card.png +0 -0
- package/docs/scripts/b3pay-static.mjs +22 -0
- package/docs/scripts/check-example-docs.mjs +214 -0
- package/docs/scripts/check-links.mjs +212 -0
- package/docs/scripts/fix-dist-links.js +125 -0
- package/docs/scripts/fix-frontmatter.js +71 -0
- package/docs/scripts/remove-redundant-h1.cjs +48 -0
- package/docs/scripts/rewrite-typedoc-links.js +110 -0
- package/docs/scripts/split-index.js +69 -0
- package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
- package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
- package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
- package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
- package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
- package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
- package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
- package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
- package/docs/src/components/LibraryCards.astro +172 -0
- package/docs/src/components/LibraryIcon.astro +28 -0
- package/docs/src/components/SiteTitle.astro +241 -0
- package/docs/src/content/docs/404.mdx +19 -0
- package/docs/src/content/docs/build/agent-tools.mdx +593 -0
- package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
- package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
- package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
- package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
- package/docs/src/content/docs/examples/index.mdx +38 -0
- package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
- package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
- package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
- package/docs/src/content/docs/guides/auth.mdx +1181 -0
- package/docs/src/content/docs/guides/client.mdx +551 -0
- package/docs/src/content/docs/guides/deploy.mdx +180 -0
- package/docs/src/content/docs/guides/errors.mdx +475 -0
- package/docs/src/content/docs/guides/getting-started.mdx +620 -0
- package/docs/src/content/docs/guides/reads.mdx +586 -0
- package/docs/src/content/docs/guides/ssr.mdx +529 -0
- package/docs/src/content/docs/guides/testing.mdx +401 -0
- package/docs/src/content/docs/guides/values.mdx +261 -0
- package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
- package/docs/src/content/docs/guides/writes.mdx +572 -0
- package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
- package/docs/src/content/docs/index.mdx +65 -0
- package/docs/src/content/docs/migrating-from-3.mdx +535 -0
- package/docs/src/content/docs/packages/core.mdx +186 -0
- package/docs/src/content/docs/packages/mcp.mdx +163 -0
- package/docs/src/content/docs/packages/react.mdx +207 -0
- package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
- package/docs/src/content/docs/versioning.mdx +104 -0
- package/docs/src/content.config.ts +7 -0
- package/docs/src/styles/custom.css +189 -0
- package/docs/tsconfig.typedoc.json +26 -0
- package/docs/typedoc-entry.ts +17 -0
- package/docs/typedoc.json +37 -0
- package/e2e/Cargo.lock +646 -0
- package/e2e/Cargo.toml +3 -0
- package/e2e/README.md +101 -0
- package/e2e/frontends/build.mjs +94 -0
- package/e2e/frontends/index.html +10 -0
- package/e2e/global-setup.ts +65 -0
- package/e2e/icp.yaml +38 -0
- package/e2e/package.json +40 -0
- package/e2e/setup.ts +6 -0
- package/e2e/src/actor/Cargo.toml +14 -0
- package/e2e/src/actor/hello_actor.did +10 -0
- package/e2e/src/actor/src/lib.rs +73 -0
- package/e2e/src/calls.test.ts +132 -0
- package/e2e/src/declarations/hello_actor.envelope.json +175 -0
- package/e2e/src/declarations/hello_actor.ts +15 -0
- package/e2e/src/errors.test.ts +107 -0
- package/e2e/src/frontend-headers.test.ts +253 -0
- package/e2e/src/identity.test.ts +61 -0
- package/e2e/src/react.test.tsx +59 -0
- package/e2e/src/replica.ts +70 -0
- package/e2e/src/vite-env.test.ts +108 -0
- package/e2e/test.sh +47 -0
- package/e2e/tsconfig.json +20 -0
- package/e2e/tsconfig.typecheck.json +9 -0
- package/e2e/vitest.config.ts +22 -0
- package/eslint.config.mjs +100 -0
- package/evals/PREREGISTRATION.md +1346 -0
- package/evals/README.md +756 -0
- package/evals/conditions/thin/condition.json +8 -0
- package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
- package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
- package/evals/conditions/thin/package.json +25 -0
- package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
- package/evals/conditions/thin-guide/condition.json +10 -0
- package/evals/conditions/thin-guide/docs/llms.txt +292 -0
- package/evals/conditions/v3/condition.json +9 -0
- package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
- package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
- package/evals/conditions/v3/docs/llms-full.txt +1053 -0
- package/evals/conditions/v3/package.json +26 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
- package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
- package/evals/conditions/v4/PORTING.md +384 -0
- package/evals/conditions/v4/README.md +106 -0
- package/evals/conditions/v4/condition.json +5 -0
- package/evals/conditions/v4/docs/llms.txt +323 -0
- package/evals/conditions/v4/package.json +27 -0
- package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
- package/evals/conditions/v4-proto/condition.json +6 -0
- package/evals/conditions/v4-proto/docs/llms.txt +291 -0
- package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
- package/evals/conditions/v4-proto/lib/package.json +27 -0
- package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
- package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
- package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
- package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
- package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
- package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
- package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
- package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
- package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
- package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
- package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
- package/evals/conditions/v4-proto/package.json +26 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
- package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
- package/evals/drive.mjs +1221 -0
- package/evals/gate.mjs +161 -0
- package/evals/harness/aggregate.mjs +534 -0
- package/evals/harness/aggregate.test.mjs +344 -0
- package/evals/harness/assemble.mjs +216 -0
- package/evals/harness/assemble.test.mjs +143 -0
- package/evals/harness/auth.mjs +91 -0
- package/evals/harness/check-docs.mjs +157 -0
- package/evals/harness/declarations/icrc1.did.d.ts +95 -0
- package/evals/harness/declarations/icrc1.did.js +140 -0
- package/evals/harness/drive-auth.test.mjs +584 -0
- package/evals/harness/drive.test.mjs +509 -0
- package/evals/harness/fake-auth.ts +67 -0
- package/evals/harness/fake-ledger.ts +178 -0
- package/evals/harness/gate-plan.mjs +104 -0
- package/evals/harness/gate-plan.test.mjs +243 -0
- package/evals/harness/icrc1.did +88 -0
- package/evals/harness/judge.mjs +63 -0
- package/evals/harness/judge.test.mjs +81 -0
- package/evals/harness/leak-scan.mjs +1373 -0
- package/evals/harness/leak-scan.test.mjs +1300 -0
- package/evals/harness/probes/amounts.ts +14 -0
- package/evals/harness/probes/node-tool.probe.ts +48 -0
- package/evals/harness/probes/probe.mjs +149 -0
- package/evals/harness/probes/react-wallet.probe.ts +96 -0
- package/evals/harness/public/support/auth.ts +38 -0
- package/evals/harness/public/support/fake-replica.d.ts +28 -0
- package/evals/harness/public/support/fake-replica.js +434 -0
- package/evals/harness/public/support/ledger.idl.d.ts +95 -0
- package/evals/harness/public/support/ledger.idl.js +140 -0
- package/evals/harness/public/support/world.ts +179 -0
- package/evals/harness/runs.mjs +139 -0
- package/evals/harness/sandbox.mjs +88 -0
- package/evals/harness/sandbox.test.mjs +134 -0
- package/evals/harness/ship.mjs +264 -0
- package/evals/harness/ship.test.mjs +552 -0
- package/evals/harness/world.ts +291 -0
- package/evals/package.json +26 -0
- package/evals/pnpm-lock.yaml +1537 -0
- package/evals/pnpm-workspace.yaml +12 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
- package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
- package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
- package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
- package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
- package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
- package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
- package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
- package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
- package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
- package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
- package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
- package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
- package/evals/results/README.md +31 -0
- package/evals/runs-pilot.log +150 -0
- package/evals/runs-pilot2.log +150 -0
- package/evals/score.mjs +256 -0
- package/evals/setup.mjs +547 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
- package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
- package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
- package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
- package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
- package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
- package/evals/tasks/node-tool/prompt.md +55 -0
- package/evals/tasks/node-tool/prompt.minimal.md +30 -0
- package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
- package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
- package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
- package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
- package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
- package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
- package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
- package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
- package/evals/tasks/node-tool/starter/src/index.ts +7 -0
- package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
- package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
- package/evals/tasks/node-tool/task.json +63 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
- package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
- package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
- package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
- package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
- package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
- package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
- package/evals/tasks/react-wallet/prompt.md +88 -0
- package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
- package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
- package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
- package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
- package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
- package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
- package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
- package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
- package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
- package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
- package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
- package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
- package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
- package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
- package/evals/tasks/react-wallet/task.json +84 -0
- package/examples/canister-studio/.stackblitzrc +4 -0
- package/examples/canister-studio/README.md +183 -0
- package/examples/canister-studio/did/ckbtc_minter.did +806 -0
- package/examples/canister-studio/did/icp_ledger.did +586 -0
- package/examples/canister-studio/did/icrc1.did +88 -0
- package/examples/canister-studio/package.json +45 -0
- package/examples/canister-studio/scripts/smoke.ts +188 -0
- package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
- package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
- package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
- package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
- package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
- package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
- package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
- package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
- package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
- package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
- package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
- package/examples/canister-studio/src/calls/dedupe.ts +189 -0
- package/examples/canister-studio/src/calls/error-info.ts +168 -0
- package/examples/canister-studio/src/calls/history.test.ts +284 -0
- package/examples/canister-studio/src/calls/history.ts +287 -0
- package/examples/canister-studio/src/calls/kinds.ts +68 -0
- package/examples/canister-studio/src/calls/prepare.ts +32 -0
- package/examples/canister-studio/src/calls/query-options.ts +30 -0
- package/examples/canister-studio/src/calls/read.ts +47 -0
- package/examples/canister-studio/src/calls/readback.ts +100 -0
- package/examples/canister-studio/src/calls/record.ts +123 -0
- package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
- package/examples/canister-studio/src/calls/share.ts +46 -0
- package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
- package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
- package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
- package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
- package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
- package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
- package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
- package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
- package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
- package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
- package/examples/canister-studio/src/components/AppShell.tsx +29 -0
- package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
- package/examples/canister-studio/src/components/Badge.tsx +79 -0
- package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
- package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
- package/examples/canister-studio/src/components/Card.tsx +64 -0
- package/examples/canister-studio/src/components/Code.tsx +101 -0
- package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
- package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
- package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
- package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
- package/examples/canister-studio/src/components/MethodList.tsx +244 -0
- package/examples/canister-studio/src/components/Notice.tsx +36 -0
- package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
- package/examples/canister-studio/src/components/RouteError.tsx +42 -0
- package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
- package/examples/canister-studio/src/components/Spinner.tsx +33 -0
- package/examples/canister-studio/src/components/Tabs.tsx +96 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
- package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
- package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
- package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
- package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
- package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
- package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
- package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
- package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
- package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
- package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
- package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
- package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
- package/examples/canister-studio/src/components/canister-names.ts +11 -0
- package/examples/canister-studio/src/components/download.ts +34 -0
- package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
- package/examples/canister-studio/src/components/helpers.test.ts +107 -0
- package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
- package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
- package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
- package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
- package/examples/canister-studio/src/components/home/featured.ts +64 -0
- package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
- package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
- package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
- package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
- package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
- package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
- package/examples/canister-studio/src/components/modes.ts +31 -0
- package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
- package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
- package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
- package/examples/canister-studio/src/components/playground/sample.ts +43 -0
- package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
- package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
- package/examples/canister-studio/src/components/provenance.ts +38 -0
- package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
- package/examples/canister-studio/src/components/use-task.ts +56 -0
- package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
- package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
- package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
- package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
- package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
- package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
- package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
- package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
- package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
- package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
- package/examples/canister-studio/src/forms/context.ts +62 -0
- package/examples/canister-studio/src/forms/convert.ts +131 -0
- package/examples/canister-studio/src/forms/edge.test.ts +252 -0
- package/examples/canister-studio/src/forms/engine.test.ts +358 -0
- package/examples/canister-studio/src/forms/engine.ts +1492 -0
- package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
- package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
- package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
- package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
- package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
- package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
- package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
- package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
- package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
- package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
- package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
- package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
- package/examples/canister-studio/src/forms/hints.ts +128 -0
- package/examples/canister-studio/src/forms/json.ts +119 -0
- package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
- package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
- package/examples/canister-studio/src/interface/browser.test.ts +23 -0
- package/examples/canister-studio/src/interface/browser.ts +153 -0
- package/examples/canister-studio/src/interface/cache.ts +68 -0
- package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
- package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
- package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
- package/examples/canister-studio/src/interface/compile.server.ts +262 -0
- package/examples/canister-studio/src/interface/compile.test.ts +108 -0
- package/examples/canister-studio/src/interface/compile.ts +305 -0
- package/examples/canister-studio/src/interface/docs.test.ts +75 -0
- package/examples/canister-studio/src/interface/docs.ts +100 -0
- package/examples/canister-studio/src/interface/functions.test.ts +42 -0
- package/examples/canister-studio/src/interface/functions.ts +200 -0
- package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
- package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
- package/examples/canister-studio/src/interface/live-loader.ts +364 -0
- package/examples/canister-studio/src/interface/load.test.ts +328 -0
- package/examples/canister-studio/src/interface/load.ts +249 -0
- package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
- package/examples/canister-studio/src/interface/read-candid.ts +233 -0
- package/examples/canister-studio/src/interface/server-result.ts +42 -0
- package/examples/canister-studio/src/interface/typed.ts +68 -0
- package/examples/canister-studio/src/interface/types.ts +121 -0
- package/examples/canister-studio/src/lib/format.test.ts +107 -0
- package/examples/canister-studio/src/lib/format.ts +139 -0
- package/examples/canister-studio/src/lib/hex.test.ts +43 -0
- package/examples/canister-studio/src/lib/hex.ts +33 -0
- package/examples/canister-studio/src/lib/ids.test.ts +70 -0
- package/examples/canister-studio/src/lib/ids.ts +93 -0
- package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
- package/examples/canister-studio/src/lib/reactor.ts +55 -0
- package/examples/canister-studio/src/lib/registry.test.ts +74 -0
- package/examples/canister-studio/src/lib/registry.ts +238 -0
- package/examples/canister-studio/src/lib/schema.ts +148 -0
- package/examples/canister-studio/src/routeTree.gen.ts +227 -0
- package/examples/canister-studio/src/router.test.ts +290 -0
- package/examples/canister-studio/src/router.tsx +127 -0
- package/examples/canister-studio/src/routes/__root.tsx +58 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
- package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
- package/examples/canister-studio/src/routes/history.tsx +55 -0
- package/examples/canister-studio/src/routes/index.tsx +169 -0
- package/examples/canister-studio/src/routes/playground.tsx +286 -0
- package/examples/canister-studio/src/routes/safety.tsx +57 -0
- package/examples/canister-studio/src/routes/wallet.tsx +213 -0
- package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
- package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
- package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
- package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
- package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
- package/examples/canister-studio/src/safety/lab-run.ts +283 -0
- package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
- package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
- package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
- package/examples/canister-studio/src/safety/scenarios.ts +924 -0
- package/examples/canister-studio/src/styles.css +3453 -0
- package/examples/canister-studio/src/styles.test.ts +46 -0
- package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
- package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
- package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
- package/examples/canister-studio/src/values/Inspector.tsx +133 -0
- package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
- package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
- package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
- package/examples/canister-studio/src/values/candid-text.ts +479 -0
- package/examples/canister-studio/src/values/hints.ts +58 -0
- package/examples/canister-studio/src/values/json.test.ts +66 -0
- package/examples/canister-studio/src/values/json.ts +31 -0
- package/examples/canister-studio/src/values/leaves.tsx +309 -0
- package/examples/canister-studio/src/values/schema.test.ts +123 -0
- package/examples/canister-studio/src/values/schema.ts +78 -0
- package/examples/canister-studio/src/values/test-router.tsx +21 -0
- package/examples/canister-studio/src/values/tree.tsx +670 -0
- package/examples/canister-studio/src/values/use-sha256.ts +29 -0
- package/examples/canister-studio/tsconfig.json +20 -0
- package/examples/canister-studio/vite.config.ts +50 -0
- package/examples/canister-studio/vitest.config.ts +20 -0
- package/examples/icrc-ledger/.stackblitzrc +4 -0
- package/examples/icrc-ledger/README.md +100 -0
- package/examples/icrc-ledger/ckbtc_minter.did +12 -0
- package/examples/icrc-ledger/icp_ledger.did +147 -0
- package/examples/icrc-ledger/icrc1.did +88 -0
- package/examples/icrc-ledger/index.html +13 -0
- package/examples/icrc-ledger/package.json +40 -0
- package/examples/icrc-ledger/scripts/smoke.ts +151 -0
- package/examples/icrc-ledger/src/App.tsx +71 -0
- package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
- package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
- package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
- package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
- package/examples/icrc-ledger/src/blocks.test.ts +247 -0
- package/examples/icrc-ledger/src/blocks.ts +140 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
- package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
- package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
- package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
- package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
- package/examples/icrc-ledger/src/format.test.ts +41 -0
- package/examples/icrc-ledger/src/format.ts +68 -0
- package/examples/icrc-ledger/src/ledger.ts +15 -0
- package/examples/icrc-ledger/src/main.tsx +10 -0
- package/examples/icrc-ledger/src/minter.ts +16 -0
- package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
- package/examples/icrc-ledger/src/sandbox.ts +281 -0
- package/examples/icrc-ledger/src/styles.css +391 -0
- package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
- package/examples/icrc-ledger/src/transfer-form.ts +97 -0
- package/examples/icrc-ledger/tsconfig.json +19 -0
- package/examples/icrc-ledger/vite.config.ts +44 -0
- package/examples/icrc-ledger/vitest.config.ts +12 -0
- package/examples/next-ssr/.stackblitzrc +4 -0
- package/examples/next-ssr/README.md +112 -0
- package/examples/next-ssr/icrc1.did +88 -0
- package/examples/next-ssr/next.config.ts +10 -0
- package/examples/next-ssr/package.json +41 -0
- package/examples/next-ssr/scripts/smoke.ts +169 -0
- package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
- package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
- package/examples/next-ssr/src/app/account/page.tsx +82 -0
- package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
- package/examples/next-ssr/src/app/globals.css +278 -0
- package/examples/next-ssr/src/app/layout.tsx +34 -0
- package/examples/next-ssr/src/app/page.tsx +46 -0
- package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
- package/examples/next-ssr/src/app/providers.test.tsx +73 -0
- package/examples/next-ssr/src/app/providers.tsx +60 -0
- package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
- package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
- package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
- package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
- package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
- package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
- package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
- package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
- package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
- package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
- package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
- package/examples/next-ssr/src/components/server-data.ts +10 -0
- package/examples/next-ssr/src/format.ts +46 -0
- package/examples/next-ssr/src/ledgers.ts +38 -0
- package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
- package/examples/next-ssr/src/server/balance-route.ts +157 -0
- package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
- package/examples/next-ssr/src/server/error-summary.ts +37 -0
- package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
- package/examples/next-ssr/src/server/parse-owner.ts +34 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
- package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
- package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
- package/examples/next-ssr/src/server/read-balances.ts +78 -0
- package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
- package/examples/next-ssr/src/server/request-client.ts +24 -0
- package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
- package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
- package/examples/next-ssr/src/testing/setup.ts +10 -0
- package/examples/next-ssr/tsconfig.json +31 -0
- package/examples/next-ssr/vitest.config.ts +59 -0
- package/examples/node-agent-tool/.stackblitzrc +4 -0
- package/examples/node-agent-tool/README.md +165 -0
- package/examples/node-agent-tool/icp.yaml +11 -0
- package/examples/node-agent-tool/icrc1.did +88 -0
- package/examples/node-agent-tool/package.json +32 -0
- package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
- package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
- package/examples/node-agent-tool/src/cli.test.ts +64 -0
- package/examples/node-agent-tool/src/cli.ts +23 -0
- package/examples/node-agent-tool/src/command-line.ts +159 -0
- package/examples/node-agent-tool/src/commands/balance.ts +73 -0
- package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
- package/examples/node-agent-tool/src/commands/demo.ts +359 -0
- package/examples/node-agent-tool/src/commands/info.ts +69 -0
- package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
- package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
- package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
- package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
- package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
- package/examples/node-agent-tool/src/commands/watch.ts +135 -0
- package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
- package/examples/node-agent-tool/src/context.ts +20 -0
- package/examples/node-agent-tool/src/dispatch.ts +70 -0
- package/examples/node-agent-tool/src/failure.test.ts +59 -0
- package/examples/node-agent-tool/src/failure.ts +176 -0
- package/examples/node-agent-tool/src/identity.test.ts +242 -0
- package/examples/node-agent-tool/src/identity.ts +228 -0
- package/examples/node-agent-tool/src/input.ts +110 -0
- package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
- package/examples/node-agent-tool/src/ledgers.ts +62 -0
- package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
- package/examples/node-agent-tool/src/network.test.ts +128 -0
- package/examples/node-agent-tool/src/network.ts +101 -0
- package/examples/node-agent-tool/src/output.test.ts +208 -0
- package/examples/node-agent-tool/src/output.ts +163 -0
- package/examples/node-agent-tool/src/run.ts +73 -0
- package/examples/node-agent-tool/src/test-kit.ts +167 -0
- package/examples/node-agent-tool/tsconfig.json +18 -0
- package/examples/node-agent-tool/vitest.config.ts +8 -0
- package/examples/vite-wallet/.stackblitzrc +4 -0
- package/examples/vite-wallet/Cargo.lock +646 -0
- package/examples/vite-wallet/Cargo.toml +5 -0
- package/examples/vite-wallet/README.md +107 -0
- package/examples/vite-wallet/backend/Cargo.toml +12 -0
- package/examples/vite-wallet/backend/backend.did +37 -0
- package/examples/vite-wallet/backend/src/lib.rs +133 -0
- package/examples/vite-wallet/icp.yaml +44 -0
- package/examples/vite-wallet/index.html +13 -0
- package/examples/vite-wallet/ledger.did +54 -0
- package/examples/vite-wallet/package.json +46 -0
- package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
- package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
- package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
- package/examples/vite-wallet/scripts/faucet.ts +41 -0
- package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
- package/examples/vite-wallet/src/AddressBook.tsx +147 -0
- package/examples/vite-wallet/src/App.tsx +53 -0
- package/examples/vite-wallet/src/Balance.test.tsx +57 -0
- package/examples/vite-wallet/src/Balance.tsx +58 -0
- package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
- package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
- package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
- package/examples/vite-wallet/src/Profile.test.tsx +145 -0
- package/examples/vite-wallet/src/Profile.tsx +92 -0
- package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
- package/examples/vite-wallet/src/SendIcp.tsx +244 -0
- package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
- package/examples/vite-wallet/src/SendToContact.tsx +102 -0
- package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
- package/examples/vite-wallet/src/SignIn.tsx +120 -0
- package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
- package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
- package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
- package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
- package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
- package/examples/vite-wallet/src/auth/local-page.ts +20 -0
- package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
- package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
- package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
- package/examples/vite-wallet/src/canisters/backend.ts +58 -0
- package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
- package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
- package/examples/vite-wallet/src/client.ts +45 -0
- package/examples/vite-wallet/src/main.tsx +18 -0
- package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
- package/examples/vite-wallet/src/refusal.ts +17 -0
- package/examples/vite-wallet/src/styles.css +282 -0
- package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
- package/examples/vite-wallet/src/token.ts +50 -0
- package/examples/vite-wallet/src/transfer.test.ts +178 -0
- package/examples/vite-wallet/src/transfer.ts +110 -0
- package/examples/vite-wallet/src/use-canisters.ts +49 -0
- package/examples/vite-wallet/tsconfig.json +19 -0
- package/examples/vite-wallet/vite.config.ts +47 -0
- package/examples/vite-wallet/vitest.config.ts +17 -0
- package/package.json +9 -4
- package/packages/core/.size-limit.js +47 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +84 -0
- package/packages/core/llms.txt +610 -0
- package/packages/core/package.json +89 -0
- package/packages/core/src/call.ts +961 -0
- package/packages/core/src/canister.ts +259 -0
- package/packages/core/src/client.ts +2061 -0
- package/packages/core/src/errors.ts +1079 -0
- package/packages/core/src/hydration.ts +239 -0
- package/packages/core/src/index.ts +23 -0
- package/packages/core/src/keys.ts +108 -0
- package/packages/core/src/management.ts +138 -0
- package/packages/core/src/network.ts +1020 -0
- package/packages/core/src/options.ts +1000 -0
- package/packages/core/src/runtime.ts +50 -0
- package/packages/core/src/testing/fake-replica.ts +1166 -0
- package/packages/core/src/testing/index.ts +16 -0
- package/packages/core/src/testing/test-auth.ts +340 -0
- package/packages/core/src/testing/test-client.ts +559 -0
- package/packages/core/src/types.ts +541 -0
- package/packages/core/src/units.ts +299 -0
- package/packages/core/src/version.ts +4 -0
- package/packages/core/tests/canister-helpers.ts +187 -0
- package/packages/core/tests/canister-id.test.ts +158 -0
- package/packages/core/tests/canister-unwrap.test.ts +94 -0
- package/packages/core/tests/canister.test-d.ts +406 -0
- package/packages/core/tests/canister.test.ts +1921 -0
- package/packages/core/tests/client-account-change.test.ts +131 -0
- package/packages/core/tests/client-agents.test.ts +402 -0
- package/packages/core/tests/client-auth-network.test.ts +719 -0
- package/packages/core/tests/client-disposed.test.ts +403 -0
- package/packages/core/tests/client-helpers.ts +136 -0
- package/packages/core/tests/client-hydration.test.ts +166 -0
- package/packages/core/tests/client-pinned.test.ts +626 -0
- package/packages/core/tests/client-session.test.ts +348 -0
- package/packages/core/tests/client.test-d.ts +181 -0
- package/packages/core/tests/client.test.ts +1162 -0
- package/packages/core/tests/encode.test.ts +171 -0
- package/packages/core/tests/env-canister-id.test.ts +666 -0
- package/packages/core/tests/env-cookie-copies.test.ts +182 -0
- package/packages/core/tests/error-guard.test.ts +222 -0
- package/packages/core/tests/errors.test-d.ts +280 -0
- package/packages/core/tests/errors.test.ts +1359 -0
- package/packages/core/tests/fixtures/README.md +28 -0
- package/packages/core/tests/fixtures/accounts.did +21 -0
- package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
- package/packages/core/tests/fixtures/accounts.ts +26 -0
- package/packages/core/tests/fixtures/archive.did +31 -0
- package/packages/core/tests/fixtures/archive.envelope.json +259 -0
- package/packages/core/tests/fixtures/archive.ts +55 -0
- package/packages/core/tests/fixtures/icrc1.did +88 -0
- package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
- package/packages/core/tests/fixtures/icrc1.ts +78 -0
- package/packages/core/tests/fixtures/management.did +724 -0
- package/packages/core/tests/fixtures/management.envelope.json +4455 -0
- package/packages/core/tests/fixtures/management.ts +486 -0
- package/packages/core/tests/fixtures/shapes.did +33 -0
- package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
- package/packages/core/tests/fixtures/shapes.ts +56 -0
- package/packages/core/tests/fixtures/skippable.did +15 -0
- package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
- package/packages/core/tests/fixtures/skippable.ts +33 -0
- package/packages/core/tests/func.test.ts +154 -0
- package/packages/core/tests/identity-attributes.test.ts +214 -0
- package/packages/core/tests/keys.test.ts +289 -0
- package/packages/core/tests/management.test.ts +206 -0
- package/packages/core/tests/network-helpers.ts +121 -0
- package/packages/core/tests/network.test-d.ts +78 -0
- package/packages/core/tests/network.test.ts +1051 -0
- package/packages/core/tests/options.test.ts +1594 -0
- package/packages/core/tests/redirect-sign-in.test.ts +393 -0
- package/packages/core/tests/resend.test-d.ts +81 -0
- package/packages/core/tests/resend.test.ts +723 -0
- package/packages/core/tests/skip.test-d.ts +125 -0
- package/packages/core/tests/testing-entry.test.ts +73 -0
- package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
- package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
- package/packages/core/tests/testing-test-auth.test.ts +483 -0
- package/packages/core/tests/testing-test-client.test-d.ts +184 -0
- package/packages/core/tests/testing-test-client.test.ts +1272 -0
- package/packages/core/tests/time-realm.test.ts +63 -0
- package/packages/core/tests/time.test.ts +238 -0
- package/packages/core/tests/traps/README.md +39 -0
- package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
- package/packages/core/tests/traps/classification-table.test.ts +256 -0
- package/packages/core/tests/traps/err-arm.test.ts +144 -0
- package/packages/core/tests/traps/fetch-spy.ts +39 -0
- package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
- package/packages/core/tests/traps/ledger.ts +248 -0
- package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
- package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
- package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
- package/packages/core/tests/traps/update-as-query.test.ts +244 -0
- package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
- package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
- package/packages/core/tests/traps.test-d.ts +476 -0
- package/packages/core/tests/units.test-d.ts +118 -0
- package/packages/core/tests/units.test.ts +634 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/core/tsconfig.typecheck.json +16 -0
- package/packages/core/vitest.config.ts +13 -0
- package/packages/mcp/LICENSE +21 -0
- package/packages/mcp/README.md +189 -0
- package/packages/mcp/package.json +62 -0
- package/packages/mcp/src/candid/candid-text.test.ts +455 -0
- package/packages/mcp/src/candid/candid-text.ts +501 -0
- package/packages/mcp/src/candid/contract.test.ts +398 -0
- package/packages/mcp/src/candid/formats.ts +155 -0
- package/packages/mcp/src/candid/hex.ts +29 -0
- package/packages/mcp/src/candid/json-schema.test.ts +758 -0
- package/packages/mcp/src/candid/json-schema.ts +454 -0
- package/packages/mcp/src/candid/json.test.ts +1207 -0
- package/packages/mcp/src/candid/json.ts +1053 -0
- package/packages/mcp/src/candid/schema-walk.ts +189 -0
- package/packages/mcp/src/candid/type-text.test.ts +179 -0
- package/packages/mcp/src/candid/type-text.ts +238 -0
- package/packages/mcp/src/cli.ts +134 -0
- package/packages/mcp/src/identity.ts +188 -0
- package/packages/mcp/src/interface/cache.test.ts +48 -0
- package/packages/mcp/src/interface/cache.ts +68 -0
- package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
- package/packages/mcp/src/interface/cli-cache.ts +122 -0
- package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
- package/packages/mcp/src/interface/compile-worker.ts +290 -0
- package/packages/mcp/src/interface/compile.test.ts +155 -0
- package/packages/mcp/src/interface/compile.ts +281 -0
- package/packages/mcp/src/interface/docs.test.ts +105 -0
- package/packages/mcp/src/interface/docs.ts +86 -0
- package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
- package/packages/mcp/src/interface/hex.ts +8 -0
- package/packages/mcp/src/interface/ids.test.ts +71 -0
- package/packages/mcp/src/interface/ids.ts +87 -0
- package/packages/mcp/src/interface/loader.test.ts +804 -0
- package/packages/mcp/src/interface/loader.ts +519 -0
- package/packages/mcp/src/interface/read-candid.test.ts +300 -0
- package/packages/mcp/src/interface/read-candid.ts +259 -0
- package/packages/mcp/src/interface/types.ts +113 -0
- package/packages/mcp/src/network.ts +122 -0
- package/packages/mcp/src/options.ts +149 -0
- package/packages/mcp/src/server/approval.ts +159 -0
- package/packages/mcp/src/server/naming.ts +43 -0
- package/packages/mcp/src/server/results.ts +102 -0
- package/packages/mcp/src/server/server.ts +797 -0
- package/packages/mcp/src/server/tools.ts +116 -0
- package/packages/mcp/src/usage.ts +40 -0
- package/packages/mcp/tests/approval.test.ts +60 -0
- package/packages/mcp/tests/identity.test.ts +234 -0
- package/packages/mcp/tests/naming.test.ts +62 -0
- package/packages/mcp/tests/options.test.ts +140 -0
- package/packages/mcp/tests/server.test.ts +997 -0
- package/packages/mcp/tsconfig.json +19 -0
- package/packages/mcp/tsconfig.typecheck.json +10 -0
- package/packages/mcp/vitest.config.ts +10 -0
- package/packages/react/LICENSE +21 -0
- package/packages/react/README.md +328 -0
- package/packages/react/llms.txt +95 -0
- package/packages/react/package.json +95 -0
- package/packages/react/src/index.tsx +678 -0
- package/packages/react/tests/abandoned-render.test.tsx +816 -0
- package/packages/react/tests/auth.test.tsx +315 -0
- package/packages/react/tests/caller-keys.test.tsx +2334 -0
- package/packages/react/tests/client-time.test.ts +40 -0
- package/packages/react/tests/entry.test.ts +52 -0
- package/packages/react/tests/helpers.tsx +291 -0
- package/packages/react/tests/hydration.test.tsx +262 -0
- package/packages/react/tests/ownership.test.tsx +260 -0
- package/packages/react/tests/provider.server.test.tsx +169 -0
- package/packages/react/tests/query-client.test.tsx +107 -0
- package/packages/react/tests/strict-mode.test.tsx +288 -0
- package/packages/react/tests/suspense.test-d.tsx +357 -0
- package/packages/react/tests/two-query-cores.test.tsx +135 -0
- package/packages/react/tests/types.test-d.tsx +63 -0
- package/packages/react/tsconfig.json +23 -0
- package/packages/react/tsconfig.test.json +8 -0
- package/packages/react/tsconfig.typecheck.json +16 -0
- package/packages/react/vitest.config.ts +39 -0
- package/packages/vite-plugin/LICENSE +21 -0
- package/packages/vite-plugin/README.md +532 -0
- package/packages/vite-plugin/package.json +76 -0
- package/packages/vite-plugin/src/describe-value.test.ts +43 -0
- package/packages/vite-plugin/src/describe-value.ts +167 -0
- package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
- package/packages/vite-plugin/src/dev-environment.ts +510 -0
- package/packages/vite-plugin/src/env.ts +431 -0
- package/packages/vite-plugin/src/fetch.ts +348 -0
- package/packages/vite-plugin/src/file-identity.ts +106 -0
- package/packages/vite-plugin/src/generate.ts +691 -0
- package/packages/vite-plugin/src/index.test.ts +1243 -0
- package/packages/vite-plugin/src/index.ts +2565 -0
- package/packages/vite-plugin/src/interface.ts +339 -0
- package/packages/vite-plugin/src/local-host.ts +34 -0
- package/packages/vite-plugin/src/options.ts +641 -0
- package/packages/vite-plugin/src/principal-text.test.ts +87 -0
- package/packages/vite-plugin/src/principal-text.ts +98 -0
- package/packages/vite-plugin/tests/close.test.ts +141 -0
- package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
- package/packages/vite-plugin/tests/fake-ic.ts +214 -0
- package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
- package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
- package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
- package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
- package/packages/vite-plugin/tests/generation.test.ts +1036 -0
- package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
- package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
- package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
- package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
- package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
- package/packages/vite-plugin/tests/package.test.ts +62 -0
- package/packages/vite-plugin/tests/project.test.ts +2993 -0
- package/packages/vite-plugin/tests/support.ts +339 -0
- package/packages/vite-plugin/tests/timeout.test.ts +107 -0
- package/packages/vite-plugin/tsconfig.json +23 -0
- package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
- package/packages/vite-plugin/vitest.config.ts +9 -0
- package/pnpm-workspace.yaml +23 -0
- package/scripts/ai-context-files.js +72 -0
- package/scripts/assemble-docs-site.mjs +485 -0
- package/scripts/assemble-docs-site.test.mjs +568 -0
- package/scripts/build-examples.js +58 -0
- package/scripts/check-ai-context.js +963 -0
- package/scripts/check-ai-context.test.mjs +811 -0
- package/scripts/check-exports.mjs +767 -0
- package/scripts/check-exports.test.mjs +1136 -0
- package/scripts/check-snippets/ambient.d.ts +428 -0
- package/scripts/check-snippets/app/generated/backend.did +7 -0
- package/scripts/check-snippets/app/generated/backend.ts +15 -0
- package/scripts/check-snippets/app/generated/icrc1.did +88 -0
- package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
- package/scripts/check-snippets/app/globals.ts +26 -0
- package/scripts/check-snippets/docs/guides/App.tsx +21 -0
- package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
- package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
- package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
- package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
- package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
- package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
- package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
- package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
- package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
- package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
- package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
- package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
- package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
- package/scripts/check-snippets.mjs +993 -0
- package/scripts/check-snippets.test.mjs +338 -0
- package/scripts/check-stack.mjs +501 -0
- package/scripts/check-stack.test.mjs +800 -0
- package/scripts/clean-node-modules.js +67 -0
- package/scripts/example-projects.js +58 -0
- package/scripts/export-budget.mjs +145 -0
- package/scripts/faults.json +6749 -0
- package/scripts/lib/fault-workspace.mjs +210 -0
- package/scripts/lib/io.mjs +29 -0
- package/scripts/lib/io.test.mjs +38 -0
- package/scripts/lib/markdown-fences.mjs +56 -0
- package/scripts/lib/markdown-fences.test.mjs +84 -0
- package/scripts/lib/package-exports.mjs +54 -0
- package/scripts/lib/package-exports.test.mjs +66 -0
- package/scripts/lib/release-pr-steps.mjs +47 -0
- package/scripts/release-tag.mjs +95 -0
- package/scripts/release-tag.test.mjs +278 -0
- package/scripts/release-workflow.test.mjs +98 -0
- package/scripts/release.js +167 -0
- package/scripts/release.test.mjs +347 -0
- package/scripts/removed-v3-names.js +125 -0
- package/scripts/size-app/.size-limit.js +90 -0
- package/scripts/size-app/core.js +3 -0
- package/scripts/size-app/package.json +24 -0
- package/scripts/size-app/react.js +4 -0
- package/scripts/size-app.test.mjs +73 -0
- package/scripts/sync-ai-context-versions.js +102 -0
- package/scripts/sync-example-versions.js +151 -0
- package/scripts/test-examples-published.mjs +1585 -0
- package/scripts/test-examples-published.test.mjs +1586 -0
- package/scripts/traps/any-method-vars-not-a-list.json +5 -0
- package/scripts/traps/canister-method-unknown.json +5 -0
- package/scripts/traps/client-says-who-calls-once.json +5 -0
- package/scripts/traps/client-says-who-calls.json +5 -0
- package/scripts/traps/err-only-on-canister-err.json +14 -0
- package/scripts/traps/format-units-refuses-missing-value.json +5 -0
- package/scripts/traps/format-units-refuses-number.json +5 -0
- package/scripts/traps/format-units-unknown-option.json +5 -0
- package/scripts/traps/guard-err-is-the-methods.json +5 -0
- package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
- package/scripts/traps/internals-not-exported.json +9 -0
- package/scripts/traps/network-refuses-unknown-name.json +5 -0
- package/scripts/traps/never-err-has-no-type.json +5 -0
- package/scripts/traps/options-method-unknown.json +5 -0
- package/scripts/traps/options-vars-wrong-type.json +5 -0
- package/scripts/traps/parse-units-refuses-number.json +5 -0
- package/scripts/traps/parse-units-unknown-option.json +5 -0
- package/scripts/traps/principal-is-not-plain-text.json +14 -0
- package/scripts/traps/query-options-refuses-oneway.json +5 -0
- package/scripts/traps/query-options-refuses-update.json +5 -0
- package/scripts/traps/refuse-next-unknown-option.json +5 -0
- package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
- package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
- package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
- package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
- package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
- package/scripts/traps/test-handler-method-unknown.json +5 -0
- package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
- package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
- package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
- package/scripts/typecheck-examples.js +122 -0
- package/scripts/verify-faults.mjs +375 -0
- package/scripts/verify-faults.test.mjs +402 -0
- package/scripts/verify-package-artifacts.mjs +590 -0
- package/scripts/verify-peer-floors.js +399 -0
- package/scripts/verify-peer-floors.test.mjs +422 -0
- package/scripts/verify-test-fails-rules.mjs +104 -0
- package/scripts/verify-test-fails-rules.test.mjs +215 -0
- package/scripts/verify-test-fails.js +371 -0
- package/scripts/verify-traps.mjs +505 -0
- package/scripts/verify-traps.test.mjs +521 -0
- package/scripts/verify-typescript-floor.js +239 -0
- package/skill-packages/ic-reactor/SKILL.md +103 -0
- package/stack.json +8 -0
- package/tsconfig.base.json +23 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,2565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ic-reactor/vite-plugin
|
|
3
|
+
*
|
|
4
|
+
* Vite plugin for an app built on a module Meant generates.
|
|
5
|
+
*
|
|
6
|
+
* - Generation: at the start of a build or dev server, and when a `.did` file
|
|
7
|
+
* changes, it runs `meant gen` in a child process (see
|
|
8
|
+
* generate.ts) and leaves the module as the generator wrote it. No
|
|
9
|
+
* wrapper files, hooks or reactors are generated.
|
|
10
|
+
* - Fetching: a `didFile` that is not on disk is written from the live
|
|
11
|
+
* canister's certified `candid:service` before it is generated, when the
|
|
12
|
+
* canister names a `canisterId` (see fetch.ts), projected to its `methods`
|
|
13
|
+
* when it names some (see interface.ts). Nothing is fetched for a
|
|
14
|
+
* `didFile` that exists, unless `IC_REACTOR_FETCH` asks for it.
|
|
15
|
+
* - The live check: with `verify: "live"` or `IC_REACTOR_VERIFY`, a `didFile`
|
|
16
|
+
* on disk is checked against the live canister's interface (interface.ts).
|
|
17
|
+
* - Environment: under `vite dev` and `vite preview` it sets the `ic_env`
|
|
18
|
+
* cookie and proxies `/api` to the local IC network (see dev-environment.ts).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from "node:fs"
|
|
22
|
+
import path from "node:path"
|
|
23
|
+
import type {
|
|
24
|
+
Logger,
|
|
25
|
+
Plugin,
|
|
26
|
+
ProxyOptions,
|
|
27
|
+
UserConfig,
|
|
28
|
+
ViteDevServer,
|
|
29
|
+
} from "vite"
|
|
30
|
+
import { describeError, describePath, describeValue } from "./describe-value.js"
|
|
31
|
+
import {
|
|
32
|
+
createLocalEnvironment,
|
|
33
|
+
icEnvMiddleware,
|
|
34
|
+
type LocalEnvironment,
|
|
35
|
+
type LocalEnvironmentState,
|
|
36
|
+
} from "./dev-environment.js"
|
|
37
|
+
import { installIcpAdvice } from "./env.js"
|
|
38
|
+
import { fetchCandid, resolveNetwork, writeDid } from "./fetch.js"
|
|
39
|
+
import { cachedFoldsCase, fileIdentity, realFile } from "./file-identity.js"
|
|
40
|
+
import {
|
|
41
|
+
GENERATE_TIMEOUT_MS,
|
|
42
|
+
foldsCase,
|
|
43
|
+
generate,
|
|
44
|
+
moduleCollisions,
|
|
45
|
+
outputsOf,
|
|
46
|
+
quoteNames,
|
|
47
|
+
resolveCliBin,
|
|
48
|
+
type GenerateCanister,
|
|
49
|
+
} from "./generate.js"
|
|
50
|
+
import {
|
|
51
|
+
check,
|
|
52
|
+
describeFinding,
|
|
53
|
+
firstLines,
|
|
54
|
+
listMethods,
|
|
55
|
+
project,
|
|
56
|
+
type CliContext,
|
|
57
|
+
type Projection,
|
|
58
|
+
} from "./interface.js"
|
|
59
|
+
import { readOptions, type Generated, type Refused } from "./options.js"
|
|
60
|
+
|
|
61
|
+
const PLUGIN_NAME = "ic-reactor-plugin"
|
|
62
|
+
|
|
63
|
+
/** The first line the plugin logs: where an agent reads how to use the library. */
|
|
64
|
+
const GUIDE_LINE =
|
|
65
|
+
"ic-reactor: agent guide at node_modules/@ic-reactor/core/llms.txt"
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The options of {@link icReactor}. Every one is optional: with none, the
|
|
69
|
+
* plugin generates nothing and, under `vite dev`, only injects the local IC
|
|
70
|
+
* environment, for no canister of the app's. A key that is none of these
|
|
71
|
+
* three (a misspelled `canister`) throws when the config loads, whatever its
|
|
72
|
+
* value, and the message names the option it may misspell.
|
|
73
|
+
*/
|
|
74
|
+
export interface IcReactorPluginOptions {
|
|
75
|
+
/**
|
|
76
|
+
* The app's canisters, by name: the canister's name in the `icp` project,
|
|
77
|
+
* which is also the name the `ic_env` cookie carries its ID under. A name
|
|
78
|
+
* is kept as given: `__proto__` and `constructor` name canisters as any
|
|
79
|
+
* other name does, as a JSON config or a computed key
|
|
80
|
+
* (`["__proto__"]: { … }`) gives them, and so does a name that begins with
|
|
81
|
+
* `-`. (The literal `__proto__: { … }` of a JavaScript object sets the
|
|
82
|
+
* object's prototype instead, which makes it no plain object: see below.)
|
|
83
|
+
* The cookie joins its entries with `&` and each name to its ID with `=`,
|
|
84
|
+
* so a name that holds either, which no name `icp` gives a canister does,
|
|
85
|
+
* is left out of it, with a warning under `vite dev`, and `icp` is not
|
|
86
|
+
* asked for its ID; the canister is generated as any other.
|
|
87
|
+
*
|
|
88
|
+
* - `didFile`: the canister's Candid interface, relative to the Vite root.
|
|
89
|
+
* The plugin runs `meant gen` on it at the start of a build or
|
|
90
|
+
* dev server and again each time the file changes, regenerating only the
|
|
91
|
+
* canisters that name the file that changed. The generator names its
|
|
92
|
+
* output after the file's name without its `.did`, so
|
|
93
|
+
* `didFile: "../backend/ledger.did"` writes `ledger.ts` (the module: it
|
|
94
|
+
* exports `actor` and the type `Actor`) and `ledger.envelope.json` into
|
|
95
|
+
* `outDir`. It reads only a file whose name ends in `.did`, in lowercase:
|
|
96
|
+
* `api.candid` or `api.DID` fails its canister. Canisters that name the
|
|
97
|
+
* same `didFile` and `outDir` share that one module, which is generated
|
|
98
|
+
* once. Different `.did` files that would write the same module are
|
|
99
|
+
* refused: the canister later in `canisters` fails at the start of each
|
|
100
|
+
* run, before anything is fetched, run or written for it, and a save of
|
|
101
|
+
* its file does not generate it either. Give one an `outDir` of its own. A
|
|
102
|
+
* canister without a `didFile` generates nothing and is only named in
|
|
103
|
+
* the cookie, its other options checked all the same. A run (the start of
|
|
104
|
+
* a build or dev server, or a save's regeneration) reads each `didFile`
|
|
105
|
+
* once, and takes nothing of one written before it is done with it, even
|
|
106
|
+
* saved back as it was: the methods comparison, the live check and
|
|
107
|
+
* generation read the file again, and might each read another text. The
|
|
108
|
+
* run does not generate such a canister, reports none of the failures
|
|
109
|
+
* its steps found for it, and puts back a module the generator wrote
|
|
110
|
+
* while the file was written. A build fails it, for that reason alone
|
|
111
|
+
* ("changed during the build"); under `vite dev` it is queued again, as a
|
|
112
|
+
* save is, and that run reports it.
|
|
113
|
+
* - `outDir`: where the generator writes, relative to the Vite root.
|
|
114
|
+
* Default: `"src/canisters"`.
|
|
115
|
+
* - `canisterId`: the canister's ID. Under `vite dev` and `vite preview` the
|
|
116
|
+
* cookie carries it, and it wins over the ID `icp` reports for the
|
|
117
|
+
* canister. With a `didFile` that is not on disk, the build first reads
|
|
118
|
+
* the canister's `candid:service` from certified state on `network` and
|
|
119
|
+
* writes it to `didFile`, which is then generated as any other and
|
|
120
|
+
* committed with the app. A `didFile` on disk is never fetched or
|
|
121
|
+
* rewritten: the build reads it with no network access. To fetch it again,
|
|
122
|
+
* run the build or dev server with `IC_REACTOR_FETCH=<name>` (several
|
|
123
|
+
* names separated by commas, or `all`). A fetch that fails (the network
|
|
124
|
+
* cannot be reached, the canister keeps its interface private or publishes
|
|
125
|
+
* none) fails the canister as a generation does, under `failOnError`.
|
|
126
|
+
* Canisters that name one file, under any path to it, share one fetch;
|
|
127
|
+
* when they name different canisters (or networks) to fetch it from, none
|
|
128
|
+
* is fetched and each fails, since whose interface it is cannot be told.
|
|
129
|
+
* Only an omitted `canisterId` is none: any other value that is not the
|
|
130
|
+
* text of a principal (`""`, `null` from a JavaScript config, or text
|
|
131
|
+
* with a letter wrong, which its checksum tells) fails the canister when
|
|
132
|
+
* the plugin is made, and is left out of the cookie with a warning.
|
|
133
|
+
* - `network`: where the canister to fetch is. `"ic"`, mainnet, checked
|
|
134
|
+
* against the mainnet root key; `"local"`, icp-cli's local network on
|
|
135
|
+
* `http://127.0.0.1:8000`, whose root key is fetched from it; or
|
|
136
|
+
* `{ host }`, any other replica, whose root key is fetched only when the
|
|
137
|
+
* host is local (`localhost`, `*.localhost` or a loopback address) and is
|
|
138
|
+
* mainnet's otherwise. Only a fetch and the live check use it, but any
|
|
139
|
+
* other value fails the canister. Default: `"ic"`.
|
|
140
|
+
* - `methods`: the methods of the canister the app calls. A `didFile`
|
|
141
|
+
* fetched from `canisterId` is written as the projection of the live
|
|
142
|
+
* interface to these methods and every type they reach
|
|
143
|
+
* (`meant project`), so its module has only them. A name the
|
|
144
|
+
* live interface lacks fails the canister and writes nothing. A
|
|
145
|
+
* `didFile` on disk is not fetched again for a change of `methods`: one
|
|
146
|
+
* that lacks a name of `methods` fails the canister, and one that holds a
|
|
147
|
+
* method `methods` does not name is generated with a warning. Both say to
|
|
148
|
+
* fetch it again with `IC_REACTOR_FETCH`. Canisters that name one
|
|
149
|
+
* `didFile` to be fetched must name one list, those with no `canisterId`
|
|
150
|
+
* included (no `methods` is the whole interface): otherwise none is
|
|
151
|
+
* fetched and each fails, before any request. Only an omitted `methods`
|
|
152
|
+
* is the whole interface: any other value that is not a list of method
|
|
153
|
+
* names (`null`, `false`, `""`) fails the canister.
|
|
154
|
+
* - `verify`: `"live"` checks the `didFile` on disk against the live
|
|
155
|
+
* canister's certified interface at the start of each build and dev
|
|
156
|
+
* server (`meant check`). `IC_REACTOR_VERIFY=1` (or `true`,
|
|
157
|
+
* `all`, or canister names separated by commas) does the same for the
|
|
158
|
+
* canisters with a `canisterId`, and `IC_REACTOR_VERIFY=0` (or `false`)
|
|
159
|
+
* turns the check off for the run, `verify: "live"` included. A method
|
|
160
|
+
* of the file that the canister no longer has, whose mode changed, or
|
|
161
|
+
* whose types are no longer compatible fails the canister, under
|
|
162
|
+
* `failOnError`; a compatible change is logged as a warning. A check that
|
|
163
|
+
* cannot read the canister fails it too. Within one build or dev server,
|
|
164
|
+
* a rebuild or another Vite environment does not check a text that passed
|
|
165
|
+
* again, nor a file it just fetched, until a check or a fetch of that
|
|
166
|
+
* canister fails or reads another interface. The check of a file saved
|
|
167
|
+
* before the check was done (even saved back) counts for nothing: it
|
|
168
|
+
* records no pass, and neither sets nor clears a failed check (see
|
|
169
|
+
* `didFile` for the rest of that run). Each new build or dev server, a
|
|
170
|
+
* restart included, checks again. Any value other than `"live"`, `false`
|
|
171
|
+
* included, fails the canister, and so does `"live"` with no
|
|
172
|
+
* `canisterId`: remove `verify` for no check.
|
|
173
|
+
*
|
|
174
|
+
* Every value given is checked when the plugin is made, on every canister,
|
|
175
|
+
* one without a `didFile` included, and whether or not a run would use it:
|
|
176
|
+
* only an omitted option is one not given, and takes its default. So is
|
|
177
|
+
* every key: a key of an entry that is none of the six above (a misspelled
|
|
178
|
+
* `didfile` or `canisterID`), or a key of a `network` of `{ host }` other
|
|
179
|
+
* than `host`, refuses the canister whatever its value, `undefined`
|
|
180
|
+
* included, and the message names the option it may misspell. Never read,
|
|
181
|
+
* it would take the canister for one only named in the cookie, or generate
|
|
182
|
+
* it without its ID, without a word. The options, `canisters`, each entry
|
|
183
|
+
* and a `network` of `{ host }` are plain objects, whose prototype is
|
|
184
|
+
* `Object.prototype` or `null`: written `{ … }`, or made by `JSON.parse`,
|
|
185
|
+
* `Object.fromEntries` or `Object.create(null)`. A `Map`, a `Promise`, an
|
|
186
|
+
* instance of a class and an object that inherits from another are not,
|
|
187
|
+
* and are refused as any other value that is not one: read for their keys,
|
|
188
|
+
* they would be read as none. `canisters` itself that is not a plain
|
|
189
|
+
* object, `null` included, throws when the config loads, as the options do.
|
|
190
|
+
* A canister whose options are not one, or hold a value that is not one (a
|
|
191
|
+
* `didFile` or `outDir` that is not a path, any of the values above, or an
|
|
192
|
+
* option whose getter throws), or a key that is no option, is refused:
|
|
193
|
+
* each run fails it with every reason, under `failOnError`, before the run
|
|
194
|
+
* does anything for it. Nothing is fetched, written, run or requested for
|
|
195
|
+
* it, nor fetched for a `didFile` it shares with other canisters, and the
|
|
196
|
+
* others run as they would. The same holds for a canister whose module a
|
|
197
|
+
* canister before it writes (see `didFile`), and for one that
|
|
198
|
+
* `IC_REACTOR_FETCH` or `IC_REACTOR_VERIFY` names but that has no
|
|
199
|
+
* `canisterId`. Under `vite dev`, a save of its file, and the file's
|
|
200
|
+
* deletion, leave it failed for the same reason and run nothing for it. A
|
|
201
|
+
* refused canister is still named in the cookie, as every entry is (but one
|
|
202
|
+
* whose name the cookie cannot carry): under `vite dev`, `icp` is asked for
|
|
203
|
+
* its ID, since the app may still call it. Messages quote each value as it
|
|
204
|
+
* was given, whatever its kind (a bigint, a symbol, an object that holds
|
|
205
|
+
* itself).
|
|
206
|
+
*
|
|
207
|
+
* The generator is the `@meantjs/cli` the app has installed, run as a
|
|
208
|
+
* child process so that a failure on one `.did` stops that process and not
|
|
209
|
+
* the dev server.
|
|
210
|
+
*/
|
|
211
|
+
canisters?: Record<
|
|
212
|
+
string,
|
|
213
|
+
{
|
|
214
|
+
didFile?: string
|
|
215
|
+
outDir?: string
|
|
216
|
+
canisterId?: string
|
|
217
|
+
network?: "ic" | "local" | { host: string }
|
|
218
|
+
methods?: readonly string[]
|
|
219
|
+
verify?: "live"
|
|
220
|
+
}
|
|
221
|
+
>
|
|
222
|
+
/**
|
|
223
|
+
* Inject the local IC environment under `vite dev` and `vite preview`: set
|
|
224
|
+
* the `ic_env` cookie on each response and proxy `/api` to the network the
|
|
225
|
+
* `icp` CLI reports.
|
|
226
|
+
*
|
|
227
|
+
* Until `icp` reports a network and every configured canister has an ID
|
|
228
|
+
* (a configured `canisterId` counts), each page load asks `icp` again, so a
|
|
229
|
+
* deploy after the server started needs only a reload. Once detection is
|
|
230
|
+
* complete, page loads run no `icp` command, and a redeploy into a fresh
|
|
231
|
+
* network needs a restart. An `/api` proxy that the Vite config or another
|
|
232
|
+
* plugin sets is left alone.
|
|
233
|
+
*
|
|
234
|
+
* Never injected in mode `"test"` (Vitest's), where `icp` is not run at all.
|
|
235
|
+
*
|
|
236
|
+
* Default: true. Any value given other than `true` or `false` (`null`,
|
|
237
|
+
* `0`, `"false"`) throws when the config loads.
|
|
238
|
+
*/
|
|
239
|
+
injectEnvironment?: boolean
|
|
240
|
+
/**
|
|
241
|
+
* Abort the Vite run when a canister fails to generate.
|
|
242
|
+
*
|
|
243
|
+
* Default: `true` under `vite build` and under Vitest (mode `test`), `false`
|
|
244
|
+
* under `vite dev`. A build that silently ships the bindings left over from
|
|
245
|
+
* the last successful run is worse than no build at all, and a test run
|
|
246
|
+
* that passes against them proves nothing, while a dev server has to
|
|
247
|
+
* survive the broken intermediate states of a `.did` file being edited:
|
|
248
|
+
* there the failure is logged and shown in the browser's error overlay, and
|
|
249
|
+
* the server keeps serving. Any value given other than `true` or `false`
|
|
250
|
+
* (`null`, `0`, `"false"`) throws when the config loads.
|
|
251
|
+
*/
|
|
252
|
+
failOnError?: boolean
|
|
253
|
+
/**
|
|
254
|
+
* Called once for each module a run generated, after the generator wrote
|
|
255
|
+
* or checked it, in `vite build` and `vite dev` alike: the place for a
|
|
256
|
+
* step that reads the generated output (a module's envelope, say) and must
|
|
257
|
+
* run after every generation.
|
|
258
|
+
*
|
|
259
|
+
* ```ts
|
|
260
|
+
* icReactor({
|
|
261
|
+
* canisters: { ledger: { didFile: "did/ledger.did" } },
|
|
262
|
+
* onGenerated: async ({ names, envelope, signal }) => {
|
|
263
|
+
* // read the envelope, write derived files, or throw to fail the run
|
|
264
|
+
* },
|
|
265
|
+
* })
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* - `names`: the canisters the module is for. Canisters that share a
|
|
269
|
+
* `didFile` and an `outDir` share one module and one call.
|
|
270
|
+
* - `didFile`, `module`, `envelope`: absolute paths of the `.did` it was
|
|
271
|
+
* generated from, the module and its envelope. The module and the
|
|
272
|
+
* envelope are on disk when it is called.
|
|
273
|
+
* - `status`: `"written"` when the generator wrote the module,
|
|
274
|
+
* `"unchanged"` when it found the module already as it would write it.
|
|
275
|
+
* - `signal`: aborted when the build or dev server the run belongs to
|
|
276
|
+
* closes. The run then stops waiting for the hook and reports nothing,
|
|
277
|
+
* whatever the hook does, and the next run generates the module again
|
|
278
|
+
* and calls the hook again.
|
|
279
|
+
*
|
|
280
|
+
* It runs in the plugin's one queue of runs, so it never overlaps a
|
|
281
|
+
* generation, and a build's `buildStart` waits for it: under `vite build`
|
|
282
|
+
* a hook that never settles holds the build until it closes. It is not
|
|
283
|
+
* called for a canister that failed or was refused, for a run whose
|
|
284
|
+
* `.did` was written while the generator read it (the run takes nothing of
|
|
285
|
+
* it), nor for a run that does not generate the module again: the second
|
|
286
|
+
* `buildStart` of Vite 6 and later, and each rebuild of
|
|
287
|
+
* `vite build --watch` not caused by a `.did` or by a missing module or
|
|
288
|
+
* envelope. A save under `vite dev`
|
|
289
|
+
* always generates again, so a save of the same text calls it, with
|
|
290
|
+
* `"unchanged"`. A `.did` written while the hook runs is caught after it:
|
|
291
|
+
* a build then fails as changed during the build, and a dev server
|
|
292
|
+
* generates it again and calls the hook again.
|
|
293
|
+
*
|
|
294
|
+
* A throw or a rejection fails the module's canisters as a failed
|
|
295
|
+
* generation does: a build or a Vitest run fails, unless `failOnError` is
|
|
296
|
+
* `false`, and otherwise the failure is logged and, under `vite dev`, shown
|
|
297
|
+
* in the error overlay. The next run generates those canisters again, and calls
|
|
298
|
+
* the hook again. A value given that is not a function throws when the
|
|
299
|
+
* config loads.
|
|
300
|
+
*/
|
|
301
|
+
onGenerated?: (generated: {
|
|
302
|
+
readonly names: readonly string[]
|
|
303
|
+
readonly didFile: string
|
|
304
|
+
readonly module: string
|
|
305
|
+
readonly envelope: string
|
|
306
|
+
readonly status: "written" | "unchanged"
|
|
307
|
+
readonly signal: AbortSignal
|
|
308
|
+
}) => void | Promise<void>
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The environment variable that asks for a `didFile` to be fetched again. */
|
|
312
|
+
const FETCH_VARIABLE = "IC_REACTOR_FETCH"
|
|
313
|
+
|
|
314
|
+
/** The environment variable that asks for the live check. */
|
|
315
|
+
const VERIFY_VARIABLE = "IC_REACTOR_VERIFY"
|
|
316
|
+
|
|
317
|
+
/** The values of `IC_REACTOR_VERIFY` that check every canister. */
|
|
318
|
+
const VERIFY_ALL = ["1", "true", "all"]
|
|
319
|
+
|
|
320
|
+
/** The values of `IC_REACTOR_VERIFY` that check none, over `verify`. */
|
|
321
|
+
const VERIFY_OFF = ["0", "false"]
|
|
322
|
+
|
|
323
|
+
/** `methods` once each, in name order (code point), as `project` lists them. */
|
|
324
|
+
function methodList(methods: readonly string[]): string[] {
|
|
325
|
+
return [...new Set(methods)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** `a, b and 3 more`: names as messages list them, at most `max` of them. */
|
|
329
|
+
function listNames(names: readonly string[], max = 30): string {
|
|
330
|
+
if (names.length === 0) return "none"
|
|
331
|
+
const shown = names.slice(0, max).join(", ")
|
|
332
|
+
return names.length > max ? `${shown} and ${names.length - max} more` : shown
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** The names an environment variable lists, and whether it says `all`. */
|
|
336
|
+
function namesIn(
|
|
337
|
+
variable: string,
|
|
338
|
+
all: string[]
|
|
339
|
+
): {
|
|
340
|
+
names: string[]
|
|
341
|
+
all: boolean
|
|
342
|
+
} {
|
|
343
|
+
const names = (process.env[variable] ?? "")
|
|
344
|
+
.split(",")
|
|
345
|
+
.map((name) => name.trim())
|
|
346
|
+
.filter(Boolean)
|
|
347
|
+
return {
|
|
348
|
+
names,
|
|
349
|
+
all: names.some((name) => all.includes(name)),
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** A canister that did not generate, and why. */
|
|
354
|
+
interface Failure {
|
|
355
|
+
canister: Generated | Refused
|
|
356
|
+
message: string
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* What `IC_REACTOR_FETCH` and `IC_REACTOR_VERIFY` ask of one run, read once
|
|
361
|
+
* when its steps start.
|
|
362
|
+
*/
|
|
363
|
+
interface Requests {
|
|
364
|
+
/** Whether `IC_REACTOR_FETCH` asks for the canister's `didFile` again. */
|
|
365
|
+
fetches: (canister: { name: string }) => boolean
|
|
366
|
+
/**
|
|
367
|
+
* Whether the canister's live check runs: by `verify: "live"`, or by
|
|
368
|
+
* `IC_REACTOR_VERIFY`. `"off"` when `IC_REACTOR_VERIFY` turns it off.
|
|
369
|
+
*/
|
|
370
|
+
checks: ((canister: Generated) => boolean) | "off"
|
|
371
|
+
/** The canisters a variable names that cannot do what it asks, and why. */
|
|
372
|
+
failures: Array<{ canister: Generated; message: string }>
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* What one build or one dev server works with, from the moment Vite resolves
|
|
377
|
+
* its config (`configResolved`) to its close (`closeBundle`). A dev server
|
|
378
|
+
* restarted with the same plugin object (`server.restart()`, or `r` and
|
|
379
|
+
* Enter, of an inline config) is a new lifetime. Vite resolves the new
|
|
380
|
+
* config, and may start the new server, before it closes the old one: each
|
|
381
|
+
* lifetime holds the state of its own runs, so that the old server's close
|
|
382
|
+
* stops only its own and leaves the new server's alone.
|
|
383
|
+
*/
|
|
384
|
+
interface Lifetime {
|
|
385
|
+
/**
|
|
386
|
+
* Whether a failed generation fails the run when `failOnError` is omitted:
|
|
387
|
+
* under `vite build`, and under Vitest (mode `test`), whose tests would
|
|
388
|
+
* otherwise run against the modules left over from the last good run.
|
|
389
|
+
*/
|
|
390
|
+
failsByDefault: boolean
|
|
391
|
+
/** The dev server, once `configureServer` hands it over. */
|
|
392
|
+
server: ViteDevServer | null
|
|
393
|
+
/**
|
|
394
|
+
* Aborted when this build or dev server ends (`closeBundle`), which kills
|
|
395
|
+
* the generator processes it runs and drops its runs still queued. A
|
|
396
|
+
* fresh controller follows each abort: Vite 6 and later run the next
|
|
397
|
+
* environment's build in the same lifetime, and `vite build --watch` its
|
|
398
|
+
* next rebuild.
|
|
399
|
+
*/
|
|
400
|
+
stopper: AbortController
|
|
401
|
+
/** The `.did` files waiting for a run that has not started, by `identify`. */
|
|
402
|
+
queued: Set<string>
|
|
403
|
+
/**
|
|
404
|
+
* The text this plugin last wrote to each `.did` it fetched under this dev
|
|
405
|
+
* server, by `identify`, until the server's watcher reports that write.
|
|
406
|
+
*/
|
|
407
|
+
justFetched: Map<string, string>
|
|
408
|
+
/**
|
|
409
|
+
* The text of each `didFile` the live check passed, by canister name. The
|
|
410
|
+
* next `buildStart` of the lifetime (a rebuild of `vite build --watch`, or
|
|
411
|
+
* another Vite environment's) checks it again only when the text changed.
|
|
412
|
+
* A new lifetime starts with none: Vite resolves the config again for each
|
|
413
|
+
* build and each dev server, a restart (`r` and Enter, or
|
|
414
|
+
* `server.restart()`) included, so a restart checks every canister again,
|
|
415
|
+
* and finds a canister that changed since.
|
|
416
|
+
*
|
|
417
|
+
* A file the lifetime fetched is the live interface, and passes as
|
|
418
|
+
* fetched. A check that failed, or could not be made, and a fetch that
|
|
419
|
+
* failed, drop the canister's entry: a text that passed before is checked
|
|
420
|
+
* again, so restoring it cannot stand in for a check that passes now.
|
|
421
|
+
*
|
|
422
|
+
* What passes is the text that was checked: the text the run read (see
|
|
423
|
+
* {@link Run}), and only if the file still holds it, unwritten, once the
|
|
424
|
+
* check is done. A file written since the run read it (even saved back) is
|
|
425
|
+
* not recorded, and its next `buildStart` checks it again. A fetched file
|
|
426
|
+
* passes as the text the fetch wrote, not as the file read again.
|
|
427
|
+
*/
|
|
428
|
+
verified: Map<string, string>
|
|
429
|
+
/**
|
|
430
|
+
* The failure of each canister whose last live check failed (or could not
|
|
431
|
+
* be made), by canister name. Only a `buildStart` checks: a save under
|
|
432
|
+
* `vite dev` makes no request, so it cannot tell that the file matches the
|
|
433
|
+
* canister again, and it leaves the canister failed, in the log and the
|
|
434
|
+
* overlay, and not generated, whatever the save changed.
|
|
435
|
+
*
|
|
436
|
+
* A new lifetime starts with a copy of the failures of the one before it.
|
|
437
|
+
* Its `buildStart` (a restart's, which keeps the plugin object for an inline
|
|
438
|
+
* config) clears one when it checks the canister and the check passes, when
|
|
439
|
+
* it fetched the file, or when the check is off for it
|
|
440
|
+
* (`IC_REACTOR_VERIFY=0`); a canister it fails before its check keeps the
|
|
441
|
+
* failure. A pass from before the failure cannot clear it: the failure
|
|
442
|
+
* dropped it, and a new lifetime starts with none. What the old lifetime
|
|
443
|
+
* finds after the copy is its own.
|
|
444
|
+
*
|
|
445
|
+
* Only a check of the text the run read counts, failed or passed: one
|
|
446
|
+
* whose file was written before the check was done (even saved back) may
|
|
447
|
+
* have read another text, so it neither sets nor clears a failure here (see
|
|
448
|
+
* `verifyLive`'s `unsure`). A check that read the run's text keeps its
|
|
449
|
+
* verdict when the file is written after it, as for a save made just after
|
|
450
|
+
* the run.
|
|
451
|
+
*/
|
|
452
|
+
liveFailures: Map<string, Failure>
|
|
453
|
+
/**
|
|
454
|
+
* The failures not yet fixed, by canister name, which this dev server shows
|
|
455
|
+
* in its overlay and hands each browser that connects. A new lifetime starts
|
|
456
|
+
* with a copy of the one before it: a restart leaves the overlay as it was
|
|
457
|
+
* until one of its own runs reports a canister, as a run that took nothing
|
|
458
|
+
* of a file written while it read it does not (see `writtenSince`).
|
|
459
|
+
*/
|
|
460
|
+
unfixed: Map<string, Failure>
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** A line to log, held until it is known to be of the text a run read. */
|
|
464
|
+
interface Note {
|
|
465
|
+
level: "info" | "warn"
|
|
466
|
+
message: string
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** A `.did` as one read found it: its text, and a stamp of the write that made it. */
|
|
470
|
+
interface Snapshot {
|
|
471
|
+
text: string
|
|
472
|
+
stamp: string
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* One run's reading of the `didFile`s it works on. A run is a `buildStart`,
|
|
477
|
+
* from the end of its fetch, or the regeneration a save starts. It reads each
|
|
478
|
+
* file once, when its steps start, and takes the file's text from that
|
|
479
|
+
* reading wherever it compares or records one: the methods it listed, the text
|
|
480
|
+
* the live check passed, the text it generated. The steps that run
|
|
481
|
+
* `meant` (the methods listing, the live check, generation) read the
|
|
482
|
+
* file themselves, later, so what one of them found is of the run's text only
|
|
483
|
+
* while the file is still as the run read it.
|
|
484
|
+
*/
|
|
485
|
+
interface Run {
|
|
486
|
+
/** The text the run read, `undefined` when the file was missing. */
|
|
487
|
+
textOf(canister: Generated): string | undefined
|
|
488
|
+
/**
|
|
489
|
+
* Whether the file is still as the run read it: the same text and the same
|
|
490
|
+
* stamp, or still missing.
|
|
491
|
+
*/
|
|
492
|
+
asRead(canister: Generated): boolean
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
type PluginLog = Pick<Logger, "info" | "warn" | "error">
|
|
496
|
+
|
|
497
|
+
/** What the hooks log through before Vite hands over its logger. */
|
|
498
|
+
const consoleLog: PluginLog = {
|
|
499
|
+
info: (message) => console.log(message),
|
|
500
|
+
warn: (message) => console.warn(message),
|
|
501
|
+
error: (message) => console.error(message),
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The Vite plugin for an app built on a module Meant generates.
|
|
506
|
+
*
|
|
507
|
+
* It does three things:
|
|
508
|
+
*
|
|
509
|
+
* - **Fetches a missing `.did`.** A canister with a `canisterId` whose
|
|
510
|
+
* `didFile` is not on disk has it written from the live canister's
|
|
511
|
+
* certified `candid:service` first. A `didFile` on disk costs no network
|
|
512
|
+
* request. See {@link IcReactorPluginOptions.canisters}.
|
|
513
|
+
* - **Generates the module.** When a build or the dev server starts, and
|
|
514
|
+
* when a configured `.did` changes, it runs the app's `meant gen`
|
|
515
|
+
* on each `didFile` and leaves the module as the generator wrote it: no
|
|
516
|
+
* wrapper files, hooks or reactors. The generator is WebAssembly, so it runs
|
|
517
|
+
* in a child process (the running Node binary on the CLI's bin script, never
|
|
518
|
+
* through a shell, killed after 60 seconds). A trap, a crash or a hang on a
|
|
519
|
+
* bad `.did` then ends that process and not the dev server: under
|
|
520
|
+
* `vite build` it fails the build with the CLI's own message, under Vitest
|
|
521
|
+
* the run's startup, and under `vite dev` it is logged and shown in the
|
|
522
|
+
* error overlay while the server keeps serving. See
|
|
523
|
+
* {@link IcReactorPluginOptions.failOnError}.
|
|
524
|
+
* - **Injects the local IC environment.** Under `vite dev` and `vite preview`
|
|
525
|
+
* it sets the `ic_env` cookie, which carries the replica's root key and the
|
|
526
|
+
* canister IDs, and proxies `/api` to the local replica, so the app needs no
|
|
527
|
+
* configuration to find them. It asks the `icp` CLI for both, and is off in
|
|
528
|
+
* mode `"test"` (Vitest's), where `icp` is never run.
|
|
529
|
+
*
|
|
530
|
+
* The first thing it logs names where an agent reads how to use the library:
|
|
531
|
+
* `ic-reactor: agent guide at node_modules/@ic-reactor/core/llms.txt`.
|
|
532
|
+
*
|
|
533
|
+
* The plugin needs `@meantjs/cli`, at the exact release that pairs with
|
|
534
|
+
* the `@meantjs/schema` the generated modules import, installed in the
|
|
535
|
+
* app. It imports neither, and no `@ic-reactor` runtime package. To fetch a
|
|
536
|
+
* missing `didFile` from a live canister it imports `@icp-sdk/core`, an
|
|
537
|
+
* optional peer that every app on `@ic-reactor/core` has installed; an app
|
|
538
|
+
* whose `.did` files are all on disk never loads it.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```ts
|
|
542
|
+
* // vite.config.ts
|
|
543
|
+
* export default defineConfig({
|
|
544
|
+
* plugins: [
|
|
545
|
+
* icReactor({
|
|
546
|
+
* canisters: {
|
|
547
|
+
* backend: { didFile: "../backend/backend.did" },
|
|
548
|
+
* // Written from mainnet's ledger when missing; commit it.
|
|
549
|
+
* ledger: {
|
|
550
|
+
* didFile: "did/icp_ledger.did",
|
|
551
|
+
* canisterId: "ryjl3-tyaaa-aaaaa-aaaba-cai",
|
|
552
|
+
* },
|
|
553
|
+
* },
|
|
554
|
+
* }),
|
|
555
|
+
* ],
|
|
556
|
+
* })
|
|
557
|
+
* ```
|
|
558
|
+
*/
|
|
559
|
+
export function icReactor(options: IcReactorPluginOptions = {}): Plugin {
|
|
560
|
+
// Every option read once, and every value and key checked, before anything
|
|
561
|
+
// else (see options.ts). An option of the whole plugin that is not one, or
|
|
562
|
+
// a key of the options that is no option, throws here, so that the config
|
|
563
|
+
// does not load.
|
|
564
|
+
const {
|
|
565
|
+
injectEnvironment,
|
|
566
|
+
failOnError,
|
|
567
|
+
onGenerated,
|
|
568
|
+
names,
|
|
569
|
+
uncarriedNames,
|
|
570
|
+
configuredCanisterIds,
|
|
571
|
+
notCanisterIds,
|
|
572
|
+
generated,
|
|
573
|
+
refused,
|
|
574
|
+
} = readOptions(options)
|
|
575
|
+
|
|
576
|
+
/** Each refused canister's failure, which every run reports first. */
|
|
577
|
+
const refusals: Failure[] = refused.map((canister) => ({
|
|
578
|
+
canister,
|
|
579
|
+
message: canister.problem,
|
|
580
|
+
}))
|
|
581
|
+
|
|
582
|
+
/** Every canister with a `didFile`, refused or not: what a run reports on. */
|
|
583
|
+
const configured: Array<Generated | Refused> = [...generated, ...refused]
|
|
584
|
+
|
|
585
|
+
// Vite resolves relative project paths against the resolved `config.root`,
|
|
586
|
+
// which only equals the process cwd when vite happens to be started from the
|
|
587
|
+
// project directory, not for `root: "frontend"` and not when the root is
|
|
588
|
+
// passed positionally (`vite build apps/web`). `configResolved` overwrites
|
|
589
|
+
// this before any hook that resolves a path runs; the cwd is only Vite's own
|
|
590
|
+
// default root. A restart of an inline config resolves the same root.
|
|
591
|
+
let projectRoot = process.cwd()
|
|
592
|
+
|
|
593
|
+
let log: PluginLog = consoleLog
|
|
594
|
+
let announced = false
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* The local IC environment `vite dev` and `vite preview` inject. The
|
|
598
|
+
* `config` hook creates it when `injectEnvironment` is on, for the server
|
|
599
|
+
* whose config it resolves.
|
|
600
|
+
*/
|
|
601
|
+
let localEnvironment: LocalEnvironment | undefined
|
|
602
|
+
|
|
603
|
+
// ── Lifetimes ───────────────────────────────────────────────────────────
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* A new lifetime (see {@link Lifetime}), after `previous`: its stopper, its
|
|
607
|
+
* queue, the fetched writes its watcher is to skip and its passes its own,
|
|
608
|
+
* and copies of the failed live checks and the overlay of the one before,
|
|
609
|
+
* which only its own runs change.
|
|
610
|
+
*/
|
|
611
|
+
const lifetimeAfter = (
|
|
612
|
+
previous: Lifetime | undefined,
|
|
613
|
+
failsByDefault: boolean
|
|
614
|
+
): Lifetime => ({
|
|
615
|
+
failsByDefault,
|
|
616
|
+
server: null,
|
|
617
|
+
stopper: new AbortController(),
|
|
618
|
+
queued: new Set(),
|
|
619
|
+
justFetched: new Map(),
|
|
620
|
+
verified: new Map(),
|
|
621
|
+
liveFailures: new Map(previous?.liveFailures),
|
|
622
|
+
unfixed: new Map(previous?.unfixed),
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* The newest lifetime: the one `configResolved` made last. A hook that runs
|
|
627
|
+
* before any `configResolved` runs in one of its own, as a build, the safer
|
|
628
|
+
* default: `vite build` and `vite dev` want opposite failure behaviour.
|
|
629
|
+
*/
|
|
630
|
+
let current = lifetimeAfter(undefined, true)
|
|
631
|
+
|
|
632
|
+
/** The lifetime of each config `configResolved` was handed. */
|
|
633
|
+
const byConfig = new WeakMap<object, Lifetime>()
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* The lifetimes that started (`configureServer` or `buildStart`) and have
|
|
637
|
+
* not closed, oldest first.
|
|
638
|
+
*/
|
|
639
|
+
const open = new Set<Lifetime>()
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The lifetime a hook runs in, when its context names one: Vite 6 and
|
|
643
|
+
* later hand each hook the environment it runs for, and the environment
|
|
644
|
+
* its config, which is the one `configResolved` was handed. Vite 4 and 5
|
|
645
|
+
* have no environments. Never throws.
|
|
646
|
+
*/
|
|
647
|
+
const lifetimeOf = (context: unknown): Lifetime | undefined => {
|
|
648
|
+
try {
|
|
649
|
+
const environment = (context as { environment?: unknown } | undefined)
|
|
650
|
+
?.environment as { getTopLevelConfig?: () => unknown } | undefined
|
|
651
|
+
const config = environment?.getTopLevelConfig?.()
|
|
652
|
+
return typeof config === "object" && config !== null
|
|
653
|
+
? byConfig.get(config)
|
|
654
|
+
: undefined
|
|
655
|
+
} catch {
|
|
656
|
+
return undefined
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ── Generation ──────────────────────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
const didPath = (canister: { didFile: string }) =>
|
|
663
|
+
path.resolve(projectRoot, canister.didFile)
|
|
664
|
+
const relativeToRoot = (file: string) =>
|
|
665
|
+
describePath(path.relative(projectRoot, file)) || "."
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* The file a path names, as a key: equal for every spelling of one file,
|
|
669
|
+
* through a symlinked directory or, where the filesystem ignores case, in
|
|
670
|
+
* another case. See file-identity.ts. Fetches, the files fetched and the
|
|
671
|
+
* watcher's events are all matched by it, never by the path as written.
|
|
672
|
+
*/
|
|
673
|
+
const foldsCaseOnce = cachedFoldsCase(foldsCase)
|
|
674
|
+
const identify = (file: string) => fileIdentity(file, foldsCaseOnce)
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* The `.did` text each canister last generated from. A rebuild that finds
|
|
678
|
+
* the text unchanged, and the module and envelope still there, skips the
|
|
679
|
+
* canister, which is every rebuild of `vite build --watch` that was not
|
|
680
|
+
* caused by a `.did`, and the second `buildStart` Vite 6 and later run for
|
|
681
|
+
* another environment. It is the plugin's, not a lifetime's: it says what
|
|
682
|
+
* is on disk, which a restart keeps.
|
|
683
|
+
*/
|
|
684
|
+
const generatedFrom = new Map<string, string>()
|
|
685
|
+
|
|
686
|
+
// Generation runs one job after another, whichever lifetime asked for it.
|
|
687
|
+
// Two `buildStart`s (one per Vite 6+ environment), a save during a run, or
|
|
688
|
+
// the runs of a restarted server and of the old one before it closes,
|
|
689
|
+
// would otherwise start two generator processes that write the same files.
|
|
690
|
+
let tail: Promise<unknown> = Promise.resolve()
|
|
691
|
+
const serially = <T>(job: () => Promise<T>): Promise<T> => {
|
|
692
|
+
const result = tail.then(job)
|
|
693
|
+
tail = result.catch(() => undefined)
|
|
694
|
+
return result
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The `.did` of `canister` as it is now: its text, and a stamp of the write
|
|
699
|
+
* that made it, or `undefined` when it cannot be read. Two snapshots with
|
|
700
|
+
* one text and one stamp are of a file nobody wrote between them: the text
|
|
701
|
+
* alone cannot tell a file saved and saved back.
|
|
702
|
+
*/
|
|
703
|
+
const snapshot = (canister: Generated): Snapshot | undefined => {
|
|
704
|
+
try {
|
|
705
|
+
const file = didPath(canister)
|
|
706
|
+
const { mtimeMs, ctimeMs, size, ino } = fs.statSync(file)
|
|
707
|
+
return {
|
|
708
|
+
stamp: `${mtimeMs}:${ctimeMs}:${size}:${ino}`,
|
|
709
|
+
text: fs.readFileSync(file, "utf-8"),
|
|
710
|
+
}
|
|
711
|
+
} catch {
|
|
712
|
+
return undefined
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Read the `didFile` of each of `canisters` once, for one run. See
|
|
718
|
+
* {@link Run}.
|
|
719
|
+
*/
|
|
720
|
+
const startRun = (canisters: Generated[]): Run => {
|
|
721
|
+
const keys = new Map<Generated, string>()
|
|
722
|
+
const read = new Map<string, Snapshot | undefined>()
|
|
723
|
+
for (const canister of canisters) {
|
|
724
|
+
const key = identify(didPath(canister))
|
|
725
|
+
keys.set(canister, key)
|
|
726
|
+
if (!read.has(key)) read.set(key, snapshot(canister))
|
|
727
|
+
}
|
|
728
|
+
const first = (canister: Generated) =>
|
|
729
|
+
read.get(keys.get(canister) ?? identify(didPath(canister)))
|
|
730
|
+
return {
|
|
731
|
+
textOf: (canister) => first(canister)?.text,
|
|
732
|
+
asRead: (canister) => {
|
|
733
|
+
const was = first(canister)
|
|
734
|
+
const now = snapshot(canister)
|
|
735
|
+
return was === undefined || now === undefined
|
|
736
|
+
? was === now
|
|
737
|
+
: now.text === was.text && now.stamp === was.stamp
|
|
738
|
+
},
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* The canisters of `canisters` whose `didFile` was written since `run`
|
|
744
|
+
* read it: what the run's steps compared, checked and generated of it may
|
|
745
|
+
* not be one text, so the run takes none of it. It reports neither the
|
|
746
|
+
* failures its steps found for such a canister nor a success: a build fails
|
|
747
|
+
* it as changed during the build, and a dev server queues it again. Asked
|
|
748
|
+
* when the run is done with the files, after generation, the last step
|
|
749
|
+
* that reads them.
|
|
750
|
+
*/
|
|
751
|
+
const writtenSince = (run: Run, canisters: Generated[]): Generated[] =>
|
|
752
|
+
canisters.filter((canister) => !run.asRead(canister))
|
|
753
|
+
|
|
754
|
+
/** `canister` as the generator is given it: its paths resolved against the root. */
|
|
755
|
+
const generationOf = (canister: Generated): GenerateCanister => ({
|
|
756
|
+
name: canister.name,
|
|
757
|
+
didFile: didPath(canister),
|
|
758
|
+
outDir: path.resolve(projectRoot, canister.outDir),
|
|
759
|
+
})
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Each canister whose module a canister before it in the options writes,
|
|
763
|
+
* with why it cannot be generated. The generator names a module after its
|
|
764
|
+
* `.did`, so `a/ping.did` and `b/ping.did` in one `outDir` write one
|
|
765
|
+
* `ping.ts`, and so do `Ping.did` and `ping.did` where the filesystem of the
|
|
766
|
+
* `outDir` ignores case: the first canister keeps the module.
|
|
767
|
+
*
|
|
768
|
+
* Asked at the start of each run, of every canister with valid options, so
|
|
769
|
+
* that the answer is the same whichever canisters the run generates: it
|
|
770
|
+
* depends on the Vite root and on the filesystem, so it cannot be told when
|
|
771
|
+
* the plugin is made. Such a canister fails as a refused one does, before
|
|
772
|
+
* the run fetches, writes, runs or requests anything for it, and a save of
|
|
773
|
+
* its file generates nothing for it (see `runRefusals`). `generate` refuses
|
|
774
|
+
* one among the canisters of one run too, which is the check of last
|
|
775
|
+
* resort. Never throws.
|
|
776
|
+
*/
|
|
777
|
+
const collisions = (): Map<Generated, string> => {
|
|
778
|
+
const byName = moduleCollisions(generated.map(generationOf), projectRoot)
|
|
779
|
+
return new Map(
|
|
780
|
+
generated.flatMap((canister) => {
|
|
781
|
+
const failure = byName.get(canister.name)
|
|
782
|
+
return failure === undefined ? [] : [[canister, failure] as const]
|
|
783
|
+
})
|
|
784
|
+
)
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* The files `gen` writes for `canister`: its module and its envelope, named
|
|
789
|
+
* by the one rule generation names them by (`outputsOf` of generate.ts).
|
|
790
|
+
*/
|
|
791
|
+
const outputFiles = (canister: Generated): string[] => {
|
|
792
|
+
const { module, envelope } = outputsOf(generationOf(canister))
|
|
793
|
+
return [module, envelope]
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const readBytes = (file: string): Buffer | undefined => {
|
|
797
|
+
try {
|
|
798
|
+
return fs.readFileSync(file)
|
|
799
|
+
} catch {
|
|
800
|
+
return undefined
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Put `files` back as `before` holds them: rewritten, or removed where
|
|
806
|
+
* there was none. A file that holds what it held is not touched.
|
|
807
|
+
*/
|
|
808
|
+
const putBack = (
|
|
809
|
+
before: Map<string, Buffer | undefined>,
|
|
810
|
+
files: string[]
|
|
811
|
+
): void => {
|
|
812
|
+
for (const file of files) {
|
|
813
|
+
const was = before.get(file)
|
|
814
|
+
const now = readBytes(file)
|
|
815
|
+
if (was === undefined ? now === undefined : now?.equals(was)) continue
|
|
816
|
+
if (was === undefined) fs.rmSync(file, { force: true })
|
|
817
|
+
else fs.writeFileSync(file, was)
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Generate `wanted`, in one generator process for each output directory, and
|
|
823
|
+
* report what the generator said. Never rejects: an unexpected error fails
|
|
824
|
+
* the canisters, since a rejection from a watcher callback could end the dev
|
|
825
|
+
* server.
|
|
826
|
+
*
|
|
827
|
+
* Resolves `undefined` when `signal` was aborted before it finished: the
|
|
828
|
+
* server it belonged to is gone, so nothing is recorded, logged or shown.
|
|
829
|
+
*
|
|
830
|
+
* Only a canister whose file is still as `run` read it is generated, and its
|
|
831
|
+
* module is taken only if the file is still so once the generator read it.
|
|
832
|
+
* One written while the generator ran is neither recorded nor reported
|
|
833
|
+
* here, and its module and envelope are put back as they were: the run
|
|
834
|
+
* takes nothing of it (see `writtenSince`).
|
|
835
|
+
*
|
|
836
|
+
* @param force - Generate even a canister whose `.did` text is unchanged.
|
|
837
|
+
* @param signal - The signal of the run's lifetime when the run was asked
|
|
838
|
+
* for: its server's close aborts it.
|
|
839
|
+
*/
|
|
840
|
+
const generateNow = async (
|
|
841
|
+
wanted: Generated[],
|
|
842
|
+
force: boolean,
|
|
843
|
+
run: Run,
|
|
844
|
+
signal: AbortSignal
|
|
845
|
+
): Promise<Failure[] | undefined> => {
|
|
846
|
+
if (signal.aborted) return undefined
|
|
847
|
+
try {
|
|
848
|
+
const ready = wanted.filter((canister) => run.asRead(canister))
|
|
849
|
+
const stale = ready.filter((canister) => {
|
|
850
|
+
const source = run.textOf(canister)
|
|
851
|
+
return (
|
|
852
|
+
force ||
|
|
853
|
+
source === undefined ||
|
|
854
|
+
generatedFrom.get(canister.name) !== source ||
|
|
855
|
+
// Deleted since it was generated: the text alone would skip it,
|
|
856
|
+
// and every rebuild would fail until the `.did` changed (#980).
|
|
857
|
+
outputFiles(canister).some((file) => !fs.existsSync(file))
|
|
858
|
+
)
|
|
859
|
+
})
|
|
860
|
+
if (stale.length === 0) return []
|
|
861
|
+
|
|
862
|
+
let cli: string
|
|
863
|
+
try {
|
|
864
|
+
cli = resolveCliBin(projectRoot)
|
|
865
|
+
} catch (error) {
|
|
866
|
+
return stale.map((canister) => ({
|
|
867
|
+
canister,
|
|
868
|
+
message: describeError(error),
|
|
869
|
+
}))
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Each module and envelope as it is before the generator writes it.
|
|
873
|
+
const before = new Map<string, Buffer | undefined>()
|
|
874
|
+
for (const canister of stale) {
|
|
875
|
+
for (const file of outputFiles(canister)) {
|
|
876
|
+
if (!before.has(file)) before.set(file, readBytes(file))
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const result = await generate({
|
|
881
|
+
cli,
|
|
882
|
+
root: projectRoot,
|
|
883
|
+
timeoutMs: GENERATE_TIMEOUT_MS,
|
|
884
|
+
signal,
|
|
885
|
+
// Shown as it arrives: a run that takes long, or is killed at the
|
|
886
|
+
// timeout, would otherwise say nothing until it is over.
|
|
887
|
+
onStderr: (line, didFiles) => {
|
|
888
|
+
if (signal.aborted) return
|
|
889
|
+
log.warn(
|
|
890
|
+
`ic-reactor: \`meant\` (${didFiles.map((file) => relativeToRoot(file)).join(", ")}): ${line}`
|
|
891
|
+
)
|
|
892
|
+
},
|
|
893
|
+
canisters: stale.map(generationOf),
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
if (signal.aborted) return undefined
|
|
897
|
+
|
|
898
|
+
const failures: Failure[] = []
|
|
899
|
+
for (const outcome of result.outcomes) {
|
|
900
|
+
// The names of one outcome share a `.did`, so they succeed or fail
|
|
901
|
+
// together and are reported once.
|
|
902
|
+
const members = stale.filter(({ name }) => outcome.names.includes(name))
|
|
903
|
+
// They share one file: written while the generator ran, it may have
|
|
904
|
+
// been read as another text than the run's.
|
|
905
|
+
if (members.length > 0 && !run.asRead(members[0])) {
|
|
906
|
+
putBack(before, outputFiles(members[0]))
|
|
907
|
+
continue
|
|
908
|
+
}
|
|
909
|
+
const names = members.map(({ name }) => name).join(", ")
|
|
910
|
+
if (outcome.status === "failed") {
|
|
911
|
+
for (const canister of members) {
|
|
912
|
+
generatedFrom.delete(canister.name)
|
|
913
|
+
failures.push({ canister, message: outcome.failure ?? "" })
|
|
914
|
+
}
|
|
915
|
+
continue
|
|
916
|
+
}
|
|
917
|
+
for (const canister of members) {
|
|
918
|
+
const source = run.textOf(canister)
|
|
919
|
+
if (source !== undefined) generatedFrom.set(canister.name, source)
|
|
920
|
+
}
|
|
921
|
+
if (outcome.status === "written" && outcome.module) {
|
|
922
|
+
log.info(
|
|
923
|
+
`ic-reactor: generated ${names} into ${relativeToRoot(outcome.module)}`
|
|
924
|
+
)
|
|
925
|
+
}
|
|
926
|
+
for (const { kind, name: what, reason, via } of outcome.omitted) {
|
|
927
|
+
log.warn(
|
|
928
|
+
`ic-reactor: ${names}: omitted ${kind} ${what} (${reason}${via ? ` via ${via}` : ""})`
|
|
929
|
+
)
|
|
930
|
+
}
|
|
931
|
+
if (onGenerated === undefined || members.length === 0) continue
|
|
932
|
+
// Inside the run, and so inside the queue of runs: the next run waits
|
|
933
|
+
// for it, and so does a build's `buildStart`.
|
|
934
|
+
const { module, envelope } = outputsOf(generationOf(members[0]))
|
|
935
|
+
// A close abandons a hook still running, as it kills a generator: the
|
|
936
|
+
// run waits for the hook or for the close, whichever comes first, so a
|
|
937
|
+
// hook that never settles cannot hold the queue past its server.
|
|
938
|
+
let stopWaiting = () => {}
|
|
939
|
+
const closed = new Promise<void>((resolve) => {
|
|
940
|
+
stopWaiting = () => signal.removeEventListener("abort", onAbort)
|
|
941
|
+
const onAbort = () => resolve()
|
|
942
|
+
signal.addEventListener("abort", onAbort, { once: true })
|
|
943
|
+
})
|
|
944
|
+
try {
|
|
945
|
+
const hook = Promise.resolve(
|
|
946
|
+
onGenerated({
|
|
947
|
+
names: members.map(({ name }) => name),
|
|
948
|
+
didFile: didPath(members[0]),
|
|
949
|
+
module,
|
|
950
|
+
envelope,
|
|
951
|
+
status: outcome.status,
|
|
952
|
+
signal,
|
|
953
|
+
})
|
|
954
|
+
)
|
|
955
|
+
// Settled after an abandoned run, it is reported nowhere.
|
|
956
|
+
hook.catch(() => undefined)
|
|
957
|
+
await Promise.race([hook, closed])
|
|
958
|
+
} catch (error) {
|
|
959
|
+
// Not recorded as generated, so the next run generates the
|
|
960
|
+
// canisters again and calls the hook again.
|
|
961
|
+
for (const canister of members) {
|
|
962
|
+
generatedFrom.delete(canister.name)
|
|
963
|
+
failures.push({
|
|
964
|
+
canister,
|
|
965
|
+
message: `onGenerated failed for ${relativeToRoot(module)}: ${describeError(error)}`,
|
|
966
|
+
})
|
|
967
|
+
}
|
|
968
|
+
} finally {
|
|
969
|
+
stopWaiting()
|
|
970
|
+
}
|
|
971
|
+
if (signal.aborted) {
|
|
972
|
+
// The hook may not have finished: the next run, a restarted
|
|
973
|
+
// server's included, generates the canisters again and calls it.
|
|
974
|
+
for (const canister of members) generatedFrom.delete(canister.name)
|
|
975
|
+
return undefined
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return failures
|
|
979
|
+
} catch (error) {
|
|
980
|
+
if (signal.aborted) return undefined
|
|
981
|
+
return wanted.map((canister) => ({
|
|
982
|
+
canister,
|
|
983
|
+
message: describeError(error),
|
|
984
|
+
}))
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* The `.did` files `IC_REACTOR_FETCH` has had fetched again, by
|
|
990
|
+
* `identify`. A request is served once in the life of the plugin: the second
|
|
991
|
+
* `buildStart` of Vite 6 and later, and each rebuild of
|
|
992
|
+
* `vite build --watch`, read the file it wrote. A file is recorded only once
|
|
993
|
+
* its fetch succeeded and the file on disk holds what was fetched, so a
|
|
994
|
+
* fetch or a write that failed is tried again on the next `buildStart`.
|
|
995
|
+
*/
|
|
996
|
+
const refetched = new Set<string>()
|
|
997
|
+
|
|
998
|
+
/** Whether `canister` gave a `canisterId`, of any value. */
|
|
999
|
+
const givesCanisterId = (canister: Generated | Refused): boolean =>
|
|
1000
|
+
"problem" in canister
|
|
1001
|
+
? canister.givesCanisterId
|
|
1002
|
+
: canister.canisterId !== undefined
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Why the canisters that name one file (`members`) cannot have it fetched,
|
|
1006
|
+
* or `undefined` when they can. Asked only when the file is to be fetched,
|
|
1007
|
+
* and before any request: a file on disk that nobody asked to fetch again
|
|
1008
|
+
* is read by them all, whatever their `canisterId`s and `methods` (two
|
|
1009
|
+
* ICRC-1 ledgers on one `icrc1.did`), and `compareMethods` compares it with
|
|
1010
|
+
* the list of each.
|
|
1011
|
+
*
|
|
1012
|
+
* In this order: a member cannot be run (`blocked`: its options hold a
|
|
1013
|
+
* value that is not one, or a variable asks of it what it cannot do), so
|
|
1014
|
+
* which canister or which list the file is for cannot be told either; the
|
|
1015
|
+
* members that name a `canisterId` name different canisters, or one
|
|
1016
|
+
* canister on different networks, so whose interface the file is cannot be
|
|
1017
|
+
* told; or the members name different projections (no `methods` is the
|
|
1018
|
+
* whole interface), so which one the file holds cannot be told. Every
|
|
1019
|
+
* member generates from the one file, so a member with no `canisterId`
|
|
1020
|
+
* counts as much as the one it is fetched for.
|
|
1021
|
+
*/
|
|
1022
|
+
const conflictOf = (
|
|
1023
|
+
members: Array<Generated | Refused>,
|
|
1024
|
+
blocked: Map<Generated | Refused, string>
|
|
1025
|
+
): string | undefined => {
|
|
1026
|
+
const spellings = [
|
|
1027
|
+
...new Set(
|
|
1028
|
+
members.map((canister) =>
|
|
1029
|
+
relativeToRoot(didPath(canister as Generated))
|
|
1030
|
+
)
|
|
1031
|
+
),
|
|
1032
|
+
]
|
|
1033
|
+
const file =
|
|
1034
|
+
spellings.length === 1
|
|
1035
|
+
? spellings[0]
|
|
1036
|
+
: `${spellings.join(" and ")}, which are one file,`
|
|
1037
|
+
const names = quoteNames(members.map(({ name }) => name))
|
|
1038
|
+
|
|
1039
|
+
// Refused before any request, as the member is.
|
|
1040
|
+
const reasons = members.flatMap((canister) => {
|
|
1041
|
+
const reason = blocked.get(canister)
|
|
1042
|
+
return reason === undefined ? [] : [{ canister, reason }]
|
|
1043
|
+
})
|
|
1044
|
+
if (reasons.length > 0) {
|
|
1045
|
+
if (members.length === 1) return reasons[0].reason
|
|
1046
|
+
return (
|
|
1047
|
+
`${names} name ${file}, which is not fetched, since ` +
|
|
1048
|
+
`${quoteNames(reasons.map(({ canister }) => canister.name))} cannot be generated ` +
|
|
1049
|
+
`(${reasons.map(({ canister, reason }) => `${canister.name}: ${reason}`).join("; ")}). ` +
|
|
1050
|
+
`Correct ${reasons.length === 1 ? "it" : "them"} and build again.`
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
1053
|
+
// Every member's options are values: refused ones are blocked.
|
|
1054
|
+
const usable = members as Generated[]
|
|
1055
|
+
const fetchable = usable.filter(
|
|
1056
|
+
({ canisterId }) => canisterId !== undefined
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
const sourceOf = ({ canisterId, network }: Generated) => {
|
|
1060
|
+
// Every network given was checked when the plugin was made.
|
|
1061
|
+
const resolved = resolveNetwork(network)
|
|
1062
|
+
return typeof resolved === "string"
|
|
1063
|
+
? { key: `${canisterId}\0${resolved}`, label: resolved }
|
|
1064
|
+
: {
|
|
1065
|
+
key: `${canisterId}\0${resolved.host}\0${resolved.fetchRootKey}`,
|
|
1066
|
+
label: resolved.label,
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const sources = new Set(fetchable.map((canister) => sourceOf(canister).key))
|
|
1070
|
+
if (sources.size > 1) {
|
|
1071
|
+
const fetches = fetchable
|
|
1072
|
+
.map(
|
|
1073
|
+
(canister) =>
|
|
1074
|
+
`${canister.name}: ${canister.canisterId} on ${sourceOf(canister).label}`
|
|
1075
|
+
)
|
|
1076
|
+
.join("; ")
|
|
1077
|
+
return (
|
|
1078
|
+
`${names} name ${file} but not one canister to fetch it from ` +
|
|
1079
|
+
`(${fetches}): which one's interface the file holds cannot be told, so it is not fetched. ` +
|
|
1080
|
+
`Give each canister a didFile of its own, or write ${spellings[0]} yourself and the plugin reads it instead of fetching.`
|
|
1081
|
+
)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// One file holds one projection, read by every member: the lists of the
|
|
1085
|
+
// members with no canisterId count too.
|
|
1086
|
+
const projectionOf = ({ methods }: Generated) =>
|
|
1087
|
+
methods !== undefined
|
|
1088
|
+
? {
|
|
1089
|
+
key: JSON.stringify(methodList(methods)),
|
|
1090
|
+
label: listNames(methodList(methods)),
|
|
1091
|
+
}
|
|
1092
|
+
: { key: "", label: "no methods, the whole interface" }
|
|
1093
|
+
const projections = new Set(
|
|
1094
|
+
usable.map((canister) => projectionOf(canister).key)
|
|
1095
|
+
)
|
|
1096
|
+
if (projections.size < 2) return undefined
|
|
1097
|
+
const lists = usable
|
|
1098
|
+
.map((canister) => `${canister.name}: ${projectionOf(canister).label}`)
|
|
1099
|
+
.join("; ")
|
|
1100
|
+
return (
|
|
1101
|
+
`${names} name ${file} but not one list of methods to project it to (${lists}): ` +
|
|
1102
|
+
`the file holds one projection, which every one of them generates from, so it is not fetched. ` +
|
|
1103
|
+
`Give them one list, or give each list a didFile of its own.`
|
|
1104
|
+
)
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/** `failures` with each canister once, for the first reason it failed. */
|
|
1108
|
+
const dedupe = (failures: Failure[]): Failure[] =>
|
|
1109
|
+
failures.filter(
|
|
1110
|
+
(failure, index) =>
|
|
1111
|
+
failures.findIndex(({ canister }) => canister === failure.canister) ===
|
|
1112
|
+
index
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
/**
|
|
1116
|
+
* Write each `didFile` that a canister names a `canisterId` for and that is
|
|
1117
|
+
* not on disk, from the live canister's certified `candid:service`, and
|
|
1118
|
+
* each one `IC_REACTOR_FETCH` names (`fetches`). Canisters that name one
|
|
1119
|
+
* `.did`, under any spelling of it, share one fetch when they name one
|
|
1120
|
+
* canister on one network and one list of `methods`, and fail with no
|
|
1121
|
+
* request when they name more, or when one of them is `blocked` (see
|
|
1122
|
+
* `conflictOf`). A `didFile` on disk that nobody asked to fetch again costs
|
|
1123
|
+
* no network request, and `@icp-sdk/core` is not even loaded.
|
|
1124
|
+
*
|
|
1125
|
+
* A fetch that failed is not remembered: the next `buildStart` (a rebuild
|
|
1126
|
+
* of `vite build --watch`, or another Vite environment's) tries it again,
|
|
1127
|
+
* so a network that comes back is used without restarting Vite.
|
|
1128
|
+
*
|
|
1129
|
+
* A canister with `methods` has the projection of the live interface to
|
|
1130
|
+
* them written instead of the whole interface. A name of `methods` that the
|
|
1131
|
+
* interface lacks fails its canisters, and nothing is written.
|
|
1132
|
+
*
|
|
1133
|
+
* A file written under a dev server is recorded in the `lifetime`'s
|
|
1134
|
+
* `justFetched`, so that its watcher's event for the write starts no second
|
|
1135
|
+
* run: the run that fetched the file generates it.
|
|
1136
|
+
*
|
|
1137
|
+
* Resolves with the canisters that cannot be generated, since their fetch
|
|
1138
|
+
* failed or was refused, and the files (by `identify`) this run wrote or
|
|
1139
|
+
* found matching the canister, each with the text it fetched, or
|
|
1140
|
+
* `undefined` when `signal` was aborted first. Never rejects.
|
|
1141
|
+
*/
|
|
1142
|
+
const fetchMissing = async (
|
|
1143
|
+
lifetime: Lifetime,
|
|
1144
|
+
blocked: Map<Generated | Refused, string>,
|
|
1145
|
+
fetches: Requests["fetches"],
|
|
1146
|
+
signal: AbortSignal
|
|
1147
|
+
): Promise<
|
|
1148
|
+
{ failures: Failure[]; fetched: Map<string, string> } | undefined
|
|
1149
|
+
> => {
|
|
1150
|
+
if (signal.aborted) return undefined
|
|
1151
|
+
const failures: Failure[] = []
|
|
1152
|
+
const fetched = new Map<string, string>()
|
|
1153
|
+
try {
|
|
1154
|
+
// By the file, not by its spelling: two spellings of one file (through
|
|
1155
|
+
// a symlinked directory, or in another case where the filesystem
|
|
1156
|
+
// ignores case) are one fetch and one write. Fetched once for each
|
|
1157
|
+
// spelling, the last write would win, and every canister would generate
|
|
1158
|
+
// from the interface of whichever canister was fetched last. A refused
|
|
1159
|
+
// canister that names a path counts: it shares the file.
|
|
1160
|
+
const byFile = new Map<string, Array<Generated | Refused>>()
|
|
1161
|
+
for (const canister of configured) {
|
|
1162
|
+
if (canister.didFile === undefined) continue
|
|
1163
|
+
const key = identify(didPath(canister as Generated))
|
|
1164
|
+
byFile.set(key, [...(byFile.get(key) ?? []), canister])
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const jobs = [...byFile].flatMap(([key, members]) => {
|
|
1168
|
+
// Named in messages by the first spelling.
|
|
1169
|
+
const file = didPath(members[0] as Generated)
|
|
1170
|
+
// Any canisterId given makes the file one to fetch, a refused
|
|
1171
|
+
// canister's included: `conflictOf` then refuses the fetch.
|
|
1172
|
+
const fetchable = members.filter(givesCanisterId)
|
|
1173
|
+
const again = refetched.has(key)
|
|
1174
|
+
? undefined
|
|
1175
|
+
: fetchable.find((canister) => fetches(canister))
|
|
1176
|
+
const source = again ?? (fs.existsSync(file) ? undefined : fetchable[0])
|
|
1177
|
+
if (!source) return []
|
|
1178
|
+
const conflict = conflictOf(members, blocked)
|
|
1179
|
+
if (conflict) {
|
|
1180
|
+
for (const canister of members) {
|
|
1181
|
+
failures.push({ canister, message: conflict })
|
|
1182
|
+
}
|
|
1183
|
+
return []
|
|
1184
|
+
}
|
|
1185
|
+
// Not blocked, so not refused.
|
|
1186
|
+
return [
|
|
1187
|
+
{ key, file, members, source: source as Generated, again: !!again },
|
|
1188
|
+
]
|
|
1189
|
+
})
|
|
1190
|
+
if (jobs.length === 0) return { failures: dedupe(failures), fetched }
|
|
1191
|
+
|
|
1192
|
+
const results = await Promise.all(
|
|
1193
|
+
jobs.map(async (job) => ({
|
|
1194
|
+
...job,
|
|
1195
|
+
result: await fetchProjected(job.source, job.file, signal),
|
|
1196
|
+
}))
|
|
1197
|
+
)
|
|
1198
|
+
if (signal.aborted) return undefined
|
|
1199
|
+
|
|
1200
|
+
for (const { key, file, members, again, result } of results) {
|
|
1201
|
+
if (!result.ok) {
|
|
1202
|
+
for (const canister of members) {
|
|
1203
|
+
failures.push({ canister, message: result.message })
|
|
1204
|
+
}
|
|
1205
|
+
continue
|
|
1206
|
+
}
|
|
1207
|
+
const { from } = result
|
|
1208
|
+
// Read and written through every symlink: a write to a symlinked
|
|
1209
|
+
// spelling would replace the symlink with a file of its own, and leave
|
|
1210
|
+
// the target, which the other spellings read, as it was.
|
|
1211
|
+
const target = realFile(file)
|
|
1212
|
+
let before: string | undefined
|
|
1213
|
+
try {
|
|
1214
|
+
before = fs.readFileSync(target, "utf-8")
|
|
1215
|
+
} catch {
|
|
1216
|
+
before = undefined
|
|
1217
|
+
}
|
|
1218
|
+
if (before === result.did) {
|
|
1219
|
+
log.info(`ic-reactor: ${relativeToRoot(file)} matches ${from}`)
|
|
1220
|
+
if (again) refetched.add(key)
|
|
1221
|
+
fetched.set(key, result.did)
|
|
1222
|
+
continue
|
|
1223
|
+
}
|
|
1224
|
+
// Only a dev server's watcher reports the write and clears the entry.
|
|
1225
|
+
if (lifetime.server) lifetime.justFetched.set(key, result.did)
|
|
1226
|
+
try {
|
|
1227
|
+
writeDid(target, result.did)
|
|
1228
|
+
} catch (writeError) {
|
|
1229
|
+
// Only the canisters of this file fail: the others generate.
|
|
1230
|
+
lifetime.justFetched.delete(key)
|
|
1231
|
+
for (const canister of members) {
|
|
1232
|
+
failures.push({
|
|
1233
|
+
canister,
|
|
1234
|
+
message:
|
|
1235
|
+
`fetched ${from}, but could not write it to ${relativeToRoot(file)}: ${describeError(writeError)}. ` +
|
|
1236
|
+
`Make its directory writable and build again.`,
|
|
1237
|
+
})
|
|
1238
|
+
}
|
|
1239
|
+
continue
|
|
1240
|
+
}
|
|
1241
|
+
// Recorded only now: a fetch or a write that failed is tried again.
|
|
1242
|
+
if (again) refetched.add(key)
|
|
1243
|
+
fetched.set(key, result.did)
|
|
1244
|
+
log.info(
|
|
1245
|
+
`ic-reactor: ${before === undefined ? "wrote" : "rewrote"} ${relativeToRoot(file)} from ${from} ` +
|
|
1246
|
+
`(${Buffer.byteLength(result.did)} bytes): commit it with the app`
|
|
1247
|
+
)
|
|
1248
|
+
}
|
|
1249
|
+
return { failures: dedupe(failures), fetched }
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
if (signal.aborted) return undefined
|
|
1252
|
+
const failed = new Set(failures.map(({ canister }) => canister))
|
|
1253
|
+
return {
|
|
1254
|
+
failures: [
|
|
1255
|
+
...failures,
|
|
1256
|
+
...generated
|
|
1257
|
+
.filter(
|
|
1258
|
+
(canister) => !failed.has(canister) && !blocked.has(canister)
|
|
1259
|
+
)
|
|
1260
|
+
.map((canister) => ({ canister, message: describeError(error) })),
|
|
1261
|
+
],
|
|
1262
|
+
fetched,
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/** The CLI's context for `project` and `check`, or why there is none. */
|
|
1268
|
+
const cliContext = (signal: AbortSignal): CliContext | string => {
|
|
1269
|
+
try {
|
|
1270
|
+
return { cli: resolveCliBin(projectRoot), root: projectRoot, signal }
|
|
1271
|
+
} catch (error) {
|
|
1272
|
+
return describeError(error)
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* The certified `candid:service` of `source`'s canister, projected to its
|
|
1278
|
+
* `methods` when it names some, with how the log names where it came from.
|
|
1279
|
+
* Never rejects.
|
|
1280
|
+
*/
|
|
1281
|
+
const fetchProjected = async (
|
|
1282
|
+
source: Generated,
|
|
1283
|
+
file: string,
|
|
1284
|
+
signal: AbortSignal
|
|
1285
|
+
): Promise<
|
|
1286
|
+
{ ok: true; did: string; from: string } | { ok: false; message: string }
|
|
1287
|
+
> => {
|
|
1288
|
+
const didFile = relativeToRoot(file)
|
|
1289
|
+
const result = await fetchCandid({
|
|
1290
|
+
canisterId: source.canisterId as string,
|
|
1291
|
+
network: source.network,
|
|
1292
|
+
didFile,
|
|
1293
|
+
signal,
|
|
1294
|
+
})
|
|
1295
|
+
if (!result.ok) return result
|
|
1296
|
+
const where = `${source.canisterId} on ${result.network}`
|
|
1297
|
+
const from = `the certified candid:service of ${where}`
|
|
1298
|
+
if (source.methods === undefined) return { ok: true, did: result.did, from }
|
|
1299
|
+
|
|
1300
|
+
const context = cliContext(signal)
|
|
1301
|
+
if (typeof context === "string") {
|
|
1302
|
+
return {
|
|
1303
|
+
ok: false,
|
|
1304
|
+
message: `fetched ${from}, but cannot project it to methods: ${context}`,
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const asked = methodList(source.methods)
|
|
1308
|
+
const projection = await project(context, { text: result.did }, asked)
|
|
1309
|
+
if (projection.ok) {
|
|
1310
|
+
return {
|
|
1311
|
+
ok: true,
|
|
1312
|
+
did: projection.did,
|
|
1313
|
+
from: `${from}, projected to ${listNames(projection.methods)}`,
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
ok: false,
|
|
1318
|
+
message: describeProjectionFailure(projection, asked, where, didFile),
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** Why the live interface of `where` could not be projected to `asked`. */
|
|
1323
|
+
const describeProjectionFailure = (
|
|
1324
|
+
projection: Extract<Projection, { ok: false }>,
|
|
1325
|
+
asked: string[],
|
|
1326
|
+
where: string,
|
|
1327
|
+
didFile: string
|
|
1328
|
+
): string => {
|
|
1329
|
+
const [first] = projection.diagnostics ?? []
|
|
1330
|
+
if (first?.code === "unknown_method" && Array.isArray(first.notes)) {
|
|
1331
|
+
const has = first.notes.map(String)
|
|
1332
|
+
const lacking = asked.filter((name) => !has.includes(name))
|
|
1333
|
+
return (
|
|
1334
|
+
`methods names ${listNames(lacking)}, which the live interface of ${where} does not have, ` +
|
|
1335
|
+
`so ${didFile} is not written. It has ${has.length} method${has.length === 1 ? "" : "s"}: ` +
|
|
1336
|
+
`${listNames(has)}. Correct methods and build again.`
|
|
1337
|
+
)
|
|
1338
|
+
}
|
|
1339
|
+
return (
|
|
1340
|
+
`the certified candid:service of ${where} could not be projected to methods, ` +
|
|
1341
|
+
`so ${didFile} is not written: ${projection.message}`
|
|
1342
|
+
)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* The outcome of comparing each `methods` canister's `didFile` with its
|
|
1347
|
+
* `methods`, by canister name, for the text and the list it compared: the
|
|
1348
|
+
* file is compared again only when either changes, and a mismatch that is
|
|
1349
|
+
* only a warning is logged once.
|
|
1350
|
+
*/
|
|
1351
|
+
const comparedMethods = new Map<
|
|
1352
|
+
string,
|
|
1353
|
+
{ text: string; methods: string; failure?: string }
|
|
1354
|
+
>()
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Compare the `didFile` of each canister of `wanted` that names `methods`
|
|
1358
|
+
* with them, with no network access: a file that lacks a name of `methods`
|
|
1359
|
+
* fails the canister, since its module would lack the method, and a file
|
|
1360
|
+
* that holds a method `methods` does not name is generated, with a warning.
|
|
1361
|
+
* Either way the file was written for other methods, and the message says to
|
|
1362
|
+
* fetch it again. A file is never fetched for it: only `IC_REACTOR_FETCH`
|
|
1363
|
+
* fetches a file on disk. A file this run just fetched is compared too: a
|
|
1364
|
+
* canister that shares it may name another list.
|
|
1365
|
+
*
|
|
1366
|
+
* A file whose methods cannot be listed (it does not compile, or the CLI
|
|
1367
|
+
* failed) fails the canister: a comparison that did not run must not pass
|
|
1368
|
+
* for one that did. Resolves with the canisters that cannot be generated,
|
|
1369
|
+
* or `undefined` when `signal` was aborted first. Never rejects.
|
|
1370
|
+
*
|
|
1371
|
+
* The comparison is of the text `run` read, and only when the file is still
|
|
1372
|
+
* as the run read it once the CLI listed it: the CLI reads the file itself,
|
|
1373
|
+
* so after a save before or while it ran, what it listed may be another
|
|
1374
|
+
* text's. Such a listing is neither recorded, nor reported, nor warned
|
|
1375
|
+
* about. The canister fails here only so that no later step of the run
|
|
1376
|
+
* (the live check, generation) reads the file: the end of the run takes
|
|
1377
|
+
* nothing of it, that failure included (see `writtenSince`).
|
|
1378
|
+
*/
|
|
1379
|
+
const compareMethods = async (
|
|
1380
|
+
wanted: Generated[],
|
|
1381
|
+
run: Run,
|
|
1382
|
+
signal: AbortSignal
|
|
1383
|
+
): Promise<Failure[] | undefined> => {
|
|
1384
|
+
const failures: Failure[] = []
|
|
1385
|
+
const jobs = wanted.flatMap((canister) => {
|
|
1386
|
+
// A list given that is not one refused the canister when the plugin
|
|
1387
|
+
// was made (options.ts): only an omitted list is the whole interface.
|
|
1388
|
+
if (canister.methods === undefined) return []
|
|
1389
|
+
const text = run.textOf(canister)
|
|
1390
|
+
// A missing file is the generator's to report.
|
|
1391
|
+
if (text === undefined) return []
|
|
1392
|
+
const methods = methodList(canister.methods)
|
|
1393
|
+
const key = JSON.stringify(methods)
|
|
1394
|
+
const known = comparedMethods.get(canister.name)
|
|
1395
|
+
if (known && known.text === text && known.methods === key) {
|
|
1396
|
+
if (known.failure) failures.push({ canister, message: known.failure })
|
|
1397
|
+
return []
|
|
1398
|
+
}
|
|
1399
|
+
return [{ canister, text, methods, key }]
|
|
1400
|
+
})
|
|
1401
|
+
if (jobs.length === 0) return failures
|
|
1402
|
+
|
|
1403
|
+
const context = cliContext(signal)
|
|
1404
|
+
// No CLI: the generator's run says so for every canister.
|
|
1405
|
+
if (typeof context === "string") return failures
|
|
1406
|
+
const results = await Promise.all(
|
|
1407
|
+
jobs.map(async (job) => ({
|
|
1408
|
+
...job,
|
|
1409
|
+
listed: await listMethods(context, { file: didPath(job.canister) }),
|
|
1410
|
+
}))
|
|
1411
|
+
)
|
|
1412
|
+
if (signal.aborted) return undefined
|
|
1413
|
+
|
|
1414
|
+
for (const { canister, text, methods, key, listed } of results) {
|
|
1415
|
+
const file = relativeToRoot(didPath(canister))
|
|
1416
|
+
const ofText = run.asRead(canister)
|
|
1417
|
+
if (!ofText) {
|
|
1418
|
+
failures.push({
|
|
1419
|
+
canister,
|
|
1420
|
+
message: `${file} was written while its methods were listed`,
|
|
1421
|
+
})
|
|
1422
|
+
continue
|
|
1423
|
+
}
|
|
1424
|
+
if (!listed.ok) {
|
|
1425
|
+
// Not remembered: the next save or buildStart lists it again.
|
|
1426
|
+
failures.push({
|
|
1427
|
+
canister,
|
|
1428
|
+
message: `methods cannot be compared with ${file}, whose methods could not be listed:\n${listed.message}`,
|
|
1429
|
+
})
|
|
1430
|
+
continue
|
|
1431
|
+
}
|
|
1432
|
+
const lacking = methods.filter((name) => !listed.methods.includes(name))
|
|
1433
|
+
const extra = listed.methods.filter((name) => !methods.includes(name))
|
|
1434
|
+
const fix =
|
|
1435
|
+
canister.canisterId !== undefined
|
|
1436
|
+
? `Fetch it again with ${FETCH_VARIABLE}=${canister.name} to write the projection to methods`
|
|
1437
|
+
: `Write the projection to methods with \`meant project\`, or give the canister a canisterId to fetch it from`
|
|
1438
|
+
let failure: string | undefined
|
|
1439
|
+
if (lacking.length > 0) {
|
|
1440
|
+
failure =
|
|
1441
|
+
`methods names ${listNames(lacking)}, which ${file} does not hold (it holds ${listNames(listed.methods)}): ` +
|
|
1442
|
+
`the file was written for other methods, and its module would lack ${lacking.length === 1 ? "it" : "them"}. ` +
|
|
1443
|
+
`${fix}, or take ${lacking.length === 1 ? "the name" : "the names"} out of methods.`
|
|
1444
|
+
} else if (extra.length > 0) {
|
|
1445
|
+
log.warn(
|
|
1446
|
+
`ic-reactor: ${canister.name}: ${file} also holds ${listNames(extra)}, which methods does not name, ` +
|
|
1447
|
+
`so its module has ${extra.length === 1 ? "it" : "them"} too. ${fix}.`
|
|
1448
|
+
)
|
|
1449
|
+
}
|
|
1450
|
+
comparedMethods.set(canister.name, { text, methods: key, failure })
|
|
1451
|
+
if (failure) failures.push({ canister, message: failure })
|
|
1452
|
+
}
|
|
1453
|
+
return failures
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* The live check: fetch the certified interface of each canister of
|
|
1458
|
+
* `wanted` that asks for it (`checks`: `verify: "live"`, or
|
|
1459
|
+
* `IC_REACTOR_VERIFY`) and check its `didFile` against it with
|
|
1460
|
+
* `meant check`. Canisters that name one file and one canister on
|
|
1461
|
+
* one network share one fetch and one check. A `verify` that is not
|
|
1462
|
+
* `"live"`, `"live"` with no `canisterId`, and a canister
|
|
1463
|
+
* `IC_REACTOR_VERIFY` names with no `canisterId` were refused before the
|
|
1464
|
+
* run did anything: `wanted` holds none of them.
|
|
1465
|
+
*
|
|
1466
|
+
* - A written method that the live canister no longer has, whose mode
|
|
1467
|
+
* changed, or whose types are no longer compatible fails the canister,
|
|
1468
|
+
* which is not generated, as one whose fetch failed: the failure follows
|
|
1469
|
+
* `failOnError`.
|
|
1470
|
+
* - A compatible change is logged as a warning, and the canister generates:
|
|
1471
|
+
* one that Candid accepts only by reading a value as `null`
|
|
1472
|
+
* (`special_opt_rule`), and one that changed the types of the file's
|
|
1473
|
+
* methods compatibly (the projections of the two interfaces to the file's
|
|
1474
|
+
* methods have other identities).
|
|
1475
|
+
* - A check that cannot be made (the network, the canister, a side that does
|
|
1476
|
+
* not compile) fails the canister: a check that was asked for and did not
|
|
1477
|
+
* run must not pass for one that did.
|
|
1478
|
+
*
|
|
1479
|
+
* Files `fetched` (by `identify`) were just read from the live canister, and
|
|
1480
|
+
* are not checked while `run` read the text fetched. `passed` is the
|
|
1481
|
+
* lifetime's `verified`, read and written.
|
|
1482
|
+
*
|
|
1483
|
+
* The CLI reads the file itself, after the fetch, so a check is of the
|
|
1484
|
+
* run's text only if the file is still as the run read it once the check is
|
|
1485
|
+
* done. A canister whose file was written before that is `unsure`, whether
|
|
1486
|
+
* its check passed or failed: what the check found may be of another text.
|
|
1487
|
+
* Its pass is not recorded, and its verdict neither sets nor clears a
|
|
1488
|
+
* failure that stands (see the lifetime's `liveFailures`).
|
|
1489
|
+
*
|
|
1490
|
+
* Resolves with the canisters that cannot be generated, and the `unsure`
|
|
1491
|
+
* ones, or `undefined` when `signal` was aborted first. Never rejects.
|
|
1492
|
+
*/
|
|
1493
|
+
const verifyLive = async (
|
|
1494
|
+
wanted: Generated[],
|
|
1495
|
+
checks: Requests["checks"],
|
|
1496
|
+
fetched: Map<string, string>,
|
|
1497
|
+
passed: Map<string, string>,
|
|
1498
|
+
run: Run,
|
|
1499
|
+
signal: AbortSignal
|
|
1500
|
+
): Promise<{ failures: Failure[]; unsure: Set<Generated> } | undefined> => {
|
|
1501
|
+
const failures: Failure[] = []
|
|
1502
|
+
const unsure = new Set<Generated>()
|
|
1503
|
+
// A canister that fails here passed no check now: what passed before is
|
|
1504
|
+
// checked again.
|
|
1505
|
+
const forget = (failed: Failure[]) => {
|
|
1506
|
+
for (const { canister } of failed) passed.delete(canister.name)
|
|
1507
|
+
return { failures: failed, unsure }
|
|
1508
|
+
}
|
|
1509
|
+
// `0` turns the check off for the run, over `verify: "live"`: a project
|
|
1510
|
+
// that checks every build can still build offline.
|
|
1511
|
+
if (checks === "off") return { failures, unsure }
|
|
1512
|
+
|
|
1513
|
+
const byTarget = new Map<string, Generated[]>()
|
|
1514
|
+
for (const canister of wanted) {
|
|
1515
|
+
// `IC_REACTOR_VERIFY=1` passes over a canister with no canisterId.
|
|
1516
|
+
if (!checks(canister) || canister.canisterId === undefined) continue
|
|
1517
|
+
const key = identify(didPath(canister))
|
|
1518
|
+
const text = run.textOf(canister)
|
|
1519
|
+
// A file just fetched is the live interface, until it is saved; one
|
|
1520
|
+
// that is missing is the generator's to report.
|
|
1521
|
+
if (text === undefined || fetched.get(key) === text) continue
|
|
1522
|
+
if (passed.get(canister.name) === text) continue
|
|
1523
|
+
const target = JSON.stringify([
|
|
1524
|
+
key,
|
|
1525
|
+
canister.canisterId,
|
|
1526
|
+
resolveNetwork(canister.network),
|
|
1527
|
+
])
|
|
1528
|
+
byTarget.set(target, [...(byTarget.get(target) ?? []), canister])
|
|
1529
|
+
}
|
|
1530
|
+
if (byTarget.size === 0) return forget(failures)
|
|
1531
|
+
|
|
1532
|
+
const outcomes = await Promise.all(
|
|
1533
|
+
[...byTarget.values()].map(async (members) => {
|
|
1534
|
+
const outcome = await checkLive(members[0], run, signal)
|
|
1535
|
+
// Read again as soon as this check is done.
|
|
1536
|
+
return { members, outcome, ofText: run.asRead(members[0]) }
|
|
1537
|
+
})
|
|
1538
|
+
)
|
|
1539
|
+
if (signal.aborted) return undefined
|
|
1540
|
+
for (const { members, outcome, ofText } of outcomes) {
|
|
1541
|
+
// What a check of another text found is not logged either.
|
|
1542
|
+
if (outcome.ok && ofText) {
|
|
1543
|
+
for (const { level, message } of outcome.notes) log[level](message)
|
|
1544
|
+
}
|
|
1545
|
+
for (const canister of members) {
|
|
1546
|
+
if (!ofText) unsure.add(canister)
|
|
1547
|
+
if (!outcome.ok) {
|
|
1548
|
+
failures.push({ canister, message: outcome.message })
|
|
1549
|
+
continue
|
|
1550
|
+
}
|
|
1551
|
+
// Not recorded for a file written while it was checked: the next
|
|
1552
|
+
// buildStart checks it again.
|
|
1553
|
+
const text = run.textOf(canister)
|
|
1554
|
+
if (ofText && text !== undefined) passed.set(canister.name, text)
|
|
1555
|
+
else passed.delete(canister.name)
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return forget(failures)
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* The live check of one canister: whether it passed, with the lines it has
|
|
1563
|
+
* to log (a compatible change, or a match), or what failed. Whether what the
|
|
1564
|
+
* CLI read is the text `run` read is for the caller to tell, once the check
|
|
1565
|
+
* is done, so the lines are only returned. Never rejects.
|
|
1566
|
+
*/
|
|
1567
|
+
const checkLive = async (
|
|
1568
|
+
canister: Generated,
|
|
1569
|
+
run: Run,
|
|
1570
|
+
signal: AbortSignal
|
|
1571
|
+
): Promise<{ ok: true; notes: Note[] } | { ok: false; message: string }> => {
|
|
1572
|
+
const notes: Note[] = []
|
|
1573
|
+
try {
|
|
1574
|
+
const file = didPath(canister)
|
|
1575
|
+
const didFile = relativeToRoot(file)
|
|
1576
|
+
if (run.textOf(canister) === undefined)
|
|
1577
|
+
return { ok: false, message: `${didFile} is gone` }
|
|
1578
|
+
const live = await fetchCandid({
|
|
1579
|
+
canisterId: canister.canisterId as string,
|
|
1580
|
+
network: canister.network,
|
|
1581
|
+
didFile,
|
|
1582
|
+
signal,
|
|
1583
|
+
verify: true,
|
|
1584
|
+
})
|
|
1585
|
+
if (!live.ok) return live
|
|
1586
|
+
const where = `${canister.canisterId} on ${live.network}`
|
|
1587
|
+
const fetchAgain = `fetch ${didFile} again with ${FETCH_VARIABLE}=${canister.name}`
|
|
1588
|
+
|
|
1589
|
+
const context = cliContext(signal)
|
|
1590
|
+
if (typeof context === "string") {
|
|
1591
|
+
return {
|
|
1592
|
+
ok: false,
|
|
1593
|
+
message: `the live check of ${didFile} cannot run: ${context}`,
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
const report = await check(context, file, live.did)
|
|
1597
|
+
if (!report.ok) {
|
|
1598
|
+
const side =
|
|
1599
|
+
report.input === "live"
|
|
1600
|
+
? `the live interface of ${where} does not compile`
|
|
1601
|
+
: report.input === "written"
|
|
1602
|
+
? `${didFile} does not compile`
|
|
1603
|
+
: `\`meant check\` failed`
|
|
1604
|
+
return {
|
|
1605
|
+
ok: false,
|
|
1606
|
+
message: `the live check of ${didFile} against ${where} could not run: ${side}:\n${report.message}`,
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const errors = report.diagnostics.filter(
|
|
1610
|
+
({ severity }) => severity !== "warning"
|
|
1611
|
+
)
|
|
1612
|
+
const warnings = report.diagnostics.filter(
|
|
1613
|
+
({ severity }) => severity === "warning"
|
|
1614
|
+
)
|
|
1615
|
+
if (!report.compatible || errors.length > 0) {
|
|
1616
|
+
const count = errors.length
|
|
1617
|
+
return {
|
|
1618
|
+
ok: false,
|
|
1619
|
+
message:
|
|
1620
|
+
`the live check found ${where} incompatible with ${didFile} ` +
|
|
1621
|
+
`(${count} error${count === 1 ? "" : "s"}):\n` +
|
|
1622
|
+
firstLines(errors.map(describeFinding)).join("\n") +
|
|
1623
|
+
`\nThe generated module no longer matches the canister for these methods: ` +
|
|
1624
|
+
`${fetchAgain}, regenerate, and fix the calls the type checker then reports.`,
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
const label = `ic-reactor: ${canister.name}:`
|
|
1628
|
+
if (warnings.length > 0) {
|
|
1629
|
+
const count = warnings.length
|
|
1630
|
+
notes.push({
|
|
1631
|
+
level: "warn",
|
|
1632
|
+
message:
|
|
1633
|
+
`${label} ${where} is compatible with ${didFile}, with ${count} warning${count === 1 ? "" : "s"} ` +
|
|
1634
|
+
`(a value at each path now decodes as null):\n` +
|
|
1635
|
+
firstLines(warnings.map(describeFinding))
|
|
1636
|
+
.map((line) => ` ${line}`)
|
|
1637
|
+
.join("\n") +
|
|
1638
|
+
`\nTo take the change, ${fetchAgain}.`,
|
|
1639
|
+
})
|
|
1640
|
+
return { ok: true, notes }
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// Compatible with no finding: unchanged for the file's methods, or
|
|
1644
|
+
// changed compatibly (an added optional field, an added variant arm in
|
|
1645
|
+
// an argument). The projections of both sides to the file's methods have
|
|
1646
|
+
// one identity exactly when the methods are unchanged.
|
|
1647
|
+
const changed = await changedFor(context, file, live.did)
|
|
1648
|
+
if (signal.aborted) return { ok: false, message: "was stopped" }
|
|
1649
|
+
if (changed === true) {
|
|
1650
|
+
notes.push({
|
|
1651
|
+
level: "warn",
|
|
1652
|
+
message:
|
|
1653
|
+
`${label} ${where} changed compatibly for the methods of ${didFile}: its module still works. ` +
|
|
1654
|
+
`To take the change, ${fetchAgain}.`,
|
|
1655
|
+
})
|
|
1656
|
+
} else if (changed === false) {
|
|
1657
|
+
notes.push({
|
|
1658
|
+
level: "info",
|
|
1659
|
+
message: `${label} ${didFile} matches the live interface of ${where}`,
|
|
1660
|
+
})
|
|
1661
|
+
} else {
|
|
1662
|
+
notes.push({
|
|
1663
|
+
level: "info",
|
|
1664
|
+
message: `${label} ${didFile} is compatible with the live interface of ${where}`,
|
|
1665
|
+
})
|
|
1666
|
+
}
|
|
1667
|
+
return { ok: true, notes }
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
return { ok: false, message: describeError(error) }
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* Whether the live interface `live` differs from the `.did` at `file` for
|
|
1675
|
+
* the methods the file holds, `undefined` when that cannot be told.
|
|
1676
|
+
*/
|
|
1677
|
+
const changedFor = async (
|
|
1678
|
+
context: CliContext,
|
|
1679
|
+
file: string,
|
|
1680
|
+
live: string
|
|
1681
|
+
): Promise<boolean | undefined> => {
|
|
1682
|
+
const listed = await listMethods(context, { file })
|
|
1683
|
+
if (
|
|
1684
|
+
!listed.ok ||
|
|
1685
|
+
listed.methods.length === 0 ||
|
|
1686
|
+
listed.methods.some((name) => name.includes(","))
|
|
1687
|
+
) {
|
|
1688
|
+
return undefined
|
|
1689
|
+
}
|
|
1690
|
+
const [written, current] = await Promise.all([
|
|
1691
|
+
project(context, { file }, listed.methods),
|
|
1692
|
+
project(context, { text: live }, listed.methods),
|
|
1693
|
+
])
|
|
1694
|
+
if (!written.ok || !current.ok) return undefined
|
|
1695
|
+
return written.projection.interface_id !== current.projection.interface_id
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/**
|
|
1699
|
+
* The error text for failed canisters. Canisters that failed for the same
|
|
1700
|
+
* reason (no CLI installed, one crash) are listed under it once.
|
|
1701
|
+
*/
|
|
1702
|
+
const describeFailures = (failures: Failure[]): string => {
|
|
1703
|
+
const byMessage = new Map<string, string[]>()
|
|
1704
|
+
for (const { canister, message } of failures) {
|
|
1705
|
+
// A refused canister whose didFile is not a path is named alone.
|
|
1706
|
+
const label =
|
|
1707
|
+
canister.didFile === undefined
|
|
1708
|
+
? canister.name
|
|
1709
|
+
: `${canister.name} (${relativeToRoot(didPath(canister as Generated))})`
|
|
1710
|
+
byMessage.set(message, [...(byMessage.get(message) ?? []), label])
|
|
1711
|
+
}
|
|
1712
|
+
return (
|
|
1713
|
+
`ic-reactor: could not generate ${failures.length} of ${configured.length} canisters:\n` +
|
|
1714
|
+
[...byMessage]
|
|
1715
|
+
.map(
|
|
1716
|
+
([message, labels]) =>
|
|
1717
|
+
` - ${labels.join(", ")}: ${message.replace(/\n/g, "\n ")}`
|
|
1718
|
+
)
|
|
1719
|
+
.join("\n")
|
|
1720
|
+
)
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
/**
|
|
1724
|
+
* Report the outcome of a run that does not end the Vite run (a save, or a
|
|
1725
|
+
* build or a Vitest start with `failOnError` off): log a failure and put every unfixed one in
|
|
1726
|
+
* the error overlay of the `lifetime`'s browsers. When a canister is fixed,
|
|
1727
|
+
* the overlay is replaced by one that lists only the canisters still
|
|
1728
|
+
* failing, or cleared once there are none.
|
|
1729
|
+
*/
|
|
1730
|
+
const publish = (
|
|
1731
|
+
lifetime: Lifetime,
|
|
1732
|
+
attempted: Array<Generated | Refused>,
|
|
1733
|
+
failures: Failure[]
|
|
1734
|
+
): void => {
|
|
1735
|
+
// Every attempted canister leaves `unfixed`, not just the first one found
|
|
1736
|
+
// there: a save regenerates all the canisters that name the `.did`, and
|
|
1737
|
+
// `some` would stop at the first, leaving the others listed in an overlay
|
|
1738
|
+
// nothing clears.
|
|
1739
|
+
let wasFailing = false
|
|
1740
|
+
for (const { name } of attempted) {
|
|
1741
|
+
if (lifetime.unfixed.delete(name)) wasFailing = true
|
|
1742
|
+
}
|
|
1743
|
+
for (const failure of failures) {
|
|
1744
|
+
lifetime.unfixed.set(failure.canister.name, failure)
|
|
1745
|
+
}
|
|
1746
|
+
if (failures.length > 0) {
|
|
1747
|
+
log.error(describeFailures(failures))
|
|
1748
|
+
showOverlay(lifetime)
|
|
1749
|
+
} else if (wasFailing && lifetime.unfixed.size > 0) {
|
|
1750
|
+
// The open overlay still lists the canister that was just fixed.
|
|
1751
|
+
showOverlay(lifetime)
|
|
1752
|
+
} else if (wasFailing) {
|
|
1753
|
+
// Nothing may have changed on disk when a canister is fixed back to what
|
|
1754
|
+
// it generated before, so the page has nothing else to reload it.
|
|
1755
|
+
lifetime.server?.ws.send({ type: "full-reload" })
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Put the `lifetime`'s unfixed failures in the error overlay: of the
|
|
1761
|
+
* browser that just connected when the WebSocket hands it over
|
|
1762
|
+
* (`ws.on("connection")` does), and of every browser of its server
|
|
1763
|
+
* otherwise. Vite's client clears the overlay on every hot update it
|
|
1764
|
+
* applies, and nothing here sends it again, so the overlay can go while a
|
|
1765
|
+
* canister is still broken. The terminal log keeps the error, and the
|
|
1766
|
+
* replay on connection shows it after the next full reload.
|
|
1767
|
+
*/
|
|
1768
|
+
const showOverlay = (
|
|
1769
|
+
lifetime: Lifetime,
|
|
1770
|
+
client?: { send?: (data: string) => void }
|
|
1771
|
+
): void => {
|
|
1772
|
+
if (lifetime.unfixed.size === 0) return
|
|
1773
|
+
const payload = {
|
|
1774
|
+
type: "error" as const,
|
|
1775
|
+
err: {
|
|
1776
|
+
message: describeFailures([...lifetime.unfixed.values()]),
|
|
1777
|
+
stack: "",
|
|
1778
|
+
plugin: PLUGIN_NAME,
|
|
1779
|
+
},
|
|
1780
|
+
}
|
|
1781
|
+
if (typeof client?.send === "function") {
|
|
1782
|
+
try {
|
|
1783
|
+
client.send(JSON.stringify(payload))
|
|
1784
|
+
} catch {
|
|
1785
|
+
// A browser that is already gone has no use for the overlay, and a
|
|
1786
|
+
// listener must not throw into the WebSocket server.
|
|
1787
|
+
}
|
|
1788
|
+
return
|
|
1789
|
+
}
|
|
1790
|
+
lifetime.server?.ws.send(payload)
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* A `.did` file was saved, as the watcher of the `lifetime`'s server
|
|
1795
|
+
* reports: regenerate the canisters that name it, and only those. Canisters
|
|
1796
|
+
* with one interface share its file and its run. Saves that arrive while it
|
|
1797
|
+
* runs collapse into one run after it, so the last saved file wins without
|
|
1798
|
+
* runs piling up.
|
|
1799
|
+
*/
|
|
1800
|
+
const onDidSaved = (lifetime: Lifetime, file: string): void => {
|
|
1801
|
+
const saved = path.normalize(file)
|
|
1802
|
+
// By the file: the watcher may report it under any of its spellings.
|
|
1803
|
+
const key = identify(saved)
|
|
1804
|
+
const affected = generated.filter(
|
|
1805
|
+
(canister) => identify(didPath(canister)) === key
|
|
1806
|
+
)
|
|
1807
|
+
if (affected.length === 0 || lifetime.queued.has(key)) return
|
|
1808
|
+
// The plugin's own write of a fetched file, which the run that fetched it
|
|
1809
|
+
// generates.
|
|
1810
|
+
const fetched = lifetime.justFetched.get(key)
|
|
1811
|
+
if (fetched !== undefined) {
|
|
1812
|
+
lifetime.justFetched.delete(key)
|
|
1813
|
+
try {
|
|
1814
|
+
if (fs.readFileSync(saved, "utf-8") === fetched) return
|
|
1815
|
+
} catch {
|
|
1816
|
+
// Gone again: regenerate, which reports it.
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
regenerate(lifetime, saved, key, affected)
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
/**
|
|
1823
|
+
* Regenerate `affected`, the canisters that name the `.did` at `saved`
|
|
1824
|
+
* (`key`, by `identify`), in a run of the `lifetime` after the one in
|
|
1825
|
+
* flight. See `onDidSaved`.
|
|
1826
|
+
*
|
|
1827
|
+
* A canister of `affected` that each start refuses (see `runRefusals`), and
|
|
1828
|
+
* one whose live check failed (see `liveFailures`), is reported again, for
|
|
1829
|
+
* its reason, and nothing is run for it.
|
|
1830
|
+
*
|
|
1831
|
+
* The run reads the file once, when it starts (see {@link Run}). A file
|
|
1832
|
+
* written again before it is done is queued once more, and this run takes
|
|
1833
|
+
* nothing of it: it publishes nothing, neither a failure nor that a
|
|
1834
|
+
* canister is fixed, and the run after it reports the file as it is then.
|
|
1835
|
+
*
|
|
1836
|
+
* Only the close of the `lifetime`'s own server stops it: a restart starts
|
|
1837
|
+
* the new server, whose runs these may be, before it closes the old one.
|
|
1838
|
+
*/
|
|
1839
|
+
const regenerate = (
|
|
1840
|
+
lifetime: Lifetime,
|
|
1841
|
+
saved: string,
|
|
1842
|
+
key: string,
|
|
1843
|
+
affected: Generated[]
|
|
1844
|
+
): void => {
|
|
1845
|
+
lifetime.queued.add(key)
|
|
1846
|
+
const { signal } = lifetime.stopper
|
|
1847
|
+
void serially(async () => {
|
|
1848
|
+
lifetime.queued.delete(key)
|
|
1849
|
+
// Dropped when the server closed while it waited.
|
|
1850
|
+
if (signal.aborted) return
|
|
1851
|
+
// No live check here: a save is never a network request. So a canister
|
|
1852
|
+
// whose live check failed stays failed, and is not generated, until a
|
|
1853
|
+
// start checks it again (see `liveFailures`). Read here, in the job, and
|
|
1854
|
+
// not when the save is queued: a check in flight may still set it.
|
|
1855
|
+
const standing: Failure[] = []
|
|
1856
|
+
const open: Generated[] = []
|
|
1857
|
+
// A canister that each start refuses is not generated by a save either:
|
|
1858
|
+
// the run of one file sees only the canisters that name it.
|
|
1859
|
+
const refusedNow = runRefusals()
|
|
1860
|
+
for (const canister of affected) {
|
|
1861
|
+
const refusal = refusedNow.get(canister)
|
|
1862
|
+
if (refusal !== undefined) {
|
|
1863
|
+
standing.push({ canister, message: refusal })
|
|
1864
|
+
continue
|
|
1865
|
+
}
|
|
1866
|
+
const failure = lifetime.liveFailures.get(canister.name)
|
|
1867
|
+
if (!failure) {
|
|
1868
|
+
open.push(canister)
|
|
1869
|
+
continue
|
|
1870
|
+
}
|
|
1871
|
+
standing.push({
|
|
1872
|
+
canister,
|
|
1873
|
+
message:
|
|
1874
|
+
`${failure.message}\n${relativeToRoot(saved)} was saved, and a save makes no request: ` +
|
|
1875
|
+
`this stands until a live check passes, which runs when the dev server restarts.`,
|
|
1876
|
+
})
|
|
1877
|
+
}
|
|
1878
|
+
if (open.length > 0) {
|
|
1879
|
+
log.info(
|
|
1880
|
+
`ic-reactor: ${relativeToRoot(saved)} changed, regenerating ${open.map(({ name }) => name).join(", ")}`
|
|
1881
|
+
)
|
|
1882
|
+
}
|
|
1883
|
+
const run = startRun(open)
|
|
1884
|
+
const mismatched = await compareMethods(open, run, signal)
|
|
1885
|
+
if (!mismatched) return
|
|
1886
|
+
const failed = new Set(mismatched.map(({ canister }) => canister))
|
|
1887
|
+
const failures = await generateNow(
|
|
1888
|
+
open.filter((canister) => !failed.has(canister)),
|
|
1889
|
+
true,
|
|
1890
|
+
run,
|
|
1891
|
+
signal
|
|
1892
|
+
)
|
|
1893
|
+
if (!failures) return
|
|
1894
|
+
const written = writtenSince(run, open)
|
|
1895
|
+
if (written.length > 0) {
|
|
1896
|
+
requeue(lifetime, written)
|
|
1897
|
+
return
|
|
1898
|
+
}
|
|
1899
|
+
publish(lifetime, affected, [...standing, ...mismatched, ...failures])
|
|
1900
|
+
})
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Queue the `didFile` of each of `canisters` again in the `lifetime`, as a
|
|
1905
|
+
* save of it does, unless a run is queued for it already: a run under a dev
|
|
1906
|
+
* server took nothing of it, since the file was written while the run read
|
|
1907
|
+
* it (see `writtenSince`).
|
|
1908
|
+
*/
|
|
1909
|
+
const requeue = (lifetime: Lifetime, canisters: Generated[]): void => {
|
|
1910
|
+
for (const canister of canisters) {
|
|
1911
|
+
const key = identify(didPath(canister))
|
|
1912
|
+
if (lifetime.queued.has(key)) continue
|
|
1913
|
+
regenerate(
|
|
1914
|
+
lifetime,
|
|
1915
|
+
didPath(canister),
|
|
1916
|
+
key,
|
|
1917
|
+
generated.filter((other) => identify(didPath(other)) === key)
|
|
1918
|
+
)
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
/**
|
|
1923
|
+
* A `.did` file was deleted, as the watcher of the `lifetime`'s server
|
|
1924
|
+
* reports: fail the canisters that name it, in the log and the overlay,
|
|
1925
|
+
* until it comes back (an `add` regenerates them). The module generated
|
|
1926
|
+
* from it is still on disk and would otherwise go on looking current. No
|
|
1927
|
+
* generator runs, since there is nothing to read.
|
|
1928
|
+
*
|
|
1929
|
+
* It waits behind the run in flight, so that run cannot report the file as
|
|
1930
|
+
* generated after this has reported it gone.
|
|
1931
|
+
*/
|
|
1932
|
+
const onDidRemoved = (lifetime: Lifetime, file: string): void => {
|
|
1933
|
+
const removed = path.normalize(file)
|
|
1934
|
+
const key = identify(removed)
|
|
1935
|
+
const affected = generated.filter(
|
|
1936
|
+
(canister) => identify(didPath(canister)) === key
|
|
1937
|
+
)
|
|
1938
|
+
if (affected.length === 0) return
|
|
1939
|
+
const { signal } = lifetime.stopper
|
|
1940
|
+
void serially(async () => {
|
|
1941
|
+
try {
|
|
1942
|
+
// Back already, and the run its `add` queued regenerates it.
|
|
1943
|
+
if (signal.aborted || fs.existsSync(removed)) return
|
|
1944
|
+
for (const { name } of affected) generatedFrom.delete(name)
|
|
1945
|
+
// A canister that each start refuses keeps that reason: the file
|
|
1946
|
+
// coming back would not generate it either (see `runRefusals`).
|
|
1947
|
+
const refusedNow = runRefusals()
|
|
1948
|
+
publish(
|
|
1949
|
+
lifetime,
|
|
1950
|
+
affected,
|
|
1951
|
+
affected.map((canister) => ({
|
|
1952
|
+
canister,
|
|
1953
|
+
message:
|
|
1954
|
+
refusedNow.get(canister) ??
|
|
1955
|
+
"the .did file was deleted; its module is not regenerated until the file comes back" +
|
|
1956
|
+
(canister.canisterId !== undefined
|
|
1957
|
+
? `, or until the dev server restarts and fetches it from ${canister.canisterId}`
|
|
1958
|
+
: ""),
|
|
1959
|
+
}))
|
|
1960
|
+
)
|
|
1961
|
+
} catch (error) {
|
|
1962
|
+
// A listener must not throw into the watcher.
|
|
1963
|
+
log.error(`ic-reactor: ${describeError(error)}`)
|
|
1964
|
+
}
|
|
1965
|
+
})
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* What `IC_REACTOR_FETCH` and `IC_REACTOR_VERIFY` ask of this run, read
|
|
1970
|
+
* when its steps start, and the canisters they name that cannot do it: one
|
|
1971
|
+
* with no `canisterId` cannot be fetched or checked. Such a canister fails
|
|
1972
|
+
* as a refused one does, before the run does anything for it, the run of a
|
|
1973
|
+
* save of its file included (see `runRefusals`). With `warn`, as a start
|
|
1974
|
+
* reads them, a name that is no canister with a `didFile` is warned about,
|
|
1975
|
+
* and so is a name beside `0`.
|
|
1976
|
+
*/
|
|
1977
|
+
const readRequests = (warn = true): Requests => {
|
|
1978
|
+
const failures: Requests["failures"] = []
|
|
1979
|
+
const named = (variable: string, name: string): Generated | undefined => {
|
|
1980
|
+
const canister = generated.find((entry) => entry.name === name)
|
|
1981
|
+
// A refused canister fails for its options already.
|
|
1982
|
+
if (warn && !canister && !refused.some((entry) => entry.name === name)) {
|
|
1983
|
+
log.warn(
|
|
1984
|
+
`ic-reactor: ${variable} names "${name}", which is not a canister with a didFile in this plugin's options`
|
|
1985
|
+
)
|
|
1986
|
+
}
|
|
1987
|
+
return canister
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
const fetch = namesIn(FETCH_VARIABLE, ["all"])
|
|
1991
|
+
for (const name of fetch.all ? [] : fetch.names) {
|
|
1992
|
+
const canister = named(FETCH_VARIABLE, name)
|
|
1993
|
+
if (canister && canister.canisterId === undefined) {
|
|
1994
|
+
failures.push({
|
|
1995
|
+
canister,
|
|
1996
|
+
message: `${FETCH_VARIABLE} asks to fetch its didFile again, but it names no canisterId to fetch it from`,
|
|
1997
|
+
})
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
const verify = namesIn(VERIFY_VARIABLE, VERIFY_ALL)
|
|
2002
|
+
// `0` turns the check off for the run, over `verify: "live"`: a project
|
|
2003
|
+
// that checks every build can still build offline.
|
|
2004
|
+
const off = verify.names.find((name) => VERIFY_OFF.includes(name))
|
|
2005
|
+
if (warn && off !== undefined && verify.names.length > 1) {
|
|
2006
|
+
log.warn(
|
|
2007
|
+
`ic-reactor: ${VERIFY_VARIABLE} is "${verify.names.join(",")}", and ${off} turns the live check off: no canister is checked`
|
|
2008
|
+
)
|
|
2009
|
+
}
|
|
2010
|
+
for (const name of off !== undefined || verify.all ? [] : verify.names) {
|
|
2011
|
+
const canister = named(VERIFY_VARIABLE, name)
|
|
2012
|
+
if (canister && canister.canisterId === undefined) {
|
|
2013
|
+
failures.push({
|
|
2014
|
+
canister,
|
|
2015
|
+
message: `${VERIFY_VARIABLE} asks for its live check, but it names no canisterId to check its didFile against`,
|
|
2016
|
+
})
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
return {
|
|
2021
|
+
fetches: (canister) => fetch.all || fetch.names.includes(canister.name),
|
|
2022
|
+
checks:
|
|
2023
|
+
off !== undefined
|
|
2024
|
+
? "off"
|
|
2025
|
+
: (canister) =>
|
|
2026
|
+
canister.verify === "live" ||
|
|
2027
|
+
(verify.all && canister.canisterId !== undefined) ||
|
|
2028
|
+
verify.names.includes(canister.name),
|
|
2029
|
+
failures,
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
/**
|
|
2034
|
+
* The canisters with valid options that each start refuses all the same,
|
|
2035
|
+
* with why, before its run does anything for them: one whose module a
|
|
2036
|
+
* canister before it writes (see `collisions`), and one that a variable
|
|
2037
|
+
* asks of what it cannot do (see `readRequests`, read again here without
|
|
2038
|
+
* the warnings the start gave). The first reason of each stands, as at a
|
|
2039
|
+
* start.
|
|
2040
|
+
*
|
|
2041
|
+
* The run of a save sees only the canisters that name the file saved, so it
|
|
2042
|
+
* asks this as a start does, and reports such a canister again instead of
|
|
2043
|
+
* generating it. The file's deletion keeps the reason too, since the file
|
|
2044
|
+
* coming back would not generate the canister either. Never throws.
|
|
2045
|
+
*/
|
|
2046
|
+
const runRefusals = (): Map<Generated, string> => {
|
|
2047
|
+
const reasons = collisions()
|
|
2048
|
+
for (const { canister, message } of readRequests(false).failures) {
|
|
2049
|
+
if (!reasons.has(canister)) reasons.set(canister, message)
|
|
2050
|
+
}
|
|
2051
|
+
return reasons
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/** Log the guide line once, before anything else the plugin says. */
|
|
2055
|
+
const announceGuide = (userConfig: UserConfig): void => {
|
|
2056
|
+
if (announced) return
|
|
2057
|
+
announced = true
|
|
2058
|
+
if (userConfig.customLogger) {
|
|
2059
|
+
userConfig.customLogger.info(GUIDE_LINE)
|
|
2060
|
+
} else if (
|
|
2061
|
+
!["silent", "error", "warn"].includes(userConfig.logLevel ?? "")
|
|
2062
|
+
) {
|
|
2063
|
+
console.log(GUIDE_LINE)
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
const plugin: Plugin = {
|
|
2068
|
+
name: PLUGIN_NAME,
|
|
2069
|
+
enforce: "pre", // Run before other plugins
|
|
2070
|
+
|
|
2071
|
+
async config(userConfig, { command: viteCommand, mode }) {
|
|
2072
|
+
announceGuide(userConfig)
|
|
2073
|
+
|
|
2074
|
+
// Vitest runs the plugin with the `serve` command and mode `test`, and
|
|
2075
|
+
// a test run has no use for a cookie or a proxy, or for asking `icp`
|
|
2076
|
+
// about a network.
|
|
2077
|
+
if (viteCommand !== "serve" || mode === "test" || !injectEnvironment) {
|
|
2078
|
+
return {}
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// ── Local Development Proxy & Cookies ────────────────────────────────
|
|
2082
|
+
|
|
2083
|
+
if (notCanisterIds.length > 0) {
|
|
2084
|
+
console.warn(
|
|
2085
|
+
`[ic-reactor] canisterId is not a canister ID (${notCanisterIds.join("; ")}), ` +
|
|
2086
|
+
`so the ic_env cookie takes the ID of ${notCanisterIds.length === 1 ? "that canister" : "those canisters"} from icp. ` +
|
|
2087
|
+
`Give the canister's principal as text, or remove canisterId.`
|
|
2088
|
+
)
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (uncarriedNames.length > 0) {
|
|
2092
|
+
const one = uncarriedNames.length === 1
|
|
2093
|
+
console.warn(
|
|
2094
|
+
`[ic-reactor] The ic_env cookie cannot carry the canister${one ? "" : "s"} ` +
|
|
2095
|
+
`${uncarriedNames.map((name) => describeValue(name)).join(", ")}: it joins its entries with "&" ` +
|
|
2096
|
+
`and each name to its ID with "=", so a name that holds either would be read back as other keys, ` +
|
|
2097
|
+
`and it cannot encode text that is not well-formed. ${one ? "It is" : "They are"} left out of the ` +
|
|
2098
|
+
`cookie, and icp is not asked for ${one ? "its ID" : "their IDs"}. Name the canister as icp does: ` +
|
|
2099
|
+
`ASCII letters, digits, "_" and "-".`
|
|
2100
|
+
)
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// The plugin's own `/api` entry, unless the Vite config has one.
|
|
2104
|
+
const ownsApiProxy = !userConfig.server?.proxy?.["/api"]
|
|
2105
|
+
|
|
2106
|
+
// The options of this server's `/api` proxy, as Vite hands them over:
|
|
2107
|
+
// this config's, so that the detection of another server (the one a
|
|
2108
|
+
// restart replaces) never moves this one's proxy.
|
|
2109
|
+
const apiProxyOptions = new Set<ProxyOptions>()
|
|
2110
|
+
|
|
2111
|
+
const environment = createLocalEnvironment({
|
|
2112
|
+
canisterNames: names,
|
|
2113
|
+
configuredCanisterIds,
|
|
2114
|
+
// `configResolved` has not run yet, so resolve the root the way Vite
|
|
2115
|
+
// will. icp finds the project from the directory it starts in, and
|
|
2116
|
+
// with `vite apps/web` or a `root` option that is not the process cwd.
|
|
2117
|
+
projectRoot: path.resolve(userConfig.root ?? process.cwd()),
|
|
2118
|
+
onDiagnostic: debugLog,
|
|
2119
|
+
onUpdate: (previous, next) => {
|
|
2120
|
+
for (const proxyOptions of apiProxyOptions) {
|
|
2121
|
+
proxyOptions.target = next.proxyTarget
|
|
2122
|
+
}
|
|
2123
|
+
if (previous) {
|
|
2124
|
+
// Only a proxy the plugin kept following moves with detection.
|
|
2125
|
+
reportDetectionProgress(previous, next, apiProxyOptions.size > 0)
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
})
|
|
2129
|
+
localEnvironment = environment
|
|
2130
|
+
|
|
2131
|
+
const state = await environment.detect()
|
|
2132
|
+
warnAboutIncompleteDetection(state, names.length > 0, ownsApiProxy)
|
|
2133
|
+
|
|
2134
|
+
return {
|
|
2135
|
+
server: {
|
|
2136
|
+
// The cookie is not a static `server.headers` entry: the middleware
|
|
2137
|
+
// that configureServer adds sets it per response, from the latest
|
|
2138
|
+
// detection. See dev-environment.ts.
|
|
2139
|
+
proxy: apiProxy(userConfig, state.proxyTarget, (proxyOptions) => {
|
|
2140
|
+
// A plugin whose config hook runs after this one can proxy /api
|
|
2141
|
+
// as well. Vite merges its entry over the one returned here and
|
|
2142
|
+
// keeps this `configure`, so a target other than the one returned
|
|
2143
|
+
// here is that plugin's, and it stays where that plugin put it.
|
|
2144
|
+
if (proxyOptions.target !== state.proxyTarget) {
|
|
2145
|
+
debugLog(
|
|
2146
|
+
"Another plugin changed the target of the /api proxy, so the plugin leaves that proxy alone."
|
|
2147
|
+
)
|
|
2148
|
+
return
|
|
2149
|
+
}
|
|
2150
|
+
// Vite hands the proxy these options on every request, so a new
|
|
2151
|
+
// target set here takes effect on the next one.
|
|
2152
|
+
apiProxyOptions.add(proxyOptions)
|
|
2153
|
+
proxyOptions.target =
|
|
2154
|
+
environment.state?.proxyTarget ?? state.proxyTarget
|
|
2155
|
+
}),
|
|
2156
|
+
},
|
|
2157
|
+
}
|
|
2158
|
+
},
|
|
2159
|
+
|
|
2160
|
+
configResolved(config) {
|
|
2161
|
+
// Everything the plugin resolves, `didFile` and `outDir`, is documented
|
|
2162
|
+
// as relative to the project root, so it has to be Vite's resolved root
|
|
2163
|
+
// and not wherever the process started.
|
|
2164
|
+
projectRoot = config.root
|
|
2165
|
+
log = config.logger
|
|
2166
|
+
// A new lifetime: a build, or a dev server, a restart's included. Its
|
|
2167
|
+
// live checks start again (see `Lifetime`).
|
|
2168
|
+
current = lifetimeAfter(
|
|
2169
|
+
current,
|
|
2170
|
+
config.command === "build" || config.mode === "test"
|
|
2171
|
+
)
|
|
2172
|
+
byConfig.set(config, current)
|
|
2173
|
+
},
|
|
2174
|
+
|
|
2175
|
+
configureServer(server) {
|
|
2176
|
+
// The server's own lifetime: its watcher's saves, its overlay and its
|
|
2177
|
+
// close are its own, whatever server the restart that made it closes.
|
|
2178
|
+
const lifetime = byConfig.get(server.config) ?? current
|
|
2179
|
+
lifetime.server = server
|
|
2180
|
+
open.add(lifetime)
|
|
2181
|
+
|
|
2182
|
+
// Added here rather than returned as a post hook, so it runs before
|
|
2183
|
+
// Vite's own middlewares, which serve the page.
|
|
2184
|
+
if (localEnvironment) {
|
|
2185
|
+
const environment = localEnvironment
|
|
2186
|
+
server.middlewares.use(
|
|
2187
|
+
icEnvMiddleware(environment, (message) => log.warn(message))
|
|
2188
|
+
)
|
|
2189
|
+
// A page left open keeps the cookie it loaded with, and its calls
|
|
2190
|
+
// would reach the old IDs on a network started again (#881). Reloaded,
|
|
2191
|
+
// it loads without them. With `server.ws: false` the reload reaches no
|
|
2192
|
+
// page, and the warning does not say it did.
|
|
2193
|
+
const reloads =
|
|
2194
|
+
(server.config.server as { ws?: unknown } | undefined)?.ws !== false
|
|
2195
|
+
environment.watch((lost) => {
|
|
2196
|
+
server.ws.send({ type: "full-reload" })
|
|
2197
|
+
log.warn(lostNetworkWarning(lost.proxyTarget, reloads))
|
|
2198
|
+
})
|
|
2199
|
+
lifetime.stopper.signal.addEventListener(
|
|
2200
|
+
"abort",
|
|
2201
|
+
() => environment.close(),
|
|
2202
|
+
{ once: true }
|
|
2203
|
+
)
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
if (configured.length === 0) return
|
|
2207
|
+
|
|
2208
|
+
// Hand each browser that connects the failures not yet fixed. Guarded:
|
|
2209
|
+
// the peer range spans several Vite majors and `ws.on` is not present on
|
|
2210
|
+
// every one of them. Losing the replay is acceptable; throwing out of
|
|
2211
|
+
// configureServer is not.
|
|
2212
|
+
server.ws.on?.(
|
|
2213
|
+
"connection",
|
|
2214
|
+
(client?: { send?: (data: string) => void }) =>
|
|
2215
|
+
showOverlay(lifetime, client)
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
if (generated.length === 0) return
|
|
2219
|
+
// `.did` files are not in the module graph, so the watcher is told about
|
|
2220
|
+
// them. Regenerate from its own events and not from `handleHotUpdate`,
|
|
2221
|
+
// which Vite calls only for a file changed in place and only while HMR
|
|
2222
|
+
// is on: a `.did` created after startup, or written again by a build
|
|
2223
|
+
// tool or `git checkout`, arrives as an `add` event, and a deleted one
|
|
2224
|
+
// as `unlink`.
|
|
2225
|
+
server.watcher.add(generated.map(didPath))
|
|
2226
|
+
server.watcher.on("change", (file: string) => onDidSaved(lifetime, file))
|
|
2227
|
+
server.watcher.on("add", (file: string) => onDidSaved(lifetime, file))
|
|
2228
|
+
server.watcher.on("unlink", (file: string) =>
|
|
2229
|
+
onDidRemoved(lifetime, file)
|
|
2230
|
+
)
|
|
2231
|
+
},
|
|
2232
|
+
|
|
2233
|
+
// `vite preview` resolves the config with the `serve` command too, and
|
|
2234
|
+
// used to inherit the cookie from `server.headers`.
|
|
2235
|
+
configurePreviewServer(server) {
|
|
2236
|
+
if (localEnvironment) {
|
|
2237
|
+
const environment = localEnvironment
|
|
2238
|
+
server.middlewares.use(
|
|
2239
|
+
icEnvMiddleware(environment, (message) => log.warn(message))
|
|
2240
|
+
)
|
|
2241
|
+
// A preview server has no HMR connection to reload its pages with.
|
|
2242
|
+
environment.watch((lost) =>
|
|
2243
|
+
log.warn(lostNetworkWarning(lost.proxyTarget, false))
|
|
2244
|
+
)
|
|
2245
|
+
server.httpServer.once("close", () => environment.close())
|
|
2246
|
+
}
|
|
2247
|
+
},
|
|
2248
|
+
|
|
2249
|
+
async buildStart() {
|
|
2250
|
+
if (configured.length === 0) return
|
|
2251
|
+
// The lifetime of the build or server this start is for.
|
|
2252
|
+
const lifetime = lifetimeOf(this) ?? current
|
|
2253
|
+
open.add(lifetime)
|
|
2254
|
+
|
|
2255
|
+
// `vite build --watch` rebuilds when a file it watches changes, and a
|
|
2256
|
+
// `.did` file is never part of the module graph. Registered here, a save
|
|
2257
|
+
// starts a rebuild, and the rebuild's buildStart regenerates.
|
|
2258
|
+
for (const canister of generated) this.addWatchFile(didPath(canister))
|
|
2259
|
+
|
|
2260
|
+
const { signal } = lifetime.stopper
|
|
2261
|
+
// This lifetime's passes: a restart starts another lifetime before the
|
|
2262
|
+
// old server closes, so a fetch or a check of this run that ends after
|
|
2263
|
+
// it writes only to this lifetime's map.
|
|
2264
|
+
const passed = lifetime.verified
|
|
2265
|
+
const outcome = await serially(async () => {
|
|
2266
|
+
// Every canister that cannot be run fails first, before the run
|
|
2267
|
+
// fetches, writes, runs or requests anything for it: one whose
|
|
2268
|
+
// options hold a value that is not one, one whose module another
|
|
2269
|
+
// canister writes (see `collisions`), and one that a variable asks of
|
|
2270
|
+
// what it cannot do (see `readRequests`).
|
|
2271
|
+
const requests = readRequests()
|
|
2272
|
+
const problems = [
|
|
2273
|
+
...refusals,
|
|
2274
|
+
...[...collisions()].map(([canister, message]) => ({
|
|
2275
|
+
canister,
|
|
2276
|
+
message,
|
|
2277
|
+
})),
|
|
2278
|
+
...requests.failures,
|
|
2279
|
+
]
|
|
2280
|
+
const blocked = new Map<Generated | Refused, string>()
|
|
2281
|
+
for (const { canister, message } of problems) {
|
|
2282
|
+
if (!blocked.has(canister)) blocked.set(canister, message)
|
|
2283
|
+
}
|
|
2284
|
+
// A canister whose `.did` could not be fetched has nothing to
|
|
2285
|
+
// generate from: it fails with the reason, and the others generate.
|
|
2286
|
+
// So does one whose file lacks a name of its `methods`, and one that
|
|
2287
|
+
// failed its live check.
|
|
2288
|
+
const fetchedNow = await fetchMissing(
|
|
2289
|
+
lifetime,
|
|
2290
|
+
blocked,
|
|
2291
|
+
requests.fetches,
|
|
2292
|
+
signal
|
|
2293
|
+
)
|
|
2294
|
+
if (!fetchedNow) return undefined
|
|
2295
|
+
const { fetched } = fetchedNow
|
|
2296
|
+
// A refused fetch names every canister of its file, so it goes first.
|
|
2297
|
+
const failures = [...fetchedNow.failures, ...problems]
|
|
2298
|
+
// A file just fetched is the live interface: the text fetched passes
|
|
2299
|
+
// for the canisters that name it, and not the file read again, which
|
|
2300
|
+
// may have been saved since. A fetch that failed (or was refused) read
|
|
2301
|
+
// nothing, so what passed before is checked again: restoring it
|
|
2302
|
+
// cannot stand in for a fetch that showed another interface, or for
|
|
2303
|
+
// a check that cannot be made.
|
|
2304
|
+
for (const canister of generated) {
|
|
2305
|
+
const did = fetched.get(identify(didPath(canister)))
|
|
2306
|
+
if (did !== undefined) passed.set(canister.name, did)
|
|
2307
|
+
}
|
|
2308
|
+
for (const { canister } of failures) passed.delete(canister.name)
|
|
2309
|
+
// From here on, one reading of each file the run works on (see
|
|
2310
|
+
// `Run`): not of a blocked canister's, which it does nothing for.
|
|
2311
|
+
const working = generated.filter((canister) => !blocked.has(canister))
|
|
2312
|
+
const run = startRun(working)
|
|
2313
|
+
const remaining = () => {
|
|
2314
|
+
const failed = new Set(failures.map(({ canister }) => canister))
|
|
2315
|
+
return working.filter((canister) => !failed.has(canister))
|
|
2316
|
+
}
|
|
2317
|
+
const mismatched = await compareMethods(remaining(), run, signal)
|
|
2318
|
+
if (!mismatched) return undefined
|
|
2319
|
+
failures.push(...mismatched)
|
|
2320
|
+
const checking = remaining()
|
|
2321
|
+
const live = await verifyLive(
|
|
2322
|
+
checking,
|
|
2323
|
+
requests.checks,
|
|
2324
|
+
fetched,
|
|
2325
|
+
passed,
|
|
2326
|
+
run,
|
|
2327
|
+
signal
|
|
2328
|
+
)
|
|
2329
|
+
if (!live) return undefined
|
|
2330
|
+
// Only a check of the text the run read sets or clears a failure
|
|
2331
|
+
// that stands, whether it passed or failed (see `liveFailures`).
|
|
2332
|
+
for (const canister of checking) {
|
|
2333
|
+
if (!live.unsure.has(canister)) {
|
|
2334
|
+
lifetime.liveFailures.delete(canister.name)
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
for (const failure of live.failures) {
|
|
2338
|
+
if (!live.unsure.has(failure.canister as Generated)) {
|
|
2339
|
+
lifetime.liveFailures.set(failure.canister.name, failure)
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
failures.push(...live.failures)
|
|
2343
|
+
const generation = await generateNow(remaining(), false, run, signal)
|
|
2344
|
+
if (!generation) return undefined
|
|
2345
|
+
return {
|
|
2346
|
+
failures: dedupe([...failures, ...generation]),
|
|
2347
|
+
written: writtenSince(run, working),
|
|
2348
|
+
}
|
|
2349
|
+
})
|
|
2350
|
+
if (!outcome) return
|
|
2351
|
+
// A canister whose file was written while this run read it takes
|
|
2352
|
+
// nothing of the run (see `writtenSince`): none of the failures its
|
|
2353
|
+
// steps found, which may be of another text, and no success.
|
|
2354
|
+
const { written } = outcome
|
|
2355
|
+
const wasWritten = new Set<Generated | Refused>(written)
|
|
2356
|
+
const taken = configured.filter((canister) => !wasWritten.has(canister))
|
|
2357
|
+
let failures = outcome.failures.filter(
|
|
2358
|
+
({ canister }) => !wasWritten.has(canister)
|
|
2359
|
+
)
|
|
2360
|
+
if (lifetime.server) {
|
|
2361
|
+
// Queued again, as a save is: that run reports it.
|
|
2362
|
+
requeue(lifetime, written)
|
|
2363
|
+
} else {
|
|
2364
|
+
// A build fails it, for this reason alone.
|
|
2365
|
+
failures = [
|
|
2366
|
+
...failures,
|
|
2367
|
+
...written.map((canister) => ({
|
|
2368
|
+
canister,
|
|
2369
|
+
message:
|
|
2370
|
+
`${relativeToRoot(didPath(canister))} changed during the build, after the build read it: ` +
|
|
2371
|
+
`the steps that read it (listing its methods, the live check, generation) may each have read ` +
|
|
2372
|
+
`another text, so the build takes none of them. Build again.`,
|
|
2373
|
+
})),
|
|
2374
|
+
]
|
|
2375
|
+
}
|
|
2376
|
+
// Only an omitted `failOnError` takes the default (options.ts).
|
|
2377
|
+
if (
|
|
2378
|
+
failures.length > 0 &&
|
|
2379
|
+
(failOnError === undefined ? lifetime.failsByDefault : failOnError)
|
|
2380
|
+
) {
|
|
2381
|
+
// A build that exits 0 would ship, and a test run would test, whatever
|
|
2382
|
+
// stale bindings are still on disk, which no longer match the canister.
|
|
2383
|
+
this.error(describeFailures(failures))
|
|
2384
|
+
}
|
|
2385
|
+
// Under a dev server, nothing is said of a written canister here, not
|
|
2386
|
+
// even that it is fixed: the run queued for it reports it.
|
|
2387
|
+
publish(lifetime, lifetime.server ? taken : configured, failures)
|
|
2388
|
+
},
|
|
2389
|
+
|
|
2390
|
+
// The end of a build, and the close of a dev server, which Vite reports
|
|
2391
|
+
// here once for each of its environments: it stops the runs of that
|
|
2392
|
+
// build or server, and only those.
|
|
2393
|
+
closeBundle() {
|
|
2394
|
+
// In Vite 6 and later the environment names its config. Vite 4 and 5
|
|
2395
|
+
// have none, but call this once for a server's close, and a restart
|
|
2396
|
+
// closes the old server after the new one started: the lifetime that
|
|
2397
|
+
// closes is the oldest still open.
|
|
2398
|
+
const lifetime = lifetimeOf(this) ?? open.values().next().value ?? current
|
|
2399
|
+
open.delete(lifetime)
|
|
2400
|
+
lifetime.stopper.abort()
|
|
2401
|
+
lifetime.stopper = new AbortController()
|
|
2402
|
+
},
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
return plugin
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/**
|
|
2409
|
+
* The `server.proxy` entry that sends `/api` to `target`, or nothing when the
|
|
2410
|
+
* user's Vite config already proxies `/api`.
|
|
2411
|
+
*
|
|
2412
|
+
* Vite deep-merges the object a `config` hook returns over the user's config,
|
|
2413
|
+
* so an `/api` entry returned here replaced the user's own. A project that
|
|
2414
|
+
* pointed `/api` at its own replica (dfx's 4943, say) got the plugin's
|
|
2415
|
+
* fallback instead, and nothing reported the swap. Vite's merge skips an
|
|
2416
|
+
* undefined value, so returning nothing leaves the user's entry in place.
|
|
2417
|
+
*
|
|
2418
|
+
* `configure` receives the options object Vite builds the proxy from. Every
|
|
2419
|
+
* supported Vite major copies it for each request, so the target can follow
|
|
2420
|
+
* detection while the server runs.
|
|
2421
|
+
*/
|
|
2422
|
+
function apiProxy(
|
|
2423
|
+
userConfig: UserConfig,
|
|
2424
|
+
target: string,
|
|
2425
|
+
configure: (options: ProxyOptions) => void
|
|
2426
|
+
): Record<string, ProxyOptions> | undefined {
|
|
2427
|
+
if (userConfig.server?.proxy?.["/api"]) {
|
|
2428
|
+
debugLog(
|
|
2429
|
+
`The Vite config already proxies /api, so the plugin keeps that proxy instead of sending /api to ${target}.`
|
|
2430
|
+
)
|
|
2431
|
+
return undefined
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
return {
|
|
2435
|
+
"/api": {
|
|
2436
|
+
target,
|
|
2437
|
+
changeOrigin: true,
|
|
2438
|
+
configure: (_proxy, options) => configure(options),
|
|
2439
|
+
},
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* Warn at startup when detection is incomplete, and say what the plugin does
|
|
2445
|
+
* about it: it asks `icp` again on each page load until detection completes.
|
|
2446
|
+
*
|
|
2447
|
+
* Failing detection used to be indistinguishable from success: no cookie was
|
|
2448
|
+
* set, no warning was printed, and the app only broke later on an undefined
|
|
2449
|
+
* canister id.
|
|
2450
|
+
*/
|
|
2451
|
+
function warnAboutIncompleteDetection(
|
|
2452
|
+
state: LocalEnvironmentState,
|
|
2453
|
+
hasCanisters: boolean,
|
|
2454
|
+
ownsApiProxy: boolean
|
|
2455
|
+
): void {
|
|
2456
|
+
if (!state.environment) {
|
|
2457
|
+
// Env-only mode (no canisters configured) has nothing to inject.
|
|
2458
|
+
if (!hasCanisters) return
|
|
2459
|
+
const proxyNote = ownsApiProxy
|
|
2460
|
+
? ` and /api goes to ${state.proxyTarget} for now`
|
|
2461
|
+
: ""
|
|
2462
|
+
if (state.icpNotFound) {
|
|
2463
|
+
console.warn(
|
|
2464
|
+
`[ic-reactor] Could not run \`icp\`, so the local IC environment is not detected and no ic_env cookie is set${proxyNote}. ` +
|
|
2465
|
+
`No \`icp\` command was found. ${installIcpAdvice()} Then reload the page: the plugin tries again on each page load.`
|
|
2466
|
+
)
|
|
2467
|
+
return
|
|
2468
|
+
}
|
|
2469
|
+
console.warn(
|
|
2470
|
+
`[ic-reactor] Could not detect the local IC environment, so no ic_env cookie is set${proxyNote}. ` +
|
|
2471
|
+
`Is the local network running? The plugin asks \`icp\` again on each page load until it answers` +
|
|
2472
|
+
`${ownsApiProxy ? ", then sends /api to the network it reports" : ""}: start the network ` +
|
|
2473
|
+
`(\`icp network start\`) and reload the page. Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
2474
|
+
)
|
|
2475
|
+
return
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
// The network can be up while a configured canister has never been
|
|
2479
|
+
// deployed. Every `icp canister status <name>` then fails and that id is
|
|
2480
|
+
// absent, so the cookie carries a root key and no PUBLIC_CANISTER_ID for it,
|
|
2481
|
+
// which is the same silent failure. Only configured canisters count, and one
|
|
2482
|
+
// with a configured `canisterId` is resolved.
|
|
2483
|
+
const missing = state.missingCanisterIds
|
|
2484
|
+
if (missing.length > 0) {
|
|
2485
|
+
const it = missing.length === 1 ? "it" : "them"
|
|
2486
|
+
console.warn(
|
|
2487
|
+
`[ic-reactor] The local replica is running, but no canister ID could be resolved for ${quoteNames(missing)}. ` +
|
|
2488
|
+
`Until one is, the ic_env cookie carries no PUBLIC_CANISTER_ID for ${it} and the app will see an ` +
|
|
2489
|
+
`undefined canister id. Deploy ${it} (\`icp deploy\`) and reload the page: the plugin asks \`icp\` ` +
|
|
2490
|
+
`again on each page load until every configured canister has an ID. ` +
|
|
2491
|
+
`Re-run with DEBUG=ic-reactor to see the \`icp\` output.`
|
|
2492
|
+
)
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
/**
|
|
2497
|
+
* Report what a detection after startup found that the one before had not.
|
|
2498
|
+
*
|
|
2499
|
+
* @param followsApiProxy - Whether the `/api` proxy moves with detection. It
|
|
2500
|
+
* does not when the Vite config or another plugin set its target.
|
|
2501
|
+
*/
|
|
2502
|
+
function reportDetectionProgress(
|
|
2503
|
+
previous: LocalEnvironmentState,
|
|
2504
|
+
next: LocalEnvironmentState,
|
|
2505
|
+
followsApiProxy: boolean
|
|
2506
|
+
): void {
|
|
2507
|
+
if (!previous.environment && next.environment) {
|
|
2508
|
+
console.log(
|
|
2509
|
+
`[ic-reactor] Detected the local IC network: the ic_env cookie now carries its root key` +
|
|
2510
|
+
(followsApiProxy ? ` and /api goes to ${next.proxyTarget}.` : ".")
|
|
2511
|
+
)
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const resolved = previous.missingCanisterIds.filter(
|
|
2515
|
+
(name) => !next.missingCanisterIds.includes(name)
|
|
2516
|
+
)
|
|
2517
|
+
if (next.environment && resolved.length > 0) {
|
|
2518
|
+
console.log(
|
|
2519
|
+
`[ic-reactor] The ic_env cookie now carries the canister ID${
|
|
2520
|
+
resolved.length === 1 ? "" : "s"
|
|
2521
|
+
} for ${quoteNames(resolved)}.`
|
|
2522
|
+
)
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
if (next.complete && !previous.complete) {
|
|
2526
|
+
console.log(
|
|
2527
|
+
"[ic-reactor] Every configured canister has an ID, so page loads no longer run `icp`. " +
|
|
2528
|
+
"Restart the dev server after the network restarts."
|
|
2529
|
+
)
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/**
|
|
2534
|
+
* The warning for a detected network that stopped, at `target`. `reloaded`:
|
|
2535
|
+
* the dev server reloaded the pages it has open.
|
|
2536
|
+
*/
|
|
2537
|
+
function lostNetworkWarning(target: string, reloaded: boolean): string {
|
|
2538
|
+
return (
|
|
2539
|
+
`[ic-reactor] The local network at ${target} stopped. ` +
|
|
2540
|
+
`A network started again can give the canister IDs this server detected to other canisters under the same root key, ` +
|
|
2541
|
+
`so page loads now get the ic_env cookie without those IDs and that root key` +
|
|
2542
|
+
(reloaded
|
|
2543
|
+
? ", and the pages open on this server were reloaded to drop the old cookie"
|
|
2544
|
+
: "") +
|
|
2545
|
+
`. A { name } canister does not resolve until you deploy again (\`icp network start\`, \`icp deploy\`) and restart this server. (Logged once.)`
|
|
2546
|
+
)
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
/**
|
|
2550
|
+
* `icp` failing to answer is routine — no local replica, or a canister that has
|
|
2551
|
+
* never been deployed — so its stderr is noise until someone is actually
|
|
2552
|
+
* debugging. Gate it the way Vite gates its own debug output.
|
|
2553
|
+
*/
|
|
2554
|
+
function debugLog(message: string): void {
|
|
2555
|
+
const enabled = (process.env.DEBUG ?? "").split(",").some((entry) => {
|
|
2556
|
+
const scope = entry.trim()
|
|
2557
|
+
return (
|
|
2558
|
+
scope === "*" || scope === "ic-reactor" || scope.startsWith("ic-reactor:")
|
|
2559
|
+
)
|
|
2560
|
+
})
|
|
2561
|
+
|
|
2562
|
+
if (enabled) {
|
|
2563
|
+
console.debug(`[ic-reactor:debug] ${message}`)
|
|
2564
|
+
}
|
|
2565
|
+
}
|