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>
141 lines
6.8 KiB
Markdown
141 lines
6.8 KiB
Markdown
# 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 when `from < version <= to`.
|
|
- `MigrationCtx` carries `appRoot`, `from`, `to`, **`dryRun`**, a report, and a logger.
|
|
- `MigrationReport` already separates `changedAutomatically`, `needsReview`, `parseFailures`,
|
|
`ambiguousFunctions`, and `unresolvedImports`.
|
|
- `update.ts` already imports `parse` and `formatWrn` from `@wrnexus/syntax`, so parsing a `.wrn`
|
|
file, 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:
|
|
|
|
1. Parse the file. A parse failure records the path in `parseFailures` and moves on — the file is
|
|
never partially rewritten.
|
|
2. Transform, then re-emit through `formatWrn`.
|
|
3. If any part of a file's transform cannot be completed, **the whole file is skipped** and recorded
|
|
in `needsReview` with 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:
|
|
|
|
```wrn
|
|
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.
|
|
- **`api` relocation** — a page with both `ssr` and `client` api blocks lands in one `apis { }`;
|
|
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 `shared` modifier; a name collision skips and reports.
|
|
- **Legacy bare body** — reported in `needsReview` with 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 `.wrn` is recorded in `parseFailures` and left untouched.
|
|
- **Exit code** — non-zero when anything needs review.
|
|
- **End to end** — `examples/basic-app` migrated 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.
|