diff --git a/.agents/docs/ink-divergences.md b/.agents/docs/ink-divergences.md index 1d284f4..1a696e4 100644 --- a/.agents/docs/ink-divergences.md +++ b/.agents/docs/ink-divergences.md @@ -64,6 +64,18 @@ deliberate. Divergences fall into a few kinds: Maintainer decision (2026-05-30): KEEP. Tests: `usePaste-only app exits on {legacy,kitty} Ctrl+C` in `input-kitty.test.ts`. +### Non-`Error` thrown values keep their message in the error overview + +- **Ink:** `ErrorOverview` renders `error.message`; a thrown non-`Error` (`throw 'boom'`) has no + `.message`, so the overview shows a blank message. +- **vue-tui:** the error boundary keeps the **raw** thrown value and `ErrorOverview` shows + `String(value)` as the message, so `throw 'boom'` renders ` ERROR boom`, not a blank + `ERROR`. Like Ink, no stack block is rendered when the value carries no stack. +- **Why:** strictly more informative for the (lint-discouraged) non-`Error` throw, and it keeps + the message vue-tui already surfaced before — when the boundary wrapped such throws in + `new Error(String(value))`, which also produced a misleading synthetic stack pointing at the + framework internals (that synthetic stack is now gone). Introduced 2026-05-31. + ## Framework-semantic divergences (Vue ≠ React) ### Removing `flexDirection` / `flexWrap` resets to the default diff --git a/packages/runtime-tests/integration/lifecycle/error-overview.test.tsx b/packages/runtime-tests/integration/lifecycle/error-overview.test.tsx new file mode 100644 index 0000000..e6c1eb3 --- /dev/null +++ b/packages/runtime-tests/integration/lifecycle/error-overview.test.tsx @@ -0,0 +1,145 @@ +import { defineComponent, h } from "vue"; +import { expect, test } from "vite-plus/test"; +import stripAnsi from "strip-ansi"; +import { createApp, Text } from "@vue-tui/runtime"; +import { + captureWrites, + getContentWrites, + makeFakeStdin, + makeFakeWritable, +} from "./test-streams.ts"; + +// The throwing components are defined INLINE in this test file (not a separate +// fixture module) on purpose: @vitejs/plugin-vue-jsx injects an SSR +// register-helper around every defineComponent in a non-test .tsx module, and +// that helper throws in the test runner before our intended error fires. +// Defining them here also means the thrown error's stack origin points at THIS +// file — which exists on disk — so ErrorOverview's fs.existsSync guard passes +// and the code-excerpt is read back and rendered. +// +// The throw below is on a known line; the excerpt test asserts that the source +// of the throw line ('throw new Error("Boom from fixture")') is highlighted. +const ThrowingComponent = defineComponent(() => { + return () => { + throw new Error("Boom from fixture"); + }; +}); + +// Nested-throw parent, mirroring Ink's errors.tsx:88-121. +const NestedThrower = defineComponent(() => { + return () => { + throw new Error("Nested component error"); + }; +}); +const ParentWithNestedThrow = defineComponent(() => { + return () => h(Text, null, ["Before error", h(NestedThrower)]); +}); + +// Finding 2: a primitive (non-Error) throw. Vue's onErrorCaptured receives the +// raw value; Ink stores the raw thrown value and ErrorOverview renders the +// stack block only when error.stack exists. A string has no .stack, so the +// frame must show " ERROR " and NO synthetic stack. +const PrimitiveThrower = defineComponent(() => { + return () => { + // eslint-disable-next-line @typescript-eslint/only-throw-error -- deliberately throwing a primitive to exercise the non-Error display path (Ink parity) + throw "primitive thrown"; + }; +}); + +// Finding 1: a thrown Error whose stack contains a frame StackUtils cannot +// parse. We overwrite .stack with the message line plus a single unparsable +// frame so ErrorOverview hits its `!parsedLine` fallback branch. +const UnparsableStackThrower = defineComponent(() => { + return () => { + const e = new Error("Unparsable stack boom"); + const firstLine = (e.stack ?? "").split("\n")[0] ?? "Error: Unparsable stack boom"; + e.stack = `${firstLine}\n <<>>`; + throw e; + }; +}); + +// Mirrors Ink's test/errors.tsx: mount a throwing component, then inspect the +// LAST content write to stdout — that is the ErrorOverview frame the boundary +// renders before exit(). We mount directly (not via @vue-tui/testing's render(), +// which re-throws the captured error and would discard the frame) so we can read +// the frame the error boundary painted. +async function renderErrorFrame(component: Parameters[0]): Promise { + const stdout = makeFakeWritable(); + const stderr = makeFakeWritable(); + const { stream: stdin } = makeFakeStdin(); + const writes = captureWrites(stdout); + + const app = createApp(component); + app.mount({ stdout, stdin, stderr, debug: true, exitOnCtrlC: false }); + + // The exit promise rejects (component threw); swallow it. Then wait for the + // boundary's onErrorCaptured → nextTick → ErrorOverview commit → exit chain. + app.waitUntilExit().catch(() => {}); + await new Promise((r) => setImmediate(r)); + await new Promise((r) => setImmediate(r)); + + const content = getContentWrites(writes); + const lastContentWrite = content.at(-1); + if (lastContentWrite === undefined) { + throw new Error("no content write captured"); + } + return stripAnsi(lastContentWrite); +} + +test("renders a full ERROR overview frame with label, origin, excerpt, and stack", async () => { + const frame = await renderErrorFrame(ThrowingComponent); + + // White-on-red " ERROR " label followed by the message. Ink renders + // " ERROR Oh no" — a space inside the label on each side, plus a leading + // space on the message — so " ERROR " appears after stripping ANSI. + expect(frame).toContain(" ERROR Boom from fixture"); + + // Parsed file:line:column origin line (dimColor). The throw is in this file. + expect(frame).toMatch(/error-overview\.test\.tsx:\d+:\d+/); + + // Code excerpt: the throwing line is read back from disk and highlighted. + // Assert the source text of the throw appears with a padded line-number gutter. + expect(frame).toMatch(/\d+:\s+throw new Error\("Boom from fixture"\);/); + + // Stack trace line: "- (::)" with a cwd-relative path. + expect(frame).toMatch(/- .*\(.*error-overview\.test\.tsx:\d+:\d+\)/); +}); + +test("nested component throw renders a frame containing ERROR and the message", async () => { + const frame = await renderErrorFrame(ParentWithNestedThrow); + + // Case-sensitive ERROR substring (mirrors errors.tsx:88-121). + expect(frame).toContain("ERROR"); + expect(frame).toContain("Nested component error"); +}); + +test("unparsable stack frame falls back to literal backslash-t (not a real TAB)", async () => { + const frame = await renderErrorFrame(UnparsableStackThrower); + + expect(frame).toContain(" ERROR Unparsable stack boom"); + + // Ink's JSX `{line}\t{' '}` emits the unparsed line followed by TWO LITERAL + // chars (backslash + t) and a space — `\t` in JSXText is not an escape. + // Match byte-for-byte: backslash, t, space after the raw frame text. + expect(frame).toContain("<<>>\\t "); + + // And it must NOT contain a real TAB (0x09) on the fallback line. + const fallbackLine = frame.split("\n").find((l) => l.includes("<<>>")); + expect(fallbackLine).toBeDefined(); + expect(fallbackLine).not.toContain("\t"); +}); + +test("primitive (non-Error) throw renders ERROR header with no synthetic stack", async () => { + const frame = await renderErrorFrame(PrimitiveThrower); + + // Ink derives the message from the value (String(value)) and renders just the + // header for a primitive throw. + expect(frame).toContain(" ERROR primitive thrown"); + + // A primitive has no .stack, so Ink renders no origin/excerpt/stack block. + // The synthetic-stack regression would surface as dist/index.mjs or Vue + // runtime frames and "- " stack-frame lines — assert none appear. + expect(frame).not.toContain("dist/index.mjs"); + expect(frame).not.toContain("dist/"); + expect(frame).not.toMatch(/^\s*- /m); +}); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 136c774..1224563 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -29,10 +29,12 @@ "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-truncate": "^6.0.0", + "code-excerpt": "^4.0.0", "is-in-ci": "catalog:", "patch-console": "catalog:", "signal-exit": "^4.1.0", "slice-ansi": "^9.0.0", + "stack-utils": "^2.0.6", "string-width": "^8.0.0", "terminal-size": "catalog:", "wrap-ansi": "^10.0.0", @@ -40,6 +42,7 @@ }, "devDependencies": { "@types/node": "^25.6.2", + "@types/stack-utils": "^2.0.3", "@vitejs/plugin-vue-jsx": "catalog:", "typescript": "^6.0.3", "vite-plus": "^0.1.20", diff --git a/packages/runtime/src/components/ErrorOverview.ts b/packages/runtime/src/components/ErrorOverview.ts index 0d2fccb..6b9046a 100644 --- a/packages/runtime/src/components/ErrorOverview.ts +++ b/packages/runtime/src/components/ErrorOverview.ts @@ -1,14 +1,225 @@ +import * as fs from "node:fs"; +import { cwd } from "node:process"; import { defineComponent, h, type PropType } from "vue"; +import StackUtils from "stack-utils"; +import codeExcerpt, { type CodeExcerpt } from "code-excerpt"; +import { Box } from "./Box.ts"; +import { Text } from "./Text.ts"; + +// Ported from Ink's src/components/ErrorOverview.tsx (v7.0.4). We use the +// and wrapper components (not raw host elements) because Ink does, and +// because aria-label support is implemented at that component layer in vue-tui — +// the labels are only emitted when a screen reader is enabled, matching Ink. + +// Error's source file is reported as file:///home/user/file.js; this removes +// the `file://[cwd]/` prefix so paths render cwd-relative (Ink cleanupPath). +const cleanupPath = (path: string | undefined): string | undefined => { + return path?.replace(`file://${cwd()}/`, ""); +}; + +const stackUtils = new StackUtils({ + cwd: cwd(), + internals: StackUtils.nodeInternals(), +}); export const ErrorOverview = defineComponent({ name: "ErrorOverview", props: { - error: { type: Object as PropType, required: true }, + // `error` is the RAW thrown value, not necessarily an Error. Ink stores the + // raw value too (ErrorBoundary.tsx:18) and a primitive throw (e.g. + // `throw "x"`) has no `.stack`, so only the header renders. Typed `unknown` + // (validator `null` = "any value, including undefined") and unwrapped + // defensively below so a non-Error can't crash the overview. + error: { type: null as unknown as PropType, required: true }, }, setup(props) { - return () => - h("box", { flexDirection: "column" }, [ - h("text", {}, [props.error.name + ": " + props.error.message]), - ]); + return () => { + const error = props.error; + + // Pull `.stack`/`.message` defensively: the value may be a primitive. + const errorStack = + typeof (error as { stack?: unknown })?.stack === "string" + ? (error as { stack: string }).stack + : undefined; + // Ink renders `{error.message}`; for an Error that's its message, for a + // primitive thrown value we fall back to String(value) so the header still + // shows something meaningful (e.g. `throw "boom"` → ` ERROR boom`). + const errorMessage = error instanceof Error ? error.message : String(error); + + // First stack line is the message; the rest are frames. The first frame + // is the throw origin used for the file:line:col header and excerpt. + const stack = errorStack ? errorStack.split("\n").slice(1) : undefined; + const origin = stack ? stackUtils.parseLine(stack[0]!) : undefined; + const filePath = cleanupPath(origin?.file); + let excerpt: CodeExcerpt[] | undefined; + let lineWidth = 0; + + if (filePath && origin?.line && fs.existsSync(filePath)) { + const sourceCode = fs.readFileSync(filePath, "utf8"); + excerpt = codeExcerpt(sourceCode, origin.line); + + if (excerpt) { + for (const { line } of excerpt) { + lineWidth = Math.max(lineWidth, String(line).length); + } + } + } + + const children: ReturnType[] = []; + + // ── White-on-red " ERROR " label + the error message ── + children.push( + h(Box, null, { + default: () => [ + h(Text, { backgroundColor: "red", color: "white" }, { default: () => " ERROR " }), + h(Text, null, { default: () => ` ${errorMessage}` }), + ], + }), + ); + + // ── Parsed file:line:column origin (dimColor) ── + if (origin && filePath) { + children.push( + h( + Box, + { marginTop: 1 }, + { + default: () => + h( + Text, + { dimColor: true }, + { + default: () => `${filePath}:${origin.line}:${origin.column}`, + }, + ), + }, + ), + ); + } + + // ── Code excerpt around the throwing line ── + if (origin && excerpt) { + children.push( + h( + Box, + { marginTop: 1, flexDirection: "column" }, + { + default: () => + excerpt!.map(({ line, value }) => + h( + Box, + { key: line }, + { + default: () => [ + // Right-padded line-number gutter (width = max digits + 1). + h( + Box, + { width: lineWidth + 1 }, + { + default: () => + h( + Text, + { + dimColor: line !== origin.line, + backgroundColor: line === origin.line ? "red" : undefined, + color: line === origin.line ? "white" : undefined, + ariaLabel: + line === origin.line ? `Line ${line}, error` : `Line ${line}`, + }, + { default: () => `${String(line).padStart(lineWidth, " ")}:` }, + ), + }, + ), + h( + Text, + { + backgroundColor: line === origin.line ? "red" : undefined, + color: line === origin.line ? "white" : undefined, + }, + { default: () => ` ${value}` }, + ), + ], + }, + ), + ), + }, + ), + ); + } + + // ── Parsed stack trace ── + // Mirrors Ink's `error.stack && …` guard (ErrorOverview.tsx:90): only + // render the stack block when the thrown value actually carries a stack. + // A primitive throw has none, so this block (and the origin/excerpt blocks + // above, which depend on `origin`) is skipped — just the header shows. + if (errorStack) { + children.push( + h( + Box, + { marginTop: 1, flexDirection: "column" }, + { + default: () => + errorStack + .split("\n") + .slice(1) + .map((line) => { + const parsedLine = stackUtils.parseLine(line); + + // Unparsable line fallback: print the raw line verbatim. + if (!parsedLine) { + return h( + Box, + { key: line }, + { + default: () => [ + h(Text, { dimColor: true }, { default: () => "- " }), + h( + Text, + { dimColor: true, bold: true }, + // Ink's JSX `{line}\t{' '}` (ErrorOverview.tsx:105-108): `\t` + // in JSXText is TWO LITERAL chars (backslash + 't'), NOT a tab + // escape. Emit the literal backslash-'t' + space for byte parity — + // a template literal `\t` here would produce a real TAB (0x09). + { default: () => line + "\\t " }, + ), + ], + }, + ); + } + + const file = cleanupPath(parsedLine.file) ?? ""; + return h( + Box, + { key: line }, + { + default: () => [ + h(Text, { dimColor: true }, { default: () => "- " }), + h( + Text, + { dimColor: true, bold: true }, + { + default: () => parsedLine.function, + }, + ), + h( + Text, + { + dimColor: true, + color: "gray", + ariaLabel: `at ${file} line ${parsedLine.line} column ${parsedLine.column}`, + }, + { default: () => ` (${file}:${parsedLine.line}:${parsedLine.column})` }, + ), + ], + }, + ); + }), + }, + ), + ); + } + + return h(Box, { flexDirection: "column", padding: 1 }, { default: () => children }); + }; }, }); diff --git a/packages/runtime/src/render.ts b/packages/runtime/src/render.ts index a2423b9..eeacc14 100644 --- a/packages/runtime/src/render.ts +++ b/packages/runtime/src/render.ts @@ -398,11 +398,20 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp const ErrorBoundaryRoot = defineComponent({ name: "InternalErrorBoundary", setup() { - const error = shallowRef(null); + // Two refs by design: `caught` is the ORIGINAL thrown value, passed to + // ErrorOverview for a faithful display (Ink stores the raw value — + // ErrorBoundary.tsx:18 — and ErrorOverview only renders a stack when the + // value has one). `errored` marks that an error occurred (the value may be + // a falsy primitive, so we can't test `caught` for truthiness). The + // exit/reject machinery still receives a wrapped Error — semantics + // unchanged. + const caught = shallowRef(null); + const errored = shallowRef(false); onErrorCaptured((err) => { const e = err instanceof Error ? err : new Error(String(err)); - error.value = e; + caught.value = err; + errored.value = true; // Flush the ErrorOverview frame, then exit void nextTick(() => { exitWithError(e); @@ -411,8 +420,8 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp }); return () => { - if (error.value) { - return h(ErrorOverview, { error: error.value }); + if (errored.value) { + return h(ErrorOverview, { error: caught.value }); } return h(userRoot, userRootProps ?? undefined); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39e8bd2..8462ef5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,10 +75,10 @@ importers: version: 24.12.4 '@vitejs/plugin-vue-jsx': specifier: ^5 - version: 5.1.5(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) + version: 5.1.5(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) vite: specifier: npm:@voidzero-dev/vite-plus-core@latest - version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' examples/basic-template: dependencies: @@ -97,10 +97,10 @@ importers: version: 24.12.4 '@vitejs/plugin-vue': specifier: ^6 - version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) vite: specifier: npm:@voidzero-dev/vite-plus-core@latest - version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' examples/coding-agent: dependencies: @@ -122,10 +122,10 @@ importers: version: 24.12.4 '@vitejs/plugin-vue': specifier: ^6 - version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) vite: specifier: npm:@voidzero-dev/vite-plus-core@latest - version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' examples/flappy-bird: dependencies: @@ -144,16 +144,16 @@ importers: version: 24.12.4 '@vitejs/plugin-vue': specifier: ^6 - version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) + version: 6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3)) vite: specifier: npm:@voidzero-dev/vite-plus-core@latest - version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' packages/cli: dependencies: vite: specifier: npm:@voidzero-dev/vite-plus-core@latest - version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + version: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' devDependencies: '@types/node': specifier: 'catalog:' @@ -163,7 +163,7 @@ importers: version: 6.0.3 vite-plus: specifier: 'catalog:' - version: 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3) + version: 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3) packages/runtime: dependencies: @@ -185,6 +185,9 @@ importers: cli-truncate: specifier: ^6.0.0 version: 6.0.0 + code-excerpt: + specifier: ^4.0.0 + version: 4.0.0 is-in-ci: specifier: 'catalog:' version: 1.0.0 @@ -197,6 +200,9 @@ importers: slice-ansi: specifier: ^9.0.0 version: 9.0.0 + stack-utils: + specifier: ^2.0.6 + version: 2.0.6 string-width: specifier: ^8.0.0 version: 8.2.1 @@ -213,6 +219,9 @@ importers: '@types/node': specifier: ^25.6.2 version: 25.8.0 + '@types/stack-utils': + specifier: ^2.0.3 + version: 2.0.3 '@vitejs/plugin-vue-jsx': specifier: 'catalog:' version: 5.1.5(vite@8.0.13(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3))(vue@3.5.34(typescript@6.0.3)) @@ -607,12 +616,19 @@ packages: resolution: {integrity: sha512-0+S67blQakgeNqoKGozOUp5rQBrz2ynXZ2QIINXZPiafsD0YL0UogB9hAWc1S7k6VSNwKYC/N7MqT0V6IzpHkQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@oxc-project/runtime@0.133.0': + resolution: {integrity: sha512-PkvjA1Lq5++V5S1E6Patr92ZVcieE6EalDr1VJTqv4BnjZdOUC4W3p8k1wMXSd5/2aFP4b/A6N5sg2Bkzcr9vQ==} + engines: {node: ^20.19.0 || >=22.12.0} + '@oxc-project/types@0.129.0': resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} '@oxc-project/types@0.130.0': resolution: {integrity: sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==} + '@oxc-project/types@0.133.0': + resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@oxfmt/binding-android-arm-eabi@0.48.0': resolution: {integrity: sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1019,6 +1035,9 @@ packages: '@types/node@25.9.1': resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@vitejs/plugin-vue-jsx@5.1.5': resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1159,6 +1178,69 @@ packages: yaml: optional: true + '@voidzero-dev/vite-plus-core@0.1.23': + resolution: {integrity: sha512-Twi+95cq1pObzkNR4u6lP7z4gPhtS0/vxeBAdbTvAeA12qlyyFED7mQZnAgaVIN3k1C1ve0997F3/ncUBAwQ8w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.22.0 + '@tsdown/exe': 0.22.0 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + publint: ^0.3.8 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + typescript: ^5.0.0 || ^6.0.0 + unplugin-unused: ^0.5.0 + unrun: '*' + yaml: ^2.4.2 + peerDependenciesMeta: + '@arethetypeswrong/core': + optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + publint: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + typescript: + optional: true + unplugin-unused: + optional: true + unrun: + optional: true + yaml: + optional: true + '@voidzero-dev/vite-plus-darwin-arm64@0.1.21': resolution: {integrity: sha512-T7mPiDbE7VtjpegtJJ/e/uQOjOA/ufMo7npAaP9WVHxUEWLaR/OjVXXL9ALiW+CfKEQ0Qk/iWDB0mI6YMndzNQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1426,6 +1508,10 @@ packages: resolution: {integrity: sha512-3+YKIUFsohD9MIoOFPFBldjAlnfCmCDcqe6aYGFqlDTRKg80p4wg35L+j83QQ63iOlKRccEkbn8IuM++HsgEjA==} engines: {node: '>=22'} + code-excerpt@4.0.0: + resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -1433,6 +1519,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + convert-to-spaces@2.0.1: + resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1496,6 +1586,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1802,6 +1896,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} @@ -2272,10 +2370,14 @@ snapshots: '@oxc-project/runtime@0.129.0': {} + '@oxc-project/runtime@0.133.0': {} + '@oxc-project/types@0.129.0': {} '@oxc-project/types@0.130.0': {} + '@oxc-project/types@0.133.0': {} + '@oxfmt/binding-android-arm-eabi@0.48.0': optional: true @@ -2498,14 +2600,16 @@ snapshots: dependencies: undici-types: 7.24.6 - '@vitejs/plugin-vue-jsx@5.1.5(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))': + '@types/stack-utils@2.0.3': {} + + '@vitejs/plugin-vue-jsx@5.1.5(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@rolldown/pluginutils': 1.0.1 '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0) - vite: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' vue: 3.5.34(typescript@6.0.3) transitivePeerDependencies: - supports-color @@ -2534,10 +2638,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.7(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))': + '@vitejs/plugin-vue@6.0.7(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(vue@3.5.34(typescript@6.0.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' vue: 3.5.34(typescript@6.0.3) '@voidzero-dev/vite-plus-core@0.1.21(@types/node@25.8.0)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)': @@ -2592,6 +2696,19 @@ snapshots: tsx: 4.22.3 typescript: 6.0.3 + '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)': + dependencies: + '@oxc-project/runtime': 0.133.0 + '@oxc-project/types': 0.133.0 + lightningcss: 1.32.0 + postcss: 8.5.14 + optionalDependencies: + '@types/node': 24.12.4 + esbuild: 0.28.0 + fsevents: 2.3.3 + tsx: 4.22.3 + typescript: 6.0.3 + '@voidzero-dev/vite-plus-darwin-arm64@0.1.21': optional: true @@ -2668,7 +2785,7 @@ snapshots: - utf-8-validate - yaml - '@voidzero-dev/vite-plus-test@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)': + '@voidzero-dev/vite-plus-test@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 @@ -2682,7 +2799,7 @@ snapshots: tinybench: 2.9.0 tinyexec: 1.2.2 tinyglobby: 0.2.16 - vite: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' + vite: '@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3)' ws: 8.21.0 optionalDependencies: '@types/node': 24.12.4 @@ -2929,12 +3046,18 @@ snapshots: slice-ansi: 9.0.0 string-width: 8.2.1 + code-excerpt@4.0.0: + dependencies: + convert-to-spaces: 2.0.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 convert-source-map@2.0.0: {} + convert-to-spaces@2.0.1: {} + csstype@3.2.3: {} debug@4.4.3: @@ -3005,6 +3128,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@2.0.0: {} + estree-walker@2.0.2: {} event-target-shim@5.0.1: {} @@ -3302,6 +3427,10 @@ snapshots: source-map-js@1.2.1: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + std-env@4.1.0: {} string-width@8.2.1: @@ -3403,12 +3532,12 @@ snapshots: - vite - yaml - vite-plus@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3): + vite-plus@0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3): dependencies: '@oxc-project/types': 0.129.0 '@oxlint/plugins': 1.61.0 '@voidzero-dev/vite-plus-core': 0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3) - '@voidzero-dev/vite-plus-test': 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3) + '@voidzero-dev/vite-plus-test': 0.1.22(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.1.23(@types/node@24.12.4)(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3))(esbuild@0.28.0)(tsx@4.22.3)(typescript@6.0.3) oxfmt: 0.48.0 oxlint: 1.63.0(oxlint-tsgolint@0.22.1) oxlint-tsgolint: 0.22.1