From f1f6eddc2616a60a2baf26b8e66f58f2bf37fc5c Mon Sep 17 00:00:00 2001 From: Bob Massarczyk Date: Wed, 17 Jun 2026 13:45:04 +0200 Subject: [PATCH 1/6] chore: tighten tsconfig --- tsconfig.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index ff4adab..ce788b7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,10 @@ "types": ["node"], "strict": true, "noUnusedLocals": true, - "declaration": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, "noEmit": true, "allowImportingTsExtensions": true, "esModuleInterop": true, From 20d662aaf01261098720947200ffc969f4c617f3 Mon Sep 17 00:00:00 2001 From: Bob Massarczyk Date: Wed, 17 Jun 2026 13:49:17 +0200 Subject: [PATCH 2/6] fix: check index access --- src/core.ts | 28 +++++++++++++++++++++------- src/transaction.ts | 8 ++++++-- tsconfig.json | 1 + 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/core.ts b/src/core.ts index 9497dd6..5161aea 100644 --- a/src/core.ts +++ b/src/core.ts @@ -37,12 +37,24 @@ export function createHistory(options: HistoryOptions = {}): History { } }; + const undoEntry = (): Entry | undefined => { + if (cursor === 0) return undefined; + // The cursor is kept in [0, length], so cursor - 1 names an undoable entry. + return entries[cursor - 1]!; + }; + + const redoEntry = (): Entry | undefined => { + if (cursor === entries.length) return undefined; + // The cursor is kept in [0, length], so cursor names a redoable entry. + return entries[cursor]!; + }; + const buildMeta = (): HistoryMeta => Object.freeze({ canUndo: cursor > 0, canRedo: cursor < entries.length, - undoLabel: cursor > 0 ? entries[cursor - 1].label : undefined, - redoLabel: cursor < entries.length ? entries[cursor].label : undefined, + undoLabel: undoEntry()?.label, + redoLabel: redoEntry()?.label, position: cursor, length: entries.length, }); @@ -88,11 +100,13 @@ export function createHistory(options: HistoryOptions = {}): History { while (remaining > 0) { if (dir < 0) { if (cursor === 0) break; - runGuarded(entries[cursor - 1].undo); + const entry = undoEntry()!; + runGuarded(entry.undo); cursor -= 1; } else { if (cursor === entries.length) break; - runGuarded(entries[cursor].do); + const entry = redoEntry()!; + runGuarded(entry.do); cursor += 1; } remaining -= 1; @@ -154,7 +168,7 @@ export function createHistory(options: HistoryOptions = {}): History { const checkpoint = (name: string): void => { assertIdle(); - checkpoints.set(name, cursor === 0 ? START : entries[cursor - 1]); + checkpoints.set(name, cursor === 0 ? START : undoEntry()!); }; const hasCheckpoint = (name: string): boolean => checkpoints.has(name); @@ -218,10 +232,10 @@ export function createHistory(options: HistoryOptions = {}): History { return cursor < entries.length; }, get undoLabel() { - return cursor > 0 ? entries[cursor - 1].label : undefined; + return undoEntry()?.label; }, get redoLabel() { - return cursor < entries.length ? entries[cursor].label : undefined; + return redoEntry()?.label; }, get position() { return cursor; diff --git a/src/transaction.ts b/src/transaction.ts index 9a4c0f1..383fa76 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -86,7 +86,9 @@ export function runTransaction( closed = true; try { for (let i = ops.length - 1; i >= 0; i -= 1) { - engine.runGuarded(ops[i].undo); + // The reverse walk starts at the last recorded op and stops before -1. + const op = ops[i]!; + engine.runGuarded(op.undo); } } finally { engine.endMutation(); @@ -106,7 +108,9 @@ export function runTransaction( }, undo() { for (let i = ops.length - 1; i >= 0; i -= 1) { - engine.runGuarded(ops[i].undo); + // The reverse walk starts at the last recorded op and stops before -1. + const op = ops[i]!; + engine.runGuarded(op.undo); } }, }); diff --git a/tsconfig.json b/tsconfig.json index ce788b7..2619c12 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "resolveJsonModule": true, "types": ["node"], "strict": true, + "noUncheckedIndexedAccess": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, From 5580642acf512aa3e1568d0adc998f2e60820456 Mon Sep 17 00:00:00 2001 From: Bob Massarczyk Date: Wed, 17 Jun 2026 13:54:14 +0200 Subject: [PATCH 3/6] chore: target es2024 --- tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 2619c12..e0e3df6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "target": "esnext", - "lib": ["es2023"], + "target": "es2024", + "lib": ["es2024"], "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", From 44fae11de231491830a2585f4cda13822d2f7197 Mon Sep 17 00:00:00 2001 From: Bob Massarczyk Date: Wed, 17 Jun 2026 13:56:32 +0200 Subject: [PATCH 4/6] chore: clean up deprecated flags --- tsconfig.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index e0e3df6..cedf001 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,6 @@ "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", - "resolveJsonModule": true, "types": ["node"], "strict": true, "noUncheckedIndexedAccess": true, @@ -16,7 +15,6 @@ "noImplicitOverride": true, "noEmit": true, "allowImportingTsExtensions": true, - "esModuleInterop": true, "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true From b5b67981d7179dd1b8c9d448cabf0d5772daa429 Mon Sep 17 00:00:00 2001 From: Bob Massarczyk Date: Wed, 17 Jun 2026 14:24:14 +0200 Subject: [PATCH 5/6] fix: split config --- package.json | 2 +- tsconfig.json | 5 +++-- tsconfig.test.json | 7 +++++++ 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 tsconfig.test.json diff --git a/package.json b/package.json index 96fee38..faae46b 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "build": "vp pack", "dev": "vp pack --watch", "test": "vp test run", - "check": "vp check", + "check": "vp check && tsc -p tsconfig.test.json", "check:exports": "vp pack && vp dlx publint && vp dlx @arethetypeswrong/cli --pack --profile esm-only", "prepublishOnly": "vp run build" }, diff --git a/tsconfig.json b/tsconfig.json index cedf001..2745bc3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "moduleDetection": "force", "module": "nodenext", "moduleResolution": "nodenext", - "types": ["node"], + "types": [], "strict": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true, @@ -18,5 +18,6 @@ "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true - } + }, + "include": ["src"] } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..3655f60 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["tests", "vite.config.ts"] +} From 0fd2a4b3356110fcd1d476bbe23055288227d93d Mon Sep 17 00:00:00 2001 From: Bob Massarczyk Date: Wed, 17 Jun 2026 14:31:43 +0200 Subject: [PATCH 6/6] fix: vp injects node types, but this is only implicit and brittle --- package.json | 2 +- tsconfig.test.json | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index faae46b..96fee38 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "build": "vp pack", "dev": "vp pack --watch", "test": "vp test run", - "check": "vp check && tsc -p tsconfig.test.json", + "check": "vp check", "check:exports": "vp pack && vp dlx publint && vp dlx @arethetypeswrong/cli --pack --profile esm-only", "prepublishOnly": "vp run build" }, diff --git a/tsconfig.test.json b/tsconfig.test.json index 3655f60..25cae3c 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -1,4 +1,6 @@ { + // Keeps editor/raw tsc coverage for Node-run files while tsconfig.json + // remains the shipped-source contract with no Node ambient globals. "extends": "./tsconfig.json", "compilerOptions": { "types": ["node"]