diff --git a/package.json b/package.json index 7bab69a..8d2ab04 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,12 @@ "preview": "pnpm --filter sample-game preview" }, "devDependencies": { - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.2.4" }, "pnpm": { - "onlyBuiltDependencies": ["esbuild"] + "onlyBuiltDependencies": [ + "esbuild" + ] } } diff --git a/packages/framework/package.json b/packages/framework/package.json index bddb52a..f2cca9e 100644 --- a/packages/framework/package.json +++ b/packages/framework/package.json @@ -13,7 +13,9 @@ }, "scripts": { "build": "tsup", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "peerDependencies": { "@preact/signals-core": "^1.5.1", @@ -29,6 +31,7 @@ "phaser": "^3.80.1", "preact": "^10.19.3", "tsup": "^8.0.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^3.2.4" } } diff --git a/packages/framework/src/input/InputMapper.test.ts b/packages/framework/src/input/InputMapper.test.ts new file mode 100644 index 0000000..32f6c09 --- /dev/null +++ b/packages/framework/src/input/InputMapper.test.ts @@ -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(); + + 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) => { + inputEvents.get('pointerdown')?.forEach(handler => handler(pointer)); + }, + }; +} + +function createMockGameObject() { + const events = new Map(); + + 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; + + 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(); + }); + }); +}); diff --git a/packages/framework/src/input/InputMapper.ts b/packages/framework/src/input/InputMapper.ts new file mode 100644 index 0000000..6575b09 --- /dev/null +++ b/packages/framework/src/input/InputMapper.ts @@ -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( + 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); +} diff --git a/packages/framework/src/input/PromptHandler.test.ts b/packages/framework/src/input/PromptHandler.test.ts new file mode 100644 index 0000000..e02c398 --- /dev/null +++ b/packages/framework/src/input/PromptHandler.test.ts @@ -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 { + 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): void { + for (const item of items) { + this.push(item); + } + } + + async pop(): Promise { + if (this.items.length > 0) { + return this.items.shift()!; + } + return new Promise((resolve) => { + this.resolvers.push(resolve); + }); + } + + get length(): number { + return this.items.length - this.resolvers.length; + } +} + +// Mock types +interface MockCommandContext { + commands: { + promptQueue: AsyncQueue; + }; +} + +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(), + }; +} + +describe('PromptHandler', () => { + let options: PromptHandlerOptions; + let mockCommands: MockCommandContext['commands']; + let onPrompt: ReturnType; + let onCancel: ReturnType; + + 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'); + }); + }); +}); diff --git a/packages/framework/src/input/PromptHandler.ts b/packages/framework/src/input/PromptHandler.ts new file mode 100644 index 0000000..de0feea --- /dev/null +++ b/packages/framework/src/input/PromptHandler.ts @@ -0,0 +1,102 @@ +import type { IGameContext, PromptEvent } from 'boardgame-core'; + +export interface PromptHandlerOptions { + commands: IGameContext['commands']; + onPrompt: (prompt: PromptEvent) => void; + onCancel: (reason?: string) => void; +} + +export class PromptHandler { + private commands: IGameContext['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); +} diff --git a/packages/framework/src/input/index.ts b/packages/framework/src/input/index.ts index aead38c..e7f0a6d 100644 --- a/packages/framework/src/input/index.ts +++ b/packages/framework/src/input/index.ts @@ -1,199 +1,11 @@ -import Phaser from 'phaser'; -import type { IGameContext, PromptEvent } from 'boardgame-core'; +export { + InputMapper, + createInputMapper, + type InputMapperOptions, +} from './InputMapper.js'; -// ─── InputMapper ─────────────────────────────────────────────────────────── - -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( - 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['commands']; - onPrompt: (prompt: PromptEvent) => void; - onCancel: (reason?: string) => void; -} - -export class PromptHandler { - private commands: IGameContext['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); -} +export { + PromptHandler, + createPromptHandler, + type PromptHandlerOptions, +} from './PromptHandler.js'; diff --git a/packages/framework/vitest.config.ts b/packages/framework/vitest.config.ts new file mode 100644 index 0000000..7dd1325 --- /dev/null +++ b/packages/framework/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11040b9..4796aa9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(lightningcss@1.32.0) packages/framework: devDependencies: @@ -35,6 +38,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(lightningcss@1.32.0) packages/sample-game: dependencies: @@ -749,9 +755,44 @@ packages: peerDependencies: 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': 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: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -760,6 +801,10 @@ packages: any-promise@1.3.0: 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: resolution: {integrity: sha512-5gafyjyyBTTdX/tQQ0hRgu4AhNHG/hqWi0ZZmg2xvs2FgRkJXzDNKBZCyoYqgFkovfDrgM8OoKg8karoUvWeCw==} peerDependencies: @@ -791,6 +836,14 @@ packages: caniuse-lite@1.0.30001784: 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: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -825,6 +878,10 @@ packages: supports-color: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -853,6 +910,9 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -870,9 +930,16 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.4: 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: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -912,6 +979,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -1010,6 +1080,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} 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: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -1050,6 +1123,10 @@ packages: pathe@2.0.3: 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: resolution: {integrity: sha512-/cziz/5ZIn02uDkC9RzN8VF9x3Gs3XdFFf9nkiMEQT3p7hQlWuyjy4QWosU802qqno2YSLn2BfqwOKLv/sSVfQ==} @@ -1113,6 +1190,9 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-code-frame@1.3.0: resolution: {integrity: sha512-MB4pQmETUBlNs62BBeRjIFGeuy/x6gGKh7+eRUemn1rCFhqo7K+4slPqsyizCbcbYLnaYqaoZ2FWsZ/jN06D8w==} @@ -1128,6 +1208,15 @@ packages: resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} 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: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1147,6 +1236,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -1154,6 +1246,18 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} 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: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1194,6 +1298,11 @@ packages: peerDependencies: 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: resolution: {integrity: sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==} peerDependencies: @@ -1230,6 +1339,39 @@ packages: terser: 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: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -1730,12 +1872,63 @@ snapshots: tailwindcss: 4.2.2 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': {} + '@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: {} any-promise@1.3.0: {} + assertion-error@2.0.1: {} + babel-plugin-transform-hook-names@1.0.2(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -1761,6 +1954,16 @@ snapshots: 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: dependencies: readdirp: 4.1.2 @@ -1787,6 +1990,8 @@ snapshots: dependencies: ms: 2.1.3 + deep-eql@5.0.2: {} + detect-libc@2.1.2: {} dom-serializer@2.0.0: @@ -1816,6 +2021,8 @@ snapshots: entities@4.5.0: {} + es-module-lexer@1.7.0: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -1875,8 +2082,14 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + eventemitter3@5.0.4: {} + expect-type@1.3.0: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -1902,6 +2115,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + jsesc@3.1.0: {} json5@2.2.3: {} @@ -1963,6 +2178,8 @@ snapshots: load-tsconfig@0.2.5: {} + loupe@3.2.1: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -2005,6 +2222,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + phaser@3.90.0: dependencies: eventemitter3: 5.0.4 @@ -2075,6 +2294,8 @@ snapshots: semver@6.3.1: {} + siginfo@2.0.0: {} + simple-code-frame@1.3.0: dependencies: kolorist: 1.8.0 @@ -2085,6 +2306,14 @@ snapshots: 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: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -2107,6 +2336,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.15: @@ -2114,6 +2345,12 @@ snapshots: fdir: 6.5.0(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: {} ts-interface-checker@0.1.13: {} @@ -2156,6 +2393,24 @@ snapshots: escalade: 3.2.0 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)): dependencies: kolorist: 1.8.0 @@ -2175,6 +2430,47 @@ snapshots: fsevents: 2.3.3 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: {} zimmerframe@1.1.4: {}