docs: four implementation plans for the cleanup, apis block, migration, and editor work
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,400 @@
|
|||||||
|
# Editor Tooling Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** The language server and VS Code extension understand `apis { }`, complete `api.<name>()`, flag the removed constructs, and stop offering syntax the compiler rejects.
|
||||||
|
|
||||||
|
**Architecture:** Four surfaces change independently — the TextMate grammar, the keyword and snippet completions, the server's call completion and hover, and diagnostics for removed constructs. A final task answers by observation whether generated type errors surface inside `.wrn`, which has never been verified.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun, TypeScript, `bun:test`, `node --test`, LSP, TextMate grammars.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-19-editor-tooling-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- `client` is one word with several jobs: `client state { }`, `runtime = "client"`, and the `client function` modifier all survive. **Only the `client { }` / `ssr { }` data-block patterns are removed.** Blanket removal would un-highlight constructs that still exist.
|
||||||
|
- Offering a construct the compiler rejects is worse than offering nothing.
|
||||||
|
- **Nothing may claim inline diagnostics work until someone has seen them work.**
|
||||||
|
- The editor bundles embed the compiler and language server — rebuild with `bun run --cwd editors/vscode build`, or the `check:editor-*` gates fail on a stale bundle.
|
||||||
|
- `bun run format` before every commit; the gate is `bun run check:production`.
|
||||||
|
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Grammar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `editors/vscode/syntaxes/wrn.tmLanguage.json`
|
||||||
|
- Test: `editors/vscode/test/grammar-apis.test.js`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `apis` highlights as a block keyword; entries highlight as declarations.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `editors/vscode/test/grammar-apis.test.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const { readFileSync } = require("node:fs");
|
||||||
|
const { join } = require("node:path");
|
||||||
|
|
||||||
|
const grammar = readFileSync(join(__dirname, "../syntaxes/wrn.tmLanguage.json"), "utf8");
|
||||||
|
|
||||||
|
test("the grammar knows the apis block", () => {
|
||||||
|
assert.ok(grammar.includes("apis"), "apis should appear as a block keyword");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("client keeps its highlighting where it is still valid", () => {
|
||||||
|
// client state {}, runtime = "client", and client function all survive.
|
||||||
|
// Only the client {} data block was removed.
|
||||||
|
assert.ok(grammar.includes("client"), "client must still be matched");
|
||||||
|
assert.ok(grammar.includes("shared"), "the shared function modifier must still be matched");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `node --test editors/vscode/test/grammar-apis.test.js`
|
||||||
|
Expected: FAIL on the first assertion — `apis` is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add `apis`, remove only the data-block patterns**
|
||||||
|
|
||||||
|
Add `apis` to the block-keyword pattern alongside `functions`. Then find the patterns matching `ssr`/`client` as **data blocks** and remove only those. Leave every rule that matches `client` in `client state`, in `runtime` values, and as a function modifier.
|
||||||
|
|
||||||
|
Add a pattern for an entry — `<name> <METHOD> <path>` — so a declaration reads as a declaration.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the test and check by eye**
|
||||||
|
|
||||||
|
Run: `node --test editors/vscode/test/grammar-apis.test.js`
|
||||||
|
Expected: PASS. Then open a `.wrn` file using `apis { }`, `client state { }`, `functions { shared function }`, and `runtime = "client"` in VS Code and confirm each still colours correctly. Record what you saw.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add editors/vscode
|
||||||
|
git commit -m "feat(editor): highlight the apis block"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Keyword and snippet completion
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/index.ts` (`WRN_KEYWORDS`, ~line 31)
|
||||||
|
- Modify: `editors/vscode/src/completion.js` (block snippets)
|
||||||
|
- Test: `packages/language-server/test/apis-completion.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: `apis` is a known keyword; `ssr` / `client` data-block snippets are gone.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/apis-completion.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { WRN_KEYWORDS } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("apis is a known page-level keyword", () => {
|
||||||
|
expect(WRN_KEYWORDS).toContain("apis");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/apis-completion.test.ts`
|
||||||
|
Expected: FAIL — `apis` is missing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the keyword and the snippets**
|
||||||
|
|
||||||
|
Add `"apis"` to `WRN_KEYWORDS`. In `editors/vscode/src/completion.js`, add a container snippet and an entry snippet including the `request` / `response` / `error` sections, and remove any `ssr {` / `client {` data-block snippet.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server && bun run --cwd editors/vscode test`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server editors/vscode
|
||||||
|
git commit -m "feat(editor): complete the apis block and drop the removed snippets"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: `api.` call completion and hover
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/server.ts` (completion and hover handlers)
|
||||||
|
- Test: `packages/language-server/test/api-call-completion.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `ast.dataApis` entries, which carry `name`, `method`, `path`, and `sections`.
|
||||||
|
- Produces: completion items for `api.` and hover detail for a block name.
|
||||||
|
|
||||||
|
This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor should know it.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/api-call-completion.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { apiCallCompletions, apiCallHover } from "../src/server.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Search {
|
||||||
|
apis {
|
||||||
|
searchUsers POST /api/users {
|
||||||
|
request { body { name?: string } }
|
||||||
|
response { return data.users }
|
||||||
|
}
|
||||||
|
|
||||||
|
listTeams GET /api/teams {
|
||||||
|
response { return data.teams }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
functions {
|
||||||
|
client async function go(): Promise<void> {
|
||||||
|
await api.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("api. offers every declared block with method and path", () => {
|
||||||
|
const items = apiCallCompletions(SOURCE);
|
||||||
|
const labels = items.map((item) => item.label);
|
||||||
|
|
||||||
|
expect(labels).toContain("searchUsers");
|
||||||
|
expect(labels).toContain("listTeams");
|
||||||
|
|
||||||
|
const search = items.find((item) => item.label === "searchUsers")!;
|
||||||
|
expect(search.detail).toContain("POST");
|
||||||
|
expect(search.detail).toContain("/api/users");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("hovering a block name reports its method, path and request fields", () => {
|
||||||
|
const hover = apiCallHover(SOURCE, "searchUsers");
|
||||||
|
|
||||||
|
expect(hover).toContain("POST");
|
||||||
|
expect(hover).toContain("/api/users");
|
||||||
|
expect(hover).toContain("name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a page with no apis block offers nothing", () => {
|
||||||
|
expect(apiCallCompletions(`page P { view { <main>x</main> } }`)).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/api-call-completion.test.ts`
|
||||||
|
Expected: FAIL — the functions do not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement and export both functions**
|
||||||
|
|
||||||
|
Add `apiCallCompletions(source: string)` and `apiCallHover(source: string, name: string)` to `packages/language-server/src/server.ts`, parsing with `@wrnexus/syntax` and reading `ast.dataApis`. Wire `apiCallCompletions` into the `textDocument/completion` handler for positions immediately after `api.`, and `apiCallHover` into `textDocument/hover`.
|
||||||
|
|
||||||
|
The parser must tolerate the half-typed `await api.` in the fixture. If it throws, fall back to returning `[]` rather than failing the request — completion fires while the document does not parse, which is the normal case.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server
|
||||||
|
git commit -m "feat(language-server): complete and describe api block calls"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: The `api=` attribute in markup
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/html-service.ts`
|
||||||
|
- Test: `packages/language-server/test/api-attribute.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: `apiCallCompletions` from Task 3.
|
||||||
|
- Produces: `api="…"` is not flagged as unknown, and completion inside the quotes offers block names.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/api-attribute.test.ts` asserting that (a) an `api="searchUsers"` attribute produces no unknown-attribute diagnostic, and (b) completion inside the quotes offers `searchUsers`. Reuse the fixture shape from Task 3.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/api-attribute.test.ts`
|
||||||
|
Expected: FAIL.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Teach the HTML service about the attribute**
|
||||||
|
|
||||||
|
Treat `api` as a known attribute on any element, and route completion inside its quotes to `apiCallCompletions`. The value is a call expression, not text — it must not be spell-checked or reformatted as prose.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test packages/language-server
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server
|
||||||
|
git commit -m "feat(language-server): understand the api binding attribute"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Diagnostics for the removed constructs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/language-server/src/diagnostics` entry point (wherever `.wrn` diagnostics are produced)
|
||||||
|
- Test: `packages/language-server/test/removed-construct-diagnostics.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: an `ssr { api … }` or `client { api … }` block yields a diagnostic naming `apis { }`, positioned on the block keyword.
|
||||||
|
|
||||||
|
The compiler already rejects these. The editor should say so while typing, and say what to do instead.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/language-server/test/removed-construct-diagnostics.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { diagnoseWrn } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("an ssr data block is flagged and names the replacement", () => {
|
||||||
|
const diagnostics = diagnoseWrn(`page P {
|
||||||
|
ssr { api x GET /api/x { response { return data } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(diagnostics.length).toBeGreaterThan(0);
|
||||||
|
expect(diagnostics[0]!.message).toContain("apis");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("client state is not flagged", () => {
|
||||||
|
const diagnostics = diagnoseWrn(`page P {
|
||||||
|
client state { count = 0 }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use whichever diagnostic entry point the language server exports; keep the assertions identical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/language-server/test/removed-construct-diagnostics.test.ts`
|
||||||
|
Expected: FAIL — either no diagnostic, or one that does not name `apis`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Surface the parse error as a diagnostic**
|
||||||
|
|
||||||
|
The parser already throws a message naming `apis { }` for these blocks. Ensure that message reaches the diagnostic with a position on the offending keyword rather than at offset zero.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun test packages/language-server
|
||||||
|
bun run format
|
||||||
|
git add packages/language-server
|
||||||
|
git commit -m "feat(language-server): flag the removed data blocks"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Answer the inline-diagnostics question, then the full gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: whatever the observation in Step 2 shows is needed, or none
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: Tasks 1-5.
|
||||||
|
|
||||||
|
The `apis` plan generates type assertions that make `tsc` fail when a block declares a field its endpoint rejects. **Whether that failure appears inside the `.wrn` file has never been confirmed** — it was inferred from reading source. This task settles it by looking.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rebuild the bundles**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Observe, and write down what you see**
|
||||||
|
|
||||||
|
In `examples/basic-app`, add a field to an `apis { }` entry that its endpoint does not accept, and run `bun run --cwd examples/basic-app wrnexus generate types`. Open the page in VS Code and record exactly where the error appears: on the block, only in `app/types/wrnexus.generated.api-checks.ts`, or nowhere.
|
||||||
|
|
||||||
|
Write the answer into the task report. **Do not skip this step and reason about it instead** — that is what left the question open the first time.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Act on what you observed**
|
||||||
|
|
||||||
|
If the error already surfaces usefully on the block, document it and stop.
|
||||||
|
|
||||||
|
If it appears only in the generated file, map the diagnostic back: the generator knows which page and block produced each assertion, so record that mapping when emitting and use it to relocate the diagnostic.
|
||||||
|
|
||||||
|
If that mapping proves larger than this task can hold, **stop and report it as follow-up work** rather than half-building it. Say so plainly in the report.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove the temporary field**
|
||||||
|
|
||||||
|
Revert the deliberate error and confirm `bun run typecheck` passes with zero net diff in `examples/basic-app`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Full gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun test
|
||||||
|
bun run typecheck
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
bun run --cwd editors/vscode test
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: exit 0 throughout, including `check:editor-compiler`, `check:editor-language-server`, and `check:editor-extension`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Manual pass, recorded**
|
||||||
|
|
||||||
|
Open the migrated `examples/basic-app` in VS Code and confirm: `apis { }` highlights, `api.` completes with the page's block names, hovering a name shows its method and path, and an `ssr { api … }` block is flagged. Record what you saw in the report — including anything that did not work.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat(editor): complete tooling support for the apis block"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the executor
|
||||||
|
|
||||||
|
- **`client` is not one thing.** Removing every `client` rule from the grammar would break `client state`, `runtime = "client"`, and `client function`. Only the data-block patterns go.
|
||||||
|
- **The parser must tolerate half-typed input.** Completion fires while the document does not parse; a thrown error must become an empty completion list, not a failed request.
|
||||||
|
- **Step 2 of Task 6 is an observation, not a deduction.** Open the editor and look.
|
||||||
|
- **If a test would still pass with the code it guards deleted, it is not a test.**
|
||||||
@@ -0,0 +1,537 @@
|
|||||||
|
# Legacy and Config Cleanup Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Delete the compatibility-flag surface, the `"legacy"` function runtime, dead migrations, and deprecated APIs before the framework's first public release.
|
||||||
|
|
||||||
|
**Architecture:** Almost all of this is deletion. Seven config keys are never read by any code, so removing them changes nothing. The one behaviour-sensitive item is the `"legacy"` function runtime, which is mapped to `"shared"` — an equivalent substitution, because an unmarked function is already emitted into both bundles.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun, TypeScript, `bun:test`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-19-legacy-and-config-cleanup-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- This plan removes configuration. It adds none.
|
||||||
|
- The legacy `api` block forms (bare-body `with ($data)`, and `api` inside `ssr {}` / `client {}`) are **out of scope** — they are replaced by the next plan, not deleted here.
|
||||||
|
- A removed config key must be **rejected loudly**, not silently ignored. Someone with a stale config must be told, not left believing a flag still applies.
|
||||||
|
- `bun run format` before every commit; the repo gate is `bun run check:production`.
|
||||||
|
- The editor bundles embed the compiler — rebuild with `bun run --cwd editors/vscode build` after any `packages/syntax` or `packages/compiler` change, or `check:editor-compiler` fails on a stale bundle.
|
||||||
|
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files; escaping mangles them silently. Use file editing tools.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Replace the `"legacy"` function runtime with `"shared"`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/syntax/src/v060.ts` (the `FunctionRuntime` type; the default at ~line 209)
|
||||||
|
- Modify: `packages/compiler/src/client-codegen.ts` (membership tests at ~lines 173 and 340)
|
||||||
|
- Modify: `packages/compiler/src/server-codegen.ts` (the `["legacy", "server", "shared"]` list)
|
||||||
|
- Modify: `packages/compiler/src/codegen.ts` (`targetFunctions`, ~line 1310)
|
||||||
|
- Test: `packages/compiler/test/legacy-runtime-removal.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `FunctionRuntime` becomes `"client" | "server" | "shared"`. Later tasks and plans rely on `"legacy"` no longer existing.
|
||||||
|
|
||||||
|
**Why this is equivalent, not a behaviour change:** an unmarked `function foo()` currently parses as `"legacy"`, and both codegens include `"legacy"` in their membership tests — `["legacy", "client", "shared"]` for the browser and `["legacy", "server", "shared"]` for the server. So an unmarked function is already emitted into _both_ bundles, exactly like `shared`. `legacyDefaultRuntime` looks like it should modulate this but is never read.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/compiler/test/legacy-runtime-removal.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { parse } from "@wrnexus/syntax";
|
||||||
|
import { generateTargets } from "../src/targets.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Probe {
|
||||||
|
functions {
|
||||||
|
function unmarkedHelper() {
|
||||||
|
return "both";
|
||||||
|
}
|
||||||
|
|
||||||
|
client function clientOnly() {
|
||||||
|
return "browser";
|
||||||
|
}
|
||||||
|
|
||||||
|
server function serverOnly() {
|
||||||
|
return "server";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("an unmarked function is emitted into both the browser and server modules", () => {
|
||||||
|
// This is the property the "legacy" runtime provided. Removing the variant
|
||||||
|
// must not change it.
|
||||||
|
const targets = generateTargets(parse(SOURCE));
|
||||||
|
|
||||||
|
expect(targets.browser).toContain("unmarkedHelper");
|
||||||
|
expect(targets.server).toContain("unmarkedHelper");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("marked functions still go only where they belong", () => {
|
||||||
|
const targets = generateTargets(parse(SOURCE));
|
||||||
|
|
||||||
|
expect(targets.browser).toContain("clientOnly");
|
||||||
|
expect(targets.browser).not.toContain("serverOnly");
|
||||||
|
expect(targets.server).toContain("serverOnly");
|
||||||
|
expect(targets.server).not.toContain("clientOnly");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no emitted target mentions the removed legacy runtime", () => {
|
||||||
|
const targets = generateTargets(parse(SOURCE));
|
||||||
|
|
||||||
|
expect(targets.browser).not.toContain('"legacy"');
|
||||||
|
expect(targets.server).not.toContain('"legacy"');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test and record the baseline**
|
||||||
|
|
||||||
|
Run: `bun test packages/compiler/test/legacy-runtime-removal.test.ts`
|
||||||
|
Expected: the first two tests PASS (they describe current behaviour and must keep passing), the third may already pass. This test file is a **regression guard written before the change**, so a green run here is correct — record the output.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Remove the `"legacy"` variant from the type and parser**
|
||||||
|
|
||||||
|
In `packages/syntax/src/v060.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type FunctionRuntime = "client" | "server" | "shared";
|
||||||
|
```
|
||||||
|
|
||||||
|
And at the parse site (~line 209), change the default:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
let runtime: FunctionRuntime = "shared";
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Drop `"legacy"` from every membership test**
|
||||||
|
|
||||||
|
In `packages/compiler/src/client-codegen.ts`, both occurrences:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
["client", "shared"].includes(fn.runtime),
|
||||||
|
```
|
||||||
|
|
||||||
|
In `packages/compiler/src/server-codegen.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const names = ast.runtimeFunctions
|
||||||
|
.filter((fn) => ["server", "shared"].includes(fn.runtime))
|
||||||
|
.map((fn) => fn.name);
|
||||||
|
```
|
||||||
|
|
||||||
|
In `packages/compiler/src/codegen.ts`, `targetFunctions`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const runtimes =
|
||||||
|
target === "browser" ? (["client", "shared"] as const) : (["server", "shared"] as const);
|
||||||
|
```
|
||||||
|
|
||||||
|
Search the repo for any remaining `"legacy"` in these packages and remove each — the string must not survive in `packages/syntax` or `packages/compiler`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/syntax packages/compiler`
|
||||||
|
Expected: PASS, including the three guards from Step 1. If the first two now fail, the substitution was not equivalent — stop and report rather than adjusting the test.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Rebuild the editor bundles and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
git add packages/syntax packages/compiler editors/vscode/src
|
||||||
|
git commit -m "refactor: replace the legacy function runtime with shared"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Delete the compatibility surface
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Delete: `packages/styles/src/compatibility.ts`
|
||||||
|
- Delete: `packages/cli/src/compatibility-command.ts`
|
||||||
|
- Delete: `packages/cli/test/compatibility.test.ts`
|
||||||
|
- Modify: `packages/styles/src/config.ts` (`FunctionsConfig` ~233, `CompatibilityConfig` ~242, `AppConfig extends CompatibilityPolicy` ~249, the `functions?:` and `compatibility?:` members, the `resolveCompatibility` validation ~619, and the `CompatibilityPolicy` import ~23)
|
||||||
|
- Modify: `packages/styles/src/index.ts` (the `./compatibility.ts` exports at ~lines 39-45)
|
||||||
|
- Modify: `packages/cli/src/index.ts` (dispatch at ~line 295, help text at ~line 75)
|
||||||
|
- Modify: `packages/cli/src/create.ts` (~lines 263, 278-283)
|
||||||
|
- Modify: `packages/cli/src/update.ts` (the config insertion string at ~line 389)
|
||||||
|
- Modify: `packages/styles/test/config.test.ts` (assertions on the removed keys)
|
||||||
|
- Modify: `examples/basic-app/wrnexus.config.ts`
|
||||||
|
- Test: `packages/styles/test/removed-config-keys.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing from Task 1.
|
||||||
|
- Produces: `AppConfig` no longer extends `CompatibilityPolicy` and has no `compatibility` or `functions` members. `@wrnexus/styles` no longer exports `resolveCompatibility`, `isCompatibilityDate`, `CURRENT_COMPATIBILITY_DATE`, `CURRENT_FRAMEWORK_BEHAVIOUR`, `CompatibilityPolicy`, or `CompatibilityReport`.
|
||||||
|
|
||||||
|
**These seven keys are never read.** `legacyEmit`, `legacyEventProps`, `legacyComponentDiscovery`, `stringLayouts`, and `legacyDefaultRuntime` appear only in the type declaration, `create.ts`, and `update.ts`. `compatibilityDate` and `frameworkBehaviour` feed only a printed report and one validation. Removing them changes no behaviour.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/styles/test/removed-config-keys.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { validateConfig } from "../src/config.ts";
|
||||||
|
|
||||||
|
// A stale config must fail loudly. Silently ignoring a removed key leaves
|
||||||
|
// someone believing a flag still applies.
|
||||||
|
const REMOVED = [
|
||||||
|
{ key: "compatibilityDate", config: { compatibilityDate: "2026-08-02" } },
|
||||||
|
{ key: "frameworkBehaviour", config: { frameworkBehaviour: 1 } },
|
||||||
|
{ key: "functions", config: { functions: { legacyDefaultRuntime: "current" } } },
|
||||||
|
{ key: "compatibility", config: { compatibility: { legacyEmit: false } } },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { key, config } of REMOVED) {
|
||||||
|
test(`a config still setting "${key}" is rejected with a message naming it`, () => {
|
||||||
|
const issues = validateConfig(config as never);
|
||||||
|
const match = issues.find((issue) => issue.path === key || issue.path.startsWith(`${key}.`));
|
||||||
|
|
||||||
|
expect(match).toBeDefined();
|
||||||
|
expect(match!.severity).toBe("error");
|
||||||
|
expect(match!.message.toLowerCase()).toContain("removed");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a config without those keys is accepted", () => {
|
||||||
|
const issues = validateConfig({} as never);
|
||||||
|
|
||||||
|
expect(issues.filter((issue) => issue.severity === "error")).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
If `validateConfig` is not the exported name in `packages/styles/src/config.ts`, use whichever function that module exports for validation and keep the assertions identical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/styles/test/removed-config-keys.test.ts`
|
||||||
|
Expected: FAIL — the keys are currently accepted, so no issue is produced.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the compatibility module and its command**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm packages/styles/src/compatibility.ts packages/cli/src/compatibility-command.ts packages/cli/test/compatibility.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
In `packages/styles/src/index.ts`, remove the whole `./compatibility.ts` export block (both the value exports and the `export type` line).
|
||||||
|
|
||||||
|
In `packages/cli/src/index.ts`, remove the `case "compatibility":` dispatch and the `wrnexus compatibility …` line from the help text.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Remove the config members and add the rejections**
|
||||||
|
|
||||||
|
In `packages/styles/src/config.ts`: delete the `CompatibilityPolicy` import, the `FunctionsConfig` and `CompatibilityConfig` interfaces, the `functions?:` and `compatibility?:` members of `AppConfig`, `extends CompatibilityPolicy` on `AppConfig`, and the `resolveCompatibility` validation block.
|
||||||
|
|
||||||
|
Then add the rejections so a stale config fails loudly:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const REMOVED_CONFIG_KEYS = [
|
||||||
|
"compatibilityDate",
|
||||||
|
"frameworkBehaviour",
|
||||||
|
"functions",
|
||||||
|
"compatibility",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const key of REMOVED_CONFIG_KEYS) {
|
||||||
|
if ((config as Record<string, unknown>)[key] !== undefined) {
|
||||||
|
issues.push({
|
||||||
|
path: key,
|
||||||
|
severity: "error",
|
||||||
|
message: "was removed; delete it from the configuration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Place this beside the other validation pushes, using whatever local variable that function accumulates issues in.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Stop scaffolding and inserting the keys**
|
||||||
|
|
||||||
|
In `packages/cli/src/create.ts`, delete the `compatibilityDate`, `frameworkBehaviour`, and `functions: { legacyDefaultRuntime: … }` lines from the generated config.
|
||||||
|
|
||||||
|
In `packages/cli/src/update.ts` (~line 389), remove `functions: { legacyDefaultRuntime: "current" },` and the whole `compatibility: { … },` fragment from the insertion string.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Trim the example app config**
|
||||||
|
|
||||||
|
In `examples/basic-app/wrnexus.config.ts`, delete `compatibilityDate`, `frameworkBehaviour`, `functions`, and `compatibility`.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Update the existing config tests**
|
||||||
|
|
||||||
|
`packages/styles/test/config.test.ts` asserts on the removed keys. Remove those assertions. Do not weaken any assertion that is still meaningful — if a test only existed to cover compatibility, delete the whole test.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/styles packages/cli`
|
||||||
|
Expected: PASS, including the new rejection tests.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add -A packages/styles packages/cli examples/basic-app
|
||||||
|
git commit -m "refactor: delete the compatibility config surface"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Drop migrations below 0.8.0
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/cli/src/update.ts` (all `Migration` entries with `version` below `"0.8.0"`)
|
||||||
|
- Test: `packages/cli/test/update-migration-floor.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: the migration list starts at `0.8.0`.
|
||||||
|
|
||||||
|
`update.ts` holds 111 migrations reaching back to `0.2.8`. The framework is pre-public and the only projects run `0.8.x`, so everything below the floor is unreachable.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/update-migration-floor.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
test("no migration targets a version below 0.8.0", () => {
|
||||||
|
const source = readFileSync(join(import.meta.dir, "../src/update.ts"), "utf8");
|
||||||
|
const versions = [...source.matchAll(/version:\s*"([0-9.]+)"/g)].map((match) => match[1]!);
|
||||||
|
|
||||||
|
expect(versions.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const belowFloor = versions.filter((version) => {
|
||||||
|
const [major, minor] = version.split(".").map(Number);
|
||||||
|
return major! === 0 && minor! < 8;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(belowFloor).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/update-migration-floor.test.ts`
|
||||||
|
Expected: FAIL, listing the `0.2.x`–`0.7.x` versions.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the migrations below the floor**
|
||||||
|
|
||||||
|
Remove every `Migration` object whose `version` is below `"0.8.0"`, along with any helper function that becomes unused as a result. Keep every `0.8.x` entry.
|
||||||
|
|
||||||
|
After deleting, search for now-unreferenced helpers in the file and remove them too — an unused private helper is dead code, and the linter will flag it.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS. Existing update tests that exercised removed migrations should be deleted with them; do not keep a test that asserts nothing.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify `update` still runs end to end**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd examples/basic-app wrnexus update --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: completes without error and reports no pending migrations for an app already at the current version. Paste the output into the commit body if it is short.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "chore: drop update migrations below 0.8.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Remove the deprecated compiler re-export shims
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/compiler/src/codegen.ts` (~line 20, the `./parser.ts` import)
|
||||||
|
- Modify: `packages/compiler/src/native-codegen.ts` (~line 1, the `./parser.ts` import)
|
||||||
|
- Delete: `packages/compiler/src/parser.ts`, `packages/compiler/src/tokenizer.ts`, `packages/compiler/src/types.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: nothing new; imports move to `@wrnexus/syntax`.
|
||||||
|
|
||||||
|
**Order matters.** These three files are two-line re-exports marked deprecated, but `codegen.ts` and `native-codegen.ts` still import from them. Deleting the files first breaks the build.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Repoint the imports**
|
||||||
|
|
||||||
|
In `packages/compiler/src/codegen.ts`, change:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
|
||||||
|
```
|
||||||
|
|
||||||
|
to import the same names from `@wrnexus/syntax`. If the file already imports from `@wrnexus/syntax`, merge them into that one import rather than adding a second.
|
||||||
|
|
||||||
|
In `packages/compiler/src/native-codegen.ts`, change:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Attr, PageAst, ViewNode } from "./parser.ts";
|
||||||
|
```
|
||||||
|
|
||||||
|
the same way.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify nothing else imports the shims**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "from \"./parser.ts\"\|from \"./tokenizer.ts\"\|from \"./types.ts\"" packages/compiler/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output. If anything remains, repoint it before continuing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Delete the shims**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git rm packages/compiler/src/parser.ts packages/compiler/src/tokenizer.ts packages/compiler/src/types.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/compiler && bun run typecheck`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Rebuild the editor bundles and commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
git add -A packages/compiler editors/vscode/src
|
||||||
|
git commit -m "refactor: drop the deprecated compiler re-export shims"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Remove the deprecated `@wrnexus/auth` options
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/auth/src/http/index.ts` (~lines 56-59)
|
||||||
|
- Modify: `packages/auth/src/plugin.ts` (~lines 55-58)
|
||||||
|
- Modify: `packages/auth/src/types.ts` (~line 423)
|
||||||
|
- Modify: `packages/auth/src/engine.ts` (~lines 163-165 and 179-181)
|
||||||
|
- Modify: `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, `packages/auth/test/engine.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: nothing.
|
||||||
|
- Produces: nothing new. Options are removed, not renamed.
|
||||||
|
|
||||||
|
**These are our own superseded options, not an out-of-date dependency.** The current form is already what `examples/auth-showcase/app/lib/auth.ts` uses — it passes `onSignedIn` / `onSignedOut` to `createAuthEngine`, which is correct and must not change. The deprecated members are the same names on _different_ option objects.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Confirm the blast radius before deleting**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "onSuccessfullSignUp" packages/ examples/ services/ | grep -v dist/
|
||||||
|
grep -rn "onSignedIn\|onSignedOut" packages/ examples/ --include=*.ts | grep -v "packages/auth/src" | grep -v dist/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `onSuccessfullSignUp` has zero references. The `onSignedIn` / `onSignedOut` hits are `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, and `examples/auth-showcase/app/lib/auth.ts`. **The example is the correct `createAuthEngine` form and must be left alone.** Record what you found; if the results differ from this, stop and report before deleting anything.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove the option declarations**
|
||||||
|
|
||||||
|
Delete `onSignedIn` and `onSignedOut` (and their `@deprecated` comments) from the options interface in `packages/auth/src/http/index.ts` and from `packages/auth/src/plugin.ts`. Delete `onSuccessfullSignUp` from `packages/auth/src/types.ts`. Delete the `rpId` and `origin` members from both verification signatures in `packages/auth/src/engine.ts`.
|
||||||
|
|
||||||
|
Then remove the code that reads them. The `rpId` / `origin` values are already ignored — verification uses the values bound to the issued challenge — so removing them changes no behaviour.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update the tests that exercised the deprecated paths**
|
||||||
|
|
||||||
|
`packages/auth/test/http.test.ts` and `plugin.test.ts` pass the deprecated options. Rewrite each to use the `createAuthEngine` form where the test is still meaningful, and delete the test where its only purpose was to cover the deprecated alias.
|
||||||
|
|
||||||
|
`packages/auth/test/engine.test.ts` passes `rpId` / `origin` to verification. Remove those arguments; the assertions on the verification result should be unchanged, which is the evidence that the options were inert.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/auth && bun run typecheck`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Confirm no `@deprecated` markers remain in auth**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -rn "@deprecated" packages/auth/src/
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add -A packages/auth
|
||||||
|
git commit -m "refactor: remove the deprecated auth options"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Full gate
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: whatever the gate reports as stale (generated types, public API baseline, editor bundles)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: Tasks 1-5.
|
||||||
|
- Produces: a green `check:production`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rebuild the editor bundles**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd editors/vscode build
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiler and language server are embedded there and both changed.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the full gate**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
bun test
|
||||||
|
bun run typecheck
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Regenerate anything the gate reports as stale**
|
||||||
|
|
||||||
|
`check:public-api` fails when exports change — and this plan removed several from `@wrnexus/styles`. Run `bun run generate:public-api`, then **read the diff and confirm it is removals only**. An unexpected addition means something was exported by accident.
|
||||||
|
|
||||||
|
`check:generated-types` may need `bun run --cwd examples/basic-app wrnexus generate types`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the gate until green**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "chore: regenerate baselines after the legacy cleanup"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the executor
|
||||||
|
|
||||||
|
- **The seven config keys are dead.** If you find code that actually reads one, stop and report — the spec's central claim would be wrong and the plan needs revisiting.
|
||||||
|
- **Task 1 is the only behaviour-sensitive change.** Its first two tests describe current behaviour and must pass both before and after. If they fail after, the substitution was not equivalent; report rather than editing the test.
|
||||||
|
- **The auth example is already correct.** `examples/auth-showcase` uses `createAuthEngine({ onSignedIn })`, which is the current API, not the deprecated one.
|
||||||
|
- **If a test would still pass with the code it guards deleted, it is not a test.** Delete the implementation, watch it fail, restore it.
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
# `wrnexus update` Migration Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** One `wrnexus update` carries an existing project from today's syntax to the syntax left by the cleanup and `apis { }` plans — or refuses precisely, naming the file and the reason.
|
||||||
|
|
||||||
|
**Architecture:** These are new `Migration` entries in the existing `update.ts` framework, which already has dry-run support and a report that separates automatic changes from ones needing review. `.wrn` rewriting parses with `@wrnexus/syntax` and re-emits through `formatWrn`, both already imported there.
|
||||||
|
|
||||||
|
**Tech Stack:** Bun, TypeScript, `bun:test`, `@wrnexus/syntax`.
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-08-19-update-migration-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- **A file is transformed correctly, or it is left untouched and reported.** There is no third outcome — never a partial rewrite.
|
||||||
|
- Every migration is **idempotent**: running it twice changes nothing the second time.
|
||||||
|
- **Dry-run reports exactly what a real run would change**, and writes nothing.
|
||||||
|
- 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.
|
||||||
|
- Migrations attach to the release that ships the breaking change, above the `0.8.0` floor.
|
||||||
|
- `bun run format` before every commit; the gate is `bun run check:production`.
|
||||||
|
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Remove the dead config keys
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/cli/src/update.ts` (add a `Migration`)
|
||||||
|
- Test: `packages/cli/test/migrate-config-keys.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: a migration with `id: "remove-dead-config-keys"`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-config-keys.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { afterEach, expect, test } from "bun:test";
|
||||||
|
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { runMigrations } from "../src/update.ts";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
afterEach(() => {
|
||||||
|
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
const CONFIG = `export default {
|
||||||
|
compatibilityDate: "2026-08-02",
|
||||||
|
frameworkBehaviour: 1,
|
||||||
|
functions: { legacyDefaultRuntime: "current" },
|
||||||
|
compatibility: { legacyEmit: false, stringLayouts: false },
|
||||||
|
observability: { sampleRate: 1 },
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
function project(): string {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-migrate-"));
|
||||||
|
roots.push(root);
|
||||||
|
mkdirSync(join(root, "app"), { recursive: true });
|
||||||
|
writeFileSync(join(root, "wrnexus.config.ts"), CONFIG);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the removed keys are deleted and the rest is kept", async () => {
|
||||||
|
const root = project();
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
|
||||||
|
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||||
|
|
||||||
|
expect(config).not.toContain("compatibilityDate");
|
||||||
|
expect(config).not.toContain("frameworkBehaviour");
|
||||||
|
expect(config).not.toContain("legacyDefaultRuntime");
|
||||||
|
expect(config).not.toContain("legacyEmit");
|
||||||
|
expect(config).toContain("observability");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("running it twice changes nothing the second time", async () => {
|
||||||
|
const root = project();
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
|
||||||
|
const once = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
|
||||||
|
|
||||||
|
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a dry run writes nothing", async () => {
|
||||||
|
const root = project();
|
||||||
|
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: true });
|
||||||
|
|
||||||
|
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(CONFIG);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Use whatever entry point `update.ts` exports for running migrations; if the name differs from `runMigrations`, adapt the calls and keep the assertions identical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-config-keys.test.ts`
|
||||||
|
Expected: FAIL — the keys survive.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the migration**
|
||||||
|
|
||||||
|
Append to the migration list in `packages/cli/src/update.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
{
|
||||||
|
version: "0.9.0",
|
||||||
|
id: "remove-dead-config-keys",
|
||||||
|
description: "Delete compatibilityDate, frameworkBehaviour, functions, and compatibility",
|
||||||
|
apply(ctx) {
|
||||||
|
const file = join(ctx.appRoot, "wrnexus.config.ts");
|
||||||
|
if (!existsSync(file)) return;
|
||||||
|
|
||||||
|
const before = readFileSync(file, "utf8");
|
||||||
|
// Each key is a whole property line or block; removing the line leaves
|
||||||
|
// valid TypeScript because these are always object members.
|
||||||
|
const after = before
|
||||||
|
.replace(/^\s*compatibilityDate:.*\n/m, "")
|
||||||
|
.replace(/^\s*frameworkBehaviour:.*\n/m, "")
|
||||||
|
.replace(/^\s*functions:\s*\{[^}]*\},?\s*\n/m, "")
|
||||||
|
.replace(/^\s*compatibility:\s*\{[^}]*\},?\s*\n/m, "");
|
||||||
|
|
||||||
|
if (after === before) return;
|
||||||
|
|
||||||
|
ctx.report.changedAutomatically.push(`${file}: removed dead compatibility keys`);
|
||||||
|
if (!ctx.dryRun) writeFileSync(file, after, "utf8");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): migrate away the dead config keys"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Move `ssr { api … }` / `client { api … }` into `apis { }`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/cli/src/migrations/apis-block.ts`
|
||||||
|
- Modify: `packages/cli/src/update.ts` (register the migration)
|
||||||
|
- Test: `packages/cli/test/migrate-apis-block.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `migrateApisBlock(source: string): { source: string; changed: boolean } | { skip: string }` — a pure function over `.wrn` text, so it is testable without a filesystem. `skip` carries the human-readable reason.
|
||||||
|
|
||||||
|
Sectioned bodies carry across unchanged, because the payload is already bound to `data`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-apis-block.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { migrateApisBlock } from "../src/migrations/apis-block.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Search {
|
||||||
|
client {
|
||||||
|
api searchUsers POST /api/users {
|
||||||
|
request { body { name?: string } }
|
||||||
|
response { return data.users }
|
||||||
|
error { return [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a client api entry moves into an apis block", () => {
|
||||||
|
const result = migrateApisBlock(SOURCE) as { source: string; changed: boolean };
|
||||||
|
|
||||||
|
expect(result.changed).toBe(true);
|
||||||
|
expect(result.source).toContain("apis {");
|
||||||
|
expect(result.source).toContain("searchUsers POST /api/users");
|
||||||
|
expect(result.source).not.toContain("client {\n api");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the sections survive unchanged", () => {
|
||||||
|
const result = migrateApisBlock(SOURCE) as { source: string };
|
||||||
|
|
||||||
|
expect(result.source).toContain("return data.users");
|
||||||
|
expect(result.source).toContain("return []");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("running it on migrated source changes nothing", () => {
|
||||||
|
const once = (migrateApisBlock(SOURCE) as { source: string }).source;
|
||||||
|
const twice = migrateApisBlock(once) as { source: string; changed: boolean };
|
||||||
|
|
||||||
|
expect(twice.changed).toBe(false);
|
||||||
|
expect(twice.source).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name declared in both modes is skipped with a reason", () => {
|
||||||
|
const clash = `page P {
|
||||||
|
ssr { api dup GET /api/a { response { return data } } }
|
||||||
|
client { api dup GET /api/a { response { return data } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = migrateApisBlock(clash) as { skip: string };
|
||||||
|
|
||||||
|
expect(result.skip).toContain("dup");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-apis-block.test.ts`
|
||||||
|
Expected: FAIL — the module does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the transform**
|
||||||
|
|
||||||
|
Create `packages/cli/src/migrations/apis-block.ts`. Parse with `parse` from `@wrnexus/syntax` to find the entries and validate the file, collect every `api` entry from `ssr` / `client` blocks, detect duplicate names across modes and return `{ skip }` when found, then emit one `apis { }` block and delete the now-empty mode blocks. Re-emit through `formatWrn`.
|
||||||
|
|
||||||
|
Detect already-migrated input by checking whether the source has an `apis` block and no mode data blocks; return `{ source, changed: false }`.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Register it**
|
||||||
|
|
||||||
|
Add a `Migration` with `id: "move-api-blocks"` that walks `app/**/*.wrn`, calls `migrateApisBlock`, and routes the outcome: a change goes to `changedAutomatically`, a `skip` goes to `needsReview` with the file and reason, and a `parse` failure goes to `parseFailures` with the file left untouched.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): migrate api entries into the apis block"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Move mode-scoped helpers into `functions { shared … }`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/cli/src/migrations/mode-functions.ts`
|
||||||
|
- Modify: `packages/cli/src/update.ts`
|
||||||
|
- Test: `packages/cli/test/migrate-mode-functions.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `migrateModeFunctions(source: string): { source: string; changed: boolean } | { skip: string }`.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-mode-functions.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { migrateModeFunctions } from "../src/migrations/mode-functions.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Hello {
|
||||||
|
ssr {
|
||||||
|
functions {
|
||||||
|
function userNames(users) {
|
||||||
|
return users.map((user) => user.name).join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a mode helper becomes a shared function", () => {
|
||||||
|
const result = migrateModeFunctions(SOURCE) as { source: string; changed: boolean };
|
||||||
|
|
||||||
|
expect(result.changed).toBe(true);
|
||||||
|
expect(result.source).toContain("shared function userNames");
|
||||||
|
expect(result.source).not.toContain("ssr {");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("running it again changes nothing", () => {
|
||||||
|
const once = (migrateModeFunctions(SOURCE) as { source: string }).source;
|
||||||
|
const twice = migrateModeFunctions(once) as { changed: boolean; source: string };
|
||||||
|
|
||||||
|
expect(twice.changed).toBe(false);
|
||||||
|
expect(twice.source).toBe(once);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name that already exists at page level is skipped with a reason", () => {
|
||||||
|
const clash = `page P {
|
||||||
|
functions { shared function userNames() { return "" } }
|
||||||
|
ssr { functions { function userNames(users) { return "" } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const result = migrateModeFunctions(clash) as { skip: string };
|
||||||
|
|
||||||
|
expect(result.skip).toContain("userNames");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-mode-functions.test.ts`
|
||||||
|
Expected: FAIL — the module does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement and register**
|
||||||
|
|
||||||
|
Create the module following Task 2's shape: relocate each mode-scoped function into the page-level `functions { }` with the `shared` modifier, skipping the file with a reason when a name already exists there. Register a `Migration` with `id: "move-mode-functions"` that routes outcomes to the same three report buckets.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): migrate mode-scoped helpers to shared functions"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Detect legacy bare-body blocks and report them — do not rewrite
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Create: `packages/cli/src/migrations/legacy-api-body.ts`
|
||||||
|
- Modify: `packages/cli/src/update.ts`
|
||||||
|
- Test: `packages/cli/test/migrate-legacy-api-body.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Produces: `detectLegacyApiBodies(source: string): { name: string; freeIdentifiers: string[] }[]`.
|
||||||
|
|
||||||
|
**This transform is deliberately manual, and the test pins that.** A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare identifiers. Converting `return userNames(users)` needs `data.users` — but **nothing in the source distinguishes `users` (payload) from `userNames` (page helper)**. The response shape belongs to the route, which may not be typed. A migration that guessed would emit code that compiles and is wrong.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/migrate-legacy-api-body.test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { detectLegacyApiBodies } from "../src/migrations/legacy-api-body.ts";
|
||||||
|
|
||||||
|
const SOURCE = `page Hello {
|
||||||
|
ssr {
|
||||||
|
api ssrUsers GET /api/users/ssr {
|
||||||
|
return userNames(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a legacy bare body is detected with its free identifiers", () => {
|
||||||
|
const found = detectLegacyApiBodies(SOURCE);
|
||||||
|
|
||||||
|
expect(found).toHaveLength(1);
|
||||||
|
expect(found[0]!.name).toBe("ssrUsers");
|
||||||
|
expect(found[0]!.freeIdentifiers).toContain("users");
|
||||||
|
expect(found[0]!.freeIdentifiers).toContain("userNames");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a sectioned block is not reported", () => {
|
||||||
|
const sectioned = `page P {
|
||||||
|
apis { x GET /api/x { response { return data.users } } }
|
||||||
|
view { <main>x</main> }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
expect(detectLegacyApiBodies(sectioned)).toEqual([]);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/migrate-legacy-api-body.test.ts`
|
||||||
|
Expected: FAIL — the module does not exist.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement detection only**
|
||||||
|
|
||||||
|
Create the module. Find `api` entries whose `sections` is absent (the bare-body form), and collect the free identifiers in the body — identifiers that are not declared locally and are not JavaScript globals. Return them. **Write no transform.**
|
||||||
|
|
||||||
|
- [ ] **Step 4: Register a report-only migration**
|
||||||
|
|
||||||
|
Add a `Migration` with `id: "report-legacy-api-bodies"` that pushes one `needsReview` entry per block, naming the file, the block, and the identifiers, and leaves the file byte-identical. Have `wrnexus update` print a short line explaining why this one is manual: the payload fields cannot be told apart from page helpers without knowing the route's response shape.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Write the byte-identical test**
|
||||||
|
|
||||||
|
Add a test that runs the full migration over a fixture project containing a legacy bare body and asserts the file's contents are unchanged afterwards, and that the report names the block. **This is the most important test in the plan** — it pins that the migration does not attempt the rewrite.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the tests**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add packages/cli
|
||||||
|
git commit -m "feat(cli): report legacy api bodies for manual migration"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Exit code, output order, and the end-to-end run
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
|
||||||
|
- Modify: `packages/cli/src/update.ts` (the command's output and exit code)
|
||||||
|
- Test: `packages/cli/test/update-exit-code.test.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
|
||||||
|
- Consumes: Tasks 1-4.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `packages/cli/test/update-exit-code.test.ts` asserting that a project with a legacy bare body produces a non-zero exit, and a fully-migratable project produces zero. Use the same temp-project pattern as Task 1.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `bun test packages/cli/test/update-exit-code.test.ts`
|
||||||
|
Expected: FAIL — the command currently exits zero regardless.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the output and exit code**
|
||||||
|
|
||||||
|
Print, in order: what changed, what needs review and why, what failed to parse. Exit non-zero when `needsReview` or `parseFailures` is non-empty.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Migrate the example app with the command alone**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd examples/basic-app wrnexus update
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: the `.wrn` pages are migrated by the tool, not by hand. **If the framework's own example cannot be migrated by the tool, the tool is not finished** — report that rather than editing the example manually.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify the migrated example**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run --cwd examples/basic-app build
|
||||||
|
bun test
|
||||||
|
bun run typecheck
|
||||||
|
bun run check:production
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run format
|
||||||
|
git add -A
|
||||||
|
git commit -m "feat(cli): fail the update when a project needs manual review"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes for the executor
|
||||||
|
|
||||||
|
- **Never half-rewrite a file.** Parse first; on failure, record and move on. If any part of a file's transform cannot complete, skip the whole file and report it.
|
||||||
|
- **Idempotency is not optional.** Every transform detects already-migrated input.
|
||||||
|
- **Task 4 writes no transform.** If you find yourself building one, stop — the spec explains why a correct automatic answer does not exist.
|
||||||
Reference in New Issue
Block a user