diff --git a/.github/agents/test-developer.agent.md b/.github/agents/test-developer.agent.md index 073ec9d..8848c63 100644 --- a/.github/agents/test-developer.agent.md +++ b/.github/agents/test-developer.agent.md @@ -1,16 +1,16 @@ --- -description: "Write and maintain tests for commands using Mocha patterns." +description: "Write and maintain tests for commands" name: "Test Developer" tools: ['execute/testFailure', 'execute/runTests', 'read/terminalLastCommand', 'read/problems', 'read/readFile', 'edit/createFile', 'edit/editFiles', 'search/codebase', 'search/fileSearch', 'search/listDirectory', 'search/searchResults', 'search/textSearch', 'search/usages', 'web', 'todo'] --- # Test Developer -Expert assistant for writing tests in this CLI project using Mocha patterns. +Expert assistant for writing tests in this CLI project using NodeJS test patterns. ## Project Testing Context -- **Test framework**: Mocha +- **Test framework**: NodeJS built-in test module - **Test location**: `test/commands/{command}.test.ts` - **Test assets**: `test/assets/` contains source images for testing - **Cleanup**: Tests create temporary directories (`assets/`, `android/`, `ios/`) that must be cleaned in hooks @@ -18,10 +18,10 @@ Expert assistant for writing tests in this CLI project using Mocha patterns. ## Test Structure Pattern ```typescript -import {expect} from 'chai' -import {afterEach, beforeEach, describe, it} from 'mocha' +import assert from 'node:assert/strict' import * as fs from 'node:fs' import * as path from 'node:path' +import {afterEach, beforeEach, describe, it} from 'node:test' import {runCommand} from '../helpers/run-command.js' @@ -76,7 +76,7 @@ describe('commandName', () => { ### Assertion Patterns -- **File existence**: `expect(fs.existsSync(path)).to.be.true` +- **File existence**: `assert.ok(fs.existsSync(path))` - **File content**: Read file and compare expected content - **Console output**: Capture `stdout` from `runCommand()` and verify messages - **Error handling**: Test with invalid inputs to verify error messages @@ -95,9 +95,6 @@ When testing image generation commands (icons, splash): # Run all tests pnpm test -# Run specific test file -pnpm test test/commands/icons.test.ts - # Run with verbose output pnpm test --verbose ``` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b432a58..52b9d58 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -52,7 +52,7 @@ src/ ```bash pnpm install # Install dependencies pnpm build # Compile TypeScript to dist/ -pnpm test # Run Mocha tests + lint +pnpm test # Run Node.js test runner with coverage + lint pnpm lint # ESLint only ``` @@ -64,6 +64,7 @@ pnpm lint # ESLint only ## Testing & Debugging - Tests live in `test/commands/{command}.test.ts` +- Uses Node.js built-in `node:test` runner with `node:assert/strict` - Use custom `runCommand()` helper from `test/helpers/run-command.ts` - Tests create temporary `assets/`, `android/`, `ios/` directories - cleaned up in `after`/`afterEach` hooks - Test assets stored in `test/assets/` diff --git a/.mocharc.json b/.mocharc.json deleted file mode 100644 index fa25f20..0000000 --- a/.mocharc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "require": [ - "ts-node/register" - ], - "watch-extensions": [ - "ts" - ], - "recursive": true, - "reporter": "spec", - "timeout": 60000, - "node-option": [ - "loader=ts-node/esm", - "experimental-specifier-resolution=node" - ] -} diff --git a/.vscode/launch.json b/.vscode/launch.json index b1991a7..1ffb073 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,9 +8,8 @@ "skipFiles": ["/**"], "runtimeExecutable": "node", "runtimeArgs": [ - "--loader", - "ts-node/esm", - "--no-warnings=ExperimentalWarning" + "--import", + "tsx" ], "program": "${workspaceFolder}/bin/dev.js", "args": ["dotenv", "dev"] @@ -22,9 +21,8 @@ "skipFiles": ["/**"], "runtimeExecutable": "node", "runtimeArgs": [ - "--loader", - "ts-node/esm", - "--no-warnings=ExperimentalWarning" + "--import", + "tsx" ], "program": "${workspaceFolder}/bin/dev.js", "args": ["icons", "./test/assets/icon.png", "--appName", "TestApp"] @@ -36,9 +34,8 @@ "skipFiles": ["/**"], "runtimeExecutable": "node", "runtimeArgs": [ - "--loader", - "ts-node/esm", - "--no-warnings=ExperimentalWarning" + "--import", + "tsx" ], "program": "${workspaceFolder}/bin/dev.js", "args": ["splash", "./test/assets/splashscreen.png", "--appName", "TestApp"] diff --git a/AGENTS.md b/AGENTS.md index 6b9fbb3..81e1104 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ This is a **zero-dependencies CLI tool** (`rn-toolbox`) that automates React Nat ```bash pnpm install # Install dependencies pnpm build # Compile TypeScript to dist/ -pnpm test # Run Mocha tests + lint +pnpm test # Run Node.js test runner with coverage + lint pnpm lint # ESLint only pnpm cleanup # Remove generated android/, ios/, dist/, .env ``` @@ -32,14 +32,16 @@ Use the development entry point during development: ## Testing Instructions - Tests are located in `test/commands/{command}.test.ts` -- Run `pnpm test` to execute all tests with Mocha +- Run `pnpm test` to execute all tests with Node.js built-in test runner +- Uses `node:test` runner with `node:assert/strict` for assertions +- TypeScript support via `tsx` loader - Run `pnpm lint` before committing to ensure ESLint passes - Tests create temporary `assets/`, `android/`, `ios/` directories - cleaned up in `after`/`afterEach` hooks - Test assets are stored in `test/assets/` To run a specific test file: ```bash -pnpm mocha --forbid-only "test/commands/icons.test.ts" +node --import tsx --test test/commands/icons.test.ts ``` ## Code Style diff --git a/bin/dev.cmd b/bin/dev.cmd index cec553b..96feba5 100644 --- a/bin/dev.cmd +++ b/bin/dev.cmd @@ -1,3 +1,3 @@ @echo off -node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* +node --import tsx "%~dp0\dev" %* diff --git a/bin/dev.js b/bin/dev.js index 9932757..2ae3c22 100755 --- a/bin/dev.js +++ b/bin/dev.js @@ -1,4 +1,4 @@ -#!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning +#!/usr/bin/env -S node --import tsx import { runCLI } from '../src/cli/runner.js' diff --git a/docs/002-MIGRATION-TEST-RUNNER.md b/docs/002-MIGRATION-TEST-RUNNER.md index a9a21dd..98384d9 100644 --- a/docs/002-MIGRATION-TEST-RUNNER.md +++ b/docs/002-MIGRATION-TEST-RUNNER.md @@ -1,7 +1,9 @@ # Migration Plan: Mocha → Node.js Test Runner -**Status:** Pending +**Status:** ✅ Completed **Created:** 2026-01-05 +**Updated:** 2026-01-11 +**Completed:** 2026-01-11 **Target:** Replace Mocha/Chai with Node.js built-in test runner, tsx, and node:assert --- @@ -22,12 +24,12 @@ ### Phase 1: Setup -- [ ] **1.1** Install tsx +- [x] **1.1** Install tsx ```bash pnpm add -D tsx ``` -- [ ] **1.2** Verify tsx works with node:test (quick spike) +- [x] **1.2** Verify tsx works with node:test (quick spike) ```bash node --import tsx --test test/commands/dotenv.test.ts ``` @@ -38,7 +40,7 @@ #### 2.1 Migrate dotenv.test.ts -- [ ] Update imports: +- [x] Update imports: ```typescript // Remove import {expect} from 'chai' @@ -57,14 +59,14 @@ #### 2.2 Migrate icons.test.ts -- [ ] Update imports (same pattern as 2.1) +- [x] Update imports (same pattern as 2.1) -- [ ] Add timeout to describe block: +- [x] Add timeout to describe block: ```typescript describe('icons', {timeout: 60_000}, () => { ``` -- [ ] Replace assertions: +- [x] Replace assertions: | Before | After | |--------|-------| | `expect(stdout).to.contain(...)` | `assert.ok(stdout.includes(...))` | @@ -75,14 +77,54 @@ #### 2.3 Migrate splash.test.ts -- [ ] Update imports (same pattern as 2.1) +- [x] Update imports (same pattern as 2.1) -- [ ] Add timeout to describe block: +- [x] Add timeout to describe block: ```typescript describe('splash', {timeout: 60_000}, () => { ``` -- [ ] Replace assertions (same patterns as 2.2) +- [x] Replace assertions (same patterns as 2.2) + +#### 2.4 Migrate app.utils.test.ts + +- [x] Update imports: + ```typescript + // Remove + import {expect} from 'chai' + + // Add + import assert from 'node:assert/strict' + import {afterEach, describe, it} from 'node:test' + ``` + +- [x] Replace assertions: + | Before | After | + |--------|-------| + | `expect(await extractAppName()).to.equal('TestApp')` | `assert.equal(await extractAppName(), 'TestApp')` | + | `expect(await extractAppName()).to.be.undefined` | `assert.equal(await extractAppName(), undefined)` | + +#### 2.5 Migrate color.utils.test.ts + +- [x] Update imports (same pattern as 2.4) + +- [x] Replace assertions: + | Before | After | + |--------|-------| + | `expect(result).to.include('test')` | `assert.ok(result.includes('test'))` | + | `expect(result).to.be.a('string')` | `assert.equal(typeof result, 'string')` | + +#### 2.6 Migrate file-utils.test.ts + +- [x] Update imports (same pattern as 2.4) + +- [x] Replace assertions: + | Before | After | + |--------|-------| + | `expect(result).to.be.true` | `assert.ok(result)` | + | `expect(result).to.be.false` | `assert.equal(result, false)` | + | `expect(fs.existsSync(dirPath)).to.be.true` | `assert.ok(fs.existsSync(dirPath))` | + | `expect(fs.statSync(dirPath).isDirectory()).to.be.true` | `assert.ok(fs.statSync(dirPath).isDirectory())` | --- @@ -90,30 +132,32 @@ #### 3.1 Update package.json -- [ ] Update scripts: +- [x] Update scripts: ```json { "scripts": { - "cleanup": "rimraf android/ ios/ dist/ coverage/ oclif.manifest.json .env", + "cleanup": "node -e \"const fs=require('fs');['android','ios','dist','coverage','.nyc_output','.env','tsconfig.tsbuildinfo'].forEach(p=>fs.rmSync(p,{force:true,recursive:true}))\"", "test": "node --import tsx --test --experimental-test-coverage 'test/**/*.test.ts'", "posttest": "pnpm run lint" } } ``` -- [ ] Remove devDependencies: + > **Note:** The test command runs tests in parallel by default. If file-based tests conflict, add `--test-concurrency=1`. + +- [x] Remove devDependencies: - `mocha` - `@types/mocha` - `chai` - `@types/chai` - `ts-node` -- [ ] Add devDependencies: +- [x] Add devDependencies: - `tsx` (already added in Phase 1) #### 3.2 Update bin/dev.js -- [ ] Change shebang from: +- [x] Change shebang from: ```javascript #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning ``` @@ -124,7 +168,7 @@ #### 3.3 Update bin/dev.cmd -- [ ] Change from: +- [x] Change from: ```cmd node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* ``` @@ -135,11 +179,11 @@ #### 3.4 Delete .mocharc.json -- [ ] Remove file: `.mocharc.json` +- [x] Remove file: `.mocharc.json` *(Manual step required: `rm .mocharc.json`)* #### 3.5 Update tsconfig.json -- [ ] Remove `ts-node` block: +- [x] Remove `ts-node` block: ```jsonc // Remove this entire section "ts-node": { @@ -149,7 +193,7 @@ #### 3.6 Update .vscode/launch.json -- [ ] Update runtimeArgs to use tsx instead of ts-node +- [x] Update runtimeArgs to use tsx instead of ts-node --- @@ -157,43 +201,44 @@ #### 4.1 Update .github/copilot-instructions.md -- [ ] Change test command reference from Mocha to Node.js test runner -- [ ] Update testing section +- [x] Change test command reference from Mocha to Node.js test runner +- [x] Update testing section #### 4.2 Update .github/agents/test-developer.agent.md -- [ ] Update test framework description -- [ ] Update test patterns and examples -- [ ] Update imports in code examples +- [x] Update test framework description +- [x] Update test patterns and examples +- [x] Update imports in code examples -#### 4.3 Update TODO.md +#### 4.3 Update AGENTS.md -- [ ] Update test file references (remove Mocha-specific notes) +- [x] Update "Testing Instructions" section to reference Node.js test runner +- [x] Update test command examples --- ### Phase 5: Cleanup & Verify -- [ ] **5.1** Run full test suite: +- [x] **5.1** Run full test suite: ```bash pnpm test ``` -- [ ] **5.2** Verify coverage output appears in stdout +- [x] **5.2** Verify coverage output appears in stdout -- [ ] **5.3** Remove old dependencies: +- [x] **5.3** Remove old dependencies *(Manual step required)*: ```bash pnpm remove mocha @types/mocha chai @types/chai ts-node ``` -- [ ] **5.4** Run tests again after dependency removal +- [x] **5.4** Run tests again after dependency removal -- [ ] **5.5** Verify bin/dev.js still works: +- [x] **5.5** Verify bin/dev.js still works: ```bash ./bin/dev.js icons --help ``` -- [ ] **5.6** Run lint to ensure no issues: +- [x] **5.6** Run lint to ensure no issues: ```bash pnpm lint ``` @@ -208,10 +253,13 @@ | `expect(x).to.eq(y)` | `assert.equal(x, y)` | | `expect(x).to.be.true` | `assert.ok(x)` | | `expect(x).to.be.false` | `assert.equal(x, false)` | +| `expect(x).to.be.undefined` | `assert.equal(x, undefined)` | | `expect(x).to.contain(y)` | `assert.ok(x.includes(y))` | +| `expect(x).to.include(y)` | `assert.ok(x.includes(y))` | | `expect(x).to.match(/re/)` | `assert.match(x, /re/)` | | `expect(x).to.deep.equal(y)` | `assert.deepEqual(x, y)` | | `expect(x).to.be.null` | `assert.equal(x, null)` | +| `expect(x).to.be.a('string')` | `assert.equal(typeof x, 'string')` | | `expect(fn).to.throw()` | `assert.throws(fn)` | --- @@ -219,19 +267,24 @@ ## Reference: Test File Template ```typescript -import {runCommand} from '@oclif/test' import assert from 'node:assert/strict' import fs from 'node:fs' import {after, afterEach, before, beforeEach, describe, it} from 'node:test' -import {rimrafSync} from 'rimraf' + +import {ExitCode} from '../../src/cli/errors.js' +import CommandClass from '../../src/commands/{command}.js' +import {runCommand} from '../helpers/run-command.js' describe('command-name', {timeout: 60_000}, () => { before(() => { - // One-time setup + // One-time setup (e.g., create assets directory, copy test files) + fs.mkdirSync('assets', {recursive: true}) + fs.copyFileSync('test/assets/icon.png', 'assets/icon.png') }) after(() => { // One-time teardown + fs.rmSync('assets', {force: true, recursive: true}) }) beforeEach(() => { @@ -239,20 +292,23 @@ describe('command-name', {timeout: 60_000}, () => { }) afterEach(() => { - // Per-test teardown + // Per-test teardown (e.g., remove generated directories) + for (const dir of ['android', 'ios']) { + fs.rmSync(dir, {force: true, recursive: true}) + } }) it('should do something', async () => { - const {stdout, error} = await runCommand(['command', '--flag', 'value']) + const {stdout, error} = await runCommand(CommandClass, ['--flag', 'value']) assert.ok(stdout.includes('expected output')) assert.equal(error, undefined) }) it('should fail gracefully', async () => { - const {error} = await runCommand(['command']) + const {error} = await runCommand(CommandClass, []) - assert.equal(error?.oclif?.exit, 2) + assert.equal(error?.exitCode, ExitCode.INVALID_ARGUMENT) }) }) ``` @@ -263,22 +319,17 @@ describe('command-name', {timeout: 60_000}, () => { ```diff "devDependencies": { - "@eslint/compat": "^2", - "@oclif/prettier-config": "^0.2.1", - "@oclif/test": "^4", - "@types/chai": "^5", - "@types/mocha": "^10", "@types/node": "^25", - "chai": "^6", "eslint": "^9", - "eslint-config-oclif": "^6", "eslint-config-prettier": "^10", - "mocha": "^11", - "oclif": "^4.22.6", - "rimraf": "^6", - "ts-node": "^10", + "tsx": "^4", - "typescript": "^5" + "typescript": "^5", + "typescript-eslint": "^8.52.0" } ``` diff --git a/docs/004-E2E-TEST-IMPLEMENTATION.md b/docs/004-E2E-TEST-IMPLEMENTATION.md index 6b246ed..5f63ead 100644 --- a/docs/004-E2E-TEST-IMPLEMENTATION.md +++ b/docs/004-E2E-TEST-IMPLEMENTATION.md @@ -162,7 +162,8 @@ Create `test/e2e/cli.test.ts`: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { expect } from 'chai' +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' import { ExitCode } from '../../src/cli/errors.js' import { runCLI } from '../helpers/run-cli.js' @@ -172,40 +173,40 @@ describe('CLI E2E', () => { it('shows help with --help flag', async () => { const { stdout, exitCode } = await runCLI(['--help']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.contain('rn-toolbox') - expect(stdout).to.contain('icons') - expect(stdout).to.contain('splash') - expect(stdout).to.contain('dotenv') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.ok(stdout.includes('rn-toolbox')) + assert.ok(stdout.includes('icons')) + assert.ok(stdout.includes('splash')) + assert.ok(stdout.includes('dotenv')) }) it('shows help with -h flag', async () => { const { stdout, exitCode } = await runCLI(['-h']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.contain('USAGE') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.ok(stdout.includes('USAGE')) }) it('shows version with --version flag', async () => { const { stdout, exitCode } = await runCLI(['--version']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.match(/rn-toolbox\/\d+\.\d+\.\d+/) - expect(stdout).to.contain('node-') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.match(stdout, /rn-toolbox\/\d+\.\d+\.\d+/) + assert.ok(stdout.includes('node-')) }) it('shows version with -V flag', async () => { const { stdout, exitCode } = await runCLI(['-V']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.match(/rn-toolbox\/\d+\.\d+\.\d+/) + assert.equal(exitCode, ExitCode.SUCCESS) + assert.match(stdout, /rn-toolbox\/\d+\.\d+\.\d+/) }) it('shows help when no command provided', async () => { const { stdout, exitCode } = await runCLI([]) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.contain('COMMANDS') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.ok(stdout.includes('COMMANDS')) }) }) @@ -213,17 +214,17 @@ describe('CLI E2E', () => { it('exits with error for unknown command', async () => { const { stderr, exitCode } = await runCLI(['unknown']) - expect(exitCode).to.equal(ExitCode.INVALID_ARGUMENT) - expect(stderr).to.contain('Unknown command: unknown') - expect(stderr).to.contain('Available commands') + assert.equal(exitCode, ExitCode.INVALID_ARGUMENT) + assert.ok(stderr.includes('Unknown command: unknown')) + assert.ok(stderr.includes('Available commands')) }) it('suggests available commands', async () => { const { stderr } = await runCLI(['icns']) // typo - expect(stderr).to.contain('icons') - expect(stderr).to.contain('splash') - expect(stderr).to.contain('dotenv') + assert.ok(stderr.includes('icons')) + assert.ok(stderr.includes('splash')) + assert.ok(stderr.includes('dotenv')) }) }) @@ -231,26 +232,26 @@ describe('CLI E2E', () => { it('shows icons command help', async () => { const { stdout, exitCode } = await runCLI(['icons', '--help']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.contain('Generate app icons') - expect(stdout).to.contain('--appName') - expect(stdout).to.contain('--verbose') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.ok(stdout.includes('Generate app icons')) + assert.ok(stdout.includes('--appName')) + assert.ok(stdout.includes('--verbose')) }) it('shows splash command help', async () => { const { stdout, exitCode } = await runCLI(['splash', '--help']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.contain('Generate app splashscreens') - expect(stdout).to.contain('--appName') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.ok(stdout.includes('Generate app splashscreens')) + assert.ok(stdout.includes('--appName')) }) it('shows dotenv command help', async () => { const { stdout, exitCode } = await runCLI(['dotenv', '--help']) - expect(exitCode).to.equal(ExitCode.SUCCESS) - expect(stdout).to.contain('Manage .env files') - expect(stdout).to.contain('ENVIRONMENTNAME') + assert.equal(exitCode, ExitCode.SUCCESS) + assert.ok(stdout.includes('Manage .env files')) + assert.ok(stdout.includes('ENVIRONMENTNAME')) }) }) @@ -258,19 +259,19 @@ describe('CLI E2E', () => { it('returns FILE_NOT_FOUND for missing source file', async () => { const { exitCode } = await runCLI(['icons', './nonexistent.png', '--appName', 'Test']) - expect(exitCode).to.equal(ExitCode.FILE_NOT_FOUND) + assert.equal(exitCode, ExitCode.FILE_NOT_FOUND) }) it('returns CONFIG_ERROR when app.json missing and no --appName', async () => { const { exitCode } = await runCLI(['icons'], { cwd: '/tmp' }) - expect(exitCode).to.equal(ExitCode.CONFIG_ERROR) + assert.equal(exitCode, ExitCode.CONFIG_ERROR) }) it('returns INVALID_ARGUMENT for missing required arg', async () => { const { exitCode } = await runCLI(['dotenv']) - expect(exitCode).to.equal(ExitCode.INVALID_ARGUMENT) + assert.equal(exitCode, ExitCode.INVALID_ARGUMENT) }) }) }) @@ -330,10 +331,10 @@ test/ pnpm test # Run only E2E tests -pnpm mocha --forbid-only "test/e2e/**/*.test.ts" +node --import tsx --test "test/e2e/**/*.test.ts" # Run only integration tests -pnpm mocha --forbid-only "test/commands/**/*.test.ts" +node --import tsx --test "test/commands/**/*.test.ts" ``` ### Package.json Scripts (Optional) @@ -341,9 +342,9 @@ pnpm mocha --forbid-only "test/commands/**/*.test.ts" ```json { "scripts": { - "test": "mocha --forbid-only \"test/**/*.test.ts\"", - "test:unit": "mocha --forbid-only \"test/commands/**/*.test.ts\" \"test/utils/**/*.test.ts\"", - "test:e2e": "mocha --forbid-only \"test/e2e/**/*.test.ts\"" + "test": "node --import tsx --test 'test/**/*.test.ts'", + "test:unit": "node --import tsx --test 'test/commands/**/*.test.ts' 'test/utils/**/*.test.ts'", + "test:e2e": "node --import tsx --test 'test/e2e/**/*.test.ts'" } } ``` @@ -360,7 +361,7 @@ E2E tests for production mode require the TypeScript to be compiled: pnpm build && pnpm test:e2e ``` -Alternatively, E2E tests can use `dev: true` option to test via `ts-node`. +Alternatively, E2E tests can use `dev: true` option to test via `tsx`. ### Test Isolation diff --git a/docs/005-CODE-REVIEW-IMPROVEMENTS.md b/docs/005-CODE-REVIEW-IMPROVEMENTS.md new file mode 100644 index 0000000..95c2b8c --- /dev/null +++ b/docs/005-CODE-REVIEW-IMPROVEMENTS.md @@ -0,0 +1,526 @@ +# Code Review - Potential Improvements + +**Date:** January 11, 2026 +**Reviewer:** AI Code Review (Code Reviewer Mode) +**Scope:** Comprehensive codebase review + +--- + +## Executive Summary + +Overall assessment: **A-** + +The React Native Toolbox codebase demonstrates excellent architecture, strong TypeScript practices, and good adherence to modern Node.js conventions. This document outlines potential improvements categorized by priority. + +--- + +## 🔴 Critical Issues + +### 1. Missing License Header in types.ts + +**File:** [src/types.ts](../src/types.ts) +**Severity:** Critical +**Current State:** File is missing the MPL-2.0 license header present in all other source files. + +**Fix Required:** +```typescript +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +export interface SplashscreenSize { + // ... rest of file +} +``` + +--- + +## 🟡 Medium Priority Issues + +### 2. Inconsistent Error Handling in CLI Runner + +**File:** [src/cli/runner.ts](../src/cli/runner.ts#L96-L101) +**Severity:** Medium +**Issue:** Error handling logic has unreachable code and inconsistent behavior. + +**Current Code:** +```typescript +try { + await command.run(argv.slice(1)) +} catch (err) { + if (err instanceof CommandError) { + error(err.message, err.exitCode) // Calls process.exit() + } + + throw err // Unreachable for CommandError, unhandled for other errors +} +``` + +**Problem:** +- The `error()` function calls `process.exit()`, making `throw err` unreachable for `CommandError` instances +- Non-`CommandError` errors are re-thrown without proper handling +- Inconsistent error reporting + +**Recommended Fix:** +```typescript +try { + await command.run(argv.slice(1)) +} catch (err) { + if (err instanceof CommandError) { + error(err.message, err.exitCode) + } + + // Handle unexpected errors + console.error('Unexpected error:', err) + process.exit(ExitCode.GENERAL_ERROR) +} +``` + +**Impact:** Better error handling and consistent exit behavior. + +--- + +### 3. Type Safety: Unsafe Type Assertions + +**Files:** +- [src/commands/icons.ts](../src/commands/icons.ts#L69) +- [src/commands/splash.ts](../src/commands/splash.ts#L61) + +**Severity:** Medium +**Issue:** Type assertions bypass TypeScript's type safety. + +**Current Code:** +```typescript +const appName = flags.appName as string | undefined +``` + +**Problem:** +Based on `ParsedArgs` type definition, `flags` values can be `boolean | string | undefined`. The type assertion bypasses compile-time safety. + +**Recommended Fix:** +```typescript +const appName = typeof flags.appName === 'string' ? flags.appName : undefined +``` + +Or alternatively, narrow the type with a type guard: +```typescript +function isStringOrUndefined(value: unknown): value is string | undefined { + return value === undefined || typeof value === 'string' +} + +const appName = isStringOrUndefined(flags.appName) ? flags.appName : undefined +``` + +**Impact:** Improved runtime type safety and clearer intent. + +--- + +### 4. Errors Don't Affect Exit Code + +**Files:** +- [src/commands/icons.ts](../src/commands/icons.ts#L88-L93) +- [src/commands/splash.ts](../src/commands/splash.ts#L79-L84) + +**Severity:** Medium +**Issue:** Commands can complete with partial failures but still exit with success code (0). + +**Current Behavior:** +```typescript +if (this.errors.length > 0) { + this.warn(`${yellow('⚠')} ${this.errors.length} asset(s) failed to generate:`) + for (const err of this.errors) { + this.log(` - ${err}`) + } +} +this.log(green('✔'), `Generated icons for '${cyan(appName)}' app.`) +// Exits with ExitCode.SUCCESS (0) even with errors +``` + +**Problem:** +- CI/CD pipelines may not detect partial failures +- Build processes might continue with incomplete assets +- No way for automation to distinguish between success and partial failure + +**Recommended Fix:** +```typescript +if (this.errors.length > 0) { + this.warn(`${yellow('⚠')} ${this.errors.length} asset(s) failed to generate:`) + for (const err of this.errors) { + this.log(` - ${err}`) + } + this.error( + `Failed to generate ${this.errors.length} asset(s)`, + ExitCode.GENERATION_ERROR + ) +} +``` + +**Alternative (Less Strict):** +Add a flag to control behavior: +```typescript +flags: { + // ... existing flags + strict: { + default: false, + description: 'Exit with error code if any asset fails to generate', + type: 'boolean', + } +} + +// In execute(): +if (this.errors.length > 0) { + this.warn(`${yellow('⚠')} ${this.errors.length} asset(s) failed to generate:`) + for (const err of this.errors) { + this.log(` - ${err}`) + } + + if (flags.strict) { + this.error( + `Failed to generate ${this.errors.length} asset(s)`, + ExitCode.GENERATION_ERROR + ) + } +} +``` + +**Impact:** Better CI/CD integration and failure detection. + +--- + +### 5. Potential Race Condition Documentation + +**File:** [src/commands/icons.ts](../src/commands/icons.ts#L133-L138) +**Severity:** Low-Medium +**Issue:** Parallel directory creation could benefit from clarifying comments. + +**Current Code:** +```typescript +private async generateAndroidIconsWithDensity(inputPath: string, outputDir: string, density: string, size: number) { + const densityFolderPath = join(outputDir, `mipmap-${density}`) + + await mkdirp(densityFolderPath) + + // ... continues with file generation +} +``` + +**Context:** +Multiple density tasks run in parallel via `Promise.all()`, each calling `mkdirp()`. While Node.js `mkdir()` with `recursive: true` is safe for concurrent calls to the same path, this could be clearer. + +**Recommended Improvement:** +Add a comment explaining the safety: +```typescript +private async generateAndroidIconsWithDensity(inputPath: string, outputDir: string, density: string, size: number) { + const densityFolderPath = join(outputDir, `mipmap-${density}`) + + // Safe for concurrent execution - mkdir with recursive:true is idempotent + await mkdirp(densityFolderPath) + + // ... continues with file generation +} +``` + +**Impact:** Improved code readability and maintenance confidence. + +--- + +## 🔵 Low Priority / Enhancements + +### 6. Code Duplication in Splashscreen Generation + +**Files:** +- [src/commands/splash.ts](../src/commands/splash.ts#L94-L107) +- [src/commands/splash.ts](../src/commands/splash.ts#L109-L139) + +**Severity:** Low +**Issue:** iOS and Android splashscreen generation share very similar structure. + +**Current Pattern:** +```typescript +// Both platforms: +// 1. Create output directory +// 2. Iterate over size definitions +// 3. Call sharp() to resize +// 4. Generate Contents.json (iOS only) +``` + +**Potential Refactoring:** +```typescript +private async generateSplashscreensForPlatform( + platform: 'ios' | 'android', + inputFile: string, + outputDir: string, + sizes: SplashscreenSize[], + generateManifest?: (dir: string, sizes: SplashscreenSize[]) => Promise +) { + this.log(yellow('≈'), cyan(platform), 'Generating splashscreens...') + await mkdirp(outputDir) + + await Promise.all( + sizes.map((sizeDef) => this.generateSplashscreen(inputFile, outputDir, sizeDef)) + ) + + if (generateManifest) { + await generateManifest(outputDir, sizes) + } + + this.log(green('✔'), cyan(platform), 'Splashscreens generated.') +} +``` + +**Note:** This is optional - current code is clear and maintainable. Only refactor if adding more platforms or similar commands. + +**Impact:** Reduced code duplication, but may reduce clarity for two-platform use case. + +--- + +### 7. Missing JSDoc Comments + +**Files:** Multiple utility files +**Severity:** Low +**Issue:** Public API methods lack documentation. + +**Examples:** +- [src/utils/app.utils.ts](../src/utils/app.utils.ts#L10) - `extractAppName()` +- [src/utils/file-utils.ts](../src/utils/file-utils.ts#L10) - `checkAssetFile()` +- [src/utils/color.utils.ts](../src/utils/color.utils.ts) - Color helpers + +**Recommended Enhancement:** +```typescript +/** + * Extracts the app name from app.json in the current directory. + * + * @returns The app name if found and valid, undefined otherwise + * @example + * const name = await extractAppName() + * if (name) { + * console.log(`Found app: ${name}`) + * } + */ +export async function extractAppName(): Promise { + // ... existing implementation +} +``` + +**Impact:** Improved developer experience and IDE autocomplete hints. + +--- + +### 8. Hardcoded CLI Binary Name + +**File:** [src/cli/help.ts](../src/cli/help.ts#L11) +**Severity:** Low +**Issue:** CLI name "rn-toolbox" is hardcoded instead of using a constant. + +**Current Code:** +```typescript +const CLI_BIN = 'rn-toolbox' +``` + +**Improvement:** +Extract to a shared constant file or read from package.json: + +```typescript +// Option 1: Shared constant +// src/constants.ts +export const CLI_BIN = 'rn-toolbox' + +// Option 2: Read from package.json (like version) +async function getCliName(): Promise { + try { + const packagePath = fileURLToPath(new URL('../../package.json', import.meta.url)) + const content = await readFile(packagePath, 'utf8') + const pkg = JSON.parse(content) as { bin?: Record } + return Object.keys(pkg.bin || {})[0] || 'rn-toolbox' + } catch { + return 'rn-toolbox' + } +} +``` + +**Impact:** Better maintainability if binary name changes. + +--- + +### 9. Outdated ESLint Comment + +**File:** [eslint.config.mjs](../eslint.config.mjs#L12) +**Severity:** Low +**Issue:** Comment references Chai but project now uses `node:assert/strict`. + +**Current Code:** +```javascript +{ + // Test files use Chai's expect().to.be.true style which triggers this rule + files: ["test/**/*.ts"], + rules: { + "@typescript-eslint/no-unused-expressions": "off", + }, +}, +``` + +**Fix:** +```javascript +{ + // Disable no-unused-expressions for test files + // Note: May no longer be necessary with node:assert/strict after Chai migration + files: ["test/**/*.ts"], + rules: { + "@typescript-eslint/no-unused-expressions": "off", + }, +}, +``` + +**Action Item:** +Verify if this rule is still needed with Node.js test runner and remove if not necessary. + +**Impact:** Accurate documentation and potentially stricter linting in tests. + +--- + +### 10. Defensive Boolean Coercion + +**File:** [src/commands/base.ts](../src/commands/base.ts#L31) +**Severity:** Low +**Issue:** Unnecessary defensive coding with `Boolean()` coercion. + +**Current Code:** +```typescript +this._isVerbose = Boolean(parsed.flags.verbose) +``` + +**Context:** +The parser guarantees that boolean flags are typed as boolean. The `Boolean()` wrapper is defensive but unnecessary. + +**Recommendation:** +Either: +1. Keep as-is for extra safety +2. Simplify to: `this._isVerbose = parsed.flags.verbose ?? false` +3. Add a type guard if concerned about type safety + +**Impact:** Minimal - code clarity preference. + +--- + +### 11. Consider Add File Size Recommendations + +**Files:** +- [src/commands/icons.ts](../src/commands/icons.ts#L35-L37) +- [src/commands/splash.ts](../src/commands/splash.ts#L35-L37) + +**Severity:** Low +**Enhancement:** Add validation for minimum recommended file sizes. + +**Suggested Addition:** +```typescript +// In icons command execute() +const metadata = await sharp(file).metadata() +if (metadata.width < 1024 || metadata.height < 1024) { + this.warn( + `${yellow('⚠')} Icon file is ${metadata.width}x${metadata.height}. ` + + `Recommended minimum: 1024x1024px for best quality.` + ) +} +``` + +**Impact:** Better user guidance and asset quality. + +--- + +### 12. Add --dry-run Flag + +**Files:** All commands +**Severity:** Low +**Enhancement:** Add ability to preview what would be generated without actually generating files. + +**Suggested Implementation:** +```typescript +flags: { + // ... existing flags + dryRun: { + default: false, + description: 'Preview what would be generated without creating files', + short: 'd', + type: 'boolean', + } +} + +// In execute(): +if (flags.dryRun) { + this.log(yellow('≈'), 'DRY RUN - No files will be created') + // List what would be created + // Skip actual file generation + return +} +``` + +**Impact:** Better user experience for previewing operations. + +--- + +## ✅ Strengths to Maintain + +1. **Clean Architecture** - Excellent separation of CLI and command layers +2. **Type Safety** - Consistent use of TypeScript strict mode +3. **Parallel Processing** - Efficient use of `Promise.all()` for concurrent operations +4. **Error Handling** - Custom error class with semantic exit codes +5. **ESM Compliance** - Proper `.js` extensions in all imports +6. **Modern Node.js** - Good use of built-in APIs (`util.parseArgs`, `util.styleText`) +7. **Test Coverage** - Comprehensive test suite with proper setup/teardown +8. **Zero Dependencies CLI** - CLI layer has no external dependencies +9. **Consistent Naming** - Clear, descriptive names throughout +10. **Code Organization** - Logical file structure and clear responsibilities + +--- + +## Priority Recommendations + +### Immediate Actions (Do Now) +1. ✅ Add license header to `src/types.ts` +2. ✅ Fix error handling in `src/cli/runner.ts` +3. ✅ Update ESLint comment to reflect current test framework + +### Short Term (This Sprint) +4. ✅ Replace type assertions with type guards in command files +5. ✅ Decide on exit code behavior for partial failures +6. ✅ Add JSDoc comments to public utility functions + +### Long Term (Future Enhancements) +7. ✅ Consider `--dry-run` flag for all commands +8. ✅ Add input file size validation and warnings +9. ✅ Evaluate if code duplication warrants refactoring +10. ✅ Extract CLI binary name to constant or read from package.json + +--- + +## Testing Recommendations + +All changes should include: +- Unit tests for new logic +- Integration tests for command behavior +- Verification that exit codes work correctly in CI/CD scenarios + +Run full test suite: +```bash +pnpm test +pnpm lint +``` + +--- + +## Conclusion + +The codebase is in excellent shape with only minor improvements suggested. The primary focus should be on: +1. Ensuring consistent error handling and exit codes +2. Improving type safety where assertions are used +3. Adding documentation for public APIs + +No breaking changes are necessary. All improvements can be implemented incrementally without disrupting existing functionality. + +**Overall Grade: A-** + +The code demonstrates professional quality and adherence to best practices. Recommended improvements are refinements rather than corrections of fundamental issues. diff --git a/package.json b/package.json index 7b4ac5a..ac87dad 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "homepage": "https://github.com/forwardsoftware/react-native-toolbox#readme", "bugs": "https://github.com/forwardsoftware/react-native-toolbox/issues", "scripts": { - "cleanup": "node -e \"const fs=require('fs');['android','ios','dist','.nyc_output','.env','tsconfig.tsbuildinfo'].forEach(p=>fs.rmSync(p,{force:true,recursive:true}))\"", + "cleanup": "node -e \"const fs=require('fs');['android','ios','dist','coverage','.nyc_output','.env','tsconfig.tsbuildinfo'].forEach(p=>fs.rmSync(p,{force:true,recursive:true}))\"", "prebuild": "pnpm run cleanup", "build": "tsc -b", "lint": "eslint", - "test": "mocha --forbid-only \"test/**/*.test.ts\"", + "test": "node --import tsx --test --test-concurrency=1 --experimental-test-coverage 'test/**/*.test.ts'", "check": "pnpm run lint && pnpm run test", "prepack": "pnpm build" }, @@ -43,14 +43,10 @@ "sharp": "^0.34.1" }, "devDependencies": { - "@types/chai": "^5", - "@types/mocha": "^10", "@types/node": "^25", - "chai": "^6", "eslint": "^9", "eslint-config-prettier": "^10", - "mocha": "^11", - "ts-node": "^10", + "tsx": "^4", "typescript": "^5", "typescript-eslint": "^8.52.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f19886..8f89d16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,30 +12,18 @@ importers: specifier: ^0.34.1 version: 0.34.5 devDependencies: - '@types/chai': - specifier: ^5 - version: 5.2.3 - '@types/mocha': - specifier: ^10 - version: 10.0.10 '@types/node': specifier: ^25 version: 25.0.6 - chai: - specifier: ^6 - version: 6.2.2 eslint: specifier: ^9 version: 9.39.2 eslint-config-prettier: specifier: ^10 version: 10.1.8(eslint@9.39.2) - mocha: - specifier: ^11 - version: 11.7.5 - ts-node: - specifier: ^10 - version: 10.9.2(@types/node@25.0.6)(typescript@5.9.3) + tsx: + specifier: ^4 + version: 4.21.0 typescript: specifier: ^5 version: 5.9.3 @@ -45,13 +33,165 @@ importers: packages: - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - '@emnapi/runtime@1.7.1': resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -243,51 +383,12 @@ packages: cpu: [x64] os: [win32] - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@tsconfig/node10@1.0.11': - resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - - '@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==} '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/mocha@10.0.10': - resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/node@25.0.6': resolution: {integrity: sha512-NNu0sjyNxpoiW3YuVFfNz7mxSQ+S4X2G28uqg2s+CzoqoQjLPsWSbsFFyztIAqt2vb8kfEAsJNepMGPTxFDx3Q==} @@ -355,10 +456,6 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -367,32 +464,13 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -402,33 +480,14 @@ packages: brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -439,9 +498,6 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -455,10 +511,6 @@ packages: supports-color: optional: true - decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -466,26 +518,10 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - - diff@7.0.0: - resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} - engines: {node: '>=0.3.1'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} @@ -569,29 +605,21 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - hasBin: true - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -600,10 +628,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -624,32 +648,13 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -677,16 +682,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -694,15 +689,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - mocha@11.7.5: - resolution: {integrity: sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -721,9 +707,6 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -736,13 +719,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -755,32 +731,18 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -793,26 +755,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.1.2: - resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -821,10 +763,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -835,23 +773,14 @@ packages: peerDependencies: typescript: '>=4.8.4' - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -874,9 +803,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -886,50 +812,93 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - workerpool@9.3.4: - resolution: {integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} +snapshots: - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.27.2': + optional: true - yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} + '@esbuild/android-arm64@0.27.2': + optional: true - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + '@esbuild/android-arm@0.27.2': + optional: true - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} + '@esbuild/android-x64@0.27.2': + optional: true - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} + '@esbuild/darwin-arm64@0.27.2': + optional: true -snapshots: + '@esbuild/darwin-x64@0.27.2': + optional: true - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 + '@esbuild/freebsd-arm64@0.27.2': + optional: true - '@emnapi/runtime@1.7.1': - dependencies: - tslib: 2.8.1 + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': optional: true '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2)': @@ -942,7 +911,7 @@ snapshots: '@eslint/config-array@0.21.1': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -958,7 +927,7 @@ snapshots: '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -1085,48 +1054,10 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@tsconfig/node10@1.0.11': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} - '@types/mocha@10.0.10': {} - '@types/node@25.0.6': dependencies: undici-types: 7.16.0 @@ -1153,7 +1084,7 @@ snapshots: '@typescript-eslint/types': 8.52.0 '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.52.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 eslint: 9.39.2 typescript: 5.9.3 transitivePeerDependencies: @@ -1163,7 +1094,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) '@typescript-eslint/types': 8.52.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -1182,7 +1113,7 @@ snapshots: '@typescript-eslint/types': 8.52.0 '@typescript-eslint/typescript-estree': 8.52.0(typescript@5.9.3) '@typescript-eslint/utils': 8.52.0(eslint@9.39.2)(typescript@5.9.3) - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 eslint: 9.39.2 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 @@ -1197,7 +1128,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.52.0(typescript@5.9.3) '@typescript-eslint/types': 8.52.0 '@typescript-eslint/visitor-keys': 8.52.0 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 @@ -1226,10 +1157,6 @@ snapshots: dependencies: acorn: 8.15.0 - acorn-walk@8.3.4: - dependencies: - acorn: 8.15.0 - acorn@8.15.0: {} ajv@6.12.6: @@ -1239,22 +1166,12 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.3: {} - - arg@4.1.3: {} - argparse@2.0.1: {} - assertion-error@2.0.1: {} - balanced-match@1.0.2: {} brace-expansion@1.1.12: @@ -1266,29 +1183,13 @@ snapshots: dependencies: balanced-match: 1.0.2 - browser-stdout@1.3.1: {} - callsites@3.1.0: {} - camelcase@6.3.0: {} - - chai@6.2.2: {} - chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -1297,37 +1198,48 @@ snapshots: concat-map@0.0.1: {} - create-require@1.1.1: {} - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - debug@4.4.3(supports-color@8.1.1): + debug@4.4.3: dependencies: ms: 2.1.3 - optionalDependencies: - supports-color: 8.1.1 - - decamelize@4.0.0: {} deep-is@0.1.4: {} detect-libc@2.1.2: {} - diff@4.0.2: {} - - diff@7.0.0: {} - - eastasianwidth@0.2.0: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - escalade@3.2.0: {} + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 escape-string-regexp@4.0.0: {} @@ -1361,7 +1273,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3(supports-color@8.1.1) + debug: 4.4.3 escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -1425,36 +1337,23 @@ snapshots: flatted: 3.3.3 keyv: 4.5.4 - flat@5.0.2: {} - flatted@3.3.3: {} - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 + fsevents@2.3.3: + optional: true - get-caller-file@2.0.5: {} + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.5 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - globals@14.0.0: {} has-flag@4.0.0: {} - he@1.2.0: {} - ignore@5.3.2: {} ignore@7.0.5: {} @@ -1468,26 +1367,12 @@ snapshots: is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-path-inside@3.0.3: {} - - is-plain-obj@2.1.0: {} - - is-unicode-supported@0.1.0: {} - isexe@2.0.0: {} - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -1513,15 +1398,6 @@ snapshots: lodash.merge@4.6.2: {} - log-symbols@4.1.0: - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - - lru-cache@10.4.3: {} - - make-error@1.3.6: {} - minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -1530,32 +1406,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minipass@7.1.2: {} - - mocha@11.7.5: - dependencies: - browser-stdout: 1.3.1 - chokidar: 4.0.3 - debug: 4.4.3(supports-color@8.1.1) - diff: 7.0.0 - escape-string-regexp: 4.0.0 - find-up: 5.0.0 - glob: 10.5.0 - he: 1.2.0 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 - log-symbols: 4.1.0 - minimatch: 9.0.5 - ms: 2.1.3 - picocolors: 1.1.1 - serialize-javascript: 6.0.2 - strip-json-comments: 3.1.1 - supports-color: 8.1.1 - workerpool: 9.3.4 - yargs: 17.7.2 - yargs-parser: 21.1.1 - yargs-unparser: 2.0.0 - ms@2.1.3: {} natural-compare@1.4.0: {} @@ -1577,8 +1427,6 @@ snapshots: dependencies: p-limit: 3.1.0 - package-json-from-dist@1.0.1: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -1587,37 +1435,18 @@ snapshots: path-key@3.1.1: {} - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - picocolors@1.1.1: {} - picomatch@4.0.3: {} prelude-ls@1.2.1: {} punycode@2.3.1: {} - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - readdirp@4.1.2: {} - - require-directory@2.1.1: {} - resolve-from@4.0.0: {} - safe-buffer@5.2.1: {} + resolve-pkg-maps@1.0.0: {} semver@7.7.3: {} - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - sharp@0.34.5: dependencies: '@img/colour': 1.0.0 @@ -1655,38 +1484,12 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@4.1.0: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.2 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.1.2: - dependencies: - ansi-regex: 6.2.2 - strip-json-comments@3.1.1: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -1696,27 +1499,16 @@ snapshots: dependencies: typescript: 5.9.3 - ts-node@10.9.2(@types/node@25.0.6)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.11 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 25.0.6 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - tslib@2.8.1: optional: true + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -1740,49 +1532,10 @@ snapshots: dependencies: punycode: 2.3.1 - v8-compile-cache-lib@3.0.1: {} - which@2.0.2: dependencies: isexe: 2.0.0 word-wrap@1.2.5: {} - workerpool@9.3.4: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.1.2 - - y18n@5.0.8: {} - - yargs-parser@21.1.1: {} - - yargs-unparser@2.0.0: - dependencies: - camelcase: 6.3.0 - decamelize: 4.0.0 - flat: 5.0.2 - is-plain-obj: 2.1.0 - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yn@3.1.1: {} - yocto-queue@0.1.0: {} diff --git a/test/cli/errors.test.ts b/test/cli/errors.test.ts new file mode 100644 index 0000000..0d5a59f --- /dev/null +++ b/test/cli/errors.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' + +import {CommandError, ExitCode} from '../../src/cli/errors.js' + +describe('errors', () => { + describe('ExitCode', () => { + it('defines SUCCESS as 0', () => { + assert.equal(ExitCode.SUCCESS, 0) + }) + + it('defines GENERAL_ERROR as 1', () => { + assert.equal(ExitCode.GENERAL_ERROR, 1) + }) + + it('defines INVALID_ARGUMENT as 2', () => { + assert.equal(ExitCode.INVALID_ARGUMENT, 2) + }) + + it('defines FILE_NOT_FOUND as 3', () => { + assert.equal(ExitCode.FILE_NOT_FOUND, 3) + }) + + it('defines CONFIG_ERROR as 4', () => { + assert.equal(ExitCode.CONFIG_ERROR, 4) + }) + + it('defines GENERATION_ERROR as 5', () => { + assert.equal(ExitCode.GENERATION_ERROR, 5) + }) + }) + + describe('CommandError', () => { + it('creates error with message and default exit code', () => { + const err = new CommandError('Test error') + assert.equal(err.message, 'Test error') + assert.equal(err.exitCode, ExitCode.GENERAL_ERROR) + assert.equal(err.name, 'CommandError') + }) + + it('creates error with custom exit code', () => { + const err = new CommandError('File not found', ExitCode.FILE_NOT_FOUND) + assert.equal(err.message, 'File not found') + assert.equal(err.exitCode, ExitCode.FILE_NOT_FOUND) + }) + + it('extends Error class', () => { + const err = new CommandError('Test') + assert.ok(err instanceof Error) + assert.ok(err instanceof CommandError) + }) + + it('can be caught and inspected', () => { + try { + throw new CommandError('Invalid argument', ExitCode.INVALID_ARGUMENT) + } catch (err) { + assert.ok(err instanceof CommandError) + if (err instanceof CommandError) { + assert.equal(err.exitCode, ExitCode.INVALID_ARGUMENT) + } + } + }) + }) +}) diff --git a/test/cli/help.test.ts b/test/cli/help.test.ts new file mode 100644 index 0000000..b61f96a --- /dev/null +++ b/test/cli/help.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' + +import {generateCommandHelp, generateGlobalHelp} from '../../src/cli/help.js' +import type {CommandConfig} from '../../src/cli/types.js' + +describe('help', () => { + describe('generateCommandHelp', () => { + it('generates help text with description', () => { + const config: CommandConfig = { + args: [], + description: 'Test command description', + examples: [], + flags: {}, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /Test command description/) + }) + + it('generates USAGE section', () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /USAGE/) + assert.match(help, /\$ rn-toolbox test/) + }) + + it('includes required arguments in usage', () => { + const config: CommandConfig = { + args: [ + { + description: 'Environment name', + name: 'environment', + required: true, + }, + ], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, //) + }) + + it('includes optional arguments in usage', () => { + const config: CommandConfig = { + args: [ + { + description: 'Optional arg', + name: 'optional', + required: false, + }, + ], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /\[optional\]/) + }) + + it('generates ARGUMENTS section with defaults', () => { + const config: CommandConfig = { + args: [ + { + default: 'default-value', + description: 'Argument with default', + name: 'arg', + required: false, + }, + ], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /ARGUMENTS/) + assert.match(help, /\[default: default-value\]/) + }) + + it('generates FLAGS section', () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + verbose: { + description: 'Verbose output', + short: 'v', + type: 'boolean', + }, + }, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /FLAGS/) + assert.match(help, /-v, --verbose/) + }) + + it('shows string flag value placeholder', () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + output: { + description: 'Output path', + type: 'string', + }, + }, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /--output=/) + }) + + it('generates EXAMPLES section', () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [ + '$ <%= config.bin %> <%= command.id %> --verbose', + '$ <%= config.bin %> <%= command.id %> production', + ], + flags: {}, + name: 'test', + } + + const help = generateCommandHelp(config) + assert.match(help, /EXAMPLES/) + assert.match(help, /\$ rn-toolbox test --verbose/) + assert.match(help, /\$ rn-toolbox test production/) + }) + + it('omits empty sections', () => { + const config: CommandConfig = { + args: [], + description: 'Minimal command', + examples: [], + flags: {}, + name: 'minimal', + } + + const help = generateCommandHelp(config) + assert.doesNotMatch(help, /ARGUMENTS/) + assert.doesNotMatch(help, /FLAGS/) + assert.doesNotMatch(help, /EXAMPLES/) + }) + }) + + describe('generateGlobalHelp', () => { + it('generates help with version', () => { + const commands: CommandConfig[] = [] + const help = generateGlobalHelp(commands, '1.0.0') + assert.match(help, /rn-toolbox\/1\.0\.0/) + }) + + it('includes global description', () => { + const commands: CommandConfig[] = [] + const help = generateGlobalHelp(commands, '1.0.0') + assert.match(help, /A set of scripts to simplify React Native development/) + }) + + it('lists all commands', () => { + const commands: CommandConfig[] = [ + { + args: [], + description: 'First command', + examples: [], + flags: {}, + name: 'first', + }, + { + args: [], + description: 'Second command with longer description', + examples: [], + flags: {}, + name: 'second', + }, + ] + + const help = generateGlobalHelp(commands, '1.0.0') + assert.match(help, /COMMANDS/) + assert.match(help, /first\s+First command/) + assert.match(help, /second\s+Second command/) + }) + + it('includes global flags', () => { + const commands: CommandConfig[] = [] + const help = generateGlobalHelp(commands, '1.0.0') + assert.match(help, /FLAGS/) + assert.match(help, /-h, --help/) + assert.match(help, /-V, --version/) + }) + + it('includes usage hint', () => { + const commands: CommandConfig[] = [] + const help = generateGlobalHelp(commands, '1.0.0') + assert.match(help, /Run 'rn-toolbox --help' for more information/) + }) + }) +}) diff --git a/test/cli/output.test.ts b/test/cli/output.test.ts new file mode 100644 index 0000000..b5505d6 --- /dev/null +++ b/test/cli/output.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' + +import {ExitCode} from '../../src/cli/errors.js' +import {error, log, logVerbose, warn} from '../../src/cli/output.js' + +describe('output', () => { + describe('log', () => { + it('logs to console', () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + log('test message') + assert.equal(logs[0], 'test message') + } finally { + console.log = originalLog + } + }) + + it('logs multiple arguments', () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + log('message', 123, true) + assert.equal(logs[0], 'message 123 true') + } finally { + console.log = originalLog + } + }) + }) + + describe('warn', () => { + it('warns to console', () => { + const warnings: string[] = [] + const originalWarn = console.warn + console.warn = (...args: unknown[]) => { + warnings.push(args.join(' ')) + } + + try { + warn('warning message') + assert.equal(warnings[0], 'warning message') + } finally { + console.warn = originalWarn + } + }) + }) + + describe('error', () => { + it('logs error and exits with default code', () => { + const errors: string[] = [] + const originalError = console.error + const originalExit = process.exit + + console.error = (...args: unknown[]) => { + errors.push(args.join(' ')) + } + + let exitCode = 0 + process.exit = ((code?: number) => { + exitCode = code ?? 0 + throw new Error('process.exit') + }) as never + + try { + assert.throws(() => { + error('test error') + }) + assert.ok(errors[0]?.includes('test error')) + assert.equal(exitCode, ExitCode.GENERAL_ERROR) + } finally { + console.error = originalError + process.exit = originalExit + } + }) + + it('logs error and exits with custom code', () => { + const errors: string[] = [] + const originalError = console.error + const originalExit = process.exit + + console.error = (...args: unknown[]) => { + errors.push(args.join(' ')) + } + + let exitCode = 0 + process.exit = ((code?: number) => { + exitCode = code ?? 0 + throw new Error('process.exit') + }) as never + + try { + assert.throws(() => { + error('config error', ExitCode.CONFIG_ERROR) + }) + assert.ok(errors[0]?.includes('config error')) + assert.equal(exitCode, ExitCode.CONFIG_ERROR) + } finally { + console.error = originalError + process.exit = originalExit + } + }) + }) + + describe('logVerbose', () => { + it('logs when verbose is true', () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + logVerbose(true, 'verbose message') + assert.equal(logs[0], 'verbose message') + } finally { + console.log = originalLog + } + }) + + it('does not log when verbose is false', () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + logVerbose(false, 'should not appear') + assert.equal(logs.length, 0) + } finally { + console.log = originalLog + } + }) + }) +}) diff --git a/test/cli/parser.test.ts b/test/cli/parser.test.ts new file mode 100644 index 0000000..fb9014e --- /dev/null +++ b/test/cli/parser.test.ts @@ -0,0 +1,232 @@ +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' + +import {CommandError, ExitCode} from '../../src/cli/errors.js' +import {parseArgs} from '../../src/cli/parser.js' +import type {CommandConfig} from '../../src/cli/types.js' + +describe('parser', () => { + describe('parseArgs', () => { + it('parses string flags correctly', async () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + appName: { + description: 'App name', + type: 'string', + }, + }, + name: 'test', + } + + const result = await parseArgs(['--appName', 'MyApp'], config) + assert.equal(result.flags.appName, 'MyApp') + }) + + it('parses boolean flags correctly', async () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + verbose: { + description: 'Verbose output', + short: 'v', + type: 'boolean', + }, + }, + name: 'test', + } + + const result = await parseArgs(['-v'], config) + assert.equal(result.flags.verbose, true) + }) + + it('parses short flags correctly', async () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + help: { + description: 'Show help', + short: 'h', + type: 'boolean', + }, + }, + name: 'test', + } + + const result = await parseArgs(['-h'], config) + assert.equal(result.flags.help, true) + }) + + it('parses required positional arguments', async () => { + const config: CommandConfig = { + args: [ + { + description: 'Environment name', + name: 'environment', + required: true, + }, + ], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + const result = await parseArgs(['development'], config) + assert.equal(result.args.environment, 'development') + }) + + it('parses optional positional arguments with defaults', async () => { + const config: CommandConfig = { + args: [ + { + default: 'default-value', + description: 'Optional argument', + name: 'optional', + required: false, + }, + ], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + const result = await parseArgs([], config) + assert.equal(result.args.optional, 'default-value') + }) + + it('uses flag defaults when not provided', async () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + output: { + default: './output', + description: 'Output directory', + type: 'string', + }, + }, + name: 'test', + } + + const result = await parseArgs([], config) + assert.equal(result.flags.output, './output') + }) + + it('supports async function defaults for flags', async () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + computed: { + default: async () => 'computed-value', + description: 'Computed flag', + type: 'string', + }, + }, + name: 'test', + } + + const result = await parseArgs([], config) + assert.equal(result.flags.computed, 'computed-value') + }) + + it('throws CommandError for missing required arguments', async () => { + const config: CommandConfig = { + args: [ + { + description: 'Required arg', + name: 'required', + required: true, + }, + ], + description: 'Test command', + examples: [], + flags: {}, + name: 'test', + } + + await assert.rejects( + async () => parseArgs([], config), + (err: unknown) => { + assert.ok(err instanceof CommandError) + assert.equal(err.exitCode, ExitCode.INVALID_ARGUMENT) + assert.match(err.message, /Missing required argument/) + return true + }, + ) + }) + + it('throws CommandError for invalid flags', async () => { + const config: CommandConfig = { + args: [], + description: 'Test command', + examples: [], + flags: { + valid: { + description: 'Valid flag', + type: 'boolean', + }, + }, + name: 'test', + } + + await assert.rejects( + async () => parseArgs(['--invalid-flag'], config), + (err: unknown) => { + assert.ok(err instanceof CommandError) + assert.equal(err.exitCode, ExitCode.INVALID_ARGUMENT) + return true + }, + ) + }) + + it('parses multiple flags and arguments together', async () => { + const config: CommandConfig = { + args: [ + { + description: 'First arg', + name: 'first', + required: true, + }, + ], + description: 'Test command', + examples: [], + flags: { + output: { + description: 'Output path', + type: 'string', + }, + verbose: { + description: 'Verbose', + short: 'v', + type: 'boolean', + }, + }, + name: 'test', + } + + const result = await parseArgs(['value1', '--output', '/tmp', '-v'], config) + assert.equal(result.args.first, 'value1') + assert.equal(result.flags.output, '/tmp') + assert.equal(result.flags.verbose, true) + }) + }) +}) diff --git a/test/cli/runner.test.ts b/test/cli/runner.test.ts new file mode 100644 index 0000000..f40ffe3 --- /dev/null +++ b/test/cli/runner.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' + +import {runCLI} from '../../src/cli/runner.js' + +describe('runner', () => { + describe('runCLI', () => { + it('shows version with --version flag', async () => { + // Capture console output + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + await runCLI(['--version']) + assert.ok(logs.some((log) => log.includes('rn-toolbox/'))) + } finally { + console.log = originalLog + } + }) + + it('shows version with -V flag', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + await runCLI(['-V']) + assert.ok(logs.some((log) => log.includes('rn-toolbox/'))) + } finally { + console.log = originalLog + } + }) + + it('shows global help with --help flag', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + await runCLI(['--help']) + const output = logs.join('\n') + assert.match(output, /USAGE/) + assert.match(output, /COMMANDS/) + } finally { + console.log = originalLog + } + }) + + it('shows global help with -h flag', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + await runCLI(['-h']) + const output = logs.join('\n') + assert.match(output, /USAGE/) + } finally { + console.log = originalLog + } + }) + + it('shows global help when no arguments provided', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + await runCLI([]) + const output = logs.join('\n') + assert.match(output, /COMMANDS/) + } finally { + console.log = originalLog + } + }) + + it('exits with error for unknown command', async () => { + const errors: string[] = [] + const originalError = console.error + const originalExit = process.exit + + console.error = (...args: unknown[]) => { + errors.push(args.join(' ')) + } + + let exitCode = 0 + process.exit = ((code?: number) => { + exitCode = code ?? 0 + throw new Error('process.exit') + }) as never + + try { + await assert.rejects( + async () => runCLI(['unknown-command']), + (err: unknown) => { + assert.ok(err instanceof Error) + assert.equal(err.message, 'process.exit') + return true + }, + ) + assert.ok(errors.some((err) => err.includes('Unknown command'))) + assert.equal(exitCode, 2) // INVALID_ARGUMENT + } finally { + console.error = originalError + process.exit = originalExit + } + }) + }) +}) diff --git a/test/commands/base.test.ts b/test/commands/base.test.ts new file mode 100644 index 0000000..6ac4f4a --- /dev/null +++ b/test/commands/base.test.ts @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2025 ForWarD Software (https://forwardsoftware.solutions/) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' + +import {BaseCommand, CommandError, ExitCode} from '../../src/commands/base.js' +import type {CommandConfig, ParsedArgs} from '../../src/cli/types.js' + +// Test implementation of BaseCommand +class TestCommand extends BaseCommand { + readonly config: CommandConfig = { + args: [ + { + description: 'Test argument', + name: 'testArg', + required: false, + }, + ], + description: 'Test command for unit testing', + examples: ['$ rn-toolbox test example'], + flags: { + help: { + description: 'Show help', + short: 'h', + type: 'boolean', + }, + verbose: { + description: 'Verbose output', + short: 'v', + type: 'boolean', + }, + }, + name: 'test', + } + + executeCalled = false + executeArgs: ParsedArgs | null = null + + async execute(parsed: ParsedArgs): Promise { + this.executeCalled = true + this.executeArgs = parsed + } +} + +describe('base', () => { + describe('BaseCommand', () => { + it('runs execute when no help flag', async () => { + const cmd = new TestCommand() + await cmd.run([]) + + assert.ok(cmd.executeCalled) + }) + + it('shows help when --help flag is passed', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + const cmd = new TestCommand() + await cmd.run(['--help']) + + // Should not execute + assert.ok(!cmd.executeCalled) + + // Should show help + const output = logs.join('\n') + assert.match(output, /Test command for unit testing/) + assert.match(output, /USAGE/) + } finally { + console.log = originalLog + } + }) + + it('shows help when -h flag is passed', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + const cmd = new TestCommand() + await cmd.run(['-h']) + + assert.ok(!cmd.executeCalled) + + const output = logs.join('\n') + assert.match(output, /USAGE/) + } finally { + console.log = originalLog + } + }) + + it('sets verbose flag correctly', async () => { + const cmd = new TestCommand() + await cmd.run(['-v']) + + assert.ok(cmd.executeCalled) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + assert.equal((cmd as any)._isVerbose, true) + }) + + it('passes parsed args to execute', async () => { + const cmd = new TestCommand() + await cmd.run(['testValue']) + + assert.ok(cmd.executeCalled) + assert.equal(cmd.executeArgs?.args.testArg, 'testValue') + }) + + describe('log', () => { + it('logs to console', () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + const cmd = new TestCommand() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(cmd as any).log('test message') + assert.equal(logs[0], 'test message') + } finally { + console.log = originalLog + } + }) + }) + + describe('logVerbose', () => { + it('logs when verbose is true', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + const cmd = new TestCommand() + await cmd.run(['-v']) + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(cmd as any).logVerbose('verbose message') + assert.ok(logs.some((log) => log.includes('verbose message'))) + } finally { + console.log = originalLog + } + }) + + it('does not log when verbose is false', async () => { + const logs: string[] = [] + const originalLog = console.log + console.log = (...args: unknown[]) => { + logs.push(args.join(' ')) + } + + try { + const cmd = new TestCommand() + await cmd.run([]) + + // Clear logs from execute + logs.length = 0 + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(cmd as any).logVerbose('should not appear') + assert.equal(logs.length, 0) + } finally { + console.log = originalLog + } + }) + }) + + describe('warn', () => { + it('warns with colored prefix', () => { + const warnings: string[] = [] + const originalWarn = console.warn + console.warn = (...args: unknown[]) => { + warnings.push(args.join(' ')) + } + + try { + const cmd = new TestCommand() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(cmd as any).warn('test warning') + assert.ok(warnings[0]?.includes('Warning:')) + assert.ok(warnings[0]?.includes('test warning')) + } finally { + console.warn = originalWarn + } + }) + }) + + describe('error', () => { + it('throws CommandError with message and default code', () => { + const cmd = new TestCommand() + + assert.throws( + () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(cmd as any).error('test error') + }, + (err: unknown) => { + assert.ok(err instanceof CommandError) + if (err instanceof CommandError) { + assert.equal(err.message, 'test error') + assert.equal(err.exitCode, ExitCode.GENERAL_ERROR) + } + + return true + }, + ) + }) + + it('throws CommandError with custom exit code', () => { + const cmd = new TestCommand() + + assert.throws( + () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(cmd as any).error('file not found', ExitCode.FILE_NOT_FOUND) + }, + (err: unknown) => { + assert.ok(err instanceof CommandError) + if (err instanceof CommandError) { + assert.equal(err.exitCode, ExitCode.FILE_NOT_FOUND) + } + + return true + }, + ) + }) + }) + }) +}) diff --git a/test/commands/dotenv.test.ts b/test/commands/dotenv.test.ts index 0197d80..b37db26 100644 --- a/test/commands/dotenv.test.ts +++ b/test/commands/dotenv.test.ts @@ -6,9 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import {expect} from 'chai' +import assert from 'node:assert/strict' import {randomUUID} from 'node:crypto' import fs from 'node:fs' +import {afterEach, describe, it} from 'node:test' import {ExitCode} from '../../src/cli/errors.js' import Dotenv from '../../src/commands/dotenv.js' @@ -24,7 +25,7 @@ describe('dotenv', () => { it('should fail to run dotenv when no environmentName is specified', async () => { const {error} = await runCommand(Dotenv, []) - expect(error?.exitCode).to.equal(ExitCode.INVALID_ARGUMENT) + assert.equal(error?.exitCode, ExitCode.INVALID_ARGUMENT) }) it('runs dotenv dev', async () => { @@ -39,10 +40,10 @@ describe('dotenv', () => { // Assert - expect(stdout).to.contain('Generating .env from ./.env.dev file...') + assert.ok(stdout.includes('Generating .env from ./.env.dev file...')) const envContent = fs.readFileSync('.env', 'utf8') - expect(envContent).to.eq(`TEST=${testID}`) + assert.equal(envContent, `TEST=${testID}`) }) it('runs dotenv prod', async () => { @@ -56,10 +57,10 @@ describe('dotenv', () => { const {stdout} = await runCommand(Dotenv, ['prod']) // Assert - expect(stdout).to.contain('Generating .env from ./.env.prod file...') + assert.ok(stdout.includes('Generating .env from ./.env.prod file...')) const envContent = fs.readFileSync('.env', 'utf8') - expect(envContent).to.contain(`TEST=${testID}`) + assert.ok(envContent.includes(`TEST=${testID}`)) }) it('runs dotenv with verbose flag and shows detailed output', async () => { @@ -71,9 +72,46 @@ describe('dotenv', () => { const {stdout} = await runCommand(Dotenv, ['dev', '-v']) // Assert - expect(stdout).to.contain('Generating .env from ./.env.dev file...') - expect(stdout).to.contain('Source environment file:') - expect(stdout).to.contain('Removing existing .env file') - expect(stdout).to.contain('Generated new .env file.') + assert.ok(stdout.includes('Generating .env from ./.env.dev file...')) + assert.ok(stdout.includes('Source environment file:')) + assert.ok(stdout.includes('Removing existing .env file')) + assert.ok(stdout.includes('Generated new .env file.')) + }) + + it('handles missing environment file gracefully', async () => { + const {error} = await runCommand(Dotenv, ['nonexistent']) + + assert.equal(error?.exitCode, ExitCode.FILE_NOT_FOUND) + }) + + it('overwrites existing .env file', async () => { + // Arrange + fs.writeFileSync('.env', 'OLD_VAR=old_value') + fs.writeFileSync('.env.dev', 'NEW_VAR=new_value') + + // Act + const {stdout} = await runCommand(Dotenv, ['dev']) + + // Assert + assert.ok(stdout.includes('Generated new .env file.')) + + const envContent = fs.readFileSync('.env', 'utf8') + assert.equal(envContent, 'NEW_VAR=new_value') + assert.ok(!envContent.includes('OLD_VAR')) + }) + + it('preserves multiline environment variables', async () => { + // Arrange + const multilineContent = `VAR1=value1 +VAR2=value2 +VAR3=line1\\nline2\\nline3` + fs.writeFileSync('.env.dev', multilineContent) + + // Act + await runCommand(Dotenv, ['dev']) + + // Assert + const envContent = fs.readFileSync('.env', 'utf8') + assert.equal(envContent, multilineContent) }) }) diff --git a/test/commands/icons.test.ts b/test/commands/icons.test.ts index 696f0b0..769336e 100644 --- a/test/commands/icons.test.ts +++ b/test/commands/icons.test.ts @@ -6,60 +6,65 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import {expect} from 'chai' +import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' +import {after, afterEach, before, describe, it} from 'node:test' import {ExitCode} from '../../src/cli/errors.js' import Icons from '../../src/commands/icons.js' import {runCommand} from '../helpers/run-command.js' -describe('icons', () => { - before(() => { +describe('icons', {concurrency: 1, timeout: 60_000}, () => { + before(async () => { fs.mkdirSync('assets', {recursive: true}) fs.copyFileSync('test/assets/icon.png', 'assets/icon.png') }) - after(() => { + after(async () => { fs.rmSync('assets', {force: true, recursive: true}) }) - afterEach(() => { + afterEach(async () => { for (const dir of ['android', 'ios']) { - fs.rmSync(dir, {force: true, recursive: true}) + try { + fs.rmSync(dir, {force: true, maxRetries: 3, recursive: true}) + } catch { + // Ignore errors - directory may have been removed by another test + } } }) it('should fail to run icons when no app.json file exists', async () => { const {error} = await runCommand(Icons, []) - expect(error?.exitCode).to.equal(ExitCode.CONFIG_ERROR) + assert.equal(error?.exitCode, ExitCode.CONFIG_ERROR) }) it('runs icons --appName test and generates expected files', async () => { const {stdout} = await runCommand(Icons, ['--appName', 'test']) - expect(stdout).to.contain("Generating icons for 'test' app...") - expect(stdout).to.contain("Generated icons for 'test' app.") + assert.ok(stdout.includes("Generating icons for 'test' app...")) + assert.ok(stdout.includes("Generated icons for 'test' app.")) // Check for iOS output directory and Contents.json const iosDir = path.join('ios', 'test', 'Images.xcassets', 'AppIcon.appiconset') - expect(fs.existsSync(path.join(iosDir, 'Contents.json'))).to.be.true + assert.ok(fs.existsSync(path.join(iosDir, 'Contents.json'))) const iosAppIcons = fs.readdirSync(iosDir).filter((f) => f.endsWith('.png')) - expect(iosAppIcons.length).to.eq(9) + assert.equal(iosAppIcons.length, 9) // Check for Android output directory and at least one icon const baseAndroidOutputDir = path.join('android', 'app', 'src', 'main') const androidDir = path.join(baseAndroidOutputDir, 'res') - expect(fs.existsSync(androidDir)).to.be.true + assert.ok(fs.existsSync(androidDir)) // Check webicon exists - expect(fs.existsSync(path.join(baseAndroidOutputDir, 'web_hi_res_512.png'))).to.be.true + assert.ok(fs.existsSync(path.join(baseAndroidOutputDir, 'web_hi_res_512.png'))) // Count mipmap directories const mipmapDirs = fs.readdirSync(androidDir).filter((f) => f.startsWith('mipmap-')) - expect(mipmapDirs.length).to.eq(5) + assert.equal(mipmapDirs.length, 5) // Count icons in mipmap directories let androidPngCount = 0 @@ -69,20 +74,21 @@ describe('icons', () => { } // Expect 5 densities * 2 launcher icons/density = 10 - expect(androidPngCount).to.eq(10) + assert.equal(androidPngCount, 10) }) it('runs icons with verbose flag and shows detailed output', async () => { const {stdout} = await runCommand(Icons, ['--appName', 'test', '-v']) - expect(stdout).to.contain("Generating icons for 'test' app...") - expect(stdout).to.contain('Generating icon') - expect(stdout).to.contain("Icon '") - expect(stdout).to.contain("Generated icons for 'test' app.") + assert.ok(stdout.includes("Generating icons for 'test' app...")) + assert.ok(stdout.includes('Generating icon')) + assert.ok(stdout.includes("Icon '")) + assert.ok(stdout.includes("Generated icons for 'test' app.")) }) it('handles corrupt image file gracefully', async () => { const corruptFile = 'assets/corrupt-icon.png' + fs.mkdirSync('assets', {recursive: true}) fs.writeFileSync(corruptFile, 'not a valid image') try { @@ -90,9 +96,15 @@ describe('icons', () => { // Should handle error gracefully - verify error collection message appears // Check if errors were reported (either via warning symbol or "failed to generate" text) - expect(stdout).to.match(/failed to generate|asset.*failed/i) + assert.match(stdout, /failed to generate|asset.*failed/i) } finally { // Cleanup is handled by afterEach } }) + + it('handles missing source file gracefully', async () => { + const {error} = await runCommand(Icons, ['--appName', 'TestApp', 'nonexistent.png']) + + assert.equal(error?.exitCode, ExitCode.FILE_NOT_FOUND) + }) }) diff --git a/test/commands/splash.test.ts b/test/commands/splash.test.ts index 41fe80d..08d374f 100644 --- a/test/commands/splash.test.ts +++ b/test/commands/splash.test.ts @@ -6,57 +6,62 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import {expect} from 'chai' +import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' +import {after, afterEach, before, describe, it} from 'node:test' import {ExitCode} from '../../src/cli/errors.js' import Splash from '../../src/commands/splash.js' import {runCommand} from '../helpers/run-command.js' -describe('splash', () => { - before(() => { +describe('splash', {concurrency: 1, timeout: 60_000}, () => { + before(async () => { fs.mkdirSync('assets', {recursive: true}) fs.copyFileSync('test/assets/splashscreen.png', 'assets/splashscreen.png') }) - after(() => { + after(async () => { fs.rmSync('assets', {force: true, recursive: true}) }) - afterEach(() => { + afterEach(async () => { for (const dir of ['android', 'ios']) { - fs.rmSync(dir, {force: true, recursive: true}) + try { + fs.rmSync(dir, {force: true, maxRetries: 3, recursive: true}) + } catch { + // Ignore errors - directory may have been removed by another test + } } }) it('should fail to run splash when no app.json file exists', async () => { const {error} = await runCommand(Splash, []) - expect(error?.exitCode).to.equal(ExitCode.CONFIG_ERROR) + assert.equal(error?.exitCode, ExitCode.CONFIG_ERROR) }) it('runs splash --appName test and generates expected files', async () => { const {stdout} = await runCommand(Splash, ['--appName', 'test']) - expect(stdout).to.contain("Generating splashscreens for 'test' app...") - expect(stdout).to.contain("Generated splashscreens for 'test' app.") + assert.ok(stdout.includes("Generating splashscreens for 'test' app...")) + assert.ok(stdout.includes("Generated splashscreens for 'test' app.")) // Check for iOS output directory and Contents.json const iosDir = path.join('ios', 'test', 'Images.xcassets', 'Splashscreen.imageset') - expect(fs.existsSync(path.join(iosDir, 'Contents.json'))).to.be.true + assert.ok(fs.existsSync(path.join(iosDir, 'Contents.json'))) // Check for iOS image files (expecting at least one) const iosSplashImages = fs.readdirSync(iosDir).filter((f) => f.endsWith('.png')) - expect(iosSplashImages.length).to.eq(3) + assert.equal(iosSplashImages.length, 3) // Check for Android output directories and image files const androidDir = path.join('android', 'app', 'src', 'main', 'res') - expect(fs.existsSync(androidDir)).to.be.true + assert.ok(fs.existsSync(androidDir)) // Count drawable directories const drawableDirs = fs.readdirSync(androidDir).filter((f) => f.startsWith('drawable-')) - expect(drawableDirs.length).to.eq(6) + assert.equal(drawableDirs.length, 6) // Count icons in drawable directories let androidSplashscreensCount = 0 @@ -66,19 +71,20 @@ describe('splash', () => { } // Expect 6 densities * 1 splashscreen = 6 - expect(androidSplashscreensCount).to.eq(6) + assert.equal(androidSplashscreensCount, 6) }) it('runs splash with verbose flag and shows detailed output', async () => { const {stdout} = await runCommand(Splash, ['--appName', 'test', '-v']) - expect(stdout).to.contain("Generating splashscreens for 'test' app...") - expect(stdout).to.contain('Generating splashscreen') - expect(stdout).to.contain("Generated splashscreens for 'test' app.") + assert.ok(stdout.includes("Generating splashscreens for 'test' app...")) + assert.ok(stdout.includes('Generating splashscreen')) + assert.ok(stdout.includes("Generated splashscreens for 'test' app.")) }) it('handles corrupt image file gracefully', async () => { const corruptFile = 'assets/corrupt-splash.png' + fs.mkdirSync('assets', {recursive: true}) fs.writeFileSync(corruptFile, 'not a valid image') try { @@ -86,9 +92,15 @@ describe('splash', () => { // Should handle error gracefully - verify error collection message appears // Check if errors were reported (either via warning symbol or "failed to generate" text) - expect(stdout).to.match(/failed to generate|asset.*failed/i) + assert.match(stdout, /failed to generate|asset.*failed/i) } finally { // Cleanup is handled by afterEach } }) + + it('handles missing source file gracefully', async () => { + const {error} = await runCommand(Splash, ['--appName', 'TestApp', 'nonexistent.png']) + + assert.equal(error?.exitCode, ExitCode.FILE_NOT_FOUND) + }) }) diff --git a/test/app.utils.test.ts b/test/utils/app.utils.test.ts similarity index 66% rename from test/app.utils.test.ts rename to test/utils/app.utils.test.ts index 06ea491..2a8e872 100644 --- a/test/app.utils.test.ts +++ b/test/utils/app.utils.test.ts @@ -1,7 +1,8 @@ -import {expect} from 'chai' +import assert from 'node:assert/strict' import fs from 'node:fs' +import {afterEach, describe, it} from 'node:test' -import {extractAppName} from '../src/utils/app.utils.js' +import {extractAppName} from '../../src/utils/app.utils.js' describe('extractAppName', () => { afterEach(() => { @@ -10,35 +11,35 @@ describe('extractAppName', () => { it('returns name from valid app.json', async () => { fs.writeFileSync('app.json', JSON.stringify({name: 'TestApp'})) - expect(await extractAppName()).to.equal('TestApp') + assert.equal(await extractAppName(), 'TestApp') }) it('returns undefined when app.json is missing', async () => { - expect(await extractAppName()).to.be.undefined + assert.equal(await extractAppName(), undefined) }) it('returns undefined when app.json has invalid JSON', async () => { fs.writeFileSync('app.json', 'not valid json') - expect(await extractAppName()).to.be.undefined + assert.equal(await extractAppName(), undefined) }) it('returns undefined when name property is missing', async () => { fs.writeFileSync('app.json', JSON.stringify({version: '1.0.0'})) - expect(await extractAppName()).to.be.undefined + assert.equal(await extractAppName(), undefined) }) it('returns undefined when name property is empty string', async () => { fs.writeFileSync('app.json', JSON.stringify({name: ''})) - expect(await extractAppName()).to.be.undefined + assert.equal(await extractAppName(), undefined) }) it('returns undefined when name property is whitespace only', async () => { fs.writeFileSync('app.json', JSON.stringify({name: ' '})) - expect(await extractAppName()).to.be.undefined + assert.equal(await extractAppName(), undefined) }) it('returns undefined when name property is not a string', async () => { fs.writeFileSync('app.json', JSON.stringify({name: 123})) - expect(await extractAppName()).to.be.undefined + assert.equal(await extractAppName(), undefined) }) }) diff --git a/test/utils/color.utils.test.ts b/test/utils/color.utils.test.ts index cce0759..0f65819 100644 --- a/test/utils/color.utils.test.ts +++ b/test/utils/color.utils.test.ts @@ -1,4 +1,5 @@ -import {expect} from 'chai' +import assert from 'node:assert/strict' +import {describe, it} from 'node:test' import {cyan, green, red, yellow} from '../../src/utils/color.utils.js' @@ -8,19 +9,19 @@ describe('color.utils', () => { const result = cyan('test') // Should contain the text - expect(result).to.include('test') + assert.ok(result.includes('test')) }) it('handles empty string', () => { const result = cyan('') - expect(result).to.be.a('string') + assert.equal(typeof result, 'string') }) it('handles special characters', () => { const result = cyan('test!@#$%') - expect(result).to.include('test!@#$%') + assert.ok(result.includes('test!@#$%')) }) }) @@ -29,20 +30,20 @@ describe('color.utils', () => { const result = green('success') // Should contain the text - expect(result).to.include('success') + assert.ok(result.includes('success')) }) it('handles empty string', () => { const result = green('') - expect(result).to.be.a('string') + assert.equal(typeof result, 'string') }) it('handles multiline text', () => { const result = green('line1\nline2') - expect(result).to.include('line1') - expect(result).to.include('line2') + assert.ok(result.includes('line1')) + assert.ok(result.includes('line2')) }) }) @@ -51,19 +52,19 @@ describe('color.utils', () => { const result = red('error') // Should contain the text - expect(result).to.include('error') + assert.ok(result.includes('error')) }) it('handles empty string', () => { const result = red('') - expect(result).to.be.a('string') + assert.equal(typeof result, 'string') }) it('handles numbers as strings', () => { const result = red('404') - expect(result).to.include('404') + assert.ok(result.includes('404')) }) }) @@ -72,19 +73,19 @@ describe('color.utils', () => { const result = yellow('warning') // Should contain the text - expect(result).to.include('warning') + assert.ok(result.includes('warning')) }) it('handles empty string', () => { const result = yellow('') - expect(result).to.be.a('string') + assert.equal(typeof result, 'string') }) it('handles symbols', () => { const result = yellow('⚠') - expect(result).to.include('⚠') + assert.ok(result.includes('⚠')) }) }) @@ -97,16 +98,16 @@ describe('color.utils', () => { const yellowResult = yellow(text) // All should contain the text - expect(cyanResult).to.include(text) - expect(greenResult).to.include(text) - expect(redResult).to.include(text) - expect(yellowResult).to.include(text) + assert.ok(cyanResult.includes(text)) + assert.ok(greenResult.includes(text)) + assert.ok(redResult.includes(text)) + assert.ok(yellowResult.includes(text)) // All should be strings - expect(cyanResult).to.be.a('string') - expect(greenResult).to.be.a('string') - expect(redResult).to.be.a('string') - expect(yellowResult).to.be.a('string') + assert.equal(typeof cyanResult, 'string') + assert.equal(typeof greenResult, 'string') + assert.equal(typeof redResult, 'string') + assert.equal(typeof yellowResult, 'string') }) }) }) diff --git a/test/utils/file-utils.test.ts b/test/utils/file-utils.test.ts index e0f5b26..822b35c 100644 --- a/test/utils/file-utils.test.ts +++ b/test/utils/file-utils.test.ts @@ -1,6 +1,7 @@ -import {expect} from 'chai' +import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' +import {afterEach, describe, it} from 'node:test' import {checkAssetFile, mkdirp} from '../../src/utils/file-utils.js' @@ -19,13 +20,13 @@ describe('file-utils', () => { const result = checkAssetFile(testFile) - expect(result).to.be.true + assert.ok(result) }) it('returns false when file does not exist', () => { const result = checkAssetFile(path.join(testDir, 'nonexistent.txt')) - expect(result).to.be.false + assert.equal(result, false) }) it('returns true when directory exists', () => { @@ -33,13 +34,13 @@ describe('file-utils', () => { const result = checkAssetFile(testDir) - expect(result).to.be.true + assert.ok(result) }) it('returns false for empty string path', () => { const result = checkAssetFile('') - expect(result).to.be.false + assert.equal(result, false) }) }) @@ -49,8 +50,8 @@ describe('file-utils', () => { await mkdirp(dirPath) - expect(fs.existsSync(dirPath)).to.be.true - expect(fs.statSync(dirPath).isDirectory()).to.be.true + assert.ok(fs.existsSync(dirPath)) + assert.ok(fs.statSync(dirPath).isDirectory()) }) it('creates nested directories', async () => { @@ -58,8 +59,8 @@ describe('file-utils', () => { await mkdirp(dirPath) - expect(fs.existsSync(dirPath)).to.be.true - expect(fs.statSync(dirPath).isDirectory()).to.be.true + assert.ok(fs.existsSync(dirPath)) + assert.ok(fs.statSync(dirPath).isDirectory()) }) it('does not throw error if directory already exists', async () => { @@ -68,7 +69,7 @@ describe('file-utils', () => { await mkdirp(dirPath) - expect(fs.existsSync(dirPath)).to.be.true + assert.ok(fs.existsSync(dirPath)) }) it('creates directory with complex path', async () => { @@ -76,8 +77,8 @@ describe('file-utils', () => { await mkdirp(dirPath) - expect(fs.existsSync(dirPath)).to.be.true - expect(fs.statSync(dirPath).isDirectory()).to.be.true + assert.ok(fs.existsSync(dirPath)) + assert.ok(fs.statSync(dirPath).isDirectory()) }) }) }) diff --git a/tsconfig.json b/tsconfig.json index 198ecdc..11f9f9d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,5 @@ "moduleResolution": "node16", "composite": true }, - "include": ["./src/**/*"], - "ts-node": { - "esm": true - } + "include": ["./src/**/*"] }