@voltro/cli 0.33.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1801 -0
- package/bin/nodeEnvironment.d.mts +30 -0
- package/bin/nodeEnvironment.mjs +158 -0
- package/bin/voltro.mjs +69 -5
- package/dist/addCommand-BNeoeSxe.js +124 -0
- package/dist/addCommand-aXSQveak.js +2 -0
- package/dist/agentsMd-BTchIZku.js +2 -0
- package/dist/agentsMd-mhQMF1bx.js +254 -0
- package/dist/apiBuild-BICVGnEC.js +2 -0
- package/dist/{apiBuild-h9VHtnlw.js → apiBuild-DZF_f0_I.js} +46 -46
- package/dist/appPort-B_HpJ_ck.js +48 -0
- package/dist/baselineCommand-C2ClWZN3.js +2 -0
- package/dist/baselineCommand-DIttzO8A.js +227 -0
- package/dist/bin.js +71 -28
- package/dist/build-CE7Ca9gm.js +711 -0
- package/dist/cacheCommand-DA4OH9xt.js +42 -0
- package/dist/capabilitiesCommand-nq_pz5xd.js +123 -0
- package/dist/checkCommand-Dv8r9tGO.js +231 -0
- package/dist/checkCommand-YZDmkAGd.js +2 -0
- package/dist/{cliArgs-qdZSElM3.js → cliArgs-D4p8n7EE.js} +12 -1
- package/dist/cliError-BmdYnghb.js +10 -0
- package/dist/cliOutput-D1tSBoRM.js +15 -0
- package/dist/{cliRuntime-Oh517vCV.js → cliRuntime-Dh7UDinH.js} +20 -20
- package/dist/cloudClient-DWL-Hw_T.js +67 -0
- package/dist/cloudCmd-Cvv5HGaZ.js +364 -0
- package/dist/clusterCmd-CNXKlcvD.js +54 -0
- package/dist/codegen-CYM3Zqrf.js +605 -0
- package/dist/codegen-ChBi_hVa.js +2 -0
- package/dist/codegenCommand-DC6w2tNZ.js +30 -0
- package/dist/codemodRunner-DRRqXR74.js +5243 -0
- package/dist/commandRunner-BLAEFLjp.js +47 -0
- package/dist/commands-gutsz-Ac.js +808 -0
- package/dist/connectionConfig-UFlIEiys.js +66 -0
- package/dist/dashboardCommand-3YG8p-UA.js +25 -0
- package/dist/dataCommand-qL0r7fPO.js +535 -0
- package/dist/dataProfile-dW-PsfLB.js +15 -0
- package/dist/{dbCommand-DTLKAfbA.js → dbCommand-B6X0FZbq.js} +551 -401
- package/dist/dbCommand-CpdKLeQq.js +2 -0
- package/dist/{dev-C_P8FLSx.js → dev-CerMd0mW.js} +3122 -3129
- package/dist/dev-CoG-ZPx8.js +3 -0
- package/dist/devActivity-Dx_3nnGv.js +100 -0
- package/dist/devActivity.js +1 -1
- package/dist/dialectDriver-CgXnDfec.js +39 -0
- package/dist/discover-C9XKJDco.js +25 -0
- package/dist/doctorCommand-BvqGBwNG.js +2 -0
- package/dist/{checkCommand-xGhRFFg2.js → doctorCommand-CnDXQxfa.js} +520 -1278
- package/dist/dormancyCommand-Dszo57d6.js +69 -0
- package/dist/e2eCmd-BRabZww-.js +147 -0
- package/dist/embeddingsCommand-C0sKVRo1.js +73 -0
- package/dist/envCommand-DPgdV1Bq.js +60 -0
- package/dist/evalCommand-6RUfPen4.js +118 -0
- package/dist/evolveCommand-DHpkgjgH.js +281 -0
- package/dist/fileTaxonomy-CJfgOllU.js +457 -0
- package/dist/frameworkTableAssembly-BGHmck-x.js +2 -0
- package/dist/{frameworkTableAssembly-BwIrO5nv.js → frameworkTableAssembly-DkkP6BgC.js} +184 -148
- package/dist/generateCommand-oibemh97.js +147 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +4 -3
- package/dist/infoCommand-BJw9nLUR.js +60 -0
- package/dist/{inspect-BUUjt773.js → inspect-CBqFtAKA.js} +82 -40
- package/dist/inspect-C_T_WGvl.js +2 -0
- package/dist/inspectCmd-Bppy-GGw.js +224 -0
- package/dist/inspectFetch-Cm8_wVvp.js +151 -0
- package/dist/inspectMetrics-CfdKLh6t.js +72 -0
- package/dist/loadEnv-D9nEOClM.js +44 -0
- package/dist/logFileSink-C_D2wRN1.js +105 -0
- package/dist/logsCmd-CCca3KRZ.js +260 -0
- package/dist/manifestBuild-ChsKAhmn.js +2 -0
- package/dist/{manifestBuild-BLrVuSlM.js → manifestBuild-sxpwdKY1.js} +1 -1
- package/dist/metaCommands-7MJfZ5cf.js +196 -0
- package/dist/migrate-CBwOt_iV.js +83 -0
- package/dist/mssqlClusterPatch-_4cE_nun.js +44 -0
- package/dist/newCommand-COWOJ1_E.js +156 -0
- package/dist/nodeEnvironment-cGFAj1J8.js +28 -0
- package/dist/packageCommand-Cug_3Ogl.js +271 -0
- package/dist/pageConvention-cEiRxdab.js +5 -0
- package/dist/privacyCommand-C-Df56U_.js +146 -0
- package/dist/projectScaffold-DmzEKHib.js +2 -0
- package/dist/projectScaffold-LMMtaavR.js +814 -0
- package/dist/renderModeScan-D7J1B7Kw.js +105 -0
- package/dist/renderProfile-1OWWAAtx.js +81 -0
- package/dist/runtimeRegistry-DMeKfTHP.js +81 -0
- package/dist/runtimeTrace-CRxalXTs.js +91 -0
- package/dist/scheduleCmd--jksTrf6.js +69 -0
- package/dist/scheduleManifestCmd-D2x0CTTY.js +249 -0
- package/dist/schemaIr-UJybUUZW.js +103 -0
- package/dist/{sdkgen-C81QIkiL.js → sdkgen-CYJscZC7.js} +111 -209
- package/dist/seedRunner-TFHHiToI.js +329 -0
- package/dist/serveCommand-B_isw7q4.js +1647 -0
- package/dist/serveCommand-DOvbgRnQ.js +2 -0
- package/dist/serveEntry.js +5 -5
- package/dist/serverlessCommand-CfJZy6dS.js +482 -0
- package/dist/start-9LiUOfES.js +1087 -0
- package/dist/start-B-9Nsp-S.js +3 -0
- package/dist/startEntry.js +2 -2
- package/dist/staticCommand-Dr2M6tpU.js +304 -0
- package/dist/storageCommand-Co6NfLqN.js +42 -0
- package/dist/templates-De8IR5-c.js +102 -0
- package/dist/test-CI6iDsYc.js +115 -0
- package/dist/tracesCmd-CkEZQrtt.js +232 -0
- package/dist/tsconfigPaths-BWXBWgcl.js +107 -0
- package/dist/tsxLoader-EuXmSJ1K.js +51 -0
- package/dist/typecheckCommand-BlsWiCNq.js +61 -0
- package/dist/updateCommand-Bkptutss.js +585 -0
- package/dist/updateCommand-us1_hdIC.js +2 -0
- package/dist/{inspectMetrics-1xzTKAFx.js → webDev-CBYvPqQr.js} +975 -1559
- package/dist/webDev-Cg-fFiyd2.js +2 -0
- package/dist/webhookDiscovery-CrGAfhIG.js +2 -0
- package/dist/webhookDiscovery-D7VaeMlz.js +51 -0
- package/dist/webhooksCommand-CID96Rga.js +267 -0
- package/dist/workflowsCmd-D1VTmLMY.js +608 -0
- package/package.json +179 -17
- package/templates/AGENTS.core.md +58 -3
- package/templates/AGENTS.md +64 -7
- package/templates/agent-docs/_index.md +6 -4
- package/templates/agent-docs/_manifest.json +22 -5
- package/templates/agent-docs/ai.md +370 -0
- package/templates/agent-docs/authentication.md +265 -31
- package/templates/agent-docs/caching.md +6 -0
- package/templates/agent-docs/cli.md +794 -50
- package/templates/agent-docs/data.md +550 -11
- package/templates/agent-docs/database/migrations.md +174 -25
- package/templates/agent-docs/database/misc.md +156 -40
- package/templates/agent-docs/database/querying.md +19 -1
- package/templates/agent-docs/database/scaling.md +60 -0
- package/templates/agent-docs/database/schema.md +1 -0
- package/templates/agent-docs/database/seedsdialects.md +208 -19
- package/templates/agent-docs/database/transactions.md +68 -0
- package/templates/agent-docs/deployment.md +69 -3
- package/templates/agent-docs/introduction.md +87 -16
- package/templates/agent-docs/local-first-mobile.md +79 -4
- package/templates/agent-docs/multi-tenancy.md +95 -20
- package/templates/agent-docs/observability.md +58 -3
- package/templates/agent-docs/plugins/ai-flows.md +161 -2
- package/templates/agent-docs/plugins/analytics-postgres.md +1 -1
- package/templates/agent-docs/plugins/audit.md +37 -1
- package/templates/agent-docs/plugins/auth-social.md +143 -0
- package/templates/agent-docs/plugins/auth-workos.md +4 -2
- package/templates/agent-docs/plugins/auth.md +131 -6
- package/templates/agent-docs/plugins/billing.md +132 -15
- package/templates/agent-docs/plugins/cdc-out.md +46 -7
- package/templates/agent-docs/plugins/clickhouse.md +1 -1
- package/templates/agent-docs/plugins/duckdb.md +1 -1
- package/templates/agent-docs/plugins/flags.md +132 -0
- package/templates/agent-docs/plugins/governance.md +105 -7
- package/templates/agent-docs/plugins/multitenancy.md +9 -4
- package/templates/agent-docs/plugins/presence.md +13 -2
- package/templates/agent-docs/plugins/ratelimit.md +9 -0
- package/templates/agent-docs/plugins/search.md +157 -6
- package/templates/agent-docs/plugins/sso-saml.md +47 -8
- package/templates/agent-docs/plugins/webhooks.md +105 -0
- package/templates/agent-docs/plugins.md +150 -18
- package/templates/agent-docs/reference.md +60 -3
- package/templates/agent-docs/releases.md +1117 -0
- package/templates/agent-docs/routing.md +43 -25
- package/templates/agent-docs/scheduling.md +10 -0
- package/templates/agent-docs/schema-driven-ui.md +92 -12
- package/templates/agent-docs/security.md +426 -0
- package/templates/agent-docs/templates/apibackends.md +87 -18
- package/templates/agent-docs/templates/appshells.md +32 -14
- package/templates/agent-docs/templates/overview.md +13 -8
- package/templates/agent-docs/testing.md +211 -14
- package/templates/agent-docs/whats-new.md +1700 -104
- package/templates/agent-docs/workflows.md +130 -14
- package/templates/apps/api-ai/actions/summarize.action.ts +11 -0
- package/templates/apps/api-ai/package.json +8 -7
- package/templates/apps/api-auth/actions/me.action.ts +13 -0
- package/templates/apps/api-auth/package.json +9 -8
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend/package.json +12 -8
- package/templates/apps/api-backend/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +14 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +8 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +10 -0
- package/templates/apps/api-backend-deactivation/package.json +8 -7
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +17 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mail/package.json +9 -8
- package/templates/apps/api-backend-mail/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-mariadb/.env.example +14 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-mariadb/package.json +10 -9
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-sqlite/.env.example +14 -0
- package/templates/apps/api-backend-sqlite/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-sqlite/package.json +9 -8
- package/templates/apps/api-backend-sqlite/queries/notes.query.ts +30 -8
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +13 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +12 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +9 -0
- package/templates/apps/api-backend-storage/package.json +9 -8
- package/templates/apps/api-backend-storage/queries/notes.query.ts +30 -8
- package/templates/apps/api-cms/actions/content.get.action.ts +7 -0
- package/templates/apps/api-cms/actions/content.types.action.ts +6 -0
- package/templates/apps/api-cms/actions/me.action.ts +13 -0
- package/templates/apps/api-cms/app.config.ts +19 -0
- package/templates/apps/api-cms/authz.ts +63 -0
- package/templates/apps/api-cms/mutations/content.publish.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.saveDraft.mutation.ts +10 -0
- package/templates/apps/api-cms/mutations/content.unpublish.mutation.ts +5 -0
- package/templates/apps/api-cms/package.json +11 -10
- package/templates/apps/api-cms/queries/content.list.query.ts +23 -7
- package/templates/apps/api-cms/tests/accessDecisions.test.ts +121 -0
- package/templates/apps/api-cms/tests/content.descriptors.test.ts +8 -4
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +8 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +13 -0
- package/templates/apps/api-collab/package.json +9 -8
- package/templates/apps/api-collab/queries/documents.query.ts +19 -7
- package/templates/apps/api-data-advanced/package.json +9 -8
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +12 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +9 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +17 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +9 -0
- package/templates/apps/api-durable/package.json +9 -8
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +16 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +12 -0
- package/templates/apps/api-feature-flags/package.json +10 -9
- package/templates/apps/api-governance/README.md +30 -8
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -1
- package/templates/apps/api-governance/actions/profiles.get.action.ts +29 -4
- package/templates/apps/api-governance/database/schema.ts +16 -4
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +12 -0
- package/templates/apps/api-governance/package.json +9 -8
- package/templates/apps/api-kv/actions/sync.pull.action.ts +15 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +13 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +7 -0
- package/templates/apps/api-kv/package.json +9 -8
- package/templates/apps/api-kv/queries/events.list.query.ts +19 -8
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +11 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +13 -0
- package/templates/apps/api-moderation/package.json +9 -8
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +8 -0
- package/templates/apps/api-observability/package.json +9 -8
- package/templates/apps/api-observability/queries/notes.list.query.ts +13 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +14 -0
- package/templates/apps/api-ratelimit/package.json +9 -8
- package/templates/apps/api-rbac/package.json +9 -8
- package/templates/apps/api-rest/package.json +8 -7
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +13 -0
- package/templates/apps/api-saas/package.json +12 -11
- package/templates/apps/api-saas/queries/projects.list.query.ts +11 -0
- package/templates/apps/api-saas-starter/actions/me.action.ts +13 -0
- package/templates/apps/api-saas-starter/app.config.ts +19 -0
- package/templates/apps/api-saas-starter/authz.ts +75 -0
- package/templates/apps/api-saas-starter/mutations/invites.create.mutation.ts +10 -0
- package/templates/apps/api-saas-starter/mutations/projects.create.mutation.ts +9 -0
- package/templates/apps/api-saas-starter/package.json +15 -11
- package/templates/apps/api-saas-starter/queries/invites.list.query.ts +19 -8
- package/templates/apps/api-saas-starter/queries/projects.list.query.ts +18 -7
- package/templates/apps/api-saas-starter/tests/accessDecisions.test.ts +135 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +14 -0
- package/templates/apps/api-search/package.json +9 -8
- package/templates/apps/api-search/queries/articles.list.query.ts +21 -8
- package/templates/apps/api-status/README.md +10 -3
- package/templates/apps/api-status/app.config.ts +8 -3
- package/templates/apps/api-status/authz.ts +5 -3
- package/templates/apps/api-status/package.json +9 -8
- package/templates/apps/api-status/queries/components.list.query.ts +14 -6
- package/templates/apps/api-status/queries/incidents.live.query.ts +23 -10
- package/templates/apps/api-status/queries/updates.list.query.ts +16 -9
- package/templates/apps/api-status/tests/status.test.ts +9 -1
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +12 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +13 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +12 -0
- package/templates/apps/api-versioning/package.json +9 -8
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +16 -0
- package/templates/apps/api-webhooks/package.json +10 -9
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +10 -0
- package/templates/apps/changelog/package.json +8 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +10 -8
- package/templates/apps/frontend-admin/src/lib/admin.ts +20 -9
- package/templates/apps/frontend-admin/src/locales/de.ts +11 -1
- package/templates/apps/frontend-admin/src/locales/en.ts +13 -1
- package/templates/apps/frontend-admin/src/pages/admin/[entity]/page.tsx +65 -22
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +130 -23
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +3 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.test.tsx +19 -2
- package/templates/apps/frontend-admin/src/pages/admin/page.tsx +9 -4
- package/templates/apps/frontend-app/app.config.ts +4 -3
- package/templates/apps/frontend-app/package.json +11 -8
- package/templates/apps/frontend-app/src/lib/api.ts +25 -0
- package/templates/apps/frontend-app/src/pages/page.test.tsx +130 -82
- package/templates/apps/frontend-app/src/pages/page.tsx +14 -18
- package/templates/apps/frontend-auth/package.json +10 -8
- package/templates/apps/frontend-blank/package.json +9 -7
- package/templates/apps/frontend-cms/package.json +11 -9
- package/templates/apps/frontend-collab/package.json +12 -9
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +122 -78
- package/templates/apps/frontend-contact/package.json +9 -7
- package/templates/apps/frontend-dashboard/package.json +9 -7
- package/templates/apps/frontend-docs/package.json +9 -7
- package/templates/apps/frontend-i18n/package.json +8 -6
- package/templates/apps/frontend-landing/package.json +9 -7
- package/templates/apps/frontend-portal/package.json +10 -8
- package/templates/apps/frontend-saas/app.config.ts +10 -6
- package/templates/apps/frontend-saas/package.json +10 -8
- package/templates/apps/frontend-saas/src/lib/api.ts +27 -32
- package/templates/apps/frontend-saas/src/pages/dashboard/billing/page.tsx +7 -8
- package/templates/apps/frontend-saas/src/pages/dashboard/page.test.tsx +27 -3
- package/templates/apps/frontend-saas/src/pages/dashboard/page.tsx +4 -4
- package/templates/apps/frontend-saas/src/pages/dashboard/team/page.tsx +3 -4
- package/templates/apps/frontend-spa/package.json +9 -7
- package/templates/apps/frontend-ssr/package.json +9 -7
- package/templates/apps/frontend-ssr-api/package.json +10 -8
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-status/package.json +10 -8
- package/templates/apps/mobile-app/README.md +1 -0
- package/templates/apps/mobile-app/package.json +4 -2
- package/templates/apps/mobile-app/src/app/index.tsx +22 -12
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts +34 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +4 -2
- package/templates/baselines/bare/.env.example +14 -0
- package/templates/baselines/bare/baseline.json +4 -4
- package/templates/baselines/compose/.env.example +14 -0
- package/templates/baselines/compose/README.md +1 -1
- package/templates/baselines/compose/baseline.json +5 -5
- package/templates/baselines/compose-mariadb/.env.example +14 -0
- package/templates/baselines/compose-mariadb/README.md +1 -1
- package/templates/baselines/compose-mariadb/baseline.json +5 -5
- package/templates/baselines/helm/.env.example +14 -0
- package/templates/baselines/helm/baseline.json +4 -4
- package/dist/apiBuild-C-x9YacA.js +0 -2
- package/dist/checkCommand-DRovTKza.js +0 -2
- package/dist/commands-CJfepbm4.js +0 -11541
- package/dist/dbCommand-b1gum4td.js +0 -2
- package/dist/dev-iiMtlkfs.js +0 -3
- package/dist/devActivity-BhIu6ncs.js +0 -159
- package/dist/frameworkTableAssembly-D-EebUQX.js +0 -2
- package/dist/inspect-mmBuRXmy.js +0 -2
- package/dist/manifestBuild-Dj8Jjoto.js +0 -2
- package/dist/seedRunner-Bqxgp7HZ.js +0 -230
- package/dist/serveCommand-DdaM4Hup.js +0 -1608
- package/dist/start-C0koT0UO.js +0 -1084
- /package/templates/apps/api-ai/actions/{summarize.action.server.tsx → summarize.action.server.ts} +0 -0
|
@@ -1,55 +1,69 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
//
|
|
3
|
-
// `/` — the collaborative editor
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
// `/` — the collaborative editor, driven through `makeVoltroTestClient`
|
|
4
|
+
// (`@voltro/testing/client`) rather than a hand-rolled `vi.mock('@voltro/client')`.
|
|
5
|
+
//
|
|
6
|
+
// WHY THE HARNESS HERE and not a mock. This page is stateful: a local CRDT
|
|
7
|
+
// handle in a ref, seeded from the server body, folding every INCOMING body
|
|
8
|
+
// back in. A `vi.mock` can return one snapshot per mount, so the fold —
|
|
9
|
+
// the whole point of the page — was untestable, and the suite that replaced
|
|
10
|
+
// this one never pushed a second value into a mounted tree. `setSubscription`
|
|
11
|
+
// is a real server delta, so the last case below asserts convergence.
|
|
12
|
+
//
|
|
13
|
+
// Two other things fall out of it: the real `@voltro/client` hooks run (so a
|
|
14
|
+
// change in their loading/error semantics is caught here instead of being
|
|
15
|
+
// re-implemented by the mock), and the page can be imported statically —
|
|
16
|
+
// `await import('./page')` only existed to sequence around `vi.mock`'s hoist.
|
|
17
|
+
//
|
|
18
|
+
// `@voltro/local-first` is NOT faked either: the editor's real CRDT handle
|
|
19
|
+
// decodes the seeded body and turns keystrokes into encoded updates, which is
|
|
20
|
+
// the behaviour worth asserting.
|
|
21
|
+
|
|
22
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
11
23
|
import { act, createElement, type ReactNode } from 'react'
|
|
12
24
|
import { createRoot, type Root } from 'react-dom/client'
|
|
13
25
|
import { I18nProvider } from '@voltro/i18n'
|
|
14
|
-
import { crdtText } from '@voltro/local-first'
|
|
26
|
+
import { crdtText, type CrdtState } from '@voltro/local-first'
|
|
27
|
+
import { makeVoltroTestClient, type VoltroTestClient } from '@voltro/testing/client'
|
|
15
28
|
import enCatalog from '../locales/en'
|
|
29
|
+
import IndexPage from './page'
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
data: unknown
|
|
19
|
-
revision?: number
|
|
20
|
-
error?: unknown
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const useSubscription = vi.fn<() => SubState>()
|
|
24
|
-
const createMutate = vi.fn<(input: unknown) => Promise<unknown>>()
|
|
25
|
-
const setBodyMutate = vi.fn<(input: unknown) => Promise<unknown>>()
|
|
31
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
const useMutation = vi.fn((_api: unknown, tag: unknown) =>
|
|
29
|
-
tag === 'documents.create'
|
|
30
|
-
? { mutate: createMutate, pending: false, error: undefined, data: undefined }
|
|
31
|
-
: { mutate: setBodyMutate, pending: false, error: undefined, data: undefined },
|
|
32
|
-
)
|
|
33
|
+
const LIST = 'documents.list'
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}))
|
|
35
|
+
let container: HTMLDivElement
|
|
36
|
+
let root: Root
|
|
37
|
+
let client: VoltroTestClient
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/** A CRDT state holding `text`, from a fresh document. */
|
|
40
|
+
const bodyWith = (text: string): CrdtState => crdtText().insert(0, text).encode()
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
/** A REMOTE edit derived from `base`, so merging it back converges instead of
|
|
43
|
+
* interleaving two independently-created runs of the same characters. */
|
|
44
|
+
const remoteEditOf = (base: CrdtState, at: number, insert: string): CrdtState =>
|
|
45
|
+
crdtText().merge(base).insert(at, insert).encode()
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
const docRow = (over: Partial<{ id: string; title: string; body: CrdtState | null }> = {}) => ({
|
|
48
|
+
id: over.id ?? 'doc_1',
|
|
49
|
+
title: over.title ?? 'Shared document',
|
|
50
|
+
body: over.body ?? null,
|
|
51
|
+
tenantId: 'acme',
|
|
52
|
+
createdAt: new Date(0),
|
|
53
|
+
})
|
|
45
54
|
|
|
46
|
-
const render = (
|
|
55
|
+
const render = (): void => {
|
|
47
56
|
container = document.createElement('div')
|
|
48
57
|
document.body.appendChild(container)
|
|
49
58
|
act(() => {
|
|
50
59
|
root = createRoot(container)
|
|
51
60
|
root.render(
|
|
52
|
-
createElement(I18nProvider, {
|
|
61
|
+
createElement(I18nProvider, {
|
|
62
|
+
locale: 'en',
|
|
63
|
+
messages: enCatalog,
|
|
64
|
+
defaultLocale: 'en',
|
|
65
|
+
children: createElement(client.Provider, null, createElement(IndexPage) as ReactNode),
|
|
66
|
+
}),
|
|
53
67
|
)
|
|
54
68
|
})
|
|
55
69
|
}
|
|
@@ -62,26 +76,20 @@ const setTextarea = (el: HTMLTextAreaElement, value: string): void => {
|
|
|
62
76
|
})
|
|
63
77
|
}
|
|
64
78
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
79
|
+
const textarea = (): HTMLTextAreaElement => container.querySelector('textarea') as HTMLTextAreaElement
|
|
80
|
+
|
|
81
|
+
/** The harness with both writes wired. Handlers return void; the assertions
|
|
82
|
+
* read `client.calls`, which records tag + input in order. */
|
|
83
|
+
const harness = (subscriptions: Readonly<Record<string, unknown>> = {}): VoltroTestClient =>
|
|
84
|
+
makeVoltroTestClient({
|
|
85
|
+
subscriptions,
|
|
86
|
+
mutations: {
|
|
87
|
+
'documents.create': () => undefined,
|
|
88
|
+
'documents.setBody': () => undefined,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
76
91
|
|
|
77
|
-
beforeEach(() => {
|
|
78
|
-
useSubscription.mockReset()
|
|
79
|
-
createMutate.mockReset()
|
|
80
|
-
createMutate.mockResolvedValue(undefined)
|
|
81
|
-
setBodyMutate.mockReset()
|
|
82
|
-
setBodyMutate.mockResolvedValue(undefined)
|
|
83
|
-
useMutation.mockClear()
|
|
84
|
-
})
|
|
92
|
+
beforeEach(() => { client = harness() })
|
|
85
93
|
|
|
86
94
|
afterEach(() => {
|
|
87
95
|
if (root) act(() => root.unmount())
|
|
@@ -90,24 +98,25 @@ afterEach(() => {
|
|
|
90
98
|
})
|
|
91
99
|
|
|
92
100
|
describe('collab editor — subscription states', () => {
|
|
93
|
-
test('shows the loading hint before the first snapshot
|
|
94
|
-
|
|
95
|
-
|
|
101
|
+
test('shows the loading hint before the first snapshot', () => {
|
|
102
|
+
// A tag with NO fixture is the harness's cold state — the real thing a
|
|
103
|
+
// component sees before the first delta arrives.
|
|
104
|
+
render()
|
|
96
105
|
expect(container.textContent).toContain('loading…')
|
|
97
106
|
expect(container.querySelector('textarea')).toBeNull()
|
|
98
107
|
})
|
|
99
108
|
|
|
100
109
|
test('shows the empty state + create button once a snapshot arrives with no documents', () => {
|
|
101
|
-
|
|
102
|
-
render(
|
|
110
|
+
client = harness({ [LIST]: [] })
|
|
111
|
+
render()
|
|
103
112
|
expect(container.textContent).toContain('No document yet')
|
|
104
113
|
expect(container.querySelector('button')).not.toBeNull()
|
|
105
114
|
expect(container.querySelector('textarea')).toBeNull()
|
|
106
115
|
})
|
|
107
116
|
|
|
108
117
|
test('renders the error banner when the subscription errors', () => {
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
render()
|
|
119
|
+
act(() => { client.failSubscription(LIST, 'boom') })
|
|
111
120
|
expect(container.textContent).toContain('subscription error')
|
|
112
121
|
expect(container.textContent).toContain('boom')
|
|
113
122
|
})
|
|
@@ -115,36 +124,71 @@ describe('collab editor — subscription states', () => {
|
|
|
115
124
|
|
|
116
125
|
describe('collab editor — create', () => {
|
|
117
126
|
test('clicking Create document calls documents.create with the tenant + a title', async () => {
|
|
118
|
-
|
|
119
|
-
render(
|
|
127
|
+
client = harness({ [LIST]: [] })
|
|
128
|
+
render()
|
|
120
129
|
const button = container.querySelector('button') as HTMLButtonElement
|
|
121
130
|
await act(async () => {
|
|
122
131
|
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
123
132
|
})
|
|
124
|
-
expect(
|
|
125
|
-
|
|
133
|
+
expect(client.calls).toEqual([
|
|
134
|
+
{ kind: 'mutation', tag: 'documents.create', input: { tenantId: 'acme', title: 'Shared document' } },
|
|
135
|
+
])
|
|
126
136
|
})
|
|
127
137
|
})
|
|
128
138
|
|
|
129
139
|
describe('collab editor — editing a document', () => {
|
|
130
140
|
test('binds the textarea to the decoded CRDT body', () => {
|
|
131
|
-
|
|
132
|
-
render(
|
|
133
|
-
|
|
134
|
-
expect(textarea).
|
|
135
|
-
expect(textarea.value).toBe('Hello')
|
|
141
|
+
client = harness({ [LIST]: [docRow({ body: bodyWith('Hello') })] })
|
|
142
|
+
render()
|
|
143
|
+
expect(textarea()).not.toBeNull()
|
|
144
|
+
expect(textarea().value).toBe('Hello')
|
|
136
145
|
expect(container.textContent).toContain('Shared document')
|
|
137
146
|
})
|
|
138
147
|
|
|
139
148
|
test('typing sends an encoded CRDT update via documents.setBody', () => {
|
|
140
|
-
|
|
141
|
-
render(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
expect(
|
|
149
|
+
client = harness({ [LIST]: [docRow({ body: bodyWith('Hello') })] })
|
|
150
|
+
render()
|
|
151
|
+
setTextarea(textarea(), 'Hello!')
|
|
152
|
+
|
|
153
|
+
expect(client.calls).toHaveLength(1)
|
|
154
|
+
const call = client.calls[0]!
|
|
155
|
+
expect(call.tag).toBe('documents.setBody')
|
|
156
|
+
const input = call.input as { id: string; update: unknown }
|
|
157
|
+
expect(input.id).toBe('doc_1')
|
|
158
|
+
expect(input.update).toBeInstanceOf(Uint8Array)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// The case the hand-rolled mock could not express, and the page's whole
|
|
162
|
+
// reason to exist: a body edited in ANOTHER tab arrives as a delta, and the
|
|
163
|
+
// local handle folds it in without remounting the editor.
|
|
164
|
+
test('a remote edit pushed by the server converges into the open editor', () => {
|
|
165
|
+
const base = bodyWith('Hello')
|
|
166
|
+
client = harness({ [LIST]: [docRow({ body: base })] })
|
|
167
|
+
render()
|
|
168
|
+
expect(textarea().value).toBe('Hello')
|
|
169
|
+
|
|
170
|
+
act(() => {
|
|
171
|
+
client.setSubscription(LIST, [docRow({ body: remoteEditOf(base, 5, ' world') })])
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(textarea().value).toBe('Hello world')
|
|
175
|
+
// A fold is not a write — nothing is echoed back to the server.
|
|
176
|
+
expect(client.calls).toEqual([])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('a local edit survives a concurrent remote edit — both are in the merged text', () => {
|
|
180
|
+
const base = bodyWith('Hello')
|
|
181
|
+
client = harness({ [LIST]: [docRow({ body: base })] })
|
|
182
|
+
render()
|
|
183
|
+
setTextarea(textarea(), 'Hello!')
|
|
184
|
+
expect(textarea().value).toBe('Hello!')
|
|
185
|
+
|
|
186
|
+
// The other tab branched from the SAME base and appended elsewhere.
|
|
187
|
+
act(() => {
|
|
188
|
+
client.setSubscription(LIST, [docRow({ body: remoteEditOf(base, 0, 'Oh, ') })])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
expect(textarea().value).toContain('Oh, ')
|
|
192
|
+
expect(textarea().value).toContain('!')
|
|
149
193
|
})
|
|
150
194
|
})
|
|
@@ -15,21 +15,23 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@effect/platform": "^0.97.0",
|
|
18
|
-
"@voltro/cli": "0.
|
|
19
|
-
"@voltro/client": "0.
|
|
20
|
-
"@voltro/env": "0.
|
|
21
|
-
"@voltro/i18n": "0.
|
|
22
|
-
"@voltro/serverless": "0.
|
|
23
|
-
"@voltro/web": "0.
|
|
18
|
+
"@voltro/cli": "0.34.0",
|
|
19
|
+
"@voltro/client": "0.34.0",
|
|
20
|
+
"@voltro/env": "0.34.0",
|
|
21
|
+
"@voltro/i18n": "0.34.0",
|
|
22
|
+
"@voltro/serverless": "0.34.0",
|
|
23
|
+
"@voltro/web": "0.34.0",
|
|
24
24
|
"effect": "^3.22.0",
|
|
25
25
|
"react": "^19.0.0",
|
|
26
26
|
"react-dom": "^19.0.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@voltro/devtools": "0.
|
|
29
|
+
"@voltro/devtools": "0.34.0",
|
|
30
30
|
"@types/react": "^19.0.0",
|
|
31
31
|
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"jsdom": "^30.0.0",
|
|
32
33
|
"typescript": "^6.0.3",
|
|
34
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
33
35
|
"vitest": "^4.1.10"
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.34.0",
|
|
16
|
+
"@voltro/client": "0.34.0",
|
|
17
|
+
"@voltro/env": "0.34.0",
|
|
18
|
+
"@voltro/i18n": "0.34.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
20
|
+
"@voltro/web": "0.34.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.34.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"jsdom": "^30.0.0",
|
|
28
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
31
|
"vitest": "^4.1.10"
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.34.0",
|
|
16
|
+
"@voltro/client": "0.34.0",
|
|
17
|
+
"@voltro/env": "0.34.0",
|
|
18
|
+
"@voltro/i18n": "0.34.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
20
|
+
"@voltro/web": "0.34.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.34.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"jsdom": "^30.0.0",
|
|
28
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
31
|
"vitest": "^4.1.10"
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -12,19 +12,21 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.34.0",
|
|
16
|
+
"@voltro/client": "0.34.0",
|
|
17
|
+
"@voltro/env": "0.34.0",
|
|
18
|
+
"@voltro/i18n": "0.34.0",
|
|
19
|
+
"@voltro/web": "0.34.0",
|
|
20
20
|
"react": "^19.0.0",
|
|
21
21
|
"react-dom": "^19.0.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/devtools": "0.
|
|
24
|
+
"@voltro/devtools": "0.34.0",
|
|
25
25
|
"@types/react": "^19.0.0",
|
|
26
26
|
"@types/react-dom": "^19.0.0",
|
|
27
|
+
"jsdom": "^30.0.0",
|
|
27
28
|
"typescript": "^6.0.3",
|
|
29
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
28
30
|
"vitest": "^4.1.10"
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -12,20 +12,22 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/client": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/i18n": "0.
|
|
19
|
-
"@voltro/ui-shadcn": "0.
|
|
20
|
-
"@voltro/web": "0.
|
|
15
|
+
"@voltro/cli": "0.34.0",
|
|
16
|
+
"@voltro/client": "0.34.0",
|
|
17
|
+
"@voltro/env": "0.34.0",
|
|
18
|
+
"@voltro/i18n": "0.34.0",
|
|
19
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
20
|
+
"@voltro/web": "0.34.0",
|
|
21
21
|
"react": "^19.0.0",
|
|
22
22
|
"react-dom": "^19.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@voltro/devtools": "0.
|
|
25
|
+
"@voltro/devtools": "0.34.0",
|
|
26
26
|
"@types/react": "^19.0.0",
|
|
27
27
|
"@types/react-dom": "^19.0.0",
|
|
28
|
+
"jsdom": "^30.0.0",
|
|
28
29
|
"typescript": "^6.0.3",
|
|
30
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
29
31
|
"vitest": "^4.1.10"
|
|
30
32
|
}
|
|
31
33
|
}
|
|
@@ -12,21 +12,23 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/client": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/i18n": "0.34.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
21
|
+
"@voltro/web": "0.34.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.34.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"jsdom": "^30.0.0",
|
|
29
30
|
"typescript": "^6.0.3",
|
|
31
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
30
32
|
"vitest": "^4.1.10"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
// `renderMode: 'ssr'` so the gate runs per request.
|
|
12
12
|
//
|
|
13
13
|
// The pages read the api live with `useSubscription` and write with
|
|
14
|
-
// `useMutation` / `useAction
|
|
14
|
+
// `useMutation` / `useAction` — imported from `src/lib/api.ts`, which binds them
|
|
15
|
+
// ONCE to this api's generated procedure map so every tag and every input/output
|
|
16
|
+
// type is checked by the compiler. The auth forms POST to the api's `/auth/*`.
|
|
15
17
|
import { defineEnv, envVar } from '@voltro/env'
|
|
16
18
|
|
|
17
19
|
// Public, build-time env (baked into the bundle; read via `publicEnv`).
|
|
@@ -35,11 +37,13 @@ export default {
|
|
|
35
37
|
// grow the app to more languages.
|
|
36
38
|
locales: ['en', 'de'] as const,
|
|
37
39
|
defaultLocale: 'en' as const,
|
|
38
|
-
// The api this dashboard consumes. The NAME ('app') is the lookup key
|
|
39
|
-
//
|
|
40
|
-
// `
|
|
41
|
-
//
|
|
42
|
-
//
|
|
40
|
+
// The api this dashboard consumes. The NAME ('app') is the lookup key — spelled
|
|
41
|
+
// ONCE, in `src/lib/api.ts`'s `createHooks<AppProcedures>('app')`, and then
|
|
42
|
+
// never again: pages call `useSubscription('projects.list')` /
|
|
43
|
+
// `useMutation('projects.create')`. (A loader still names the tag itself:
|
|
44
|
+
// `query('session.me', {})`.) The api's port is auto-discovered from its own
|
|
45
|
+
// app.config.ts, and codegen pulls its typed rpc surface (including the
|
|
46
|
+
// plugins' `billing.*` / `/auth`) into that same map.
|
|
43
47
|
apis: {
|
|
44
48
|
app: { package: '@{{projectName}}/api' },
|
|
45
49
|
},
|
|
@@ -12,21 +12,23 @@
|
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@{{projectName}}/api": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/client": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/i18n": "0.
|
|
20
|
-
"@voltro/ui-shadcn": "0.
|
|
21
|
-
"@voltro/web": "0.
|
|
15
|
+
"@{{projectName}}/api": "0.34.0",
|
|
16
|
+
"@voltro/cli": "0.34.0",
|
|
17
|
+
"@voltro/client": "0.34.0",
|
|
18
|
+
"@voltro/env": "0.34.0",
|
|
19
|
+
"@voltro/i18n": "0.34.0",
|
|
20
|
+
"@voltro/ui-shadcn": "0.34.0",
|
|
21
|
+
"@voltro/web": "0.34.0",
|
|
22
22
|
"react": "^19.0.0",
|
|
23
23
|
"react-dom": "^19.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/devtools": "0.
|
|
26
|
+
"@voltro/devtools": "0.34.0",
|
|
27
27
|
"@types/react": "^19.0.0",
|
|
28
28
|
"@types/react-dom": "^19.0.0",
|
|
29
|
+
"jsdom": "^30.0.0",
|
|
29
30
|
"typescript": "^6.0.3",
|
|
31
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
30
32
|
"vitest": "^4.1.10"
|
|
31
33
|
}
|
|
32
34
|
}
|
|
@@ -1,37 +1,32 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// `
|
|
5
|
-
//
|
|
1
|
+
// The TYPED hook surface for the `app` api — bound ONCE, here, and imported by
|
|
2
|
+
// every page instead of `@voltro/client`.
|
|
3
|
+
//
|
|
4
|
+
// `AppProcedures` is emitted by codegen into the api's `rpcGroup.generated.ts`:
|
|
5
|
+
// a type-level map of every rpc tag (the app's own AND the plugins', e.g.
|
|
6
|
+
// `billing.*`) to the descriptor behind it. Feeding it to `createHooks` makes
|
|
7
|
+
// the tag a literal union and the input/output types INFER:
|
|
8
|
+
//
|
|
9
|
+
// const { data } = useSubscription('projects.list') // rows, typed
|
|
10
|
+
// const create = useMutation('projects.create') // input + output typed
|
|
11
|
+
//
|
|
12
|
+
// So the row shapes this file used to declare by hand are gone. They were a
|
|
13
|
+
// MIRROR of the api's descriptor outputs, kept in sync by nobody: the old
|
|
14
|
+
// `useSubscription<ReadonlyArray<Project>>('app', 'projects.list')` asserted a
|
|
15
|
+
// type nothing checked against the server, and a typo'd tag only showed up at
|
|
16
|
+
// runtime. Both are compile errors now.
|
|
17
|
+
//
|
|
18
|
+
// `'app'` is the key this web app gave the api in `app.config.ts` → `apis`.
|
|
19
|
+
// `import type` is erased, so the binding costs the browser bundle nothing.
|
|
20
|
+
import { createHooks } from '@voltro/client'
|
|
21
|
+
import type { AppProcedures } from '@{{projectName}}/api/rpcGroup'
|
|
6
22
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
readonly createdAt: string | Date
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface Invite {
|
|
15
|
-
readonly id: string
|
|
16
|
-
readonly email: string
|
|
17
|
-
readonly status: string
|
|
18
|
-
readonly tenantId: string
|
|
19
|
-
readonly createdAt: string | Date
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// `billing.subscription` streams `{ subscription: … | null }` — null before the
|
|
23
|
-
// tenant has any subscription row (they're on the implicit free plan).
|
|
24
|
-
export interface SubscriptionState {
|
|
25
|
-
readonly subscription: {
|
|
26
|
-
readonly plan: string
|
|
27
|
-
readonly status: string
|
|
28
|
-
readonly quantity: number
|
|
29
|
-
readonly currentPeriodEnd: string | null
|
|
30
|
-
readonly cancelAt: string | null
|
|
31
|
-
} | null
|
|
32
|
-
}
|
|
23
|
+
// Destructured on purpose — `react-hooks/rules-of-hooks` only treats a member
|
|
24
|
+
// call as a hook when the object is PascalCase, so top-level `useX` identifiers
|
|
25
|
+
// are what keep the React lint rules working at every call site.
|
|
26
|
+
export const { useSubscription, useMutation, useAction } = createHooks<AppProcedures>('app')
|
|
33
27
|
|
|
34
|
-
// The caller's resolved identity, from `session.me`. The SSR gate reads it
|
|
28
|
+
// The caller's resolved identity, from `session.me`. The SSR gate reads it, and
|
|
29
|
+
// a LOADER is not a hook — it calls `ctx.query` — so this shape stays hand-written.
|
|
35
30
|
export interface Identity {
|
|
36
31
|
readonly type: string
|
|
37
32
|
readonly id: string | null
|
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
// row yet means the tenant is on the implicit free plan.
|
|
7
7
|
import type { ReactNode } from 'react'
|
|
8
8
|
import type { PageMeta } from '@voltro/web'
|
|
9
|
-
import { useSubscription, useAction } from '@voltro/client'
|
|
10
9
|
import { T, useT } from '@voltro/i18n'
|
|
11
10
|
import { getCatalog } from '../../../locales/index'
|
|
12
|
-
import
|
|
11
|
+
import { useAction, useSubscription } from '../../../lib/api'
|
|
13
12
|
|
|
14
13
|
export const renderMode = 'ssr' as const
|
|
15
14
|
|
|
@@ -17,13 +16,13 @@ export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
|
17
16
|
title: getCatalog(locale)['meta.billing.title'],
|
|
18
17
|
})
|
|
19
18
|
|
|
20
|
-
interface CheckoutInput { plan: string; successUrl: string; cancelUrl: string }
|
|
21
|
-
interface SeatsInput { quantity: number }
|
|
22
|
-
|
|
23
19
|
export default function Billing(): ReactNode {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
// `billing.*` is contributed by @voltro/plugin-billing and lands in the same
|
|
21
|
+
// generated map as the app's own procedures, so a plugin route is typed here
|
|
22
|
+
// exactly like a hand-written one — no local Checkout/Seats input mirrors.
|
|
23
|
+
const { data } = useSubscription('billing.subscription')
|
|
24
|
+
const checkout = useAction('billing.startCheckout')
|
|
25
|
+
const seats = useAction('billing.changeSeats')
|
|
27
26
|
|
|
28
27
|
const sub = data?.subscription ?? null
|
|
29
28
|
const planLabel = useT('billing.plan.free')
|