docs: specs for apis blocks, migration, and editor tooling
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>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user