chore: add tests

This commit is contained in:
hypercross 2026-04-04 00:48:44 +08:00
parent 3395a315a6
commit 32509d7812
9 changed files with 1076 additions and 202 deletions

View File

@ -9,9 +9,12 @@
"preview": "pnpm --filter sample-game preview" "preview": "pnpm --filter sample-game preview"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5.3.3" "typescript": "^5.3.3",
"vitest": "^3.2.4"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": ["esbuild"] "onlyBuiltDependencies": [
"esbuild"
]
} }
} }

View File

@ -13,7 +13,9 @@
}, },
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
}, },
"peerDependencies": { "peerDependencies": {
"@preact/signals-core": "^1.5.1", "@preact/signals-core": "^1.5.1",
@ -29,6 +31,7 @@
"phaser": "^3.80.1", "phaser": "^3.80.1",
"preact": "^10.19.3", "preact": "^10.19.3",
"tsup": "^8.0.2", "tsup": "^8.0.2",
"typescript": "^5.3.3" "typescript": "^5.3.3",
"vitest": "^3.2.4"
} }
} }

View File

@ -0,0 +1,286 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { InputMapper, createInputMapper, type InputMapperOptions } from './InputMapper.js';
import Phaser from 'phaser';
// Mock Phaser
function createMockScene() {
const inputEvents = new Map<string, Function[]>();
const mockScene = {
input: {
on: vi.fn((event: string, callback: Function) => {
if (!inputEvents.has(event)) {
inputEvents.set(event, []);
}
inputEvents.get(event)!.push(callback);
}),
off: vi.fn((event: string, callback: Function) => {
const handlers = inputEvents.get(event);
if (handlers) {
const index = handlers.indexOf(callback);
if (index > -1) {
handlers.splice(index, 1);
}
}
}),
_emit: (event: string, ...args: any[]) => {
const handlers = inputEvents.get(event);
if (handlers) {
handlers.forEach(handler => handler(...args));
}
},
},
} as unknown as Phaser.Scene;
return {
mockScene,
inputEvents,
emitPointerDown: (pointer: Partial<Phaser.Input.Pointer>) => {
inputEvents.get('pointerdown')?.forEach(handler => handler(pointer));
},
};
}
function createMockGameObject() {
const events = new Map<string, Function[]>();
const mockObj = {
setInteractive: vi.fn(),
on: vi.fn((event: string, handler: Function) => {
if (!events.has(event)) {
events.set(event, []);
}
events.get(event)!.push(handler);
}),
off: vi.fn((event: string, handler: Function) => {
const handlers = events.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
}
}),
_emit: (event: string, ...args: any[]) => {
events.get(event)?.forEach(handler => handler(...args));
},
} as unknown as Phaser.GameObjects.GameObject;
return {
mockObj,
emitPointerDown: () => events.get('pointerdown')?.forEach(handler => handler()),
};
}
describe('InputMapper', () => {
let options: InputMapperOptions;
let onSubmit: ReturnType<typeof vi.fn>;
beforeEach(() => {
onSubmit = vi.fn();
options = {
onSubmit,
};
});
describe('constructor', () => {
it('should create instance with options', () => {
const { mockScene } = createMockScene();
const mapper = new InputMapper(mockScene, options);
expect(mapper).toBeDefined();
});
});
describe('createInputMapper', () => {
it('should create instance via factory', () => {
const { mockScene } = createMockScene();
const mapper = createInputMapper(mockScene, options);
expect(mapper).toBeDefined();
});
});
describe('mapGridClick', () => {
it('should register pointerdown listener', () => {
const { mockScene, inputEvents } = createMockScene();
const mapper = new InputMapper(mockScene, options);
mapper.mapGridClick(
{ x: 50, y: 50 },
{ x: 100, y: 100 },
{ cols: 3, rows: 3 },
vi.fn(() => null),
);
expect(mockScene.input.on).toHaveBeenCalledWith('pointerdown', expect.any(Function));
});
it('should call onSubmit with command from onCellClick', () => {
const { mockScene, emitPointerDown } = createMockScene();
const mapper = new InputMapper(mockScene, options);
const onCellClick = vi.fn((col, row) => `place ${col} ${row}`);
mapper.mapGridClick(
{ x: 50, y: 50 },
{ x: 100, y: 100 },
{ cols: 3, rows: 3 },
onCellClick,
);
// Simulate pointer at position (120, 130)
// Local: (120-100, 130-100) = (20, 30)
// Cell: (floor(20/50), floor(30/50)) = (0, 0)
emitPointerDown({ x: 120, y: 130 });
expect(onCellClick).toHaveBeenCalledWith(0, 0);
expect(onSubmit).toHaveBeenCalledWith('place 0 0');
});
it('should ignore clicks outside grid', () => {
const { mockScene, emitPointerDown } = createMockScene();
const mapper = new InputMapper(mockScene, options);
const onCellClick = vi.fn(() => 'click');
mapper.mapGridClick(
{ x: 50, y: 50 },
{ x: 100, y: 100 },
{ cols: 3, rows: 3 },
onCellClick,
);
// Click outside grid (negative local coordinates)
emitPointerDown({ x: 50, y: 50 });
expect(onCellClick).not.toHaveBeenCalled();
expect(onSubmit).not.toHaveBeenCalled();
});
it('should ignore clicks outside grid bounds', () => {
const { mockScene, emitPointerDown } = createMockScene();
const mapper = new InputMapper(mockScene, options);
const onCellClick = vi.fn(() => 'click');
mapper.mapGridClick(
{ x: 50, y: 50 },
{ x: 100, y: 100 },
{ cols: 3, rows: 3 },
onCellClick,
);
// Click at col 5 (beyond cols 0-2)
// Local X: 300-100 = 200, col = floor(200/50) = 4
emitPointerDown({ x: 300, y: 120 });
expect(onCellClick).not.toHaveBeenCalled();
});
it('should not call onSubmit when onCellClick returns null', () => {
const { mockScene, emitPointerDown } = createMockScene();
const mapper = new InputMapper(mockScene, options);
const onCellClick = vi.fn(() => null);
mapper.mapGridClick(
{ x: 50, y: 50 },
{ x: 100, y: 100 },
{ cols: 3, rows: 3 },
onCellClick,
);
emitPointerDown({ x: 120, y: 130 });
expect(onCellClick).toHaveBeenCalled();
expect(onSubmit).not.toHaveBeenCalled();
});
});
describe('mapObjectClick', () => {
it('should set game objects as interactive', () => {
const { mockScene } = createMockScene();
const { mockObj: obj1 } = createMockGameObject();
const { mockObj: obj2 } = createMockGameObject();
const mapper = new InputMapper(mockScene, options);
mapper.mapObjectClick([obj1, obj2], vi.fn(() => null));
expect(obj1.setInteractive).toHaveBeenCalledWith({ useHandCursor: true });
expect(obj2.setInteractive).toHaveBeenCalledWith({ useHandCursor: true });
});
it('should call onSubmit when object is clicked', () => {
const { mockScene } = createMockScene();
const { mockObj, emitPointerDown } = createMockGameObject();
const onClick = vi.fn((obj) => `clicked ${obj}`);
const mapper = new InputMapper(mockScene, options);
mapper.mapObjectClick([mockObj], onClick);
emitPointerDown();
expect(onClick).toHaveBeenCalledWith(mockObj);
expect(onSubmit).toHaveBeenCalledWith(`clicked ${mockObj}`);
});
it('should skip objects without setInteractive', () => {
const { mockScene } = createMockScene();
const nonInteractiveObj = {} as Phaser.GameObjects.GameObject;
const mapper = new InputMapper(mockScene, options);
// Should not throw
expect(() => {
mapper.mapObjectClick([nonInteractiveObj], vi.fn());
}).not.toThrow();
});
it('should not call onSubmit when onClick returns null', () => {
const { mockScene } = createMockScene();
const { mockObj, emitPointerDown } = createMockGameObject();
const onClick = vi.fn(() => null);
const mapper = new InputMapper(mockScene, options);
mapper.mapObjectClick([mockObj], onClick);
emitPointerDown();
expect(onClick).toHaveBeenCalled();
expect(onSubmit).not.toHaveBeenCalled();
});
});
describe('destroy', () => {
it('should remove pointerdown listener from mapGridClick', () => {
const { mockScene } = createMockScene();
const mapper = new InputMapper(mockScene, options);
mapper.mapGridClick(
{ x: 50, y: 50 },
{ x: 100, y: 100 },
{ cols: 3, rows: 3 },
vi.fn(() => null),
);
mapper.destroy();
expect(mockScene.input.off).toHaveBeenCalledWith('pointerdown', expect.any(Function));
});
it('should remove object listeners from mapObjectClick', () => {
const { mockScene } = createMockScene();
const { mockObj } = createMockGameObject();
const mapper = new InputMapper(mockScene, options);
mapper.mapObjectClick([mockObj], vi.fn(() => null));
mapper.destroy();
expect(mockObj.off).toHaveBeenCalledWith('pointerdown', expect.any(Function));
});
it('should be safe to call without any mappings', () => {
const { mockScene } = createMockScene();
const mapper = new InputMapper(mockScene, options);
expect(() => mapper.destroy()).not.toThrow();
});
});
});

View File

@ -0,0 +1,93 @@
import Phaser from 'phaser';
export interface InputMapperOptions {
onSubmit: (input: string) => string | null;
}
export class InputMapper {
private scene: Phaser.Scene;
private onSubmit: (input: string) => string | null;
private pointerDownCallback: ((pointer: Phaser.Input.Pointer) => void) | null = null;
/** Track interactive objects registered via mapObjectClick for cleanup */
private trackedObjects: Array<{
obj: Phaser.GameObjects.GameObject;
handler: () => void;
}> = [];
constructor(scene: Phaser.Scene, options: InputMapperOptions) {
this.scene = scene;
this.onSubmit = options.onSubmit;
}
mapGridClick(
cellSize: { x: number; y: number },
offset: { x: number; y: number },
gridDimensions: { cols: number; rows: number },
onCellClick: (col: number, row: number) => string | null,
): void {
const pointerDown = (pointer: Phaser.Input.Pointer) => {
const localX = pointer.x - offset.x;
const localY = pointer.y - offset.y;
if (localX < 0 || localY < 0) return;
const col = Math.floor(localX / cellSize.x);
const row = Math.floor(localY / cellSize.y);
if (col < 0 || col >= gridDimensions.cols || row < 0 || row >= gridDimensions.rows) return;
const cmd = onCellClick(col, row);
if (cmd) {
this.onSubmit(cmd);
}
};
this.pointerDownCallback = pointerDown;
this.scene.input.on('pointerdown', pointerDown);
}
mapObjectClick<T>(
gameObjects: Phaser.GameObjects.GameObject[],
onClick: (obj: T) => string | null,
): void {
for (const obj of gameObjects) {
if ('setInteractive' in obj && typeof (obj as any).setInteractive === 'function') {
const interactiveObj = obj as any;
interactiveObj.setInteractive({ useHandCursor: true });
const handler = () => {
const cmd = onClick(obj as unknown as T);
if (cmd) {
this.onSubmit(cmd);
}
};
interactiveObj.on('pointerdown', handler);
this.trackedObjects.push({ obj, handler });
}
}
}
destroy(): void {
// Remove global pointerdown listener from mapGridClick
if (this.pointerDownCallback) {
this.scene.input.off('pointerdown', this.pointerDownCallback);
this.pointerDownCallback = null;
}
// Remove per-object pointerdown listeners from mapObjectClick
for (const { obj, handler } of this.trackedObjects) {
if ('off' in obj && typeof (obj as any).off === 'function') {
(obj as any).off('pointerdown', handler);
}
}
this.trackedObjects = [];
}
}
export function createInputMapper(
scene: Phaser.Scene,
options: InputMapperOptions,
): InputMapper {
return new InputMapper(scene, options);
}

View File

@ -0,0 +1,270 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { PromptHandler, type PromptHandlerOptions } from './PromptHandler.js';
import type { PromptEvent } from 'boardgame-core';
// 内联 AsyncQueue 实现用于测试(避免导入 boardgame-core 内部模块)
class AsyncQueue<T> {
private items: T[] = [];
private resolvers: ((value: T) => void)[] = [];
push(item: T): void {
if (this.resolvers.length > 0) {
const resolve = this.resolvers.shift()!;
resolve(item);
} else {
this.items.push(item);
}
}
pushAll(items: Iterable<T>): void {
for (const item of items) {
this.push(item);
}
}
async pop(): Promise<T> {
if (this.items.length > 0) {
return this.items.shift()!;
}
return new Promise<T>((resolve) => {
this.resolvers.push(resolve);
});
}
get length(): number {
return this.items.length - this.resolvers.length;
}
}
// Mock types
interface MockCommandContext {
commands: {
promptQueue: AsyncQueue<PromptEvent>;
};
}
function createMockPromptEvent(
schema: any = { name: 'test', params: [] },
tryCommitImpl?: (input: string | any) => string | null,
): PromptEvent {
return {
schema,
tryCommit: tryCommitImpl ?? vi.fn(() => null),
cancel: vi.fn(),
};
}
function createMockCommands(): MockCommandContext['commands'] {
return {
promptQueue: new AsyncQueue<PromptEvent>(),
};
}
describe('PromptHandler', () => {
let options: PromptHandlerOptions;
let mockCommands: MockCommandContext['commands'];
let onPrompt: ReturnType<typeof vi.fn>;
let onCancel: ReturnType<typeof vi.fn>;
beforeEach(() => {
onPrompt = vi.fn();
onCancel = vi.fn();
mockCommands = createMockCommands();
options = {
commands: mockCommands as any,
onPrompt,
onCancel,
};
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('constructor', () => {
it('should create instance with options', () => {
const handler = new PromptHandler(options);
expect(handler).toBeDefined();
});
});
describe('createPromptHandler', () => {
it('should create instance via factory', async () => {
const { createPromptHandler } = await import('./PromptHandler.js');
const handler = createPromptHandler(options);
expect(handler).toBeDefined();
});
});
describe('start', () => {
it('should listen for prompts when started', async () => {
const handler = new PromptHandler(options);
handler.start();
const mockEvent = createMockPromptEvent();
mockCommands.promptQueue.push(mockEvent);
// Wait for async operation
await new Promise(resolve => setTimeout(resolve, 10));
expect(onPrompt).toHaveBeenCalledWith(mockEvent);
});
it('should not listen when not started', async () => {
const handler = new PromptHandler(options);
// Don't call start()
const mockEvent = createMockPromptEvent();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
expect(onPrompt).not.toHaveBeenCalled();
});
});
describe('submit', () => {
it('should submit to active prompt successfully', async () => {
const handler = new PromptHandler(options);
handler.start();
const mockEvent = createMockPromptEvent();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
const result = handler.submit('test input');
expect(mockEvent.tryCommit).toHaveBeenCalledWith('test input');
expect(result).toBeNull();
});
it('should return error when submit fails', async () => {
const mockEvent = createMockPromptEvent(
{ name: 'test', params: [] },
vi.fn(() => 'Invalid input'),
);
const handler = new PromptHandler(options);
handler.start();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
const result = handler.submit('bad input');
expect(result).toBe('Invalid input');
expect(onPrompt).toHaveBeenCalledWith(mockEvent);
});
it('should store input as pending when no active prompt', () => {
const handler = new PromptHandler(options);
// Don't start, so no active prompt
const result = handler.submit('pending input');
expect(result).toBeNull();
});
it('should auto-submit pending input when new prompt arrives', async () => {
const tryCommitFn = vi.fn(() => null);
const mockEvent = createMockPromptEvent(
{ name: 'test', params: [] },
tryCommitFn,
);
const handler = new PromptHandler(options);
// Submit before starting (pending input)
handler.submit('pending input');
// Start and push event
handler.start();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
expect(tryCommitFn).toHaveBeenCalledWith('pending input');
expect(onPrompt).not.toHaveBeenCalled(); // Auto-submitted, no UI needed
});
});
describe('cancel', () => {
it('should cancel active prompt', async () => {
const mockEvent = createMockPromptEvent();
const handler = new PromptHandler(options);
handler.start();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
handler.cancel('user cancelled');
expect(mockEvent.cancel).toHaveBeenCalledWith('user cancelled');
expect(onCancel).toHaveBeenCalledWith('user cancelled');
});
it('should call onCancel without active prompt', () => {
const handler = new PromptHandler(options);
handler.cancel('no prompt');
expect(onCancel).toHaveBeenCalledWith('no prompt');
});
});
describe('stop', () => {
it('should stop listening for new prompts after stop', async () => {
const handler = new PromptHandler(options);
handler.start();
// Push first event, it should be handled
const mockEvent1 = createMockPromptEvent();
mockCommands.promptQueue.push(mockEvent1);
await new Promise(resolve => setTimeout(resolve, 10));
expect(onPrompt).toHaveBeenCalledWith(mockEvent1);
onPrompt.mockClear();
// Now stop
handler.stop();
// Push another event, should not be handled
const mockEvent2 = createMockPromptEvent();
mockCommands.promptQueue.push(mockEvent2);
await new Promise(resolve => setTimeout(resolve, 10));
// The second event should not trigger onPrompt
// (Note: due to async nature, we check it wasn't called AFTER stop)
expect(onPrompt).not.toHaveBeenCalledWith(mockEvent2);
});
it('should cancel active prompt on stop', async () => {
const mockEvent = createMockPromptEvent();
const handler = new PromptHandler(options);
handler.start();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
handler.stop();
expect(mockEvent.cancel).toHaveBeenCalledWith('Handler stopped');
});
});
describe('destroy', () => {
it('should call stop', async () => {
const mockEvent = createMockPromptEvent();
const handler = new PromptHandler(options);
handler.start();
mockCommands.promptQueue.push(mockEvent);
await new Promise(resolve => setTimeout(resolve, 10));
handler.destroy();
expect(mockEvent.cancel).toHaveBeenCalledWith('Handler stopped');
});
});
});

View File

@ -0,0 +1,102 @@
import type { IGameContext, PromptEvent } from 'boardgame-core';
export interface PromptHandlerOptions {
commands: IGameContext<any>['commands'];
onPrompt: (prompt: PromptEvent) => void;
onCancel: (reason?: string) => void;
}
export class PromptHandler {
private commands: IGameContext<any>['commands'];
private onPrompt: (prompt: PromptEvent) => void;
private onCancel: (reason?: string) => void;
private activePrompt: PromptEvent | null = null;
private isListening = false;
private pendingInput: string | null = null;
constructor(options: PromptHandlerOptions) {
this.commands = options.commands;
this.onPrompt = options.onPrompt;
this.onCancel = options.onCancel;
}
start(): void {
this.isListening = true;
this.listenForPrompt();
}
private listenForPrompt(): void {
if (!this.isListening) return;
this.commands.promptQueue.pop()
.then((promptEvent) => {
this.activePrompt = promptEvent;
// 如果有等待的输入,自动提交
if (this.pendingInput) {
const input = this.pendingInput;
this.pendingInput = null;
const error = this.activePrompt.tryCommit(input);
if (error === null) {
this.activePrompt = null;
this.listenForPrompt();
} else {
// 提交失败,把 prompt 交给 UI 显示错误
this.onPrompt(promptEvent);
}
return;
}
this.onPrompt(promptEvent);
})
.catch((reason) => {
this.activePrompt = null;
this.onCancel(reason?.message || 'Cancelled');
});
}
/**
* Submit an input string to the current prompt.
* @returns null on success (input accepted), error string on validation failure
*/
submit(input: string): string | null {
if (!this.activePrompt) {
// 没有活跃 prompt保存为待处理输入
this.pendingInput = input;
return null;
}
const error = this.activePrompt.tryCommit(input);
if (error === null) {
this.activePrompt = null;
this.listenForPrompt();
}
return error;
}
cancel(reason?: string): void {
if (this.activePrompt) {
this.activePrompt.cancel(reason);
this.activePrompt = null;
}
this.onCancel(reason);
}
stop(): void {
this.isListening = false;
if (this.activePrompt) {
this.activePrompt.cancel('Handler stopped');
this.activePrompt = null;
}
}
destroy(): void {
this.stop();
}
}
export function createPromptHandler(
options: PromptHandlerOptions,
): PromptHandler {
return new PromptHandler(options);
}

View File

@ -1,199 +1,11 @@
import Phaser from 'phaser'; export {
import type { IGameContext, PromptEvent } from 'boardgame-core'; InputMapper,
createInputMapper,
type InputMapperOptions,
} from './InputMapper.js';
// ─── InputMapper ─────────────────────────────────────────────────────────── export {
PromptHandler,
export interface InputMapperOptions { createPromptHandler,
onSubmit: (input: string) => string | null; type PromptHandlerOptions,
} } from './PromptHandler.js';
export class InputMapper {
private scene: Phaser.Scene;
private onSubmit: (input: string) => string | null;
private pointerDownCallback: ((pointer: Phaser.Input.Pointer) => void) | null = null;
/** Track interactive objects registered via mapObjectClick for cleanup */
private trackedObjects: Array<{
obj: Phaser.GameObjects.GameObject;
handler: () => void;
}> = [];
constructor(scene: Phaser.Scene, options: InputMapperOptions) {
this.scene = scene;
this.onSubmit = options.onSubmit;
}
mapGridClick(
cellSize: { x: number; y: number },
offset: { x: number; y: number },
gridDimensions: { cols: number; rows: number },
onCellClick: (col: number, row: number) => string | null,
): void {
const pointerDown = (pointer: Phaser.Input.Pointer) => {
const localX = pointer.x - offset.x;
const localY = pointer.y - offset.y;
if (localX < 0 || localY < 0) return;
const col = Math.floor(localX / cellSize.x);
const row = Math.floor(localY / cellSize.y);
if (col < 0 || col >= gridDimensions.cols || row < 0 || row >= gridDimensions.rows) return;
const cmd = onCellClick(col, row);
if (cmd) {
this.onSubmit(cmd);
}
};
this.pointerDownCallback = pointerDown;
this.scene.input.on('pointerdown', pointerDown);
}
mapObjectClick<T>(
gameObjects: Phaser.GameObjects.GameObject[],
onClick: (obj: T) => string | null,
): void {
for (const obj of gameObjects) {
if ('setInteractive' in obj && typeof (obj as any).setInteractive === 'function') {
const interactiveObj = obj as any;
interactiveObj.setInteractive({ useHandCursor: true });
const handler = () => {
const cmd = onClick(obj as unknown as T);
if (cmd) {
this.onSubmit(cmd);
}
};
interactiveObj.on('pointerdown', handler);
this.trackedObjects.push({ obj, handler });
}
}
}
destroy(): void {
// Remove global pointerdown listener from mapGridClick
if (this.pointerDownCallback) {
this.scene.input.off('pointerdown', this.pointerDownCallback);
this.pointerDownCallback = null;
}
// Remove per-object pointerdown listeners from mapObjectClick
for (const { obj, handler } of this.trackedObjects) {
if ('off' in obj && typeof (obj as any).off === 'function') {
(obj as any).off('pointerdown', handler);
}
}
this.trackedObjects = [];
}
}
export function createInputMapper(
scene: Phaser.Scene,
options: InputMapperOptions,
): InputMapper {
return new InputMapper(scene, options);
}
// ─── PromptHandler ─────────────────────────────────────────────────────────
export interface PromptHandlerOptions {
commands: IGameContext<any>['commands'];
onPrompt: (prompt: PromptEvent) => void;
onCancel: (reason?: string) => void;
}
export class PromptHandler {
private commands: IGameContext<any>['commands'];
private onPrompt: (prompt: PromptEvent) => void;
private onCancel: (reason?: string) => void;
private activePrompt: PromptEvent | null = null;
private isListening = false;
private pendingInput: string | null = null;
constructor(options: PromptHandlerOptions) {
this.commands = options.commands;
this.onPrompt = options.onPrompt;
this.onCancel = options.onCancel;
}
start(): void {
this.isListening = true;
this.listenForPrompt();
}
private listenForPrompt(): void {
if (!this.isListening) return;
this.commands.promptQueue.pop()
.then((promptEvent) => {
this.activePrompt = promptEvent;
// 如果有等待的输入,自动提交
if (this.pendingInput) {
const input = this.pendingInput;
this.pendingInput = null;
const error = this.activePrompt.tryCommit(input);
if (error === null) {
this.activePrompt = null;
this.listenForPrompt();
} else {
// 提交失败,把 prompt 交给 UI 显示错误
this.onPrompt(promptEvent);
}
return;
}
this.onPrompt(promptEvent);
})
.catch((reason) => {
this.activePrompt = null;
this.onCancel(reason?.message || 'Cancelled');
});
}
/**
* Submit an input string to the current prompt.
* @returns null on success (input accepted), error string on validation failure
*/
submit(input: string): string | null {
if (!this.activePrompt) {
// 没有活跃 prompt保存为待处理输入
this.pendingInput = input;
return null;
}
const error = this.activePrompt.tryCommit(input);
if (error === null) {
this.activePrompt = null;
this.listenForPrompt();
}
return error;
}
cancel(reason?: string): void {
if (this.activePrompt) {
this.activePrompt.cancel(reason);
this.activePrompt = null;
}
this.onCancel(reason);
}
stop(): void {
this.isListening = false;
if (this.activePrompt) {
this.activePrompt.cancel('Handler stopped');
this.activePrompt = null;
}
}
destroy(): void {
this.stop();
}
}
export function createPromptHandler(
options: PromptHandlerOptions,
): PromptHandler {
return new PromptHandler(options);
}

View File

@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.test.ts'],
},
});

View File

@ -11,6 +11,9 @@ importers:
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.9.3 version: 5.9.3
vitest:
specifier: ^3.2.4
version: 3.2.4(lightningcss@1.32.0)
packages/framework: packages/framework:
devDependencies: devDependencies:
@ -35,6 +38,9 @@ importers:
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.9.3 version: 5.9.3
vitest:
specifier: ^3.2.4
version: 3.2.4(lightningcss@1.32.0)
packages/sample-game: packages/sample-game:
dependencies: dependencies:
@ -749,9 +755,44 @@ packages:
peerDependencies: peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8 vite: ^5.2.0 || ^6 || ^7 || ^8
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@vitest/expect@3.2.4':
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
'@vitest/mocker@3.2.4':
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
peerDependencies:
msw: ^2.4.9
vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
peerDependenciesMeta:
msw:
optional: true
vite:
optional: true
'@vitest/pretty-format@3.2.4':
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
'@vitest/runner@3.2.4':
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
'@vitest/snapshot@3.2.4':
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
'@vitest/spy@3.2.4':
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
acorn@8.16.0: acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@ -760,6 +801,10 @@ packages:
any-promise@1.3.0: any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
babel-plugin-transform-hook-names@1.0.2: babel-plugin-transform-hook-names@1.0.2:
resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==}
peerDependencies: peerDependencies:
@ -791,6 +836,14 @@ packages:
caniuse-lite@1.0.30001784: caniuse-lite@1.0.30001784:
resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==} resolution: {integrity: sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'}
check-error@2.1.3:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@ -825,6 +878,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
detect-libc@2.1.2: detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -853,6 +910,9 @@ packages:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
es-module-lexer@1.7.0:
resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==}
esbuild@0.21.5: esbuild@0.21.5:
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -870,9 +930,16 @@ packages:
estree-walker@2.0.2: estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
estree-walker@3.0.3:
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
eventemitter3@5.0.4: eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
expect-type@1.3.0:
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
fdir@6.5.0: fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@ -912,6 +979,9 @@ packages:
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
jsesc@3.1.0: jsesc@3.1.0:
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -1010,6 +1080,9 @@ packages:
resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lru-cache@5.1.1: lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@ -1050,6 +1123,10 @@ packages:
pathe@2.0.3: pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
pathval@2.0.1:
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
engines: {node: '>= 14.16'}
phaser@3.90.0: phaser@3.90.0:
resolution: {integrity: sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==} resolution: {integrity: sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==}
@ -1113,6 +1190,9 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
simple-code-frame@1.3.0: simple-code-frame@1.3.0:
resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==}
@ -1128,6 +1208,15 @@ packages:
resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==}
engines: {node: '>=16'} engines: {node: '>=16'}
stackback@0.0.2:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
strip-literal@3.1.0:
resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==}
sucrase@3.35.1: sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
@ -1147,6 +1236,9 @@ packages:
thenify@3.3.1: thenify@3.3.1:
resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==}
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyexec@0.3.2: tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
@ -1154,6 +1246,18 @@ packages:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
tinypool@1.1.1:
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
engines: {node: ^18.0.0 || >=20.0.0}
tinyrainbow@2.0.0:
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
engines: {node: '>=14.0.0'}
tinyspy@4.0.4:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
tree-kill@1.2.2: tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true hasBin: true
@ -1194,6 +1298,11 @@ packages:
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>= 4.21.0'
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite-prerender-plugin@0.5.13: vite-prerender-plugin@0.5.13:
resolution: {integrity: sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==} resolution: {integrity: sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==}
peerDependencies: peerDependencies:
@ -1230,6 +1339,39 @@ packages:
terser: terser:
optional: true optional: true
vitest@3.2.4:
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@types/debug': ^4.1.12
'@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0
'@vitest/browser': 3.2.4
'@vitest/ui': 3.2.4
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
'@edge-runtime/vm':
optional: true
'@types/debug':
optional: true
'@types/node':
optional: true
'@vitest/browser':
optional: true
'@vitest/ui':
optional: true
happy-dom:
optional: true
jsdom:
optional: true
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
hasBin: true
yallist@3.1.1: yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
@ -1730,12 +1872,63 @@ snapshots:
tailwindcss: 4.2.2 tailwindcss: 4.2.2
vite: 5.4.21(lightningcss@1.32.0) vite: 5.4.21(lightningcss@1.32.0)
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
assertion-error: 2.0.1
'@types/deep-eql@4.0.2': {}
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@vitest/expect@3.2.4':
dependencies:
'@types/chai': 5.2.3
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@5.4.21(lightningcss@1.32.0))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 5.4.21(lightningcss@1.32.0)
'@vitest/pretty-format@3.2.4':
dependencies:
tinyrainbow: 2.0.0
'@vitest/runner@3.2.4':
dependencies:
'@vitest/utils': 3.2.4
pathe: 2.0.3
strip-literal: 3.1.0
'@vitest/snapshot@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
magic-string: 0.30.21
pathe: 2.0.3
'@vitest/spy@3.2.4':
dependencies:
tinyspy: 4.0.4
'@vitest/utils@3.2.4':
dependencies:
'@vitest/pretty-format': 3.2.4
loupe: 3.2.1
tinyrainbow: 2.0.0
acorn@8.16.0: {} acorn@8.16.0: {}
any-promise@1.3.0: {} any-promise@1.3.0: {}
assertion-error@2.0.1: {}
babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0): babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0):
dependencies: dependencies:
'@babel/core': 7.29.0 '@babel/core': 7.29.0
@ -1761,6 +1954,16 @@ snapshots:
caniuse-lite@1.0.30001784: {} caniuse-lite@1.0.30001784: {}
chai@5.3.3:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.3
deep-eql: 5.0.2
loupe: 3.2.1
pathval: 2.0.1
check-error@2.1.3: {}
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@ -1787,6 +1990,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
deep-eql@5.0.2: {}
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
dom-serializer@2.0.0: dom-serializer@2.0.0:
@ -1816,6 +2021,8 @@ snapshots:
entities@4.5.0: {} entities@4.5.0: {}
es-module-lexer@1.7.0: {}
esbuild@0.21.5: esbuild@0.21.5:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.21.5 '@esbuild/aix-ppc64': 0.21.5
@ -1875,8 +2082,14 @@ snapshots:
estree-walker@2.0.2: {} estree-walker@2.0.2: {}
estree-walker@3.0.3:
dependencies:
'@types/estree': 1.0.8
eventemitter3@5.0.4: {} eventemitter3@5.0.4: {}
expect-type@1.3.0: {}
fdir@6.5.0(picomatch@4.0.4): fdir@6.5.0(picomatch@4.0.4):
optionalDependencies: optionalDependencies:
picomatch: 4.0.4 picomatch: 4.0.4
@ -1902,6 +2115,8 @@ snapshots:
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-tokens@9.0.1: {}
jsesc@3.1.0: {} jsesc@3.1.0: {}
json5@2.2.3: {} json5@2.2.3: {}
@ -1963,6 +2178,8 @@ snapshots:
load-tsconfig@0.2.5: {} load-tsconfig@0.2.5: {}
loupe@3.2.1: {}
lru-cache@5.1.1: lru-cache@5.1.1:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
@ -2005,6 +2222,8 @@ snapshots:
pathe@2.0.3: {} pathe@2.0.3: {}
pathval@2.0.1: {}
phaser@3.90.0: phaser@3.90.0:
dependencies: dependencies:
eventemitter3: 5.0.4 eventemitter3: 5.0.4
@ -2075,6 +2294,8 @@ snapshots:
semver@6.3.1: {} semver@6.3.1: {}
siginfo@2.0.0: {}
simple-code-frame@1.3.0: simple-code-frame@1.3.0:
dependencies: dependencies:
kolorist: 1.8.0 kolorist: 1.8.0
@ -2085,6 +2306,14 @@ snapshots:
stack-trace@1.0.0-pre2: {} stack-trace@1.0.0-pre2: {}
stackback@0.0.2: {}
std-env@3.10.0: {}
strip-literal@3.1.0:
dependencies:
js-tokens: 9.0.1
sucrase@3.35.1: sucrase@3.35.1:
dependencies: dependencies:
'@jridgewell/gen-mapping': 0.3.13 '@jridgewell/gen-mapping': 0.3.13
@ -2107,6 +2336,8 @@ snapshots:
dependencies: dependencies:
any-promise: 1.3.0 any-promise: 1.3.0
tinybench@2.9.0: {}
tinyexec@0.3.2: {} tinyexec@0.3.2: {}
tinyglobby@0.2.15: tinyglobby@0.2.15:
@ -2114,6 +2345,12 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4 picomatch: 4.0.4
tinypool@1.1.1: {}
tinyrainbow@2.0.0: {}
tinyspy@4.0.4: {}
tree-kill@1.2.2: {} tree-kill@1.2.2: {}
ts-interface-checker@0.1.13: {} ts-interface-checker@0.1.13: {}
@ -2156,6 +2393,24 @@ snapshots:
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
vite-node@3.2.4(lightningcss@1.32.0):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 5.4.21(lightningcss@1.32.0)
transitivePeerDependencies:
- '@types/node'
- less
- lightningcss
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
vite-prerender-plugin@0.5.13(vite@5.4.21(lightningcss@1.32.0)): vite-prerender-plugin@0.5.13(vite@5.4.21(lightningcss@1.32.0)):
dependencies: dependencies:
kolorist: 1.8.0 kolorist: 1.8.0
@ -2175,6 +2430,47 @@ snapshots:
fsevents: 2.3.3 fsevents: 2.3.3
lightningcss: 1.32.0 lightningcss: 1.32.0
vitest@3.2.4(lightningcss@1.32.0):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@5.4.21(lightningcss@1.32.0))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
'@vitest/spy': 3.2.4
'@vitest/utils': 3.2.4
chai: 5.3.3
debug: 4.4.3
expect-type: 1.3.0
magic-string: 0.30.21
pathe: 2.0.3
picomatch: 4.0.4
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 5.4.21(lightningcss@1.32.0)
vite-node: 3.2.4(lightningcss@1.32.0)
why-is-node-running: 2.3.0
transitivePeerDependencies:
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- supports-color
- terser
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
yallist@3.1.1: {} yallist@3.1.1: {}
zimmerframe@1.1.4: {} zimmerframe@1.1.4: {}