Files
WRNexusJS/docs/superpowers/plans/2026-08-19-legacy-and-config-cleanup.md
T

21 KiB
Raw Blame History

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:

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:

export type FunctionRuntime = "client" | "server" | "shared";

And at the parse site (~line 209), change the default:

let runtime: FunctionRuntime = "shared";
  • Step 4: Drop "legacy" from every membership test

In packages/compiler/src/client-codegen.ts, both occurrences:

    ["client", "shared"].includes(fn.runtime),

In packages/compiler/src/server-codegen.ts:

const names = ast.runtimeFunctions
  .filter((fn) => ["server", "shared"].includes(fn.runtime))
  .map((fn) => fn.name);

In packages/compiler/src/codegen.ts, targetFunctions:

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
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:

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
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:

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
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:

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.x0.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
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
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:

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:

import type { Attr, PageAst, ViewNode } from "./parser.ts";

the same way.

  • Step 2: Verify nothing else imports the shims
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
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
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
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
grep -rn "@deprecated" packages/auth/src/

Expected: no output.

  • Step 6: Commit
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

bun run --cwd editors/vscode build

The compiler and language server are embedded there and both changed.

  • Step 2: Run the full gate
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
bun run check:production

Expected: exit 0.

  • Step 5: Commit
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.