Three specs completing the set, each depending on the one before it:
- apis {}: one container, mode-less declarations, api.<name>() callable
anywhere with build-time dispatch, AsyncLocalStorage for server context,
usage-driven emission, three render-binding forms. Replaces the ssr {} /
client {} data blocks and the untypeable with($data) legacy body.
- update: migrations to the new syntax. The legacy bare-body rewrite is
deliberately manual -- which free identifiers are payload fields is not
knowable from the source, so an automatic guess would compile and be wrong.
- editor tooling: grammar, completions for api. and the api= attribute,
diagnostics for removed constructs, and resolving by observation whether
generated type errors surface inline.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6.8 KiB
Carrying projects to the new syntax with wrnexus update — Design
Date: 2026-08-19
Status: Approved for implementation
Scope: Migrations that take an existing project from today's syntax to the syntax left by the
cleanup and apis { } specs.
Depends on: 2026-08-19-legacy-and-config-cleanup-design.md and
2026-08-19-apis-block-design.md. Both define the target this migrates to, so both must land first.
Goal
After the two preceding specs, every existing project is written in a syntax the framework no longer
accepts. One wrnexus update should carry a project across — config keys removed, api blocks
moved into apis { }, mode-scoped helpers relocated — and, where it cannot do that safely, say so
precisely instead of guessing.
What already exists
This is an extension of working machinery, not a new subsystem:
Migration { version, id, description, apply(ctx) }, run whenfrom < version <= to.MigrationCtxcarriesappRoot,from,to,dryRun, a report, and a logger.MigrationReportalready separateschangedAutomatically,needsReview,parseFailures,ambiguousFunctions, andunresolvedImports.update.tsalready importsparseandformatWrnfrom@wrnexus/syntax, so parsing a.wrnfile, transforming it, and re-emitting formatted source is an established pattern here.
The report's shape matters: it was built around the idea that some changes are safe to make and others must be handed back to a human. That distinction is the backbone of this spec.
Non-goals
- Migrating projects below
0.8.0. The cleanup spec removes those migrations; no such project exists. - Rewriting application logic. Only the constructs these specs changed.
The safety contract
A file is transformed correctly, or it is left untouched and reported. There is no third outcome. Concretely:
- Parse the file. A parse failure records the path in
parseFailuresand moves on — the file is never partially rewritten. - Transform, then re-emit through
formatWrn. - If any part of a file's transform cannot be completed, the whole file is skipped and recorded
in
needsReviewwith the reason and the construct involved.
Running the migration twice must be a no-op: every transform detects already-migrated input and does nothing. Dry-run must report exactly what a real run would change.
The migrations
1. Remove the dead config keys
Delete compatibility, functions, compatibilityDate, and frameworkBehaviour from
wrnexus.config.ts. Mechanical, fully automatic, and safe because none of them was ever read.
2. ssr { api … } / client { api … } → apis { … }
Move each api entry into a page-level apis { } block, dropping the mode. Sectioned bodies —
those already using request / response / error — carry across unchanged, because the payload
is already bound to data.
Fully automatic. If a page has entries in both an ssr and a client block sharing a name, that is
a duplicate under the new rules and the file is skipped and reported, since choosing which one
survives is a decision about intent.
3. ssr { functions { … } } → functions { shared function … }
Relocate mode-scoped helpers to the page-level functions { } block with the shared modifier.
Automatic. If the page already has a function of the same name, the file is skipped and reported.
4. Legacy bare-body api blocks — needs review, not automatic
This is the one transform that cannot be done safely, and the spec is explicit about it rather than attempting a best effort.
A legacy bare body is evaluated inside with ($data ?? {}), so it references payload fields as bare
identifiers:
api ssrUsers GET /api/users/ssr {
return userNames(users)
}
The sectioned form binds the payload to data, so this must become data.users. But which free
identifiers are payload fields is not knowable from the source. In the example, users comes from
the response and userNames is a page helper — and nothing in the file distinguishes them. The
response shape belongs to the route, and the route may not even be typed.
A migration that guessed would produce code that compiles and is wrong: data.userNames(...) or an
untouched users that silently resolves to undefined. That is precisely the silent-wrong-answer
failure this project keeps paying for.
So: legacy bare-body blocks are detected, reported in needsReview with the file, the block name,
and the free identifiers found, and left untouched. The report tells the author exactly what to
decide. wrnexus update prints a short explanation of why this one is manual.
5. Deprecated @wrnexus/auth options
Only if the cleanup spec's optional auth section is included. Rename call sites of the superseded options. Automatic where the rename is unambiguous; reported otherwise.
Version
All of these attach to the release that ships the breaking change. After the cleanup spec the
migration floor is 0.8.0, so the list is short and every entry is reachable.
Output
At the end of a run the command prints, in this order: what it changed, what needs review and why,
and what failed to parse. A run with anything in needsReview or parseFailures exits non-zero, so
a scripted upgrade cannot appear to succeed while leaving a project half-migrated.
Testing
Each migration gets a fixture project and three assertions: the transform produces the expected source, running it a second time changes nothing, and a dry run reports the same set without writing.
- Config removal — keys gone, rest of the config untouched.
apirelocation — a page with bothssrandclientapi blocks lands in oneapis { }; entries keep their names, methods, paths, and sections.- Name collision across modes — the file is skipped and reported, not silently merged.
- Mode functions — relocated with the
sharedmodifier; a name collision skips and reports. - Legacy bare body — reported in
needsReviewwith the block name and free identifiers, and the file is byte-identical afterwards. This is the most important test in the spec: it pins that the migration does not attempt the rewrite. - Parse failure — a malformed
.wrnis recorded inparseFailuresand left untouched. - Exit code — non-zero when anything needs review.
- End to end —
examples/basic-appmigrated by the command alone, then built and tested. If the framework's own example cannot be migrated by the tool, the tool is not finished.
What this does not promise
Automated source rewriting cannot be promised as "perfect". What is promised is bounded: every file is either correctly transformed or untouched and named in the report, with the reason. Nothing is half-rewritten, and nothing is guessed. The legacy bare-body case is deliberately manual because a correct automatic answer does not exist.