@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.
Files changed (1041) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +108 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +77 -0
  4. package/.github/actions/setup-rust-wasm/action.yml +25 -0
  5. package/.github/dependabot.yml +107 -0
  6. package/.github/workflows/auth-latest.yml +67 -0
  7. package/.github/workflows/b3pay-site.yml +49 -0
  8. package/.github/workflows/ci.yml +430 -0
  9. package/.github/workflows/codeql.yml +41 -0
  10. package/.github/workflows/dependency-review.yml +68 -0
  11. package/.github/workflows/docs-check.yml +98 -0
  12. package/.github/workflows/docs.yml +265 -0
  13. package/.github/workflows/e2e.yml +90 -0
  14. package/.github/workflows/examples-published.yml +97 -0
  15. package/.github/workflows/release.yml +326 -0
  16. package/.husky/pre-commit +12 -0
  17. package/.prettierignore +109 -0
  18. package/.prettierrc +6 -0
  19. package/AGENTS.md +103 -0
  20. package/CHANGELOG.md +3268 -0
  21. package/CLAUDE.md +93 -0
  22. package/CNAME +1 -0
  23. package/CODE_OF_CONDUCT.md +7 -0
  24. package/CONTRIBUTING.md +433 -0
  25. package/LICENSE +21 -0
  26. package/README.md +2 -2
  27. package/SECURITY.md +74 -0
  28. package/b3pay-module.json +194 -0
  29. package/docs/.remarkrc.json +10 -0
  30. package/docs/README.md +64 -0
  31. package/docs/astro.config.mjs +205 -0
  32. package/docs/favicon.svg +15 -0
  33. package/docs/package.json +39 -0
  34. package/docs/public/favicon.svg +15 -0
  35. package/docs/public/ic-reactor-social-card.png +0 -0
  36. package/docs/scripts/b3pay-static.mjs +22 -0
  37. package/docs/scripts/check-example-docs.mjs +214 -0
  38. package/docs/scripts/check-links.mjs +212 -0
  39. package/docs/scripts/fix-dist-links.js +125 -0
  40. package/docs/scripts/fix-frontmatter.js +71 -0
  41. package/docs/scripts/remove-redundant-h1.cjs +48 -0
  42. package/docs/scripts/rewrite-typedoc-links.js +110 -0
  43. package/docs/scripts/split-index.js +69 -0
  44. package/docs/src/assets/ic-reactor-hero-dark.svg +23 -0
  45. package/docs/src/assets/ic-reactor-hero-light.svg +23 -0
  46. package/docs/src/assets/ic-reactor-lockup-dark.svg +15 -0
  47. package/docs/src/assets/ic-reactor-lockup-light.svg +15 -0
  48. package/docs/src/assets/ic-reactor-mark-dark.svg +14 -0
  49. package/docs/src/assets/ic-reactor-mark-light.svg +14 -0
  50. package/docs/src/assets/layers/candid-core-layer-icon-dark.svg +4 -0
  51. package/docs/src/assets/layers/candid-core-layer-icon-light.svg +4 -0
  52. package/docs/src/assets/layers/candid-profile-layer-icon-dark.svg +4 -0
  53. package/docs/src/assets/layers/candid-profile-layer-icon-light.svg +4 -0
  54. package/docs/src/assets/layers/ic-reactor-layer-icon-dark.svg +4 -0
  55. package/docs/src/assets/layers/ic-reactor-layer-icon-light.svg +4 -0
  56. package/docs/src/components/LibraryCards.astro +172 -0
  57. package/docs/src/components/LibraryIcon.astro +28 -0
  58. package/docs/src/components/SiteTitle.astro +241 -0
  59. package/docs/src/content/docs/404.mdx +19 -0
  60. package/docs/src/content/docs/build/agent-tools.mdx +593 -0
  61. package/docs/src/content/docs/build/icrc1-wallet.mdx +619 -0
  62. package/docs/src/content/docs/build/ssr-per-user.mdx +552 -0
  63. package/docs/src/content/docs/examples/canister-studio.mdx +95 -0
  64. package/docs/src/content/docs/examples/icrc-ledger.mdx +45 -0
  65. package/docs/src/content/docs/examples/index.mdx +38 -0
  66. package/docs/src/content/docs/examples/next-ssr.mdx +73 -0
  67. package/docs/src/content/docs/examples/node-agent-tool.mdx +80 -0
  68. package/docs/src/content/docs/examples/vite-wallet.mdx +71 -0
  69. package/docs/src/content/docs/guides/auth.mdx +1181 -0
  70. package/docs/src/content/docs/guides/client.mdx +551 -0
  71. package/docs/src/content/docs/guides/deploy.mdx +180 -0
  72. package/docs/src/content/docs/guides/errors.mdx +475 -0
  73. package/docs/src/content/docs/guides/getting-started.mdx +620 -0
  74. package/docs/src/content/docs/guides/reads.mdx +586 -0
  75. package/docs/src/content/docs/guides/ssr.mdx +529 -0
  76. package/docs/src/content/docs/guides/testing.mdx +401 -0
  77. package/docs/src/content/docs/guides/values.mdx +261 -0
  78. package/docs/src/content/docs/guides/vite-plugin.mdx +690 -0
  79. package/docs/src/content/docs/guides/writes.mdx +572 -0
  80. package/docs/src/content/docs/ic-reactor/index.mdx +167 -0
  81. package/docs/src/content/docs/index.mdx +65 -0
  82. package/docs/src/content/docs/migrating-from-3.mdx +535 -0
  83. package/docs/src/content/docs/packages/core.mdx +186 -0
  84. package/docs/src/content/docs/packages/mcp.mdx +163 -0
  85. package/docs/src/content/docs/packages/react.mdx +207 -0
  86. package/docs/src/content/docs/packages/vite-plugin.mdx +73 -0
  87. package/docs/src/content/docs/versioning.mdx +104 -0
  88. package/docs/src/content.config.ts +7 -0
  89. package/docs/src/styles/custom.css +189 -0
  90. package/docs/tsconfig.typedoc.json +26 -0
  91. package/docs/typedoc-entry.ts +17 -0
  92. package/docs/typedoc.json +37 -0
  93. package/e2e/Cargo.lock +646 -0
  94. package/e2e/Cargo.toml +3 -0
  95. package/e2e/README.md +101 -0
  96. package/e2e/frontends/build.mjs +94 -0
  97. package/e2e/frontends/index.html +10 -0
  98. package/e2e/global-setup.ts +65 -0
  99. package/e2e/icp.yaml +38 -0
  100. package/e2e/package.json +40 -0
  101. package/e2e/setup.ts +6 -0
  102. package/e2e/src/actor/Cargo.toml +14 -0
  103. package/e2e/src/actor/hello_actor.did +10 -0
  104. package/e2e/src/actor/src/lib.rs +73 -0
  105. package/e2e/src/calls.test.ts +132 -0
  106. package/e2e/src/declarations/hello_actor.envelope.json +175 -0
  107. package/e2e/src/declarations/hello_actor.ts +15 -0
  108. package/e2e/src/errors.test.ts +107 -0
  109. package/e2e/src/frontend-headers.test.ts +253 -0
  110. package/e2e/src/identity.test.ts +61 -0
  111. package/e2e/src/react.test.tsx +59 -0
  112. package/e2e/src/replica.ts +70 -0
  113. package/e2e/src/vite-env.test.ts +108 -0
  114. package/e2e/test.sh +47 -0
  115. package/e2e/tsconfig.json +20 -0
  116. package/e2e/tsconfig.typecheck.json +9 -0
  117. package/e2e/vitest.config.ts +22 -0
  118. package/eslint.config.mjs +100 -0
  119. package/evals/PREREGISTRATION.md +1346 -0
  120. package/evals/README.md +756 -0
  121. package/evals/conditions/thin/condition.json +8 -0
  122. package/evals/conditions/thin/docs/candid-core-cli.README.md +90 -0
  123. package/evals/conditions/thin/docs/candid-core-schema.README.md +339 -0
  124. package/evals/conditions/thin/package.json +25 -0
  125. package/evals/conditions/thin/starter/src/generated/icrc1.ts +57 -0
  126. package/evals/conditions/thin-guide/condition.json +10 -0
  127. package/evals/conditions/thin-guide/docs/llms.txt +292 -0
  128. package/evals/conditions/v3/condition.json +9 -0
  129. package/evals/conditions/v3/docs/ic-reactor-core.llms.txt +232 -0
  130. package/evals/conditions/v3/docs/ic-reactor-react.llms.txt +280 -0
  131. package/evals/conditions/v3/docs/llms-full.txt +1053 -0
  132. package/evals/conditions/v3/package.json +26 -0
  133. package/evals/conditions/v3/starter/src/declarations/icrc1.did.d.ts +95 -0
  134. package/evals/conditions/v3/starter/src/declarations/icrc1.did.js +140 -0
  135. package/evals/conditions/v4/PORTING.md +384 -0
  136. package/evals/conditions/v4/README.md +106 -0
  137. package/evals/conditions/v4/condition.json +5 -0
  138. package/evals/conditions/v4/docs/llms.txt +323 -0
  139. package/evals/conditions/v4/package.json +27 -0
  140. package/evals/conditions/v4/starter/src/generated/icrc1.ts +78 -0
  141. package/evals/conditions/v4-proto/condition.json +6 -0
  142. package/evals/conditions/v4-proto/docs/llms.txt +291 -0
  143. package/evals/conditions/v4-proto/lib/SHIM-NOTES.md +20 -0
  144. package/evals/conditions/v4-proto/lib/package.json +27 -0
  145. package/evals/conditions/v4-proto/lib/src/client.ts +471 -0
  146. package/evals/conditions/v4-proto/lib/src/errors.ts +214 -0
  147. package/evals/conditions/v4-proto/lib/src/index.ts +36 -0
  148. package/evals/conditions/v4-proto/lib/src/principal.ts +175 -0
  149. package/evals/conditions/v4-proto/lib/src/service.ts +69 -0
  150. package/evals/conditions/v4-proto/lib/src/types.ts +174 -0
  151. package/evals/conditions/v4-proto/lib/test/runtime.test.ts +170 -0
  152. package/evals/conditions/v4-proto/lib/test/tsconfig.json +10 -0
  153. package/evals/conditions/v4-proto/lib/test/types.test-d.ts +145 -0
  154. package/evals/conditions/v4-proto/lib/test/vitest.config.mjs +8 -0
  155. package/evals/conditions/v4-proto/lib/tsconfig.json +17 -0
  156. package/evals/conditions/v4-proto/package.json +26 -0
  157. package/evals/conditions/v4-proto/starter/src/generated/icrc1.service.ts +19 -0
  158. package/evals/conditions/v4-proto/starter/src/generated/icrc1.ts +57 -0
  159. package/evals/drive.mjs +1221 -0
  160. package/evals/gate.mjs +161 -0
  161. package/evals/harness/aggregate.mjs +534 -0
  162. package/evals/harness/aggregate.test.mjs +344 -0
  163. package/evals/harness/assemble.mjs +216 -0
  164. package/evals/harness/assemble.test.mjs +143 -0
  165. package/evals/harness/auth.mjs +91 -0
  166. package/evals/harness/check-docs.mjs +157 -0
  167. package/evals/harness/declarations/icrc1.did.d.ts +95 -0
  168. package/evals/harness/declarations/icrc1.did.js +140 -0
  169. package/evals/harness/drive-auth.test.mjs +584 -0
  170. package/evals/harness/drive.test.mjs +509 -0
  171. package/evals/harness/fake-auth.ts +67 -0
  172. package/evals/harness/fake-ledger.ts +178 -0
  173. package/evals/harness/gate-plan.mjs +104 -0
  174. package/evals/harness/gate-plan.test.mjs +243 -0
  175. package/evals/harness/icrc1.did +88 -0
  176. package/evals/harness/judge.mjs +63 -0
  177. package/evals/harness/judge.test.mjs +81 -0
  178. package/evals/harness/leak-scan.mjs +1373 -0
  179. package/evals/harness/leak-scan.test.mjs +1300 -0
  180. package/evals/harness/probes/amounts.ts +14 -0
  181. package/evals/harness/probes/node-tool.probe.ts +48 -0
  182. package/evals/harness/probes/probe.mjs +149 -0
  183. package/evals/harness/probes/react-wallet.probe.ts +96 -0
  184. package/evals/harness/public/support/auth.ts +38 -0
  185. package/evals/harness/public/support/fake-replica.d.ts +28 -0
  186. package/evals/harness/public/support/fake-replica.js +434 -0
  187. package/evals/harness/public/support/ledger.idl.d.ts +95 -0
  188. package/evals/harness/public/support/ledger.idl.js +140 -0
  189. package/evals/harness/public/support/world.ts +179 -0
  190. package/evals/harness/runs.mjs +139 -0
  191. package/evals/harness/sandbox.mjs +88 -0
  192. package/evals/harness/sandbox.test.mjs +134 -0
  193. package/evals/harness/ship.mjs +264 -0
  194. package/evals/harness/ship.test.mjs +552 -0
  195. package/evals/harness/world.ts +291 -0
  196. package/evals/package.json +26 -0
  197. package/evals/pnpm-lock.yaml +1537 -0
  198. package/evals/pnpm-workspace.yaml +12 -0
  199. package/evals/results/2026-09-30T14-22-05-924Z/pilot.json +384 -0
  200. package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.json +404 -0
  201. package/evals/results/2026-09-30T14-22-05-924Z/pilot.rescanned.rescored.json +404 -0
  202. package/evals/results/2026-09-30T14-22-05-924Z/plan.json +240 -0
  203. package/evals/results/2026-09-30T14-22-05-924Z/summary.json +1800 -0
  204. package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.json +1843 -0
  205. package/evals/results/2026-09-30T14-22-05-924Z/summary.rescanned.rescored.json +2100 -0
  206. package/evals/results/2026-09-30T15-48-23-343Z/pilot.json +404 -0
  207. package/evals/results/2026-09-30T15-48-23-343Z/pilot.rescored.json +404 -0
  208. package/evals/results/2026-09-30T15-48-23-343Z/plan.json +242 -0
  209. package/evals/results/2026-09-30T15-48-23-343Z/summary.json +1843 -0
  210. package/evals/results/2026-09-30T15-48-23-343Z/summary.rescored.json +2195 -0
  211. package/evals/results/2026-10-03-beta1-gate/pilot.json +162 -0
  212. package/evals/results/2026-10-03-beta1-gate/plan.json +141 -0
  213. package/evals/results/2026-10-03-beta1-gate/summary.json +983 -0
  214. package/evals/results/2026-10-05-ga-gate/contaminated-run.json +55 -0
  215. package/evals/results/2026-10-05-ga-gate/pilot.json +162 -0
  216. package/evals/results/2026-10-05-ga-gate/plan.json +457 -0
  217. package/evals/results/2026-10-05-ga-gate/summary.json +1037 -0
  218. package/evals/results/2026-10-05-ga-gate/summary.rescanned.json +1037 -0
  219. package/evals/results/README.md +31 -0
  220. package/evals/runs-pilot.log +150 -0
  221. package/evals/runs-pilot2.log +150 -0
  222. package/evals/score.mjs +256 -0
  223. package/evals/setup.mjs +547 -0
  224. package/evals/tasks/node-tool/faulty/thin-accepts-exponent/meta.json +5 -0
  225. package/evals/tasks/node-tool/faulty/thin-accepts-exponent/src/index.ts +121 -0
  226. package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/meta.json +5 -0
  227. package/evals/tasks/node-tool/faulty/thin-anonymous-transfer/src/index.ts +116 -0
  228. package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/meta.json +9 -0
  229. package/evals/tasks/node-tool/faulty/thin-every-reject-may-have-executed/src/index.ts +118 -0
  230. package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/meta.json +5 -0
  231. package/evals/tasks/node-tool/faulty/thin-fetches-root-key-off-local/src/index.ts +118 -0
  232. package/evals/tasks/node-tool/faulty/thin-ignores-root-key/meta.json +5 -0
  233. package/evals/tasks/node-tool/faulty/thin-ignores-root-key/src/index.ts +117 -0
  234. package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/meta.json +9 -0
  235. package/evals/tasks/node-tool/faulty/thin-misclassifies-reject-codes/src/index.ts +119 -0
  236. package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/meta.json +9 -0
  237. package/evals/tasks/node-tool/faulty/thin-no-nat64-cap/src/index.ts +118 -0
  238. package/evals/tasks/node-tool/faulty/thin-number-amount/meta.json +10 -0
  239. package/evals/tasks/node-tool/faulty/thin-number-amount/src/index.ts +117 -0
  240. package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/meta.json +5 -0
  241. package/evals/tasks/node-tool/faulty/thin-truncates-fraction-digits/src/index.ts +120 -0
  242. package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/meta.json +5 -0
  243. package/evals/tasks/node-tool/faulty/thin-unvalidated-recipient/src/index.ts +118 -0
  244. package/evals/tasks/node-tool/faulty/v3-retry-call-errors/meta.json +11 -0
  245. package/evals/tasks/node-tool/faulty/v3-retry-call-errors/src/index.ts +149 -0
  246. package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/meta.json +5 -0
  247. package/evals/tasks/node-tool/faulty/v4-anonymous-identity-sent/src/index.ts +71 -0
  248. package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/meta.json +9 -0
  249. package/evals/tasks/node-tool/faulty/v4-never-may-have-executed/src/index.ts +84 -0
  250. package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/meta.json +9 -0
  251. package/evals/tasks/node-tool/faulty/v4-proto-never-may-have-executed/src/index.ts +72 -0
  252. package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/meta.json +5 -0
  253. package/evals/tasks/node-tool/faulty/v4-proto-refuses-nat64-max/src/index.ts +72 -0
  254. package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/meta.json +5 -0
  255. package/evals/tasks/node-tool/faulty/v4-refuses-nat64-max/src/index.ts +84 -0
  256. package/evals/tasks/node-tool/hidden/node-tool.test.ts +312 -0
  257. package/evals/tasks/node-tool/prompt.md +55 -0
  258. package/evals/tasks/node-tool/prompt.minimal.md +30 -0
  259. package/evals/tasks/node-tool/solutions/thin/reference/src/index.ts +118 -0
  260. package/evals/tasks/node-tool/solutions/thin/reference-module-scope/src/index.ts +145 -0
  261. package/evals/tasks/node-tool/solutions/thin-guide/reference/src/index.ts +118 -0
  262. package/evals/tasks/node-tool/solutions/thin-guide/reference-module-scope/src/index.ts +145 -0
  263. package/evals/tasks/node-tool/solutions/v3/reference/src/index.ts +140 -0
  264. package/evals/tasks/node-tool/solutions/v3/reference-module-scope/src/index.ts +153 -0
  265. package/evals/tasks/node-tool/solutions/v4/reference/src/index.ts +84 -0
  266. package/evals/tasks/node-tool/solutions/v4/reference-module-scope/src/index.ts +105 -0
  267. package/evals/tasks/node-tool/solutions/v4-proto/reference/src/index.ts +72 -0
  268. package/evals/tasks/node-tool/solutions/v4-proto/reference-module-scope/src/index.ts +87 -0
  269. package/evals/tasks/node-tool/starter/src/contract.ts +31 -0
  270. package/evals/tasks/node-tool/starter/src/index.ts +7 -0
  271. package/evals/tasks/node-tool/starter/test/smoke.test.ts +22 -0
  272. package/evals/tasks/node-tool/starter/tsconfig.json +17 -0
  273. package/evals/tasks/node-tool/task.json +63 -0
  274. package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/meta.json +5 -0
  275. package/evals/tasks/react-wallet/faulty/thin-accepts-exponent/src/Wallet.tsx +216 -0
  276. package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/meta.json +5 -0
  277. package/evals/tasks/react-wallet/faulty/thin-anonymous-transfer/src/Wallet.tsx +212 -0
  278. package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/meta.json +5 -0
  279. package/evals/tasks/react-wallet/faulty/thin-both-auth-buttons/src/Wallet.tsx +210 -0
  280. package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/meta.json +8 -0
  281. package/evals/tasks/react-wallet/faulty/thin-constant-balance-key/src/Wallet.tsx +213 -0
  282. package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/meta.json +9 -0
  283. package/evals/tasks/react-wallet/faulty/thin-no-nat64-cap/src/Wallet.tsx +213 -0
  284. package/evals/tasks/react-wallet/faulty/thin-number-balance/meta.json +5 -0
  285. package/evals/tasks/react-wallet/faulty/thin-number-balance/src/Wallet.tsx +213 -0
  286. package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/meta.json +5 -0
  287. package/evals/tasks/react-wallet/faulty/thin-transfer-as-query/src/Wallet.tsx +254 -0
  288. package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/meta.json +5 -0
  289. package/evals/tasks/react-wallet/faulty/thin-truncates-fraction-digits/src/Wallet.tsx +215 -0
  290. package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/meta.json +5 -0
  291. package/evals/tasks/react-wallet/faulty/thin-unguarded-balance-effect/src/Wallet.tsx +214 -0
  292. package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/meta.json +5 -0
  293. package/evals/tasks/react-wallet/faulty/v3-http-4xx-unknown/src/Wallet.tsx +224 -0
  294. package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/meta.json +12 -0
  295. package/evals/tasks/react-wallet/faulty/v3-mutation-retry-3/src/Wallet.tsx +225 -0
  296. package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/meta.json +11 -0
  297. package/evals/tasks/react-wallet/faulty/v3-unknown-as-error/src/Wallet.tsx +182 -0
  298. package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/meta.json +5 -0
  299. package/evals/tasks/react-wallet/faulty/v4-every-reject-unknown/src/Wallet.tsx +160 -0
  300. package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/meta.json +5 -0
  301. package/evals/tasks/react-wallet/faulty/v4-keep-previous-data/src/Wallet.tsx +166 -0
  302. package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/meta.json +5 -0
  303. package/evals/tasks/react-wallet/faulty/v4-proto-every-reject-unknown/src/Wallet.tsx +161 -0
  304. package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/meta.json +5 -0
  305. package/evals/tasks/react-wallet/faulty/v4-proto-keep-previous-data/src/Wallet.tsx +163 -0
  306. package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/meta.json +12 -0
  307. package/evals/tasks/react-wallet/faulty/v4-proto-retry-spread/src/Wallet.tsx +162 -0
  308. package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/meta.json +5 -0
  309. package/evals/tasks/react-wallet/faulty/v4-proto-status-not-idle/src/Wallet.tsx +163 -0
  310. package/evals/tasks/react-wallet/faulty/v4-retry-spread/meta.json +12 -0
  311. package/evals/tasks/react-wallet/faulty/v4-retry-spread/src/Wallet.tsx +163 -0
  312. package/evals/tasks/react-wallet/faulty/v4-status-not-idle/meta.json +5 -0
  313. package/evals/tasks/react-wallet/faulty/v4-status-not-idle/src/Wallet.tsx +162 -0
  314. package/evals/tasks/react-wallet/hidden/react-wallet.test.ts +536 -0
  315. package/evals/tasks/react-wallet/prompt.md +88 -0
  316. package/evals/tasks/react-wallet/prompt.minimal.md +51 -0
  317. package/evals/tasks/react-wallet/solutions/thin/reference/src/Wallet.tsx +213 -0
  318. package/evals/tasks/react-wallet/solutions/thin/reference-module-scope/src/Wallet.tsx +227 -0
  319. package/evals/tasks/react-wallet/solutions/thin-guide/reference/src/Wallet.tsx +213 -0
  320. package/evals/tasks/react-wallet/solutions/thin-guide/reference-module-scope/src/Wallet.tsx +227 -0
  321. package/evals/tasks/react-wallet/solutions/v3/reference/src/Wallet.tsx +224 -0
  322. package/evals/tasks/react-wallet/solutions/v3/reference-module-scope/src/Wallet.tsx +230 -0
  323. package/evals/tasks/react-wallet/solutions/v4/reference/src/Wallet.tsx +160 -0
  324. package/evals/tasks/react-wallet/solutions/v4/reference-module-scope/src/Wallet.tsx +178 -0
  325. package/evals/tasks/react-wallet/solutions/v4-proto/reference/src/Wallet.tsx +161 -0
  326. package/evals/tasks/react-wallet/solutions/v4-proto/reference-module-scope/src/Wallet.tsx +161 -0
  327. package/evals/tasks/react-wallet/starter/src/Wallet.tsx +12 -0
  328. package/evals/tasks/react-wallet/starter/src/auth.ts +16 -0
  329. package/evals/tasks/react-wallet/starter/src/config.ts +14 -0
  330. package/evals/tasks/react-wallet/starter/src/ii-auth.ts +43 -0
  331. package/evals/tasks/react-wallet/starter/src/main.tsx +15 -0
  332. package/evals/tasks/react-wallet/starter/test/smoke.test.ts +34 -0
  333. package/evals/tasks/react-wallet/starter/tsconfig.json +18 -0
  334. package/evals/tasks/react-wallet/task.json +84 -0
  335. package/examples/canister-studio/.stackblitzrc +4 -0
  336. package/examples/canister-studio/README.md +183 -0
  337. package/examples/canister-studio/did/ckbtc_minter.did +806 -0
  338. package/examples/canister-studio/did/icp_ledger.did +586 -0
  339. package/examples/canister-studio/did/icrc1.did +88 -0
  340. package/examples/canister-studio/package.json +45 -0
  341. package/examples/canister-studio/scripts/smoke.ts +188 -0
  342. package/examples/canister-studio/src/calls/CacheKeyPanel.tsx +104 -0
  343. package/examples/canister-studio/src/calls/ConfirmWrite.tsx +265 -0
  344. package/examples/canister-studio/src/calls/ErrorPanel.test.tsx +113 -0
  345. package/examples/canister-studio/src/calls/ErrorPanel.tsx +214 -0
  346. package/examples/canister-studio/src/calls/MethodRunner.test.tsx +354 -0
  347. package/examples/canister-studio/src/calls/MethodRunner.tsx +57 -0
  348. package/examples/canister-studio/src/calls/ReadRunner.tsx +244 -0
  349. package/examples/canister-studio/src/calls/ResendPanel.tsx +165 -0
  350. package/examples/canister-studio/src/calls/ShareLink.tsx +59 -0
  351. package/examples/canister-studio/src/calls/WriteRunner.test.tsx +678 -0
  352. package/examples/canister-studio/src/calls/WriteRunner.tsx +670 -0
  353. package/examples/canister-studio/src/calls/dedupe.ts +189 -0
  354. package/examples/canister-studio/src/calls/error-info.ts +168 -0
  355. package/examples/canister-studio/src/calls/history.test.ts +284 -0
  356. package/examples/canister-studio/src/calls/history.ts +287 -0
  357. package/examples/canister-studio/src/calls/kinds.ts +68 -0
  358. package/examples/canister-studio/src/calls/prepare.ts +32 -0
  359. package/examples/canister-studio/src/calls/query-options.ts +30 -0
  360. package/examples/canister-studio/src/calls/read.ts +47 -0
  361. package/examples/canister-studio/src/calls/readback.ts +100 -0
  362. package/examples/canister-studio/src/calls/record.ts +123 -0
  363. package/examples/canister-studio/src/calls/safe-calls.test.ts +299 -0
  364. package/examples/canister-studio/src/calls/share.ts +46 -0
  365. package/examples/canister-studio/src/calls/testing/doubles.tsx +108 -0
  366. package/examples/canister-studio/src/calls/testing/fixtures.ts +185 -0
  367. package/examples/canister-studio/src/calls/useCanisterHandle.ts +35 -0
  368. package/examples/canister-studio/src/canisters/ckbtc_minter.envelope.json +4228 -0
  369. package/examples/canister-studio/src/canisters/ckbtc_minter.ts +868 -0
  370. package/examples/canister-studio/src/canisters/icp_ledger.envelope.json +3454 -0
  371. package/examples/canister-studio/src/canisters/icp_ledger.ts +584 -0
  372. package/examples/canister-studio/src/canisters/icrc1.envelope.json +840 -0
  373. package/examples/canister-studio/src/canisters/icrc1.ts +78 -0
  374. package/examples/canister-studio/src/components/ActionButton.tsx +32 -0
  375. package/examples/canister-studio/src/components/AppHeader.tsx +83 -0
  376. package/examples/canister-studio/src/components/AppShell.tsx +29 -0
  377. package/examples/canister-studio/src/components/AuthButton.tsx +79 -0
  378. package/examples/canister-studio/src/components/Badge.tsx +79 -0
  379. package/examples/canister-studio/src/components/CanisterHeader.tsx +200 -0
  380. package/examples/canister-studio/src/components/CanisterJump.tsx +130 -0
  381. package/examples/canister-studio/src/components/Card.tsx +64 -0
  382. package/examples/canister-studio/src/components/Code.tsx +101 -0
  383. package/examples/canister-studio/src/components/CopyButton.tsx +126 -0
  384. package/examples/canister-studio/src/components/Disclosure.tsx +34 -0
  385. package/examples/canister-studio/src/components/EmptyState.tsx +34 -0
  386. package/examples/canister-studio/src/components/MethodList.test.tsx +137 -0
  387. package/examples/canister-studio/src/components/MethodList.tsx +244 -0
  388. package/examples/canister-studio/src/components/Notice.tsx +36 -0
  389. package/examples/canister-studio/src/components/ProvenanceBadge.tsx +44 -0
  390. package/examples/canister-studio/src/components/RouteError.tsx +42 -0
  391. package/examples/canister-studio/src/components/SectionBoundary.tsx +76 -0
  392. package/examples/canister-studio/src/components/Spinner.tsx +33 -0
  393. package/examples/canister-studio/src/components/Tabs.tsx +96 -0
  394. package/examples/canister-studio/src/components/canister/CanisterLoadFailure.test.tsx +56 -0
  395. package/examples/canister-studio/src/components/canister/CanisterLoadFailure.tsx +165 -0
  396. package/examples/canister-studio/src/components/canister/CanisterNotFound.tsx +59 -0
  397. package/examples/canister-studio/src/components/canister/CanisterPending.tsx +33 -0
  398. package/examples/canister-studio/src/components/canister/MethodOverview.tsx +93 -0
  399. package/examples/canister-studio/src/components/canister/MethodWorkspace.test.tsx +201 -0
  400. package/examples/canister-studio/src/components/canister/MethodWorkspace.tsx +187 -0
  401. package/examples/canister-studio/src/components/canister/shared-call.test.ts +186 -0
  402. package/examples/canister-studio/src/components/canister/shared-call.ts +133 -0
  403. package/examples/canister-studio/src/components/canister/shared-link-loader.test.ts +116 -0
  404. package/examples/canister-studio/src/components/canister/use-canister-page.ts +44 -0
  405. package/examples/canister-studio/src/components/canister/workspace-key.test.ts +27 -0
  406. package/examples/canister-studio/src/components/canister/workspace-key.ts +36 -0
  407. package/examples/canister-studio/src/components/canister-id-input.ts +69 -0
  408. package/examples/canister-studio/src/components/canister-names.ts +11 -0
  409. package/examples/canister-studio/src/components/download.ts +34 -0
  410. package/examples/canister-studio/src/components/fallbacks.tsx +33 -0
  411. package/examples/canister-studio/src/components/helpers.test.ts +107 -0
  412. package/examples/canister-studio/src/components/history/HistoryList.tsx +284 -0
  413. package/examples/canister-studio/src/components/home/FeaturedLedger.tsx +149 -0
  414. package/examples/canister-studio/src/components/home/OpenCanister.tsx +99 -0
  415. package/examples/canister-studio/src/components/home/featured.test.tsx +92 -0
  416. package/examples/canister-studio/src/components/home/featured.ts +64 -0
  417. package/examples/canister-studio/src/components/interface/CandidTab.tsx +38 -0
  418. package/examples/canister-studio/src/components/interface/CompatReport.tsx +92 -0
  419. package/examples/canister-studio/src/components/interface/CompatibilityTab.tsx +190 -0
  420. package/examples/canister-studio/src/components/interface/ProjectTab.tsx +196 -0
  421. package/examples/canister-studio/src/components/interface/TypeScriptTab.tsx +114 -0
  422. package/examples/canister-studio/src/components/interface/tools.test.tsx +169 -0
  423. package/examples/canister-studio/src/components/modes.ts +31 -0
  424. package/examples/canister-studio/src/components/playground/MethodPicker.tsx +67 -0
  425. package/examples/canister-studio/src/components/playground/PlaygroundForms.tsx +112 -0
  426. package/examples/canister-studio/src/components/playground/PlaygroundTools.tsx +219 -0
  427. package/examples/canister-studio/src/components/playground/sample.ts +43 -0
  428. package/examples/canister-studio/src/components/playground/target.test.tsx +86 -0
  429. package/examples/canister-studio/src/components/primitives.test.tsx +121 -0
  430. package/examples/canister-studio/src/components/provenance.ts +38 -0
  431. package/examples/canister-studio/src/components/route-render.test.tsx +71 -0
  432. package/examples/canister-studio/src/components/use-task.ts +56 -0
  433. package/examples/canister-studio/src/components/wallet/BalancePanel.tsx +110 -0
  434. package/examples/canister-studio/src/components/wallet/LedgerSummary.tsx +63 -0
  435. package/examples/canister-studio/src/components/wallet/MinterPanel.tsx +255 -0
  436. package/examples/canister-studio/src/components/wallet/SendPanel.test.tsx +297 -0
  437. package/examples/canister-studio/src/components/wallet/SendPanel.tsx +446 -0
  438. package/examples/canister-studio/src/components/wallet/ledgers.test.ts +36 -0
  439. package/examples/canister-studio/src/components/wallet/ledgers.ts +111 -0
  440. package/examples/canister-studio/src/forms/JsonPanel.tsx +157 -0
  441. package/examples/canister-studio/src/forms/MethodForm.test.tsx +369 -0
  442. package/examples/canister-studio/src/forms/MethodForm.tsx +368 -0
  443. package/examples/canister-studio/src/forms/TypedForm.tsx +55 -0
  444. package/examples/canister-studio/src/forms/context.ts +62 -0
  445. package/examples/canister-studio/src/forms/convert.ts +131 -0
  446. package/examples/canister-studio/src/forms/edge.test.ts +252 -0
  447. package/examples/canister-studio/src/forms/engine.test.ts +358 -0
  448. package/examples/canister-studio/src/forms/engine.ts +1492 -0
  449. package/examples/canister-studio/src/forms/fields/BytesField.tsx +210 -0
  450. package/examples/canister-studio/src/forms/fields/Field.tsx +106 -0
  451. package/examples/canister-studio/src/forms/fields/FieldShell.tsx +101 -0
  452. package/examples/canister-studio/src/forms/fields/Group.tsx +74 -0
  453. package/examples/canister-studio/src/forms/fields/ListField.tsx +256 -0
  454. package/examples/canister-studio/src/forms/fields/Segmented.tsx +78 -0
  455. package/examples/canister-studio/src/forms/fields/TimestampField.tsx +108 -0
  456. package/examples/canister-studio/src/forms/fields/UnitsField.tsx +119 -0
  457. package/examples/canister-studio/src/forms/fields/composites.tsx +389 -0
  458. package/examples/canister-studio/src/forms/fields/leaves.tsx +350 -0
  459. package/examples/canister-studio/src/forms/fields.test.tsx +396 -0
  460. package/examples/canister-studio/src/forms/helpers.test.ts +225 -0
  461. package/examples/canister-studio/src/forms/hints.ts +128 -0
  462. package/examples/canister-studio/src/forms/json.ts +119 -0
  463. package/examples/canister-studio/src/forms/parsing.test.ts +217 -0
  464. package/examples/canister-studio/src/forms/useMethodForm.ts +251 -0
  465. package/examples/canister-studio/src/interface/browser.test.ts +23 -0
  466. package/examples/canister-studio/src/interface/browser.ts +153 -0
  467. package/examples/canister-studio/src/interface/cache.ts +68 -0
  468. package/examples/canister-studio/src/interface/cli-cache.test.ts +155 -0
  469. package/examples/canister-studio/src/interface/cli-cache.ts +125 -0
  470. package/examples/canister-studio/src/interface/compile.server.test.ts +198 -0
  471. package/examples/canister-studio/src/interface/compile.server.ts +262 -0
  472. package/examples/canister-studio/src/interface/compile.test.ts +108 -0
  473. package/examples/canister-studio/src/interface/compile.ts +305 -0
  474. package/examples/canister-studio/src/interface/docs.test.ts +75 -0
  475. package/examples/canister-studio/src/interface/docs.ts +100 -0
  476. package/examples/canister-studio/src/interface/functions.test.ts +42 -0
  477. package/examples/canister-studio/src/interface/functions.ts +200 -0
  478. package/examples/canister-studio/src/interface/live-candid.server.ts +91 -0
  479. package/examples/canister-studio/src/interface/live-loader.test.ts +515 -0
  480. package/examples/canister-studio/src/interface/live-loader.ts +364 -0
  481. package/examples/canister-studio/src/interface/load.test.ts +328 -0
  482. package/examples/canister-studio/src/interface/load.ts +249 -0
  483. package/examples/canister-studio/src/interface/read-candid.test.ts +185 -0
  484. package/examples/canister-studio/src/interface/read-candid.ts +233 -0
  485. package/examples/canister-studio/src/interface/server-result.ts +42 -0
  486. package/examples/canister-studio/src/interface/typed.ts +68 -0
  487. package/examples/canister-studio/src/interface/types.ts +121 -0
  488. package/examples/canister-studio/src/lib/format.test.ts +107 -0
  489. package/examples/canister-studio/src/lib/format.ts +139 -0
  490. package/examples/canister-studio/src/lib/hex.test.ts +43 -0
  491. package/examples/canister-studio/src/lib/hex.ts +33 -0
  492. package/examples/canister-studio/src/lib/ids.test.ts +70 -0
  493. package/examples/canister-studio/src/lib/ids.ts +93 -0
  494. package/examples/canister-studio/src/lib/reactor.test.ts +55 -0
  495. package/examples/canister-studio/src/lib/reactor.ts +55 -0
  496. package/examples/canister-studio/src/lib/registry.test.ts +74 -0
  497. package/examples/canister-studio/src/lib/registry.ts +238 -0
  498. package/examples/canister-studio/src/lib/schema.ts +148 -0
  499. package/examples/canister-studio/src/routeTree.gen.ts +227 -0
  500. package/examples/canister-studio/src/router.test.ts +290 -0
  501. package/examples/canister-studio/src/router.tsx +127 -0
  502. package/examples/canister-studio/src/routes/__root.tsx +58 -0
  503. package/examples/canister-studio/src/routes/canister.$canisterId.index.tsx +172 -0
  504. package/examples/canister-studio/src/routes/canister.$canisterId.interface.tsx +98 -0
  505. package/examples/canister-studio/src/routes/canister.$canisterId.tsx +91 -0
  506. package/examples/canister-studio/src/routes/history.tsx +55 -0
  507. package/examples/canister-studio/src/routes/index.tsx +169 -0
  508. package/examples/canister-studio/src/routes/playground.tsx +286 -0
  509. package/examples/canister-studio/src/routes/safety.tsx +57 -0
  510. package/examples/canister-studio/src/routes/wallet.tsx +213 -0
  511. package/examples/canister-studio/src/safety/LabTransferForm.tsx +84 -0
  512. package/examples/canister-studio/src/safety/RequestLog.tsx +79 -0
  513. package/examples/canister-studio/src/safety/SafetyLab.test.tsx +89 -0
  514. package/examples/canister-studio/src/safety/SafetyLab.tsx +140 -0
  515. package/examples/canister-studio/src/safety/ScenarioCard.tsx +262 -0
  516. package/examples/canister-studio/src/safety/lab-run.ts +283 -0
  517. package/examples/canister-studio/src/safety/mock-ledger.test.ts +99 -0
  518. package/examples/canister-studio/src/safety/mock-ledger.ts +263 -0
  519. package/examples/canister-studio/src/safety/scenarios.test.ts +157 -0
  520. package/examples/canister-studio/src/safety/scenarios.ts +924 -0
  521. package/examples/canister-studio/src/styles.css +3453 -0
  522. package/examples/canister-studio/src/styles.test.ts +46 -0
  523. package/examples/canister-studio/src/values/CallPreview.test.tsx +86 -0
  524. package/examples/canister-studio/src/values/CallPreview.tsx +148 -0
  525. package/examples/canister-studio/src/values/ErrValueView.tsx +66 -0
  526. package/examples/canister-studio/src/values/Inspector.tsx +133 -0
  527. package/examples/canister-studio/src/values/ReplyView.test.tsx +341 -0
  528. package/examples/canister-studio/src/values/ReplyView.tsx +164 -0
  529. package/examples/canister-studio/src/values/candid-text.test.ts +516 -0
  530. package/examples/canister-studio/src/values/candid-text.ts +479 -0
  531. package/examples/canister-studio/src/values/hints.ts +58 -0
  532. package/examples/canister-studio/src/values/json.test.ts +66 -0
  533. package/examples/canister-studio/src/values/json.ts +31 -0
  534. package/examples/canister-studio/src/values/leaves.tsx +309 -0
  535. package/examples/canister-studio/src/values/schema.test.ts +123 -0
  536. package/examples/canister-studio/src/values/schema.ts +78 -0
  537. package/examples/canister-studio/src/values/test-router.tsx +21 -0
  538. package/examples/canister-studio/src/values/tree.tsx +670 -0
  539. package/examples/canister-studio/src/values/use-sha256.ts +29 -0
  540. package/examples/canister-studio/tsconfig.json +20 -0
  541. package/examples/canister-studio/vite.config.ts +50 -0
  542. package/examples/canister-studio/vitest.config.ts +20 -0
  543. package/examples/icrc-ledger/.stackblitzrc +4 -0
  544. package/examples/icrc-ledger/README.md +100 -0
  545. package/examples/icrc-ledger/ckbtc_minter.did +12 -0
  546. package/examples/icrc-ledger/icp_ledger.did +147 -0
  547. package/examples/icrc-ledger/icrc1.did +88 -0
  548. package/examples/icrc-ledger/index.html +13 -0
  549. package/examples/icrc-ledger/package.json +40 -0
  550. package/examples/icrc-ledger/scripts/smoke.ts +151 -0
  551. package/examples/icrc-ledger/src/App.tsx +71 -0
  552. package/examples/icrc-ledger/src/ErrorPanel.tsx +105 -0
  553. package/examples/icrc-ledger/src/MainnetTab.tsx +450 -0
  554. package/examples/icrc-ledger/src/SandboxTab.test.tsx +379 -0
  555. package/examples/icrc-ledger/src/SandboxTab.tsx +761 -0
  556. package/examples/icrc-ledger/src/blocks.test.ts +247 -0
  557. package/examples/icrc-ledger/src/blocks.ts +140 -0
  558. package/examples/icrc-ledger/src/canisters/ckbtc_minter.envelope.json +96 -0
  559. package/examples/icrc-ledger/src/canisters/ckbtc_minter.ts +16 -0
  560. package/examples/icrc-ledger/src/canisters/icp_ledger.envelope.json +690 -0
  561. package/examples/icrc-ledger/src/canisters/icp_ledger.ts +223 -0
  562. package/examples/icrc-ledger/src/canisters/icrc1.envelope.json +840 -0
  563. package/examples/icrc-ledger/src/canisters/icrc1.ts +78 -0
  564. package/examples/icrc-ledger/src/format.test.ts +41 -0
  565. package/examples/icrc-ledger/src/format.ts +68 -0
  566. package/examples/icrc-ledger/src/ledger.ts +15 -0
  567. package/examples/icrc-ledger/src/main.tsx +10 -0
  568. package/examples/icrc-ledger/src/minter.ts +16 -0
  569. package/examples/icrc-ledger/src/sandbox.test.ts +464 -0
  570. package/examples/icrc-ledger/src/sandbox.ts +281 -0
  571. package/examples/icrc-ledger/src/styles.css +391 -0
  572. package/examples/icrc-ledger/src/transfer-form.test.ts +62 -0
  573. package/examples/icrc-ledger/src/transfer-form.ts +97 -0
  574. package/examples/icrc-ledger/tsconfig.json +19 -0
  575. package/examples/icrc-ledger/vite.config.ts +44 -0
  576. package/examples/icrc-ledger/vitest.config.ts +12 -0
  577. package/examples/next-ssr/.stackblitzrc +4 -0
  578. package/examples/next-ssr/README.md +112 -0
  579. package/examples/next-ssr/icrc1.did +88 -0
  580. package/examples/next-ssr/next.config.ts +10 -0
  581. package/examples/next-ssr/package.json +41 -0
  582. package/examples/next-ssr/scripts/smoke.ts +169 -0
  583. package/examples/next-ssr/src/app/account/CertifiedBalances.tsx +58 -0
  584. package/examples/next-ssr/src/app/account/account.test.tsx +165 -0
  585. package/examples/next-ssr/src/app/account/page.tsx +82 -0
  586. package/examples/next-ssr/src/app/api/balance/[ledger]/[principal]/route.ts +10 -0
  587. package/examples/next-ssr/src/app/globals.css +278 -0
  588. package/examples/next-ssr/src/app/layout.tsx +34 -0
  589. package/examples/next-ssr/src/app/page.tsx +46 -0
  590. package/examples/next-ssr/src/app/providers.browser.test.tsx +127 -0
  591. package/examples/next-ssr/src/app/providers.test.tsx +73 -0
  592. package/examples/next-ssr/src/app/providers.tsx +60 -0
  593. package/examples/next-ssr/src/canisters/icrc1.envelope.json +840 -0
  594. package/examples/next-ssr/src/canisters/icrc1.ts +78 -0
  595. package/examples/next-ssr/src/components/BalanceTable.tsx +43 -0
  596. package/examples/next-ssr/src/components/LedgerCard.test.tsx +96 -0
  597. package/examples/next-ssr/src/components/LedgerCard.tsx +230 -0
  598. package/examples/next-ssr/src/components/LedgerError.tsx +32 -0
  599. package/examples/next-ssr/src/components/LedgerSections.tsx +22 -0
  600. package/examples/next-ssr/src/components/MyBalances.test.tsx +253 -0
  601. package/examples/next-ssr/src/components/MyBalances.tsx +129 -0
  602. package/examples/next-ssr/src/components/SessionBadge.tsx +18 -0
  603. package/examples/next-ssr/src/components/hydration.test.tsx +259 -0
  604. package/examples/next-ssr/src/components/server-data.ts +10 -0
  605. package/examples/next-ssr/src/format.ts +46 -0
  606. package/examples/next-ssr/src/ledgers.ts +38 -0
  607. package/examples/next-ssr/src/server/balance-route.test.ts +151 -0
  608. package/examples/next-ssr/src/server/balance-route.ts +157 -0
  609. package/examples/next-ssr/src/server/error-summary.test.ts +34 -0
  610. package/examples/next-ssr/src/server/error-summary.ts +37 -0
  611. package/examples/next-ssr/src/server/parse-owner.test.ts +39 -0
  612. package/examples/next-ssr/src/server/parse-owner.ts +34 -0
  613. package/examples/next-ssr/src/server/prefetch-ledgers.test.ts +137 -0
  614. package/examples/next-ssr/src/server/prefetch-ledgers.ts +70 -0
  615. package/examples/next-ssr/src/server/read-balances.test.ts +103 -0
  616. package/examples/next-ssr/src/server/read-balances.ts +78 -0
  617. package/examples/next-ssr/src/server/request-client.rsc.test.tsx +110 -0
  618. package/examples/next-ssr/src/server/request-client.ts +24 -0
  619. package/examples/next-ssr/src/testing/mock-ledgers.ts +115 -0
  620. package/examples/next-ssr/src/testing/react-server-dom-turbopack.d.ts +15 -0
  621. package/examples/next-ssr/src/testing/setup.ts +10 -0
  622. package/examples/next-ssr/tsconfig.json +31 -0
  623. package/examples/next-ssr/vitest.config.ts +59 -0
  624. package/examples/node-agent-tool/.stackblitzrc +4 -0
  625. package/examples/node-agent-tool/README.md +165 -0
  626. package/examples/node-agent-tool/icp.yaml +11 -0
  627. package/examples/node-agent-tool/icrc1.did +88 -0
  628. package/examples/node-agent-tool/package.json +32 -0
  629. package/examples/node-agent-tool/src/canisters/icrc1.envelope.json +840 -0
  630. package/examples/node-agent-tool/src/canisters/icrc1.ts +78 -0
  631. package/examples/node-agent-tool/src/cli.test.ts +64 -0
  632. package/examples/node-agent-tool/src/cli.ts +23 -0
  633. package/examples/node-agent-tool/src/command-line.ts +159 -0
  634. package/examples/node-agent-tool/src/commands/balance.ts +73 -0
  635. package/examples/node-agent-tool/src/commands/demo.test.ts +51 -0
  636. package/examples/node-agent-tool/src/commands/demo.ts +359 -0
  637. package/examples/node-agent-tool/src/commands/info.ts +69 -0
  638. package/examples/node-agent-tool/src/commands/reads.test.ts +133 -0
  639. package/examples/node-agent-tool/src/commands/resend.test.ts +363 -0
  640. package/examples/node-agent-tool/src/commands/transfer.test.ts +313 -0
  641. package/examples/node-agent-tool/src/commands/transfer.ts +415 -0
  642. package/examples/node-agent-tool/src/commands/watch.test.ts +159 -0
  643. package/examples/node-agent-tool/src/commands/watch.ts +135 -0
  644. package/examples/node-agent-tool/src/commands/whoami.ts +36 -0
  645. package/examples/node-agent-tool/src/context.ts +20 -0
  646. package/examples/node-agent-tool/src/dispatch.ts +70 -0
  647. package/examples/node-agent-tool/src/failure.test.ts +59 -0
  648. package/examples/node-agent-tool/src/failure.ts +176 -0
  649. package/examples/node-agent-tool/src/identity.test.ts +242 -0
  650. package/examples/node-agent-tool/src/identity.ts +228 -0
  651. package/examples/node-agent-tool/src/input.ts +110 -0
  652. package/examples/node-agent-tool/src/ledgers.test.ts +74 -0
  653. package/examples/node-agent-tool/src/ledgers.ts +62 -0
  654. package/examples/node-agent-tool/src/mock-ledger.ts +211 -0
  655. package/examples/node-agent-tool/src/network.test.ts +128 -0
  656. package/examples/node-agent-tool/src/network.ts +101 -0
  657. package/examples/node-agent-tool/src/output.test.ts +208 -0
  658. package/examples/node-agent-tool/src/output.ts +163 -0
  659. package/examples/node-agent-tool/src/run.ts +73 -0
  660. package/examples/node-agent-tool/src/test-kit.ts +167 -0
  661. package/examples/node-agent-tool/tsconfig.json +18 -0
  662. package/examples/node-agent-tool/vitest.config.ts +8 -0
  663. package/examples/vite-wallet/.stackblitzrc +4 -0
  664. package/examples/vite-wallet/Cargo.lock +646 -0
  665. package/examples/vite-wallet/Cargo.toml +5 -0
  666. package/examples/vite-wallet/README.md +107 -0
  667. package/examples/vite-wallet/backend/Cargo.toml +12 -0
  668. package/examples/vite-wallet/backend/backend.did +37 -0
  669. package/examples/vite-wallet/backend/src/lib.rs +133 -0
  670. package/examples/vite-wallet/icp.yaml +44 -0
  671. package/examples/vite-wallet/index.html +13 -0
  672. package/examples/vite-wallet/ledger.did +54 -0
  673. package/examples/vite-wallet/package.json +46 -0
  674. package/examples/vite-wallet/public/.ic-assets.json5 +26 -0
  675. package/examples/vite-wallet/scripts/faucet-args.test.ts +30 -0
  676. package/examples/vite-wallet/scripts/faucet-args.ts +41 -0
  677. package/examples/vite-wallet/scripts/faucet.ts +41 -0
  678. package/examples/vite-wallet/src/AddressBook.test.tsx +191 -0
  679. package/examples/vite-wallet/src/AddressBook.tsx +147 -0
  680. package/examples/vite-wallet/src/App.tsx +53 -0
  681. package/examples/vite-wallet/src/Balance.test.tsx +57 -0
  682. package/examples/vite-wallet/src/Balance.tsx +58 -0
  683. package/examples/vite-wallet/src/EnvironmentPanel.test.tsx +83 -0
  684. package/examples/vite-wallet/src/EnvironmentPanel.tsx +103 -0
  685. package/examples/vite-wallet/src/ErrorNote.tsx +70 -0
  686. package/examples/vite-wallet/src/Profile.test.tsx +145 -0
  687. package/examples/vite-wallet/src/Profile.tsx +92 -0
  688. package/examples/vite-wallet/src/SendIcp.test.tsx +231 -0
  689. package/examples/vite-wallet/src/SendIcp.tsx +244 -0
  690. package/examples/vite-wallet/src/SendToContact.test.tsx +113 -0
  691. package/examples/vite-wallet/src/SendToContact.tsx +102 -0
  692. package/examples/vite-wallet/src/SignIn.test.tsx +106 -0
  693. package/examples/vite-wallet/src/SignIn.tsx +120 -0
  694. package/examples/vite-wallet/src/Wallet.test.tsx +89 -0
  695. package/examples/vite-wallet/src/account-switch.test.tsx +108 -0
  696. package/examples/vite-wallet/src/auth/dev-accounts.test.ts +113 -0
  697. package/examples/vite-wallet/src/auth/dev-accounts.ts +216 -0
  698. package/examples/vite-wallet/src/auth/local-page.test.ts +28 -0
  699. package/examples/vite-wallet/src/auth/local-page.ts +20 -0
  700. package/examples/vite-wallet/src/auth/wallet-auth.test.ts +159 -0
  701. package/examples/vite-wallet/src/auth/wallet-auth.ts +131 -0
  702. package/examples/vite-wallet/src/canisters/backend.envelope.json +275 -0
  703. package/examples/vite-wallet/src/canisters/backend.ts +58 -0
  704. package/examples/vite-wallet/src/canisters/ledger.envelope.json +451 -0
  705. package/examples/vite-wallet/src/canisters/ledger.ts +56 -0
  706. package/examples/vite-wallet/src/client.ts +45 -0
  707. package/examples/vite-wallet/src/main.tsx +18 -0
  708. package/examples/vite-wallet/src/optimistic-contacts.ts +113 -0
  709. package/examples/vite-wallet/src/refusal.ts +17 -0
  710. package/examples/vite-wallet/src/styles.css +282 -0
  711. package/examples/vite-wallet/src/test/test-wallet.tsx +322 -0
  712. package/examples/vite-wallet/src/token.ts +50 -0
  713. package/examples/vite-wallet/src/transfer.test.ts +178 -0
  714. package/examples/vite-wallet/src/transfer.ts +110 -0
  715. package/examples/vite-wallet/src/use-canisters.ts +49 -0
  716. package/examples/vite-wallet/tsconfig.json +19 -0
  717. package/examples/vite-wallet/vite.config.ts +47 -0
  718. package/examples/vite-wallet/vitest.config.ts +17 -0
  719. package/package.json +9 -4
  720. package/packages/core/.size-limit.js +47 -0
  721. package/packages/core/LICENSE +21 -0
  722. package/packages/core/README.md +84 -0
  723. package/packages/core/llms.txt +610 -0
  724. package/packages/core/package.json +89 -0
  725. package/packages/core/src/call.ts +961 -0
  726. package/packages/core/src/canister.ts +259 -0
  727. package/packages/core/src/client.ts +2061 -0
  728. package/packages/core/src/errors.ts +1079 -0
  729. package/packages/core/src/hydration.ts +239 -0
  730. package/packages/core/src/index.ts +23 -0
  731. package/packages/core/src/keys.ts +108 -0
  732. package/packages/core/src/management.ts +138 -0
  733. package/packages/core/src/network.ts +1020 -0
  734. package/packages/core/src/options.ts +1000 -0
  735. package/packages/core/src/runtime.ts +50 -0
  736. package/packages/core/src/testing/fake-replica.ts +1166 -0
  737. package/packages/core/src/testing/index.ts +16 -0
  738. package/packages/core/src/testing/test-auth.ts +340 -0
  739. package/packages/core/src/testing/test-client.ts +559 -0
  740. package/packages/core/src/types.ts +541 -0
  741. package/packages/core/src/units.ts +299 -0
  742. package/packages/core/src/version.ts +4 -0
  743. package/packages/core/tests/canister-helpers.ts +187 -0
  744. package/packages/core/tests/canister-id.test.ts +158 -0
  745. package/packages/core/tests/canister-unwrap.test.ts +94 -0
  746. package/packages/core/tests/canister.test-d.ts +406 -0
  747. package/packages/core/tests/canister.test.ts +1921 -0
  748. package/packages/core/tests/client-account-change.test.ts +131 -0
  749. package/packages/core/tests/client-agents.test.ts +402 -0
  750. package/packages/core/tests/client-auth-network.test.ts +719 -0
  751. package/packages/core/tests/client-disposed.test.ts +403 -0
  752. package/packages/core/tests/client-helpers.ts +136 -0
  753. package/packages/core/tests/client-hydration.test.ts +166 -0
  754. package/packages/core/tests/client-pinned.test.ts +626 -0
  755. package/packages/core/tests/client-session.test.ts +348 -0
  756. package/packages/core/tests/client.test-d.ts +181 -0
  757. package/packages/core/tests/client.test.ts +1162 -0
  758. package/packages/core/tests/encode.test.ts +171 -0
  759. package/packages/core/tests/env-canister-id.test.ts +666 -0
  760. package/packages/core/tests/env-cookie-copies.test.ts +182 -0
  761. package/packages/core/tests/error-guard.test.ts +222 -0
  762. package/packages/core/tests/errors.test-d.ts +280 -0
  763. package/packages/core/tests/errors.test.ts +1359 -0
  764. package/packages/core/tests/fixtures/README.md +28 -0
  765. package/packages/core/tests/fixtures/accounts.did +21 -0
  766. package/packages/core/tests/fixtures/accounts.envelope.json +367 -0
  767. package/packages/core/tests/fixtures/accounts.ts +26 -0
  768. package/packages/core/tests/fixtures/archive.did +31 -0
  769. package/packages/core/tests/fixtures/archive.envelope.json +259 -0
  770. package/packages/core/tests/fixtures/archive.ts +55 -0
  771. package/packages/core/tests/fixtures/icrc1.did +88 -0
  772. package/packages/core/tests/fixtures/icrc1.envelope.json +840 -0
  773. package/packages/core/tests/fixtures/icrc1.ts +78 -0
  774. package/packages/core/tests/fixtures/management.did +724 -0
  775. package/packages/core/tests/fixtures/management.envelope.json +4455 -0
  776. package/packages/core/tests/fixtures/management.ts +486 -0
  777. package/packages/core/tests/fixtures/shapes.did +33 -0
  778. package/packages/core/tests/fixtures/shapes.envelope.json +440 -0
  779. package/packages/core/tests/fixtures/shapes.ts +56 -0
  780. package/packages/core/tests/fixtures/skippable.did +15 -0
  781. package/packages/core/tests/fixtures/skippable.envelope.json +182 -0
  782. package/packages/core/tests/fixtures/skippable.ts +33 -0
  783. package/packages/core/tests/func.test.ts +154 -0
  784. package/packages/core/tests/identity-attributes.test.ts +214 -0
  785. package/packages/core/tests/keys.test.ts +289 -0
  786. package/packages/core/tests/management.test.ts +206 -0
  787. package/packages/core/tests/network-helpers.ts +121 -0
  788. package/packages/core/tests/network.test-d.ts +78 -0
  789. package/packages/core/tests/network.test.ts +1051 -0
  790. package/packages/core/tests/options.test.ts +1594 -0
  791. package/packages/core/tests/redirect-sign-in.test.ts +393 -0
  792. package/packages/core/tests/resend.test-d.ts +81 -0
  793. package/packages/core/tests/resend.test.ts +723 -0
  794. package/packages/core/tests/skip.test-d.ts +125 -0
  795. package/packages/core/tests/testing-entry.test.ts +73 -0
  796. package/packages/core/tests/testing-fake-replica.test.ts +1242 -0
  797. package/packages/core/tests/testing-test-auth.test-d.ts +84 -0
  798. package/packages/core/tests/testing-test-auth.test.ts +483 -0
  799. package/packages/core/tests/testing-test-client.test-d.ts +184 -0
  800. package/packages/core/tests/testing-test-client.test.ts +1272 -0
  801. package/packages/core/tests/time-realm.test.ts +63 -0
  802. package/packages/core/tests/time.test.ts +238 -0
  803. package/packages/core/tests/traps/README.md +39 -0
  804. package/packages/core/tests/traps/anonymous-write.test.ts +145 -0
  805. package/packages/core/tests/traps/classification-table.test.ts +256 -0
  806. package/packages/core/tests/traps/err-arm.test.ts +144 -0
  807. package/packages/core/tests/traps/fetch-spy.ts +39 -0
  808. package/packages/core/tests/traps/invalidation-after-write.test.ts +178 -0
  809. package/packages/core/tests/traps/ledger.ts +248 -0
  810. package/packages/core/tests/traps/root-key-off-local.test.ts +130 -0
  811. package/packages/core/tests/traps/stale-data-across-users.test.ts +173 -0
  812. package/packages/core/tests/traps/unresolved-name.test.ts +155 -0
  813. package/packages/core/tests/traps/update-as-query.test.ts +244 -0
  814. package/packages/core/tests/traps/update-not-resent.test.ts +126 -0
  815. package/packages/core/tests/traps/update-resent-when-never-delivered.test.ts +159 -0
  816. package/packages/core/tests/traps.test-d.ts +476 -0
  817. package/packages/core/tests/units.test-d.ts +118 -0
  818. package/packages/core/tests/units.test.ts +634 -0
  819. package/packages/core/tsconfig.json +19 -0
  820. package/packages/core/tsconfig.typecheck.json +16 -0
  821. package/packages/core/vitest.config.ts +13 -0
  822. package/packages/mcp/LICENSE +21 -0
  823. package/packages/mcp/README.md +189 -0
  824. package/packages/mcp/package.json +62 -0
  825. package/packages/mcp/src/candid/candid-text.test.ts +455 -0
  826. package/packages/mcp/src/candid/candid-text.ts +501 -0
  827. package/packages/mcp/src/candid/contract.test.ts +398 -0
  828. package/packages/mcp/src/candid/formats.ts +155 -0
  829. package/packages/mcp/src/candid/hex.ts +29 -0
  830. package/packages/mcp/src/candid/json-schema.test.ts +758 -0
  831. package/packages/mcp/src/candid/json-schema.ts +454 -0
  832. package/packages/mcp/src/candid/json.test.ts +1207 -0
  833. package/packages/mcp/src/candid/json.ts +1053 -0
  834. package/packages/mcp/src/candid/schema-walk.ts +189 -0
  835. package/packages/mcp/src/candid/type-text.test.ts +179 -0
  836. package/packages/mcp/src/candid/type-text.ts +238 -0
  837. package/packages/mcp/src/cli.ts +134 -0
  838. package/packages/mcp/src/identity.ts +188 -0
  839. package/packages/mcp/src/interface/cache.test.ts +48 -0
  840. package/packages/mcp/src/interface/cache.ts +68 -0
  841. package/packages/mcp/src/interface/cli-cache.test.ts +150 -0
  842. package/packages/mcp/src/interface/cli-cache.ts +122 -0
  843. package/packages/mcp/src/interface/compile-worker.test.ts +246 -0
  844. package/packages/mcp/src/interface/compile-worker.ts +290 -0
  845. package/packages/mcp/src/interface/compile.test.ts +155 -0
  846. package/packages/mcp/src/interface/compile.ts +281 -0
  847. package/packages/mcp/src/interface/docs.test.ts +105 -0
  848. package/packages/mcp/src/interface/docs.ts +86 -0
  849. package/packages/mcp/src/interface/fixtures/icrc1.did +88 -0
  850. package/packages/mcp/src/interface/hex.ts +8 -0
  851. package/packages/mcp/src/interface/ids.test.ts +71 -0
  852. package/packages/mcp/src/interface/ids.ts +87 -0
  853. package/packages/mcp/src/interface/loader.test.ts +804 -0
  854. package/packages/mcp/src/interface/loader.ts +519 -0
  855. package/packages/mcp/src/interface/read-candid.test.ts +300 -0
  856. package/packages/mcp/src/interface/read-candid.ts +259 -0
  857. package/packages/mcp/src/interface/types.ts +113 -0
  858. package/packages/mcp/src/network.ts +122 -0
  859. package/packages/mcp/src/options.ts +149 -0
  860. package/packages/mcp/src/server/approval.ts +159 -0
  861. package/packages/mcp/src/server/naming.ts +43 -0
  862. package/packages/mcp/src/server/results.ts +102 -0
  863. package/packages/mcp/src/server/server.ts +797 -0
  864. package/packages/mcp/src/server/tools.ts +116 -0
  865. package/packages/mcp/src/usage.ts +40 -0
  866. package/packages/mcp/tests/approval.test.ts +60 -0
  867. package/packages/mcp/tests/identity.test.ts +234 -0
  868. package/packages/mcp/tests/naming.test.ts +62 -0
  869. package/packages/mcp/tests/options.test.ts +140 -0
  870. package/packages/mcp/tests/server.test.ts +997 -0
  871. package/packages/mcp/tsconfig.json +19 -0
  872. package/packages/mcp/tsconfig.typecheck.json +10 -0
  873. package/packages/mcp/vitest.config.ts +10 -0
  874. package/packages/react/LICENSE +21 -0
  875. package/packages/react/README.md +328 -0
  876. package/packages/react/llms.txt +95 -0
  877. package/packages/react/package.json +95 -0
  878. package/packages/react/src/index.tsx +678 -0
  879. package/packages/react/tests/abandoned-render.test.tsx +816 -0
  880. package/packages/react/tests/auth.test.tsx +315 -0
  881. package/packages/react/tests/caller-keys.test.tsx +2334 -0
  882. package/packages/react/tests/client-time.test.ts +40 -0
  883. package/packages/react/tests/entry.test.ts +52 -0
  884. package/packages/react/tests/helpers.tsx +291 -0
  885. package/packages/react/tests/hydration.test.tsx +262 -0
  886. package/packages/react/tests/ownership.test.tsx +260 -0
  887. package/packages/react/tests/provider.server.test.tsx +169 -0
  888. package/packages/react/tests/query-client.test.tsx +107 -0
  889. package/packages/react/tests/strict-mode.test.tsx +288 -0
  890. package/packages/react/tests/suspense.test-d.tsx +357 -0
  891. package/packages/react/tests/two-query-cores.test.tsx +135 -0
  892. package/packages/react/tests/types.test-d.tsx +63 -0
  893. package/packages/react/tsconfig.json +23 -0
  894. package/packages/react/tsconfig.test.json +8 -0
  895. package/packages/react/tsconfig.typecheck.json +16 -0
  896. package/packages/react/vitest.config.ts +39 -0
  897. package/packages/vite-plugin/LICENSE +21 -0
  898. package/packages/vite-plugin/README.md +532 -0
  899. package/packages/vite-plugin/package.json +76 -0
  900. package/packages/vite-plugin/src/describe-value.test.ts +43 -0
  901. package/packages/vite-plugin/src/describe-value.ts +167 -0
  902. package/packages/vite-plugin/src/dev-environment.test.ts +747 -0
  903. package/packages/vite-plugin/src/dev-environment.ts +510 -0
  904. package/packages/vite-plugin/src/env.ts +431 -0
  905. package/packages/vite-plugin/src/fetch.ts +348 -0
  906. package/packages/vite-plugin/src/file-identity.ts +106 -0
  907. package/packages/vite-plugin/src/generate.ts +691 -0
  908. package/packages/vite-plugin/src/index.test.ts +1243 -0
  909. package/packages/vite-plugin/src/index.ts +2565 -0
  910. package/packages/vite-plugin/src/interface.ts +339 -0
  911. package/packages/vite-plugin/src/local-host.ts +34 -0
  912. package/packages/vite-plugin/src/options.ts +641 -0
  913. package/packages/vite-plugin/src/principal-text.test.ts +87 -0
  914. package/packages/vite-plugin/src/principal-text.ts +98 -0
  915. package/packages/vite-plugin/tests/close.test.ts +141 -0
  916. package/packages/vite-plugin/tests/environment-mode.test.ts +136 -0
  917. package/packages/vite-plugin/tests/fake-ic.ts +214 -0
  918. package/packages/vite-plugin/tests/fetch-sdk.test.ts +27 -0
  919. package/packages/vite-plugin/tests/fetch.test.ts +1196 -0
  920. package/packages/vite-plugin/tests/fixtures/fake-cli/bin.js +90 -0
  921. package/packages/vite-plugin/tests/fixtures/fake-cli/package.json +8 -0
  922. package/packages/vite-plugin/tests/fixtures/icrc1.did +88 -0
  923. package/packages/vite-plugin/tests/generation.test.ts +1036 -0
  924. package/packages/vite-plugin/tests/icp-command.test.ts +194 -0
  925. package/packages/vite-plugin/tests/icp-timeout.test.ts +83 -0
  926. package/packages/vite-plugin/tests/no-process-exit.test.ts +83 -0
  927. package/packages/vite-plugin/tests/on-generated.test.ts +323 -0
  928. package/packages/vite-plugin/tests/output-collision.test.ts +329 -0
  929. package/packages/vite-plugin/tests/package.test.ts +62 -0
  930. package/packages/vite-plugin/tests/project.test.ts +2993 -0
  931. package/packages/vite-plugin/tests/support.ts +339 -0
  932. package/packages/vite-plugin/tests/timeout.test.ts +107 -0
  933. package/packages/vite-plugin/tsconfig.json +23 -0
  934. package/packages/vite-plugin/tsconfig.typecheck.json +16 -0
  935. package/packages/vite-plugin/vitest.config.ts +9 -0
  936. package/pnpm-workspace.yaml +23 -0
  937. package/scripts/ai-context-files.js +72 -0
  938. package/scripts/assemble-docs-site.mjs +485 -0
  939. package/scripts/assemble-docs-site.test.mjs +568 -0
  940. package/scripts/build-examples.js +58 -0
  941. package/scripts/check-ai-context.js +963 -0
  942. package/scripts/check-ai-context.test.mjs +811 -0
  943. package/scripts/check-exports.mjs +767 -0
  944. package/scripts/check-exports.test.mjs +1136 -0
  945. package/scripts/check-snippets/ambient.d.ts +428 -0
  946. package/scripts/check-snippets/app/generated/backend.did +7 -0
  947. package/scripts/check-snippets/app/generated/backend.ts +15 -0
  948. package/scripts/check-snippets/app/generated/icrc1.did +88 -0
  949. package/scripts/check-snippets/app/generated/icrc1.ts +78 -0
  950. package/scripts/check-snippets/app/globals.ts +26 -0
  951. package/scripts/check-snippets/docs/guides/App.tsx +21 -0
  952. package/scripts/check-snippets/docs/guides/app/providers.tsx +23 -0
  953. package/scripts/check-snippets/docs/guides/auth-client.ts +47 -0
  954. package/scripts/check-snippets/docs/guides/canisters/backend.ts +3 -0
  955. package/scripts/check-snippets/docs/guides/canisters/icrc1.ts +3 -0
  956. package/scripts/check-snippets/docs/guides/components/LedgerCard.tsx +26 -0
  957. package/scripts/check-snippets/docs/guides/components/shell.tsx +11 -0
  958. package/scripts/check-snippets/docs/guides/components/sign-in-page.tsx +29 -0
  959. package/scripts/check-snippets/docs/guides/components/slow-section.tsx +5 -0
  960. package/scripts/check-snippets/docs/guides/generated/accounts.ts +95 -0
  961. package/scripts/check-snippets/docs/guides/generated/ckbtc_minter.ts +40 -0
  962. package/scripts/check-snippets/docs/guides/generated/icp_ledger.ts +267 -0
  963. package/scripts/check-snippets/docs/guides/ledger.ts +10 -0
  964. package/scripts/check-snippets/docs/guides/server/request-client.ts +8 -0
  965. package/scripts/check-snippets.mjs +993 -0
  966. package/scripts/check-snippets.test.mjs +338 -0
  967. package/scripts/check-stack.mjs +501 -0
  968. package/scripts/check-stack.test.mjs +800 -0
  969. package/scripts/clean-node-modules.js +67 -0
  970. package/scripts/example-projects.js +58 -0
  971. package/scripts/export-budget.mjs +145 -0
  972. package/scripts/faults.json +6749 -0
  973. package/scripts/lib/fault-workspace.mjs +210 -0
  974. package/scripts/lib/io.mjs +29 -0
  975. package/scripts/lib/io.test.mjs +38 -0
  976. package/scripts/lib/markdown-fences.mjs +56 -0
  977. package/scripts/lib/markdown-fences.test.mjs +84 -0
  978. package/scripts/lib/package-exports.mjs +54 -0
  979. package/scripts/lib/package-exports.test.mjs +66 -0
  980. package/scripts/lib/release-pr-steps.mjs +47 -0
  981. package/scripts/release-tag.mjs +95 -0
  982. package/scripts/release-tag.test.mjs +278 -0
  983. package/scripts/release-workflow.test.mjs +98 -0
  984. package/scripts/release.js +167 -0
  985. package/scripts/release.test.mjs +347 -0
  986. package/scripts/removed-v3-names.js +125 -0
  987. package/scripts/size-app/.size-limit.js +90 -0
  988. package/scripts/size-app/core.js +3 -0
  989. package/scripts/size-app/package.json +24 -0
  990. package/scripts/size-app/react.js +4 -0
  991. package/scripts/size-app.test.mjs +73 -0
  992. package/scripts/sync-ai-context-versions.js +102 -0
  993. package/scripts/sync-example-versions.js +151 -0
  994. package/scripts/test-examples-published.mjs +1585 -0
  995. package/scripts/test-examples-published.test.mjs +1586 -0
  996. package/scripts/traps/any-method-vars-not-a-list.json +5 -0
  997. package/scripts/traps/canister-method-unknown.json +5 -0
  998. package/scripts/traps/client-says-who-calls-once.json +5 -0
  999. package/scripts/traps/client-says-who-calls.json +5 -0
  1000. package/scripts/traps/err-only-on-canister-err.json +14 -0
  1001. package/scripts/traps/format-units-refuses-missing-value.json +5 -0
  1002. package/scripts/traps/format-units-refuses-number.json +5 -0
  1003. package/scripts/traps/format-units-unknown-option.json +5 -0
  1004. package/scripts/traps/guard-err-is-the-methods.json +5 -0
  1005. package/scripts/traps/guard-false-keeps-every-kind.json +5 -0
  1006. package/scripts/traps/internals-not-exported.json +9 -0
  1007. package/scripts/traps/network-refuses-unknown-name.json +5 -0
  1008. package/scripts/traps/never-err-has-no-type.json +5 -0
  1009. package/scripts/traps/options-method-unknown.json +5 -0
  1010. package/scripts/traps/options-vars-wrong-type.json +5 -0
  1011. package/scripts/traps/parse-units-refuses-number.json +5 -0
  1012. package/scripts/traps/parse-units-unknown-option.json +5 -0
  1013. package/scripts/traps/principal-is-not-plain-text.json +14 -0
  1014. package/scripts/traps/query-options-refuses-oneway.json +5 -0
  1015. package/scripts/traps/query-options-refuses-update.json +5 -0
  1016. package/scripts/traps/refuse-next-unknown-option.json +5 -0
  1017. package/scripts/traps/resend-key-reads-the-methods-arg.json +5 -0
  1018. package/scripts/traps/resend-states-its-dedupe-key.json +5 -0
  1019. package/scripts/traps/suspense-refuses-argument-skip.json +5 -0
  1020. package/scripts/traps/suspense-refuses-skippable-read.json +5 -0
  1021. package/scripts/traps/suspense-refuses-vars-skip-fits.json +5 -0
  1022. package/scripts/traps/test-handler-method-unknown.json +5 -0
  1023. package/scripts/traps/test-handler-nat-is-bigint.json +5 -0
  1024. package/scripts/traps/test-handler-takes-the-domain-value.json +5 -0
  1025. package/scripts/traps/vars-of-two-arguments-are-the-tuple.json +5 -0
  1026. package/scripts/typecheck-examples.js +122 -0
  1027. package/scripts/verify-faults.mjs +375 -0
  1028. package/scripts/verify-faults.test.mjs +402 -0
  1029. package/scripts/verify-package-artifacts.mjs +590 -0
  1030. package/scripts/verify-peer-floors.js +399 -0
  1031. package/scripts/verify-peer-floors.test.mjs +422 -0
  1032. package/scripts/verify-test-fails-rules.mjs +104 -0
  1033. package/scripts/verify-test-fails-rules.test.mjs +215 -0
  1034. package/scripts/verify-test-fails.js +371 -0
  1035. package/scripts/verify-traps.mjs +505 -0
  1036. package/scripts/verify-traps.test.mjs +521 -0
  1037. package/scripts/verify-typescript-floor.js +239 -0
  1038. package/skill-packages/ic-reactor/SKILL.md +103 -0
  1039. package/stack.json +8 -0
  1040. package/tsconfig.base.json +23 -0
  1041. 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
+ }