@voltro/cli 0.1.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 +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +20236 -0
- package/bin/voltro.mjs +43 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +9 -0
- package/dist/commands-CfPH2Wf4.js +18061 -0
- package/dist/frameworkInspectState-CX2250XB.js +86 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +5 -0
- package/dist/inspectState.d.ts +116 -0
- package/dist/inspectState.js +2 -0
- package/dist/startup.d.ts +26 -0
- package/dist/startup.js +2 -0
- package/dist/startupRunner-CRhuUl91.js +71 -0
- package/package.json +88 -0
- package/templates/AGENTS.core.md +258 -0
- package/templates/AGENTS.md +351 -0
- package/templates/agent-docs/_index.md +93 -0
- package/templates/agent-docs/_manifest.json +655 -0
- package/templates/agent-docs/ai.md +1845 -0
- package/templates/agent-docs/authentication.md +1788 -0
- package/templates/agent-docs/caching.md +624 -0
- package/templates/agent-docs/cli.md +1650 -0
- package/templates/agent-docs/configuration.md +295 -0
- package/templates/agent-docs/data.md +2172 -0
- package/templates/agent-docs/database/advancedqueries.md +1583 -0
- package/templates/agent-docs/database/columntypes.md +1200 -0
- package/templates/agent-docs/database/hosting.md +881 -0
- package/templates/agent-docs/database/migrations.md +2938 -0
- package/templates/agent-docs/database/misc.md +270 -0
- package/templates/agent-docs/database/overview.md +108 -0
- package/templates/agent-docs/database/querying.md +1622 -0
- package/templates/agent-docs/database/scaling.md +331 -0
- package/templates/agent-docs/database/schema.md +1458 -0
- package/templates/agent-docs/database/seedsdialects.md +1235 -0
- package/templates/agent-docs/database/transactions.md +285 -0
- package/templates/agent-docs/deployment.md +999 -0
- package/templates/agent-docs/internationalization.md +359 -0
- package/templates/agent-docs/introduction.md +438 -0
- package/templates/agent-docs/multi-tenancy.md +610 -0
- package/templates/agent-docs/observability.md +350 -0
- package/templates/agent-docs/plugins.md +1273 -0
- package/templates/agent-docs/reference.md +978 -0
- package/templates/agent-docs/routing.md +1553 -0
- package/templates/agent-docs/scheduling.md +666 -0
- package/templates/agent-docs/schema-driven-ui.md +607 -0
- package/templates/agent-docs/security.md +42 -0
- package/templates/agent-docs/templates/apibackends.md +3214 -0
- package/templates/agent-docs/templates/appshells.md +2062 -0
- package/templates/agent-docs/templates/custom.md +128 -0
- package/templates/agent-docs/templates/overview.md +122 -0
- package/templates/agent-docs/templates/serverless.md +315 -0
- package/templates/agent-docs/testing.md +376 -0
- package/templates/agent-docs/workflows.md +1351 -0
- package/templates/apps/api-ai/README.md +105 -0
- package/templates/apps/api-ai/actions/summarize.action.server.tsx +32 -0
- package/templates/apps/api-ai/actions/summarize.action.ts +33 -0
- package/templates/apps/api-ai/agents/support.agent.server.tsx +31 -0
- package/templates/apps/api-ai/agents/support.agent.tsx +27 -0
- package/templates/apps/api-ai/app.config.ts +57 -0
- package/templates/apps/api-ai/database/schema.ts +67 -0
- package/templates/apps/api-ai/package.json +27 -0
- package/templates/apps/api-ai/seeds/docs.seed.ts +67 -0
- package/templates/apps/api-ai/template.json +6 -0
- package/templates/apps/api-ai/tests/summarize.test.ts +50 -0
- package/templates/apps/api-ai/tools/searchDocs.tool.tsx +60 -0
- package/templates/apps/api-ai/tsconfig.json +5 -0
- package/templates/apps/api-auth/.env +17 -0
- package/templates/apps/api-auth/README.md +109 -0
- package/templates/apps/api-auth/actions/me.action.server.ts +20 -0
- package/templates/apps/api-auth/actions/me.action.ts +19 -0
- package/templates/apps/api-auth/app.config.ts +59 -0
- package/templates/apps/api-auth/database/schema.ts +26 -0
- package/templates/apps/api-auth/package.json +28 -0
- package/templates/apps/api-auth/template.json +6 -0
- package/templates/apps/api-auth/tests/me.test.ts +37 -0
- package/templates/apps/api-auth/tsconfig.json +5 -0
- package/templates/apps/api-backend/README.md +25 -0
- package/templates/apps/api-backend/app.config.ts +39 -0
- package/templates/apps/api-backend/database/schema.ts +57 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend/package.json +28 -0
- package/templates/apps/api-backend/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend/template.json +6 -0
- package/templates/apps/api-backend/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend/tsconfig.json +5 -0
- package/templates/apps/api-backend-deactivation/README.md +49 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.server.ts +17 -0
- package/templates/apps/api-backend-deactivation/actions/users.get.action.ts +18 -0
- package/templates/apps/api-backend-deactivation/app.config.ts +20 -0
- package/templates/apps/api-backend-deactivation/database/schema.ts +41 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.server.ts +6 -0
- package/templates/apps/api-backend-deactivation/mutations/users.create.mutation.ts +16 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.server.ts +10 -0
- package/templates/apps/api-backend-deactivation/mutations/users.deactivate.mutation.ts +19 -0
- package/templates/apps/api-backend-deactivation/package.json +28 -0
- package/templates/apps/api-backend-deactivation/template.json +6 -0
- package/templates/apps/api-backend-deactivation/tests/users.deactivate.test.ts +47 -0
- package/templates/apps/api-backend-deactivation/tsconfig.json +5 -0
- package/templates/apps/api-backend-mail/README.md +39 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.server.ts +15 -0
- package/templates/apps/api-backend-mail/actions/sendWelcome.action.ts +21 -0
- package/templates/apps/api-backend-mail/app.config.ts +34 -0
- package/templates/apps/api-backend-mail/database/schema.ts +57 -0
- package/templates/apps/api-backend-mail/emails/welcome.email.tsx +106 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mail/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mail/package.json +30 -0
- package/templates/apps/api-backend-mail/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mail/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mail/template.json +6 -0
- package/templates/apps/api-backend-mail/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mail/tsconfig.json +5 -0
- package/templates/apps/api-backend-mariadb/.env.example +35 -0
- package/templates/apps/api-backend-mariadb/README.md +31 -0
- package/templates/apps/api-backend-mariadb/app.config.ts +68 -0
- package/templates/apps/api-backend-mariadb/database/schema.ts +57 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-mariadb/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-mariadb/package.json +30 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-mariadb/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-mariadb/template.json +6 -0
- package/templates/apps/api-backend-mariadb/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-mariadb/tsconfig.json +5 -0
- package/templates/apps/api-backend-storage/README.md +86 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadAvatar.action.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.server.ts +21 -0
- package/templates/apps/api-backend-storage/actions/uploadDocument.action.ts +20 -0
- package/templates/apps/api-backend-storage/app.config.ts +38 -0
- package/templates/apps/api-backend-storage/database/schema.ts +57 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.server.ts +19 -0
- package/templates/apps/api-backend-storage/mutations/notes.create.mutation.ts +37 -0
- package/templates/apps/api-backend-storage/package.json +27 -0
- package/templates/apps/api-backend-storage/queries/notes.query.server.ts +14 -0
- package/templates/apps/api-backend-storage/queries/notes.query.ts +20 -0
- package/templates/apps/api-backend-storage/template.json +6 -0
- package/templates/apps/api-backend-storage/tests/notes.create.test.ts +50 -0
- package/templates/apps/api-backend-storage/tsconfig.json +5 -0
- package/templates/apps/api-data-advanced/.env +17 -0
- package/templates/apps/api-data-advanced/README.md +88 -0
- package/templates/apps/api-data-advanced/app.config.ts +56 -0
- package/templates/apps/api-data-advanced/database/actors.entity.ts +14 -0
- package/templates/apps/api-data-advanced/database/authors.entity.ts +30 -0
- package/templates/apps/api-data-advanced/database/authors.relations.ts +15 -0
- package/templates/apps/api-data-advanced/database/books.entity.ts +59 -0
- package/templates/apps/api-data-advanced/database/books.relations.ts +10 -0
- package/templates/apps/api-data-advanced/database/index.ts +23 -0
- package/templates/apps/api-data-advanced/database/tenants.entity.ts +11 -0
- package/templates/apps/api-data-advanced/package.json +28 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.server.ts +18 -0
- package/templates/apps/api-data-advanced/queries/authors.withBooks.query.ts +40 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.server.ts +17 -0
- package/templates/apps/api-data-advanced/queries/books.search.query.ts +39 -0
- package/templates/apps/api-data-advanced/seeds/catalog.seed.ts +101 -0
- package/templates/apps/api-data-advanced/template.json +6 -0
- package/templates/apps/api-data-advanced/tests/queries.test.ts +124 -0
- package/templates/apps/api-data-advanced/tsconfig.json +5 -0
- package/templates/apps/api-durable/README.md +81 -0
- package/templates/apps/api-durable/aggregates/orderStats.aggregate.ts +49 -0
- package/templates/apps/api-durable/app.config.ts +41 -0
- package/templates/apps/api-durable/database/schema.ts +65 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.server.ts +43 -0
- package/templates/apps/api-durable/mutations/orders.approve.mutation.ts +23 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.server.ts +45 -0
- package/templates/apps/api-durable/mutations/orders.place.mutation.ts +40 -0
- package/templates/apps/api-durable/package.json +28 -0
- package/templates/apps/api-durable/schedules/nightlyReport.cron.tsx +37 -0
- package/templates/apps/api-durable/startup/warm.startup.tsx +33 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +27 -0
- package/templates/apps/api-durable/template.json +6 -0
- package/templates/apps/api-durable/tests/orders.place.test.ts +91 -0
- package/templates/apps/api-durable/triggers/order.placed.trigger.tsx +21 -0
- package/templates/apps/api-durable/tsconfig.json +5 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.server.tsx +103 -0
- package/templates/apps/api-durable/workflows/order.fulfill.workflow.tsx +25 -0
- package/templates/apps/api-feature-flags/README.md +63 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.server.ts +11 -0
- package/templates/apps/api-feature-flags/actions/notes.export.action.ts +18 -0
- package/templates/apps/api-feature-flags/app.config.ts +41 -0
- package/templates/apps/api-feature-flags/database/schema.ts +37 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.server.ts +29 -0
- package/templates/apps/api-feature-flags/mutations/notes.create.mutation.ts +28 -0
- package/templates/apps/api-feature-flags/package.json +29 -0
- package/templates/apps/api-feature-flags/template.json +6 -0
- package/templates/apps/api-feature-flags/tests/notes.create.test.ts +67 -0
- package/templates/apps/api-feature-flags/tsconfig.json +5 -0
- package/templates/apps/api-governance/.env +4 -0
- package/templates/apps/api-governance/README.md +61 -0
- package/templates/apps/api-governance/actions/profiles.get.action.server.ts +17 -0
- package/templates/apps/api-governance/actions/profiles.get.action.ts +17 -0
- package/templates/apps/api-governance/app.config.ts +35 -0
- package/templates/apps/api-governance/database/schema.ts +41 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.server.ts +17 -0
- package/templates/apps/api-governance/mutations/profiles.create.mutation.ts +21 -0
- package/templates/apps/api-governance/package.json +29 -0
- package/templates/apps/api-governance/template.json +6 -0
- package/templates/apps/api-governance/tests/profiles.create.test.ts +59 -0
- package/templates/apps/api-governance/tsconfig.json +5 -0
- package/templates/apps/api-kv/README.md +100 -0
- package/templates/apps/api-kv/actions/sync.pull.action.server.ts +75 -0
- package/templates/apps/api-kv/actions/sync.pull.action.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.server.ts +25 -0
- package/templates/apps/api-kv/actions/sync.reset.action.ts +22 -0
- package/templates/apps/api-kv/actions/sync.status.action.server.ts +28 -0
- package/templates/apps/api-kv/actions/sync.status.action.ts +17 -0
- package/templates/apps/api-kv/app.config.ts +48 -0
- package/templates/apps/api-kv/database/schema.ts +62 -0
- package/templates/apps/api-kv/package.json +28 -0
- package/templates/apps/api-kv/queries/events.list.query.server.ts +14 -0
- package/templates/apps/api-kv/queries/events.list.query.ts +23 -0
- package/templates/apps/api-kv/template.json +6 -0
- package/templates/apps/api-kv/tests/sync.test.ts +103 -0
- package/templates/apps/api-kv/tsconfig.json +5 -0
- package/templates/apps/api-moderation/README.md +46 -0
- package/templates/apps/api-moderation/app.config.ts +33 -0
- package/templates/apps/api-moderation/database/schema.ts +45 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.server.ts +8 -0
- package/templates/apps/api-moderation/mutations/comments.create.mutation.ts +20 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.server.ts +9 -0
- package/templates/apps/api-moderation/mutations/posts.create.mutation.ts +25 -0
- package/templates/apps/api-moderation/package.json +29 -0
- package/templates/apps/api-moderation/template.json +6 -0
- package/templates/apps/api-moderation/tests/posts.create.test.ts +79 -0
- package/templates/apps/api-moderation/tsconfig.json +5 -0
- package/templates/apps/api-observability/README.md +106 -0
- package/templates/apps/api-observability/app.config.ts +32 -0
- package/templates/apps/api-observability/database/schema.ts +38 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.server.ts +33 -0
- package/templates/apps/api-observability/mutations/notes.create.mutation.ts +15 -0
- package/templates/apps/api-observability/package.json +29 -0
- package/templates/apps/api-observability/queries/notes.list.query.server.ts +7 -0
- package/templates/apps/api-observability/queries/notes.list.query.ts +14 -0
- package/templates/apps/api-observability/template.json +6 -0
- package/templates/apps/api-observability/tests/notes.create.test.ts +36 -0
- package/templates/apps/api-observability/tsconfig.json +5 -0
- package/templates/apps/api-ratelimit/README.md +49 -0
- package/templates/apps/api-ratelimit/app.config.ts +42 -0
- package/templates/apps/api-ratelimit/database/schema.ts +37 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.server.ts +16 -0
- package/templates/apps/api-ratelimit/mutations/notes.create.mutation.ts +34 -0
- package/templates/apps/api-ratelimit/package.json +29 -0
- package/templates/apps/api-ratelimit/template.json +6 -0
- package/templates/apps/api-ratelimit/tests/notes.create.test.ts +69 -0
- package/templates/apps/api-ratelimit/tsconfig.json +5 -0
- package/templates/apps/api-rbac/README.md +59 -0
- package/templates/apps/api-rbac/app.config.ts +48 -0
- package/templates/apps/api-rbac/database/schema.ts +37 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.server.ts +22 -0
- package/templates/apps/api-rbac/mutations/notes.create.mutation.ts +26 -0
- package/templates/apps/api-rbac/package.json +29 -0
- package/templates/apps/api-rbac/template.json +6 -0
- package/templates/apps/api-rbac/tests/notes.create.test.ts +80 -0
- package/templates/apps/api-rbac/tsconfig.json +5 -0
- package/templates/apps/api-rest/README.md +85 -0
- package/templates/apps/api-rest/app.config.ts +70 -0
- package/templates/apps/api-rest/database/schema.ts +52 -0
- package/templates/apps/api-rest/lib/product.ts +30 -0
- package/templates/apps/api-rest/package.json +27 -0
- package/templates/apps/api-rest/routes/v1/products.create.route.tsx +45 -0
- package/templates/apps/api-rest/routes/v1/products.delete.route.tsx +25 -0
- package/templates/apps/api-rest/routes/v1/products.get.route.tsx +29 -0
- package/templates/apps/api-rest/routes/v1/products.list.route.tsx +43 -0
- package/templates/apps/api-rest/template.json +6 -0
- package/templates/apps/api-rest/tests/products.create.test.ts +53 -0
- package/templates/apps/api-rest/tsconfig.json +5 -0
- package/templates/apps/api-saas/README.md +106 -0
- package/templates/apps/api-saas/app.config.ts +51 -0
- package/templates/apps/api-saas/database/schema.ts +50 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.server.ts +52 -0
- package/templates/apps/api-saas/mutations/projects.create.mutation.ts +18 -0
- package/templates/apps/api-saas/package.json +31 -0
- package/templates/apps/api-saas/queries/projects.list.query.server.ts +7 -0
- package/templates/apps/api-saas/queries/projects.list.query.ts +18 -0
- package/templates/apps/api-saas/template.json +6 -0
- package/templates/apps/api-saas/tests/projects.create.test.ts +47 -0
- package/templates/apps/api-saas/tsconfig.json +5 -0
- package/templates/apps/api-search/README.md +68 -0
- package/templates/apps/api-search/app.config.ts +31 -0
- package/templates/apps/api-search/database/schema.ts +47 -0
- package/templates/apps/api-search/lib/search.ts +27 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.server.ts +19 -0
- package/templates/apps/api-search/mutations/articles.create.mutation.ts +35 -0
- package/templates/apps/api-search/package.json +29 -0
- package/templates/apps/api-search/queries/articles.list.query.server.ts +14 -0
- package/templates/apps/api-search/queries/articles.list.query.ts +19 -0
- package/templates/apps/api-search/seeds/articles.seed.ts +41 -0
- package/templates/apps/api-search/startup/searchBackfill.startup.tsx +24 -0
- package/templates/apps/api-search/template.json +6 -0
- package/templates/apps/api-search/tests/articles.create.test.ts +69 -0
- package/templates/apps/api-search/tsconfig.json +5 -0
- package/templates/apps/api-versioning/README.md +51 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.server.ts +13 -0
- package/templates/apps/api-versioning/actions/documents.asOf.action.ts +14 -0
- package/templates/apps/api-versioning/actions/documents.history.action.server.ts +15 -0
- package/templates/apps/api-versioning/actions/documents.history.action.ts +17 -0
- package/templates/apps/api-versioning/app.config.ts +24 -0
- package/templates/apps/api-versioning/database/schema.ts +38 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.server.ts +8 -0
- package/templates/apps/api-versioning/mutations/documents.create.mutation.ts +21 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.server.ts +9 -0
- package/templates/apps/api-versioning/mutations/documents.update.mutation.ts +21 -0
- package/templates/apps/api-versioning/package.json +29 -0
- package/templates/apps/api-versioning/template.json +6 -0
- package/templates/apps/api-versioning/tests/documents.create.test.ts +37 -0
- package/templates/apps/api-versioning/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/.env +6 -0
- package/templates/apps/api-webhooks/README.md +106 -0
- package/templates/apps/api-webhooks/app.config.ts +16 -0
- package/templates/apps/api-webhooks/database/schema.ts +49 -0
- package/templates/apps/api-webhooks/events/order.completed.webhook.tsx +22 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.server.ts +37 -0
- package/templates/apps/api-webhooks/mutations/orders.fulfill.mutation.ts +15 -0
- package/templates/apps/api-webhooks/package.json +28 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.server.ts +7 -0
- package/templates/apps/api-webhooks/queries/orders.list.query.ts +15 -0
- package/templates/apps/api-webhooks/template.json +6 -0
- package/templates/apps/api-webhooks/tests/orders.fulfill.test.ts +51 -0
- package/templates/apps/api-webhooks/tsconfig.json +5 -0
- package/templates/apps/api-webhooks/webhooks/orders.webhook.tsx +35 -0
- package/templates/apps/changelog/README.md +77 -0
- package/templates/apps/changelog/app.config.ts +27 -0
- package/templates/apps/changelog/content/releases/0.1.0.mdx +19 -0
- package/templates/apps/changelog/content/releases/0.2.0.mdx +28 -0
- package/templates/apps/changelog/package.json +30 -0
- package/templates/apps/changelog/scripts/generate-rss.mjs +38 -0
- package/templates/apps/changelog/src/globals.css +66 -0
- package/templates/apps/changelog/src/globals.d.ts +16 -0
- package/templates/apps/changelog/src/lib/locale.test.ts +72 -0
- package/templates/apps/changelog/src/lib/locale.ts +55 -0
- package/templates/apps/changelog/src/lib/releases.ts +21 -0
- package/templates/apps/changelog/src/locales/de.ts +22 -0
- package/templates/apps/changelog/src/locales/en.ts +30 -0
- package/templates/apps/changelog/src/pages/[locale]/[slug].tsx +24 -0
- package/templates/apps/changelog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +95 -0
- package/templates/apps/changelog/src/pages/[slug].test.tsx +127 -0
- package/templates/apps/changelog/src/pages/[slug].tsx +52 -0
- package/templates/apps/changelog/src/pages/index.test.tsx +112 -0
- package/templates/apps/changelog/src/pages/index.tsx +55 -0
- package/templates/apps/changelog/src/pages/layout.tsx +84 -0
- package/templates/apps/changelog/template.json +6 -0
- package/templates/apps/changelog/tsconfig.json +11 -0
- package/templates/apps/edge-functions/README.md +55 -0
- package/templates/apps/edge-functions/functions/aiComplete.serverless.ts +58 -0
- package/templates/apps/edge-functions/functions/currencyConvert.serverless.ts +47 -0
- package/templates/apps/edge-functions/functions/geoGreeting.serverless.ts +44 -0
- package/templates/apps/edge-functions/functions/health.serverless.ts +29 -0
- package/templates/apps/edge-functions/functions/resolveLink.serverless.ts +32 -0
- package/templates/apps/edge-functions/functions/shareLink.serverless.ts +30 -0
- package/templates/apps/edge-functions/functions/slackNotify.serverless.ts +40 -0
- package/templates/apps/edge-functions/functions/verifySignature.serverless.ts +52 -0
- package/templates/apps/edge-functions/package.json +21 -0
- package/templates/apps/edge-functions/template.json +6 -0
- package/templates/apps/edge-functions/tsconfig.json +10 -0
- package/templates/apps/frontend-admin/README.md +40 -0
- package/templates/apps/frontend-admin/app.config.ts +37 -0
- package/templates/apps/frontend-admin/package.json +30 -0
- package/templates/apps/frontend-admin/src/config.ts +8 -0
- package/templates/apps/frontend-admin/src/globals.css +76 -0
- package/templates/apps/frontend-admin/src/globals.d.ts +6 -0
- package/templates/apps/frontend-admin/src/lib/admin.ts +16 -0
- package/templates/apps/frontend-admin/src/lib/auth.ts +24 -0
- package/templates/apps/frontend-admin/src/locales/de.ts +67 -0
- package/templates/apps/frontend-admin/src/locales/en.ts +79 -0
- package/templates/apps/frontend-admin/src/locales/index.ts +15 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/index.tsx +32 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/layout.tsx +29 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.test.tsx +73 -0
- package/templates/apps/frontend-admin/src/pages/(marketing)/login.tsx +35 -0
- package/templates/apps/frontend-admin/src/pages/admin/[entity].tsx +121 -0
- package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +119 -0
- package/templates/apps/frontend-admin/src/pages/admin/error.tsx +20 -0
- package/templates/apps/frontend-admin/src/pages/admin/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.test.tsx +88 -0
- package/templates/apps/frontend-admin/src/pages/admin/index.tsx +65 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.test.tsx +114 -0
- package/templates/apps/frontend-admin/src/pages/admin/layout.tsx +97 -0
- package/templates/apps/frontend-admin/src/pages/admin/loading.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/admin/not-found.tsx +15 -0
- package/templates/apps/frontend-admin/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-admin/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-admin/template.json +6 -0
- package/templates/apps/frontend-admin/tsconfig.json +11 -0
- package/templates/apps/frontend-app/README.md +98 -0
- package/templates/apps/frontend-app/app.config.ts +43 -0
- package/templates/apps/frontend-app/package.json +30 -0
- package/templates/apps/frontend-app/src/locales/de.ts +36 -0
- package/templates/apps/frontend-app/src/locales/en.ts +43 -0
- package/templates/apps/frontend-app/src/locales/index.ts +15 -0
- package/templates/apps/frontend-app/src/pages/index.test.tsx +167 -0
- package/templates/apps/frontend-app/src/pages/index.tsx +136 -0
- package/templates/apps/frontend-app/src/pages/layout.tsx +41 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.test.tsx +99 -0
- package/templates/apps/frontend-app/src/pages/schema-ui.tsx +74 -0
- package/templates/apps/frontend-app/template.json +6 -0
- package/templates/apps/frontend-app/tsconfig.json +11 -0
- package/templates/apps/frontend-blank/README.md +18 -0
- package/templates/apps/frontend-blank/app.config.ts +29 -0
- package/templates/apps/frontend-blank/package.json +29 -0
- package/templates/apps/frontend-blank/src/locales/de.ts +15 -0
- package/templates/apps/frontend-blank/src/locales/en.ts +22 -0
- package/templates/apps/frontend-blank/src/locales/index.ts +15 -0
- package/templates/apps/frontend-blank/src/pages/index.test.tsx +55 -0
- package/templates/apps/frontend-blank/src/pages/index.tsx +27 -0
- package/templates/apps/frontend-blank/src/pages/layout.test.tsx +54 -0
- package/templates/apps/frontend-blank/src/pages/layout.tsx +35 -0
- package/templates/apps/frontend-blank/template.json +6 -0
- package/templates/apps/frontend-blank/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/README.md +65 -0
- package/templates/apps/frontend-contact/app.config.ts +25 -0
- package/templates/apps/frontend-contact/functions/sendMessage.serverless.ts +69 -0
- package/templates/apps/frontend-contact/package.json +33 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.test.tsx +142 -0
- package/templates/apps/frontend-contact/src/components/ContactForm.island.tsx +104 -0
- package/templates/apps/frontend-contact/src/config.ts +12 -0
- package/templates/apps/frontend-contact/src/globals.css +84 -0
- package/templates/apps/frontend-contact/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-contact/src/locales/de.ts +26 -0
- package/templates/apps/frontend-contact/src/locales/en.ts +29 -0
- package/templates/apps/frontend-contact/src/pages/[locale]/index.tsx +14 -0
- package/templates/apps/frontend-contact/src/pages/index.test.tsx +70 -0
- package/templates/apps/frontend-contact/src/pages/index.tsx +63 -0
- package/templates/apps/frontend-contact/src/pages/layout.tsx +65 -0
- package/templates/apps/frontend-contact/template.json +6 -0
- package/templates/apps/frontend-contact/tsconfig.json +11 -0
- package/templates/apps/frontend-dashboard/README.md +54 -0
- package/templates/apps/frontend-dashboard/app.config.ts +39 -0
- package/templates/apps/frontend-dashboard/package.json +29 -0
- package/templates/apps/frontend-dashboard/src/config.ts +8 -0
- package/templates/apps/frontend-dashboard/src/globals.css +72 -0
- package/templates/apps/frontend-dashboard/src/globals.d.ts +6 -0
- package/templates/apps/frontend-dashboard/src/lib/auth.ts +27 -0
- package/templates/apps/frontend-dashboard/src/locales/de.ts +49 -0
- package/templates/apps/frontend-dashboard/src/locales/en.ts +60 -0
- package/templates/apps/frontend-dashboard/src/locales/index.ts +15 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/index.tsx +34 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/layout.tsx +32 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.test.tsx +74 -0
- package/templates/apps/frontend-dashboard/src/pages/(marketing)/login.tsx +40 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/error.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/fallbacks.test.tsx +68 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.test.tsx +55 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/index.tsx +38 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.test.tsx +88 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/layout.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/loading.tsx +21 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/not-found.tsx +19 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.test.tsx +69 -0
- package/templates/apps/frontend-dashboard/src/pages/dashboard/settings.tsx +35 -0
- package/templates/apps/frontend-dashboard/src/pages/layout.tsx +12 -0
- package/templates/apps/frontend-dashboard/src/pages/layouts.test.tsx +54 -0
- package/templates/apps/frontend-dashboard/template.json +6 -0
- package/templates/apps/frontend-dashboard/tsconfig.json +11 -0
- package/templates/apps/frontend-docs/README.md +19 -0
- package/templates/apps/frontend-docs/app.config.ts +25 -0
- package/templates/apps/frontend-docs/package.json +29 -0
- package/templates/apps/frontend-docs/src/globals.css +32 -0
- package/templates/apps/frontend-docs/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-docs/src/lib/locale.ts +56 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +24 -0
- package/templates/apps/frontend-docs/src/locales/en.ts +28 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug].tsx +18 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +67 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].test.tsx +115 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug].tsx +82 -0
- package/templates/apps/frontend-docs/src/pages/index.test.tsx +90 -0
- package/templates/apps/frontend-docs/src/pages/index.tsx +45 -0
- package/templates/apps/frontend-docs/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-docs/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-docs/template.json +6 -0
- package/templates/apps/frontend-docs/tsconfig.json +11 -0
- package/templates/apps/frontend-i18n/README.md +61 -0
- package/templates/apps/frontend-i18n/app.config.ts +33 -0
- package/templates/apps/frontend-i18n/package.json +28 -0
- package/templates/apps/frontend-i18n/src/globals.css +46 -0
- package/templates/apps/frontend-i18n/src/globals.d.ts +6 -0
- package/templates/apps/frontend-i18n/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-i18n/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-i18n/src/locales/de.ts +24 -0
- package/templates/apps/frontend-i18n/src/locales/en.ts +25 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/about.tsx +11 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +50 -0
- package/templates/apps/frontend-i18n/src/pages/about.test.tsx +75 -0
- package/templates/apps/frontend-i18n/src/pages/about.tsx +31 -0
- package/templates/apps/frontend-i18n/src/pages/index.test.tsx +102 -0
- package/templates/apps/frontend-i18n/src/pages/index.tsx +43 -0
- package/templates/apps/frontend-i18n/src/pages/layout.test.tsx +86 -0
- package/templates/apps/frontend-i18n/src/pages/layout.tsx +70 -0
- package/templates/apps/frontend-i18n/template.json +6 -0
- package/templates/apps/frontend-i18n/tsconfig.json +11 -0
- package/templates/apps/frontend-landing/README.md +17 -0
- package/templates/apps/frontend-landing/app.config.ts +25 -0
- package/templates/apps/frontend-landing/package.json +29 -0
- package/templates/apps/frontend-landing/src/globals.css +23 -0
- package/templates/apps/frontend-landing/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-landing/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +24 -0
- package/templates/apps/frontend-landing/src/locales/en.ts +26 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +37 -0
- package/templates/apps/frontend-landing/src/pages/index.test.tsx +110 -0
- package/templates/apps/frontend-landing/src/pages/index.tsx +75 -0
- package/templates/apps/frontend-landing/src/pages/layout.test.tsx +84 -0
- package/templates/apps/frontend-landing/src/pages/layout.tsx +66 -0
- package/templates/apps/frontend-landing/template.json +6 -0
- package/templates/apps/frontend-landing/tsconfig.json +11 -0
- package/templates/apps/frontend-spa/README.md +45 -0
- package/templates/apps/frontend-spa/app.config.ts +27 -0
- package/templates/apps/frontend-spa/package.json +29 -0
- package/templates/apps/frontend-spa/src/globals.css +84 -0
- package/templates/apps/frontend-spa/src/locales/de.ts +22 -0
- package/templates/apps/frontend-spa/src/locales/en.ts +29 -0
- package/templates/apps/frontend-spa/src/locales/index.ts +15 -0
- package/templates/apps/frontend-spa/src/pages/index.test.tsx +137 -0
- package/templates/apps/frontend-spa/src/pages/index.tsx +123 -0
- package/templates/apps/frontend-spa/src/pages/layout.tsx +27 -0
- package/templates/apps/frontend-spa/template.json +6 -0
- package/templates/apps/frontend-spa/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr/README.md +68 -0
- package/templates/apps/frontend-ssr/app.config.ts +32 -0
- package/templates/apps/frontend-ssr/package.json +29 -0
- package/templates/apps/frontend-ssr/src/globals.css +67 -0
- package/templates/apps/frontend-ssr/src/locales/de.ts +41 -0
- package/templates/apps/frontend-ssr/src/locales/en.ts +54 -0
- package/templates/apps/frontend-ssr/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.test.tsx +69 -0
- package/templates/apps/frontend-ssr/src/pages/feed-swr.tsx +54 -0
- package/templates/apps/frontend-ssr/src/pages/feed.test.tsx +73 -0
- package/templates/apps/frontend-ssr/src/pages/feed.tsx +64 -0
- package/templates/apps/frontend-ssr/src/pages/index.test.tsx +89 -0
- package/templates/apps/frontend-ssr/src/pages/index.tsx +72 -0
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +37 -0
- package/templates/apps/frontend-ssr/template.json +6 -0
- package/templates/apps/frontend-ssr/tsconfig.json +11 -0
- package/templates/apps/frontend-ssr-api/README.md +50 -0
- package/templates/apps/frontend-ssr-api/app.config.ts +43 -0
- package/templates/apps/frontend-ssr-api/package.json +30 -0
- package/templates/apps/frontend-ssr-api/src/globals.css +38 -0
- package/templates/apps/frontend-ssr-api/src/globals.d.ts +6 -0
- package/templates/apps/frontend-ssr-api/src/locales/de.ts +20 -0
- package/templates/apps/frontend-ssr-api/src/locales/en.ts +31 -0
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +16 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.test.tsx +105 -0
- package/templates/apps/frontend-ssr-api/src/pages/index.tsx +83 -0
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +28 -0
- package/templates/apps/frontend-ssr-api/template.json +6 -0
- package/templates/apps/frontend-ssr-api/tsconfig.json +11 -0
- package/templates/apps/frontend-static-blog/README.md +49 -0
- package/templates/apps/frontend-static-blog/app.config.ts +34 -0
- package/templates/apps/frontend-static-blog/package.json +28 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.test.tsx +65 -0
- package/templates/apps/frontend-static-blog/src/components/ReadingProgress.island.tsx +35 -0
- package/templates/apps/frontend-static-blog/src/content/posts.ts +64 -0
- package/templates/apps/frontend-static-blog/src/globals.css +75 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.test.ts +72 -0
- package/templates/apps/frontend-static-blog/src/lib/locale.ts +55 -0
- package/templates/apps/frontend-static-blog/src/locales/de.ts +19 -0
- package/templates/apps/frontend-static-blog/src/locales/en.ts +26 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug].tsx +20 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/index.tsx +13 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +62 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].test.tsx +116 -0
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug].tsx +69 -0
- package/templates/apps/frontend-static-blog/src/pages/index.test.tsx +100 -0
- package/templates/apps/frontend-static-blog/src/pages/index.tsx +58 -0
- package/templates/apps/frontend-static-blog/src/pages/layout.tsx +63 -0
- package/templates/apps/frontend-static-blog/template.json +6 -0
- package/templates/apps/frontend-static-blog/tsconfig.json +11 -0
- package/templates/baselines/bare/.env.example +37 -0
- package/templates/baselines/bare/README.md +47 -0
- package/templates/baselines/bare/baseline.json +31 -0
- package/templates/baselines/bare/deploy/README.md +43 -0
- package/templates/baselines/bare/deploy/voltro.service.example +36 -0
- package/templates/baselines/compose/.env.example +46 -0
- package/templates/baselines/compose/README.md +69 -0
- package/templates/baselines/compose/baseline.json +51 -0
- package/templates/baselines/compose/docker/.dockerignore +38 -0
- package/templates/baselines/compose/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose/docker-compose.dev.yml +89 -0
- package/templates/baselines/compose/docker-compose.prod.yml +87 -0
- package/templates/baselines/compose/docker-compose.yml +41 -0
- package/templates/baselines/compose-mariadb/.env.example +57 -0
- package/templates/baselines/compose-mariadb/README.md +78 -0
- package/templates/baselines/compose-mariadb/baseline.json +51 -0
- package/templates/baselines/compose-mariadb/docker/.dockerignore +38 -0
- package/templates/baselines/compose-mariadb/docker/api.Dockerfile +57 -0
- package/templates/baselines/compose-mariadb/docker/dev.Dockerfile +35 -0
- package/templates/baselines/compose-mariadb/docker/mariadb-init.sql +6 -0
- package/templates/baselines/compose-mariadb/docker/web.Dockerfile +59 -0
- package/templates/baselines/compose-mariadb/docker-compose.dev.yml +117 -0
- package/templates/baselines/compose-mariadb/docker-compose.prod.yml +114 -0
- package/templates/baselines/compose-mariadb/docker-compose.yml +79 -0
- package/templates/baselines/helm/.env.example +39 -0
- package/templates/baselines/helm/README.md +98 -0
- package/templates/baselines/helm/baseline.json +53 -0
- package/templates/baselines/helm/charts/voltro-app/.helmignore +10 -0
- package/templates/baselines/helm/charts/voltro-app/Chart.yaml +10 -0
- package/templates/baselines/helm/charts/voltro-app/templates/_helpers.tpl +36 -0
- package/templates/baselines/helm/charts/voltro-app/templates/configmap.yaml +13 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-api.yaml +120 -0
- package/templates/baselines/helm/charts/voltro-app/templates/deployment-web.yaml +45 -0
- package/templates/baselines/helm/charts/voltro-app/templates/ingress.yaml +37 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-service.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/postgres-statefulset.yaml +73 -0
- package/templates/baselines/helm/charts/voltro-app/templates/secret.yaml +33 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-api.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/templates/service-web.yaml +19 -0
- package/templates/baselines/helm/charts/voltro-app/values-dev.yaml +21 -0
- package/templates/baselines/helm/charts/voltro-app/values-prod.yaml +58 -0
- package/templates/baselines/helm/charts/voltro-app/values-staging.yaml +25 -0
- package/templates/baselines/helm/charts/voltro-app/values.yaml +109 -0
- package/templates/baselines/helm/deploy/README.md +94 -0
- package/templates/patches/@effect__cluster@0.59.0.patch +262 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project. Read by `voltro dev` / `voltro start`.
|
|
2
|
+
//
|
|
3
|
+
// `port` is allocated by `voltro create-project` / `voltro add-app` from
|
|
4
|
+
// the project's portRange (in project.json). Hard-pinned per app; the
|
|
5
|
+
// dev server fails fast on conflicts instead of silently incrementing.
|
|
6
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
7
|
+
|
|
8
|
+
// Typed PUBLIC environment — public vars are validated + baked into the browser
|
|
9
|
+
// bundle at build time and read via `publicEnv` from '@voltro/env/public'
|
|
10
|
+
// (typed, autocompleted). NEVER put a secret here (it would ship to every
|
|
11
|
+
// visitor) — declare secrets on the api instead. Run `voltro env` for the manifest.
|
|
12
|
+
export const env = defineEnv({
|
|
13
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export default {
|
|
17
|
+
type: 'web' as const,
|
|
18
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
19
|
+
port: {{port}},
|
|
20
|
+
env,
|
|
21
|
+
// Bilingual by default. The framework auto-wires a cookie-driven
|
|
22
|
+
// <I18nProvider> from these codes and loads the matching
|
|
23
|
+
// `src/locales/<code>.ts` catalog — so `<T id="…" />` / `useT()` /
|
|
24
|
+
// `useLocale()` work app-wide with no manual setup. The <LocaleSwitcher>
|
|
25
|
+
// in the layout writes the `voltro:lang` cookie + reloads. Grow the app to
|
|
26
|
+
// more languages by adding a code here + a `src/locales/<code>.ts`.
|
|
27
|
+
locales: ['en', 'de'] as const,
|
|
28
|
+
defaultLocale: 'en' as const,
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"build": "voltro build .",
|
|
9
|
+
"start": "voltro start .",
|
|
10
|
+
"test": "voltro test",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@voltro/cli": "0.1.0",
|
|
15
|
+
"@voltro/client": "0.1.0",
|
|
16
|
+
"@voltro/env": "0.1.0",
|
|
17
|
+
"@voltro/i18n": "0.1.0",
|
|
18
|
+
"@voltro/ui-shadcn": "0.1.0",
|
|
19
|
+
"@voltro/web": "0.1.0",
|
|
20
|
+
"react": "^19.0.0",
|
|
21
|
+
"react-dom": "^19.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^19.0.0",
|
|
25
|
+
"@types/react-dom": "^19.0.0",
|
|
26
|
+
"typescript": "^5.7.0",
|
|
27
|
+
"vitest": "^3.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// German catalog. Mirrors every key in `en.ts` — `defineLocale<typeof en>()`
|
|
2
|
+
// fails the build if a key is missing or misspelled. Translate the prose only;
|
|
3
|
+
// keep ICU placeholders and rich tags (`<code>`) identical to `en.ts`.
|
|
4
|
+
|
|
5
|
+
import { defineLocale } from '@voltro/i18n'
|
|
6
|
+
import en from './en'
|
|
7
|
+
|
|
8
|
+
export default defineLocale<typeof en>()({
|
|
9
|
+
'meta.home.title': 'Startseite',
|
|
10
|
+
|
|
11
|
+
'lang.label': 'Sprache',
|
|
12
|
+
|
|
13
|
+
'home.intro':
|
|
14
|
+
'Bearbeite <code>src/pages/index.tsx</code>, um loszulegen. Lege weitere <code>*.tsx</code>-Dateien in <code>src/pages/</code> ab, um Routen hinzuzufügen — ganz ohne Router-Konfiguration.',
|
|
15
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Base catalog — the source of truth for this app's UI strings. Every key here
|
|
2
|
+
// MUST be mirrored in `de.ts` (and any other locale); `defineLocale<typeof en>()`
|
|
3
|
+
// enforces that parity at build time, so a missing translation fails typecheck
|
|
4
|
+
// rather than silently falling back.
|
|
5
|
+
//
|
|
6
|
+
// Messages use ICU syntax: `{name}` placeholders, `{n, plural, …}`, and rich
|
|
7
|
+
// tags like `<code>…</code>` (rendered by passing a matching function to
|
|
8
|
+
// `<T values={{ code: (c) => <code>{c}</code> }} />`).
|
|
9
|
+
|
|
10
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
11
|
+
|
|
12
|
+
export default defineCatalog({
|
|
13
|
+
// Browser tab title — locale-aware via the page's `meta({ locale })`.
|
|
14
|
+
'meta.home.title': 'Home',
|
|
15
|
+
|
|
16
|
+
// ---- Layout shell (pages/layout.tsx) ----
|
|
17
|
+
'lang.label': 'Language',
|
|
18
|
+
|
|
19
|
+
// ---- Index page (pages/index.tsx) ----
|
|
20
|
+
'home.intro':
|
|
21
|
+
'Edit <code>src/pages/index.tsx</code> to start building. Drop more <code>*.tsx</code> files in <code>src/pages/</code> to add routes — no router config required.',
|
|
22
|
+
} as const)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Catalog lookup OUTSIDE React — for a page's `meta({ locale })` locale-aware
|
|
2
|
+
// <title>. This app is cookie-i18n (no `/<locale>` URL prefix), so @voltro/web
|
|
3
|
+
// hands `meta` the active locale from the `voltro:lang` cookie. Inside React,
|
|
4
|
+
// use `<T>` / `useT()` via the framework's auto-wired <I18nProvider> instead.
|
|
5
|
+
|
|
6
|
+
import en from './en'
|
|
7
|
+
import de from './de'
|
|
8
|
+
|
|
9
|
+
// Both catalogs share `en`'s keys (defineLocale enforces the mirror), so a
|
|
10
|
+
// known-key lookup returns `string` — not `string | undefined` — which is what
|
|
11
|
+
// `PageMeta.title` requires under exactOptionalPropertyTypes.
|
|
12
|
+
export type Messages = Record<keyof typeof en, string>
|
|
13
|
+
|
|
14
|
+
export const getCatalog = (locale?: string): Messages =>
|
|
15
|
+
(locale === 'de' ? de : en) as Messages
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// `/` — the blank starter's single page. A pure static component with no
|
|
4
|
+
// hooks, loader, or meta; we mount it and assert its observable structure
|
|
5
|
+
// (the heading element + the "start building" copy + the src/pages hint).
|
|
6
|
+
// The heading text itself carries a `{{capProjectName}}` scaffold placeholder,
|
|
7
|
+
// so we assert the <h1> EXISTS rather than pinning its substituted text.
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
10
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
13
|
+
import enCatalog from '../locales/en'
|
|
14
|
+
|
|
15
|
+
import Index from './index'
|
|
16
|
+
|
|
17
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
18
|
+
|
|
19
|
+
let container: HTMLDivElement
|
|
20
|
+
let root: Root
|
|
21
|
+
|
|
22
|
+
// The page uses <T>, so it needs an <I18nProvider>. Wrap with the real English
|
|
23
|
+
// catalog (the framework auto-wires this provider in the running app).
|
|
24
|
+
const render = (node: ReactNode): void => {
|
|
25
|
+
container = document.createElement('div')
|
|
26
|
+
document.body.appendChild(container)
|
|
27
|
+
act(() => {
|
|
28
|
+
root = createRoot(container)
|
|
29
|
+
root.render(
|
|
30
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
if (root) act(() => root.unmount())
|
|
37
|
+
container?.remove()
|
|
38
|
+
document.body.innerHTML = ''
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('index — render', () => {
|
|
42
|
+
test('renders a top-level heading inside a <main>', () => {
|
|
43
|
+
render(createElement(Index))
|
|
44
|
+
const main = container.querySelector('main')
|
|
45
|
+
expect(main).toBeTruthy()
|
|
46
|
+
expect(main!.querySelector('h1')).toBeTruthy()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('shows the getting-started hint pointing at src/pages/index.tsx', () => {
|
|
50
|
+
render(createElement(Index))
|
|
51
|
+
expect(container.textContent).toContain('Edit')
|
|
52
|
+
const codes = Array.from(container.querySelectorAll('code')).map((c) => c.textContent)
|
|
53
|
+
expect(codes).toContain('src/pages/index.tsx')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Mounted at `/`. The router maps `src/pages/<file>.tsx` to `/<file>`
|
|
2
|
+
// — see AGENTS.md for the conventions (groups, [param], [...slug]).
|
|
3
|
+
//
|
|
4
|
+
// Bilingual by default: the copy comes from the catalog via `<T>` (see
|
|
5
|
+
// src/locales), and <LocaleSwitcher> in the layout switches language.
|
|
6
|
+
|
|
7
|
+
import type { ReactNode } from 'react'
|
|
8
|
+
import type { PageMeta } from '@voltro/web'
|
|
9
|
+
import { T } from '@voltro/i18n'
|
|
10
|
+
import { getCatalog } from '../locales'
|
|
11
|
+
|
|
12
|
+
// Locale-aware tab title: @voltro/web resolves meta({ locale }) from the
|
|
13
|
+
// active locale (the `voltro:lang` cookie in this cookie-i18n app).
|
|
14
|
+
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
15
|
+
title: getCatalog(locale)['meta.home.title'],
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export default function Index(): ReactNode {
|
|
19
|
+
return (
|
|
20
|
+
<main>
|
|
21
|
+
<h1>{{capProjectName}}</h1>
|
|
22
|
+
<p>
|
|
23
|
+
<T id="home.intro" values={{ code: (c: ReactNode) => <code>{c}</code> }} />
|
|
24
|
+
</p>
|
|
25
|
+
</main>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// Root layout — renders a language switcher in the header, then its children
|
|
4
|
+
// (no <html>/<head>/<body>, which the framework shell owns). We assert the
|
|
5
|
+
// children render, the <LocaleSwitcher> is present, and no document element is
|
|
6
|
+
// introduced. The layout uses <LocaleSwitcher> + useLocale/useT, so we wrap the
|
|
7
|
+
// render in the real <I18nProvider> (the framework auto-wires it in the app).
|
|
8
|
+
|
|
9
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
10
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
11
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
+
import { I18nProvider } from '@voltro/i18n'
|
|
13
|
+
import enCatalog from '../locales/en'
|
|
14
|
+
|
|
15
|
+
import Layout from './layout'
|
|
16
|
+
|
|
17
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
18
|
+
|
|
19
|
+
let container: HTMLDivElement
|
|
20
|
+
let root: Root
|
|
21
|
+
|
|
22
|
+
const render = (node: ReactNode): void => {
|
|
23
|
+
container = document.createElement('div')
|
|
24
|
+
document.body.appendChild(container)
|
|
25
|
+
act(() => {
|
|
26
|
+
root = createRoot(container)
|
|
27
|
+
root.render(
|
|
28
|
+
createElement(I18nProvider, { locale: 'en', messages: enCatalog, defaultLocale: 'en', children: node }),
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
if (root) act(() => root.unmount())
|
|
35
|
+
container?.remove()
|
|
36
|
+
document.body.innerHTML = ''
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('layout — render', () => {
|
|
40
|
+
test('renders its children and the language switcher', () => {
|
|
41
|
+
render(
|
|
42
|
+
createElement(Layout, { children: createElement('span', { 'data-testid': 'child' }, 'hi') }),
|
|
43
|
+
)
|
|
44
|
+
const child = container.querySelector('[data-testid="child"]')
|
|
45
|
+
expect(child).toBeTruthy()
|
|
46
|
+
expect(child!.textContent).toBe('hi')
|
|
47
|
+
expect(container.querySelector('select[aria-label]')).toBeTruthy()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('does not introduce a document element (html/head/body)', () => {
|
|
51
|
+
render(createElement(Layout, { children: createElement('p', {}, 'body content') }))
|
|
52
|
+
expect(container.querySelector('html, head, body')).toBeNull()
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Root layout that wraps every page in this app. Add nav, providers,
|
|
2
|
+
// error boundaries, etc. here. `<html>` / `<head>` / `<body>` are
|
|
3
|
+
// owned by the framework's index.html shell — don't render them here
|
|
4
|
+
// or the browser will unwrap them under #root and the document
|
|
5
|
+
// structure breaks. Use the page's `meta` export for title + meta
|
|
6
|
+
// tags, and `theme: 'dark' | 'light' | 'system'` in app.config.ts
|
|
7
|
+
// to control the dark class.
|
|
8
|
+
//
|
|
9
|
+
// The app is bilingual (see `locales` in app.config.ts): <LocaleSwitcher>
|
|
10
|
+
// writes the `voltro:lang` cookie + reloads so the framework re-renders in
|
|
11
|
+
// the chosen language.
|
|
12
|
+
|
|
13
|
+
import type { ReactNode } from 'react'
|
|
14
|
+
import { useLocale, useT } from '@voltro/i18n'
|
|
15
|
+
import { LocaleSwitcher } from '@voltro/ui-shadcn'
|
|
16
|
+
|
|
17
|
+
interface LayoutProps {
|
|
18
|
+
readonly children: ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LOCALES = [
|
|
22
|
+
{ code: 'en', label: 'English' },
|
|
23
|
+
{ code: 'de', label: 'Deutsch' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export default function Layout({ children }: LayoutProps): ReactNode {
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<header style={{ display: 'flex', justifyContent: 'flex-end', padding: '12px 16px' }}>
|
|
30
|
+
<LocaleSwitcher locales={LOCALES} current={useLocale()} ariaLabel={useT('lang.label')} />
|
|
31
|
+
</header>
|
|
32
|
+
{children}
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"jsx": "react-jsx",
|
|
5
|
+
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleResolution": "Bundler"
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "dist", ".framework"]
|
|
11
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# {{projectName}} / {{appName}}
|
|
2
|
+
|
|
3
|
+
Static page **+** serverless email form (template: **frontend-contact**) —
|
|
4
|
+
the canonical "static frontend, serverless backend" combo.
|
|
5
|
+
|
|
6
|
+
- `src/pages/index.tsx` — a static page (`renderMode: 'static'`).
|
|
7
|
+
- `src/components/ContactForm.island.tsx` — the only hydrated part (an island).
|
|
8
|
+
- `functions/sendMessage.serverless.ts` — a standalone serverless function
|
|
9
|
+
that sends mail via Resend. Deployed SEPARATELY from the page.
|
|
10
|
+
|
|
11
|
+
## Run it locally (two terminals)
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm install
|
|
15
|
+
|
|
16
|
+
# Terminal 1 — the static site (HMR dev server)
|
|
17
|
+
pnpm --filter @{{projectName}}/{{appName}} dev
|
|
18
|
+
|
|
19
|
+
# Terminal 2 — the serverless function on http://localhost:8910
|
|
20
|
+
pnpm --filter @{{projectName}}/{{appName}} fn:dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The form POSTs to `http://localhost:8910` (see `src/config.ts`). The function
|
|
24
|
+
answers cross-origin because the framework's runner sends CORS headers — the
|
|
25
|
+
same headers Cloudflare / Scaleway add at the edge.
|
|
26
|
+
|
|
27
|
+
Without `RESEND_API_KEY` set, the function returns a clean `503
|
|
28
|
+
email-not-configured` and the form shows it — it never pretends to send. To
|
|
29
|
+
send for real:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
RESEND_API_KEY=re_... CONTACT_TO=you@example.com \
|
|
33
|
+
pnpm --filter @{{projectName}}/{{appName}} fn:dev
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Deploy
|
|
37
|
+
|
|
38
|
+
The page and the function ship to **different** places — that's the point.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 1. The static page → a CDN
|
|
42
|
+
pnpm --filter @{{projectName}}/{{appName}} build
|
|
43
|
+
voltro static deploy --host cloudflare-pages --project-name {{appName}}
|
|
44
|
+
|
|
45
|
+
# 2. The function → a function host (scales to zero)
|
|
46
|
+
cd apps/{{projectName}}/{{appName}}
|
|
47
|
+
voltro serverless deploy --target scaleway --namespace-id <id>
|
|
48
|
+
# ...or --target cloudflare, or --target node for a self-hosted sidecar
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then point the form at the deployed function: set `CONTACT_ENDPOINT` in
|
|
52
|
+
`src/config.ts` to the URL `voltro serverless deploy` printed, and rebuild.
|
|
53
|
+
|
|
54
|
+
## What this template demonstrates
|
|
55
|
+
|
|
56
|
+
| Concept | Where |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Static page + selective hydration (`interactive: 'islands'`) | `src/pages/index.tsx` |
|
|
59
|
+
| An island with `hydrate: 'visible'` | `src/components/ContactForm.island.tsx` |
|
|
60
|
+
| A serverless function (`defineServerless`) | `functions/sendMessage.serverless.ts` |
|
|
61
|
+
| Schema-validated input → typed 400 | the function's `input` |
|
|
62
|
+
| Outbound HTTP via `HttpClient` (Resend) | the function's handler |
|
|
63
|
+
| `ServerlessHttpError` to control the status (503 / 502) | the function's handler |
|
|
64
|
+
| Secrets from `ctx.env` (RESEND_API_KEY) | the function's handler |
|
|
65
|
+
| Cross-origin fetch that works locally (CORS) | the runner + the island |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Web app for the {{projectName}} project — a STATIC page with a serverless
|
|
2
|
+
// contact form. The page itself is pre-rendered (`renderMode = 'static'`); the
|
|
3
|
+
// form is an island that POSTs to a SEPARATE serverless function
|
|
4
|
+
// (functions/sendMessage.serverless.ts), which you deploy with
|
|
5
|
+
// `voltro serverless`. The page can ship to a CDN; the function scales to zero.
|
|
6
|
+
import { defineEnv, envVar } from '@voltro/env'
|
|
7
|
+
|
|
8
|
+
export const env = defineEnv({
|
|
9
|
+
VOLTRO_PUBLIC_APP_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
type: 'web' as const,
|
|
14
|
+
name: '{{capProjectName}}{{capAppName}}',
|
|
15
|
+
port: {{port}},
|
|
16
|
+
theme: 'system' as const,
|
|
17
|
+
// Bilingual, Strategy B (URL-prefix): English at the bare path, German
|
|
18
|
+
// under /de. `voltro build` pre-renders a per-locale variant for every
|
|
19
|
+
// `[locale]/…` mirror page. Keep in sync with SUPPORTED_LOCALES /
|
|
20
|
+
// DEFAULT_LOCALE in src/lib/locale.ts. Every code listed MUST have a
|
|
21
|
+
// matching src/locales/<code>.ts or boot fails.
|
|
22
|
+
locales: ['en', 'de'] as const,
|
|
23
|
+
defaultLocale: 'en' as const,
|
|
24
|
+
env,
|
|
25
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// The serverless backend for the contact form.
|
|
2
|
+
//
|
|
3
|
+
// A standalone `*.serverless.ts` — it is NOT part of a long-running api. The
|
|
4
|
+
// CLI bundles + ships it on its own:
|
|
5
|
+
//
|
|
6
|
+
// voltro serverless dev functions/sendMessage.serverless.ts # run locally (:8910)
|
|
7
|
+
// voltro serverless deploy --target node | cloudflare | scaleway # ship it
|
|
8
|
+
//
|
|
9
|
+
// It receives the form payload, validates it against `input`, and sends an
|
|
10
|
+
// email via Resend's HTTP API using the framework-provided `HttpClient`.
|
|
11
|
+
// Edge-safe: no `node:*`, so it also runs on a Cloudflare Worker.
|
|
12
|
+
//
|
|
13
|
+
// Secrets come from `ctx.env` (set at deploy time): RESEND_API_KEY is required;
|
|
14
|
+
// CONTACT_TO / CONTACT_FROM are optional overrides.
|
|
15
|
+
|
|
16
|
+
import { Effect, Schema } from 'effect'
|
|
17
|
+
import { HttpClient, HttpClientRequest } from '@effect/platform'
|
|
18
|
+
import { defineServerless, ServerlessHttpError } from '@voltro/serverless'
|
|
19
|
+
|
|
20
|
+
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/
|
|
21
|
+
|
|
22
|
+
export default defineServerless({
|
|
23
|
+
name: 'send-message',
|
|
24
|
+
method: 'POST',
|
|
25
|
+
input: Schema.Struct({
|
|
26
|
+
name: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(120)),
|
|
27
|
+
email: Schema.String.pipe(Schema.pattern(EMAIL_RE)),
|
|
28
|
+
message: Schema.String.pipe(Schema.minLength(1), Schema.maxLength(5000)),
|
|
29
|
+
}),
|
|
30
|
+
output: Schema.Struct({ ok: Schema.Boolean }),
|
|
31
|
+
runtime: { memoryMb: 128, timeoutSeconds: 10 },
|
|
32
|
+
handler: ({ name, email, message }, ctx) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const apiKey = ctx.env.RESEND_API_KEY
|
|
35
|
+
const to = ctx.env.CONTACT_TO ?? 'you@example.com'
|
|
36
|
+
const from = ctx.env.CONTACT_FROM ?? 'Contact form <onboarding@resend.dev>'
|
|
37
|
+
|
|
38
|
+
// No key wired yet → fail with a clear 503 the form can render. The
|
|
39
|
+
// function never pretends to send: honest in dev, honest in prod.
|
|
40
|
+
if (!apiKey) {
|
|
41
|
+
return yield* Effect.fail(
|
|
42
|
+
new ServerlessHttpError({
|
|
43
|
+
status: 503,
|
|
44
|
+
message: 'email-not-configured',
|
|
45
|
+
detail: 'Set RESEND_API_KEY (+ optionally CONTACT_TO / CONTACT_FROM) in the function env.',
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const http = yield* HttpClient.HttpClient
|
|
51
|
+
const request = HttpClientRequest.post('https://api.resend.com/emails').pipe(
|
|
52
|
+
HttpClientRequest.setHeader('authorization', `Bearer ${apiKey}`),
|
|
53
|
+
HttpClientRequest.bodyUnsafeJson({
|
|
54
|
+
from,
|
|
55
|
+
to: [to],
|
|
56
|
+
reply_to: email,
|
|
57
|
+
subject: `New message from ${name}`,
|
|
58
|
+
text: `${message}\n\n— ${name} <${email}>`,
|
|
59
|
+
}),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const response = yield* http.execute(request)
|
|
63
|
+
if (response.status >= 400) {
|
|
64
|
+
const detail = yield* response.text
|
|
65
|
+
return yield* Effect.fail(new ServerlessHttpError({ status: 502, message: 'email-send-failed', detail }))
|
|
66
|
+
}
|
|
67
|
+
return { ok: true }
|
|
68
|
+
}),
|
|
69
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@{{projectName}}/{{appName}}",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "voltro dev .",
|
|
8
|
+
"build": "voltro build .",
|
|
9
|
+
"start": "voltro start .",
|
|
10
|
+
"fn:dev": "voltro serverless dev functions/sendMessage.serverless.ts",
|
|
11
|
+
"fn:deploy": "voltro serverless deploy --target node",
|
|
12
|
+
"test": "voltro test",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@effect/platform": "^0.96.1",
|
|
17
|
+
"@voltro/cli": "0.1.0",
|
|
18
|
+
"@voltro/client": "0.1.0",
|
|
19
|
+
"@voltro/env": "0.1.0",
|
|
20
|
+
"@voltro/i18n": "0.1.0",
|
|
21
|
+
"@voltro/serverless": "0.1.0",
|
|
22
|
+
"@voltro/web": "0.1.0",
|
|
23
|
+
"effect": "^3.21.2",
|
|
24
|
+
"react": "^19.0.0",
|
|
25
|
+
"react-dom": "^19.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^19.0.0",
|
|
29
|
+
"@types/react-dom": "^19.0.0",
|
|
30
|
+
"typescript": "^5.7.0",
|
|
31
|
+
"vitest": "^3.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// <ContactForm> — the island inside the static contact page. It POSTs
|
|
4
|
+
// { name, email, message } as JSON to CONTACT_ENDPOINT and reflects the
|
|
5
|
+
// request lifecycle (idle → sending → sent | error). We mock `island` to a
|
|
6
|
+
// passthrough (so the bare component renders without the marker wrapper),
|
|
7
|
+
// pin CONTACT_ENDPOINT, and stub `fetch` to assert the submit payload and the
|
|
8
|
+
// success + error UI transitions.
|
|
9
|
+
//
|
|
10
|
+
// The island is i18n-agnostic: it takes all its UI copy as a `labels` prop
|
|
11
|
+
// (the page resolves it per-locale). We pass the English labels here so the
|
|
12
|
+
// assertions read the same strings the page would supply.
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
15
|
+
import { act, createElement, type ComponentType, type ReactNode } from 'react'
|
|
16
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
17
|
+
import type { ContactFormLabels } from './ContactForm.island'
|
|
18
|
+
|
|
19
|
+
vi.mock('@voltro/web', () => ({
|
|
20
|
+
// Passthrough: return the component untouched so we test its own behavior,
|
|
21
|
+
// not the island marker wrapper.
|
|
22
|
+
island: (Component: ComponentType<unknown>) => Component,
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
vi.mock('../config', () => ({ CONTACT_ENDPOINT: 'https://fn.test/contact' }))
|
|
26
|
+
|
|
27
|
+
const { default: ContactForm } = await import('./ContactForm.island')
|
|
28
|
+
|
|
29
|
+
// English labels — the same strings the page resolves from the en catalog.
|
|
30
|
+
const labels: ContactFormLabels = {
|
|
31
|
+
name: 'Name',
|
|
32
|
+
email: 'Email',
|
|
33
|
+
message: 'Message',
|
|
34
|
+
send: 'Send message',
|
|
35
|
+
sending: 'Sending…',
|
|
36
|
+
sentNotice: 'Thanks — your message is on its way. ✓',
|
|
37
|
+
errorPrefix: 'Could not send:',
|
|
38
|
+
networkError: 'network error',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
42
|
+
|
|
43
|
+
let container: HTMLDivElement
|
|
44
|
+
let root: Root
|
|
45
|
+
const fetchMock = vi.fn<typeof fetch>()
|
|
46
|
+
|
|
47
|
+
const render = (node: ReactNode): void => {
|
|
48
|
+
container = document.createElement('div')
|
|
49
|
+
document.body.appendChild(container)
|
|
50
|
+
act(() => {
|
|
51
|
+
root = createRoot(container)
|
|
52
|
+
root.render(node)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const setInput = (el: HTMLInputElement | HTMLTextAreaElement, value: string): void => {
|
|
57
|
+
const proto =
|
|
58
|
+
el instanceof window.HTMLTextAreaElement
|
|
59
|
+
? window.HTMLTextAreaElement.prototype
|
|
60
|
+
: window.HTMLInputElement.prototype
|
|
61
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')!.set!
|
|
62
|
+
act(() => {
|
|
63
|
+
setter.call(el, value)
|
|
64
|
+
el.dispatchEvent(new Event('input', { bubbles: true }))
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fill = (): void => {
|
|
69
|
+
setInput(container.querySelector('#name') as HTMLInputElement, 'Ada')
|
|
70
|
+
setInput(container.querySelector('#email') as HTMLInputElement, 'ada@example.com')
|
|
71
|
+
setInput(container.querySelector('#message') as HTMLTextAreaElement, 'hello there')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const submit = async (): Promise<void> => {
|
|
75
|
+
const form = container.querySelector('form') as HTMLFormElement
|
|
76
|
+
await act(async () => {
|
|
77
|
+
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
fetchMock.mockReset()
|
|
83
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
if (root) act(() => root.unmount())
|
|
88
|
+
container?.remove()
|
|
89
|
+
document.body.innerHTML = ''
|
|
90
|
+
vi.unstubAllGlobals()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('ContactForm — render', () => {
|
|
94
|
+
test('renders the name/email/message fields and a submit button', () => {
|
|
95
|
+
render(createElement(ContactForm, { labels }))
|
|
96
|
+
expect(container.querySelector('#name')).toBeTruthy()
|
|
97
|
+
expect(container.querySelector('#email')).toBeTruthy()
|
|
98
|
+
expect(container.querySelector('#message')).toBeTruthy()
|
|
99
|
+
expect((container.querySelector('button[type="submit"]') as HTMLButtonElement).textContent)
|
|
100
|
+
.toContain('Send message')
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('ContactForm — submit', () => {
|
|
105
|
+
test('POSTs the form values as JSON to CONTACT_ENDPOINT and shows the sent notice', async () => {
|
|
106
|
+
fetchMock.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }))
|
|
107
|
+
render(createElement(ContactForm, { labels }))
|
|
108
|
+
fill()
|
|
109
|
+
await submit()
|
|
110
|
+
|
|
111
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
112
|
+
const [url, init] = fetchMock.mock.calls[0]!
|
|
113
|
+
expect(url).toBe('https://fn.test/contact')
|
|
114
|
+
expect(init?.method).toBe('POST')
|
|
115
|
+
expect(JSON.parse(init?.body as string)).toEqual({
|
|
116
|
+
name: 'Ada',
|
|
117
|
+
email: 'ada@example.com',
|
|
118
|
+
message: 'hello there',
|
|
119
|
+
})
|
|
120
|
+
expect(container.textContent).toContain('on its way')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('surfaces the server error detail when the response is not ok', async () => {
|
|
124
|
+
fetchMock.mockResolvedValue(
|
|
125
|
+
new Response(JSON.stringify({ detail: 'rate limited' }), { status: 429 }),
|
|
126
|
+
)
|
|
127
|
+
render(createElement(ContactForm, { labels }))
|
|
128
|
+
fill()
|
|
129
|
+
await submit()
|
|
130
|
+
expect(container.textContent).toContain('Could not send')
|
|
131
|
+
expect(container.textContent).toContain('rate limited')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('surfaces a network error when fetch rejects', async () => {
|
|
135
|
+
fetchMock.mockRejectedValue(new Error('offline'))
|
|
136
|
+
render(createElement(ContactForm, { labels }))
|
|
137
|
+
fill()
|
|
138
|
+
await submit()
|
|
139
|
+
expect(container.textContent).toContain('Could not send')
|
|
140
|
+
expect(container.textContent).toContain('offline')
|
|
141
|
+
})
|
|
142
|
+
})
|