fix(cli): guard the one migration write path that skips parse validation

mode-functions writes without a parse check when a mode wrapper survives
holding api entries, since that intermediate state is unparseable until
move-api-blocks runs later in the same pass. Brace balance is the invariant
a bad splice offset would break, so check that instead; nothing downstream
could tell a corrupted wrapper from an untouched one.

Also records that Task 5's example-app migration ran against an already-
migrated target and so did not prove end-to-end behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:24:19 +05:30
co-authored by Claude Opus 5
parent aded2daab9
commit 7a1b4e5b33
2 changed files with 48 additions and 1 deletions
+31 -1
View File
@@ -32,6 +32,24 @@ function findMatching(source: string, open: number, openChar = "{", closeChar =
return -1;
}
/** Net `{` minus `}` outside string literals -- a splice that loses a brace changes it. */
function braceBalance(source: string): number {
let balance = 0;
let quote = "";
for (let index = 0; index < source.length; index++) {
const char = source[index]!;
if (quote) {
if (char === "\\") index++;
else if (char === quote) quote = "";
continue;
}
if (char === '"' || char === "'" || char === "`") quote = char;
else if (char === "{") balance++;
else if (char === "}") balance--;
}
return balance;
}
interface ModeBlock {
mode: "ssr" | "client";
/** Start of the `ssr`/`client` keyword. */
@@ -261,7 +279,19 @@ export function migrateModeFunctions(
if (after === source) return { source, changed: false };
if (!leavesModeWrapper) {
if (leavesModeWrapper) {
// The only path that writes without a parse check. A surviving `ssr {`/`client {`
// wrapper still holding `api` entries is unparseable by design -- `move-api-blocks`
// finishes the job later in the same run -- so `parse` cannot validate it here.
// Brace balance is the one invariant still checkable, and it is what a bad splice
// offset would break. Downstream nothing else would catch it: the parser rejects a
// corrupted wrapper and an untouched one identically.
if (braceBalance(after) !== braceBalance(source)) {
throw new Error(
"mode-functions migration unbalanced the source braces; refusing to write the file",
);
}
} else {
try {
parse(after);
} catch (error) {