@voltro/cli 0.51.0 → 0.53.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 +356 -0
- package/THIRD-PARTY-NOTICES.md +8311 -3318
- package/dist/{agentsMd-D6yD7IQv.js → agentsMd-0l980yhL.js} +4 -1
- package/dist/agentsMd-SDDSkyl4.js +2 -0
- package/dist/{apiBuild-CPDHXF72.js → apiBuild-CaPfoWku.js} +11 -5
- package/dist/apiBuild-DHtLXYx9.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/build-D-OnvNMf.js +843 -0
- package/dist/{checkCommand-DNuPiWMc.js → checkCommand-C5elt0tW.js} +92 -46
- package/dist/checkCommand-D2ZduVlh.js +2 -0
- package/dist/{cloudCmd-F4YJeqM3.js → cloudCmd-QUXh-b5w.js} +1 -1
- package/dist/codegen-BWpt3VgF.js +2 -0
- package/dist/{codegen-CrMXs4hb.js → codegen-FEk8AZHb.js} +2 -2
- package/dist/{codegenCommand-BNBHcNNj.js → codegenCommand-BOiWQ5hz.js} +12 -12
- package/dist/{codemodRunner-BDVixlSw.js → codemodRunner-BjtB2lq6.js} +691 -545
- package/dist/{commands-B1OiS9bX.js → commands-DyxAmhP0.js} +36 -36
- package/dist/{dashboardCommand-C-vvPY1B.js → dashboardCommand-BdKTyT13.js} +3 -3
- package/dist/{dataCommand-C1GxXW5q.js → dataCommand-Bab9X7s8.js} +27 -27
- package/dist/{dbCommand-If4Y1xQ-.js → dbCommand-06O2finM.js} +277 -236
- package/dist/dbCommand-B1EXBC6f.js +2 -0
- package/dist/{dev-kdAg9Q7l.js → dev-C6LGF4iY.js} +2998 -2379
- package/dist/dev-GjJWAYo2.js +3 -0
- package/dist/doctorCommand-B0hX0tdz.js +2 -0
- package/dist/{doctorCommand-nKmeW78u.js → doctorCommand-etMkflRc.js} +332 -220
- package/dist/{dormancyCommand-CY3wa_SW.js → dormancyCommand-UwZ1AZzB.js} +1 -1
- package/dist/{embeddingsCommand-BDLIgje_.js → embeddingsCommand-C70zWHwo.js} +1 -1
- package/dist/{envCommand-C6V_xVlT.js → envCommand-dSyKvRkM.js} +15 -15
- package/dist/{evolveCommand-D3c4DSfN.js → evolveCommand-CG0_ebO5.js} +2 -2
- package/dist/fileConventions-DASGEmj-.js +35 -0
- package/dist/{fileTaxonomy-CJfgOllU.js → fileTaxonomy-B7uxipWS.js} +55 -55
- package/dist/fontPipeline-LxIHa1vo.js +2 -0
- package/dist/fontPipeline-Tsh8kZfA.js +152 -0
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
- package/dist/{frameworkTableAssembly-BwJVEKLr.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
- package/dist/imagePipeline-B_GVJgm6.js +2 -0
- package/dist/imagePipeline-CBZmjT4i.js +127 -0
- package/dist/index.js +1 -1
- package/dist/{infoCommand-BnRFEF1o.js → infoCommand-_53iOc_j.js} +1 -1
- package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
- package/dist/inspect-CuoDInfZ.js +2 -0
- package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
- package/dist/inspectMetrics-CGF94puw.js +143 -0
- package/dist/manifestBuild-C4-J1-m_.js +2 -0
- package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
- package/dist/{metaCommands-CfRLra0s.js → metaCommands-Cn2oboG4.js} +9 -3
- package/dist/{migrate-DehuBakM.js → migrate-Cko9rswM.js} +2 -2
- package/dist/{pageConvention-cEiRxdab.js → pageConvention-C938S8oC.js} +1 -1
- package/dist/{privacyCommand-XejDMvmu.js → privacyCommand-DWTQMC6R.js} +2 -2
- package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
- package/dist/{projectScaffold-mIX_DpSe.js → projectScaffold-B4dmTlwT.js} +1 -1
- package/dist/{projectScaffold-BIl97_E6.js → projectScaffold-EzlErR4E.js} +1 -1
- package/dist/{renderModeScan-D7J1B7Kw.js → renderModeScan-CUbOeOAg.js} +28 -11
- package/dist/{renderProfile-1OWWAAtx.js → renderProfile-CskIgAfn.js} +2 -2
- package/dist/{runtimeTrace-ZsBU7Tkx.js → runtimeTrace-c0APJz7E.js} +1 -1
- package/dist/{sdkgen-O4XqWOjM.js → sdkgen-BiQCgIEr.js} +1 -1
- package/dist/serveCommand-CueKQgzl.js +2443 -0
- package/dist/serveCommand-DsnrVN3U.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/start-BJzZLbt8.js +3 -0
- package/dist/start-ekPan8BT.js +1510 -0
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-Dr2M6tpU.js → staticCommand-xlSL-IWk.js} +1 -1
- package/dist/{test-rXFq4S76.js → test-BWPQcRoB.js} +1 -1
- package/dist/updateCommand-Bqql_rsQ.js +2 -0
- package/dist/{updateCommand-Bs322Q78.js → updateCommand-C_8I8Rzo.js} +139 -115
- package/dist/webDev-C7jWJ5dX.js +2 -0
- package/dist/{webDev-B-ubQEMX.js → webDev-oczpugbx.js} +1767 -913
- package/dist/{webhooksCommand-FLYY9IXh.js → webhooksCommand-4SVPDjKg.js} +1 -1
- package/package.json +72 -18
- package/templates/AGENTS.core.md +11 -0
- package/templates/AGENTS.md +19 -6
- package/templates/agent-docs/_index.md +8 -6
- package/templates/agent-docs/_manifest.json +31 -15
- package/templates/agent-docs/ai.md +2 -2
- package/templates/agent-docs/authentication.md +1 -1
- package/templates/agent-docs/cli.md +97 -15
- package/templates/agent-docs/configuration.md +17 -0
- package/templates/agent-docs/data.md +680 -33
- package/templates/agent-docs/database/advancedqueries.md +7 -7
- package/templates/agent-docs/database/columntypes.md +2 -2
- package/templates/agent-docs/database/querying.md +1 -1
- package/templates/agent-docs/database/schema.md +2 -2
- package/templates/agent-docs/database/seedsdialects.md +2 -2
- package/templates/agent-docs/database/transactions.md +3 -3
- package/templates/agent-docs/deployment.md +30 -3
- package/templates/agent-docs/internationalization.md +2 -2
- package/templates/agent-docs/introduction.md +52 -0
- package/templates/agent-docs/local-first-mobile.md +132 -7
- package/templates/agent-docs/observability.md +2 -0
- package/templates/agent-docs/plugins/ai-flows.md +1 -1
- package/templates/agent-docs/plugins/audit.md +5 -5
- package/templates/agent-docs/plugins/auth.md +1 -1
- package/templates/agent-docs/plugins/cdc-out.md +2 -2
- package/templates/agent-docs/plugins/comments.md +142 -0
- package/templates/agent-docs/plugins/notifications.md +47 -4
- package/templates/agent-docs/plugins/presence.md +16 -3
- package/templates/agent-docs/plugins/prometheus.md +1 -1
- package/templates/agent-docs/plugins/queue.md +129 -0
- package/templates/agent-docs/plugins/{versioning.md → row-history.md} +22 -22
- package/templates/agent-docs/plugins/storage.md +2 -2
- package/templates/agent-docs/plugins.md +38 -12
- package/templates/agent-docs/reference.md +54 -5
- package/templates/agent-docs/routing.md +868 -50
- package/templates/agent-docs/schema-driven-ui.md +292 -5
- package/templates/agent-docs/security.md +125 -8
- package/templates/agent-docs/templates/apibackends.md +14 -14
- package/templates/agent-docs/templates/overview.md +1 -1
- package/templates/agent-docs/whats-new.md +171 -54
- package/templates/apps/api-ai/package.json +6 -7
- package/templates/apps/api-ai/tests/summarize.test.ts +1 -1
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/{api-versioning → api-row-history}/README.md +3 -3
- package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.server.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.server.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/app.config.ts +3 -3
- package/templates/apps/{api-versioning → api-row-history}/database/schema.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.server.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/mutations/documents.create.mutation.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.ts +1 -1
- package/templates/apps/{api-versioning → api-row-history}/package.json +8 -8
- package/templates/apps/api-row-history/template.json +6 -0
- package/templates/apps/{api-versioning → api-row-history}/tests/documents.create.test.ts +1 -1
- package/templates/apps/api-saas/app.config.ts +1 -0
- package/templates/apps/api-saas/package.json +10 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/app.config.ts +26 -2
- package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
- package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
- package/templates/apps/changelog/package.json +8 -8
- package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
- package/templates/apps/changelog/src/globals.d.ts +1 -1
- package/templates/apps/changelog/src/locales/de.ts +1 -1
- package/templates/apps/changelog/src/locales/en.ts +1 -1
- package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
- package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
- package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
- package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
- package/templates/apps/changelog/src/pages/page.tsx +18 -12
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +10 -9
- package/templates/apps/frontend-collab/src/pages/page.tsx +42 -65
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/package.json +8 -7
- package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
- package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
- package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
- package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +6 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
- package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
- package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/agentsMd-Bu_XQgVf.js +0 -2
- package/dist/apiBuild-GDKuGOMV.js +0 -2
- package/dist/build-DETLZAFt.js +0 -752
- package/dist/checkCommand-CWcnDArJ.js +0 -2
- package/dist/codegen-DiMn2KkZ.js +0 -2
- package/dist/dbCommand-C27HIsGE.js +0 -2
- package/dist/dev-CK522MV5.js +0 -3
- package/dist/doctorCommand-BK4l18eG.js +0 -2
- package/dist/fileConventions-Cof68_BL.js +0 -33
- package/dist/frameworkTableAssembly-CVDB2hCq.js +0 -2
- package/dist/inspect-CuGDYES0.js +0 -2
- package/dist/inspectMetrics-CfdKLh6t.js +0 -72
- package/dist/manifestBuild-CPjhvM62.js +0 -2
- package/dist/serveCommand-BRnPCxVd.js +0 -2
- package/dist/serveCommand-DdiYNBBu.js +0 -2362
- package/dist/start-BLNmWkLa.js +0 -1154
- package/dist/start-Dzicuyw8.js +0 -3
- package/dist/updateCommand-eXB35SEv.js +0 -2
- package/dist/webDev-DposiF3j.js +0 -2
- package/templates/apps/api-versioning/template.json +0 -6
- package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
- package/templates/apps/changelog/src/lib/releases.ts +0 -21
- package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
- /package/templates/apps/{api-versioning → api-row-history}/actions/documents.asOf.action.ts +0 -0
- /package/templates/apps/{api-versioning → api-row-history}/actions/documents.history.action.ts +0 -0
- /package/templates/apps/{api-versioning → api-row-history}/mutations/documents.update.mutation.server.ts +0 -0
- /package/templates/apps/{api-versioning → api-row-history}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Unit test with `@voltro/testing` — no database, no running server. The base
|
|
2
2
|
// `documents.create` mutation is a plain tenant()-scoped insert, so it's fully
|
|
3
|
-
// unit-runnable. The row-HISTORY / as-of reads are populated by the
|
|
3
|
+
// unit-runnable. The row-HISTORY / as-of reads are populated by the row-history
|
|
4
4
|
// plugin's post-commit tap (not wired into the unit store), so those are
|
|
5
5
|
// integration-tested elsewhere; here we cover the write + tenant isolation that
|
|
6
6
|
// every version-1 snapshot is built from. Run with `voltro test` (vitest).
|
|
@@ -45,6 +45,7 @@ export default {
|
|
|
45
45
|
// on the zero-infra memory store. To actually STORE + query events, add a
|
|
46
46
|
// sink AND a SQL store (the postgres-lite sink needs a SqlClient):
|
|
47
47
|
//
|
|
48
|
+
// pnpm add @voltro/plugin-analytics-postgres
|
|
48
49
|
// import { postgresAnalytics } from '@voltro/plugin-analytics-postgres'
|
|
49
50
|
// store: 'postgres' as const, // or 'sqlite' (file:./.voltro/db.sqlite)
|
|
50
51
|
// analytics: postgresAnalytics(), // events → _voltro_events; aggregate/timeseries/topN
|
|
@@ -13,20 +13,19 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/plugin-
|
|
19
|
-
"@voltro/plugin-
|
|
20
|
-
"@voltro/plugin-
|
|
21
|
-
"@voltro/plugin-
|
|
22
|
-
"@voltro/
|
|
23
|
-
"@voltro/
|
|
24
|
-
"@voltro/
|
|
25
|
-
"@voltro/sql-postgres": "0.51.0",
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/database": "0.53.0",
|
|
18
|
+
"@voltro/plugin-billing": "0.53.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.53.0",
|
|
20
|
+
"@voltro/plugin-notifications": "0.53.0",
|
|
21
|
+
"@voltro/plugin-presence": "0.53.0",
|
|
22
|
+
"@voltro/protocol": "0.53.0",
|
|
23
|
+
"@voltro/runtime": "0.53.0",
|
|
24
|
+
"@voltro/sql-postgres": "0.53.0",
|
|
26
25
|
"effect": "^3.22.0"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|
|
29
|
-
"@voltro/testing": "0.
|
|
28
|
+
"@voltro/testing": "0.53.0",
|
|
30
29
|
"typescript": "^6.0.3",
|
|
31
30
|
"@vitest/coverage-v8": "^4.1.10",
|
|
32
31
|
"vitest": "^4.1.10"
|
|
@@ -16,19 +16,19 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@effect/platform": "^0.97.0",
|
|
18
18
|
"@effect/rpc": "^0.76.0",
|
|
19
|
-
"@voltro/cli": "0.
|
|
20
|
-
"@voltro/database": "0.
|
|
21
|
-
"@voltro/env": "0.
|
|
22
|
-
"@voltro/plugin-auth": "0.
|
|
23
|
-
"@voltro/plugin-billing": "0.
|
|
24
|
-
"@voltro/plugin-multitenancy": "0.
|
|
25
|
-
"@voltro/protocol": "0.
|
|
26
|
-
"@voltro/runtime": "0.
|
|
27
|
-
"@voltro/sql-postgres": "0.
|
|
19
|
+
"@voltro/cli": "0.53.0",
|
|
20
|
+
"@voltro/database": "0.53.0",
|
|
21
|
+
"@voltro/env": "0.53.0",
|
|
22
|
+
"@voltro/plugin-auth": "0.53.0",
|
|
23
|
+
"@voltro/plugin-billing": "0.53.0",
|
|
24
|
+
"@voltro/plugin-multitenancy": "0.53.0",
|
|
25
|
+
"@voltro/protocol": "0.53.0",
|
|
26
|
+
"@voltro/runtime": "0.53.0",
|
|
27
|
+
"@voltro/sql-postgres": "0.53.0",
|
|
28
28
|
"effect": "^3.22.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@voltro/testing": "0.
|
|
31
|
+
"@voltro/testing": "0.53.0",
|
|
32
32
|
"typescript": "^6.0.3",
|
|
33
33
|
"@vitest/coverage-v8": "^4.1.10",
|
|
34
34
|
"vitest": "^4.1.10"
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-search": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/database": "0.53.0",
|
|
18
|
+
"@voltro/env": "0.53.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.53.0",
|
|
20
|
+
"@voltro/plugin-search": "0.53.0",
|
|
21
|
+
"@voltro/protocol": "0.53.0",
|
|
22
|
+
"@voltro/runtime": "0.53.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.53.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"@vitest/coverage-v8": "^4.1.10",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-rbac": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/database": "0.53.0",
|
|
18
|
+
"@voltro/env": "0.53.0",
|
|
19
|
+
"@voltro/plugin-rbac": "0.53.0",
|
|
20
|
+
"@voltro/protocol": "0.53.0",
|
|
21
|
+
"@voltro/runtime": "0.53.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.53.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.53.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"@vitest/coverage-v8": "^4.1.10",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
@@ -13,18 +13,18 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-multitenancy": "0.
|
|
20
|
-
"@voltro/plugin-webhooks": "0.
|
|
21
|
-
"@voltro/protocol": "0.
|
|
22
|
-
"@voltro/runtime": "0.
|
|
23
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.53.0",
|
|
17
|
+
"@voltro/database": "0.53.0",
|
|
18
|
+
"@voltro/env": "0.53.0",
|
|
19
|
+
"@voltro/plugin-multitenancy": "0.53.0",
|
|
20
|
+
"@voltro/plugin-webhooks": "0.53.0",
|
|
21
|
+
"@voltro/protocol": "0.53.0",
|
|
22
|
+
"@voltro/runtime": "0.53.0",
|
|
23
|
+
"@voltro/sql-postgres": "0.53.0",
|
|
24
24
|
"effect": "^3.22.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@voltro/testing": "0.
|
|
27
|
+
"@voltro/testing": "0.53.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"@vitest/coverage-v8": "^4.1.10",
|
|
30
30
|
"vitest": "^4.1.10"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// `port` is allocated by `voltro create-project` / `voltro add-app` from
|
|
2
2
|
// the project's portRange (in project.json).
|
|
3
3
|
import { defineEnv, envVar } from '@voltro/env'
|
|
4
|
+
import { releases, tagsOf, visibleReleases, type ReleaseEntry } from './src/collections/releases.collection'
|
|
4
5
|
|
|
5
6
|
// Typed PUBLIC environment. `VOLTRO_PUBLIC_SITE_NAME` titles the changelog
|
|
6
7
|
// (header brand + RSS feed). Public vars are validated at boot and baked
|
|
7
8
|
// into the browser bundle — NEVER put a secret here. Run `voltro env` for
|
|
8
|
-
// the manifest.
|
|
9
|
-
// value from process.env at build time.
|
|
9
|
+
// the manifest.
|
|
10
10
|
export const env = defineEnv({
|
|
11
11
|
VOLTRO_PUBLIC_SITE_NAME: envVar.string({ access: 'public', default: '{{capProjectName}}' }),
|
|
12
12
|
})
|
|
@@ -24,4 +24,28 @@ export default {
|
|
|
24
24
|
locales: ['en', 'de'] as const,
|
|
25
25
|
defaultLocale: 'en' as const,
|
|
26
26
|
env,
|
|
27
|
+
// The RSS feed is a BUILD ARTIFACT (dist/rss.xml, next to sitemap.xml) and
|
|
28
|
+
// a live dev route — built from the releases collection by the framework.
|
|
29
|
+
// Set SITE_URL in your deploy env; feed links must be absolute.
|
|
30
|
+
seo: {
|
|
31
|
+
siteUrl: process.env['SITE_URL'] ?? 'http://localhost:{{port}}',
|
|
32
|
+
},
|
|
33
|
+
feeds: [
|
|
34
|
+
{
|
|
35
|
+
path: '/rss.xml',
|
|
36
|
+
collection: releases.name,
|
|
37
|
+
title: '{{capProjectName}} Changelog',
|
|
38
|
+
description: 'Product updates and release notes.',
|
|
39
|
+
item: (entry: ReleaseEntry) =>
|
|
40
|
+
visibleReleases([entry]).length === 0
|
|
41
|
+
? null
|
|
42
|
+
: {
|
|
43
|
+
title: entry.data.title,
|
|
44
|
+
link: `/${entry.slug}`,
|
|
45
|
+
date: entry.data.releasedAt,
|
|
46
|
+
description: entry.data.summary,
|
|
47
|
+
categories: tagsOf(entry.data),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
],
|
|
27
51
|
}
|
|
@@ -6,25 +6,25 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "voltro dev .",
|
|
8
8
|
"lint": "voltro doctor .",
|
|
9
|
-
"build": "
|
|
9
|
+
"build": "voltro build .",
|
|
10
10
|
"start": "voltro start .",
|
|
11
11
|
"typecheck": "tsc --noEmit",
|
|
12
12
|
"test": "voltro test"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@voltro/
|
|
16
|
-
"
|
|
17
|
-
"@voltro/
|
|
18
|
-
"@voltro/
|
|
19
|
-
"@voltro/
|
|
20
|
-
"@voltro/
|
|
15
|
+
"@voltro/content": "0.53.0",
|
|
16
|
+
"effect": "^3.22.0",
|
|
17
|
+
"@voltro/cli": "0.53.0",
|
|
18
|
+
"@voltro/client": "0.53.0",
|
|
19
|
+
"@voltro/env": "0.53.0",
|
|
20
|
+
"@voltro/i18n": "0.53.0",
|
|
21
|
+
"@voltro/web": "0.53.0",
|
|
21
22
|
"react": "^19.0.0",
|
|
22
23
|
"react-dom": "^19.0.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/react": "^19.0.0",
|
|
26
27
|
"@types/react-dom": "^19.0.0",
|
|
27
|
-
"tsx": "^4.22.4",
|
|
28
28
|
"jsdom": "^30.0.0",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"@vitest/coverage-v8": "^4.1.10",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// The release notes as a CONTENT COLLECTION — content/releases/*.mdx through
|
|
2
|
+
// the framework's content pipeline (@voltro/content). Frontmatter is
|
|
3
|
+
// schema-checked at build time (a violation FAILS the build naming the file);
|
|
4
|
+
// bodies render to HTML with shiki-highlighted code fences on the server.
|
|
5
|
+
// The `.mdx` extension is a NAMING convention for release-note files — the
|
|
6
|
+
// body is plain markdown, not evaluated MDX.
|
|
7
|
+
//
|
|
8
|
+
// `draft: true` entries and future-dated `releasedAt` values are filtered by
|
|
9
|
+
// the pages + the RSS feed (see visibleReleases / app.config.ts feeds) — so
|
|
10
|
+
// an upcoming release can sit staged in content/releases/ without leaking.
|
|
11
|
+
|
|
12
|
+
import { Schema } from 'effect'
|
|
13
|
+
import { defineCollection, type ContentEntry } from '@voltro/content'
|
|
14
|
+
|
|
15
|
+
export const releases = defineCollection({
|
|
16
|
+
name: 'releases',
|
|
17
|
+
directory: 'content/releases',
|
|
18
|
+
schema: Schema.Struct({
|
|
19
|
+
version: Schema.String,
|
|
20
|
+
/** ISO date/datetime — ordering + the future-dated staging filter. */
|
|
21
|
+
releasedAt: Schema.String,
|
|
22
|
+
title: Schema.String,
|
|
23
|
+
summary: Schema.String,
|
|
24
|
+
/** `[feature, fix]`-style inline list in the frontmatter. */
|
|
25
|
+
tags: Schema.optional(Schema.Union(Schema.Array(Schema.String), Schema.String)),
|
|
26
|
+
/** `draft: true` keeps the entry out of the list + feed. */
|
|
27
|
+
draft: Schema.optional(Schema.String),
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
export type ReleaseData = Schema.Schema.Type<typeof releases.schema>
|
|
32
|
+
export type ReleaseEntry = ContentEntry<ReleaseData>
|
|
33
|
+
|
|
34
|
+
/** The tags as a plain list (a single-tag frontmatter parses as a string). */
|
|
35
|
+
export const tagsOf = (r: ReleaseData): ReadonlyArray<string> =>
|
|
36
|
+
Array.isArray(r.tags) ? r.tags : typeof r.tags === 'string' && r.tags !== '' ? [r.tags] : []
|
|
37
|
+
|
|
38
|
+
/** Published releases, newest first — drafts and future-dated entries are
|
|
39
|
+
* excluded (staged-but-hidden until the date arrives). */
|
|
40
|
+
export const visibleReleases = (
|
|
41
|
+
entries: ReadonlyArray<ReleaseEntry>,
|
|
42
|
+
now: Date = new Date(),
|
|
43
|
+
): ReadonlyArray<ReleaseEntry> => {
|
|
44
|
+
const nowIso = now.toISOString()
|
|
45
|
+
return [...entries]
|
|
46
|
+
.filter((e) => e.data.draft !== 'true' && e.data.releasedAt <= nowIso)
|
|
47
|
+
.sort((a, b) => b.data.releasedAt.localeCompare(a.data.releasedAt))
|
|
48
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// declared. Vite resolves it at build time; the declaration is type-only.
|
|
6
6
|
declare module '*.css'
|
|
7
7
|
|
|
8
|
-
// Minimal typing for Vite's `import.meta.glob`
|
|
8
|
+
// Minimal typing for Vite's `import.meta.glob`
|
|
9
9
|
// so the app typechecks without depending on `vite/client` types being
|
|
10
10
|
// resolvable. Vite provides the real implementation at build time.
|
|
11
11
|
interface ImportMeta {
|
|
@@ -10,7 +10,7 @@ export default defineLocale<typeof en>()({
|
|
|
10
10
|
'nav.rss': 'RSS',
|
|
11
11
|
'nav.language': 'Sprache',
|
|
12
12
|
|
|
13
|
-
'footer.builtWith': 'Gebaut mit
|
|
13
|
+
'footer.builtWith': 'Gebaut mit Voltro Content Collections',
|
|
14
14
|
|
|
15
15
|
'empty.body':
|
|
16
16
|
'Noch keine Releases — lege eine <code>.mdx</code>-Datei unter <code>content/releases/</code> mit <code>version</code>- und <code>releasedAt</code>-Frontmatter an.',
|
|
@@ -18,7 +18,7 @@ export default defineCatalog({
|
|
|
18
18
|
'nav.rss': 'RSS',
|
|
19
19
|
'nav.language': 'Language',
|
|
20
20
|
|
|
21
|
-
'footer.builtWith': 'Built with
|
|
21
|
+
'footer.builtWith': 'Built with Voltro content collections',
|
|
22
22
|
|
|
23
23
|
'empty.body':
|
|
24
24
|
'No releases yet — add an <code>.mdx</code> file under <code>content/releases/</code> with <code>version</code> + <code>releasedAt</code> frontmatter.',
|
|
@@ -10,15 +10,18 @@
|
|
|
10
10
|
// routes at the root. `voltro build` only emits the paths getStaticPaths
|
|
11
11
|
// enumerates, so the static output is unambiguous — the bare `[slug]` never
|
|
12
12
|
// enumerates a locale code, and only this mirror emits `/de/…`.
|
|
13
|
+
import { getCollection } from '@voltro/content'
|
|
13
14
|
import { SUPPORTED_LOCALES, DEFAULT_LOCALE } from '../../../lib/locale'
|
|
14
|
-
import { releases } from '../../../
|
|
15
|
+
import { releases, visibleReleases, type ReleaseData } from '../../../collections/releases.collection'
|
|
15
16
|
|
|
16
17
|
export { default } from '../../[slug]/page'
|
|
17
18
|
export { renderMode, interactive, loader, meta } from '../../[slug]/page'
|
|
18
19
|
|
|
19
20
|
export const getStaticPaths = async (): Promise<
|
|
20
21
|
Array<{ params: { locale: string; slug: string } }>
|
|
21
|
-
> =>
|
|
22
|
-
|
|
22
|
+
> => {
|
|
23
|
+
const entries = visibleReleases(await getCollection<ReleaseData>(releases.name))
|
|
24
|
+
return SUPPORTED_LOCALES
|
|
23
25
|
.filter((l) => l !== DEFAULT_LOCALE)
|
|
24
|
-
.flatMap((locale) =>
|
|
26
|
+
.flatMap((locale) => entries.map((release) => ({ params: { locale, slug: release.slug } })))
|
|
27
|
+
}
|
|
@@ -7,48 +7,16 @@
|
|
|
7
7
|
// locales × release slugs (here: de × every release).
|
|
8
8
|
|
|
9
9
|
import { describe, expect, test, vi } from 'vitest'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
import { setContentRoot } from '@voltro/content'
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
slug: string
|
|
15
|
-
tags: ReadonlyArray<string>
|
|
16
|
-
title: string
|
|
17
|
-
summary: string
|
|
18
|
-
html: string
|
|
19
|
-
}
|
|
14
|
+
// The mirrors' getStaticPaths read the REAL `releases` content collection —
|
|
15
|
+
// this app's content/releases/*.mdx.
|
|
16
|
+
setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'))
|
|
20
17
|
|
|
21
|
-
const releases: ReadonlyArray<Release> = [
|
|
22
|
-
{
|
|
23
|
-
version: '0.2.0',
|
|
24
|
-
releasedAt: '2026-06-21',
|
|
25
|
-
slug: 'v0-2-0',
|
|
26
|
-
tags: ['feature'],
|
|
27
|
-
title: 'Typed env',
|
|
28
|
-
summary: 'Declare your environment once.',
|
|
29
|
-
html: '<p>body</p>',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
version: '0.1.0',
|
|
33
|
-
releasedAt: '2026-01-15',
|
|
34
|
-
slug: 'v0-1-0',
|
|
35
|
-
tags: ['feature'],
|
|
36
|
-
title: 'First release',
|
|
37
|
-
summary: 'The initial cut.',
|
|
38
|
-
html: '<p>body</p>',
|
|
39
|
-
},
|
|
40
|
-
]
|
|
41
|
-
|
|
42
|
-
// The real release source reads .mdx via `import.meta.glob` + node-gated shiki;
|
|
43
|
-
// mock it (used by the [slug] mirror's cross-product getStaticPaths and by the
|
|
44
|
-
// transitively-imported base pages).
|
|
45
|
-
const releaseBySlug = (slug: string): Release | undefined => releases.find((r) => r.slug === slug)
|
|
46
|
-
vi.mock('../../lib/releases', () => ({ releases, releaseBySlug }))
|
|
47
|
-
vi.mock('@voltro/changelog', () => ({
|
|
48
|
-
highlightRelease: async (r: Release) => r,
|
|
49
|
-
}))
|
|
50
18
|
vi.mock('@voltro/web', () => ({
|
|
51
|
-
useLoaderData: () =>
|
|
19
|
+
useLoaderData: () => null,
|
|
52
20
|
notFound: (detail?: string) => {
|
|
53
21
|
throw new Error(detail)
|
|
54
22
|
},
|
|
@@ -81,9 +49,13 @@ describe('index mirror — getStaticPaths', () => {
|
|
|
81
49
|
})
|
|
82
50
|
|
|
83
51
|
describe('release mirror — getStaticPaths (locale × slug cross-product)', () => {
|
|
84
|
-
test('emits one path per non-default locale × release slug', async () => {
|
|
52
|
+
test('emits one path per non-default locale × published release slug', async () => {
|
|
85
53
|
const paths = await releaseMirror.getStaticPaths()
|
|
86
|
-
|
|
54
|
+
// Newest first, from the real collection.
|
|
55
|
+
expect(paths).toEqual([
|
|
56
|
+
{ params: { locale: 'de', slug: 'v0-2-0' } },
|
|
57
|
+
{ params: { locale: 'de', slug: 'v0-1-0' } },
|
|
58
|
+
])
|
|
87
59
|
})
|
|
88
60
|
|
|
89
61
|
test('re-exports the base release page + its config exports', () => {
|
|
@@ -1,51 +1,22 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
//
|
|
3
|
-
// `/[slug]` per-release page — `getStaticPaths` (one path per
|
|
4
|
-
// `loader` (
|
|
5
|
-
// `meta(loaderData)`, and the render
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// `/[slug]` per-release page — `getStaticPaths` (one path per published
|
|
4
|
+
// release), `loader` (entry lookup through the REAL `releases` content
|
|
5
|
+
// collection, `notFound()` on a miss), `meta(loaderData)`, and the render
|
|
6
|
+
// (which injects the collection's server-rendered HTML). The collection reads
|
|
7
|
+
// this app's real content/releases/*.mdx — no canned copies to drift.
|
|
8
8
|
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
|
10
10
|
import { act, createElement, type ReactNode } from 'react'
|
|
11
11
|
import { createRoot, type Root } from 'react-dom/client'
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import type { Release } from '@voltro/changelog'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { fileURLToPath } from 'node:url'
|
|
14
|
+
import { setContentRoot } from '@voltro/content'
|
|
15
|
+
import type { ReleaseEntry } from '../../collections/releases.collection'
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
version: '0.1.0',
|
|
21
|
-
releasedAt: '2026-01-15',
|
|
22
|
-
slug: 'v0-1-0',
|
|
23
|
-
tags: ['feature'],
|
|
24
|
-
title: 'First release',
|
|
25
|
-
summary: 'The initial cut.',
|
|
26
|
-
draft: false,
|
|
27
|
-
body: 'The initial cut.',
|
|
28
|
-
html: '<p>plain</p>',
|
|
29
|
-
},
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
const releaseBySlug = (slug: string): Release | undefined => releases.find((r) => r.slug === slug)
|
|
33
|
-
|
|
34
|
-
// `../../lib/releases` — the path the PAGE imports. This read `../lib/releases`,
|
|
35
|
-
// which from `src/pages/[slug]/` resolves to `src/pages/lib/releases`: a module
|
|
36
|
-
// that does not exist, so the mock registered against nothing and the REAL
|
|
37
|
-
// `src/lib/releases.ts` loaded. A wrong `vi.mock` specifier is silent in vitest
|
|
38
|
-
// — it fails later and elsewhere, here as "No `loadReleases` export is defined
|
|
39
|
-
// on the `@voltro/changelog` mock", because the real module calls it at import
|
|
40
|
-
// time. The sibling `[locale]/mirrors.test.tsx`, at the same depth, had it right.
|
|
41
|
-
vi.mock('../../lib/releases', () => ({ releases, releaseBySlug }))
|
|
42
|
-
// `highlightRelease` runs shiki in node; the page only needs it to return a
|
|
43
|
-
// (possibly re-rendered) release — the identity, tagged so we can assert it ran.
|
|
44
|
-
vi.mock('@voltro/changelog', () => ({
|
|
45
|
-
highlightRelease: async (r: Release) => ({ ...r, html: `<pre>${r.title}</pre>` }),
|
|
46
|
-
}))
|
|
17
|
+
setContentRoot(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..'))
|
|
47
18
|
|
|
48
|
-
const loaderData = vi.fn<() => { release:
|
|
19
|
+
const loaderData = vi.fn<() => { release: ReleaseEntry }>()
|
|
49
20
|
class NotFoundError extends Error {}
|
|
50
21
|
vi.mock('@voltro/web', () => ({
|
|
51
22
|
useLoaderData: () => loaderData(),
|
|
@@ -95,17 +66,19 @@ afterEach(() => {
|
|
|
95
66
|
})
|
|
96
67
|
|
|
97
68
|
describe('changelog [slug] — getStaticPaths', () => {
|
|
98
|
-
test('enumerates one path per release slug', async () => {
|
|
69
|
+
test('enumerates one path per published release slug, newest first', async () => {
|
|
99
70
|
const paths = await getStaticPaths()
|
|
100
|
-
expect(paths.map((p) => p.params.slug)).toEqual(['v0-1-0'])
|
|
71
|
+
expect(paths.map((p) => p.params.slug)).toEqual(['v0-2-0', 'v0-1-0'])
|
|
101
72
|
})
|
|
102
73
|
})
|
|
103
74
|
|
|
104
75
|
describe('changelog [slug] — loader', () => {
|
|
105
|
-
test('resolves the release
|
|
76
|
+
test('resolves the release with server-rendered, highlighted html', async () => {
|
|
106
77
|
const { release } = await loader({ params: { slug: 'v0-1-0' } } as never)
|
|
107
78
|
expect(release.slug).toBe('v0-1-0')
|
|
108
|
-
expect(release.
|
|
79
|
+
expect(release.data.version).toBe('0.1.0')
|
|
80
|
+
// The collection renders code fences with shiki on the server.
|
|
81
|
+
expect(release.html).toContain('class="shiki')
|
|
109
82
|
})
|
|
110
83
|
|
|
111
84
|
test('throws notFound for an unknown slug', async () => {
|
|
@@ -116,18 +89,20 @@ describe('changelog [slug] — loader', () => {
|
|
|
116
89
|
})
|
|
117
90
|
|
|
118
91
|
describe('changelog [slug] — meta', () => {
|
|
119
|
-
test('builds the per-release title + description from the loader data', () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
expect(m.
|
|
92
|
+
test('builds the per-release title + description from the loader data', async () => {
|
|
93
|
+
const { release } = await loader({ params: { slug: 'v0-1-0' } } as never)
|
|
94
|
+
const m = meta({ loaderData: { release } })
|
|
95
|
+
expect(m.title).toContain(release.data.title)
|
|
96
|
+
expect(m.description).toBe(release.data.summary)
|
|
123
97
|
})
|
|
124
98
|
})
|
|
125
99
|
|
|
126
100
|
describe('changelog [slug] — render', () => {
|
|
127
|
-
test('renders the title, version, and injects the
|
|
128
|
-
|
|
101
|
+
test('renders the title, version, tags, and injects the HTML body', async () => {
|
|
102
|
+
const { release } = await loader({ params: { slug: 'v0-1-0' } } as never)
|
|
103
|
+
loaderData.mockReturnValue({ release: { ...release, html: '<p>the body html</p>' } })
|
|
129
104
|
render(createElement(ReleasePage))
|
|
130
|
-
expect(container.querySelector('h1')?.textContent).toBe(
|
|
105
|
+
expect(container.querySelector('h1')?.textContent).toBe(release.data.title)
|
|
131
106
|
expect(container.textContent).toContain('v0.1.0')
|
|
132
107
|
expect(container.innerHTML).toContain('the body html')
|
|
133
108
|
})
|
|
@@ -1,34 +1,36 @@
|
|
|
1
|
-
// Per-release page. `getStaticPaths` enumerates every release slug
|
|
2
|
-
// `voltro build` pre-renders one HTML file per release. The loader
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Per-release page. `getStaticPaths` enumerates every published release slug
|
|
2
|
+
// so `voltro build` pre-renders one HTML file per release. The loader reads
|
|
3
|
+
// the entry from the `releases` collection — the body arrives as RENDERED
|
|
4
|
+
// html with shiki-highlighted code fences (server-side; no markdown engine
|
|
5
|
+
// in the browser).
|
|
5
6
|
|
|
6
7
|
import type { ReactNode } from 'react'
|
|
7
8
|
import { useLoaderData, notFound, type LoaderFn } from '@voltro/web'
|
|
8
|
-
import {
|
|
9
|
+
import { getCollection, getEntry } from '@voltro/content'
|
|
9
10
|
import { T, useLocale } from '@voltro/i18n'
|
|
10
|
-
import { releases,
|
|
11
|
+
import { releases, tagsOf, visibleReleases, type ReleaseData, type ReleaseEntry } from '../../collections/releases.collection'
|
|
11
12
|
import { withLocalePrefix } from '../../lib/locale'
|
|
12
13
|
|
|
13
14
|
export const renderMode = 'static' as const
|
|
14
15
|
export const interactive = 'none' as const
|
|
15
16
|
|
|
16
17
|
export const getStaticPaths = async (): Promise<Array<{ params: { slug: string } }>> =>
|
|
17
|
-
|
|
18
|
+
visibleReleases(await getCollection<ReleaseData>(releases.name))
|
|
19
|
+
.map((release) => ({ params: { slug: release.slug } }))
|
|
18
20
|
|
|
19
|
-
export const loader: LoaderFn<{ release:
|
|
20
|
-
const release = params.slug ?
|
|
21
|
-
if (!release) return notFound(`release ${params.slug ?? ''}`)
|
|
22
|
-
return { release
|
|
21
|
+
export const loader: LoaderFn<{ release: ReleaseEntry }> = async ({ params }) => {
|
|
22
|
+
const release = params.slug ? await getEntry<ReleaseData>(releases.name, params.slug) : null
|
|
23
|
+
if (!release || release.data.draft === 'true') return notFound(`release ${params.slug ?? ''}`)
|
|
24
|
+
return { release }
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
export const meta = ({ loaderData }: { loaderData: { release:
|
|
26
|
-
title: `${loaderData.release.title} — Changelog`,
|
|
27
|
-
description: loaderData.release.summary,
|
|
27
|
+
export const meta = ({ loaderData }: { loaderData: { release: ReleaseEntry } }) => ({
|
|
28
|
+
title: `${loaderData.release.data.title} — Changelog`,
|
|
29
|
+
description: loaderData.release.data.summary,
|
|
28
30
|
})
|
|
29
31
|
|
|
30
32
|
export default function ReleasePage(): ReactNode {
|
|
31
|
-
const { release } = useLoaderData<{ release:
|
|
33
|
+
const { release } = useLoaderData<{ release: ReleaseEntry }>()
|
|
32
34
|
// The active locale (from the layout's URL provider) keeps the back-link on
|
|
33
35
|
// the current language.
|
|
34
36
|
const locale = useLocale()
|
|
@@ -39,14 +41,14 @@ export default function ReleasePage(): ReactNode {
|
|
|
39
41
|
<T id="post.backToList" />
|
|
40
42
|
</a>
|
|
41
43
|
</p>
|
|
42
|
-
<h1>{release.title}</h1>
|
|
44
|
+
<h1>{release.data.title}</h1>
|
|
43
45
|
<p className="changelog-meta">
|
|
44
|
-
<time dateTime={release.releasedAt}>{release.releasedAt}</time> · v{release.version}
|
|
45
|
-
{release.
|
|
46
|
+
<time dateTime={release.data.releasedAt}>{release.data.releasedAt}</time> · v{release.data.version}
|
|
47
|
+
{tagsOf(release.data).map((tag) => (
|
|
46
48
|
<span key={tag} className="changelog-tag">{tag}</span>
|
|
47
49
|
))}
|
|
48
50
|
</p>
|
|
49
|
-
<div dangerouslySetInnerHTML={{ __html: release.html }} />
|
|
51
|
+
<div dangerouslySetInnerHTML={{ __html: release.html ?? '' }} />
|
|
50
52
|
</article>
|
|
51
53
|
)
|
|
52
54
|
}
|