diff --git a/.vscode/settings.json b/.vscode/settings.json index 25fa6215..a1785ea3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "editor.defaultFormatter": "biomejs.biome" } diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..c6f016ea --- /dev/null +++ b/jest.config.js @@ -0,0 +1,14 @@ +/** @type {import('jest').Config} */ +export default { + bail: true, + clearMocks: true, + testEnvironment: 'node', + transform: { + '^.+\\.ts$': '@swc/jest', + }, + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + testRegex: ['__tests__/.+(spec|test).ts$'], + setupFiles: ['/setup.tests.ts'], +}; diff --git a/package.json b/package.json index 1666382f..ae0736b9 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,20 @@ "format": "biome check --write", "lint": "biome lint --write --unsafe", "type-check": "biome lint && tsc", - "test": "pnpm -r run test", + "test": "jest --no-cache", + "test:w": "npm test -- --watch", + "test:ci": "npm test -- --coverage", "ci:version": "changeset version && pnpm install --no-frozen-lockfile", "ci:publish": "changeset publish" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@changesets/cli": "^2.26.2", + "@swc/core": "^1.3.83", + "@swc/jest": "^0.2.29", + "@types/jest": "^29.5.4", "@types/node": "^18.16.0", + "jest": "^29.6.4", "typescript": "^5.2.2", "unbuild": "^2.0.0" }, diff --git a/packages/core/__tests__/index.spec.ts b/packages/core/__tests__/index.spec.ts new file mode 100644 index 00000000..d8a2cebd --- /dev/null +++ b/packages/core/__tests__/index.spec.ts @@ -0,0 +1,25 @@ +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import * as packageExports from '../src/index'; + +describe('Package', () => { + const exportedKeys = Object.keys(packageExports); + + it('should export all prompts', async () => { + const promptsPath = join(__dirname, '../src/prompts'); + const promptFiles = readdirSync(promptsPath); + + for (const file of promptFiles) { + const prompt = await import(join(promptsPath, file)); + expect(exportedKeys).toContain(prompt.default.name); + } + }); + + it('should export selected utils', async () => { + const utils: string[] = ['block', 'isCancel', 'mockPrompt', 'setGlobalAliases']; + + for (const util of utils) { + expect(exportedKeys).toContain(util); + } + }); +}); diff --git a/packages/core/__tests__/mocks/index.ts b/packages/core/__tests__/mocks/index.ts new file mode 100644 index 00000000..293f5013 --- /dev/null +++ b/packages/core/__tests__/mocks/index.ts @@ -0,0 +1,2 @@ +export * from './mock-readable.js'; +export * from './mock-writable.js'; diff --git a/packages/core/test/mock-readable.ts b/packages/core/__tests__/mocks/mock-readable.ts similarity index 100% rename from packages/core/test/mock-readable.ts rename to packages/core/__tests__/mocks/mock-readable.ts diff --git a/packages/core/test/mock-writable.ts b/packages/core/__tests__/mocks/mock-writable.ts similarity index 100% rename from packages/core/test/mock-writable.ts rename to packages/core/__tests__/mocks/mock-writable.ts diff --git a/packages/core/__tests__/prompts/confirm.spec.ts b/packages/core/__tests__/prompts/confirm.spec.ts new file mode 100644 index 00000000..9d193245 --- /dev/null +++ b/packages/core/__tests__/prompts/confirm.spec.ts @@ -0,0 +1,96 @@ +import { ConfirmPrompt, mockPrompt, setGlobalAliases } from '../../src'; +import type { ConfirmOptions } from '../../src/prompts/confirm'; + +const makeSut = (opts?: Partial) => { + return new ConfirmPrompt({ + render() { + return this.value; + }, + active: 'yes', + inactive: 'no', + ...opts, + }).prompt(); +}; + +describe('ConfirmPrompt', () => { + const mock = mockPrompt(); + + afterEach(() => { + mock.close(); + }); + + it('should set a boolean value', () => { + makeSut({ + // @ts-expect-error + initialValue: ' ', + }); + expect(mock.value).toBe(true); + mock.close(); + + makeSut({ + // @ts-expect-error + initialValue: '', + }); + expect(mock.state).toBe('initial'); + expect(mock.value).toBe(false); + }); + + it('should change value when cursor changes', () => { + makeSut(); + + expect(mock.value).toBe(false); + mock.pressKey('up', { name: 'up' }); + expect(mock.cursor).toBe(0); + expect(mock.value).toBe(true); + mock.pressKey('right', { name: 'right' }); + expect(mock.cursor).toBe(1); + expect(mock.value).toBe(false); + mock.pressKey('left', { name: 'left' }); + expect(mock.cursor).toBe(0); + expect(mock.value).toBe(true); + }); + + it('should change value on cursor alias', () => { + setGlobalAliases([['u', 'up']]); + makeSut(); + + expect(mock.value).toBe(false); + mock.pressKey('u', { name: 'u' }); + expect(mock.value).toBe(true); + }); + + it('should not change value on type', () => { + makeSut(); + + expect(mock.value).toBe(false); + mock.pressKey('t', { name: 't' }); + expect(mock.value).toBe(false); + mock.pressKey('e', { name: 'e' }); + expect(mock.value).toBe(false); + }); + + it('should submit value', () => { + makeSut(); + + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(false); + }); + + it('should submit value on confirm alias', () => { + const aliases = [ + ['y', true], + ['n', false], + ] as const; + + for (const [alias, expected] of aliases) { + makeSut(); + expect(mock.state).not.toBe('submit'); + mock.pressKey(alias, { name: alias }); + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(expected); + mock.close(); + } + }); +}); diff --git a/packages/core/__tests__/prompts/group-multiselect.spec.ts b/packages/core/__tests__/prompts/group-multiselect.spec.ts new file mode 100644 index 00000000..77df3034 --- /dev/null +++ b/packages/core/__tests__/prompts/group-multiselect.spec.ts @@ -0,0 +1,177 @@ +import { GroupMultiSelectPrompt, mockPrompt, setGlobalAliases } from '../../src'; +import type { GroupMultiSelectOptions } from '../../src/prompts/group-multiselect'; + +const makeSut = (opts?: Partial>) => { + return new GroupMultiSelectPrompt({ + render() { + return this.value; + }, + options: { + 'changed packages': [{ value: '@scope/a' }, { value: '@scope/b' }, { value: '@scope/c' }], + 'unchanged packages': [{ value: '@scope/x' }, { value: '@scope/y' }, { value: '@scope/z' }], + }, + ...opts, + }).prompt(); +}; + +describe('GroupMultiSelectPrompt', () => { + const mock = mockPrompt>(); + + afterEach(() => { + mock.close(); + }); + + it('should set options', () => { + makeSut(); + + expect(mock.options).toStrictEqual([ + { label: 'changed packages', value: 'changed packages', group: true }, + { value: '@scope/a', group: 'changed packages' }, + { value: '@scope/b', group: 'changed packages' }, + { value: '@scope/c', group: 'changed packages' }, + { label: 'unchanged packages', value: 'unchanged packages', group: true }, + { value: '@scope/x', group: 'unchanged packages' }, + { value: '@scope/y', group: 'unchanged packages' }, + { value: '@scope/z', group: 'unchanged packages' }, + ]); + }); + + it('should set initialValues', () => { + makeSut({ + initialValues: ['@scope/a', 'unchanged packages'], + }); + + expect(mock.value).toStrictEqual(['@scope/a', '@scope/x', '@scope/y', '@scope/z']); + }); + + it('should set initial cursor position', () => { + makeSut({ + cursorAt: '@scope/b', + }); + + expect(mock.cursor).toBe(2); + }); + + it('should set default cursor position', () => { + makeSut(); + + expect(mock.cursor).toBe(0); + }); + + it('should change cursor position on cursor', () => { + makeSut({ + options: { + groupA: [{ value: '1' }], + groupB: [{ value: '1' }], + }, + }); + const moves = [ + ['down', 1], + ['right', 2], + ['down', 3], + ['right', 0], + ['left', 3], + ['up', 2], + ] as const; + + for (const [cursor, index] of moves) { + mock.emit('cursor', cursor); + expect(mock.cursor).toBe(index); + } + }); + + it('should change cursor position on cursor alias', () => { + setGlobalAliases([ + ['d', 'down'], + ['u', 'up'], + ]); + makeSut(); + const moves = [ + ['d', 1], + ['u', 0], + ] as const; + + for (const [cursor, index] of moves) { + mock.pressKey(cursor, { name: cursor }); + expect(mock.cursor).toBe(index); + } + }); + + it('should toggle option', () => { + makeSut(); + + mock.emit('cursor', 'down'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual(['@scope/a']); + }); + + it('should toggle multiple options', () => { + makeSut(); + + mock.emit('cursor', 'down'); + mock.emit('cursor', 'space'); + mock.emit('cursor', 'down'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual(['@scope/a', '@scope/b']); + }); + + it('should untoggle option', () => { + makeSut(); + + mock.emit('cursor', 'down'); + mock.emit('cursor', 'space'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual([]); + }); + + it('should toggle group', () => { + makeSut(); + + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual(['@scope/a', '@scope/b', '@scope/c']); + }); + + it('should toggle multiple groups', () => { + makeSut(); + + mock.emit('cursor', 'space'); + mock.emit('cursor', 'down'); + mock.emit('cursor', 'down'); + mock.emit('cursor', 'down'); + mock.emit('cursor', 'down'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual([ + '@scope/a', + '@scope/b', + '@scope/c', + '@scope/x', + '@scope/y', + '@scope/z', + ]); + }); + + it('should untoggle group', () => { + makeSut(); + + mock.emit('cursor', 'space'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual([]); + }); + + it('should submit value', () => { + makeSut({ + initialValues: ['changed packages'], + }); + + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.value).toStrictEqual(['@scope/a', '@scope/b', '@scope/c']); + }); +}); diff --git a/packages/core/__tests__/prompts/multiselect.spec.ts b/packages/core/__tests__/prompts/multiselect.spec.ts new file mode 100644 index 00000000..0b1402da --- /dev/null +++ b/packages/core/__tests__/prompts/multiselect.spec.ts @@ -0,0 +1,134 @@ +import { MultiSelectPrompt, mockPrompt, setGlobalAliases } from '../../src'; +import type { MultiSelectOptions } from '../../src/prompts/multiselect'; + +const makeSut = (opts?: Partial>) => { + return new MultiSelectPrompt({ + render() { + return this.value; + }, + options: [{ value: 1 }, { value: 2 }, { value: 3 }], + ...opts, + }).prompt(); +}; + +describe('MultiSelectPrompt', () => { + const mock = mockPrompt>(); + + afterEach(() => { + mock.close(); + }); + + it('should set initialValues', () => { + makeSut({ + cursorAt: 2, + initialValues: [1, 2], + }); + + expect(mock.state).toBe('initial'); + expect(mock.cursor).toBe(1); + expect(mock.value).toStrictEqual([1, 2]); + }); + + it('should set first option as default', () => { + makeSut(); + + expect(mock.state).toBe('initial'); + expect(mock.cursor).toBe(0); + expect(mock.value).toStrictEqual([]); + }); + + it('should change cursor position on cursor', () => { + makeSut(); + mock.setCursor(1); + + expect(mock.cursor).toBe(1); + }); + + it('should change cursor position on emit cursor', () => { + makeSut(); + const moves = [ + ['down', 1], + ['right', 2], + ['right', 0], + ['left', 2], + ['up', 1], + ] as const; + + for (const [cursor, index] of moves) { + mock.emit('cursor', cursor); + expect(mock.cursor).toBe(index); + } + }); + + it('should change cursor position on cursor alias', () => { + setGlobalAliases([ + ['d', 'down'], + ['u', 'up'], + ]); + makeSut(); + const moves = [ + ['d', 1], + ['u', 0], + ] as const; + + for (const [cursor, index] of moves) { + mock.pressKey(cursor, { name: cursor }); + expect(mock.cursor).toBe(index); + } + }); + + it('should toggle option', () => { + makeSut(); + + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual([1]); + }); + + it('should untoggle option', () => { + makeSut(); + + mock.emit('cursor', 'space'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual([]); + }); + + it('should toggle multiple options', () => { + makeSut(); + + mock.emit('cursor', 'space'); + mock.emit('cursor', 'down'); + mock.emit('cursor', 'space'); + + expect(mock.value).toStrictEqual([1, 2]); + }); + + it('should toggle all options', () => { + makeSut(); + + mock.emit('key', 'a'); + + expect(mock.value).toStrictEqual([1, 2, 3]); + }); + + it('should untoggle all options', () => { + makeSut(); + + mock.emit('key', 'a'); + mock.emit('key', 'a'); + + expect(mock.value).toStrictEqual([]); + }); + + it('should submit value', () => { + makeSut({ + initialValues: [1, 2], + }); + + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.value).toStrictEqual([1, 2]); + }); +}); diff --git a/packages/core/__tests__/prompts/password.spec.ts b/packages/core/__tests__/prompts/password.spec.ts new file mode 100644 index 00000000..bfe2f8b1 --- /dev/null +++ b/packages/core/__tests__/prompts/password.spec.ts @@ -0,0 +1,86 @@ +import { randomUUID } from 'node:crypto'; +import color from 'picocolors'; +import { mockPrompt } from '../../src'; +import PasswordPrompt, { type PasswordOptions } from '../../src/prompts/password'; +import { cursor } from '../utils'; + +const makeSut = (opts?: Partial) => { + return new PasswordPrompt({ + ...opts, + render() { + return this.value; + }, + }).prompt(); +}; + +describe('PasswordPrompt', () => { + const mock = mockPrompt(); + + afterEach(() => { + mock.close(); + }); + + it('should mask value', () => { + const value = randomUUID(); + + makeSut(); + mock.emit('value', value); + + expect(mock.masked).toBe(value.replace(/./g, '•')); + }); + + it('should mask value with custom mask', () => { + const value = randomUUID(); + + makeSut({ + mask: '*', + }); + mock.emit('value', value); + + expect(mock.masked).toBe(value.replace(/./g, '*')); + }); + + it('should change cursor position when cursor changes', () => { + const value = randomUUID(); + let cursorIndex = value.length; + + makeSut({ + initialValue: value, + }); + + expect(mock.valueWithCursor).toBe(mock.masked + cursor); + + cursorIndex--; + mock.setCursor(cursorIndex); + mock.emit('value', value); + expect(mock.valueWithCursor).toBe( + mock.masked.slice(0, cursorIndex) + + color.inverse(mock.masked[cursorIndex]) + + mock.masked.slice(cursorIndex + 1) + ); + + cursorIndex--; + mock.setCursor(cursorIndex); + mock.emit('value', value); + expect(mock.valueWithCursor).toBe( + mock.masked.slice(0, cursorIndex) + + color.inverse(mock.masked[cursorIndex]) + + mock.masked.slice(cursorIndex + 1) + ); + + cursorIndex += 2; + mock.setCursor(cursorIndex); + mock.emit('value', value); + expect(mock.valueWithCursor).toBe(mock.masked + cursor); + }); + + it('should submit value', () => { + const value = randomUUID(); + + makeSut(); + mock.submit(value); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(value); + }); +}); diff --git a/packages/core/__tests__/prompts/prompt.spec.ts b/packages/core/__tests__/prompts/prompt.spec.ts new file mode 100644 index 00000000..5fb56384 --- /dev/null +++ b/packages/core/__tests__/prompts/prompt.spec.ts @@ -0,0 +1,278 @@ +import { randomUUID } from 'node:crypto'; +import { cursor } from 'sisteransi'; +import { mockPrompt, setGlobalAliases } from '../../src'; +import Prompt, { type PromptOptions } from '../../src/prompts/prompt'; +import { MockReadable, MockWritable } from '../mocks'; + +const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(); + +const makeSut = (opts?: Omit, 'render'>, trackValue?: boolean) => { + return new Prompt( + { + render() { + return this.value; + }, + ...opts, + }, + trackValue + ).prompt(); +}; + +describe('Prompt', () => { + let input: MockReadable; + let output: MockWritable; + const mock = mockPrompt(); + + beforeEach(() => { + input = new MockReadable(); + output = new MockWritable(); + }); + + afterEach(() => { + mock.close(); + }); + + it('should set initialValue as value', () => { + const value = randomUUID(); + + makeSut({ + initialValue: value, + }); + + expect(mock.value).toBe(value); + }); + + it('should listen to event', () => { + let counter = 0; + makeSut(); + + mock.on('value', () => { + counter++; + }); + for (let i = 0; i < 5; i++) { + mock.emit('value', ''); + } + + expect(counter).toBe(5); + }); + + it('should listen to event just once', () => { + let counter = 0; + makeSut(); + + mock.once('value', () => { + counter++; + }); + for (let i = 0; i < 5; i++) { + mock.emit('value', ''); + } + + expect(counter).toBe(1); + }); + + it('should submit value', () => { + const value = randomUUID(); + + makeSut(); + mock.submit(value); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(value); + }); + + it('should cancel prompt', () => { + const value = randomUUID(); + + makeSut(); + mock.cancel(value); + + expect(mock.state).toBe('cancel'); + expect(mock.value).toBe(value); + }); + + it('should validate value', () => { + const value = randomUUID(); + const error = 'invalid value'; + + makeSut({ + validate(value) { + return error; + }, + }); + mock.submit(value); + + expect(mock.state).toBe('error'); + expect(mock.value).toBe(value); + expect(mock.error).toBe(error); + }); + + it('should set prompt state as `active` after error', () => { + makeSut(); + + mock.setState('error'); + mock.pressKey('', { name: '' }); + + expect(mock.state).toBe('active'); + }); + + it('should emit `cursor` on press key alias', (done) => { + setGlobalAliases([['c', 'cancel']]); + makeSut({}, false); + + mock.on('cursor', (key) => { + key === 'cancel' ? done() : done(`invalid key received: ${key}`); + }); + mock.pressKey('', { name: 'c' }); + }); + + it('should emit `cursor` on press internal key alias', (done) => { + makeSut(); + + mock.on('cursor', (key) => { + key === 'cancel' ? done() : done(`invalid key received: ${key}`); + }); + mock.pressKey('', { name: 'cancel' }); + }); + + it('should emit `confirm` on press y/n', () => { + makeSut(); + let counter = 0; + + mock.on('confirm', (key) => { + counter++; + }); + mock.pressKey('y', { name: '' }); + mock.pressKey('n', { name: '' }); + + expect(counter).toBe(2); + }); + + it('should allow tab completion for placeholders', () => { + makeSut({ initialValue: '', placeholder: 'bar' }); + + mock.pressKey('\t', { name: 'tab' }); + + expect(mock.value).toBe('bar'); + }); + + it('should not allow tab completion if value is set', () => { + makeSut({ initialValue: 'foo', placeholder: 'bar' }); + + mock.pressKey('\t', { name: 'tab' }); + + expect(mock.value).toBe('foo'); + }); + + it('should render prompt on default output', () => { + mock.setIsTestMode(false); + const value = randomUUID(); + + makeSut({ + initialValue: value, + }); + + expect(outputSpy).toHaveBeenCalledWith(value); + }); + + test('should render prompt on custom output', () => { + mock.setIsTestMode(false); + const value = 'foo'; + + makeSut({ input, output, initialValue: value }); + + expect(output.buffer).toStrictEqual([cursor.hide, 'foo']); + }); + + it('should re-render prompt on resize', () => { + const renderFn = jest.fn().mockImplementation(() => 'foo'); + const instance = new Prompt({ + input, + output, + render: renderFn, + }); + instance.prompt(); + + expect(renderFn).toHaveBeenCalledTimes(1); + + output.emit('resize'); + + expect(renderFn).toHaveBeenCalledTimes(2); + }); + + it('should update single line', () => { + mock.setIsTestMode(false); + const value = randomUUID(); + const char = 't'; + + makeSut({ + initialValue: value, + }); + mock.on('key', (key) => { + mock.setValue(mock.value + key); + }); + mock.pressKey(char, { name: char }); + + expect(mock.frame).toBe(value + char); + expect(outputSpy).toHaveBeenCalledWith(value + char); + }); + + it('should update frame', () => { + mock.setIsTestMode(false); + const value = randomUUID(); + const newValue = 't\ng'; + + makeSut({ initialValue: value }); + mock.setValue(newValue); + mock.pressKey('', { name: '' }); + + expect(mock.frame).toBe(newValue); + expect(outputSpy).toHaveBeenCalledWith(value); + expect(outputSpy).toHaveBeenCalledWith(newValue); + }); + + it('should emit cursor events for movement keys', () => { + const keys = ['up', 'down', 'left', 'right']; + const eventSpy = jest.fn(); + const instance = new Prompt({ + input, + output, + render: () => 'foo', + }); + + instance.on('cursor', eventSpy); + + instance.prompt(); + + for (const key of keys) { + input.emit('keypress', key, { name: key }); + expect(eventSpy).toBeCalledWith(key); + } + }); + + it('should emit cursor events for movement key aliases when not tracking', () => { + const keys = [ + ['k', 'up'], + ['j', 'down'], + ['h', 'left'], + ['l', 'right'], + ]; + const eventSpy = jest.fn(); + const instance = new Prompt( + { + input, + output, + render: () => 'foo', + }, + false + ); + + instance.on('cursor', eventSpy); + + instance.prompt(); + + for (const [alias, key] of keys) { + input.emit('keypress', alias, { name: alias }); + expect(eventSpy).toBeCalledWith(key); + } + }); +}); diff --git a/packages/core/__tests__/prompts/select-key.spec.ts b/packages/core/__tests__/prompts/select-key.spec.ts new file mode 100644 index 00000000..bb7c6f26 --- /dev/null +++ b/packages/core/__tests__/prompts/select-key.spec.ts @@ -0,0 +1,61 @@ +import { SelectKeyPrompt, mockPrompt } from '../../src'; +import type { SelectKeyOptions } from '../../src/prompts/select-key'; + +const makeSut = (opts?: Partial>) => { + return new SelectKeyPrompt<{ value: string }>({ + render() { + return this.value; + }, + options: [{ value: 'alpha' }, { value: 'bravo' }, { value: 'charle' }], + ...opts, + }).prompt(); +}; + +describe('SelectKeyPrompt', () => { + const mock = mockPrompt>(); + + afterEach(() => { + mock.close(); + }); + + it('should set options', () => { + makeSut(); + + expect(mock.options).toStrictEqual([ + { value: 'alpha' }, + { value: 'bravo' }, + { value: 'charle' }, + ]); + }); + + it('should set cursor position', () => { + makeSut({ + initialValue: 'b', + }); + + expect(mock.cursor).toBe(1); + }); + + it('should set default cursor position', () => { + makeSut(); + + expect(mock.cursor).toBe(0); + }); + + it('should not submit value on invalid key', () => { + makeSut(); + + mock.emit('key', 't'); + + expect(mock.state).toBe('initial'); + }); + + it('should submit value', () => { + makeSut(); + + mock.emit('key', 'a'); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe('alpha'); + }); +}); diff --git a/packages/core/__tests__/prompts/select.spec.ts b/packages/core/__tests__/prompts/select.spec.ts new file mode 100644 index 00000000..06141924 --- /dev/null +++ b/packages/core/__tests__/prompts/select.spec.ts @@ -0,0 +1,89 @@ +import { SelectPrompt, mockPrompt, setGlobalAliases } from '../../src'; +import type { SelectOptions } from '../../src/prompts/select'; + +const makeSut = (opts?: Partial>) => { + return new SelectPrompt({ + render() { + return this.value; + }, + options: [{ value: 1 }, { value: 2 }, { value: 3 }], + ...opts, + }).prompt(); +}; + +describe('SelectPrompt', () => { + const mock = mockPrompt>(); + + afterEach(() => { + mock.close(); + }); + + it('should set initialValue', () => { + makeSut({ + initialValue: 2, + }); + + expect(mock.state).toBe('initial'); + expect(mock.cursor).toBe(1); + expect(mock.value).toBe(2); + }); + + it('should set first option as default', () => { + makeSut(); + + expect(mock.state).toBe('initial'); + expect(mock.cursor).toBe(0); + expect(mock.value).toBe(1); + }); + + it('should change cursor position on cursor', () => { + makeSut(); + mock.setCursor(1); + + expect(mock.cursor).toBe(1); + }); + + it('should change value on emit cursor', () => { + makeSut(); + const moves = [ + ['down', 1, 2], + ['right', 2, 3], + ['right', 0, 1], + ['left', 2, 3], + ['up', 1, 2], + ] as const; + + for (const [cursor, index, value] of moves) { + mock.emit('cursor', cursor); + expect(mock.cursor).toBe(index); + expect(mock.value).toBe(value); + } + }); + + it('should change value on cursor alias', () => { + setGlobalAliases([ + ['d', 'down'], + ['u', 'up'], + ]); + makeSut(); + const moves = [ + ['d', 1, 2], + ['u', 0, 1], + ] as const; + + for (const [cursor, index, value] of moves) { + mock.pressKey(cursor, { name: cursor }); + expect(mock.cursor).toBe(index); + expect(mock.value).toBe(value); + } + }); + + it('should submit value', () => { + makeSut(); + + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(1); + }); +}); diff --git a/packages/core/__tests__/prompts/text.spec.ts b/packages/core/__tests__/prompts/text.spec.ts new file mode 100644 index 00000000..5c17905f --- /dev/null +++ b/packages/core/__tests__/prompts/text.spec.ts @@ -0,0 +1,85 @@ +import { randomUUID } from 'node:crypto'; +import color from 'picocolors'; +import { TextPrompt, mockPrompt } from '../../src'; +import type { TextOptions } from '../../src/prompts/text'; +import { cursor } from '../utils'; + +const makeSut = (opts?: Partial) => { + return new TextPrompt({ + render() { + return this.value; + }, + ...opts, + }).prompt(); +}; + +describe('TextPrompt', () => { + const mock = mockPrompt(); + + afterEach(() => { + mock.close(); + }); + + it('should set initialValue', () => { + const value = randomUUID(); + + makeSut({ + initialValue: value, + }); + + expect(mock.value).toBe(value); + }); + + it('should change cursor position when cursor changes', () => { + const value = randomUUID(); + let cursorIndex = value.length; + + makeSut({ + initialValue: value, + }); + + expect(mock.valueWithCursor).toBe(value + cursor); + + cursorIndex--; + mock.setCursor(cursorIndex); + mock.emit('value', value); + expect(mock.valueWithCursor).toBe( + value.slice(0, cursorIndex) + color.inverse(value[cursorIndex]) + value.slice(cursorIndex + 1) + ); + + cursorIndex--; + mock.setCursor(cursorIndex); + mock.emit('value', value); + expect(mock.valueWithCursor).toBe( + value.slice(0, cursorIndex) + color.inverse(value[cursorIndex]) + value.slice(cursorIndex + 1) + ); + + cursorIndex += 2; + mock.setCursor(cursorIndex); + mock.emit('value', value); + expect(mock.valueWithCursor).toBe(value + cursor); + }); + + it('should submit default value if no value is provided', () => { + const defaultValue = randomUUID(); + + makeSut({ + defaultValue, + initialValue: '', + }); + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(defaultValue); + }); + + it('should submit value', () => { + const value = randomUUID(); + + makeSut(); + mock.submit(value); + + expect(mock.state).toBe('submit'); + expect(mock.value).toBe(value); + }); +}); diff --git a/packages/core/__tests__/utils.ts b/packages/core/__tests__/utils.ts new file mode 100644 index 00000000..431983a9 --- /dev/null +++ b/packages/core/__tests__/utils.ts @@ -0,0 +1,3 @@ +import color from 'picocolors'; + +export const cursor = color.inverse(color.hidden('_')); diff --git a/packages/core/__tests__/utils/alias.spec.ts b/packages/core/__tests__/utils/alias.spec.ts new file mode 100644 index 00000000..1159e436 --- /dev/null +++ b/packages/core/__tests__/utils/alias.spec.ts @@ -0,0 +1,46 @@ +import { ALIASES, hasAliasKey, setGlobalAliases } from '../../src/utils'; + +describe('Alias', () => { + describe('setGlobalAliases()', () => { + it('should set custom global aliases for the default keys', () => { + expect(ALIASES.get('u')).toBeUndefined(); + expect(ALIASES.get('t')).toBeUndefined(); + expect(ALIASES.get('c')).toBeUndefined(); + setGlobalAliases([ + ['u', 'up'], + ['t', 'up'], + ['c', 'cancel'], + ]); + expect(ALIASES.get('u')).toBe('up'); + expect(ALIASES.get('t')).toBe('up'); + expect(ALIASES.get('c')).toBe('cancel'); + }); + }); + + describe('hasAliasKey()', () => { + it('should check if a key is an alias for a default key', () => { + ALIASES.delete('u'); + expect(hasAliasKey('u', 'up')).toBe(false); + + ALIASES.set('u', 'up'); + expect(hasAliasKey('u', 'up')).toBe(true); + }); + + it('should check if a key list has an alias for a default key', () => { + ALIASES.delete('u'); + ALIASES.delete('t'); + expect(hasAliasKey(['u', 't'], 'up')).toBe(false); + + ALIASES.set('u', 'up'); + expect(hasAliasKey(['u', 't'], 'up')).toBe(true); + + ALIASES.delete('u'); + ALIASES.set('t', 'up'); + expect(hasAliasKey(['u', 't'], 'up')).toBe(true); + + ALIASES.delete('t'); + ALIASES.set('t', 'down'); + expect(hasAliasKey(['u', 't'], 'up')).toBe(false); + }); + }); +}); diff --git a/packages/core/__tests__/utils/index.spec.ts b/packages/core/__tests__/utils/index.spec.ts new file mode 100644 index 00000000..79a5f251 --- /dev/null +++ b/packages/core/__tests__/utils/index.spec.ts @@ -0,0 +1,224 @@ +import { platform } from 'node:os'; +import { stdin, stdout } from 'node:process'; +import * as readline from 'node:readline'; +import { cursor } from 'sisteransi'; +import { isCancel } from '../../src'; +import { CANCEL_SYMBOL, block, setGlobalAliases, setRawMode } from '../../src/utils'; + +const rlSpy = { + terminal: true, + close: jest.fn(), +}; + +jest.mock('node:readline', () => ({ + __esModule: true, + createInterface: jest.fn(() => rlSpy), + emitKeypressEvents: jest.fn(), + moveCursor: jest.fn(), + clearLine: jest.fn(), +})); + +jest.mock('node:process', () => ({ + __esModule: true, + stdin: { + isTTY: false, + once: jest.fn(), + setRawMode: jest.fn(), + off: jest.fn(), + }, + stdout: { + write: jest.fn(), + }, +})); + +jest.mock('node:os', () => ({ + platform: jest.fn(() => 'linux'), +})); + +describe('Utils', () => { + describe('isCancel()', () => { + it('should verify if is cancel symbol', () => { + expect(isCancel('clack:cancel')).toBe(false); + expect(isCancel(Symbol('clack:cancel'))).toBe(false); + expect(isCancel(CANCEL_SYMBOL)).toBe(true); + }); + }); + + describe('setRawMode()', () => { + it('should set raw mode as true', (done) => { + const input = { + isTTY: true, + setRawMode: (value: boolean) => { + value === true ? done() : done('invalid value'); + }, + } as any; + setRawMode(input, true); + }); + + it('should set raw mode as false', (done) => { + const input = { + isTTY: true, + setRawMode: (value: boolean) => { + value === false ? done() : done('invalid value'); + }, + } as any; + setRawMode(input, false); + }); + + it('should not set raw mode', () => { + let calledTimes = 0; + const input = { + isTTY: false, + setRawMode: () => { + calledTimes++; + }, + } as any; + + setRawMode(input, true); + + expect(calledTimes).toBe(0); + }); + }); + + describe('block()', () => { + it('should emit keypress events', () => { + block(); + + expect(readline.emitKeypressEvents).toHaveBeenCalledTimes(1); + }); + + it('should set input on raw mode', () => { + stdin.isTTY = true; + + block(); + + expect(stdin.setRawMode).toHaveBeenCalledTimes(1); + expect(stdin.setRawMode).toHaveBeenCalledWith(true); + }); + + it('should not set input on raw mode', () => { + stdin.isTTY = false; + + block(); + + expect(stdin.setRawMode).toHaveBeenCalledTimes(0); + }); + + it('should hide cursor', () => { + block(); + + expect(stdout.write).toHaveBeenCalledWith(cursor.hide); + }); + + it('should not hide cursor', () => { + block({ + hideCursor: false, + }); + + expect(stdout.write).not.toHaveBeenCalledWith(cursor.hide); + }); + + it('should set keypress listener', () => { + block(); + + expect(stdin.once).toHaveBeenCalledWith('keypress', expect.anything()); + }); + + it('should clear keypress listener', () => { + block()(); + + expect(stdin.off).toHaveBeenCalledWith('keypress', expect.anything()); + }); + + it('should restore cursor', () => { + block()(); + + expect(stdout.write).toHaveBeenCalledWith(cursor.show); + }); + + it('should restore terminal default mode', () => { + stdin.isTTY = true; + + block()(); + + expect(stdin.setRawMode).toHaveBeenCalledTimes(2); + expect(stdin.setRawMode).toHaveBeenNthCalledWith(2, false); + }); + + it('should not disable input raw mode on Windows', () => { + stdin.isTTY = true; + (platform as jest.Mock).mockReturnValueOnce('win32'); + + block()(); + + expect(stdin.setRawMode).toHaveBeenCalledTimes(1); + expect(stdin.setRawMode).toHaveBeenCalledWith(true); + }); + + it('should close readline interface', () => { + block()(); + + expect(rlSpy.terminal).toBe(false); + expect(rlSpy.close).toHaveBeenCalledTimes(1); + }); + + it('should `clear` char on keypress', () => { + (stdin.once as jest.Mock).mockImplementationOnce((event, cb) => { + cb(Buffer.from('c'), { name: 'c', sequence: undefined }); + }); + (readline.moveCursor as jest.Mock).mockImplementationOnce((output, dx, dy, cb) => { + cb(); + }); + (readline.clearLine as jest.Mock).mockImplementationOnce((output, dir, cb) => { + cb(); + }); + + block()(); + + expect(readline.moveCursor).toHaveBeenCalledWith(stdout, -1, 0, expect.any(Function)); + expect(readline.clearLine).toHaveBeenCalledWith(stdout, 1, expect.any(Function)); + expect(stdin.once).toHaveBeenCalledTimes(2); + expect(stdin.once).toHaveBeenCalledWith('keypress', expect.any(Function)); + }); + + it('should not clear char on keypress if overwrite is false', () => { + (stdin.once as jest.Mock).mockImplementationOnce((event, cb) => { + cb(Buffer.from('c'), { name: 'c', sequence: undefined }); + }); + (readline.moveCursor as jest.Mock).mockImplementationOnce((output, dx, dy, cb) => { + cb(); + }); + + block({ + overwrite: false, + })(); + + expect(stdin.once).toHaveBeenCalledTimes(1); + expect(stdin.once).toHaveBeenCalledWith('keypress', expect.any(Function)); + expect(readline.moveCursor).toHaveBeenCalledTimes(0); + }); + + it('should `clear` line on enter', () => { + (stdin.once as jest.Mock).mockImplementationOnce((event, cb) => { + cb(Buffer.from('c'), { name: 'return', sequence: undefined }); + }); + + block()(); + + expect(readline.moveCursor).toHaveBeenCalledWith(stdout, 0, -1, expect.any(Function)); + expect(readline.clearLine).toHaveBeenCalledWith(stdout, 1, expect.any(Function)); + }); + + it('should exit on cancel alias', () => { + setGlobalAliases([['c', 'cancel']]); + (stdin.once as jest.Mock).mockImplementationOnce((event, cb) => { + cb(Buffer.from('c'), { name: 'c', sequence: undefined }); + }); + const exitSpy = jest.spyOn(process, 'exit').mockImplementation(); + + block()(); + + expect(exitSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/core/__tests__/utils/string.spec.ts b/packages/core/__tests__/utils/string.spec.ts new file mode 100644 index 00000000..1cecacb5 --- /dev/null +++ b/packages/core/__tests__/utils/string.spec.ts @@ -0,0 +1,18 @@ +import { diffLines } from '../../src/utils'; + +describe('String Utils', () => { + it('should return `undefined` if there is no diff line', () => { + const input = 'Lorem Ipsum is simply dummy text\n of the printing \nand typesetting industry.'; + + expect(diffLines(input, input)).toBeUndefined(); + }); + + it('should return a list with the index of each diff line', () => { + const inputA = + 'Lorem Ipsum is simply dummy\n text\n of the \nprinting\n \nand typesetting industry.'; + const inputB = + 'Lorem Ipsum is simply dummy\n \ntext of the \nprinting\n and\n typesetting industry.'; + + expect(diffLines(inputA, inputB)).toStrictEqual([1, 2, 4, 5]); + }); +}); diff --git a/packages/core/package.json b/packages/core/package.json index 753e30ba..24a28310 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,15 +47,13 @@ "packageManager": "pnpm@8.6.12", "scripts": { "build": "unbuild", - "prepack": "pnpm build", - "test": "vitest run" + "prepack": "pnpm build" }, "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, "devDependencies": { - "vitest": "^1.6.0", "wrap-ansi": "^8.1.0" } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 25125f99..16aca3b3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,10 +1,13 @@ -export { default as ConfirmPrompt } from './prompts/confirm'; -export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect'; -export { default as MultiSelectPrompt } from './prompts/multi-select'; -export { default as PasswordPrompt } from './prompts/password'; -export { default as Prompt } from './prompts/prompt'; -export { default as SelectPrompt } from './prompts/select'; -export { default as SelectKeyPrompt } from './prompts/select-key'; -export { default as TextPrompt } from './prompts/text'; +export { default as ConfirmPrompt, type ConfirmOptions } from './prompts/confirm'; +export { + default as GroupMultiSelectPrompt, + type GroupMultiSelectOptions, +} from './prompts/group-multiselect'; +export { default as MultiSelectPrompt, type MultiSelectOptions } from './prompts/multiselect'; +export { default as PasswordPrompt, type PasswordOptions } from './prompts/password'; +export { default as Prompt, type PromptOptions } from './prompts/prompt'; +export { default as SelectPrompt, type SelectOptions } from './prompts/select'; +export { default as SelectKeyPrompt, type SelectKeyOptions } from './prompts/select-key'; +export { default as TextPrompt, type TextOptions } from './prompts/text'; export type { ClackState as State } from './types'; -export { block, isCancel, setGlobalAliases } from './utils'; +export { block, isCancel, mockPrompt, setGlobalAliases } from './utils'; diff --git a/packages/core/src/prompts/confirm.ts b/packages/core/src/prompts/confirm.ts index 9b24df31..4a06d33a 100644 --- a/packages/core/src/prompts/confirm.ts +++ b/packages/core/src/prompts/confirm.ts @@ -1,27 +1,23 @@ import { cursor } from 'sisteransi'; +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; -interface ConfirmOptions extends PromptOptions { +export interface ConfirmOptions extends PromptOptions { active: string; inactive: string; initialValue?: boolean; } + export default class ConfirmPrompt extends Prompt { get cursor() { return this.value ? 0 : 1; } - private get _value() { - return this.cursor === 0; - } - constructor(opts: ConfirmOptions) { super(opts, false); this.value = !!opts.initialValue; - this.on('value', () => { - this.value = this._value; - }); + this.exposeTestUtils(); this.on('confirm', (confirm) => { this.output.write(cursor.move(0, -1)); @@ -32,6 +28,11 @@ export default class ConfirmPrompt extends Prompt { this.on('cursor', () => { this.value = !this.value; + this.exposeTestUtils(); }); } + + private exposeTestUtils() { + exposeTestUtils({ value: this.value, cursor: this.cursor }); + } } diff --git a/packages/core/src/prompts/group-multiselect.ts b/packages/core/src/prompts/group-multiselect.ts index 0f2e10e4..5bf3e3ad 100644 --- a/packages/core/src/prompts/group-multiselect.ts +++ b/packages/core/src/prompts/group-multiselect.ts @@ -1,26 +1,28 @@ +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; -interface GroupMultiSelectOptions +export interface GroupMultiSelectOptions extends PromptOptions> { options: Record; initialValues?: T['value'][]; required?: boolean; cursorAt?: T['value']; } + export default class GroupMultiSelectPrompt extends Prompt { - options: (T & { group: string | boolean })[]; - cursor = 0; + public options: (T & { group: string | boolean })[]; + public cursor: number; getGroupItems(group: string): T[] { return this.options.filter((o) => o.group === group); } - isGroupSelected(group: string) { + isGroupSelected(group: string): boolean { const items = this.getGroupItems(group); return items.every((i) => this.value.includes(i.value)); } - private toggleValue() { + private toggleValue(): void { const item = this.options[this.cursor]; if (item.group === true) { const group = item.value; @@ -43,17 +45,31 @@ export default class GroupMultiSelectPrompt extends Pr constructor(opts: GroupMultiSelectOptions) { super(opts, false); + const { options } = opts; this.options = Object.entries(options).flatMap(([key, option]) => [ { value: key, group: true, label: key }, ...option.map((opt) => ({ ...opt, group: key })), ]) as any; - this.value = [...(opts.initialValues ?? [])]; + + this.value = []; + if (opts.initialValues) { + for (const value of opts.initialValues) { + const optionIndex = this.options.findIndex((o) => o.value === value); + if (optionIndex !== -1) { + this.cursor = optionIndex; + this.toggleValue(); + } + } + } + this.cursor = Math.max( this.options.findIndex(({ value }) => value === opts.cursorAt), 0 ); + this.exposeTestUtils(); + this.on('cursor', (key) => { switch (key) { case 'left': @@ -68,6 +84,21 @@ export default class GroupMultiSelectPrompt extends Pr this.toggleValue(); break; } + this.exposeTestUtils(); + }); + } + + private exposeTestUtils() { + exposeTestUtils>({ + options: this.options, + cursor: this.cursor, + value: this.value, + isGroupSelected: this.isGroupSelected.bind(this), + getGroupItems: this.getGroupItems.bind(this), + setCursor: (cursor: number) => { + this.cursor = cursor; + this.exposeTestUtils(); + }, }); } } diff --git a/packages/core/src/prompts/multi-select.ts b/packages/core/src/prompts/multiselect.ts similarity index 70% rename from packages/core/src/prompts/multi-select.ts rename to packages/core/src/prompts/multiselect.ts index 4e6eb72e..6a50f276 100644 --- a/packages/core/src/prompts/multi-select.ts +++ b/packages/core/src/prompts/multiselect.ts @@ -1,14 +1,17 @@ +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; -interface MultiSelectOptions extends PromptOptions> { +export interface MultiSelectOptions + extends PromptOptions> { options: T[]; initialValues?: T['value'][]; required?: boolean; cursorAt?: T['value']; } + export default class MultiSelectPrompt extends Prompt { - options: T[]; - cursor = 0; + public options: T[]; + public cursor: number; private get _value() { return this.options[this.cursor].value; @@ -30,14 +33,18 @@ export default class MultiSelectPrompt extends Prompt super(opts, false); this.options = opts.options; - this.value = [...(opts.initialValues ?? [])]; + this.value = opts.initialValues ?? []; this.cursor = Math.max( this.options.findIndex(({ value }) => value === opts.cursorAt), 0 ); + + this.exposeTestUtils(); + this.on('key', (char) => { if (char === 'a') { this.toggleAll(); + this.exposeTestUtils(); } }); @@ -55,6 +62,19 @@ export default class MultiSelectPrompt extends Prompt this.toggleValue(); break; } + this.exposeTestUtils(); + }); + } + + private exposeTestUtils() { + exposeTestUtils>({ + options: this.options, + cursor: this.cursor, + value: this.value, + setCursor: (cursor) => { + this.cursor = cursor; + this.exposeTestUtils(); + }, }); } } diff --git a/packages/core/src/prompts/password.ts b/packages/core/src/prompts/password.ts index 453b05fd..99c928b9 100644 --- a/packages/core/src/prompts/password.ts +++ b/packages/core/src/prompts/password.ts @@ -1,26 +1,40 @@ import color from 'picocolors'; +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; -interface PasswordOptions extends PromptOptions { +export interface PasswordOptions extends PromptOptions { mask?: string; } + export default class PasswordPrompt extends Prompt { - valueWithCursor = ''; - private _mask = '•'; + public valueWithCursor: string; + + private _mask: string; + get cursor() { return this._cursor; } - get masked() { + + get masked(): string { return this.value.replaceAll(/./g, this._mask); } + constructor({ mask, ...opts }: PasswordOptions) { - super(opts); + super(opts, true); + + this.value = ''; this._mask = mask ?? '•'; + this.valueWithCursor = this.masked + color.inverse(color.hidden('_')); + + this.exposeTestUtils(); this.on('finalize', () => { this.valueWithCursor = this.masked; + this.exposeTestUtils(); }); - this.on('value', () => { + + this.on('value', (value) => { + this.value = value; if (this.cursor >= this.value.length) { this.valueWithCursor = `${this.masked}${color.inverse(color.hidden('_'))}`; } else { @@ -28,6 +42,15 @@ export default class PasswordPrompt extends Prompt { const s2 = this.masked.slice(this.cursor); this.valueWithCursor = `${s1}${color.inverse(s2[0])}${s2.slice(1)}`; } + this.exposeTestUtils(); + }); + } + + private exposeTestUtils() { + exposeTestUtils({ + cursor: this.cursor, + masked: this.masked, + valueWithCursor: this.valueWithCursor, }); } } diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index f2c85771..47d1f08e 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -5,7 +5,16 @@ import { WriteStream } from 'node:tty'; import { cursor, erase } from 'sisteransi'; import wrap from 'wrap-ansi'; -import { ALIASES, CANCEL_SYMBOL, KEYS, diffLines, hasAliasKey, setRawMode } from '../utils'; +import { + ALIASES, + CANCEL_SYMBOL, + KEYS, + diffLines, + exposeTestUtils, + hasAliasKey, + isTestMode, + setRawMode, +} from '../utils'; import type { ClackEvents, ClackState, InferSetType } from '../types'; @@ -47,6 +56,33 @@ export default class Prompt { this.input = input; this.output = output; + + exposeTestUtils({ + ...this, + on: this.on.bind(this), + once: this.once.bind(this), + emit: this.emit.bind(this), + pressKey: this.onKeypress.bind(this), + updateFrame: this.render.bind(this), + close: this.close.bind(this), + setState: (state) => { + this.state = state; + }, + setCursor: (cursor) => { + this._cursor = cursor; + }, + setValue: (value) => { + this.value = value; + }, + cancel: (value) => { + this.value = value ?? this.value; + this.onKeypress('', { name: '\x03' }); + }, + submit: (value) => { + this.value = value ?? this.value; + this.onKeypress('', { name: 'return' }); + }, + }); } /** @@ -107,6 +143,8 @@ export default class Prompt { for (const cb of cleanup) { cb(); } + + exposeTestUtils({ value: this.value }); } public prompt() { @@ -142,12 +180,12 @@ export default class Prompt { this.render(); this.once('submit', () => { + if (!isTestMode) this.output.write('\n'); this.output.write(cursor.show); - this.output.off('resize', this.render); - setRawMode(this.input, false); resolve(this.value); }); this.once('cancel', () => { + if (!isTestMode) this.output.write('\n'); this.output.write(cursor.show); this.output.off('resize', this.render); setRawMode(this.input, false); @@ -208,7 +246,7 @@ export default class Prompt { protected close() { this.input.unpipe(); this.input.removeListener('keypress', this.onKeypress); - this.output.write('\n'); + this.output.off('resize', this.render); setRawMode(this.input, false); this.rl.close(); this.emit(`${this.state}`, this.value); @@ -222,8 +260,14 @@ export default class Prompt { } private render() { - const frame = wrap(this._render(this) ?? '', process.stdout.columns, { hard: true }); - if (frame === this._prevFrame) return; + const frame = wrap(this._render(this) ?? '', process.stdout.columns, { + hard: true, + }); + exposeTestUtils({ frame, state: this.state, error: this.error }); + + if (isTestMode || frame === this._prevFrame) { + return; + } if (this.state === 'initial') { this.output.write(cursor.hide); diff --git a/packages/core/src/prompts/select-key.ts b/packages/core/src/prompts/select-key.ts index c99c30ad..825f5dbb 100644 --- a/packages/core/src/prompts/select-key.ts +++ b/packages/core/src/prompts/select-key.ts @@ -1,11 +1,14 @@ +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; -interface SelectKeyOptions extends PromptOptions> { +export interface SelectKeyOptions + extends PromptOptions> { options: T[]; } + export default class SelectKeyPrompt extends Prompt { - options: T[]; - cursor = 0; + public options: T[]; + public cursor: number; constructor(opts: SelectKeyOptions) { super(opts, false); @@ -13,6 +16,9 @@ export default class SelectKeyPrompt extends Prompt { this.options = opts.options; const keys = this.options.map(({ value: [initial] }) => initial?.toLowerCase()); this.cursor = Math.max(keys.indexOf(opts.initialValue), 0); + this.value = this.options[this.cursor].value; + + this.exposeTestUtils(); this.on('key', (key) => { if (!keys.includes(key)) return; @@ -20,8 +26,18 @@ export default class SelectKeyPrompt extends Prompt { if (value) { this.value = value.value; this.state = 'submit'; - this.emit('submit'); + this.close(); } + this.exposeTestUtils(); + }); + } + + private exposeTestUtils() { + exposeTestUtils>({ + state: this.state, + options: this.options, + cursor: this.cursor, + value: this.value, }); } } diff --git a/packages/core/src/prompts/select.ts b/packages/core/src/prompts/select.ts index d5afca2c..3c052e63 100644 --- a/packages/core/src/prompts/select.ts +++ b/packages/core/src/prompts/select.ts @@ -1,12 +1,14 @@ +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; -interface SelectOptions extends PromptOptions> { +export interface SelectOptions extends PromptOptions> { options: T[]; initialValue?: T['value']; } + export default class SelectPrompt extends Prompt { - options: T[]; - cursor = 0; + public options: T[]; + public cursor: number; private get _value() { return this.options[this.cursor]; @@ -20,10 +22,14 @@ export default class SelectPrompt extends Prompt { super(opts, false); this.options = opts.options; - this.cursor = this.options.findIndex(({ value }) => value === opts.initialValue); - if (this.cursor === -1) this.cursor = 0; + this.cursor = Math.max( + this.options.findIndex(({ value }) => value === opts.initialValue), + 0 + ); this.changeValue(); + this.exposeTestUtils(); + this.on('cursor', (key) => { switch (key) { case 'left': @@ -36,6 +42,19 @@ export default class SelectPrompt extends Prompt { break; } this.changeValue(); + this.exposeTestUtils(); + }); + } + + private exposeTestUtils() { + exposeTestUtils>({ + options: this.options, + cursor: this.cursor, + value: this.value, + setCursor: (cursor) => { + this.cursor = cursor; + this.exposeTestUtils(); + }, }); } } diff --git a/packages/core/src/prompts/text.ts b/packages/core/src/prompts/text.ts index 54f20562..2ded50c5 100644 --- a/packages/core/src/prompts/text.ts +++ b/packages/core/src/prompts/text.ts @@ -1,4 +1,5 @@ import color from 'picocolors'; +import { exposeTestUtils } from '../utils'; import Prompt, { type PromptOptions } from './prompt'; export interface TextOptions extends PromptOptions { @@ -7,20 +8,30 @@ export interface TextOptions extends PromptOptions { } export default class TextPrompt extends Prompt { - valueWithCursor = ''; + public valueWithCursor: string; + get cursor() { return this._cursor; } + constructor(opts: TextOptions) { - super(opts); + super(opts, true); + + this.value = opts.initialValue ?? ''; + this.valueWithCursor = this.value + color.inverse(color.hidden('_')); + + this.exposeTestUtils(); this.on('finalize', () => { - if (!this.value) { + if (!this.value && opts.defaultValue) { this.value = opts.defaultValue; } this.valueWithCursor = this.value; + this.exposeTestUtils(); }); - this.on('value', () => { + + this.on('value', (value) => { + this.value = value; if (this.cursor >= this.value.length) { this.valueWithCursor = `${this.value}${color.inverse(color.hidden('_'))}`; } else { @@ -28,6 +39,15 @@ export default class TextPrompt extends Prompt { const s2 = this.value.slice(this.cursor); this.valueWithCursor = `${s1}${color.inverse(s2[0])}${s2.slice(1)}`; } + this.exposeTestUtils(); + }); + } + + private exposeTestUtils() { + exposeTestUtils({ + value: this.value, + valueWithCursor: this.valueWithCursor, + cursor: this.cursor, }); } } diff --git a/packages/core/src/utils/aliases.ts b/packages/core/src/utils/aliases.ts index 819dec7d..04f354a4 100644 --- a/packages/core/src/utils/aliases.ts +++ b/packages/core/src/utils/aliases.ts @@ -18,7 +18,7 @@ export const ALIASES = new Map>([ * @default * new Map([['k', 'up'], ['j', 'down'], ['h', 'left'], ['l', 'right'], ['\x03', 'cancel'],]) */ -export function setGlobalAliases(alias: Array<[string, InferSetType]>) { +export function setGlobalAliases(alias: Array<[string, InferSetType]>): void { for (const [newAlias, key] of alias) { if (!ALIASES.has(newAlias)) { ALIASES.set(newAlias, key); @@ -35,7 +35,7 @@ export function setGlobalAliases(alias: Array<[string, InferSetType export function hasAliasKey( key: string | Array, type: InferSetType -) { +): boolean { if (typeof key === 'string') { return ALIASES.has(key) && ALIASES.get(key) === type; } diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index ef0707c8..5ae4e8d5 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,4 @@ +import { platform } from 'node:os'; import { stdin, stdout } from 'node:process'; import type { Key } from 'node:readline'; import * as readline from 'node:readline'; @@ -6,10 +7,9 @@ import { cursor } from 'sisteransi'; import { hasAliasKey } from './aliases'; export * from './aliases'; +export * from './mock'; export * from './string'; -const isWindows = globalThis.process.platform.startsWith('win'); - export const CANCEL_SYMBOL = Symbol('clack:cancel'); export function isCancel(value: unknown): value is symbol { @@ -22,6 +22,10 @@ export function setRawMode(input: Readable, value: boolean) { if (i.isTTY) i.setRawMode(value); } +function isWindows(): boolean { + return platform().startsWith('win'); +} + export function block({ input = stdin, output = stdout, @@ -42,7 +46,6 @@ export function block({ if (hasAliasKey([str, name, sequence], 'cancel')) { if (hideCursor) output.write(cursor.show); process.exit(0); - return; } if (!overwrite) return; const dx = name === 'return' ? 0 : -1; @@ -62,7 +65,7 @@ export function block({ if (hideCursor) output.write(cursor.show); // Prevent Windows specific issues: https://github.com/natemoo-re/clack/issues/176 - if (input.isTTY && !isWindows) input.setRawMode(false); + if (input.isTTY && !isWindows()) input.setRawMode(false); // @ts-expect-error fix for https://github.com/nodejs/node/issues/31762#issuecomment-1441223907 rl.terminal = false; diff --git a/packages/core/src/utils/mock.ts b/packages/core/src/utils/mock.ts new file mode 100644 index 00000000..c0fb8869 --- /dev/null +++ b/packages/core/src/utils/mock.ts @@ -0,0 +1,35 @@ +import type { Key } from 'node:readline'; +import type Prompt from '../prompts/prompt'; +import type { ClackState } from '../types'; + +export type MockResult = TPrompt & { + frame: string; + pressKey: (char: string, key: Key) => void; + setCursor: (cursor: number) => void; + setState: (state: ClackState) => void; + setValue: (value: any) => void; + setIsTestMode: (state: boolean) => void; + cancel: (value?: any) => void; + submit: (value?: any) => void; + close: () => void; + updateFrame: () => void; +}; + +export let isTestMode = false; + +const _mockResult: Partial = { + setIsTestMode(state) { + isTestMode = state; + }, +}; + +export function mockPrompt(): MockResult { + isTestMode = true; + return _mockResult as MockResult; +} + +export function exposeTestUtils( + params: Partial> +): void { + Object.assign(_mockResult, params); +} diff --git a/packages/core/test/prompts/prompt.test.ts b/packages/core/test/prompts/prompt.test.ts deleted file mode 100644 index 0f976c34..00000000 --- a/packages/core/test/prompts/prompt.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { cursor } from 'sisteransi'; -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { default as Prompt } from '../../src/prompts/prompt.js'; -import { isCancel } from '../../src/utils/index.js'; -import { MockReadable } from '../mock-readable.js'; -import { MockWritable } from '../mock-writable.js'; - -describe('Prompt', () => { - let input: MockReadable; - let output: MockWritable; - - beforeEach(() => { - input = new MockReadable(); - output = new MockWritable(); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - test('renders render() result', () => { - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - // leave the promise hanging since we don't want to submit in this test - instance.prompt(); - expect(output.buffer).to.deep.equal([cursor.hide, 'foo']); - }); - - test('submits on return key', async () => { - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - const resultPromise = instance.prompt(); - input.emit('keypress', '', { name: 'return' }); - const result = await resultPromise; - expect(result).to.equal(''); - expect(isCancel(result)).to.equal(false); - expect(instance.state).to.equal('submit'); - expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); - }); - - test('cancels on ctrl-c', async () => { - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - const resultPromise = instance.prompt(); - input.emit('keypress', '\x03', { name: 'c' }); - const result = await resultPromise; - expect(isCancel(result)).to.equal(true); - expect(instance.state).to.equal('cancel'); - expect(output.buffer).to.deep.equal([cursor.hide, 'foo', '\n', cursor.show]); - }); - - test('writes initialValue to value', () => { - const eventSpy = vi.fn(); - const instance = new Prompt({ - input, - output, - render: () => 'foo', - initialValue: 'bananas', - }); - instance.on('value', eventSpy); - instance.prompt(); - expect(instance.value).to.equal('bananas'); - expect(eventSpy).toHaveBeenCalled(); - }); - - test('re-renders on resize', () => { - const renderFn = vi.fn().mockImplementation(() => 'foo'); - const instance = new Prompt({ - input, - output, - render: renderFn, - }); - instance.prompt(); - - expect(renderFn).toHaveBeenCalledTimes(1); - - output.emit('resize'); - - expect(renderFn).toHaveBeenCalledTimes(2); - }); - - test('state is active after first render', async () => { - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - - expect(instance.state).to.equal('initial'); - - instance.prompt(); - - expect(instance.state).to.equal('active'); - }); - - test('emits truthy confirm on y press', () => { - const eventFn = vi.fn(); - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - - instance.on('confirm', eventFn); - - instance.prompt(); - - input.emit('keypress', 'y', { name: 'y' }); - - expect(eventFn).toBeCalledWith(true); - }); - - test('emits falsey confirm on n press', () => { - const eventFn = vi.fn(); - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - - instance.on('confirm', eventFn); - - instance.prompt(); - - input.emit('keypress', 'n', { name: 'n' }); - - expect(eventFn).toBeCalledWith(false); - }); - - test('sets value as placeholder on tab if one is set', () => { - const instance = new Prompt({ - input, - output, - render: () => 'foo', - placeholder: 'piwa', - }); - - instance.prompt(); - - input.emit('keypress', '\t', { name: 'tab' }); - - expect(instance.value).to.equal('piwa'); - }); - - test('does not set placeholder value on tab if value already set', () => { - const instance = new Prompt({ - input, - output, - render: () => 'foo', - placeholder: 'piwa', - initialValue: 'trzy', - }); - - instance.prompt(); - - input.emit('keypress', '\t', { name: 'tab' }); - - expect(instance.value).to.equal('trzy'); - }); - - test('emits key event for unknown chars', () => { - const eventSpy = vi.fn(); - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - - instance.on('key', eventSpy); - - instance.prompt(); - - input.emit('keypress', 'z', { name: 'z' }); - - expect(eventSpy).toBeCalledWith('z'); - }); - - test('emits cursor events for movement keys', () => { - const keys = ['up', 'down', 'left', 'right']; - const eventSpy = vi.fn(); - const instance = new Prompt({ - input, - output, - render: () => 'foo', - }); - - instance.on('cursor', eventSpy); - - instance.prompt(); - - for (const key of keys) { - input.emit('keypress', key, { name: key }); - expect(eventSpy).toBeCalledWith(key); - } - }); - - test('emits cursor events for movement key aliases when not tracking', () => { - const keys = [ - ['k', 'up'], - ['j', 'down'], - ['h', 'left'], - ['l', 'right'], - ]; - const eventSpy = vi.fn(); - const instance = new Prompt( - { - input, - output, - render: () => 'foo', - }, - false - ); - - instance.on('cursor', eventSpy); - - instance.prompt(); - - for (const [alias, key] of keys) { - input.emit('keypress', alias, { name: alias }); - expect(eventSpy).toBeCalledWith(key); - } - }); -}); diff --git a/packages/core/test/utils.test.ts b/packages/core/test/utils.test.ts deleted file mode 100644 index 68ec9769..00000000 --- a/packages/core/test/utils.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Key } from 'node:readline'; -import { cursor } from 'sisteransi'; -import { afterEach, describe, expect, test, vi } from 'vitest'; -import { block } from '../src/utils/index.js'; -import { MockReadable } from './mock-readable.js'; -import { MockWritable } from './mock-writable.js'; - -describe('utils', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('block', () => { - test('clears output on keypress', () => { - const input = new MockReadable(); - const output = new MockWritable(); - // @ts-ignore - const callback = block({ input, output }); - - const event: Key = { - name: 'x', - }; - const eventData = Buffer.from('bloop'); - input.emit('keypress', eventData, event); - callback(); - expect(output.buffer).to.deep.equal([cursor.hide, cursor.move(-1, 0), cursor.show]); - }); - - test('clears output vertically when return pressed', () => { - const input = new MockReadable(); - const output = new MockWritable(); - // @ts-ignore - const callback = block({ input, output }); - - const event: Key = { - name: 'return', - }; - const eventData = Buffer.from('bloop'); - input.emit('keypress', eventData, event); - callback(); - expect(output.buffer).to.deep.equal([cursor.hide, cursor.move(0, -1), cursor.show]); - }); - - test('ignores additional keypresses after dispose', () => { - const input = new MockReadable(); - const output = new MockWritable(); - // @ts-ignore - const callback = block({ input, output }); - - const event: Key = { - name: 'x', - }; - const eventData = Buffer.from('bloop'); - input.emit('keypress', eventData, event); - callback(); - input.emit('keypress', eventData, event); - expect(output.buffer).to.deep.equal([cursor.hide, cursor.move(-1, 0), cursor.show]); - }); - - test('exits on ctrl-c', () => { - const input = new MockReadable(); - const output = new MockWritable(); - // purposely don't keep the callback since we would exit the process - // @ts-ignore - block({ input, output }); - // @ts-ignore - const spy = vi.spyOn(process, 'exit').mockImplementation(() => { - return; - }); - - const event: Key = { - name: 'c', - }; - const eventData = Buffer.from('\x03'); - input.emit('keypress', eventData, event); - expect(spy).toHaveBeenCalled(); - expect(output.buffer).to.deep.equal([cursor.hide, cursor.show]); - }); - - test('does not clear if overwrite=false', () => { - const input = new MockReadable(); - const output = new MockWritable(); - // @ts-ignore - const callback = block({ input, output, overwrite: false }); - - const event: Key = { - name: 'c', - }; - const eventData = Buffer.from('bloop'); - input.emit('keypress', eventData, event); - callback(); - expect(output.buffer).to.deep.equal([cursor.hide, cursor.show]); - }); - }); -}); diff --git a/packages/prompts/README.md b/packages/prompts/README.md index ed7b5b4f..781537f3 100644 --- a/packages/prompts/README.md +++ b/packages/prompts/README.md @@ -164,6 +164,8 @@ console.log(group.name, group.age, group.color); Execute multiple tasks in spinners. ```js +import * as p from '@clack/prompts'; + await p.tasks([ { title: 'Installing via npm', @@ -189,3 +191,52 @@ log.message('Hello, World', { symbol: color.cyan('~') }); ``` [clack-log-prompts](https://github.com/natemoo-re/clack/blob/main/.github/assets/clack-logs.png) + +## How to Test + +```js +import { mockPrompt } from '@clack/core'; +import * as p from '@clack/prompts'; +import { setTimeout as sleep } from 'node:timers/promises'; + +const cli = async () => { + return p.group({ + name: () => p.text({ message: 'What is your name?' }), + age: () => p.text({ message: 'What is your age?' }), + color: ({ results }) => + p.multiselect({ + message: `What is your favorite color ${results.name}?`, + options: [ + { value: 'red', label: 'Red' }, + { value: 'green', label: 'Green' }, + { value: 'blue', label: 'Blue' }, + ], + }), + }); +}; + +describe('cli', () => { + const prompt = mockPrompt(); + + afterEach(() => { + prompt.close(); + }); + + it('should simulate user interaction', async () => { + // Dot not use `await` or `.then()` before `prompt.submit()` or `prompt.cancel()` + const promise = cli(); + + prompt.submit('John'); + await sleep(100); + prompt.submit('22'); + await sleep(100); + prompt.submit(['green']); + + await expect(promise).resolves.toStrictEqual({ + name: 'John', + age: '22', + color: ['green'], + }); + }); +}); +``` diff --git a/packages/prompts/__tests__/index.spec.ts b/packages/prompts/__tests__/index.spec.ts new file mode 100644 index 00000000..83f9fe3f --- /dev/null +++ b/packages/prompts/__tests__/index.spec.ts @@ -0,0 +1,35 @@ +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import * as packageExports from '../src/index'; + +function camelCase(input: string): string { + return input.replace(/[-_]+(.)?/g, (_, c) => (c ? c.toUpperCase() : '')); +} + +describe('Package', () => { + const exportedKeys = Object.keys(packageExports); + + it('should export all prompts', async () => { + const promptsPath = join(__dirname, '../src/prompts'); + const promptFiles = readdirSync(promptsPath); + + for (const file of promptFiles) { + const prompt = await import(join(promptsPath, file)); + expect(exportedKeys).toContain(camelCase(prompt.default.name)); + } + }); + + it('should export selected utils', async () => { + const utils: (keyof typeof packageExports)[] = ['isCancel', 'mockPrompt', 'setGlobalAliases']; + for (const util of utils) { + expect(exportedKeys).toContain(util); + } + }); + + it('should export selected loggers', async () => { + const loggers: (keyof typeof packageExports)[] = ['cancel', 'intro', 'outro']; + for (const logger of loggers) { + expect(exportedKeys).toContain(logger); + } + }); +}); diff --git a/packages/prompts/__tests__/prompts/__snapshots__/group-multiselect.spec.ts.snap b/packages/prompts/__tests__/prompts/__snapshots__/group-multiselect.spec.ts.snap new file mode 100644 index 00000000..708f34c6 --- /dev/null +++ b/packages/prompts/__tests__/prompts/__snapshots__/group-multiselect.spec.ts.snap @@ -0,0 +1,107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`groupMultiselect should render error 1`] = ` +"│ +▲ test message +│ ◻ changed packages +│ │ ◻ @scope/a +│ │ ◻ @scope/b +│ └ ◻ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ Please select at least one option. +- Press  space  to select,  enter  to submit +" +`; + +exports[`groupMultiselect should render initial state 1`] = ` +"│ +◆ test message +│ ◻ changed packages +│ │ ◻ @scope/a +│ │ ◻ @scope/b +│ └ ◻ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ +" +`; + +exports[`groupMultiselect should render initial state with option active 1`] = ` +"│ +◆ test message +│ ◻ changed packages +│ │ ◻ @scope/a +│ │ ◻ @scope/b +│ └ ◻ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ +" +`; + +exports[`groupMultiselect should render initial state with option active and selected 1`] = ` +"│ +◆ test message +│ ◻ changed packages +│ │ ◼ @scope/a +│ │ ◻ @scope/b +│ └ ◻ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ +" +`; + +exports[`groupMultiselect should render initial state with option not active and selected 1`] = ` +"│ +◆ test message +│ ◻ changed packages +│ │ ◼ @scope/a +│ │ ◻ @scope/b +│ └ ◻ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ +" +`; + +exports[`groupMultiselect should render initial with group active 1`] = ` +"│ +◆ test message +│ ◻ changed packages +│ │ ◻ @scope/a +│ │ ◻ @scope/b +│ └ ◻ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ +" +`; + +exports[`groupMultiselect should render initial with group selected 1`] = ` +"│ +◆ test message +│ ◼ changed packages +│ │ ◼ @scope/a +│ │ ◼ @scope/b +│ └ ◼ @scope/c +│ ◻ unchanged packages +│ │ ◻ @scope/x +│ │ ◻ @scope/y +│ └ ◻ @scope/z +└ +" +`; diff --git a/packages/prompts/__tests__/prompts/__snapshots__/multiselect.spec.ts.snap b/packages/prompts/__tests__/prompts/__snapshots__/multiselect.spec.ts.snap new file mode 100644 index 00000000..529bc8a3 --- /dev/null +++ b/packages/prompts/__tests__/prompts/__snapshots__/multiselect.spec.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`multiselect should render error 1`] = ` +"│ +▲ test message +│ ◻ foo +│ ◻ bar +│ ◻ baz +└ Please select at least one option. +- Press  space  to select,  enter  to submit +" +`; + +exports[`multiselect should render initial state 1`] = ` +"│ +◆ test message +│ ◻ foo +│ ◻ bar +│ ◻ baz +└ +" +`; + +exports[`multiselect should render initial state with initialValue 1`] = ` +"│ +◆ test message +│ ◼ foo +│ ◼ bar +│ ◻ baz +└ +" +`; diff --git a/packages/prompts/__tests__/prompts/__snapshots__/select-key.spec.ts.snap b/packages/prompts/__tests__/prompts/__snapshots__/select-key.spec.ts.snap new file mode 100644 index 00000000..a5662ff4 --- /dev/null +++ b/packages/prompts/__tests__/prompts/__snapshots__/select-key.spec.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`selectKey should render initial state 1`] = ` +"│ +◆ test message +│  a  a +│  b  b +│  c  c +└ +" +`; + +exports[`selectKey should render initial state with initialValue 1`] = ` +"│ +◆ test message +│  a  a +│  b  b +│  c  c +└ +" +`; diff --git a/packages/prompts/__tests__/prompts/__snapshots__/select.spec.ts.snap b/packages/prompts/__tests__/prompts/__snapshots__/select.spec.ts.snap new file mode 100644 index 00000000..3f822176 --- /dev/null +++ b/packages/prompts/__tests__/prompts/__snapshots__/select.spec.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`select should render initial state 1`] = ` +"│ +◆ test message +│ ● foo +│ ○ bar +│ ○ baz +└ +" +`; + +exports[`select should render initial state with initialValue 1`] = ` +"│ +◆ test message +│ ○ foo +│ ● bar +│ ○ baz +└ +" +`; diff --git a/packages/prompts/__tests__/prompts/confirm.spec.ts b/packages/prompts/__tests__/prompts/confirm.spec.ts new file mode 100644 index 00000000..8727ecd9 --- /dev/null +++ b/packages/prompts/__tests__/prompts/confirm.spec.ts @@ -0,0 +1,98 @@ +import { type ConfirmPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { confirm } from '../../src'; +import { S_BAR, S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, symbol } from '../../src/utils'; + +const SLASH = color.dim('/'); +const ACTIVE_YES = `${color.green(S_RADIO_ACTIVE)} Yes ${SLASH} ${color.dim( + S_RADIO_INACTIVE +)} ${color.dim('No')}`; +const ACTIVE_NO = `${color.dim(S_RADIO_INACTIVE)} ${color.dim('Yes')} ${SLASH} ${color.green( + S_RADIO_ACTIVE +)} No`; + +describe('confirm', () => { + const mock = mockPrompt(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should render initial state', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + + confirm({ message }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${ACTIVE_YES}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should render initial state with custom `active` and `inactive`', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + + confirm({ message, active: 'active', inactive: 'inactive' }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${color.green(S_RADIO_ACTIVE)} active ${SLASH} ${color.dim( + S_RADIO_INACTIVE + )} ${color.dim('inactive')}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should render initial state with initialValue', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + + confirm({ message, initialValue: false }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${ACTIVE_NO}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should submit value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + confirm({ message }); + mock.submit(true); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)} ${color.dim('Yes')}`); + }); + + it('should render cancel', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + confirm({ message }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim('No'))}\n${color.gray(S_BAR)}` + ); + }); + + it('should render cancel with value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + confirm({ message }); + mock.cancel(false); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim('Yes'))}\n${color.gray(S_BAR)}` + ); + }); + + it('should return value on submit', async () => { + const promise = confirm({ message }); + mock.submit(true); + const result = await promise; + + expect(result).toBe(true); + }); +}); diff --git a/packages/prompts/__tests__/prompts/group-multiselect.spec.ts b/packages/prompts/__tests__/prompts/group-multiselect.spec.ts new file mode 100644 index 00000000..db974063 --- /dev/null +++ b/packages/prompts/__tests__/prompts/group-multiselect.spec.ts @@ -0,0 +1,262 @@ +import { type SelectPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { groupMultiselect } from '../../src'; +import { opt } from '../../src/prompts/group-multiselect'; +import { + type Option, + S_BAR, + S_BAR_END, + S_CHECKBOX_ACTIVE, + S_CHECKBOX_INACTIVE, + S_CHECKBOX_SELECTED, + symbol, +} from '../../src/utils'; + +const options = { + 'changed packages': [{ value: '@scope/a' }, { value: '@scope/b' }, { value: '@scope/c' }], + 'unchanged packages': [{ value: '@scope/x' }, { value: '@scope/y' }, { value: '@scope/z' }], +} satisfies Record[]>; + +describe('groupMultiselect', () => { + const mock = mockPrompt>(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should format group option', () => { + const option: Option & { group: boolean } = { + label: 'Foo', + value: 'foo', + group: true, + }; + + groupMultiselect({ message, options }); + const label = option.label; + + expect(opt(option, 'group-active')).toBe( + `${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}` + ); + expect(opt(option, 'group-active-selected')).toBe( + `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}` + ); + }); + + it('should format group option without label', () => { + const option: Option & { group: boolean } = { + value: 'foo', + group: true, + }; + + groupMultiselect({ message, options }); + const label = option.value; + + expect(opt(option, 'group-active')).toBe( + `${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}` + ); + expect(opt(option, 'group-active-selected')).toBe( + `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}` + ); + }); + + it('should format option with hint', () => { + const option: Option & { group: string } = { + label: 'Foo', + value: 'foo', + hint: 'bar', + group: '', + }; + + groupMultiselect({ message, options }); + const hint = color.dim(`(${option.hint})`); + const prefix = color.dim(`${S_BAR_END} `); + const label = option.label; + + expect(opt(option, 'active')).toBe( + `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${hint}` + ); + expect(opt(option, 'active-selected')).toBe( + `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${label} ${hint}` + ); + }); + + it('should format option without hint', () => { + const option: Option & { group: string } = { + label: 'Foo', + value: 'foo', + group: '', + }; + + groupMultiselect({ message, options }); + const prefix = color.dim(`${S_BAR_END} `); + const label = option.label; + + expect(opt(option, 'active')).toBe(`${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${label}`); + expect(opt(option, 'active-selected')).toBe( + `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${label}` + ); + expect(opt(option, 'selected')).toBe( + `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}` + ); + expect(opt(option, 'cancelled')).toBe(`${color.strikethrough(color.dim(label))}`); + expect(opt(option, 'submitted')).toBe(`${color.dim(label)}`); + expect(opt(option, 'inactive')).toBe( + `${prefix}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}` + ); + }); + + it('should format option without label', () => { + const option: Option & { group: string } = { + value: 'foo', + group: '', + }; + + groupMultiselect({ message, options }); + const prefix = `${S_BAR_END} `; + const label = option.value; + + expect(opt(option, 'active')).toBe( + `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label}` + ); + expect(opt(option, 'active-selected')).toBe( + `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label}` + ); + expect(opt(option, 'selected')).toBe( + `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}` + ); + expect(opt(option, 'cancelled')).toBe(`${color.strikethrough(color.dim(label))}`); + expect(opt(option, 'submitted')).toBe(`${color.dim(label)}`); + expect(opt(option, 'inactive')).toBe( + `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}` + ); + }); + + it('should render initial state', () => { + groupMultiselect({ message, options }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial with group active', () => { + groupMultiselect({ message, options }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial with group selected', () => { + groupMultiselect({ message, options, initialValues: ['changed packages'] }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial state with option active', () => { + groupMultiselect({ + message, + options, + cursorAt: options['changed packages'][0].value, + }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial state with option active and selected', () => { + groupMultiselect({ + message, + options, + initialValues: [options['changed packages'][0].value], + cursorAt: options['changed packages'][0].value, + }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial state with option not active and selected', () => { + groupMultiselect({ + message, + options, + initialValues: [options['changed packages'][0].value], + cursorAt: options['changed packages'][1].value, + }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render error', () => { + groupMultiselect({ message, options }); + mock.submit(); + + expect(mock.state).toBe('error'); + expect(mock.frame).toMatchSnapshot(); + }); + + // Implement when `groupMultiselect.validate` be exposed + it.todo('should render error with option selected'); + + // Implement when `groupMultiselect.validate` be exposed + it.todo('should render error with option active and selected'); + + it('should render cancel', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + groupMultiselect({ message, options }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)}`); + }); + + it('should render cancel with group selected', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + const selectedOptions = options['changed packages']; + groupMultiselect({ + message, + options, + initialValues: selectedOptions.map((item) => item.value), + }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${selectedOptions + .map((option) => opt(option, 'cancelled')) + .join(color.dim(', '))}\n${color.gray(S_BAR)}` + ); + }); + + it('should submit value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + const selectedOptions = options['changed packages']; + groupMultiselect({ + message, + options, + initialValues: selectedOptions.map((item) => item.value), + }); + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${selectedOptions + .map((option) => opt(option, 'submitted')) + .join(color.dim(', '))}` + ); + }); + + it('should return value on submit', async () => { + const value = options['changed packages'].map((item) => item.value); + + const promise = groupMultiselect({ message, options, initialValues: value }); + mock.submit(); + const result = await promise; + + expect(result).toStrictEqual(value); + }); +}); diff --git a/packages/prompts/__tests__/prompts/group.spec.ts b/packages/prompts/__tests__/prompts/group.spec.ts new file mode 100644 index 00000000..5faa165b --- /dev/null +++ b/packages/prompts/__tests__/prompts/group.spec.ts @@ -0,0 +1,62 @@ +import { group, mockPrompt, text } from '../../src'; + +describe('group', () => { + const mock = mockPrompt(); + + it('should inject prev results', (done) => { + group({ + foo: async () => 'Foo', + bar: async () => true, + baz: async () => [1, 2, 3], + tee: async ({ results }) => { + results.foo === 'Foo' && + results.bar === true && + results.baz?.[0] === 1 && + results.gee === undefined + ? done() + : done(`invalid results: ${JSON.stringify(results, null, 2)}`); + }, + gee: async () => 0, + }); + }); + + it('should not cancel group on `prompt.cancel`', (done) => { + group({ + foo: () => text({ message: '' }), + bar: () => done(), + }); + + mock.cancel(); + }); + + it('should call onCancel callback on `prompt.cancel`', (done) => { + group( + { + foo: async () => true, + bar: () => { + const promise = text({ message: '' }); + mock.cancel(); + return promise; + }, + baz: async () => false, + }, + { + onCancel: ({ results }) => { + results.foo === true && results.baz === undefined + ? done() + : done(`invalid results: ${JSON.stringify(results, null, 2)}`); + }, + } + ); + }); + + it('should throw on error', async () => { + const promise = group({ + foo: () => { + throw new Error(); + }, + }); + + await expect(promise).rejects.toThrow(); + }); +}); diff --git a/packages/prompts/__tests__/prompts/log.spec.ts b/packages/prompts/__tests__/prompts/log.spec.ts new file mode 100644 index 00000000..5dbe2f88 --- /dev/null +++ b/packages/prompts/__tests__/prompts/log.spec.ts @@ -0,0 +1,171 @@ +import { randomUUID } from 'node:crypto'; +import color from 'picocolors'; +import { cancel, intro, log, outro } from '../../src'; +import { + S_BAR, + S_BAR_END, + S_BAR_START, + S_ERROR, + S_INFO, + S_STEP_SUBMIT, + S_SUCCESS, + S_WARN, +} from '../../src/utils'; + +const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(); + +describe('log', () => { + it('should log `cancel`', () => { + const message = 'Canceled'; + + cancel(); + cancel(message); + + expect(outputSpy).toHaveBeenCalledWith(`${color.gray(S_BAR_END)} ${color.red('')}\n\n`); + expect(outputSpy).toHaveBeenCalledWith(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); + }); + + it('should log `intro`', () => { + const title = 'Hello World'; + + intro(); + intro(title); + + expect(outputSpy).toHaveBeenCalledWith(`${color.gray(S_BAR_START)} \n`); + expect(outputSpy).toHaveBeenCalledWith(`${color.gray(S_BAR_START)} ${title}\n`); + }); + + it('should log `outro`', () => { + const message = 'Good Bye World'; + + outro(); + outro(message); + + expect(outputSpy).toHaveBeenCalledWith(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} \n\n`); + expect(outputSpy).toHaveBeenCalledWith( + `${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n` + ); + }); + + it('should log `log.message`', () => { + const messageParts = randomUUID().split('-'); + + log.message(); + log.message(messageParts.join('\n'), { symbol: color.green(S_INFO) }); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_INFO)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n') + ); + expect(outputSpy).toHaveBeenCalledWith(`${color.gray(S_BAR)}\n`); + }); + + it('should log `log.info`', () => { + const messageParts = randomUUID().split('-'); + + log.info(messageParts.join('\n')); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.blue(S_INFO)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n') + ); + }); + + it('should log `log.success`', () => { + const messageParts = randomUUID().split('-'); + + log.success(messageParts.join('\n')); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_SUCCESS)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n') + ); + }); + + it('should log `log.step`', () => { + const messageParts = randomUUID().split('-'); + + log.step(messageParts.join('\n')); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n') + ); + }); + + it('should log `log.warn`', () => { + const messageParts = randomUUID().split('-'); + + log.warn(messageParts.join('\n')); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.yellow(S_WARN)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n') + ); + }); + + it('should log `log.warning`', () => { + const messageParts = randomUUID().split('-'); + + log.warn(messageParts.join('\n')); + log.warning(messageParts.join('\n')); + const expected = [ + color.gray(S_BAR), + `${color.yellow(S_WARN)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n'); + + expect(outputSpy).toHaveBeenNthCalledWith(1, expected); + expect(outputSpy).toHaveBeenNthCalledWith(2, expected); + }); + + it('should log `log.error`', () => { + const messageParts = randomUUID().split('-'); + + log.error(messageParts.join('\n')); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.red(S_ERROR)} ${messageParts[0]}`, + messageParts.slice(1).map((part) => `${color.gray(S_BAR)} ${part}`), + '', + ] + .flat() + .join('\n') + ); + }); +}); diff --git a/packages/prompts/__tests__/prompts/multiselect.spec.ts b/packages/prompts/__tests__/prompts/multiselect.spec.ts new file mode 100644 index 00000000..b2a29cd4 --- /dev/null +++ b/packages/prompts/__tests__/prompts/multiselect.spec.ts @@ -0,0 +1,167 @@ +import { type SelectPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { multiselect } from '../../src'; +import { opt } from '../../src/prompts/multiselect'; +import { + type Option, + S_BAR, + S_BAR_END, + S_CHECKBOX_ACTIVE, + S_CHECKBOX_INACTIVE, + S_CHECKBOX_SELECTED, + symbol, +} from '../../src/utils'; + +const options: Option[] = [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }]; + +describe('multiselect', () => { + const mock = mockPrompt>(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should format option state with hint', () => { + const option: Option = { + label: 'Foo', + value: 'foo', + hint: 'bar', + }; + + multiselect({ message, options }); + const hint = color.dim(`(${option.hint})`); + + expect(opt(option, 'active')).toBe(`${color.cyan(S_CHECKBOX_ACTIVE)} ${option.label} ${hint}`); + expect(opt(option, 'active-selected')).toBe( + `${color.green(S_CHECKBOX_SELECTED)} ${option.label} ${hint}` + ); + }); + + it('should format option state without hint', () => { + const option: Option = { + label: 'Foo', + value: 'foo', + }; + + multiselect({ message, options }); + + expect(opt(option, 'active')).toBe(`${color.cyan(S_CHECKBOX_ACTIVE)} ${option.label}`); + expect(opt(option, 'active-selected')).toBe( + `${color.green(S_CHECKBOX_SELECTED)} ${option.label}` + ); + expect(opt(option, 'cancelled')).toBe(`${color.strikethrough(color.dim(option.label))}`); + expect(opt(option, 'inactive')).toBe( + `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(option.label)}` + ); + option.label = undefined; + expect(opt(option, 'inactive')).toBe( + `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(option.value)}` + ); + }); + + it('should render initial state', () => { + multiselect({ message, options }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial state with initialValue', () => { + multiselect({ + message, + options, + initialValues: [options[0].value, options[1].value], + cursorAt: options[1].value, + }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render error', () => { + multiselect({ message, options }); + mock.submit(); + + expect(mock.state).toBe('error'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render cancel', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + multiselect({ message, options }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)}`); + }); + + it('should render cancel with value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + multiselect({ message, options }); + mock.cancel([options[0].value, options[1].value]); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${[options[0], options[1]] + .map((option) => opt(option, 'cancelled')) + .join(color.dim(', '))}\n${color.gray(S_BAR)}` + ); + }); + + it('should submit value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + multiselect({ message, options }); + mock.submit([options[0].value, options[1].value]); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.dim(options[0].value)}${color.dim(', ')}${color.dim( + options[1].value + )}` + ); + }); + + it('should return value on submit', async () => { + const promise = multiselect({ message, options }); + + mock.submit([options[0].value, options[1].value]); + const result = await promise; + + expect(result).toStrictEqual([options[0].value, options[1].value]); + }); + + it('should render limited options', async () => { + const extendedOptions = options.concat(options); + const cursor = 0; + + multiselect({ message, options: extendedOptions, maxItems: 5 }); + mock.setCursor(cursor); + mock.updateFrame(); + + const INACTIVE_CHECKBOX = color.dim(S_CHECKBOX_INACTIVE); + const ACTIVE_CHECKBOX = color.cyan(S_CHECKBOX_ACTIVE); + expect(mock.frame).toBe( + [ + color.gray(S_BAR), + `${symbol('initial')} ${message}`, + `${color.cyan(S_BAR)} ${ACTIVE_CHECKBOX} ${extendedOptions[cursor].value}`, + `${color.cyan(S_BAR)} ${INACTIVE_CHECKBOX} ${color.dim( + extendedOptions[cursor + 1].value + )}`, + `${color.cyan(S_BAR)} ${INACTIVE_CHECKBOX} ${color.dim( + extendedOptions[cursor + 2].value + )}`, + `${color.cyan(S_BAR)} ${INACTIVE_CHECKBOX} ${color.dim( + extendedOptions[cursor + 3].value + )}`, + `${color.cyan(S_BAR)} ${color.dim('...')}`, + color.cyan(S_BAR_END), + '', + ].join('\n') + ); + }); +}); diff --git a/packages/prompts/__tests__/prompts/note.spec.ts b/packages/prompts/__tests__/prompts/note.spec.ts new file mode 100644 index 00000000..22283e3a --- /dev/null +++ b/packages/prompts/__tests__/prompts/note.spec.ts @@ -0,0 +1,159 @@ +import { randomUUID } from 'node:crypto'; +import color from 'picocolors'; +import { note } from '../../src'; +import { + S_BAR, + S_BAR_H, + S_CONNECT_LEFT, + S_CORNER_BOTTOM_RIGHT, + S_CORNER_TOP_RIGHT, + S_STEP_SUBMIT, +} from '../../src/utils'; + +const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(); + +describe('note', () => { + it('should render note box', () => { + const length = 2; + + note(); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset('')} ${color.gray( + S_BAR_H.repeat(length - 1) + S_CORNER_TOP_RIGHT + )}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(length + 2) + S_CORNER_BOTTOM_RIGHT)}`, + '', + ].join('\n') + ); + }); + + it('should render note box with message', () => { + const title = ''; + const message = randomUUID(); + const length = message.length + 2; + + note(message); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(length - 1) + S_CORNER_TOP_RIGHT + )}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim(message)}${' '.repeat( + length - message.length + )}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(length + 2) + S_CORNER_BOTTOM_RIGHT)}`, + '', + ].join('\n') + ); + }); + + it('should render note box with message and title', () => { + const title = randomUUID(); + const message = randomUUID(); + const length = message.length + 2; + + note(message, title); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(length - title.length - 1) + S_CORNER_TOP_RIGHT + )}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim(message)}${' '.repeat( + length - message.length + )}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(length + 2) + S_CORNER_BOTTOM_RIGHT)}`, + '', + ].join('\n') + ); + }); + + it('should render note box with message bigger than title', () => { + const title = randomUUID().replace('-', ''); + const message = randomUUID(); + const length = message.length + 2; + + note(message, title); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(length - title.length - 1) + S_CORNER_TOP_RIGHT + )}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim(message)}${' '.repeat( + length - message.length + )}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(length + 2) + S_CORNER_BOTTOM_RIGHT)}`, + '', + ].join('\n') + ); + }); + + it('should render note box with title bigger than message', () => { + const title = randomUUID(); + const message = randomUUID().replace('-', ''); + const length = title.length + 2; + + note(message, title); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(length - title.length - 1) + S_CORNER_TOP_RIGHT + )}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim(message)}${' '.repeat( + length - message.length + )}${color.gray(S_BAR)}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(length + 2) + S_CORNER_BOTTOM_RIGHT)}`, + '', + ].join('\n') + ); + }); + + it('should render note box with multiline message', () => { + const title = randomUUID(); + const message = randomUUID().replace('\n', ''); + const length = title.length + 2; + + note(message, title); + + expect(outputSpy).toHaveBeenCalledWith( + [ + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(length - title.length - 1) + S_CORNER_TOP_RIGHT + )}`, + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + message.split('\n').map((line) => { + return `${color.gray(S_BAR)} ${color.dim(line)}${' '.repeat( + length - message.length + )}${color.gray(S_BAR)}`; + }), + `${color.gray(S_BAR)} ${color.dim('')}${' '.repeat(length)}${color.gray(S_BAR)}`, + `${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(length + 2) + S_CORNER_BOTTOM_RIGHT)}`, + '', + ] + .flat() + .join('\n') + ); + }); +}); diff --git a/packages/prompts/__tests__/prompts/password.spec.ts b/packages/prompts/__tests__/prompts/password.spec.ts new file mode 100644 index 00000000..4f5655ea --- /dev/null +++ b/packages/prompts/__tests__/prompts/password.spec.ts @@ -0,0 +1,92 @@ +import { randomUUID } from 'node:crypto'; +import { type PasswordPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { password } from '../../src'; +import { S_BAR, S_BAR_END, symbol } from '../../src/utils'; + +describe('password', () => { + const mock = mockPrompt(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should render initial state', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + + password({ message }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${mock.valueWithCursor}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should render error', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('error')} ${message}\n`; + const error = 'invalid value'; + + password({ + message, + validate: (value) => { + return error; + }, + }); + mock.submit(value); + + expect(mock.state).toBe('error'); + expect(mock.frame).toBe( + `${title}${color.yellow(S_BAR)} ${mock.valueWithCursor}\n${color.yellow( + S_BAR_END + )} ${color.yellow(error)}\n` + ); + }); + + it('should submit value', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + password({ message }); + mock.submit(value); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)} ${color.dim(mock.masked)}`); + }); + + it('should render cancel', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + password({ message }); + mock.cancel(value); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(mock.masked))}\n${color.gray( + S_BAR + )}` + ); + }); + + it('should render cancel without value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + password({ message }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)}`); + }); + + it('should return value on submit', async () => { + const value = randomUUID(); + + const promise = password({ message }); + mock.submit(value); + const result = await promise; + + expect(result).toBe(value); + }); +}); diff --git a/packages/prompts/__tests__/prompts/select-key.spec.ts b/packages/prompts/__tests__/prompts/select-key.spec.ts new file mode 100644 index 00000000..1d36a3a6 --- /dev/null +++ b/packages/prompts/__tests__/prompts/select-key.spec.ts @@ -0,0 +1,107 @@ +import { type SelectPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { selectKey } from '../../src'; +import { opt } from '../../src/prompts/select-key'; +import { type Option, S_BAR, symbol } from '../../src/utils'; + +const options: Option[] = [{ value: 'a' }, { value: 'b' }, { value: 'c' }]; + +describe('selectKey', () => { + const mock = mockPrompt>(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should format option state', () => { + const option: Option = { + label: 'Foo', + value: 'foo', + hint: 'bar', + }; + const hint = option.hint; + + selectKey({ message, options }); + + expect(opt(option, 'selected')).toBe(`${color.dim(option.label)}`); + expect(opt(option, 'active')).toBe( + `${color.bgCyan(color.gray(` ${option.value} `))} ${option.label} ${color.dim( + `(${option.hint})` + )}` + ); + option.hint = undefined; + expect(opt(option, 'active')).toBe( + `${color.bgCyan(color.gray(` ${option.value} `))} ${option.label}` + ); + expect(opt(option, 'cancelled')).toBe(`${color.strikethrough(color.dim(option.label))}`); + option.hint = hint; + expect(opt(option, 'inactive')).toBe( + `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${option.label} ${color.dim( + `(${option.hint})` + )}` + ); + option.hint = undefined; + expect(opt(option, 'inactive')).toBe( + `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${option.label}` + ); + option.label = undefined; + expect(opt(option)).toBe( + `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${option.value}` + ); + }); + + it('should render initial state', () => { + selectKey({ message, options }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial state with initialValue', () => { + selectKey({ message, options, initialValue: options[1].value[0] }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render cancel', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + selectKey({ message, options }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${opt(options[0], 'cancelled')}\n${color.gray(S_BAR)}` + ); + }); + + it('should submit value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + selectKey({ message, options }); + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)} ${opt(options[0], 'selected')}`); + }); + + it('should return value on submit', async () => { + const promise = selectKey({ message, options }); + + mock.submit(); + const result = await promise; + + expect(result).toBe(options[0].value); + }); + + it('should return value on select key', async () => { + const promise = selectKey({ message, options }); + + mock.emit('key', options[1].value[0]); + const result = await promise; + + expect(result).toBe(options[1].value); + }); +}); diff --git a/packages/prompts/__tests__/prompts/select.spec.ts b/packages/prompts/__tests__/prompts/select.spec.ts new file mode 100644 index 00000000..af677ae9 --- /dev/null +++ b/packages/prompts/__tests__/prompts/select.spec.ts @@ -0,0 +1,135 @@ +import { type SelectPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { select } from '../../src'; +import { opt } from '../../src/prompts/select'; +import { + type Option, + S_BAR, + S_BAR_END, + S_RADIO_ACTIVE, + S_RADIO_INACTIVE, + symbol, +} from '../../src/utils'; + +const options: Option[] = [{ value: 'foo' }, { value: 'bar' }, { value: 'baz' }]; + +describe('select', () => { + const mock = mockPrompt>(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should format option state', () => { + const option: Option = { + label: 'Foo', + value: 'foo', + hint: 'bar', + }; + + select({ message, options }); + + expect(opt(option, 'selected')).toBe(`${color.dim(option.label)}`); + expect(opt(option, 'active')).toBe( + `${color.green(S_RADIO_ACTIVE)} ${option.label} ${ + option.hint ? color.dim(`(${option.hint})`) : '' + }` + ); + expect(opt(option, 'cancelled')).toBe(`${color.strikethrough(color.dim(option.label))}`); + expect(opt(option, 'inactive')).toBe( + `${color.dim(S_RADIO_INACTIVE)} ${color.dim(option.label)}` + ); + option.label = undefined; + expect(opt(option, 'inactive')).toBe( + `${color.dim(S_RADIO_INACTIVE)} ${color.dim(option.value)}` + ); + }); + + it('should render initial state', () => { + select({ message, options }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render initial state with initialValue', () => { + select({ message, options, initialValue: options[1].value }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toMatchSnapshot(); + }); + + it('should render cancel', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + select({ message, options }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.strikethrough( + color.dim(options[0].value) + )}\n${color.gray(S_BAR)}` + ); + }); + + it('should render cancel with value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + select({ message, options }); + mock.setCursor(1); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.strikethrough( + color.dim(options[1].value) + )}\n${color.gray(S_BAR)}` + ); + }); + + it('should submit value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + select({ message, options }); + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)} ${color.dim(options[0].value)}`); + }); + + it('should return value on submit', async () => { + const promise = select({ message, options }); + + mock.submit(); + const result = await promise; + + expect(result).toBe(options[0].value); + }); + + it('should render limited options', async () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}`; + + const extendedOptions = options.concat(options); + select({ message, options: extendedOptions, maxItems: 5 }); + const cursor = 0; + mock.setCursor(cursor); + mock.updateFrame(); + + const INACTIVE_RADIO = color.dim(S_RADIO_INACTIVE); + const ACTIVE_RADIO = color.green(S_RADIO_ACTIVE); + expect(mock.frame).toBe( + [ + title, + `${color.cyan(S_BAR)} ${ACTIVE_RADIO} ${extendedOptions[cursor].value}`, + `${color.cyan(S_BAR)} ${INACTIVE_RADIO} ${color.dim(extendedOptions[cursor + 1].value)}`, + `${color.cyan(S_BAR)} ${INACTIVE_RADIO} ${color.dim(extendedOptions[cursor + 2].value)}`, + `${color.cyan(S_BAR)} ${INACTIVE_RADIO} ${color.dim(extendedOptions[cursor + 3].value)}`, + `${color.cyan(S_BAR)} ${color.dim('...')}`, + color.cyan(S_BAR_END), + '', + ].join('\n') + ); + }); +}); diff --git a/packages/prompts/__tests__/prompts/spinner.spec.ts b/packages/prompts/__tests__/prompts/spinner.spec.ts new file mode 100644 index 00000000..711b829f --- /dev/null +++ b/packages/prompts/__tests__/prompts/spinner.spec.ts @@ -0,0 +1,181 @@ +import { randomUUID } from 'node:crypto'; +import color from 'picocolors'; +import { cursor, erase } from 'sisteransi'; +import { spinner } from '../../src'; +import { dotsInterval, frameInterval, frames } from '../../src/prompts/spinner'; +import { S_STEP_CANCEL, S_STEP_ERROR, S_STEP_SUBMIT } from '../../src/utils'; + +jest.mock('@clack/core', () => ({ + block: () => () => {}, +})); + +const outputSpy = jest.spyOn(process.stdout, 'write').mockImplementation(); + +describe('spinner', () => { + const s = spinner(); + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + s.stop(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it("should render dot's animation", () => { + const message = randomUUID(); + + s.start(message); + const dotsCounter = 5; + const dotsFrameCounter = dotsCounter / dotsInterval; + jest.advanceTimersByTime(frameInterval * dotsFrameCounter); + + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}.`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}..`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}...`); + expect(outputSpy).not.toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}....`); + }); + + it("should render spinner's animation", () => { + const message = randomUUID(); + + s.start(message); + jest.advanceTimersByTime(frameInterval * frames.length); + + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[1])} ${message}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[2])} ${message}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[3])} ${message}`); + }); + + it('should clear prev message after each render', () => { + const message = randomUUID(); + + s.start(message); + jest.advanceTimersByTime(frameInterval); + + expect(outputSpy).toHaveBeenCalledWith(cursor.move(-999, 0)); + expect(outputSpy).toHaveBeenCalledWith(erase.down(1)); + }); + + it('should remove dots from message', () => { + const message = randomUUID(); + + s.start(`${message}...`); + jest.advanceTimersByTime(frameInterval); + + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}`); + }); + + it("should update spinner's message", () => { + const message = randomUUID(); + const newMessage = randomUUID(); + + s.start(message); + jest.advanceTimersByTime(frameInterval); + s.message(newMessage); + jest.advanceTimersByTime(frameInterval); + s.message(message); + jest.advanceTimersByTime(frameInterval); + s.message(newMessage); + jest.advanceTimersByTime(frameInterval); + + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[1])} ${newMessage}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[2])} ${message}`); + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[3])} ${newMessage}`); + }); + + it('should prevent invalid message on update', () => { + const message = randomUUID(); + + s.start(message); + //@ts-expect-error + s.message(null); + jest.advanceTimersByTime(frameInterval); + + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} ${message}`); + }); + + it('should clear message on update', () => { + const message = randomUUID(); + + s.start(message); + s.message(); + jest.advanceTimersByTime(frameInterval); + + expect(outputSpy).toHaveBeenCalledWith(`${color.magenta(frames[0])} `); + }); + + it('should clear message on stop', () => { + const message = randomUUID(); + + s.start(message); + s.stop(); + jest.advanceTimersByTime(frameInterval); + + expect(outputSpy).toHaveBeenCalledWith(`${color.green(S_STEP_SUBMIT)} \n`); + }); + + it('should prevent invalid message on update', () => { + const message = randomUUID(); + + s.start(message); + jest.advanceTimersByTime(frameInterval); + //@ts-expect-error + s.stop(null); + + expect(outputSpy).toHaveBeenCalledWith(`${color.green(S_STEP_SUBMIT)} ${message}\n`); + }); + + it('should update message on stop', () => { + const message = randomUUID(); + const newMessage = randomUUID(); + + s.start(message); + jest.advanceTimersByTime(frameInterval); + s.stop(newMessage); + + expect(outputSpy).toHaveBeenCalledWith(`${color.green(S_STEP_SUBMIT)} ${newMessage}\n`); + }); + + it('should stop spinner on `exit`', () => { + s.start(); + process.emit('exit', 1); + + expect(outputSpy).toHaveBeenCalledWith(`${color.red(S_STEP_CANCEL)} Canceled\n`); + }); + + it('should stop spinner on `SIGINT`', () => { + s.start(); + process.emit('SIGINT'); + + expect(outputSpy).toHaveBeenCalledWith(`${color.red(S_STEP_CANCEL)} Canceled\n`); + }); + + it('should stop spinner on `SIGTERM`', () => { + s.start(); + process.emit('SIGTERM'); + + expect(outputSpy).toHaveBeenCalledWith(`${color.red(S_STEP_CANCEL)} Canceled\n`); + }); + + it('should stop spinner on `unhandledRejection`', () => { + s.start(); + process.emit('unhandledRejection', '', new Promise(() => {})); + + expect(outputSpy).toHaveBeenCalledWith(`${color.red(S_STEP_ERROR)} Something went wrong\n`); + }); + + it('should stop spinner on `uncaughtExceptionMonitor`', () => { + s.start(); + process.emit('uncaughtExceptionMonitor', new Error()); + + expect(outputSpy).toHaveBeenCalledWith(`${color.red(S_STEP_ERROR)} Something went wrong\n`); + }); +}); diff --git a/packages/prompts/__tests__/prompts/tasks.spec.ts b/packages/prompts/__tests__/prompts/tasks.spec.ts new file mode 100644 index 00000000..c11d87d5 --- /dev/null +++ b/packages/prompts/__tests__/prompts/tasks.spec.ts @@ -0,0 +1,88 @@ +import { randomUUID } from 'node:crypto'; +import { type Task, tasks } from '../../src'; + +const startSpy = jest.fn(); +const messageSpy = jest.fn(); +const stopSpy = jest.fn(); + +jest.mock('../../src/prompts/spinner', () => () => ({ + start: startSpy, + message: messageSpy, + stop: stopSpy, +})); + +describe('tasks', () => { + it('should start tasks in sequence', async () => { + const length = 3; + const data: Task[] = Array.from(Array(length).keys()).map((i) => ({ + title: String(i), + async task(message) {}, + })); + + await tasks(data); + + expect.assertions(length); + for (let i = 0; i < length; i++) { + expect(startSpy).toHaveBeenNthCalledWith(i + 1, String(i)); + } + }); + + it('should skip disabled task', async () => { + const length = 3; + const data: Task[] = Array.from(Array(length).keys()).map((i) => ({ + title: String(i), + enabled: !(i === 1), + async task(message) {}, + })); + + await tasks(data); + + expect(startSpy).toHaveBeenNthCalledWith(1, String(0)); + expect(startSpy).toHaveBeenNthCalledWith(2, String(2)); + }); + + it('should stop tasks in sequence', async () => { + const length = 3; + const data: Task[] = Array.from(Array(length).keys()).map((i) => ({ + title: String(i), + async task(message) {}, + })); + + await tasks(data); + + expect.assertions(length); + for (let i = 0; i < length; i++) { + expect(stopSpy).toHaveBeenNthCalledWith(i + 1, String(i)); + } + }); + + it('should update task message', async () => { + const msg = randomUUID(); + + await tasks([ + { + title: '', + task(message) { + message(msg); + }, + }, + ]); + + expect(messageSpy).toHaveBeenCalledWith(msg); + }); + + it('should stop task with returned message', async () => { + const msg = randomUUID(); + + await tasks([ + { + title: '', + task() { + return msg; + }, + }, + ]); + + expect(stopSpy).toHaveBeenCalledWith(msg); + }); +}); diff --git a/packages/prompts/__tests__/prompts/text.spec.ts b/packages/prompts/__tests__/prompts/text.spec.ts new file mode 100644 index 00000000..6999a399 --- /dev/null +++ b/packages/prompts/__tests__/prompts/text.spec.ts @@ -0,0 +1,125 @@ +import { randomUUID } from 'node:crypto'; +import { type TextPrompt, mockPrompt } from '@clack/core'; +import color from 'picocolors'; +import { text } from '../../src'; +import { S_BAR, S_BAR_END, formatPlaceholder, symbol } from '../../src/utils'; + +describe('text', () => { + const mock = mockPrompt(); + const message = 'test message'; + + afterEach(() => { + mock.close(); + }); + + it('should render initial state', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + + text({ message }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${mock.valueWithCursor}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should render initial state with placeholder', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + const placeholder = randomUUID(); + + text({ message, placeholder }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${formatPlaceholder(placeholder)}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should render initial state with initialValue', () => { + const title = `${color.gray(S_BAR)}\n${symbol('initial')} ${message}\n`; + const value = randomUUID(); + + text({ message, initialValue: value }); + + expect(mock.state).toBe('initial'); + expect(mock.frame).toBe( + `${title}${color.cyan(S_BAR)} ${mock.valueWithCursor}\n${color.cyan(S_BAR_END)}\n` + ); + }); + + it('should render error', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('error')} ${message}\n`; + const error = 'invalid value'; + + text({ + message, + validate: (value) => { + return error; + }, + }); + mock.submit(value); + + expect(mock.state).toBe('error'); + expect(mock.frame).toBe( + `${title}${color.yellow(S_BAR)} ${mock.valueWithCursor}\n${color.yellow( + S_BAR_END + )} ${color.yellow(error)}\n` + ); + }); + + it('should submit initialValue', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + text({ message, initialValue: value }); + mock.submit(); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)} ${color.dim(value)}`); + }); + + it('should submit value', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('submit')} ${message}\n`; + + text({ message }); + mock.submit(value); + + expect(mock.state).toBe('submit'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)} ${color.dim(value)}`); + }); + + it('should render cancel', () => { + const value = randomUUID(); + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + text({ message }); + mock.cancel(value); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe( + `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(value))}\n${color.gray(S_BAR)}` + ); + }); + + it('should render cancel without value', () => { + const title = `${color.gray(S_BAR)}\n${symbol('cancel')} ${message}\n`; + + text({ message }); + mock.cancel(); + + expect(mock.state).toBe('cancel'); + expect(mock.frame).toBe(`${title}${color.gray(S_BAR)}`); + }); + + it('should return value on submit', async () => { + const value = randomUUID(); + + const promise = text({ message }); + mock.submit(value); + const result = await promise; + + expect(result).toBe(value); + }); +}); diff --git a/packages/prompts/__tests__/utils/format.spec.ts b/packages/prompts/__tests__/utils/format.spec.ts new file mode 100644 index 00000000..34bd5e24 --- /dev/null +++ b/packages/prompts/__tests__/utils/format.spec.ts @@ -0,0 +1,112 @@ +import { randomUUID } from 'node:crypto'; +import color from 'picocolors'; +import { formatPlaceholder, limitOptions } from '../../src/utils'; + +describe('format', () => { + describe('limitOptions()', () => { + const options = Array.from(Array(40).keys()).map(String); + const limit = color.dim('...'); + + it('should not limit options', () => { + const result = limitOptions({ + options, + maxItems: undefined, + cursor: 0, + style(option, active) { + return option; + }, + }); + + expect(result).toHaveLength(result.length); + }); + + it('should limit bottom options', () => { + const maxItems = 5; + + const result = limitOptions({ + options, + maxItems, + cursor: 0, + style(option, active) { + return option; + }, + }); + + expect(result).toHaveLength(maxItems); + expect(result[0]).toBe(options[0]); + expect(result[result.length - 1]).toBe(limit); + }); + + it('should limit top options', () => { + const maxItems = 5; + + const result = limitOptions({ + options, + maxItems, + cursor: options.length - 1, + style(option, active) { + return option; + }, + }); + + expect(result).toHaveLength(maxItems); + expect(result[0]).toBe(limit); + expect(result[result.length - 1]).toBe(options[options.length - 1]); + }); + + it('should limit top and bottom options', () => { + const maxItems = 5; + const middleOption = Math.floor(maxItems / 2); + const cursor = Math.floor(options.length / 2); + + const result = limitOptions({ + options, + maxItems, + cursor, + style(option, active) { + return option; + }, + }); + + expect(result).toHaveLength(maxItems); + expect(result[0]).toBe(limit); + expect(result[middleOption]).toBe(options[cursor]); + expect(result[result.length - 1]).toBe(limit); + }); + + it('should limit by terminal rows', () => { + const maxTerminalRows = 20; + process.stdout.rows = maxTerminalRows; + + const result = limitOptions({ + options, + maxItems: undefined, + cursor: 0, + style(option, active) { + return option; + }, + }); + + expect(result).toHaveLength(maxTerminalRows - 4); + expect(result[0]).toBe(options[0]); + expect(result[result.length - 1]).toBe(limit); + }); + }); + + describe('formatPlaceholder()', () => { + it('should return cursor', () => { + const result = formatPlaceholder(undefined); + + expect(result).toBe(color.inverse(color.hidden('_'))); + }); + + it('should return placeholder', () => { + const value = randomUUID(); + const placeholder = color.inverse(value[0]) + color.dim(value.slice(1)); + + const result = formatPlaceholder(value); + + expect(result).toBe(placeholder); + }); + }); +}); diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 2d753648..689f5753 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -1,835 +1,22 @@ -import { - ConfirmPrompt, - GroupMultiSelectPrompt, - MultiSelectPrompt, - PasswordPrompt, - SelectKeyPrompt, - SelectPrompt, - type State, - TextPrompt, - block, - isCancel, -} from '@clack/core'; -import isUnicodeSupported from 'is-unicode-supported'; -import color from 'picocolors'; -import { cursor, erase } from 'sisteransi'; - -export { isCancel, setGlobalAliases } from '@clack/core'; - -const unicode = isUnicodeSupported(); -const s = (c: string, fallback: string) => (unicode ? c : fallback); -const S_STEP_ACTIVE = s('◆', '*'); -const S_STEP_CANCEL = s('■', 'x'); -const S_STEP_ERROR = s('▲', 'x'); -const S_STEP_SUBMIT = s('◇', 'o'); - -const S_BAR_START = s('┌', 'T'); -const S_BAR = s('│', '|'); -const S_BAR_END = s('└', '—'); - -const S_RADIO_ACTIVE = s('●', '>'); -const S_RADIO_INACTIVE = s('○', ' '); -const S_CHECKBOX_ACTIVE = s('◻', '[•]'); -const S_CHECKBOX_SELECTED = s('◼', '[+]'); -const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); -const S_PASSWORD_MASK = s('▪', '•'); - -const S_BAR_H = s('─', '-'); -const S_CORNER_TOP_RIGHT = s('╮', '+'); -const S_CONNECT_LEFT = s('├', '+'); -const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); - -const S_INFO = s('●', '•'); -const S_SUCCESS = s('◆', '*'); -const S_WARN = s('▲', '!'); -const S_ERROR = s('■', 'x'); - -const symbol = (state: State) => { - switch (state) { - case 'initial': - case 'active': - return color.cyan(S_STEP_ACTIVE); - case 'cancel': - return color.red(S_STEP_CANCEL); - case 'error': - return color.yellow(S_STEP_ERROR); - case 'submit': - return color.green(S_STEP_SUBMIT); - } -}; - -interface LimitOptionsParams { - options: TOption[]; - maxItems: number | undefined; - cursor: number; - style: (option: TOption, active: boolean) => string; -} - -const limitOptions = (params: LimitOptionsParams): string[] => { - const { cursor, options, style } = params; - - const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; - const outputMaxItems = Math.max(process.stdout.rows - 4, 0); - // We clamp to minimum 5 because anything less doesn't make sense UX wise - const maxItems = Math.min(outputMaxItems, Math.max(paramMaxItems, 5)); - let slidingWindowLocation = 0; - - if (cursor >= slidingWindowLocation + maxItems - 3) { - slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); - } else if (cursor < slidingWindowLocation + 2) { - slidingWindowLocation = Math.max(cursor - 2, 0); - } - - const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; - const shouldRenderBottomEllipsis = - maxItems < options.length && slidingWindowLocation + maxItems < options.length; - - return options - .slice(slidingWindowLocation, slidingWindowLocation + maxItems) - .map((option, i, arr) => { - const isTopLimit = i === 0 && shouldRenderTopEllipsis; - const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; - return isTopLimit || isBottomLimit - ? color.dim('...') - : style(option, i + slidingWindowLocation === cursor); - }); -}; - -export interface TextOptions { - message: string; - placeholder?: string; - defaultValue?: string; - initialValue?: string; - validate?: (value: string) => string | undefined; -} -export const text = (opts: TextOptions) => { - return new TextPrompt({ - validate: opts.validate, - placeholder: opts.placeholder, - defaultValue: opts.defaultValue, - initialValue: opts.initialValue, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - const placeholder = opts.placeholder - ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) - : color.inverse(color.hidden('_')); - const value = !this.value ? placeholder : this.valueWithCursor; - - switch (this.state) { - case 'error': - return `${title.trim()}\n${color.yellow(S_BAR)} ${value}\n${color.yellow( - S_BAR_END - )} ${color.yellow(this.error)}\n`; - case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(this.value || opts.placeholder)}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough( - color.dim(this.value ?? '') - )}${this.value?.trim() ? `\n${color.gray(S_BAR)}` : ''}`; - default: - return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`; - } - }, - }).prompt() as Promise; -}; - -export interface PasswordOptions { - message: string; - mask?: string; - validate?: (value: string) => string | undefined; -} -export const password = (opts: PasswordOptions) => { - return new PasswordPrompt({ - validate: opts.validate, - mask: opts.mask ?? S_PASSWORD_MASK, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - const value = this.valueWithCursor; - const masked = this.masked; - - switch (this.state) { - case 'error': - return `${title.trim()}\n${color.yellow(S_BAR)} ${masked}\n${color.yellow( - S_BAR_END - )} ${color.yellow(this.error)}\n`; - case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(masked)}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(masked ?? ''))}${ - masked ? `\n${color.gray(S_BAR)}` : '' - }`; - default: - return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`; - } - }, - }).prompt() as Promise; -}; - -export interface ConfirmOptions { - message: string; - active?: string; - inactive?: string; - initialValue?: boolean; -} -export const confirm = (opts: ConfirmOptions) => { - const active = opts.active ?? 'Yes'; - const inactive = opts.inactive ?? 'No'; - return new ConfirmPrompt({ - active, - inactive, - initialValue: opts.initialValue ?? true, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - const value = this.value ? active : inactive; - - switch (this.state) { - case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough( - color.dim(value) - )}\n${color.gray(S_BAR)}`; - default: { - return `${title}${color.cyan(S_BAR)} ${ - this.value - ? `${color.green(S_RADIO_ACTIVE)} ${active}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` - } ${color.dim('/')} ${ - !this.value - ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` - }\n${color.cyan(S_BAR_END)}\n`; - } - } - }, - }).prompt() as Promise; -}; - -type Primitive = Readonly; - -type Option = Value extends Primitive - ? { value: Value; label?: string; hint?: string } - : { value: Value; label: string; hint?: string }; - -export interface SelectOptions { - message: string; - options: Option[]; - initialValue?: Value; - maxItems?: number; -} - -export const select = (opts: SelectOptions) => { - const opt = (option: Option, state: 'inactive' | 'active' | 'selected' | 'cancelled') => { - const label = option.label ?? String(option.value); - switch (state) { - case 'selected': - return `${color.dim(label)}`; - case 'active': - return `${color.green(S_RADIO_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - case 'cancelled': - return `${color.strikethrough(color.dim(label))}`; - default: - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; - } - }; - - return new SelectPrompt({ - options: opts.options, - initialValue: opts.initialValue, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - - switch (this.state) { - case 'submit': - return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${opt( - this.options[this.cursor], - 'cancelled' - )}\n${color.gray(S_BAR)}`; - default: { - return `${title}${color.cyan(S_BAR)} ${limitOptions({ - cursor: this.cursor, - options: this.options, - maxItems: opts.maxItems, - style: (item, active) => opt(item, active ? 'active' : 'inactive'), - }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; - } - } - }, - }).prompt() as Promise; -}; - -export const selectKey = (opts: SelectOptions) => { - const opt = ( - option: Option, - state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive' - ) => { - const label = option.label ?? String(option.value); - if (state === 'selected') { - return `${color.dim(label)}`; - } - if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; - } - if (state === 'active') { - return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - } - return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - }; - - return new SelectKeyPrompt({ - options: opts.options, - initialValue: opts.initialValue, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - - switch (this.state) { - case 'submit': - return `${title}${color.gray(S_BAR)} ${opt( - this.options.find((opt) => opt.value === this.value) ?? opts.options[0], - 'selected' - )}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray( - S_BAR - )}`; - default: { - return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; - } - } - }, - }).prompt() as Promise; -}; - -export interface MultiSelectOptions { - message: string; - options: Option[]; - initialValues?: Value[]; - maxItems?: number; - required?: boolean; - cursorAt?: Value; -} -export const multiselect = (opts: MultiSelectOptions) => { - const opt = ( - option: Option, - state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled' - ) => { - const label = option.label ?? String(option.value); - if (state === 'active') { - return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; - } - if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; - } - if (state === 'active-selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'submitted') { - return `${color.dim(label)}`; - } - return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; - }; - - return new MultiSelectPrompt({ - options: opts.options, - initialValues: opts.initialValues, - required: opts.required ?? true, - cursorAt: opts.cursorAt, - validate(selected: Value[]) { - if (this.required && selected.length === 0) - return `Please select at least one option.\n${color.reset( - color.dim( - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( - color.bgWhite(color.inverse(' enter ')) - )} to submit` - ) - )}`; - }, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - - const styleOption = (option: Option, active: boolean) => { - const selected = this.value.includes(option.value); - if (active && selected) { - return opt(option, 'active-selected'); - } - if (selected) { - return opt(option, 'selected'); - } - return opt(option, active ? 'active' : 'inactive'); - }; - - switch (this.state) { - case 'submit': { - return `${title}${color.gray(S_BAR)} ${ - this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'submitted')) - .join(color.dim(', ')) || color.dim('none') - }`; - } - case 'cancel': { - const label = this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'cancelled')) - .join(color.dim(', ')); - return `${title}${color.gray(S_BAR)} ${ - label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' - }`; - } - case 'error': { - const footer = this.error - .split('\n') - .map((ln, i) => - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` - ) - .join('\n'); - return `${title + color.yellow(S_BAR)} ${limitOptions({ - options: this.options, - cursor: this.cursor, - maxItems: opts.maxItems, - style: styleOption, - }).join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; - } - default: { - return `${title}${color.cyan(S_BAR)} ${limitOptions({ - options: this.options, - cursor: this.cursor, - maxItems: opts.maxItems, - style: styleOption, - }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; - } - } - }, - }).prompt() as Promise; -}; - -export interface GroupMultiSelectOptions { - message: string; - options: Record[]>; - initialValues?: Value[]; - required?: boolean; - cursorAt?: Value; -} -export const groupMultiselect = (opts: GroupMultiSelectOptions) => { - const opt = ( - option: Option, - state: - | 'inactive' - | 'active' - | 'selected' - | 'active-selected' - | 'group-active' - | 'group-active-selected' - | 'submitted' - | 'cancelled', - options: Option[] = [] - ) => { - const label = option.label ?? String(option.value); - const isItem = typeof (option as any).group === 'string'; - const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); - const isLast = isItem && (next as any).group === true; - const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ''; - - if (state === 'active') { - return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'group-active') { - return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; - } - if (state === 'group-active-selected') { - return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; - } - if (state === 'selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; - } - if (state === 'cancelled') { - return `${color.strikethrough(color.dim(label))}`; - } - if (state === 'active-selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; - } - if (state === 'submitted') { - return `${color.dim(label)}`; - } - return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; - }; - - return new GroupMultiSelectPrompt({ - options: opts.options, - initialValues: opts.initialValues, - required: opts.required ?? true, - cursorAt: opts.cursorAt, - validate(selected: Value[]) { - if (this.required && selected.length === 0) - return `Please select at least one option.\n${color.reset( - color.dim( - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( - color.bgWhite(color.inverse(' enter ')) - )} to submit` - ) - )}`; - }, - render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - - switch (this.state) { - case 'submit': { - return `${title}${color.gray(S_BAR)} ${this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'submitted')) - .join(color.dim(', '))}`; - } - case 'cancel': { - const label = this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'cancelled')) - .join(color.dim(', ')); - return `${title}${color.gray(S_BAR)} ${ - label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' - }`; - } - case 'error': { - const footer = this.error - .split('\n') - .map((ln, i) => - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` - ) - .join('\n'); - return `${title}${color.yellow(S_BAR)} ${this.options - .map((option, i, options) => { - const selected = - this.value.includes(option.value) || - (option.group === true && this.isGroupSelected(`${option.value}`)); - const active = i === this.cursor; - const groupActive = - !active && - typeof option.group === 'string' && - this.options[this.cursor].value === option.group; - if (groupActive) { - return opt(option, selected ? 'group-active-selected' : 'group-active', options); - } - if (active && selected) { - return opt(option, 'active-selected', options); - } - if (selected) { - return opt(option, 'selected', options); - } - return opt(option, active ? 'active' : 'inactive', options); - }) - .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; - } - default: { - return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i, options) => { - const selected = - this.value.includes(option.value) || - (option.group === true && this.isGroupSelected(`${option.value}`)); - const active = i === this.cursor; - const groupActive = - !active && - typeof option.group === 'string' && - this.options[this.cursor].value === option.group; - if (groupActive) { - return opt(option, selected ? 'group-active-selected' : 'group-active', options); - } - if (active && selected) { - return opt(option, 'active-selected', options); - } - if (selected) { - return opt(option, 'selected', options); - } - return opt(option, active ? 'active' : 'inactive', options); - }) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; - } - } - }, - }).prompt() as Promise; -}; - -const strip = (str: string) => str.replace(ansiRegex(), ''); -export const note = (message = '', title = '') => { - const lines = `\n${message}\n`.split('\n'); - const titleLen = strip(title).length; - const len = - Math.max( - lines.reduce((sum, ln) => { - const line = strip(ln); - return line.length > sum ? line.length : sum; - }, 0), - titleLen - ) + 2; - const msg = lines - .map( - (ln) => - `${color.gray(S_BAR)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray( - S_BAR - )}` - ) - .join('\n'); - process.stdout.write( - `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( - S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT - )}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` - ); -}; - -export const cancel = (message = '') => { - process.stdout.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); -}; - -export const intro = (title = '') => { - process.stdout.write(`${color.gray(S_BAR_START)} ${title}\n`); -}; - -export const outro = (message = '') => { - process.stdout.write(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n`); -}; - -export type LogMessageOptions = { - symbol?: string; -}; -export const log = { - message: (message = '', { symbol = color.gray(S_BAR) }: LogMessageOptions = {}) => { - const parts = [`${color.gray(S_BAR)}`]; - if (message) { - const [firstLine, ...lines] = message.split('\n'); - parts.push(`${symbol} ${firstLine}`, ...lines.map((ln) => `${color.gray(S_BAR)} ${ln}`)); - } - process.stdout.write(`${parts.join('\n')}\n`); - }, - info: (message: string) => { - log.message(message, { symbol: color.blue(S_INFO) }); - }, - success: (message: string) => { - log.message(message, { symbol: color.green(S_SUCCESS) }); - }, - step: (message: string) => { - log.message(message, { symbol: color.green(S_STEP_SUBMIT) }); - }, - warn: (message: string) => { - log.message(message, { symbol: color.yellow(S_WARN) }); - }, - /** alias for `log.warn()`. */ - warning: (message: string) => { - log.warn(message); - }, - error: (message: string) => { - log.message(message, { symbol: color.red(S_ERROR) }); - }, -}; - -export const spinner = () => { - const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; - const delay = unicode ? 80 : 120; - const isCI = process.env.CI === 'true'; - - let unblock: () => void; - let loop: NodeJS.Timeout; - let isSpinnerActive = false; - let _message = ''; - let _prevMessage: string | undefined = undefined; - - const handleExit = (code: number) => { - const msg = code > 1 ? 'Something went wrong' : 'Canceled'; - if (isSpinnerActive) stop(msg, code); - }; - - const errorEventHandler = () => handleExit(2); - const signalEventHandler = () => handleExit(1); - - const registerHooks = () => { - // Reference: https://nodejs.org/api/process.html#event-uncaughtexception - process.on('uncaughtExceptionMonitor', errorEventHandler); - // Reference: https://nodejs.org/api/process.html#event-unhandledrejection - process.on('unhandledRejection', errorEventHandler); - // Reference Signal Events: https://nodejs.org/api/process.html#signal-events - process.on('SIGINT', signalEventHandler); - process.on('SIGTERM', signalEventHandler); - process.on('exit', handleExit); - }; - - const clearHooks = () => { - process.removeListener('uncaughtExceptionMonitor', errorEventHandler); - process.removeListener('unhandledRejection', errorEventHandler); - process.removeListener('SIGINT', signalEventHandler); - process.removeListener('SIGTERM', signalEventHandler); - process.removeListener('exit', handleExit); - }; - - const clearPrevMessage = () => { - if (_prevMessage === undefined) return; - if (isCI) process.stdout.write('\n'); - const prevLines = _prevMessage.split('\n'); - process.stdout.write(cursor.move(-999, prevLines.length - 1)); - process.stdout.write(erase.down(prevLines.length)); - }; - - const parseMessage = (msg: string): string => { - return msg.replace(/\.+$/, ''); - }; - - const start = (msg = ''): void => { - isSpinnerActive = true; - unblock = block(); - _message = parseMessage(msg); - process.stdout.write(`${color.gray(S_BAR)}\n`); - let frameIndex = 0; - let dotsTimer = 0; - registerHooks(); - loop = setInterval(() => { - if (isCI && _message === _prevMessage) { - return; - } - clearPrevMessage(); - _prevMessage = _message; - const frame = color.magenta(frames[frameIndex]); - const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3); - process.stdout.write(`${frame} ${_message}${loadingDots}`); - frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; - dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0; - }, delay); - }; - - const stop = (msg = '', code = 0): void => { - isSpinnerActive = false; - clearInterval(loop); - clearPrevMessage(); - const step = - code === 0 - ? color.green(S_STEP_SUBMIT) - : code === 1 - ? color.red(S_STEP_CANCEL) - : color.red(S_STEP_ERROR); - _message = parseMessage(msg ?? _message); - process.stdout.write(`${step} ${_message}\n`); - clearHooks(); - unblock(); - }; - - const message = (msg = ''): void => { - _message = parseMessage(msg ?? _message); - }; - - return { - start, - stop, - message, - }; -}; - -// Adapted from https://github.com/chalk/ansi-regex -// @see LICENSE -function ansiRegex() { - const pattern = [ - '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', - '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', - ].join('|'); - - return new RegExp(pattern, 'g'); -} - -export type PromptGroupAwaitedReturn = { - [P in keyof T]: Exclude, symbol>; -}; - -export interface PromptGroupOptions { - /** - * Control how the group can be canceled - * if one of the prompts is canceled. - */ - onCancel?: (opts: { results: Prettify>> }) => void; -} - -type Prettify = { - [P in keyof T]: T[P]; -} & {}; - -export type PromptGroup = { - [P in keyof T]: (opts: { - results: Prettify>>>; - }) => undefined | Promise; -}; - -/** - * Define a group of prompts to be displayed - * and return a results of objects within the group - */ -export const group = async ( - prompts: PromptGroup, - opts?: PromptGroupOptions -): Promise>> => { - const results = {} as any; - const promptNames = Object.keys(prompts); - - for (const name of promptNames) { - const prompt = prompts[name as keyof T]; - const result = await prompt({ results })?.catch((e) => { - throw e; - }); - - // Pass the results to the onCancel function - // so the user can decide what to do with the results - // TODO: Switch to callback within core to avoid isCancel Fn - if (typeof opts?.onCancel === 'function' && isCancel(result)) { - results[name] = 'canceled'; - opts.onCancel({ results }); - continue; - } - - results[name] = result; - } - - return results; -}; - -export type Task = { - /** - * Task title - */ - title: string; - /** - * Task function - */ - task: (message: (string: string) => void) => string | Promise | void | Promise; - - /** - * If enabled === false the task will be skipped - */ - enabled?: boolean; -}; - -/** - * Define a group of tasks to be executed - */ -export const tasks = async (tasks: Task[]) => { - for (const task of tasks) { - if (task.enabled === false) continue; - - const s = spinner(); - s.start(task.title); - const result = await task.task(s.message); - s.stop(result || task.title); - } -}; +export { isCancel, mockPrompt, setGlobalAliases } from '@clack/core'; +export { default as confirm, type ConfirmOptions } from './prompts/confirm'; +export { + default as group, + type PromptGroup, + type PromptGroupAwaitedReturn, + type PromptGroupOptions, +} from './prompts/group'; +export { + default as groupMultiselect, + type GroupMultiSelectOptions, +} from './prompts/group-multiselect'; +export { cancel, default as log, intro, outro, type LogMessageOptions } from './prompts/log'; +export { default as multiselect, type MultiSelectOptions } from './prompts/multiselect'; +export { default as note } from './prompts/note'; +export { default as password, type PasswordOptions } from './prompts/password'; +export { default as select } from './prompts/select'; +export { default as selectKey } from './prompts/select-key'; +export { default as spinner } from './prompts/spinner'; +export { default as tasks, type Task } from './prompts/tasks'; +export { default as text, type TextOptions } from './prompts/text'; +export type { Option, SelectOptions } from './utils'; diff --git a/packages/prompts/src/prompts/confirm.ts b/packages/prompts/src/prompts/confirm.ts new file mode 100644 index 00000000..5dc144f3 --- /dev/null +++ b/packages/prompts/src/prompts/confirm.ts @@ -0,0 +1,45 @@ +import { ConfirmPrompt } from '@clack/core'; +import color from 'picocolors'; +import { S_BAR, S_BAR_END, S_RADIO_ACTIVE, S_RADIO_INACTIVE, symbol } from '../utils'; + +export interface ConfirmOptions { + message: string; + active?: string; + inactive?: string; + initialValue?: boolean; +} + +const confirm = (opts: ConfirmOptions) => { + const active = opts.active ?? 'Yes'; + const inactive = opts.inactive ?? 'No'; + return new ConfirmPrompt({ + active, + inactive, + initialValue: opts.initialValue ?? true, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const value = this.value ? active : inactive; + + switch (this.state) { + case 'submit': + return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; + case 'cancel': + return `${title}${color.gray(S_BAR)} ${color.strikethrough( + color.dim(value) + )}\n${color.gray(S_BAR)}`; + default: + return `${title}${color.cyan(S_BAR)} ${ + this.value + ? `${color.green(S_RADIO_ACTIVE)} ${active}` + : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` + } ${color.dim('/')} ${ + !this.value + ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` + : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` + }\n${color.cyan(S_BAR_END)}\n`; + } + }, + }).prompt() as Promise; +}; + +export default confirm; diff --git a/packages/prompts/src/prompts/group-multiselect.ts b/packages/prompts/src/prompts/group-multiselect.ts new file mode 100644 index 00000000..c95ae0a6 --- /dev/null +++ b/packages/prompts/src/prompts/group-multiselect.ts @@ -0,0 +1,164 @@ +import { GroupMultiSelectPrompt } from '@clack/core'; +import color from 'picocolors'; +import { + type Option, + S_BAR, + S_BAR_END, + S_CHECKBOX_ACTIVE, + S_CHECKBOX_INACTIVE, + S_CHECKBOX_SELECTED, + symbol, +} from '../utils'; + +export const opt = ( + option: Option, + state: + | 'inactive' + | 'active' + | 'selected' + | 'active-selected' + | 'group-active' + | 'group-active-selected' + | 'submitted' + | 'cancelled', + options: Option[] = [] +) => { + const label = option.label ?? String(option.value); + const isItem = typeof (option as any).group === 'string'; + const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); + const isLast = isItem && (next as any).group === true; + const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ''; + + if (state === 'group-active') { + return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; + } + if (state === 'group-active-selected') { + return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + } + if (state === 'active') { + return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${ + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + }`; + } + if (state === 'active-selected') { + return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label}${ + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + }`; + } + if (state === 'selected') { + return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + } + if (state === 'cancelled') { + return `${color.strikethrough(color.dim(label))}`; + } + if (state === 'submitted') { + return `${color.dim(label)}`; + } + return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; +}; + +export interface GroupMultiSelectOptions { + message: string; + options: Record[]>; + initialValues?: TValue[]; + required?: boolean; + cursorAt?: TValue; +} + +const groupMultiselect = (opts: GroupMultiSelectOptions) => { + return new GroupMultiSelectPrompt({ + options: opts.options, + initialValues: opts.initialValues, + required: opts.required ?? true, + cursorAt: opts.cursorAt, + validate(selected: TValue[]) { + if (this.required && selected.length === 0) + return `Please select at least one option.\n${color.reset( + color.dim( + `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( + color.bgWhite(color.inverse(' enter ')) + )} to submit` + ) + )}`; + }, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + + switch (this.state) { + case 'submit': { + return `${title}${color.gray(S_BAR)} ${this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, 'submitted')) + .join(color.dim(', '))}`; + } + case 'cancel': { + const label = this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, 'cancelled')) + .join(color.dim(', ')); + return `${title}${color.gray(S_BAR)} ${ + label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' + }`; + } + case 'error': { + const footer = this.error + .split('\n') + .map((ln, i) => + i === 0 + ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` + : `${color.hidden('-')} ${ln}` + ) + .join('\n'); + return `${title}${color.yellow(S_BAR)} ${this.options + .map((option, i, options) => { + const selected = + this.value.includes(option.value) || + (option.group === true && this.isGroupSelected(`${option.value}`)); + const active = i === this.cursor; + const groupActive = + !active && + typeof option.group === 'string' && + this.options[this.cursor].value === option.group; + if (groupActive) { + return opt(option, selected ? 'group-active-selected' : 'group-active', options); + } + if (active && selected) { + return opt(option, 'active-selected', options); + } + if (selected) { + return opt(option, 'selected', options); + } + return opt(option, active ? 'active' : 'inactive', options); + }) + .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; + } + default: { + return `${title}${color.cyan(S_BAR)} ${this.options + .map((option, i, options) => { + const selected = + this.value.includes(option.value) || + (option.group === true && this.isGroupSelected(`${option.value}`)); + const active = i === this.cursor; + const groupActive = + !active && + typeof option.group === 'string' && + this.options[this.cursor].value === option.group; + if (groupActive) { + return opt(option, selected ? 'group-active-selected' : 'group-active', options); + } + if (active && selected) { + return opt(option, 'active-selected', options); + } + if (selected) { + return opt(option, 'selected', options); + } + return opt(option, active ? 'active' : 'inactive', options); + }) + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + } + } + }, + }).prompt() as Promise; +}; + +export default groupMultiselect; diff --git a/packages/prompts/src/prompts/group.ts b/packages/prompts/src/prompts/group.ts new file mode 100644 index 00000000..f2bc0751 --- /dev/null +++ b/packages/prompts/src/prompts/group.ts @@ -0,0 +1,52 @@ +import { isCancel } from '@clack/core'; +import type { Prettify } from '../utils'; + +export type PromptGroupAwaitedReturn = { + [P in keyof T]: Exclude, symbol>; +}; + +export interface PromptGroupOptions { + /** + * Control how the group can be canceled + * if one of the prompts is canceled. + */ + onCancel?: (opts: { results: Prettify>> }) => void; +} + +export type PromptGroup = { + [P in keyof T]: (opts: { + results: Prettify>>>; + }) => undefined | Promise; +}; + +/** + * Define a group of prompts to be displayed + * and return a results of objects within the group + */ +const group = async ( + prompts: PromptGroup, + opts?: PromptGroupOptions +): Promise>> => { + const results = {} as any; + const promptNames = Object.keys(prompts); + + for (const name of promptNames) { + const prompt = prompts[name as keyof T]; + const result = await prompt({ results }); + + // Pass the results to the onCancel function + // so the user can decide what to do with the results + // TODO: Switch to callback within core to avoid isCancel Fn + if (typeof opts?.onCancel === 'function' && isCancel(result)) { + results[name] = 'canceled'; + opts.onCancel({ results }); + continue; + } + + results[name] = result; + } + + return results; +}; + +export default group; diff --git a/packages/prompts/src/prompts/log.ts b/packages/prompts/src/prompts/log.ts new file mode 100644 index 00000000..c6eea2b2 --- /dev/null +++ b/packages/prompts/src/prompts/log.ts @@ -0,0 +1,62 @@ +import color from 'picocolors'; +import { + S_BAR, + S_BAR_END, + S_BAR_START, + S_ERROR, + S_INFO, + S_STEP_SUBMIT, + S_SUCCESS, + S_WARN, +} from '../utils'; + +export const cancel = (message = '') => { + process.stdout.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); +}; + +export const intro = (title = '') => { + process.stdout.write(`${color.gray(S_BAR_START)} ${title}\n`); +}; + +export const outro = (message = '') => { + process.stdout.write(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n`); +}; + +export type LogMessageOptions = { + symbol?: string; +}; + +const log = { + message: (message = '', { symbol = color.gray(S_BAR) }: LogMessageOptions = {}) => { + const parts = [`${color.gray(S_BAR)}`]; + if (message) { + const [firstLine, ...lines] = message.split('\n'); + parts.push(`${symbol} ${firstLine}`, ...lines.map((ln) => `${color.gray(S_BAR)} ${ln}`)); + } + process.stdout.write(`${parts.join('\n')}\n`); + }, + info: (message: string) => { + log.message(message, { symbol: color.blue(S_INFO) }); + }, + success: (message: string) => { + log.message(message, { symbol: color.green(S_SUCCESS) }); + }, + step: (message: string) => { + log.message(message, { symbol: color.green(S_STEP_SUBMIT) }); + }, + warn: (message: string) => { + log.message(message, { symbol: color.yellow(S_WARN) }); + }, + /** alias for `log.warn()`. */ + warning: (message: string) => { + log.warn(message); + }, + error: (message: string) => { + log.message(message, { symbol: color.red(S_ERROR) }); + }, +}; + +//@ts-expect-error - Name prompt to validate package exports +log.name = 'log'; + +export default log; diff --git a/packages/prompts/src/prompts/multiselect.ts b/packages/prompts/src/prompts/multiselect.ts new file mode 100644 index 00000000..5571b2cc --- /dev/null +++ b/packages/prompts/src/prompts/multiselect.ts @@ -0,0 +1,125 @@ +import { MultiSelectPrompt } from '@clack/core'; +import color from 'picocolors'; +import { + type Option, + S_BAR, + S_BAR_END, + S_CHECKBOX_ACTIVE, + S_CHECKBOX_INACTIVE, + S_CHECKBOX_SELECTED, + limitOptions, + symbol, +} from '../utils'; + +export const opt = ( + option: Option, + state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled' +) => { + const label = option.label ?? String(option.value); + if (state === 'active') { + return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${ + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + }`; + } + if (state === 'selected') { + return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + } + if (state === 'cancelled') { + return `${color.strikethrough(color.dim(label))}`; + } + if (state === 'active-selected') { + return `${color.green(S_CHECKBOX_SELECTED)} ${label}${ + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + }`; + } + if (state === 'submitted') { + return `${color.dim(label)}`; + } + return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; +}; + +export interface MultiSelectOptions { + message: string; + options: Option[]; + initialValues?: TValue[]; + maxItems?: number; + required?: boolean; + cursorAt?: TValue; +} + +const multiselect = (opts: MultiSelectOptions) => { + return new MultiSelectPrompt({ + options: opts.options, + initialValues: opts.initialValues, + required: opts.required ?? true, + cursorAt: opts.cursorAt, + validate(selected: TValue[]) { + if (this.required && selected.length === 0) + return `Please select at least one option.\n${color.reset( + color.dim( + `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( + color.bgWhite(color.inverse(' enter ')) + )} to submit` + ) + )}`; + }, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + + const styleOption = (option: Option, active: boolean) => { + const selected = this.value.includes(option.value); + if (active && selected) { + return opt(option, 'active-selected'); + } + if (selected) { + return opt(option, 'selected'); + } + return opt(option, active ? 'active' : 'inactive'); + }; + + switch (this.state) { + case 'submit': { + return `${title}${color.gray(S_BAR)} ${this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, 'submitted')) + .join(color.dim(', '))}`; + } + case 'cancel': { + const label = this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, 'cancelled')) + .join(color.dim(', ')); + return `${title}${color.gray(S_BAR)} ${ + label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' + }`; + } + case 'error': { + const footer = this.error + .split('\n') + .map((ln, i) => + i === 0 + ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` + : `${color.hidden('-')} ${ln}` + ) + .join('\n'); + return `${title + color.yellow(S_BAR)} ${limitOptions({ + options: this.options, + cursor: this.cursor, + maxItems: opts.maxItems, + style: styleOption, + }).join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; + } + default: { + return `${title}${color.cyan(S_BAR)} ${limitOptions({ + options: this.options, + cursor: this.cursor, + maxItems: opts.maxItems, + style: styleOption, + }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + } + } + }, + }).prompt() as Promise; +}; + +export default multiselect; diff --git a/packages/prompts/src/prompts/note.ts b/packages/prompts/src/prompts/note.ts new file mode 100644 index 00000000..2876c33e --- /dev/null +++ b/packages/prompts/src/prompts/note.ts @@ -0,0 +1,38 @@ +import color from 'picocolors'; +import { + S_BAR, + S_BAR_H, + S_CONNECT_LEFT, + S_CORNER_BOTTOM_RIGHT, + S_CORNER_TOP_RIGHT, + S_STEP_SUBMIT, + strip, +} from '../utils'; + +const note = (message = '', title = '') => { + const lines = `\n${message}\n`.split('\n'); + const titleLen = strip(title).length; + const len = + Math.max( + lines.reduce((sum, ln) => { + const stripedLine = strip(ln); + return stripedLine.length > sum ? stripedLine.length : sum; + }, 0), + titleLen + ) + 2; + const msg = lines + .map( + (ln) => + `${color.gray(S_BAR)} ${color.dim(ln)}${' '.repeat( + len - strip(ln).length + )}${color.gray(S_BAR)}` + ) + .join('\n'); + process.stdout.write( + `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT + )}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` + ); +}; + +export default note; diff --git a/packages/prompts/src/prompts/password.ts b/packages/prompts/src/prompts/password.ts new file mode 100644 index 00000000..1cc49a0d --- /dev/null +++ b/packages/prompts/src/prompts/password.ts @@ -0,0 +1,37 @@ +import { PasswordPrompt } from '@clack/core'; +import color from 'picocolors'; +import { S_BAR, S_BAR_END, S_PASSWORD_MASK, symbol } from '../utils'; + +export interface PasswordOptions { + message: string; + mask?: string; + validate?: (value: string) => string | undefined; +} + +const password = (opts: PasswordOptions) => { + return new PasswordPrompt({ + validate: opts.validate, + mask: opts.mask ?? S_PASSWORD_MASK, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const masked = this.valueWithCursor; + + switch (this.state) { + case 'error': + return `${title.trim()}\n${color.yellow(S_BAR)} ${masked}\n${color.yellow( + S_BAR_END + )} ${color.yellow(this.error)}\n`; + case 'submit': + return `${title}${color.gray(S_BAR)} ${color.dim(masked)}`; + case 'cancel': + return `${title}${color.gray(S_BAR)} ${ + masked ? color.strikethrough(color.dim(masked)) : '' + }${masked ? `\n${color.gray(S_BAR)}` : ''}`; + default: + return `${title}${color.cyan(S_BAR)} ${masked}\n${color.cyan(S_BAR_END)}\n`; + } + }, + }).prompt() as Promise; +}; + +export default password; diff --git a/packages/prompts/src/prompts/select-key.ts b/packages/prompts/src/prompts/select-key.ts new file mode 100644 index 00000000..556eccb8 --- /dev/null +++ b/packages/prompts/src/prompts/select-key.ts @@ -0,0 +1,55 @@ +import { SelectKeyPrompt } from '@clack/core'; +import color from 'picocolors'; +import { type Option, S_BAR, S_BAR_END, type SelectOptions, symbol } from '../utils'; + +export const opt = ( + option: Option, + state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive' +) => { + const label = option.label ?? String(option.value); + if (state === 'selected') { + return `${color.dim(label)}`; + } + if (state === 'cancelled') { + return `${color.strikethrough(color.dim(label))}`; + } + if (state === 'active') { + return `${color.bgCyan(color.gray(` ${option.value} `))} ${label}${ + option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' + }`; + } + return `${color.gray( + color.bgWhite(color.inverse(` ${option.value} `)) + )} ${label}${option.hint ? ` ${color.dim(`(${option.hint})`)}` : ''}`; +}; + +const selectKey = (opts: SelectOptions) => { + return new SelectKeyPrompt({ + options: opts.options, + initialValue: opts.initialValue, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + + switch (this.state) { + case 'submit': + return `${title}${color.gray(S_BAR)} ${opt( + // biome-ignore lint/style/noNonNullAssertion: + this.options.find((opt) => opt.value === this.value)!, + 'selected' + )}`; + case 'cancel': + return `${title}${color.gray(S_BAR)} ${opt( + this.options[0], + 'cancelled' + )}\n${color.gray(S_BAR)}`; + default: { + return `${title}${color.cyan(S_BAR)} ${this.options + .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + } + } + }, + }).prompt() as Promise; +}; + +export default selectKey; diff --git a/packages/prompts/src/prompts/select.ts b/packages/prompts/src/prompts/select.ts new file mode 100644 index 00000000..0be9508b --- /dev/null +++ b/packages/prompts/src/prompts/select.ts @@ -0,0 +1,60 @@ +import { SelectPrompt } from '@clack/core'; +import color from 'picocolors'; +import { + type Option, + S_BAR, + S_BAR_END, + S_RADIO_ACTIVE, + S_RADIO_INACTIVE, + type SelectOptions, + limitOptions, + symbol, +} from '../utils'; + +export const opt = ( + option: Option, + state: 'inactive' | 'active' | 'selected' | 'cancelled' +) => { + const label = option.label ?? String(option.value); + switch (state) { + case 'selected': + return `${color.dim(label)}`; + case 'active': + return `${color.green(S_RADIO_ACTIVE)} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : '' + }`; + case 'cancelled': + return `${color.strikethrough(color.dim(label))}`; + default: + return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; + } +}; + +const select = (opts: SelectOptions) => { + return new SelectPrompt({ + options: opts.options, + initialValue: opts.initialValue, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + + switch (this.state) { + case 'submit': + return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; + case 'cancel': + return `${title}${color.gray(S_BAR)} ${opt( + this.options[this.cursor], + 'cancelled' + )}\n${color.gray(S_BAR)}`; + default: + return `${title}${color.cyan(S_BAR)} ${limitOptions({ + cursor: this.cursor, + options: this.options, + maxItems: opts.maxItems, + style: (item, active) => opt(item, active ? 'active' : 'inactive'), + }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + } + }, + }).prompt() as Promise; +}; + +export default select; diff --git a/packages/prompts/src/prompts/spinner.ts b/packages/prompts/src/prompts/spinner.ts new file mode 100644 index 00000000..712c5a00 --- /dev/null +++ b/packages/prompts/src/prompts/spinner.ts @@ -0,0 +1,91 @@ +import { block } from '@clack/core'; +import color from 'picocolors'; +import { cursor, erase } from 'sisteransi'; +import { S_BAR, S_STEP_CANCEL, S_STEP_ERROR, S_STEP_SUBMIT, isUnicodeSupported } from '../utils'; + +export const frames = isUnicodeSupported ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; +export const frameInterval = isUnicodeSupported ? 80 : 120; +export const dotsInterval = 0.125; + +const spinner = () => { + let unblock: () => void; + let loop: NodeJS.Timeout; + let isSpinnerActive = false; + let _message = ''; + + const handleExit = (code: number) => { + const msg = code > 1 ? 'Something went wrong' : 'Canceled'; + if (isSpinnerActive) stop(msg, code); + }; + + const errorEventHandler = () => handleExit(2); + const signalEventHandler = () => handleExit(1); + + const registerHooks = () => { + // Reference: https://nodejs.org/api/process.html#event-uncaughtexception + process.on('uncaughtExceptionMonitor', errorEventHandler); + // Reference: https://nodejs.org/api/process.html#event-unhandledrejection + process.on('unhandledRejection', errorEventHandler); + // Reference Signal Events: https://nodejs.org/api/process.html#signal-events + process.on('SIGINT', signalEventHandler); + process.on('SIGTERM', signalEventHandler); + process.on('exit', handleExit); + }; + + const clearHooks = () => { + process.removeListener('uncaughtExceptionMonitor', errorEventHandler); + process.removeListener('unhandledRejection', errorEventHandler); + process.removeListener('SIGINT', signalEventHandler); + process.removeListener('SIGTERM', signalEventHandler); + process.removeListener('exit', handleExit); + }; + + const start = (msg = ''): void => { + isSpinnerActive = true; + unblock = block(); + _message = msg.replace(/\.+$/, ''); + process.stdout.write(`${color.gray(S_BAR)}\n`); + let frameIndex = 0; + let dotsTimer = 0; + registerHooks(); + loop = setInterval(() => { + const frame = color.magenta(frames[frameIndex]); + const loadingDots = '.'.repeat(Math.floor(dotsTimer)).slice(0, 3); + process.stdout.write(cursor.move(-999, 0)); + process.stdout.write(erase.down(1)); + process.stdout.write(`${frame} ${_message}${loadingDots}`); + frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; + dotsTimer = dotsTimer < frames.length ? dotsTimer + dotsInterval : 0; + }, frameInterval); + }; + + const stop = (msg = '', code = 0): void => { + if (!isSpinnerActive) return; + isSpinnerActive = false; + _message = msg ?? _message; + clearInterval(loop); + const step = + code === 0 + ? color.green(S_STEP_SUBMIT) + : code === 1 + ? color.red(S_STEP_CANCEL) + : color.red(S_STEP_ERROR); + process.stdout.write(cursor.move(-999, 0)); + process.stdout.write(erase.down(1)); + process.stdout.write(`${step} ${_message}\n`); + clearHooks(); + unblock(); + }; + + const message = (msg = ''): void => { + _message = msg ?? _message; + }; + + return { + start, + stop, + message, + }; +}; + +export default spinner; diff --git a/packages/prompts/src/prompts/tasks.ts b/packages/prompts/src/prompts/tasks.ts new file mode 100644 index 00000000..03b73076 --- /dev/null +++ b/packages/prompts/src/prompts/tasks.ts @@ -0,0 +1,33 @@ +import spinner from './spinner'; + +export type Task = { + /** + * Task title + */ + title: string; + /** + * Task function + */ + task: (message: (string: string) => void) => string | Promise | void | Promise; + + /** + * If enabled === false the task will be skipped + */ + enabled?: boolean; +}; + +/** + * Define a group of tasks to be executed + */ +const tasks = async (tasks: Task[]) => { + for (const task of tasks) { + if (task.enabled === false) continue; + + const s = spinner(); + s.start(task.title); + const result = await task.task(s.message); + s.stop(result || task.title); + } +}; + +export default tasks; diff --git a/packages/prompts/src/prompts/text.ts b/packages/prompts/src/prompts/text.ts new file mode 100644 index 00000000..d7e25b09 --- /dev/null +++ b/packages/prompts/src/prompts/text.ts @@ -0,0 +1,42 @@ +import { TextPrompt } from '@clack/core'; +import color from 'picocolors'; +import { S_BAR, S_BAR_END, formatPlaceholder, symbol } from '../utils'; + +export interface TextOptions { + message: string; + placeholder?: string; + defaultValue?: string; + initialValue?: string; + validate?: (value: string) => string | undefined; +} + +const text = (opts: TextOptions) => { + return new TextPrompt({ + validate: opts.validate, + placeholder: opts.placeholder, + defaultValue: opts.defaultValue, + initialValue: opts.initialValue, + render() { + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const placeholder = formatPlaceholder(opts.placeholder); + const value = !this.value ? placeholder : this.valueWithCursor; + + switch (this.state) { + case 'error': + return `${title.trim()}\n${color.yellow(S_BAR)} ${value}\n${color.yellow( + S_BAR_END + )} ${color.yellow(this.error)}\n`; + case 'submit': + return `${title}${color.gray(S_BAR)} ${color.dim(this.value)}`; + case 'cancel': + return `${title}${color.gray(S_BAR)} ${ + this.value ? color.strikethrough(color.dim(this.value)) : '' + }${this.value ? `\n${color.gray(S_BAR)}` : ''}`; + default: + return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`; + } + }, + }).prompt() as Promise; +}; + +export default text; diff --git a/packages/prompts/src/utils/format.ts b/packages/prompts/src/utils/format.ts new file mode 100644 index 00000000..901f0151 --- /dev/null +++ b/packages/prompts/src/utils/format.ts @@ -0,0 +1,60 @@ +import color from 'picocolors'; + +export interface LimitOptionsParams { + options: TOption[]; + maxItems: number | undefined; + cursor: number; + style: (option: TOption, active: boolean) => string; +} + +export const limitOptions = (params: LimitOptionsParams): string[] => { + const { cursor, options, style } = params; + + const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; + // During test `process.stdout.rows` is `undefined`, and it brakes `limitOptions` function. + const outputMaxItems = Math.max((process.stdout.rows ?? Number.POSITIVE_INFINITY) - 4, 0); + // We clamp to minimum 5 because anything less doesn't make sense UX wise + const maxItems = Math.min(outputMaxItems, Math.max(paramMaxItems, 5)); + let slidingWindowLocation = 0; + + if (cursor >= slidingWindowLocation + maxItems - 3) { + slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); + } else if (cursor < slidingWindowLocation + 2) { + slidingWindowLocation = Math.max(cursor - 2, 0); + } + + const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; + const shouldRenderBottomEllipsis = + maxItems < options.length && slidingWindowLocation + maxItems < options.length; + + return options + .slice(slidingWindowLocation, slidingWindowLocation + maxItems) + .map((option, i, arr) => { + const isTopLimit = i === 0 && shouldRenderTopEllipsis; + const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; + return isTopLimit || isBottomLimit + ? color.dim('...') + : style(option, i + slidingWindowLocation === cursor); + }); +}; + +export function formatPlaceholder(placeholder: string | undefined): string { + return placeholder + ? color.inverse(placeholder[0]) + color.dim(placeholder.slice(1)) + : color.inverse(color.hidden('_')); +} + +export function strip(str: string): string { + return str.replace(ansiRegex(), ''); +} + +// Adapted from https://github.com/chalk/ansi-regex +// @see LICENSE +function ansiRegex(): RegExp { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', + ].join('|'); + + return new RegExp(pattern, 'g'); +} diff --git a/packages/prompts/src/utils/index.ts b/packages/prompts/src/utils/index.ts new file mode 100644 index 00000000..8a6923c9 --- /dev/null +++ b/packages/prompts/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from './format'; +export * from './symbol'; +export * from './types'; diff --git a/packages/prompts/src/utils/symbol.ts b/packages/prompts/src/utils/symbol.ts new file mode 100644 index 00000000..5aa9f091 --- /dev/null +++ b/packages/prompts/src/utils/symbol.ts @@ -0,0 +1,46 @@ +import type { State } from '@clack/core'; +import isUnicodeSupportedFn from 'is-unicode-supported'; +import color from 'picocolors'; + +export const isUnicodeSupported = isUnicodeSupportedFn(); +const s = (c: string, fallback: string) => (isUnicodeSupported ? c : fallback); + +export const S_STEP_ACTIVE = s('◆', '*'); +export const S_STEP_CANCEL = s('■', 'x'); +export const S_STEP_ERROR = s('▲', 'x'); +export const S_STEP_SUBMIT = s('◇', 'o'); + +export const S_BAR_START = s('┌', 'T'); +export const S_BAR = s('│', '|'); +export const S_BAR_END = s('└', '—'); + +export const S_RADIO_ACTIVE = s('●', '>'); +export const S_RADIO_INACTIVE = s('○', ' '); +export const S_CHECKBOX_ACTIVE = s('◻', '[•]'); +export const S_CHECKBOX_SELECTED = s('◼', '[+]'); +export const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); +export const S_PASSWORD_MASK = s('▪', '•'); + +export const S_BAR_H = s('─', '-'); +export const S_CORNER_TOP_RIGHT = s('╮', '+'); +export const S_CONNECT_LEFT = s('├', '+'); +export const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); + +export const S_INFO = s('●', '•'); +export const S_SUCCESS = s('◆', '*'); +export const S_WARN = s('▲', '!'); +export const S_ERROR = s('■', 'x'); + +export const symbol = (state: State) => { + switch (state) { + case 'initial': + case 'active': + return color.cyan(S_STEP_ACTIVE); + case 'cancel': + return color.red(S_STEP_CANCEL); + case 'error': + return color.yellow(S_STEP_ERROR); + case 'submit': + return color.green(S_STEP_SUBMIT); + } +}; diff --git a/packages/prompts/src/utils/types.ts b/packages/prompts/src/utils/types.ts new file mode 100644 index 00000000..2119a68a --- /dev/null +++ b/packages/prompts/src/utils/types.ts @@ -0,0 +1,16 @@ +export type Primitive = Readonly; + +export type Option = Value extends Primitive + ? { value: Value; label?: string; hint?: string } + : { value: Value; label: string; hint?: string }; + +export interface SelectOptions { + message: string; + options: Option[]; + initialValue?: TValue; + maxItems?: number; +} + +export type Prettify = { + [P in keyof T]: T[P]; +} & {}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da2a1e3c..74820694 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,21 @@ importers: '@changesets/cli': specifier: ^2.26.2 version: 2.26.2 + '@swc/core': + specifier: ^1.3.83 + version: 1.10.1 + '@swc/jest': + specifier: ^0.2.29 + version: 0.2.37(@swc/core@1.10.1) + '@types/jest': + specifier: ^29.5.4 + version: 29.5.14 '@types/node': specifier: ^18.16.0 version: 18.16.0 + jest: + specifier: ^29.6.4 + version: 29.7.0(@types/node@18.16.0) typescript: specifier: ^5.2.2 version: 5.2.2 @@ -65,9 +77,6 @@ importers: specifier: ^1.0.5 version: 1.0.5 devDependencies: - vitest: - specifier: ^1.6.0 - version: 1.6.0(@types/node@18.16.0) wrap-ansi: specifier: ^8.1.0 version: 8.1.0 @@ -124,6 +133,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} @@ -145,6 +158,97 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.26.0': resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} @@ -165,6 +269,9 @@ packages: resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@biomejs/biome@1.9.4': resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} engines: {node: '>=14.21.3'} @@ -276,12 +383,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.24.0': resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} engines: {node: '>=18'} @@ -294,12 +395,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.24.0': resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} engines: {node: '>=18'} @@ -312,12 +407,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.24.0': resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} engines: {node: '>=18'} @@ -330,12 +419,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.24.0': resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} engines: {node: '>=18'} @@ -348,12 +431,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.24.0': resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} engines: {node: '>=18'} @@ -366,12 +443,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.24.0': resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} engines: {node: '>=18'} @@ -384,12 +455,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.24.0': resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} engines: {node: '>=18'} @@ -402,12 +467,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.24.0': resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} engines: {node: '>=18'} @@ -420,12 +479,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.24.0': resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} engines: {node: '>=18'} @@ -438,12 +491,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.24.0': resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} engines: {node: '>=18'} @@ -456,12 +503,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.24.0': resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} engines: {node: '>=18'} @@ -474,12 +515,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.24.0': resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} engines: {node: '>=18'} @@ -492,12 +527,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.24.0': resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} engines: {node: '>=18'} @@ -510,12 +539,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.24.0': resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} engines: {node: '>=18'} @@ -528,12 +551,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.24.0': resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} engines: {node: '>=18'} @@ -546,12 +563,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.24.0': resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} engines: {node: '>=18'} @@ -564,12 +575,6 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.24.0': resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} engines: {node: '>=18'} @@ -582,12 +587,6 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.24.0': resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} engines: {node: '>=18'} @@ -606,12 +605,6 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.24.0': resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} engines: {node: '>=18'} @@ -624,12 +617,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.24.0': resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} engines: {node: '>=18'} @@ -642,12 +629,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.24.0': resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} engines: {node: '>=18'} @@ -660,12 +641,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.24.0': resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} engines: {node: '>=18'} @@ -678,22 +653,90 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.24.0': resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} engines: {node: '>=18'} cpu: [x64] os: [win32] + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/create-cache-key-function@29.7.0': + resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -784,109 +827,133 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.27.3': - resolution: {integrity: sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==} - cpu: [arm] - os: [android] + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@rollup/rollup-android-arm64@4.27.3': - resolution: {integrity: sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==} - cpu: [arm64] - os: [android] + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@rollup/rollup-darwin-arm64@4.27.3': - resolution: {integrity: sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==} + '@swc/core-darwin-arm64@1.10.1': + resolution: {integrity: sha512-NyELPp8EsVZtxH/mEqvzSyWpfPJ1lugpTQcSlMduZLj1EASLO4sC8wt8hmL1aizRlsbjCX+r0PyL+l0xQ64/6Q==} + engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.27.3': - resolution: {integrity: sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==} + '@swc/core-darwin-x64@1.10.1': + resolution: {integrity: sha512-L4BNt1fdQ5ZZhAk5qoDfUnXRabDOXKnXBxMDJ+PWLSxOGBbWE6aJTnu4zbGjJvtot0KM46m2LPAPY8ttknqaZA==} + engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.27.3': - resolution: {integrity: sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.27.3': - resolution: {integrity: sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.27.3': - resolution: {integrity: sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.27.3': - resolution: {integrity: sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==} + '@swc/core-linux-arm-gnueabihf@1.10.1': + resolution: {integrity: sha512-Y1u9OqCHgvVp2tYQAJ7hcU9qO5brDMIrA5R31rwWQIAKDkJKtv3IlTHF0hrbWk1wPR0ZdngkQSJZple7G+Grvw==} + engines: {node: '>=10'} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.27.3': - resolution: {integrity: sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==} + '@swc/core-linux-arm64-gnu@1.10.1': + resolution: {integrity: sha512-tNQHO/UKdtnqjc7o04iRXng1wTUXPgVd8Y6LI4qIbHVoVPwksZydISjMcilKNLKIwOoUQAkxyJ16SlOAeADzhQ==} + engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.27.3': - resolution: {integrity: sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==} + '@swc/core-linux-arm64-musl@1.10.1': + resolution: {integrity: sha512-x0L2Pd9weQ6n8dI1z1Isq00VHFvpBClwQJvrt3NHzmR+1wCT/gcYl1tp9P5xHh3ldM8Cn4UjWCw+7PaUgg8FcQ==} + engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': - resolution: {integrity: sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.27.3': - resolution: {integrity: sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.27.3': - resolution: {integrity: sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.27.3': - resolution: {integrity: sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==} + '@swc/core-linux-x64-gnu@1.10.1': + resolution: {integrity: sha512-yyYEwQcObV3AUsC79rSzN9z6kiWxKAVJ6Ntwq2N9YoZqSPYph+4/Am5fM1xEQYf/kb99csj0FgOelomJSobxQA==} + engines: {node: '>=10'} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.27.3': - resolution: {integrity: sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==} + '@swc/core-linux-x64-musl@1.10.1': + resolution: {integrity: sha512-tcaS43Ydd7Fk7sW5ROpaf2Kq1zR+sI5K0RM+0qYLYYurvsJruj3GhBCaiN3gkzd8m/8wkqNqtVklWaQYSDsyqA==} + engines: {node: '>=10'} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.27.3': - resolution: {integrity: sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==} + '@swc/core-win32-arm64-msvc@1.10.1': + resolution: {integrity: sha512-D3Qo1voA7AkbOzQ2UGuKNHfYGKL6eejN8VWOoQYtGHHQi1p5KK/Q7V1ku55oxXBsj79Ny5FRMqiRJpVGad7bjQ==} + engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.27.3': - resolution: {integrity: sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==} + '@swc/core-win32-ia32-msvc@1.10.1': + resolution: {integrity: sha512-WalYdFoU3454Og+sDKHM1MrjvxUGwA2oralknXkXL8S0I/8RkWZOB++p3pLaGbTvOO++T+6znFbQdR8KRaa7DA==} + engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.27.3': - resolution: {integrity: sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==} + '@swc/core-win32-x64-msvc@1.10.1': + resolution: {integrity: sha512-JWobfQDbTnoqaIwPKQ3DVSywihVXlQMbDuwik/dDWlj33A8oEHcjPOGs4OqcA3RHv24i+lfCQpM3Mn4FAMfacA==} + engines: {node: '>=10'} cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.8': - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@swc/core@1.10.1': + resolution: {integrity: sha512-rQ4dS6GAdmtzKiCRt3LFVxl37FaY1cgL9kSUTnhQ2xc3fmHOd7jdJK/V4pSZMG1ruGTd0bsi34O2R0Olg9Zo/w==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '*' + peerDependenciesMeta: + '@swc/helpers': + optional: true + + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/jest@0.2.37': + resolution: {integrity: sha512-CR2BHhmXKGxTiFr21DYPRHQunLkX3mNIFGFkxBGji6r9uyIR5zftTOVYj1e0sFNMV2H7mf/+vpaglqaryBtqfQ==} + engines: {npm: '>= 7.0.0'} + peerDependencies: + '@swc/core': '*' + + '@swc/types@0.1.17': + resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/is-ci@3.0.4': resolution: {integrity: sha512-AkCYCmwlXeuH89DagDCzvCAyltI2v9lh3U3DqSg/GrBYoReAaWwxfXCqMx9UV5MajLZ4ZFwZzV4cABGIxk2XRw==} + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/minimist@1.2.5': resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==} @@ -905,24 +972,14 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@vitest/expect@1.6.0': - resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==} - - '@vitest/runner@1.6.0': - resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==} + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@vitest/snapshot@1.6.0': - resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==} + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - '@vitest/spy@1.6.0': - resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} - - '@vitest/utils@1.6.0': - resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} - - acorn-walk@8.3.4: - resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} - engines: {node: '>=0.4.0'} + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} acorn@8.14.0: resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} @@ -933,6 +990,10 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -957,6 +1018,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -980,9 +1045,6 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - autoprefixer@10.4.20: resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} engines: {node: ^10 || ^12 || >=14} @@ -994,6 +1056,31 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1004,6 +1091,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} @@ -1019,14 +1109,20 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} engines: {node: '>=8'} @@ -1035,16 +1131,16 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} caniuse-lite@1.0.30001683: resolution: {integrity: sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==} - chai@4.5.0: - resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} - engines: {node: '>=4'} - chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1057,12 +1153,13 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -1070,6 +1167,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -1081,6 +1181,13 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -1104,6 +1211,9 @@ packages: commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -1114,6 +1224,11 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -1211,9 +1326,13 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} - deep-eql@4.1.4: - resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} - engines: {node: '>=6'} + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} @@ -1237,6 +1356,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1264,6 +1387,10 @@ packages: electron-to-chromium@1.5.64: resolution: {integrity: sha512-IXEuxU+5ClW2IGEYFC2T7szbyVgehupCWQe5GNh+H065CD6U6IFN0s4KeAMFGNmQolRU4IV7zGBWSYMmZ8uuqQ==} + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1313,11 +1440,6 @@ packages: engines: {node: '>=12'} hasBin: true - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.24.0: resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} engines: {node: '>=18'} @@ -1331,6 +1453,10 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -1339,12 +1465,17 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -1357,9 +1488,15 @@ packages: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.4.2: resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} peerDependencies: @@ -1423,16 +1560,17 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} get-symbol-description@1.0.2: resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} @@ -1442,6 +1580,10 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} @@ -1512,12 +1654,15 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + human-id@1.0.2: resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} @@ -1527,6 +1672,15 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} @@ -1591,6 +1745,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} engines: {node: '>= 0.4'} @@ -1637,9 +1795,9 @@ packages: resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} engines: {node: '>= 0.4'} - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} @@ -1682,6 +1840,159 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + jiti@1.17.0: resolution: {integrity: sha512-CByzPgFqYoB9odEeef7GNmQ3S5THIBOtzRYoSCya2Sv27AuQxy2jgoFjQ6VTF53xsq1MXRm+YWNvOoDHUAteOw==} hasBin: true @@ -1697,9 +2008,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-tokens@9.0.0: - resolution: {integrity: sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==} - js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -1717,6 +2025,9 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} @@ -1724,10 +2035,18 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + lilconfig@3.1.2: resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} engines: {node: '>=14'} @@ -1739,10 +2058,6 @@ packages: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} - local-pkg@0.5.1: - resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} - engines: {node: '>=14'} - locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -1760,9 +2075,6 @@ packages: lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -1772,6 +2084,13 @@ packages: magic-string@0.30.13: resolution: {integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==} + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -1801,14 +2120,17 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@5.1.6: resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} engines: {node: '>=10'} @@ -1851,19 +2173,29 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -1883,9 +2215,9 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} os-tmpdir@1.0.2: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} @@ -1906,10 +2238,6 @@ packages: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -1934,14 +2262,14 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1952,9 +2280,6 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -1973,6 +2298,10 @@ packages: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -2180,9 +2509,16 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2227,10 +2563,18 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve.exports@2.0.3: + resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} + engines: {node: '>=10'} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -2251,11 +2595,6 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true - rollup@4.27.3: - resolution: {integrity: sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2317,16 +2656,9 @@ packages: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -2347,6 +2679,13 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + spawndamnit@2.0.0: resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} @@ -2365,15 +2704,17 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.8.0: - resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} stream-transform@2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2405,16 +2746,21 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - strip-literal@2.1.0: - resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} stylehacks@7.0.4: resolution: {integrity: sha512-i4zfNrGMt9SB4xRK9L83rlsFCgdGANfeDAYacO1pkqcE7cRHPdWHwnKZVz7WY17Veq/FvyYsRAU++Ga+qDFIww==} @@ -2430,6 +2776,10 @@ 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'} + supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -2443,25 +2793,21 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} tinyglobby@0.2.10: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} - tinypool@0.8.4: - resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} - engines: {node: '>=14.0.0'} - - tinyspy@2.2.1: - resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} - engines: {node: '>=14.0.0'} - tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2475,14 +2821,18 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} type-fest@0.13.1: resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} engines: {node: '>=10'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} @@ -2544,69 +2894,15 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - vite-node@1.6.0: - resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - - vite@5.4.11: - resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - - vitest@1.6.0: - resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.6.0 - '@vitest/ui': 1.6.0 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -2642,11 +2938,6 @@ packages: engines: {node: '>= 8'} hasBin: true - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -2662,6 +2953,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -2695,10 +2990,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - yocto-queue@1.1.1: - resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} - engines: {node: '>=12.20'} - snapshots: '@ampproject/remapping@2.3.0': @@ -2766,6 +3057,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-plugin-utils@7.25.9': {} + '@babel/helper-string-parser@7.25.9': {} '@babel/helper-validator-identifier@7.25.9': {} @@ -2781,6 +3074,91 @@ snapshots: dependencies: '@babel/types': 7.26.0 + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/runtime@7.26.0': dependencies: regenerator-runtime: 0.14.1 @@ -2810,6 +3188,8 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@bcoe/v8-coverage@0.2.3': {} + '@biomejs/biome@1.9.4': optionalDependencies: '@biomejs/cli-darwin-arm64': 1.9.4 @@ -2997,162 +3377,108 @@ snapshots: '@esbuild/aix-ppc64@0.19.12': optional: true - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.24.0': optional: true '@esbuild/android-arm64@0.19.12': optional: true - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.24.0': optional: true '@esbuild/android-arm@0.19.12': optional: true - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.24.0': optional: true '@esbuild/android-x64@0.19.12': optional: true - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.24.0': optional: true '@esbuild/darwin-arm64@0.19.12': optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.24.0': optional: true '@esbuild/darwin-x64@0.19.12': optional: true - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.24.0': optional: true '@esbuild/freebsd-arm64@0.19.12': optional: true - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.24.0': optional: true '@esbuild/freebsd-x64@0.19.12': optional: true - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.24.0': optional: true '@esbuild/linux-arm64@0.19.12': optional: true - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.24.0': optional: true '@esbuild/linux-arm@0.19.12': optional: true - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.24.0': optional: true '@esbuild/linux-ia32@0.19.12': optional: true - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.24.0': optional: true '@esbuild/linux-loong64@0.19.12': optional: true - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.24.0': optional: true '@esbuild/linux-mips64el@0.19.12': optional: true - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.24.0': optional: true '@esbuild/linux-ppc64@0.19.12': optional: true - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.24.0': optional: true '@esbuild/linux-riscv64@0.19.12': optional: true - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.24.0': optional: true '@esbuild/linux-s390x@0.19.12': optional: true - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.24.0': optional: true '@esbuild/linux-x64@0.19.12': optional: true - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.24.0': optional: true '@esbuild/netbsd-x64@0.19.12': optional: true - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.24.0': optional: true @@ -3162,52 +3488,209 @@ snapshots: '@esbuild/openbsd-x64@0.19.12': optional: true - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.24.0': optional: true '@esbuild/sunos-x64@0.19.12': optional: true - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.24.0': optional: true '@esbuild/win32-arm64@0.19.12': optional: true - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.24.0': optional: true '@esbuild/win32-ia32@0.19.12': optional: true - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.24.0': optional: true '@esbuild/win32-x64@0.19.12': optional: true - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.24.0': optional: true + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@18.16.0) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/create-cache-key-function@29.7.0': + dependencies: + '@jest/types': 29.6.3 + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 18.16.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 18.16.0 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.26.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 18.16.0 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -3299,70 +3782,123 @@ snapshots: optionalDependencies: rollup: 3.29.5 - '@rollup/rollup-android-arm-eabi@4.27.3': - optional: true + '@sinclair/typebox@0.27.8': {} - '@rollup/rollup-android-arm64@4.27.3': - optional: true + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 - '@rollup/rollup-darwin-arm64@4.27.3': - optional: true + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 - '@rollup/rollup-darwin-x64@4.27.3': + '@swc/core-darwin-arm64@1.10.1': optional: true - '@rollup/rollup-freebsd-arm64@4.27.3': + '@swc/core-darwin-x64@1.10.1': optional: true - '@rollup/rollup-freebsd-x64@4.27.3': + '@swc/core-linux-arm-gnueabihf@1.10.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + '@swc/core-linux-arm64-gnu@1.10.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.27.3': + '@swc/core-linux-arm64-musl@1.10.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.27.3': + '@swc/core-linux-x64-gnu@1.10.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.27.3': + '@swc/core-linux-x64-musl@1.10.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + '@swc/core-win32-arm64-msvc@1.10.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.27.3': + '@swc/core-win32-ia32-msvc@1.10.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.27.3': + '@swc/core-win32-x64-msvc@1.10.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.27.3': - optional: true + '@swc/core@1.10.1': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.17 + optionalDependencies: + '@swc/core-darwin-arm64': 1.10.1 + '@swc/core-darwin-x64': 1.10.1 + '@swc/core-linux-arm-gnueabihf': 1.10.1 + '@swc/core-linux-arm64-gnu': 1.10.1 + '@swc/core-linux-arm64-musl': 1.10.1 + '@swc/core-linux-x64-gnu': 1.10.1 + '@swc/core-linux-x64-musl': 1.10.1 + '@swc/core-win32-arm64-msvc': 1.10.1 + '@swc/core-win32-ia32-msvc': 1.10.1 + '@swc/core-win32-x64-msvc': 1.10.1 - '@rollup/rollup-linux-x64-musl@4.27.3': - optional: true + '@swc/counter@0.1.3': {} - '@rollup/rollup-win32-arm64-msvc@4.27.3': - optional: true + '@swc/jest@0.2.37(@swc/core@1.10.1)': + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@swc/core': 1.10.1 + '@swc/counter': 0.1.3 + jsonc-parser: 3.3.1 - '@rollup/rollup-win32-ia32-msvc@4.27.3': - optional: true + '@swc/types@0.1.17': + dependencies: + '@swc/counter': 0.1.3 - '@rollup/rollup-win32-x64-msvc@4.27.3': - optional: true + '@trysound/sax@0.2.0': {} - '@sinclair/typebox@0.27.8': {} + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 - '@trysound/sax@0.2.0': {} + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 '@types/estree@1.0.6': {} + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 18.16.0 + '@types/is-ci@3.0.4': dependencies: ci-info: 3.9.0 + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + '@types/minimist@1.2.5': {} '@types/node@12.20.55': {} @@ -3375,43 +3911,22 @@ snapshots: '@types/semver@7.5.8': {} - '@vitest/expect@1.6.0': - dependencies: - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - chai: 4.5.0 - - '@vitest/runner@1.6.0': - dependencies: - '@vitest/utils': 1.6.0 - p-limit: 5.0.0 - pathe: 1.1.2 - - '@vitest/snapshot@1.6.0': - dependencies: - magic-string: 0.30.13 - pathe: 1.1.2 - pretty-format: 29.7.0 - - '@vitest/spy@1.6.0': - dependencies: - tinyspy: 2.2.1 + '@types/stack-utils@2.0.3': {} - '@vitest/utils@1.6.0': - dependencies: - diff-sequences: 29.6.3 - estree-walker: 3.0.3 - loupe: 2.3.7 - pretty-format: 29.7.0 + '@types/yargs-parser@21.0.3': {} - acorn-walk@8.3.4: + '@types/yargs@17.0.33': dependencies: - acorn: 8.14.0 + '@types/yargs-parser': 21.0.3 acorn@8.14.0: {} ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -3428,6 +3943,11 @@ snapshots: ansi-styles@6.2.1: {} + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -3459,8 +3979,6 @@ snapshots: arrify@1.0.1: {} - assertion-error@1.1.0: {} - autoprefixer@10.4.20(postcss@8.4.49): dependencies: browserslist: 4.24.2 @@ -3475,6 +3993,61 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 + babel-jest@29.7.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.25.9 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@29.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + balanced-match@1.0.2: {} better-path-resolve@1.0.0: @@ -3483,6 +4056,11 @@ snapshots: boolbase@1.0.0: {} + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 @@ -3502,7 +4080,11 @@ snapshots: node-releases: 2.0.18 update-browserslist-db: 1.1.1(browserslist@4.24.2) - cac@6.7.14: {} + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} call-bind@1.0.7: dependencies: @@ -3512,6 +4094,8 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + callsites@3.1.0: {} + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 @@ -3520,6 +4104,8 @@ snapshots: camelcase@5.3.1: {} + camelcase@6.3.0: {} + caniuse-api@3.0.0: dependencies: browserslist: 4.24.2 @@ -3529,16 +4115,6 @@ snapshots: caniuse-lite@1.0.30001683: {} - chai@4.5.0: - dependencies: - assertion-error: 1.1.0 - check-error: 1.0.3 - deep-eql: 4.1.4 - get-func-name: 2.0.2 - loupe: 2.3.7 - pathval: 1.1.1 - type-detect: 4.1.0 - chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -3552,11 +4128,9 @@ snapshots: chalk@5.3.0: {} - chardet@0.7.0: {} + char-regex@1.0.2: {} - check-error@1.0.3: - dependencies: - get-func-name: 2.0.2 + chardet@0.7.0: {} ci-info@3.9.0: {} @@ -3564,6 +4138,8 @@ snapshots: dependencies: consola: 3.2.3 + cjs-module-lexer@1.4.1: {} + cliui@6.0.0: dependencies: string-width: 4.2.3 @@ -3578,6 +4154,10 @@ snapshots: clone@1.0.4: {} + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -3596,12 +4176,29 @@ snapshots: commondir@1.0.1: {} + concat-map@0.0.1: {} + confbox@0.1.8: {} consola@3.2.3: {} convert-source-map@2.0.0: {} + create-jest@29.7.0(@types/node@18.16.0): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@18.16.0) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -3730,9 +4327,7 @@ snapshots: decamelize@1.2.0: {} - deep-eql@4.1.4: - dependencies: - type-detect: 4.1.0 + dedent@1.5.3: {} deepmerge@4.3.1: {} @@ -3756,6 +4351,8 @@ snapshots: detect-indent@6.1.0: {} + detect-newline@3.1.0: {} + diff-sequences@29.6.3: {} dir-glob@3.0.1: @@ -3784,6 +4381,8 @@ snapshots: electron-to-chromium@1.5.64: {} + emittery@0.13.1: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -3900,32 +4499,6 @@ snapshots: '@esbuild/win32-ia32': 0.19.12 '@esbuild/win32-x64': 0.19.12 - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - esbuild@0.24.0: optionalDependencies: '@esbuild/aix-ppc64': 0.24.0 @@ -3957,25 +4530,33 @@ snapshots: escape-string-regexp@1.0.5: {} + escape-string-regexp@2.0.0: {} + esprima@4.0.1: {} estree-walker@2.0.2: {} - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.6 - - execa@8.0.1: + execa@5.1.1: dependencies: cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expect@29.7.0: + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 extendable-error@0.1.7: {} @@ -3993,10 +4574,16 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + fdir@6.4.2(picomatch@4.0.2): optionalDependencies: picomatch: 4.0.2 @@ -4058,8 +4645,6 @@ snapshots: get-caller-file@2.0.5: {} - get-func-name@2.0.2: {} - get-intrinsic@1.2.4: dependencies: es-errors: 1.3.0 @@ -4068,7 +4653,9 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 - get-stream@8.0.1: {} + get-package-type@0.1.0: {} + + get-stream@6.0.1: {} get-symbol-description@1.0.2: dependencies: @@ -4080,6 +4667,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@8.1.0: dependencies: fs.realpath: 1.0.0 @@ -4148,15 +4744,24 @@ snapshots: hosted-git-info@2.8.9: {} + html-escaper@2.0.2: {} + human-id@1.0.2: {} - human-signals@5.0.0: {} + human-signals@2.1.0: {} iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 - ignore@5.3.2: {} + ignore@5.3.2: {} + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -4219,6 +4824,8 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-generator-fn@2.1.0: {} + is-generator-function@1.0.10: dependencies: has-tostringtag: 1.0.2 @@ -4256,7 +4863,7 @@ snapshots: dependencies: call-bind: 1.0.7 - is-stream@3.0.0: {} + is-stream@2.0.1: {} is-string@1.0.7: dependencies: @@ -4293,6 +4900,355 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@18.16.0): + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@18.16.0) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@18.16.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@18.16.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 18.16.0 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 18.16.0 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.26.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.3 + slash: 3.0.0 + + jest-runner@29.7.0: + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + chalk: 4.1.2 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 18.16.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@29.7.0: + dependencies: + '@types/node': 18.16.0 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@18.16.0): + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@18.16.0) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + jiti@1.17.0: {} jiti@1.21.6: {} @@ -4301,8 +5257,6 @@ snapshots: js-tokens@4.0.0: {} - js-tokens@9.0.0: {} - js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -4314,14 +5268,20 @@ snapshots: json5@2.2.3: {} + jsonc-parser@3.3.1: {} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 kind-of@6.0.3: {} + kleur@3.0.3: {} + kleur@4.1.5: {} + leven@3.1.0: {} + lilconfig@3.1.2: {} lines-and-columns@1.2.4: {} @@ -4333,11 +5293,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - local-pkg@0.5.1: - dependencies: - mlly: 1.7.3 - pkg-types: 1.2.1 - locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -4352,10 +5307,6 @@ snapshots: lodash.uniq@4.5.0: {} - loupe@2.3.7: - dependencies: - get-func-name: 2.0.2 - lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -4369,6 +5320,14 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -4400,10 +5359,14 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 - mimic-fn@4.0.0: {} + mimic-fn@2.1.0: {} min-indent@1.0.1: {} + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 @@ -4447,6 +5410,10 @@ snapshots: nanoid@3.3.7: {} + natural-compare@1.4.0: {} + + node-int64@0.4.0: {} + node-releases@2.0.18: {} normalize-package-data@2.5.0: @@ -4456,11 +5423,13 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 + normalize-path@3.0.0: {} + normalize-range@0.1.2: {} - npm-run-path@5.3.0: + npm-run-path@4.0.1: dependencies: - path-key: 4.0.0 + path-key: 3.1.1 nth-check@2.1.1: dependencies: @@ -4481,9 +5450,9 @@ snapshots: dependencies: wrappy: 1.0.2 - onetime@6.0.0: + onetime@5.1.2: dependencies: - mimic-fn: 4.0.0 + mimic-fn: 2.1.0 os-tmpdir@1.0.2: {} @@ -4501,10 +5470,6 @@ snapshots: dependencies: yocto-queue: 0.1.0 - p-limit@5.0.0: - dependencies: - yocto-queue: 1.1.1 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -4526,9 +5491,9 @@ snapshots: path-exists@4.0.0: {} - path-key@3.1.1: {} + path-is-absolute@1.0.1: {} - path-key@4.0.0: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -4536,8 +5501,6 @@ snapshots: pathe@1.1.2: {} - pathval@1.1.1: {} - picocolors@1.0.0: {} picocolors@1.1.1: {} @@ -4548,6 +5511,8 @@ snapshots: pify@4.0.1: {} + pirates@4.0.6: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -4744,8 +5709,15 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + pseudomap@1.0.2: {} + pure-rand@6.1.0: {} + queue-microtask@1.2.3: {} quick-lru@4.0.1: {} @@ -4800,8 +5772,14 @@ snapshots: require-main-filename@2.0.0: {} + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + resolve-from@5.0.0: {} + resolve.exports@2.0.3: {} + resolve@1.22.8: dependencies: is-core-module: 2.15.1 @@ -4822,30 +5800,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - rollup@4.27.3: - dependencies: - '@types/estree': 1.0.6 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.27.3 - '@rollup/rollup-android-arm64': 4.27.3 - '@rollup/rollup-darwin-arm64': 4.27.3 - '@rollup/rollup-darwin-x64': 4.27.3 - '@rollup/rollup-freebsd-arm64': 4.27.3 - '@rollup/rollup-freebsd-x64': 4.27.3 - '@rollup/rollup-linux-arm-gnueabihf': 4.27.3 - '@rollup/rollup-linux-arm-musleabihf': 4.27.3 - '@rollup/rollup-linux-arm64-gnu': 4.27.3 - '@rollup/rollup-linux-arm64-musl': 4.27.3 - '@rollup/rollup-linux-powerpc64le-gnu': 4.27.3 - '@rollup/rollup-linux-riscv64-gnu': 4.27.3 - '@rollup/rollup-linux-s390x-gnu': 4.27.3 - '@rollup/rollup-linux-x64-gnu': 4.27.3 - '@rollup/rollup-linux-x64-musl': 4.27.3 - '@rollup/rollup-win32-arm64-msvc': 4.27.3 - '@rollup/rollup-win32-ia32-msvc': 4.27.3 - '@rollup/rollup-win32-x64-msvc': 4.27.3 - fsevents: 2.3.3 - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -4910,12 +5864,8 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.3 - siginfo@2.0.0: {} - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} - sisteransi@1.0.5: {} slash@3.0.0: {} @@ -4933,6 +5883,13 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + spawndamnit@2.0.0: dependencies: cross-spawn: 5.1.0 @@ -4954,14 +5911,19 @@ snapshots: sprintf-js@1.0.3: {} - stackback@0.0.2: {} - - std-env@3.8.0: {} + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 stream-transform@2.1.3: dependencies: mixme: 0.5.10 + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5003,15 +5965,15 @@ snapshots: strip-bom@3.0.0: {} - strip-final-newline@3.0.0: {} + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} strip-indent@3.0.0: dependencies: min-indent: 1.0.1 - strip-literal@2.1.0: - dependencies: - js-tokens: 9.0.0 + strip-json-comments@3.1.1: {} stylehacks@7.0.4(postcss@8.4.49): dependencies: @@ -5027,6 +5989,10 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} svgo@3.3.2: @@ -5041,21 +6007,23 @@ snapshots: term-size@2.2.1: {} - tinybench@2.9.0: {} + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 tinyglobby@0.2.10: dependencies: fdir: 6.4.2(picomatch@4.0.2) picomatch: 4.0.2 - tinypool@0.8.4: {} - - tinyspy@2.2.1: {} - tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 + tmpl@1.0.5: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -5072,10 +6040,12 @@ snapshots: wcwidth: 1.0.1 yargs: 17.7.2 - type-detect@4.1.0: {} + type-detect@4.0.8: {} type-fest@0.13.1: {} + type-fest@0.21.3: {} + type-fest@0.6.0: {} type-fest@0.8.1: {} @@ -5179,71 +6149,20 @@ snapshots: util-deprecate@1.0.2: {} + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vite-node@1.6.0(@types/node@18.16.0): - dependencies: - cac: 6.7.14 - debug: 4.3.7 - pathe: 1.1.2 - picocolors: 1.0.0 - vite: 5.4.11(@types/node@18.16.0) - transitivePeerDependencies: - - '@types/node' - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - vite@5.4.11(@types/node@18.16.0): - dependencies: - esbuild: 0.21.5 - postcss: 8.4.49 - rollup: 4.27.3 - optionalDependencies: - '@types/node': 18.16.0 - fsevents: 2.3.3 - - vitest@1.6.0(@types/node@18.16.0): + walker@1.0.8: dependencies: - '@vitest/expect': 1.6.0 - '@vitest/runner': 1.6.0 - '@vitest/snapshot': 1.6.0 - '@vitest/spy': 1.6.0 - '@vitest/utils': 1.6.0 - acorn-walk: 8.3.4 - chai: 4.5.0 - debug: 4.3.7 - execa: 8.0.1 - local-pkg: 0.5.1 - magic-string: 0.30.13 - pathe: 1.1.2 - picocolors: 1.0.0 - std-env: 3.8.0 - strip-literal: 2.1.0 - tinybench: 2.9.0 - tinypool: 0.8.4 - vite: 5.4.11(@types/node@18.16.0) - vite-node: 1.6.0(@types/node@18.16.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 18.16.0 - transitivePeerDependencies: - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser + makeerror: 1.0.12 wcwidth@1.0.1: dependencies: @@ -5302,11 +6221,6 @@ snapshots: dependencies: isexe: 2.0.0 - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -5327,6 +6241,11 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + y18n@4.0.3: {} y18n@5.0.8: {} @@ -5367,6 +6286,3 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} - - yocto-queue@1.1.1: {} - \ No newline at end of file diff --git a/setup.tests.ts b/setup.tests.ts new file mode 100644 index 00000000..d0610aae --- /dev/null +++ b/setup.tests.ts @@ -0,0 +1,11 @@ +/** + * `Jest` does not fully support ESM. Because of it do NOT remove these mocks! + * + * Related Jest Issue: + * title: Native support for ES Modules #9430 + * url: https://github.com/jestjs/jest/issues/9430 + **/ + +jest.mock('wrap-ansi', () => (str: string) => str); + +jest.mock('is-unicode-supported', () => () => true); diff --git a/tsconfig.json b/tsconfig.json index 34c34d38..7825e412 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,5 +12,5 @@ "@clack/core": ["./packages/core/src"] } }, - "include": ["packages"] + "include": ["packages", "setup.tests.ts"] }