@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,72 @@
|
|
|
1
|
+
// The URL-prefix i18n helpers — the load-bearing pure functions that drive the
|
|
2
|
+
// [locale] mirror routing + the language switcher. `useUrlLocale` (the one hook
|
|
3
|
+
// in this module) reads useLocation from @voltro/web; the rest are pure string
|
|
4
|
+
// functions we can exercise directly.
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
SUPPORTED_LOCALES,
|
|
9
|
+
DEFAULT_LOCALE,
|
|
10
|
+
getCatalog,
|
|
11
|
+
localeFromPathname,
|
|
12
|
+
withLocalePrefix,
|
|
13
|
+
stripLocalePrefix,
|
|
14
|
+
} from './locale'
|
|
15
|
+
|
|
16
|
+
describe('locale constants', () => {
|
|
17
|
+
test('supports en + de with en as the default', () => {
|
|
18
|
+
expect(SUPPORTED_LOCALES).toEqual(['en', 'de'])
|
|
19
|
+
expect(DEFAULT_LOCALE).toBe('en')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('getCatalog', () => {
|
|
24
|
+
test('returns the matching catalog for a supported locale', () => {
|
|
25
|
+
expect(getCatalog('de')['nav.language']).toBe('Sprache')
|
|
26
|
+
expect(getCatalog('en')['nav.language']).toBe('Language')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('falls back to the default catalog for an unsupported locale', () => {
|
|
30
|
+
expect(getCatalog('zz')['nav.language']).toBe('Language')
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('localeFromPathname', () => {
|
|
35
|
+
test('extracts a prefixed locale', () => {
|
|
36
|
+
expect(localeFromPathname('/de')).toBe('de')
|
|
37
|
+
expect(localeFromPathname('/de/about')).toBe('de')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('defaults to en for the bare path or an unknown prefix', () => {
|
|
41
|
+
expect(localeFromPathname('/')).toBe('en')
|
|
42
|
+
expect(localeFromPathname('/about')).toBe('en')
|
|
43
|
+
expect(localeFromPathname('/fr/about')).toBe('en')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('withLocalePrefix', () => {
|
|
48
|
+
test('leaves default-locale paths bare', () => {
|
|
49
|
+
expect(withLocalePrefix('/about', 'en')).toBe('/about')
|
|
50
|
+
expect(withLocalePrefix('/', 'en')).toBe('/')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('prefixes non-default locales, mapping / to /<locale>', () => {
|
|
54
|
+
expect(withLocalePrefix('/about', 'de')).toBe('/de/about')
|
|
55
|
+
expect(withLocalePrefix('/', 'de')).toBe('/de')
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('stripLocalePrefix', () => {
|
|
60
|
+
test('removes a locale prefix, mapping /<locale> back to /', () => {
|
|
61
|
+
expect(stripLocalePrefix('/de/about')).toBe('/about')
|
|
62
|
+
expect(stripLocalePrefix('/de')).toBe('/')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('leaves an unprefixed path unchanged', () => {
|
|
66
|
+
expect(stripLocalePrefix('/about')).toBe('/about')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('round-trips with withLocalePrefix', () => {
|
|
70
|
+
expect(stripLocalePrefix(withLocalePrefix('/about', 'de'))).toBe('/about')
|
|
71
|
+
})
|
|
72
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The single source of truth for this app's locales + the URL-prefix
|
|
2
|
+
// helpers. `app.config.ts` lists the same codes for the build pipeline;
|
|
3
|
+
// the [locale] mirror pages + the layout's <I18nProvider> read THIS file.
|
|
4
|
+
//
|
|
5
|
+
// Keep SUPPORTED_LOCALES / DEFAULT_LOCALE in sync with `locales` /
|
|
6
|
+
// `defaultLocale` in app.config.ts.
|
|
7
|
+
|
|
8
|
+
import { useLocation } from '@voltro/web'
|
|
9
|
+
import en from '../locales/en'
|
|
10
|
+
import de from '../locales/de'
|
|
11
|
+
|
|
12
|
+
export const SUPPORTED_LOCALES = ['en', 'de'] as const
|
|
13
|
+
export const DEFAULT_LOCALE = 'en'
|
|
14
|
+
|
|
15
|
+
export type Locale = (typeof SUPPORTED_LOCALES)[number]
|
|
16
|
+
|
|
17
|
+
// The catalogs, keyed by code. The layout picks one to feed the inner
|
|
18
|
+
// <I18nProvider>; `meta` (build-time, per locale) reads one via getCatalog.
|
|
19
|
+
export const CATALOGS = { en, de } as const
|
|
20
|
+
|
|
21
|
+
// Every catalog mirrors `en`'s keys (defineLocale enforces it), so a
|
|
22
|
+
// plain string-record over en's keys is the common type both satisfy —
|
|
23
|
+
// what `meta` reads at build time (`c['meta.home.title']` → string).
|
|
24
|
+
export type Messages = Record<keyof typeof en, string>
|
|
25
|
+
|
|
26
|
+
const isSupported = (v: string): v is Locale =>
|
|
27
|
+
(SUPPORTED_LOCALES as ReadonlyArray<string>).includes(v)
|
|
28
|
+
|
|
29
|
+
export const getCatalog = (locale: string): Messages =>
|
|
30
|
+
isSupported(locale) ? CATALOGS[locale] : CATALOGS[DEFAULT_LOCALE]
|
|
31
|
+
|
|
32
|
+
// `/de/about` → 'de'; `/about` → 'en' (default lives at the bare path).
|
|
33
|
+
export const localeFromPathname = (pathname: string): Locale => {
|
|
34
|
+
const match = /^\/([a-z]{2})(?:\/|$)/.exec(pathname)
|
|
35
|
+
return match && isSupported(match[1]!) ? (match[1] as Locale) : DEFAULT_LOCALE
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Reactive: useLocation re-renders on client-side nav, so the layout's
|
|
39
|
+
// provider swaps catalogs when you move between /about and /de/about
|
|
40
|
+
// without a full reload.
|
|
41
|
+
export const useUrlLocale = (): Locale => localeFromPathname(useLocation())
|
|
42
|
+
|
|
43
|
+
// '/about' + 'de' → '/de/about'; the default locale stays bare ('/about').
|
|
44
|
+
export const withLocalePrefix = (path: string, locale: string): string => {
|
|
45
|
+
if (locale === DEFAULT_LOCALE) return path
|
|
46
|
+
const p = path.startsWith('/') ? path : `/${path}`
|
|
47
|
+
return p === '/' ? `/${locale}` : `/${locale}${p}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// '/de/about' → '/about'; '/about' → '/about'. The inverse of the above,
|
|
51
|
+
// used to keep the active path when switching language.
|
|
52
|
+
export const stripLocalePrefix = (pathname: string): string => {
|
|
53
|
+
const match = /^\/([a-z]{2})(\/.*|$)/.exec(pathname)
|
|
54
|
+
return match && isSupported(match[1]!) ? match[2] || '/' : pathname
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// German catalog. MUST mirror en.ts key-for-key — `defineLocale<typeof en>()`
|
|
2
|
+
// returns a curried function that fails `tsc --noEmit` on a missing or extra
|
|
3
|
+
// key, so translations can never silently drift out of sync.
|
|
4
|
+
import { defineLocale } from '@voltro/i18n'
|
|
5
|
+
import en from './en'
|
|
6
|
+
|
|
7
|
+
export default defineLocale<typeof en>()({
|
|
8
|
+
'nav.language': 'Sprache',
|
|
9
|
+
|
|
10
|
+
'home.heading': 'Blog',
|
|
11
|
+
'home.postCount': '{count} Beiträge · zur Build-Zeit vorgerendert',
|
|
12
|
+
|
|
13
|
+
'post.readingTime': '{minutes} Min.',
|
|
14
|
+
'post.readingTimeRead': '{minutes} Min. Lesezeit',
|
|
15
|
+
'post.backToList': '← Alle Beiträge',
|
|
16
|
+
|
|
17
|
+
'meta.home.title': '{{capProjectName}} — Blog',
|
|
18
|
+
'meta.home.description': 'Ein statischer, vorgerenderter Blog, gebaut mit Voltro.',
|
|
19
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Base catalog — the source of truth. Every other locale (de.ts) mirrors
|
|
2
|
+
// these keys EXACTLY; `defineLocale<typeof en>()` enforces that at `tsc`.
|
|
3
|
+
//
|
|
4
|
+
// CHROME ONLY: nav, headings, section labels + page meta live here. Post
|
|
5
|
+
// titles / excerpts / bodies / dates are CONTENT (src/content/posts.ts) and
|
|
6
|
+
// are NOT translated — they stay in the data source.
|
|
7
|
+
//
|
|
8
|
+
// Keep ICU placeholders (`{count}`, `{minutes}`) identical across locales —
|
|
9
|
+
// react-intl validates them at render time. `{{capProjectName}}` is a scaffold
|
|
10
|
+
// token (replaced by `voltro create-project`), only ever read from `meta`
|
|
11
|
+
// (plain property access, never parsed as ICU) — leave it verbatim.
|
|
12
|
+
import { defineCatalog } from '@voltro/i18n'
|
|
13
|
+
|
|
14
|
+
export default defineCatalog({
|
|
15
|
+
'nav.language': 'Language',
|
|
16
|
+
|
|
17
|
+
'home.heading': 'Blog',
|
|
18
|
+
'home.postCount': '{count} posts · pre-rendered at build time',
|
|
19
|
+
|
|
20
|
+
'post.readingTime': '{minutes} min',
|
|
21
|
+
'post.readingTimeRead': '{minutes} min read',
|
|
22
|
+
'post.backToList': '← All posts',
|
|
23
|
+
|
|
24
|
+
'meta.home.title': '{{capProjectName}} — Blog',
|
|
25
|
+
'meta.home.description': 'A static, pre-rendered blog built with Voltro.',
|
|
26
|
+
} as const)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Locale mirror of `/blog/[slug]` → renders `/de/blog/<slug>`. Re-exports the
|
|
2
|
+
// bare post page verbatim; only `getStaticPaths` differs.
|
|
3
|
+
//
|
|
4
|
+
// This is a DYNAMIC route under a DYNAMIC locale segment, so the paths to
|
|
5
|
+
// pre-render are the CROSS-PRODUCT of every non-default locale × every post
|
|
6
|
+
// slug (the bare page enumerates just the slugs; here each slug is emitted
|
|
7
|
+
// once per non-default locale). `voltro build` produces one HTML file per
|
|
8
|
+
// pair, e.g. /de/blog/hello-static.
|
|
9
|
+
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../../lib/locale'
|
|
10
|
+
import { posts } from '../../../content/posts'
|
|
11
|
+
|
|
12
|
+
export { default } from '../../blog/[slug]'
|
|
13
|
+
export { renderMode, interactive, loader, meta } from '../../blog/[slug]'
|
|
14
|
+
|
|
15
|
+
export const getStaticPaths = async (): Promise<
|
|
16
|
+
Array<{ params: { locale: string; slug: string } }>
|
|
17
|
+
> =>
|
|
18
|
+
SUPPORTED_LOCALES
|
|
19
|
+
.filter((l) => l !== DEFAULT_LOCALE)
|
|
20
|
+
.flatMap((locale) => posts.map((post) => ({ params: { locale, slug: post.slug } })))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Locale mirror of `/` → renders `/de`. Re-exports the bare page verbatim
|
|
2
|
+
// (the component reads its locale from the URL via the layout's provider);
|
|
3
|
+
// `getStaticPaths` tells `voltro build` which locale-prefixed paths to emit.
|
|
4
|
+
// The default locale is NOT listed here — it lives at the bare `/`.
|
|
5
|
+
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../lib/locale'
|
|
6
|
+
|
|
7
|
+
export { default } from '../index'
|
|
8
|
+
export { renderMode, interactive, meta } from '../index'
|
|
9
|
+
|
|
10
|
+
export const getStaticPaths = async (): Promise<Array<{ params: { locale: string } }>> =>
|
|
11
|
+
SUPPORTED_LOCALES
|
|
12
|
+
.filter((l) => l !== DEFAULT_LOCALE)
|
|
13
|
+
.map((locale) => ({ params: { locale } }))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// The [locale] mirror pages re-export the bare page and add getStaticPaths,
|
|
4
|
+
// which tells `voltro build` which locale-prefixed paths to emit. The default
|
|
5
|
+
// locale (en) is NOT listed — it lives at the bare path — so only /de is
|
|
6
|
+
// produced. The dynamic post mirror emits the CROSS-PRODUCT of non-default
|
|
7
|
+
// locales × post slugs (here: de × every post).
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test, vi } from 'vitest'
|
|
10
|
+
import type { ComponentType } from 'react'
|
|
11
|
+
import { posts } from '../../content/posts'
|
|
12
|
+
|
|
13
|
+
// The mirrors transitively import the base pages, which import @voltro/web
|
|
14
|
+
// (island / loader / notFound / useLocation via lib/locale) and @voltro/i18n.
|
|
15
|
+
// Stub both so importing the modules doesn't require the real runtimes.
|
|
16
|
+
vi.mock('@voltro/web', () => ({
|
|
17
|
+
useLoaderData: () => ({}),
|
|
18
|
+
notFound: (detail?: string) => {
|
|
19
|
+
throw new Error(detail)
|
|
20
|
+
},
|
|
21
|
+
island: (Component: ComponentType<unknown>) => Component,
|
|
22
|
+
useLocation: () => '/',
|
|
23
|
+
}))
|
|
24
|
+
vi.mock('@voltro/i18n', () => ({
|
|
25
|
+
defineCatalog: <T,>(c: T): T => c,
|
|
26
|
+
defineLocale:
|
|
27
|
+
() =>
|
|
28
|
+
<T,>(c: T): T =>
|
|
29
|
+
c,
|
|
30
|
+
useLocale: () => 'en',
|
|
31
|
+
useT: (id: string) => id,
|
|
32
|
+
T: ({ id }: { id: string }) => id,
|
|
33
|
+
}))
|
|
34
|
+
|
|
35
|
+
const indexMirror = await import('./index')
|
|
36
|
+
const postMirror = await import('./blog/[slug]')
|
|
37
|
+
|
|
38
|
+
describe('home mirror — getStaticPaths', () => {
|
|
39
|
+
test('emits only non-default locales (just /de)', async () => {
|
|
40
|
+
const paths = await indexMirror.getStaticPaths()
|
|
41
|
+
expect(paths).toEqual([{ params: { locale: 'de' } }])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('re-exports the base page component + its meta function', () => {
|
|
45
|
+
expect(typeof indexMirror.default).toBe('function')
|
|
46
|
+
expect(indexMirror.meta({ locale: 'de' }).title).toContain('Blog')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('post mirror — getStaticPaths (locale × slug cross-product)', () => {
|
|
51
|
+
test('emits one path per non-default locale × post slug', async () => {
|
|
52
|
+
const paths = await postMirror.getStaticPaths()
|
|
53
|
+
expect(paths).toEqual(posts.map((post) => ({ params: { locale: 'de', slug: post.slug } })))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('re-exports the base post page + its config exports', () => {
|
|
57
|
+
expect(typeof postMirror.default).toBe('function')
|
|
58
|
+
expect(postMirror.renderMode).toBe('static')
|
|
59
|
+
expect(postMirror.interactive).toBe('islands')
|
|
60
|
+
expect(typeof postMirror.loader).toBe('function')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// `/blog/[slug]` — the three static-generation exports: `getStaticPaths`
|
|
4
|
+
// (enumerate slugs), `loader` (fetch this post, `notFound()` for a miss), and
|
|
5
|
+
// `meta(loaderData)` (per-post title). Plus the article render. The page reads
|
|
6
|
+
// `useLoaderData` and renders a `<ReadingProgress>` island — both come from
|
|
7
|
+
// `@voltro/web`, mocked here so `useLoaderData` returns our post and `island`
|
|
8
|
+
// is the identity wrapper.
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
11
|
+
import { act, createElement, type ComponentType, type ReactNode } from 'react'
|
|
12
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
13
|
+
import { posts, type Post } from '../../content/posts'
|
|
14
|
+
import en from '../../locales/en'
|
|
15
|
+
|
|
16
|
+
const loaderData = vi.fn<() => Post>()
|
|
17
|
+
|
|
18
|
+
const fill = (msg: string, values?: Record<string, unknown>): string =>
|
|
19
|
+
values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg
|
|
20
|
+
|
|
21
|
+
class NotFoundError extends Error {}
|
|
22
|
+
vi.mock('@voltro/web', () => ({
|
|
23
|
+
useLoaderData: () => loaderData(),
|
|
24
|
+
// Real `notFound()` throws to abort the loader; a distinct error lets us
|
|
25
|
+
// assert the miss path without pulling in the router runtime.
|
|
26
|
+
notFound: (detail?: string) => {
|
|
27
|
+
throw new NotFoundError(detail)
|
|
28
|
+
},
|
|
29
|
+
// Real `island()` wraps the component in a hydration marker; identity keeps
|
|
30
|
+
// the inner component observable in a unit test.
|
|
31
|
+
island: (Component: ComponentType<unknown>) => Component,
|
|
32
|
+
// `withLocalePrefix` (lib/locale) reads useLocation; the post page pins the
|
|
33
|
+
// locale via useLocale, so any bare path is fine here.
|
|
34
|
+
useLocation: () => '/',
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
// The page renders chrome (back-link, reading-time) via <T>/useLocale — resolve
|
|
38
|
+
// them against the real English catalog with a controllable "current locale".
|
|
39
|
+
vi.mock('@voltro/i18n', () => ({
|
|
40
|
+
defineCatalog: <T,>(c: T): T => c,
|
|
41
|
+
defineLocale:
|
|
42
|
+
() =>
|
|
43
|
+
<T,>(c: T): T =>
|
|
44
|
+
c,
|
|
45
|
+
useLocale: () => 'en',
|
|
46
|
+
T: ({ id, values }: { id: string; values?: Record<string, unknown> }) =>
|
|
47
|
+
fill((en as Record<string, string>)[id] ?? id, values),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
const { default: BlogPost, getStaticPaths, loader, meta } = await import('./[slug]')
|
|
51
|
+
|
|
52
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
53
|
+
|
|
54
|
+
let container: HTMLDivElement
|
|
55
|
+
let root: Root
|
|
56
|
+
|
|
57
|
+
const render = (node: ReactNode): void => {
|
|
58
|
+
container = document.createElement('div')
|
|
59
|
+
document.body.appendChild(container)
|
|
60
|
+
act(() => {
|
|
61
|
+
root = createRoot(container)
|
|
62
|
+
root.render(node)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
loaderData.mockReset()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
if (root) act(() => root.unmount())
|
|
72
|
+
container?.remove()
|
|
73
|
+
document.body.innerHTML = ''
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('blog [slug] — getStaticPaths', () => {
|
|
77
|
+
test('enumerates one path per post', async () => {
|
|
78
|
+
const paths = await getStaticPaths()
|
|
79
|
+
expect(paths.map((p) => p.params.slug)).toEqual(posts.map((p) => p.slug))
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('blog [slug] — loader', () => {
|
|
84
|
+
test('returns the matching post for a known slug', async () => {
|
|
85
|
+
const post = await loader({ params: { slug: 'hello-static' } } as never)
|
|
86
|
+
expect(post.slug).toBe('hello-static')
|
|
87
|
+
expect(post.title).toContain('zero JavaScript')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('throws notFound for an unknown slug', async () => {
|
|
91
|
+
await expect(loader({ params: { slug: 'nope' } } as never)).rejects.toBeInstanceOf(
|
|
92
|
+
NotFoundError,
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe('blog [slug] — meta', () => {
|
|
98
|
+
test('builds a per-post title + description from the loader data', () => {
|
|
99
|
+
const post = posts[0]!
|
|
100
|
+
const m = meta({ loaderData: post })
|
|
101
|
+
expect(m.title).toContain(post.title)
|
|
102
|
+
expect(m.description).toBe(post.excerpt)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('blog [slug] — render', () => {
|
|
107
|
+
test('renders the post title, meta line, and each body paragraph', () => {
|
|
108
|
+
const post = posts[1]!
|
|
109
|
+
loaderData.mockReturnValue(post)
|
|
110
|
+
render(createElement(BlogPost))
|
|
111
|
+
expect(container.querySelector('article h1')?.textContent).toBe(post.title)
|
|
112
|
+
expect(container.textContent).toContain(`${post.readingMinutes} min read`)
|
|
113
|
+
const paras = post.body.split('\n\n')
|
|
114
|
+
expect(container.querySelectorAll('article p').length).toBeGreaterThanOrEqual(paras.length)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// A dynamic route — `/blog/<slug>` — pre-rendered STATICALLY, one HTML
|
|
2
|
+
// file per post. Three exports make that work:
|
|
3
|
+
//
|
|
4
|
+
// • getStaticPaths → enumerate which slugs to build (from the content
|
|
5
|
+
// source; point it at a CMS/DB in a real app).
|
|
6
|
+
// • loader → fetch THIS post's data; runs at build per slug.
|
|
7
|
+
// • meta(loaderData)→ per-post <title>/<description> baked into the HTML.
|
|
8
|
+
//
|
|
9
|
+
// `interactive: 'islands'` hydrates ONLY the <ReadingProgress> island; the
|
|
10
|
+
// article itself is inert HTML.
|
|
11
|
+
|
|
12
|
+
import type { ReactNode } from 'react'
|
|
13
|
+
import { useLoaderData, notFound, type LoaderFn, type PageMeta } from '@voltro/web'
|
|
14
|
+
import { T, useLocale } from '@voltro/i18n'
|
|
15
|
+
import { posts, type Post } from '../../content/posts'
|
|
16
|
+
import { withLocalePrefix } from '../../lib/locale'
|
|
17
|
+
import ReadingProgress from '../../components/ReadingProgress.island'
|
|
18
|
+
|
|
19
|
+
export const renderMode = 'static' as const
|
|
20
|
+
export const interactive = 'islands' as const
|
|
21
|
+
|
|
22
|
+
// Which concrete paths to pre-render. Every returned `params` becomes one
|
|
23
|
+
// built HTML file; un-enumerated slugs are simply not built (→ 404).
|
|
24
|
+
export const getStaticPaths = async (): Promise<Array<{ params: { slug: string } }>> =>
|
|
25
|
+
posts.map((post) => ({ params: { slug: post.slug } }))
|
|
26
|
+
|
|
27
|
+
// Runs at build time for each enumerated slug. Returning `notFound()` skips
|
|
28
|
+
// the artifact at build / 404s at runtime — defensive, though getStaticPaths
|
|
29
|
+
// only ever feeds us slugs that exist.
|
|
30
|
+
export const loader: LoaderFn<Post> = async ({ params }) => {
|
|
31
|
+
const post = posts.find((p) => p.slug === params.slug)
|
|
32
|
+
if (!post) return notFound(`post ${params.slug}`)
|
|
33
|
+
return post
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// `meta` as a function of the loader data → correct per-post <title> in the
|
|
37
|
+
// pre-rendered HTML (great for SEO + social cards). The title/description are
|
|
38
|
+
// POST CONTENT, not chrome, so they are the same on `/blog/x` and `/de/blog/x`
|
|
39
|
+
// — no per-locale variant needed here.
|
|
40
|
+
export const meta = ({ loaderData }: { loaderData: Post }): PageMeta => ({
|
|
41
|
+
title: `${loaderData.title} — {{capProjectName}}`,
|
|
42
|
+
description: loaderData.excerpt,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export default function BlogPost(): ReactNode {
|
|
46
|
+
const post = useLoaderData<Post>()
|
|
47
|
+
// The active locale (from the layout's URL provider) keeps the "all posts"
|
|
48
|
+
// link on the current language.
|
|
49
|
+
const locale = useLocale()
|
|
50
|
+
return (
|
|
51
|
+
<main>
|
|
52
|
+
<ReadingProgress />
|
|
53
|
+
<p>
|
|
54
|
+
<a href={withLocalePrefix('/', locale)}>
|
|
55
|
+
<T id="post.backToList" />
|
|
56
|
+
</a>
|
|
57
|
+
</p>
|
|
58
|
+
<article>
|
|
59
|
+
<h1>{post.title}</h1>
|
|
60
|
+
<p className="muted">
|
|
61
|
+
{post.date} · <T id="post.readingTimeRead" values={{ minutes: post.readingMinutes }} />
|
|
62
|
+
</p>
|
|
63
|
+
{post.body.split('\n\n').map((para, i) => (
|
|
64
|
+
<p key={i}>{para}</p>
|
|
65
|
+
))}
|
|
66
|
+
</article>
|
|
67
|
+
</main>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// `/` blog index — a static list page. Two things worth testing:
|
|
4
|
+
// 1. `meta({ locale })` is localized — it reads the REAL catalog via
|
|
5
|
+
// getCatalog, so we assert en vs de descriptions directly (no mock).
|
|
6
|
+
// 2. the rendered list shows localized chrome + locale-prefixed links. The
|
|
7
|
+
// page pulls chrome from @voltro/i18n (<T>, useLocale); we mock those to
|
|
8
|
+
// resolve against the real catalogs for a controllable "current locale",
|
|
9
|
+
// so the assertions are genuine localized strings. Post titles/excerpts
|
|
10
|
+
// come from the real content source and are NOT translated.
|
|
11
|
+
|
|
12
|
+
import { afterEach, describe, expect, test, vi } from 'vitest'
|
|
13
|
+
import { act, createElement, type ReactNode } from 'react'
|
|
14
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
15
|
+
import { posts } from '../content/posts'
|
|
16
|
+
import en from '../locales/en'
|
|
17
|
+
import de from '../locales/de'
|
|
18
|
+
|
|
19
|
+
const catalogs: Record<string, Record<string, string>> = { en, de }
|
|
20
|
+
let currentLocale = 'en'
|
|
21
|
+
|
|
22
|
+
// Minimal ICU-ish interpolation: replace {key} with values[key].
|
|
23
|
+
const fill = (msg: string, values?: Record<string, unknown>): string =>
|
|
24
|
+
values ? msg.replace(/\{(\w+)\}/g, (_, k: string) => String(values[k] ?? `{${k}}`)) : msg
|
|
25
|
+
|
|
26
|
+
vi.mock('@voltro/i18n', () => ({
|
|
27
|
+
// The locale catalogs call these at module top-level; identity mirrors the
|
|
28
|
+
// real (compile-time-only) implementations so importing them doesn't throw.
|
|
29
|
+
defineCatalog: <T,>(c: T): T => c,
|
|
30
|
+
defineLocale:
|
|
31
|
+
() =>
|
|
32
|
+
<T,>(c: T): T =>
|
|
33
|
+
c,
|
|
34
|
+
useLocale: () => currentLocale,
|
|
35
|
+
useT: (id: string, values?: Record<string, unknown>) =>
|
|
36
|
+
fill(catalogs[currentLocale]?.[id] ?? id, values),
|
|
37
|
+
T: ({ id, values }: { id: string; values?: Record<string, unknown> }) =>
|
|
38
|
+
fill(catalogs[currentLocale]?.[id] ?? id, values),
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
const { default: Index, meta, renderMode, interactive } = await import('./index')
|
|
42
|
+
|
|
43
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
44
|
+
|
|
45
|
+
let container: HTMLDivElement
|
|
46
|
+
let root: Root
|
|
47
|
+
|
|
48
|
+
const render = (node: ReactNode): void => {
|
|
49
|
+
container = document.createElement('div')
|
|
50
|
+
document.body.appendChild(container)
|
|
51
|
+
act(() => {
|
|
52
|
+
root = createRoot(container)
|
|
53
|
+
root.render(node)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
currentLocale = 'en'
|
|
59
|
+
if (root) act(() => root.unmount())
|
|
60
|
+
container?.remove()
|
|
61
|
+
document.body.innerHTML = ''
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('index — config + localized meta', () => {
|
|
65
|
+
test('is a static page with zero client JS', () => {
|
|
66
|
+
expect(renderMode).toBe('static')
|
|
67
|
+
expect(interactive).toBe('none')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('meta() returns a localized title + description per locale', () => {
|
|
71
|
+
expect(meta({ locale: 'en' }).title).toContain('Blog')
|
|
72
|
+
expect(meta({ locale: 'en' }).description).toContain('pre-rendered')
|
|
73
|
+
expect(meta({ locale: 'de' }).description).toContain('vorgerenderter')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('meta() falls back to the default (en) catalog for an unknown locale', () => {
|
|
77
|
+
expect(meta({ locale: 'zz' }).description).toContain('pre-rendered')
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('index — localized render', () => {
|
|
82
|
+
test('lists every post, newest first, with bare links + English chrome (en)', () => {
|
|
83
|
+
currentLocale = 'en'
|
|
84
|
+
render(createElement(Index))
|
|
85
|
+
const links = Array.from(container.querySelectorAll('.post-list a'))
|
|
86
|
+
expect(links).toHaveLength(posts.length)
|
|
87
|
+
// Newest-first: the 2025-02-03 post precedes the 2025-01-15 one.
|
|
88
|
+
expect(links[0]!.getAttribute('href')).toBe('/blog/cms-to-ssg')
|
|
89
|
+
expect(links[links.length - 1]!.getAttribute('href')).toBe('/blog/hello-static')
|
|
90
|
+
expect(container.textContent).toContain(`${posts.length} posts`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('prefixes post links with /de and renders German chrome (de)', () => {
|
|
94
|
+
currentLocale = 'de'
|
|
95
|
+
render(createElement(Index))
|
|
96
|
+
const links = Array.from(container.querySelectorAll('.post-list a'))
|
|
97
|
+
expect(links[0]!.getAttribute('href')).toBe('/de/blog/cms-to-ssg')
|
|
98
|
+
expect(container.textContent).toContain(`${posts.length} Beiträge`)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Blog index, mounted at `/` (default locale) and mirrored at `/de` by
|
|
2
|
+
// src/pages/[locale]/index.tsx. The post LIST is pure content — no
|
|
3
|
+
// interactivity — so it ships zero JavaScript: `interactive = 'none'` strips
|
|
4
|
+
// every framework <script> from the rendered HTML. The list is read from the
|
|
5
|
+
// same content source the [slug] pages + getStaticPaths use.
|
|
6
|
+
//
|
|
7
|
+
// `meta` is a FUNCTION of `{ locale }` — that's what localises each variant's
|
|
8
|
+
// <title> / <description> in the pre-rendered HTML. Post titles/excerpts are
|
|
9
|
+
// CONTENT (src/content/posts.ts) and are NOT translated; only the chrome
|
|
10
|
+
// (heading, count line, reading-time label) comes from the catalogs.
|
|
11
|
+
|
|
12
|
+
import type { ReactNode } from 'react'
|
|
13
|
+
import type { PageMeta } from '@voltro/web'
|
|
14
|
+
import { T, useLocale } from '@voltro/i18n'
|
|
15
|
+
import { posts } from '../content/posts'
|
|
16
|
+
import { getCatalog, withLocalePrefix } from '../lib/locale'
|
|
17
|
+
|
|
18
|
+
export const renderMode = 'static' as const
|
|
19
|
+
export const interactive = 'none' as const
|
|
20
|
+
|
|
21
|
+
export const meta = ({ locale }: { locale: string }): PageMeta => {
|
|
22
|
+
const c = getCatalog(locale)
|
|
23
|
+
return {
|
|
24
|
+
title: c['meta.home.title'],
|
|
25
|
+
description: c['meta.home.description'],
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Newest first — sort a copy so the source array stays stable.
|
|
30
|
+
const byDateDesc = [...posts].sort((a, b) => b.date.localeCompare(a.date))
|
|
31
|
+
|
|
32
|
+
export default function Index(): ReactNode {
|
|
33
|
+
const locale = useLocale()
|
|
34
|
+
return (
|
|
35
|
+
<main>
|
|
36
|
+
<h1>
|
|
37
|
+
<T id="home.heading" />
|
|
38
|
+
</h1>
|
|
39
|
+
<p className="muted">
|
|
40
|
+
<T id="home.postCount" values={{ count: posts.length }} />
|
|
41
|
+
</p>
|
|
42
|
+
<ul className="post-list">
|
|
43
|
+
{byDateDesc.map((post) => (
|
|
44
|
+
<li key={post.slug}>
|
|
45
|
+
<a href={withLocalePrefix(`/blog/${post.slug}`, locale)}>{post.title}</a>
|
|
46
|
+
<p>
|
|
47
|
+
<span className="muted">
|
|
48
|
+
{post.date} · <T id="post.readingTime" values={{ minutes: post.readingMinutes }} />
|
|
49
|
+
</span>
|
|
50
|
+
{' — '}
|
|
51
|
+
{post.excerpt}
|
|
52
|
+
</p>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</ul>
|
|
56
|
+
</main>
|
|
57
|
+
)
|
|
58
|
+
}
|