feat: exit() accepts result value, waitUntilExit resolves with it
Change exit(error?: Error) to exit(errorOrResult?: unknown). When an
Error is passed, waitUntilExit rejects; otherwise it resolves with the
value. This matches Ink's behavior where exit("hello") resolves the
promise with "hello".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,10 +82,9 @@ test("exit() called multiple times is idempotent", async () => {
|
|||||||
await waitUntilExit();
|
await waitUntilExit();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("waitUntilExit() resolves after exit() with result value", async () => {
|
test("waitUntilExit() resolves with result value passed to exit()", async () => {
|
||||||
// Mirrors Ink's "exit on exit() with result value" — vue-tui's exit()
|
// Mirrors Ink's "exit on exit() with result value"
|
||||||
// resolves cleanly (no value); just verifies the promise resolves.
|
let exitFn!: (errorOrResult?: unknown) => void;
|
||||||
let exitFn!: () => void;
|
|
||||||
|
|
||||||
const App = defineComponent(() => {
|
const App = defineComponent(() => {
|
||||||
exitFn = useExit();
|
exitFn = useExit();
|
||||||
@@ -95,6 +94,34 @@ test("waitUntilExit() resolves after exit() with result value", async () => {
|
|||||||
const { lastFrame, waitUntilExit } = await render(App);
|
const { lastFrame, waitUntilExit } = await render(App);
|
||||||
expect(lastFrame()).toContain("hello from vue-tui");
|
expect(lastFrame()).toContain("hello from vue-tui");
|
||||||
|
|
||||||
|
exitFn("hello from ink");
|
||||||
|
await expect(waitUntilExit()).resolves.toBe("hello from ink");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitUntilExit() resolves with object result value", async () => {
|
||||||
|
// Mirrors Ink's "exit on exit() with object result"
|
||||||
|
let exitFn!: (errorOrResult?: unknown) => void;
|
||||||
|
|
||||||
|
const App = defineComponent(() => {
|
||||||
|
exitFn = useExit();
|
||||||
|
return () => <Text>hello</Text>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { waitUntilExit } = await render(App);
|
||||||
|
const resultObj = { message: "hello from ink object" };
|
||||||
|
exitFn(resultObj);
|
||||||
|
await expect(waitUntilExit()).resolves.toBe(resultObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("waitUntilExit() resolves with undefined when exit() called with no args", async () => {
|
||||||
|
let exitFn!: (errorOrResult?: unknown) => void;
|
||||||
|
|
||||||
|
const App = defineComponent(() => {
|
||||||
|
exitFn = useExit();
|
||||||
|
return () => <Text>hello</Text>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { waitUntilExit } = await render(App);
|
||||||
exitFn();
|
exitFn();
|
||||||
await expect(waitUntilExit()).resolves.toBeUndefined();
|
await expect(waitUntilExit()).resolves.toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { AppContextKey } from "../context.ts";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the exit function so a component can end the app from inside the
|
* Returns the exit function so a component can end the app from inside the
|
||||||
* tree. Pass an Error to reject `app.waitUntilExit()` (and any awaiter); call
|
* tree. Pass an Error to reject `app.waitUntilExit()` (and any awaiter); pass
|
||||||
* with no args to resolve cleanly.
|
* any other value to resolve with it as the result; call with no args to
|
||||||
|
* resolve with undefined.
|
||||||
*/
|
*/
|
||||||
export function useExit(): (error?: Error) => void {
|
export function useExit(): (errorOrResult?: unknown) => void {
|
||||||
const ctx = inject(AppContextKey);
|
const ctx = inject(AppContextKey);
|
||||||
if (!ctx) throw new Error("useExit() must be called inside a vue-tui render tree");
|
if (!ctx) throw new Error("useExit() must be called inside a vue-tui render tree");
|
||||||
return ctx.exit;
|
return ctx.exit;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { InjectionKey, ShallowRef } from "vue";
|
|||||||
import type { EventEmitter } from "node:events";
|
import type { EventEmitter } from "node:events";
|
||||||
|
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
exit: (error?: Error) => void;
|
exit: (errorOrResult?: unknown) => void;
|
||||||
stdout: NodeJS.WriteStream;
|
stdout: NodeJS.WriteStream;
|
||||||
stderr: NodeJS.WriteStream;
|
stderr: NodeJS.WriteStream;
|
||||||
stdin: NodeJS.ReadStream;
|
stdin: NodeJS.ReadStream;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface MountOptions {
|
|||||||
|
|
||||||
export interface TuiApp extends Omit<VueApp<TuiNode>, "mount"> {
|
export interface TuiApp extends Omit<VueApp<TuiNode>, "mount"> {
|
||||||
mount(options?: MountOptions): ComponentPublicInstance;
|
mount(options?: MountOptions): ComponentPublicInstance;
|
||||||
waitUntilExit(): Promise<void>;
|
waitUntilExit(): Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type RootProps = Record<string, unknown>;
|
type RootProps = Record<string, unknown>;
|
||||||
@@ -39,9 +39,9 @@ type RootProps = Record<string, unknown>;
|
|||||||
export function createApp(root: Component, rootProps?: RootProps | null): TuiApp {
|
export function createApp(root: Component, rootProps?: RootProps | null): TuiApp {
|
||||||
// exit promise — created at createApp time so waitUntilExit() works even
|
// exit promise — created at createApp time so waitUntilExit() works even
|
||||||
// before mount (it just hangs until mount + exit).
|
// before mount (it just hangs until mount + exit).
|
||||||
let exitResolve!: () => void;
|
let exitResolve!: (result?: unknown) => void;
|
||||||
let exitReject!: (e: Error) => void;
|
let exitReject!: (e: Error) => void;
|
||||||
const exitPromise = new Promise<void>((res, rej) => {
|
const exitPromise = new Promise<unknown>((res, rej) => {
|
||||||
exitResolve = res;
|
exitResolve = res;
|
||||||
exitReject = rej;
|
exitReject = rej;
|
||||||
});
|
});
|
||||||
@@ -120,14 +120,17 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
|||||||
mountedDebug = debug;
|
mountedDebug = debug;
|
||||||
|
|
||||||
const appContext: AppContext = {
|
const appContext: AppContext = {
|
||||||
exit(err?: Error) {
|
exit(errorOrResult?: unknown) {
|
||||||
if (err) exitReject(err);
|
|
||||||
// Defer teardown to a microtask: exit() is frequently called from
|
// Defer teardown to a microtask: exit() is frequently called from
|
||||||
// inside the Vue update cycle (useInput handler, setup(), errorHandler)
|
// inside the Vue update cycle (useInput handler, setup(), errorHandler)
|
||||||
// and unmounting synchronously would tear Vue down mid-flush.
|
// and unmounting synchronously would tear Vue down mid-flush.
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
teardown();
|
teardown();
|
||||||
exitResolve();
|
if (errorOrResult instanceof Error) {
|
||||||
|
exitReject(errorOrResult);
|
||||||
|
} else {
|
||||||
|
exitResolve(errorOrResult);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
stdout,
|
stdout,
|
||||||
@@ -238,7 +241,7 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
|||||||
exitResolve();
|
exitResolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
app.waitUntilExit = function waitUntilExit(): Promise<void> {
|
app.waitUntilExit = function waitUntilExit(): Promise<unknown> {
|
||||||
return exitPromise;
|
return exitPromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface RenderResult {
|
|||||||
};
|
};
|
||||||
terminal: Terminal;
|
terminal: Terminal;
|
||||||
unmount(this: void): void;
|
unmount(this: void): void;
|
||||||
waitUntilExit(this: void): Promise<void>;
|
waitUntilExit(this: void): Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimFrame(raw: string): string {
|
function trimFrame(raw: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user