@voltro/cli 0.19.0 → 0.20.1
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 +224 -0
- package/dist/apiBuild-C9aHz4Yh.js +2 -0
- package/dist/{apiBuild-CY5pEwwq.js → apiBuild-qZBEu59d.js} +2 -2
- package/dist/bin.js +41 -9
- package/dist/{commands-BvvoQL0v.js → commands-CA7jwejs.js} +1930 -1770
- package/dist/dbCommand-DA_RFj5K.js +2 -0
- package/dist/{dbCommand-DJSxnjPt.js → dbCommand-DFw2UoC-.js} +24 -24
- package/dist/{dev-C_E1XxNF.js → dev-2AvdzDg2.js} +1 -1
- package/dist/{dev-B6rfgKfo.js → dev-OHLsAq4K.js} +1488 -1462
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-D1DmLeJs.js → inspectMetrics-DvPNXmGA.js} +208 -179
- package/dist/{serveCommand-vPF5ucXC.js → serveCommand-BKqTKWTX.js} +341 -341
- package/dist/serveEntry.js +2 -2
- package/dist/{start-Clvz4IJb.js → start-CXQ7WL1W.js} +376 -368
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +33 -0
- package/templates/agent-docs/database/migrations.md +35 -2
- package/templates/agent-docs/database/misc.md +16 -0
- package/templates/agent-docs/database/seedsdialects.md +25 -0
- package/templates/agent-docs/database/transactions.md +15 -0
- package/templates/agent-docs/routing.md +13 -0
- package/templates/agent-docs/whats-new.md +78 -94
- package/templates/apps/api-ai/package.json +7 -7
- 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-storage/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-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- 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 +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- 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/package.json +6 -6
- package/dist/apiBuild-9NXH53Sd.js +0 -2
- package/dist/dbCommand-C3R5LrBZ.js +0 -2
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { et as e } from "./inspectMetrics-DvPNXmGA.js";
|
|
2
|
+
import { t } from "./start-CXQ7WL1W.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.107.0",
|
|
63
63
|
"@effect/sql": "^0.51.1",
|
|
64
64
|
"@effect/workflow": "^0.18.2",
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
65
|
+
"@voltro/ai": "0.20.1",
|
|
66
|
+
"@voltro/cache": "0.20.1",
|
|
67
|
+
"@voltro/data-transfer": "0.20.1",
|
|
68
|
+
"@voltro/database": "0.20.1",
|
|
69
|
+
"@voltro/env": "0.20.1",
|
|
70
|
+
"@voltro/kv": "0.20.1",
|
|
71
|
+
"@voltro/logger": "0.20.1",
|
|
72
|
+
"@voltro/plugin-auth": "0.20.1",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.20.1",
|
|
74
|
+
"@voltro/plugin-mail": "0.20.1",
|
|
75
|
+
"@voltro/plugin-storage": "0.20.1",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.20.1",
|
|
77
|
+
"@voltro/protocol": "0.20.1",
|
|
78
|
+
"@voltro/runtime": "0.20.1",
|
|
79
|
+
"@voltro/serverless": "0.20.1",
|
|
80
|
+
"@voltro/workflow": "0.20.1",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -595,7 +595,7 @@ each plugin's own README.
|
|
|
595
595
|
|
|
596
596
|
| Topic | Open | Summary |
|
|
597
597
|
|---|---|---|
|
|
598
|
-
| **What's new in 0.
|
|
598
|
+
| **What's new in 0.20.1** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
599
599
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
600
600
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
601
601
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.20.1** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -545,6 +545,21 @@ fully concurrent. Tune the cap with `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY` (below)
|
|
|
545
545
|
drop it on a low-memory box, raise it on a big machine.
|
|
546
546
|
|
|
547
547
|
|
|
548
|
+
Under `VOLTRO_LOG_LEVEL=debug` each cold compile logs its own duration, so a slow
|
|
549
|
+
first paint can be attributed to a specific module:
|
|
550
|
+
|
|
551
|
+
```
|
|
552
|
+
[voltro:dev:web] ssr cold-compile start id=/app/src/pages/layout.tsx
|
|
553
|
+
[voltro:dev:web] ssr cold-compile start id=/app/src/pages/(main)/layout.tsx
|
|
554
|
+
[voltro:dev:web] ssr cold-compile end 3743ms id=/app/src/pages/layout.tsx
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
Read the `ms` on the **end** line rather than subtracting timestamps: compiles run
|
|
558
|
+
concurrently up to the cap, so the start and end lines interleave and adjacent
|
|
559
|
+
lines usually belong to different modules. The duration is measured inside the
|
|
560
|
+
concurrency permit, so it is that module's own compile cost and not time spent
|
|
561
|
+
queued behind the cap. A compile that threw says `FAILED` instead of `end`.
|
|
562
|
+
|
|
548
563
|
### Fast Refresh: what hot-updates and what reloads
|
|
549
564
|
|
|
550
565
|
Editing a **page or layout component** applies as a hot update — the React tree
|
|
@@ -835,6 +850,24 @@ apps/acme/web/.framework/dist/
|
|
|
835
850
|
└── ssrEntry.js # SSR bundle for voltro start
|
|
836
851
|
```
|
|
837
852
|
|
|
853
|
+
## Unresolvable optional peers
|
|
854
|
+
|
|
855
|
+
The SSR bundle inlines everything it reaches (`ssr: { noExternal: true }`), which is what lets a production web image ship without a framework dependency tree. A package that cannot be **resolved at all** is externalised instead of failing the build — almost always an uninstalled optional native peer reached through a library's Node entry point:
|
|
856
|
+
|
|
857
|
+
```
|
|
858
|
+
Rolldown failed to resolve import "canvas" from ".../konva/lib/index-node.js"
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
`konva`'s `main` is its Node build, which requires the optional `canvas`; its `browser` field points at one that does not. An app that never renders to a canvas server-side has nothing to install, and there is no app-side workaround: making the import dynamic does not help (the bundler must still resolve it to form the chunk), and `renderMode: 'spa'` does not either — the generated router imports every page statically, so the module is in the SSR graph whatever the render mode.
|
|
862
|
+
|
|
863
|
+
Every specifier externalised this way is named on the success line:
|
|
864
|
+
|
|
865
|
+
```
|
|
866
|
+
[voltro:build] SSR bundle ready { path: 'dist/server/ssrEntry.js', externalizedOptionalPeers: 'canvas' }
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
Read that list. Externalising is right for an optional peer you never use, and wrong for a dependency you forgot to install — it turns a build failure into a runtime one, and only you can tell the two apart. Framework packages (`@voltro/*`, `@effect/*`, `effect`) are never externalised.
|
|
870
|
+
|
|
838
871
|
## `voltro start <appDir>`
|
|
839
872
|
|
|
840
873
|
```bash
|
|
@@ -1937,6 +1937,19 @@ source of truth. Just restoring the snapshot brings the data back
|
|
|
1937
1937
|
but the declared schema still says the column shouldn't exist; the
|
|
1938
1938
|
next apply would re-drop it.
|
|
1939
1939
|
|
|
1940
|
+
## The differ leaves snapshots alone
|
|
1941
|
+
|
|
1942
|
+
A soft drop leaves an object in the database that no schema declares —
|
|
1943
|
+
that is the whole point of it. The planner treats every
|
|
1944
|
+
`<name>__dropped_<ts>` as framework-managed and never plans a drop for
|
|
1945
|
+
it, the same way it skips `_voltro_*` and the cluster engine's tables.
|
|
1946
|
+
This holds for columns and for tables.
|
|
1947
|
+
|
|
1948
|
+
You do not need `VOLTRO_DB_IGNORE_TABLES` for a snapshot, and you should
|
|
1949
|
+
not add one: that list is for YOUR unmanaged infra tables, and an entry
|
|
1950
|
+
there would still be in your config long after `gc-snapshots` reclaimed
|
|
1951
|
+
the snapshot.
|
|
1952
|
+
|
|
1940
1953
|
## GC
|
|
1941
1954
|
|
|
1942
1955
|
Snapshot columns aren't automatically dropped. They survive until
|
|
@@ -2612,7 +2625,15 @@ export default migration({
|
|
|
2612
2625
|
// VOLTRO_DESTRUCTIVE_OK=1 voltro db apply --note 'retiring oldUsersTable per ticket #...'
|
|
2613
2626
|
```
|
|
2614
2627
|
|
|
2615
|
-
**
|
|
2628
|
+
**Delete the entity and drop the table in the SAME change.** The intuitive
|
|
2629
|
+
order — remove the code first, sort the schema out after — is the broken one:
|
|
2630
|
+
with `VOLTRO_AUTO_MIGRATE=1` the very next boot sees an undeclared table,
|
|
2631
|
+
refuses, and the app crashloops until the drop is authorised. There is nothing
|
|
2632
|
+
to recover from, but the app is down while you work it out. Take the entity out
|
|
2633
|
+
together with the `VOLTRO_DESTRUCTIVE_OK` apply that removes its table, or leave
|
|
2634
|
+
the entity in place until you are ready to run both.
|
|
2635
|
+
|
|
2636
|
+
**Three tables this never proposes dropping:**
|
|
2616
2637
|
|
|
2617
2638
|
- **`actors`** — the framework-provided audit subject. You don't declare an
|
|
2618
2639
|
`actors.entity.ts`; `db plan` / `db apply` auto-include the built-in `actors`
|
|
@@ -2633,6 +2654,10 @@ export default migration({
|
|
|
2633
2654
|
Strapi→Voltro `_strapi_id_map` sitting in the DB won't refuse-to-boot on a
|
|
2634
2655
|
`drop-table`. (The framework already self-excludes its own `_voltro_*` /
|
|
2635
2656
|
`cluster_*` runtime tables; this is the user list on top of that.)
|
|
2657
|
+
- **Soft-drop snapshots** — a `<name>__dropped_<ts>` left behind by
|
|
2658
|
+
`VOLTRO_SOFT_DROP=1`. The differ treats it as framework-managed until
|
|
2659
|
+
`voltro db gc-snapshots` reclaims it, so do NOT add one to
|
|
2660
|
+
`VOLTRO_DB_IGNORE_TABLES`.
|
|
2636
2661
|
|
|
2637
2662
|
## A migrate / apply DDL statement failed — find which one
|
|
2638
2663
|
|
|
@@ -2699,7 +2724,15 @@ it now is, must have nothing left to do.
|
|
|
2699
2724
|
|
|
2700
2725
|
**It is a framework bug, not a mistake in your schema.** The named operations
|
|
2701
2726
|
emit DDL that does not take effect. Report the operation kinds plus the column
|
|
2702
|
-
types involved.
|
|
2727
|
+
types involved.
|
|
2728
|
+
|
|
2729
|
+
**Read the named operations before you trust the "no-op" wording**, though —
|
|
2730
|
+
the message states a CAUSE, and a cause can be wrong. One release told users
|
|
2731
|
+
`drop-table <name>__dropped_<ts>` was a no-op when the DDL had worked perfectly:
|
|
2732
|
+
`VOLTRO_SOFT_DROP=1` had renamed the table, and the planner then read its own
|
|
2733
|
+
snapshot as one more undeclared table and proposed dropping it again. Fixed, and
|
|
2734
|
+
worth knowing as the shape to look for: an operation naming an object that the
|
|
2735
|
+
PREVIOUS operation created or renamed is a planner blind spot, not dead DDL. In the meantime the schema is unchanged and safe — nothing was
|
|
2703
2736
|
half-applied, and no fingerprint was written, so `voltro db plan` still shows you
|
|
2704
2737
|
the truth.
|
|
2705
2738
|
|
|
@@ -271,6 +271,22 @@ entirely. The downgrades are documented rather than hidden because the
|
|
|
271
271
|
alternative to a stated escape hatch is deleting the marker — and a check whose
|
|
272
272
|
only way out is to disable it gets disabled.
|
|
273
273
|
|
|
274
|
+
### Gate CI on the audit having RUN, not on its silence
|
|
275
|
+
|
|
276
|
+
`voltro doctor` can only run this audit if it can load your descriptors. When it
|
|
277
|
+
cannot, it says so instead of claiming a pass:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
• serverOnly: NOT CHECKED — the app's descriptors could not be loaded (not a pass)
|
|
281
|
+
reason: Transform failed with 1 error:
|
|
282
|
+
src/queries/broken.query.ts:2:5: ERROR: Expected ";" but found "is"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`voltro doctor --json` carries the same answer as `serverOnly: { checked, reason?, leaks? }`.
|
|
286
|
+
Assert on `checked` — an app that leans on `.serverOnly()` should treat a
|
|
287
|
+
persistent skip as a failure, because a skipped audit and a clean one look
|
|
288
|
+
identical from the outside.
|
|
289
|
+
|
|
274
290
|
(`voltro check` does **not** run this audit. It has a live-api mode that has no
|
|
275
291
|
access to your table definitions, and a rule that fires in one of its two modes
|
|
276
292
|
would be worse than one that fires in neither.)
|
|
@@ -802,6 +802,31 @@ Each replica persists its progress in the **`_voltro_cdc_offsets`** table — on
|
|
|
802
802
|
|
|
803
803
|
If the persisted offset has been purged (`err 1236`) or rejected after a failover, the reader jumps to the current binlog end and signals a resync so dependent subscriptions re-query rather than missing the gap.
|
|
804
804
|
|
|
805
|
+
### A table the reader cannot decode
|
|
806
|
+
|
|
807
|
+
`@vlasky/zongji` reads each event's column layout from the binlog's `Table_map` (fixed on disk) and compares it to a fresh `information_schema` fetch. A mismatch throws:
|
|
808
|
+
|
|
809
|
+
```
|
|
810
|
+
Table app.sessions schema changed between binlog event and metadata fetch:
|
|
811
|
+
the event has 9 columns, fetched metadata has 8
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
The usual cause on MariaDB is a **UNIQUE constraint on an unbounded text column**. MariaDB can only back that with a **HASH long-unique index**, and that index adds a hidden `DB_ROW_HASH_n` column to the InnoDB row — present in the binlog row image, absent from `information_schema.COLUMNS`. So the counts can never agree, and every write to that table trips it. Nothing is broken; the table is shaped that way.
|
|
815
|
+
|
|
816
|
+
The reader finds such tables when CDC starts, reports each once, and **excludes** it — so there is no reconnect loop. Cross-instance change events for that table are lost; own-node reactivity is unaffected, because writes still emit inline.
|
|
817
|
+
|
|
818
|
+
**The remedy is to bound the column:**
|
|
819
|
+
|
|
820
|
+
```ts
|
|
821
|
+
tokenHash: text().maxLength(64).unique() // VARCHAR(64) → ordinary B-tree index
|
|
822
|
+
```
|
|
823
|
+
|
|
824
|
+
`ALTER TABLE … FORCE` does **not** help. The rebuild recreates the index and therefore recreates the hidden column — measured before and after: same column count both times. If you already tried it, that was not your mistake.
|
|
825
|
+
|
|
826
|
+
A bounded unique is worth having anyway: it is also what keeps the key inside the index-size limits on every dialect.
|
|
827
|
+
|
|
828
|
+
The other cause of the same message is a **backlog event that predates a migration** — the reader was down while a table changed. That one is unreplayable but transient: the reader skips to the current binlog end, signals a resync, and recovers. One warning, then it is over.
|
|
829
|
+
|
|
805
830
|
### Boot summary
|
|
806
831
|
|
|
807
832
|
```
|
|
@@ -217,6 +217,21 @@ Common uses:
|
|
|
217
217
|
|
|
218
218
|
Returns the FINAL row in both cases (newly-inserted or pre-existing).
|
|
219
219
|
|
|
220
|
+
### One conflict target, and only conflicts
|
|
221
|
+
|
|
222
|
+
`conflictColumns` names ONE constraint. A duplicate on a *different* unique index is not something `insertIgnore` can resolve — it cannot know which existing row you meant — so it throws, naming the constraint that actually fired.
|
|
223
|
+
|
|
224
|
+
It also throws when the insert was **rejected** rather than skipped. This matters most on MariaDB, where the statement lowers to `INSERT IGNORE`: that downgrades *every* error to a warning — foreign key, NOT NULL, CHECK, truncation — not just the unique violation the API models. So "no row was inserted" does not imply "a conflict happened", and reporting one as the other would turn a rejected write into a silent no-op: the row is not there and the caller is told it already was.
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
MysqlStore.insertIgnore: the insert into 'docs' was REJECTED, not skipped as a
|
|
228
|
+
conflict. INSERT IGNORE downgrades every error to a warning, and the warning was:
|
|
229
|
+
[1452] Cannot add or update a child row: a foreign key constraint fails …
|
|
230
|
+
Nothing was written and nothing conflicted — fix the cause above.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The real cause comes from `SHOW WARNINGS` on the same connection, which is only attributable inside a transaction — so outside one the message says the constraint is unknown rather than guessing at it. Framework mutations are auto-transactional, so the common path has the cause.
|
|
234
|
+
|
|
220
235
|
## `updateMany` — one-statement bulk update
|
|
221
236
|
|
|
222
237
|
```ts
|
|
@@ -258,6 +258,19 @@ No naming trick is needed to keep something out of the router: the absence of th
|
|
|
258
258
|
|
|
259
259
|
> Before this convention, every `.tsx` under `src/pages/` became a route, so a co-located component silently got a URL. That route rendered nothing and nobody visited it in dev; the failure surfaced at the first production build. If you are upgrading, `voltro update` renames your pages for you.
|
|
260
260
|
|
|
261
|
+
## Two routes, one screen
|
|
262
|
+
|
|
263
|
+
When two URLs must render the same component — a versioned path kept alive because devices in the field are configured against it, say — re-export it instead of copying it:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
// src/pages/v2/page.tsx → /v2, rendering exactly what / renders
|
|
267
|
+
export { default, renderMode } from '../page'
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`export { default } from '…'` satisfies the page contract: the module has a default export, it just did not declare it here. Forwarding `renderMode` alongside it is what keeps the two routes from drifting apart — the build follows the forward when it computes the render profile, so `/v2` is classified the same as `/`, not silently as `static`.
|
|
271
|
+
|
|
272
|
+
`export { default as Screen } from '../page'` is the opposite: it renames the default away, leaving this module without one. That still fails the contract.
|
|
273
|
+
|
|
261
274
|
## Trailing slashes
|
|
262
275
|
|
|
263
276
|
The canonical form is **no trailing slash** — always link with `<Link to="/about">`, not `<Link to="/about/">`.
|