From 00ad77af064da7a4f116f1010e5df3041bd80217 Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Thu, 10 Jul 2025 12:57:28 +0100 Subject: [PATCH 1/2] CORE-510: Orphaned app dev worker processes --- .github/workflows/build.yaml | 37 ++ package.json | 6 +- packages/app/lib/cli/commands/app/dev.test.ts | 196 ++++++ packages/app/lib/cli/commands/app/dev.ts | 31 +- .../services/dev/workers/app-worker.test.ts | 125 ++++ .../cli/services/dev/workers/app-worker.ts | 16 +- .../cli/services/dev/workers/tunnel-worker.ts | 7 +- .../services/dev/workers/web-worker.test.ts | 126 ++++ .../cli/services/dev/workers/web-worker.ts | 18 + packages/app/package.json | 2 + packages/app/vitest.config.ts | 14 + packages/cli-kit/lib/node/session.ts | 2 +- packages/cli-kit/lib/node/worker.ts | 5 + .../cli-kit/lib/services/cloudflared.test.ts | 145 +++++ packages/cli-kit/lib/services/cloudflared.ts | 9 +- packages/cli-kit/package.json | 2 + packages/cli-kit/vitest.config.ts | 14 + pnpm-lock.yaml | 596 +++++++++++++++++- 18 files changed, 1332 insertions(+), 19 deletions(-) create mode 100644 packages/app/lib/cli/commands/app/dev.test.ts create mode 100644 packages/app/lib/cli/services/dev/workers/app-worker.test.ts create mode 100644 packages/app/lib/cli/services/dev/workers/web-worker.test.ts create mode 100644 packages/app/vitest.config.ts create mode 100644 packages/cli-kit/lib/services/cloudflared.test.ts create mode 100644 packages/cli-kit/vitest.config.ts diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 50e8487..155a99c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -49,6 +49,43 @@ jobs: - name: Lint run: pnpm lint + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js environment + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + - name: Install dependencies + run: pnpm i --frozen-lockfile + + - name: Run tests + run: pnpm test + build: runs-on: ubuntu-latest diff --git a/package.json b/package.json index f03eef3..2bae1cc 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "scripts": { "build": "pnpm -r build", "lint": "eslint --fix packages/**/*.{ts,js,json}", + "test": "pnpm -r test", + "test:watch": "pnpm -r test:watch", "release": "pnpm build && bumpp packages/*/package.json --commit \"release: v\" --push --tag && pnpm -r release" }, "workspaces": { @@ -17,6 +19,7 @@ }, "devDependencies": { "@types/node": "^18.18.0", + "@vitest/ui": "^2.1.8", "@youcan/lint": "^3.0.6", "bumpp": "^9.2.0", "dotenv": "^16.3.1", @@ -27,7 +30,8 @@ "rollup-plugin-node-externals": "^6.1.1", "rollup-plugin-typescript2": "^0.36.0", "tslib": "^2.8.1", - "typescript": "5.1.6" + "typescript": "5.1.6", + "vitest": "^2.1.8" }, "pnpm": { "overrides": { diff --git a/packages/app/lib/cli/commands/app/dev.test.ts b/packages/app/lib/cli/commands/app/dev.test.ts new file mode 100644 index 0000000..73b3f42 --- /dev/null +++ b/packages/app/lib/cli/commands/app/dev.test.ts @@ -0,0 +1,196 @@ +import type { Worker } from '@youcan/cli-kit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import Dev from './dev'; + +vi.mock('@/util/app-loader', () => ({ + load: vi.fn().mockResolvedValue({ + root: '/test/app', + config: { id: 'test-app' }, + webs: [], + extensions: [], + }), +})); + +vi.mock('@youcan/cli-kit', () => ({ + Session: { + authenticate: vi.fn().mockResolvedValue({}), + }, + Tasks: { + run: vi.fn().mockResolvedValue({ workers: [] }), + }, + UI: { + renderDevOutput: vi.fn(), + }, + System: { + getPortOrNextOrRandom: vi.fn().mockResolvedValue(3000), + }, + Services: { + Cloudflared: vi.fn(), + }, + Filesystem: { + writeJsonFile: vi.fn(), + }, + Path: { + join: vi.fn(), + }, + Cli: { + Command: class MockCommand { + controller = { abort: vi.fn(), signal: new AbortController().signal }; + output = { wait: vi.fn() }; + }, + }, + Env: {}, + Http: {}, + Worker: { + Interface: class MockInterface { + async run() {} + async boot() {} + async cleanup() {} + }, + Abstract: class MockAbstract { + async run() {} + async boot() {} + async cleanup() {} + }, + }, +})); + +vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); +const mockOnce = vi.spyOn(process, 'once').mockImplementation(() => process); + +describe('dev Command', () => { + let devCommand: Dev; + let mockWorkers: Worker.Interface[]; + + beforeEach(() => { + vi.clearAllMocks(); + + mockWorkers = [ + { + run: vi.fn(), + boot: vi.fn(), + cleanup: vi.fn().mockResolvedValue(undefined), + }, + { + run: vi.fn(), + boot: vi.fn(), + cleanup: vi.fn().mockResolvedValue(undefined), + }, + ]; + + devCommand = new Dev([], {}); + (devCommand as any).workers = mockWorkers; + }); + + describe('setupExitHandlers', () => { + it('should register signal handlers for graceful shutdown', () => { + (devCommand as any).setupExitHandlers(); + + expect(mockOnce).toHaveBeenCalledWith('SIGINT', expect.any(Function)); + expect(mockOnce).toHaveBeenCalledWith('SIGTERM', expect.any(Function)); + expect(mockOnce).toHaveBeenCalledWith('SIGQUIT', expect.any(Function)); + expect(mockOnce).toHaveBeenCalledWith('exit', expect.any(Function)); + }); + }); + + describe('cleanup functionality', () => { + it('should cleanup all workers when cleanupAndExit is called', async () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + (devCommand as any).setupExitHandlers(); + + const sigintCall = mockOnce.mock.calls.find(call => call[0] === 'SIGINT'); + const cleanupAndExit = sigintCall?.[1]; + + if (cleanupAndExit) { + await cleanupAndExit(); + } + + expect(consoleSpy).toHaveBeenCalledWith('Shutting down...'); + expect(mockWorkers[0].cleanup).toHaveBeenCalled(); + expect(mockWorkers[1].cleanup).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + + it('should handle worker cleanup errors gracefully', async () => { + const failingWorker = { + run: vi.fn(), + boot: vi.fn(), + cleanup: vi.fn().mockRejectedValue(new Error('Cleanup failed')), + }; + (devCommand as any).workers = [failingWorker]; + + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + (devCommand as any).setupExitHandlers(); + const sigintCall = mockOnce.mock.calls.find(call => call[0] === 'SIGINT'); + const cleanupAndExit = sigintCall?.[1]; + + if (cleanupAndExit) { + await expect(cleanupAndExit()).resolves.toBeUndefined(); + } + + expect(failingWorker.cleanup).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + }); + + describe('reloadWorkers', () => { + it('should cleanup existing workers before reload', async () => { + const mockController = { + abort: vi.fn(), + signal: new AbortController().signal, + }; + (devCommand as any).controller = mockController; + (devCommand as any).app = { + network_config: { app_port: 3000 }, + }; + + (devCommand as any).syncAppConfig = vi.fn().mockResolvedValue(undefined); + (devCommand as any).prepareDevProcesses = vi.fn().mockResolvedValue([]); + (devCommand as any).runWorkers = vi.fn().mockResolvedValue(undefined); + + await (devCommand as any).reloadWorkers(); + + expect(mockWorkers[0].cleanup).toHaveBeenCalled(); + expect(mockWorkers[1].cleanup).toHaveBeenCalled(); + }); + }); + + describe('runWorkers', () => { + it('should store workers in instance variable', async () => { + const newWorkers = [ + { + run: vi.fn().mockResolvedValue(undefined), + boot: vi.fn(), + cleanup: vi.fn(), + }, + ]; + + await (devCommand as any).runWorkers(newWorkers); + + expect((devCommand as any).workers).toBe(newWorkers); + expect(newWorkers[0].run).toHaveBeenCalled(); + }); + }); + + describe('hotkey handlers', () => { + it('should cleanup workers when q key is pressed', async () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const hotKeys = (devCommand as any).hotKeys; + const quitHandler = hotKeys.find((key: any) => key.keyboardKey === 'q')?.handler; + + if (quitHandler) { + await quitHandler(); + } + + expect(consoleSpy).toHaveBeenCalledWith('Shutting down...'); + expect(mockWorkers[0].cleanup).toHaveBeenCalled(); + expect(mockWorkers[1].cleanup).toHaveBeenCalled(); + + consoleSpy.mockRestore(); + }); + }); +}); diff --git a/packages/app/lib/cli/commands/app/dev.ts b/packages/app/lib/cli/commands/app/dev.ts index 38b2190..f56c624 100644 --- a/packages/app/lib/cli/commands/app/dev.ts +++ b/packages/app/lib/cli/commands/app/dev.ts @@ -22,14 +22,19 @@ class Dev extends AppCommand { } private setupExitHandlers() { - const cleanupAndExit = () => { + const cleanupAndExit = async () => { try { + console.log('Shutting down...'); + + if (this.workers.length > 0) { + await Promise.allSettled(this.workers.map(worker => worker.cleanup())); + } + if (this.controller) { this.controller.abort(); } this.workers = []; - console.log('Shutting down...'); setTimeout(() => { process.exit(0); }, 100); @@ -43,7 +48,10 @@ class Dev extends AppCommand { process.once('SIGTERM', cleanupAndExit); process.once('SIGQUIT', cleanupAndExit); - process.once('exit', () => { + process.once('exit', async () => { + if (this.workers.length > 0) { + await Promise.allSettled(this.workers.map(worker => worker.cleanup())); + } if (this.controller) { this.controller.abort(); } @@ -59,13 +67,19 @@ class Dev extends AppCommand { { keyboardKey: 'q', description: 'quit', - handler: () => { + handler: async () => { try { + console.log('Shutting down...'); + + if (this.workers.length > 0) { + await Promise.allSettled(this.workers.map(worker => worker.cleanup())); + } + if (this.controller) { this.controller.abort(); } this.workers = []; - console.log('Shutting down...'); + setTimeout(() => { process.exit(0); }, 100); @@ -134,6 +148,11 @@ class Dev extends AppCommand { async reloadWorkers() { this.controller = new AbortController(); + if (this.workers.length > 0) { + await Promise.allSettled(this.workers.map(worker => worker.cleanup())); + this.workers = []; + } + const networkConfig = this.app.network_config; this.app = await load(); this.app.network_config = networkConfig; @@ -144,6 +163,8 @@ class Dev extends AppCommand { } private async runWorkers(workers: Worker.Interface[]): Promise { + this.workers = workers; + await Promise.all(workers.map(worker => worker.run())).catch((_) => { }); } diff --git a/packages/app/lib/cli/services/dev/workers/app-worker.test.ts b/packages/app/lib/cli/services/dev/workers/app-worker.test.ts new file mode 100644 index 0000000..018639a --- /dev/null +++ b/packages/app/lib/cli/services/dev/workers/app-worker.test.ts @@ -0,0 +1,125 @@ +import type DevCommand from '@/cli/commands/app/dev'; +import type { App } from '@/types'; +import { Filesystem, Path, Worker } from '@youcan/cli-kit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import AppWorker from './app-worker'; + +vi.mock('@youcan/cli-kit', () => ({ + Filesystem: { + watch: vi.fn(), + }, + Path: { + resolve: vi.fn(), + }, + Worker: { + Logger: class MockLogger { + constructor(public name: string, public color: string) {} + write = vi.fn(); + }, + Abstract: class MockAbstract { + async cleanup() {} + }, + }, +})); + +describe('appWorker', () => { + let appWorker: AppWorker; + let mockCommand: DevCommand; + let mockApp: App; + let mockWatcher: any; + + beforeEach(() => { + vi.clearAllMocks(); + + mockWatcher = { + close: vi.fn(), + once: vi.fn(), + }; + + vi.mocked(Filesystem.watch).mockReturnValue(mockWatcher); + vi.mocked(Path.resolve).mockReturnValue('/path/to/app.config.json'); + + mockCommand = { + output: { + wait: vi.fn().mockResolvedValue(undefined), + }, + controller: { + abort: vi.fn(), + }, + reloadWorkers: vi.fn(), + } as any; + + mockApp = { + root: '/path/to/app', + } as App; + + appWorker = new AppWorker(mockCommand, mockApp); + }); + + describe('cleanup', () => { + it('should close the file watcher if it exists', async () => { + await appWorker.run(); + expect(Filesystem.watch).toHaveBeenCalled(); + + await appWorker.cleanup(); + + expect(mockWatcher.close).toHaveBeenCalled(); + }); + + it('should handle cleanup when no watcher exists', async () => { + await expect(appWorker.cleanup()).resolves.toBeUndefined(); + }); + + it('should log cleanup message', async () => { + await appWorker.run(); + + await appWorker.cleanup(); + + const logger = (appWorker as any).logger; + expect(logger.write).toHaveBeenCalledWith('stopping config watcher...'); + }); + + it('should set watcher to undefined after cleanup', async () => { + await appWorker.run(); + expect((appWorker as any).watcher).toBe(mockWatcher); + + await appWorker.cleanup(); + + expect((appWorker as any).watcher).toBeUndefined(); + }); + }); + + describe('run', () => { + it('should create a file watcher for the app config', async () => { + await appWorker.run(); + + expect(Path.resolve).toHaveBeenCalledWith('/path/to/app', 'youcan.app.json'); + expect(Filesystem.watch).toHaveBeenCalledWith('/path/to/app.config.json', { + persistent: true, + ignoreInitial: true, + awaitWriteFinish: { + stabilityThreshold: 50, + }, + }); + }); + + it('should set up change handler that reloads workers', async () => { + await appWorker.run(); + + expect(mockWatcher.once).toHaveBeenCalledWith('change', expect.any(Function)); + + const changeHandler = mockWatcher.once.mock.calls[0][1]; + await changeHandler(); + + expect(mockWatcher.close).toHaveBeenCalled(); + expect(mockCommand.controller.abort).toHaveBeenCalled(); + expect(mockCommand.reloadWorkers).toHaveBeenCalled(); + }); + + it('should wait 500ms before starting to watch', async () => { + await appWorker.run(); + + expect(mockCommand.output.wait).toHaveBeenCalledWith(500); + }); + }); +}); diff --git a/packages/app/lib/cli/services/dev/workers/app-worker.ts b/packages/app/lib/cli/services/dev/workers/app-worker.ts index c6e8325..1eb080c 100644 --- a/packages/app/lib/cli/services/dev/workers/app-worker.ts +++ b/packages/app/lib/cli/services/dev/workers/app-worker.ts @@ -5,6 +5,7 @@ import { Filesystem, Path, Worker } from '@youcan/cli-kit'; export default class AppWorker extends Worker.Abstract { private logger: Worker.Logger; + private watcher?: ReturnType; constructor( private command: DevCommand, @@ -21,7 +22,7 @@ export default class AppWorker extends Worker.Abstract { await this.command.output.wait(500); this.logger.write('watching for config updates...'); - const watcher = Filesystem.watch(Path.resolve(this.app.root, APP_CONFIG_FILENAME), { + this.watcher = Filesystem.watch(Path.resolve(this.app.root, APP_CONFIG_FILENAME), { persistent: true, ignoreInitial: true, awaitWriteFinish: { @@ -29,12 +30,21 @@ export default class AppWorker extends Worker.Abstract { }, }); - watcher.once('change', async () => { - await watcher.close(); + this.watcher.once('change', async () => { + await this.watcher?.close(); this.logger.write('config update detected, reloading workers...'); this.command.controller.abort(); this.command.reloadWorkers(); }); } + + public async cleanup(): Promise { + this.logger.write('stopping config watcher...'); + + if (this.watcher) { + await this.watcher.close(); + this.watcher = undefined; + } + } } diff --git a/packages/app/lib/cli/services/dev/workers/tunnel-worker.ts b/packages/app/lib/cli/services/dev/workers/tunnel-worker.ts index 5e17063..d4cb219 100644 --- a/packages/app/lib/cli/services/dev/workers/tunnel-worker.ts +++ b/packages/app/lib/cli/services/dev/workers/tunnel-worker.ts @@ -25,7 +25,7 @@ export default class TunnelWorker extends Worker.Abstract { this.logger.write('start tunneling the app'); - await this.tunnelService.tunnel(this.app.network_config.app_port); + await this.tunnelService.tunnel(this.app.network_config.app_port, 'localhost', this.command.controller.signal); let attempts = 0; @@ -51,6 +51,11 @@ export default class TunnelWorker extends Worker.Abstract { setInterval(() => this.checkForError, 500); } + public async cleanup(): Promise { + this.logger.write('stopping tunnel...'); + // The abort signal passed to cloudflared will handle process termination + } + private checkForError() { const error = this.tunnelService.getError(); diff --git a/packages/app/lib/cli/services/dev/workers/web-worker.test.ts b/packages/app/lib/cli/services/dev/workers/web-worker.test.ts new file mode 100644 index 0000000..b7ec482 --- /dev/null +++ b/packages/app/lib/cli/services/dev/workers/web-worker.test.ts @@ -0,0 +1,126 @@ +import type { App, Web } from '@/types'; +import type { Cli } from '@youcan/cli-kit'; +import { System, Worker } from '@youcan/cli-kit'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import WebWorker from './web-worker'; + +vi.mock('@youcan/cli-kit', () => ({ + System: { + exec: vi.fn(), + isPortAvailable: vi.fn(), + killPortProcess: vi.fn(), + }, + Worker: { + Logger: class MockLogger { + constructor(public name: string, public color: string) {} + write = vi.fn(); + }, + Abstract: class MockAbstract { + async cleanup() {} + }, + }, +})); + +describe('webWorker', () => { + let webWorker: WebWorker; + let mockCommand: Cli.Command; + let mockApp: App; + let mockWeb: Web; + let mockEnv: Record; + + beforeEach(() => { + vi.clearAllMocks(); + + mockCommand = { + controller: { + signal: new AbortController().signal, + }, + } as any; + + mockApp = {} as App; + + mockWeb = { + config: { + name: 'test-web', + commands: { + dev: 'npm start', + }, + }, + } as Web; + + mockEnv = { + PORT: '3001', + NODE_ENV: 'development', + }; + + webWorker = new WebWorker(mockCommand, mockApp, mockWeb, mockEnv); + }); + + describe('cleanup', () => { + it('should kill process on the assigned port when port is not available', async () => { + vi.mocked(System.isPortAvailable).mockResolvedValue(false); + vi.mocked(System.killPortProcess).mockResolvedValue(); + + await webWorker.cleanup(); + + expect(System.isPortAvailable).toHaveBeenCalledWith(3001); + expect(System.killPortProcess).toHaveBeenCalledWith(3001); + }); + + it('should not kill process when port is already available', async () => { + vi.mocked(System.isPortAvailable).mockResolvedValue(true); + + await webWorker.cleanup(); + + expect(System.isPortAvailable).toHaveBeenCalledWith(3001); + expect(System.killPortProcess).not.toHaveBeenCalled(); + }); + + it('should handle missing PORT environment variable gracefully', async () => { + const webWorkerWithoutPort = new WebWorker( + mockCommand, + mockApp, + mockWeb, + { NODE_ENV: 'development' }, + ); + + await expect(webWorkerWithoutPort.cleanup()).resolves.toBeUndefined(); + expect(System.isPortAvailable).not.toHaveBeenCalled(); + expect(System.killPortProcess).not.toHaveBeenCalled(); + }); + + it('should handle killPortProcess errors gracefully', async () => { + vi.mocked(System.isPortAvailable).mockResolvedValue(false); + vi.mocked(System.killPortProcess).mockRejectedValue(new Error('Process not found')); + + await expect(webWorker.cleanup()).resolves.toBeUndefined(); + expect(System.killPortProcess).toHaveBeenCalledWith(3001); + }); + + it('should log cleanup messages', async () => { + vi.mocked(System.isPortAvailable).mockResolvedValue(false); + vi.mocked(System.killPortProcess).mockResolvedValue(); + + await webWorker.cleanup(); + + const logger = (webWorker as any).logger; + expect(logger.write).toHaveBeenCalledWith('stopping web server...'); + expect(logger.write).toHaveBeenCalledWith('killed process on port 3001'); + }); + }); + + describe('run', () => { + it('should execute the web command with correct parameters', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await webWorker.run(); + + expect(System.exec).toHaveBeenCalledWith('npm', ['start'], { + stdout: expect.any(Object), + signal: mockCommand.controller.signal, + stderr: expect.any(Object), + env: mockEnv, + }); + }); + }); +}); diff --git a/packages/app/lib/cli/services/dev/workers/web-worker.ts b/packages/app/lib/cli/services/dev/workers/web-worker.ts index b0018fd..d9e7d8e 100644 --- a/packages/app/lib/cli/services/dev/workers/web-worker.ts +++ b/packages/app/lib/cli/services/dev/workers/web-worker.ts @@ -3,6 +3,7 @@ import { type Cli, System, Worker } from '@youcan/cli-kit'; export default class WebWorker extends Worker.Abstract { private logger: Worker.Logger; + private processPort?: number; public constructor( private readonly command: Cli.Command, @@ -13,6 +14,7 @@ export default class WebWorker extends Worker.Abstract { super(); this.logger = new Worker.Logger(this.web.config.name || 'web', 'blue'); + this.processPort = this.env.PORT ? Number.parseInt(this.env.PORT, 10) : undefined; } public async boot(): Promise { @@ -28,4 +30,20 @@ export default class WebWorker extends Worker.Abstract { env: this.env, }); } + + public async cleanup(): Promise { + this.logger.write('stopping web server...'); + + if (this.processPort) { + try { + if (!await System.isPortAvailable(this.processPort)) { + await System.killPortProcess(this.processPort); + this.logger.write(`killed process on port ${this.processPort}`); + } + } + catch (error) { + // Ignore errors when killing port process + } + } + } } diff --git a/packages/app/package.json b/packages/app/package.json index f3ac798..9365be0 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -17,6 +17,8 @@ "scripts": { "build": "shx rm -rf dist && tsc --noEmit && rollup --config rollup.config.js", "dev": "shx rm -rf dist && rollup --config rollup.config.js --watch", + "test": "vitest run", + "test:watch": "vitest", "release": "pnpm publish --access public", "type-check": "tsc --noEmit" }, diff --git a/packages/app/vitest.config.ts b/packages/app/vitest.config.ts new file mode 100644 index 0000000..1525031 --- /dev/null +++ b/packages/app/vitest.config.ts @@ -0,0 +1,14 @@ +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './lib'), + }, + }, +}); diff --git a/packages/cli-kit/lib/node/session.ts b/packages/cli-kit/lib/node/session.ts index 0030531..6b6fa91 100644 --- a/packages/cli-kit/lib/node/session.ts +++ b/packages/cli-kit/lib/node/session.ts @@ -79,7 +79,7 @@ async function authorize(command: Cli.Command, state: string = Crypto.randomHex( code_challenge_method: 'S256', }; - await command.output.anykey('Press any key to open the login page on your browser..'); + await command.output.anykey('Press any key to open the login page on your browser'); const url = `http://${AUTHORIZATION_URL}/admin/oauth/authorize?${new URLSearchParams(params).toString()}`; diff --git a/packages/cli-kit/lib/node/worker.ts b/packages/cli-kit/lib/node/worker.ts index f451500..83adbfd 100644 --- a/packages/cli-kit/lib/node/worker.ts +++ b/packages/cli-kit/lib/node/worker.ts @@ -6,6 +6,7 @@ import { UI } from '..'; export interface Interface { run: () => Promise; boot: () => Promise; + cleanup: () => Promise; } export class Logger extends Writable { @@ -40,4 +41,8 @@ export class Logger extends Writable { export abstract class Abstract implements Interface { public abstract run(): Promise; public abstract boot(): Promise; + + public async cleanup(): Promise { + + } } diff --git a/packages/cli-kit/lib/services/cloudflared.test.ts b/packages/cli-kit/lib/services/cloudflared.test.ts new file mode 100644 index 0000000..7ce8896 --- /dev/null +++ b/packages/cli-kit/lib/services/cloudflared.test.ts @@ -0,0 +1,145 @@ +import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Filesystem, System } from '..'; +import { Cloudflared } from './cloudflared'; + +vi.mock('..', () => ({ + System: { + exec: vi.fn(), + }, + Filesystem: { + isExecutable: vi.fn(), + }, + Path: { + join: vi.fn().mockReturnValue('/mock/path/cloudflared'), + }, +})); + +const originalPlatform = process.platform; +const originalArch = process.arch; + +describe('cloudflared', () => { + let cloudflared: Cloudflared; + let mockAbortController: AbortController; + + beforeEach(() => { + vi.clearAllMocks(); + + Object.defineProperty(process, 'platform', { + value: 'darwin', + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: 'arm64', + configurable: true, + }); + + vi.mocked(Filesystem.isExecutable).mockResolvedValue(true); + + mockAbortController = new AbortController(); + cloudflared = new Cloudflared(); + }); + + afterAll(() => { + Object.defineProperty(process, 'platform', { + value: originalPlatform, + configurable: true, + }); + Object.defineProperty(process, 'arch', { + value: originalArch, + configurable: true, + }); + }); + + describe('tunnel', () => { + it('should pass abort signal to System.exec', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await cloudflared.tunnel(3000, 'localhost', mockAbortController.signal); + + expect(System.exec).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ + signal: mockAbortController.signal, + }), + ); + }); + + it('should use correct cloudflared arguments', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await cloudflared.tunnel(3001, 'localhost', mockAbortController.signal); + + expect(System.exec).toHaveBeenCalledWith( + expect.any(String), + ['tunnel', '--url=localhost:3001', '--no-autoupdate'], + expect.any(Object), + ); + }); + + it('should work without abort signal', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await expect(cloudflared.tunnel(3000)).resolves.toBeUndefined(); + + expect(System.exec).toHaveBeenCalledWith( + expect.any(String), + expect.any(Array), + expect.objectContaining({ + signal: undefined, + }), + ); + }); + + it('should use default host when not provided', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await cloudflared.tunnel(3000, undefined, mockAbortController.signal); + + expect(System.exec).toHaveBeenCalledWith( + expect.any(String), + ['tunnel', '--url=localhost:3000', '--no-autoupdate'], + expect.any(Object), + ); + }); + + it('should check if cloudflared is executable before running', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await cloudflared.tunnel(3000, 'localhost', mockAbortController.signal); + + expect(Filesystem.isExecutable).toHaveBeenCalledWith('/mock/path/cloudflared'); + }); + }); + + describe('exec retry logic with signal', () => { + it('should retry with the same signal on failure', async () => { + let callCount = 0; + vi.mocked(System.exec).mockImplementation(async (bin, args, options) => { + callCount++; + if (callCount < 2) { + if (options?.errorHandler) { + await options.errorHandler(new Error('Connection failed')); + } + } + }); + + await cloudflared.tunnel(3000, 'localhost', mockAbortController.signal); + + expect(System.exec).toHaveBeenCalledTimes(2); + + expect(System.exec).toHaveBeenNthCalledWith(1, expect.any(String), expect.any(Array), expect.objectContaining({ signal: mockAbortController.signal }), + ); + expect(System.exec).toHaveBeenNthCalledWith(2, expect.any(String), expect.any(Array), expect.objectContaining({ signal: mockAbortController.signal }), + ); + }); + + it('should handle errors without crashing', async () => { + vi.mocked(System.exec).mockResolvedValue(); + + await expect(cloudflared.tunnel(3000, 'localhost', mockAbortController.signal)) + .resolves + .toBeUndefined(); + }); + }); +}); diff --git a/packages/cli-kit/lib/services/cloudflared.ts b/packages/cli-kit/lib/services/cloudflared.ts index af57626..bbd2e64 100644 --- a/packages/cli-kit/lib/services/cloudflared.ts +++ b/packages/cli-kit/lib/services/cloudflared.ts @@ -197,11 +197,11 @@ export class Cloudflared { this.system = { platform, arch }; } - public async tunnel(port: number, host = 'localhost') { + public async tunnel(port: number, host = 'localhost', signal?: AbortSignal) { await this.install(); const { bin, args } = this.composeTunnelingCommand(port, host); - this.exec(bin, args); + this.exec(bin, args, 3, signal); } private async install() { @@ -220,7 +220,7 @@ export class Cloudflared { }; } - private async exec(bin: string, args: string[], maxRetries = 3) { + private async exec(bin: string, args: string[], maxRetries = 3, signal?: AbortSignal) { if (this.getUrl()) { return; } @@ -233,8 +233,9 @@ export class Cloudflared { await System.exec(bin, args, { // Weird choice of cloudflared to write to stderr. stderr: this.output, + signal, errorHandler: async () => { - await this.exec(bin, args, maxRetries - 1); + await this.exec(bin, args, maxRetries - 1, signal); }, }); } diff --git a/packages/cli-kit/package.json b/packages/cli-kit/package.json index d4a61cf..b6dd78d 100644 --- a/packages/cli-kit/package.json +++ b/packages/cli-kit/package.json @@ -21,6 +21,8 @@ "scripts": { "build": "shx rm -rf dist && tsc --noEmit && rollup --config rollup.config.js", "dev": "shx rm -rf dist && rollup --config rollup.config.js --watch", + "test": "vitest run", + "test:watch": "vitest", "release": "pnpm publish --access public", "type-check": "tsc --noEmit" }, diff --git a/packages/cli-kit/vitest.config.ts b/packages/cli-kit/vitest.config.ts new file mode 100644 index 0000000..1525031 --- /dev/null +++ b/packages/cli-kit/vitest.config.ts @@ -0,0 +1,14 @@ +import path from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './lib'), + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4d3e14d..a564061 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,9 +17,12 @@ importers: '@types/node': specifier: ^18.18.0 version: 18.19.64 + '@vitest/ui': + specifier: ^2.1.8 + version: 2.1.9(vitest@2.1.9) '@youcan/lint': specifier: ^3.0.6 - version: 3.0.6(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@1.21.6)))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6) + version: 3.0.6(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@1.21.6)))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6)(vitest@2.1.9) bumpp: specifier: ^9.2.0 version: 9.8.1 @@ -50,6 +53,9 @@ importers: typescript: specifier: 5.1.6 version: 5.1.6 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@18.19.64)(@vitest/ui@2.1.9) packages/app: dependencies: @@ -316,6 +322,144 @@ packages: resolution: {integrity: sha512-xjZTSFgECpb9Ohuk5yMX5RhUEbfeQcuOp8IF60e+wyzWEF0M5xeSgqsfLtvPEX8BIyOX9saZqzuGPmZ8oWc+5Q==} engines: {node: '>=16'} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-plugin-eslint-comments@4.4.1': resolution: {integrity: sha512-lb/Z/MzbTf7CaVYM9WCFNQZ4L1yi3ev2fsFPF99h31ljhSEyUoyEsKsNWiU+qD1glbYTDJdqgyaLKtyTkkqtuQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -510,6 +654,9 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rollup/pluginutils@4.2.1': resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} engines: {node: '>= 8.0.0'} @@ -748,6 +895,40 @@ packages: vitest: optional: true + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/ui@2.1.9': + resolution: {integrity: sha512-izzd2zmnk8Nl5ECYkW27328RbQ1nKvkm6Bb5DAaz1Gk59EbLkiCMa6OLT0NoaAYTjOFS6N+SMYW1nh4/9ljPiw==} + peerDependencies: + vitest: 2.1.9 + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} @@ -913,6 +1094,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1027,6 +1212,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1049,6 +1238,10 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -1259,6 +1452,10 @@ packages: decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1370,6 +1567,11 @@ packages: es-toolkit@1.27.0: resolution: {integrity: sha512-ETSFA+ZJArcuSCpzD2TjAy6UHpx4E4uqFsoDg9F/nTLogrLmVVZQ+zNxco5h7cWnA1nNak07IXsLcaSMih+ZPQ==} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1591,6 +1793,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -1619,6 +1824,10 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + extract-stack@1.0.0: resolution: {integrity: sha512-M5Ge0JIrn12EtIVpje2G+hI5X78hmX4UDzynZ7Vnp1MiPSqleEonmgr2Rh59eygEEgq3YJ1GDP96rnM8tnVg/Q==} engines: {node: '>=4'} @@ -1657,6 +1866,9 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -2167,6 +2379,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -2378,6 +2593,10 @@ packages: mmap-io@1.1.7: resolution: {integrity: sha512-DSxroCWPHacAnPLpjHlE7ivkABcWrKQBb4bt49msCLmjWgWZoGpmIqaTn4WgkziAttLLRM4nlq2rQS803zr1rw==} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} @@ -2669,6 +2888,10 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -2924,6 +3147,9 @@ packages: engines: {node: '>=6'} hasBin: true + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2934,6 +3160,10 @@ packages: simple-git@3.27.0: resolution: {integrity: sha512-ivHoFS9Yi9GY49ogc6/YAi3Fl9ROnF4VyubNylgCkA+RVqLaKWnDSzXOVzya8csELIaWaYNutsEuAhZrtOjozA==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -2999,6 +3229,12 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -3115,6 +3351,9 @@ packages: resolution: {integrity: sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==} engines: {node: '>=14.16'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -3122,6 +3361,18 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} 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@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} @@ -3134,6 +3385,10 @@ packages: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + treeify@1.1.0: resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} engines: {node: '>=0.6'} @@ -3298,6 +3553,67 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vue-eslint-parser@9.4.3: resolution: {integrity: sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==} engines: {node: ^14.17.0 || >=16.0.0} @@ -3320,6 +3636,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} @@ -3496,6 +3817,75 @@ snapshots: esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.17.0(jiti@1.21.6))': dependencies: escape-string-regexp: 4.0.0 @@ -3809,6 +4199,8 @@ snapshots: '@pkgr/core@0.1.1': {} + '@polka/url@1.0.0-next.29': {} + '@rollup/pluginutils@4.2.1': dependencies: estree-walker: 2.0.2 @@ -4036,12 +4428,64 @@ snapshots: '@typescript-eslint/types': 8.19.1 eslint-visitor-keys: 4.2.0 - '@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6)': + '@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6)(vitest@2.1.9)': dependencies: '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6) eslint: 9.17.0(jiti@1.21.6) optionalDependencies: typescript: 5.1.6 + vitest: 2.1.9(@types/node@18.19.64)(@vitest/ui@2.1.9) + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.19(@types/node@18.19.64))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 5.4.19(@types/node@18.19.64) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.17 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/ui@2.1.9(vitest@2.1.9)': + dependencies: + '@vitest/utils': 2.1.9 + fflate: 0.8.2 + flatted: 3.3.2 + pathe: 1.1.2 + sirv: 3.0.1 + tinyglobby: 0.2.10 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@18.19.64)(@vitest/ui@2.1.9) + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.1.4 + tinyrainbow: 1.2.0 '@vue/compiler-core@3.5.13': dependencies: @@ -4075,7 +4519,7 @@ snapshots: '@vue/shared@3.5.13': {} - '@youcan/lint@3.0.6(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@1.21.6)))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6)': + '@youcan/lint@3.0.6(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.17.0(jiti@1.21.6)))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6)(vitest@2.1.9)': dependencies: '@antfu/install-pkg': 0.5.0 '@clack/prompts': 0.8.2 @@ -4084,7 +4528,7 @@ snapshots: '@stylistic/eslint-plugin': 2.12.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6) '@typescript-eslint/eslint-plugin': 8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6) '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6) - '@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6) + '@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6))(eslint@9.17.0(jiti@1.21.6))(typescript@5.1.6)(vitest@2.1.9) eslint: 9.17.0(jiti@1.21.6) eslint-config-flat-gitignore: 0.3.0(eslint@9.17.0(jiti@1.21.6)) eslint-flat-config-utils: 0.4.0 @@ -4237,6 +4681,8 @@ snapshots: array-union@2.1.0: {} + assertion-error@2.0.1: {} + astral-regex@2.0.0: {} async@3.2.6: {} @@ -4363,6 +4809,14 @@ snapshots: ccount@2.0.1: {} + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -4398,6 +4852,8 @@ snapshots: character-entities@2.0.2: {} + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -4613,6 +5069,8 @@ snapshots: dependencies: character-entities: 2.0.2 + deep-eql@5.0.2: {} + deep-is@0.1.4: {} default-browser-id@3.0.0: @@ -4721,6 +5179,32 @@ snapshots: es-toolkit@1.27.0: {} + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -5037,6 +5521,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + esutils@2.0.3: {} execa@0.10.0: @@ -5107,6 +5595,8 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + expect-type@1.2.2: {} + extract-stack@1.0.0: {} fast-deep-equal@3.1.3: {} @@ -5140,6 +5630,8 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -5623,6 +6115,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.1.4: {} + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -6003,6 +6497,8 @@ snapshots: nan: 2.22.0 optional: true + mrmime@2.0.1: {} + ms@2.1.2: {} ms@2.1.3: {} @@ -6215,6 +6711,8 @@ snapshots: pathe@1.1.2: {} + pathval@2.0.1: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -6485,6 +6983,8 @@ snapshots: minimist: 1.2.8 shelljs: 0.8.5 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -6497,6 +6997,12 @@ snapshots: transitivePeerDependencies: - supports-color + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} slash@3.0.0: {} @@ -6583,6 +7089,10 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + + std-env@3.9.0: {} + string-argv@0.3.2: {} string-width@4.2.3: @@ -6707,6 +7217,8 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.10: @@ -6714,6 +7226,12 @@ snapshots: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + titleize@3.0.0: {} to-regex-range@5.0.1: @@ -6724,6 +7242,8 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 + totalist@3.0.1: {} + treeify@1.1.0: {} ts-api-utils@2.0.0(typescript@5.1.6): @@ -6854,6 +7374,69 @@ snapshots: vary@1.1.2: {} + vite-node@2.1.9(@types/node@18.19.64): + dependencies: + cac: 6.7.14 + debug: 4.3.7(supports-color@8.1.1) + es-module-lexer: 1.6.0 + pathe: 1.1.2 + vite: 5.4.19(@types/node@18.19.64) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.19(@types/node@18.19.64): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.27.4 + optionalDependencies: + '@types/node': 18.19.64 + fsevents: 2.3.3 + + vitest@2.1.9(@types/node@18.19.64)(@vitest/ui@2.1.9): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.19(@types/node@18.19.64)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.2.1 + debug: 4.3.7(supports-color@8.1.1) + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 1.1.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.19(@types/node@18.19.64) + vite-node: 2.1.9(@types/node@18.19.64) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 18.19.64 + '@vitest/ui': 2.1.9(vitest@2.1.9) + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vue-eslint-parser@9.4.3(eslint@9.17.0(jiti@1.21.6)): dependencies: debug: 4.3.7(supports-color@8.1.1) @@ -6879,6 +7462,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@3.1.0: dependencies: string-width: 4.2.3 From 14aa1d4b36541d473fbb99a7696117231577676e Mon Sep 17 00:00:00 2001 From: Aymane Dara Hlamnach Date: Thu, 10 Jul 2025 12:59:58 +0100 Subject: [PATCH 2/2] build step --- .github/workflows/build.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 155a99c..6738ddc 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -83,6 +83,9 @@ jobs: - name: Install dependencies run: pnpm i --frozen-lockfile + - name: Build project + run: pnpm build + - name: Run tests run: pnpm test