diff --git a/.changeset/lovely-queens-occur.md b/.changeset/lovely-queens-occur.md new file mode 100644 index 0000000..e896bc6 --- /dev/null +++ b/.changeset/lovely-queens-occur.md @@ -0,0 +1,5 @@ +--- +'steiger': patch +--- + +Interactively suggest folders when a missing folder was specified in the command or when it wasn't specified at all diff --git a/packages/steiger-plugin-fsd/package.json b/packages/steiger-plugin-fsd/package.json index ae13fd2..ddd3a43 100644 --- a/packages/steiger-plugin-fsd/package.json +++ b/packages/steiger-plugin-fsd/package.json @@ -50,6 +50,6 @@ "@types/pluralize": "^0.0.33", "tsup": "^8.3.5", "typescript": "^5.7.2", - "vitest": "^2.1.8" + "vitest": "^3.0.0-beta.2" } } diff --git a/packages/steiger/package.json b/packages/steiger/package.json index 58353ac..f73d04f 100644 --- a/packages/steiger/package.json +++ b/packages/steiger/package.json @@ -40,15 +40,19 @@ "README.md" ], "dependencies": { + "@clack/prompts": "^0.8.2", "@feature-sliced/steiger-plugin": "workspace:*", "chokidar": "^4.0.1", "cosmiconfig": "^9.0.0", "effector": "^23.2.3", + "empathic": "^1.0.0", + "fastest-levenshtein": "^1.0.16", "globby": "^14.0.2", "immer": "^10.1.1", "lodash-es": "^4.17.21", "minimatch": "^10.0.1", "patronum": "^2.3.0", + "picocolors": "^1.1.1", "prexit": "^2.3.0", "yargs": "^17.7.2", "zod": "^3.24.0", @@ -63,9 +67,10 @@ "@total-typescript/ts-reset": "^0.6.1", "@types/lodash-es": "^4.17.12", "@types/yargs": "^17.0.33", + "memfs": "^4.15.0", "tsup": "^8.3.5", "tsx": "^4.19.2", "typescript": "^5.7.2", - "vitest": "^2.1.8" + "vitest": "^3.0.0-beta.2" } } diff --git a/packages/steiger/src/cli.ts b/packages/steiger/src/cli.ts index d407875..aaf50fb 100755 --- a/packages/steiger/src/cli.ts +++ b/packages/steiger/src/cli.ts @@ -1,7 +1,6 @@ #!/usr/bin/env node import { resolve, relative, dirname } from 'node:path' -import { stat } from 'node:fs/promises' import * as process from 'node:process' import yargs from 'yargs' import prexit from 'prexit' @@ -13,9 +12,11 @@ import { cosmiconfig } from 'cosmiconfig' import { linter } from './app' import { processConfiguration, $plugins } from './models/config' import { applyAutofixes } from './features/autofix' +import { chooseRootFolderFromGuesses, chooseRootFolderFromSimilar, ExitException } from './features/choose-root-folder' import fsd from '@feature-sliced/steiger-plugin' import type { Diagnostic } from '@steiger/types' import packageJson from '../package.json' +import { existsAndIsFolder } from './shared/file-system' const { config, filepath } = (await cosmiconfig('steiger').search()) ?? { config: null, filepath: undefined } const defaultConfig = fsd.configs.recommended @@ -64,8 +65,6 @@ const yargsProgram = yargs(hideBin(process.argv)) const filePaths = argv._ if (filePaths.length > 1) { throw new Error('Pass only one path to watch') - } else if (filePaths.length === 0) { - throw new Error('Pass a path to watch') } else { return true } @@ -87,17 +86,33 @@ const yargsProgram = yargs(hideBin(process.argv)) .showHelpOnFail(true) const consoleArgs = yargsProgram.parseSync() +const inputPaths = consoleArgs._ -const targetPath = resolve(consoleArgs._[0]) - -try { - if (!(await stat(targetPath)).isDirectory()) { - console.error(`${consoleArgs._[0]} is a file, must be a folder`) - process.exit(102) +let targetPath: string | undefined +if (inputPaths.length > 0) { + if (await existsAndIsFolder(inputPaths[0])) { + targetPath = resolve(inputPaths[0]) + } else { + try { + targetPath = resolve(await chooseRootFolderFromSimilar(inputPaths[0])) + } catch (e) { + if (e instanceof ExitException) { + process.exit(0) + } else { + throw e + } + } + } +} else { + try { + targetPath = resolve(await chooseRootFolderFromGuesses()) + } catch (e) { + if (e instanceof ExitException) { + process.exit(0) + } else { + throw e + } } -} catch { - console.error(`Folder ${consoleArgs._[0]} does not exist`) - process.exit(101) } const printDiagnostics = (diagnostics: Array) => { diff --git a/packages/steiger/src/features/choose-root-folder/choose-from-guesses.ts b/packages/steiger/src/features/choose-root-folder/choose-from-guesses.ts new file mode 100644 index 0000000..3bf73de --- /dev/null +++ b/packages/steiger/src/features/choose-root-folder/choose-from-guesses.ts @@ -0,0 +1,114 @@ +import { sep } from 'node:path' +import { confirm, isCancel, outro, select } from '@clack/prompts' + +import { ExitException } from './exit-exception' +import { formatCommand } from './format-command' +import { existsAndIsFolder } from '../../shared/file-system' + +const commonRootFolders = ['src', 'app'].map((folder) => `.${sep}${folder}`) + +/** Present the user with a choice of folders based on an informed guess. */ +export async function chooseFromGuesses(): Promise { + let targetPath: string | undefined + + const candidates = await findRootFolderCandidates() + if (candidates.length === 0) { + const answer = await confirm({ + message: `You haven't specified a path to check. Would you like to check this folder?`, + inactive: 'No, exit', + }) + + if (answer === true) { + targetPath = '.' + } + } else { + const answer = await select({ + message: `You haven't specified a path to check. Would you like to use one of the following?`, + options: candidates + .map((candidate) => ({ value: candidate, label: candidate })) + .concat({ value: '.', label: 'This folder' }) + .concat({ value: '', label: 'No, exit' }), + }) + + if (!isCancel(answer) && answer !== '') { + targetPath = answer + } + } + + if (targetPath === undefined) { + outro(`Alright! To run checks on a specific folder, run ${formatCommand(`steiger .${sep}your-folder`)}.`) + throw new ExitException() + } else { + outro(`Running ${formatCommand(`steiger ${targetPath}`)}`) + } + + return targetPath +} + +/** + * Check if any of the common root project folders are present + * and return a list of the ones that are present. + */ +async function findRootFolderCandidates(): Promise> { + return ( + await Promise.all(commonRootFolders.map(async (folder) => ((await existsAndIsFolder(folder)) ? folder : undefined))) + ).filter(Boolean) +} + +if (import.meta.vitest) { + const { describe, test, expect, vi, beforeEach } = import.meta.vitest + const { vol } = await import('memfs') + const { joinFromRoot } = await import('@steiger/toolkit') + + vi.mock('node:fs/promises', () => import('memfs').then((memfs) => memfs.fs.promises)) + + describe('findRootFolderCandidates', () => { + const root = joinFromRoot('home', 'project') + beforeEach(() => { + vol.reset() + vi.spyOn(process, 'cwd').mockReturnValue(root) + }) + + test('when src is present, app is not', async () => { + const fileStructure = { + src: {}, + dist: {}, + } + + vol.fromNestedJSON(fileStructure, root) + + await expect(findRootFolderCandidates()).resolves.toEqual([`.${sep}src`]) + }) + + test('when app is present, src is not', async () => { + const fileStructure = { + app: {}, + } + + vol.fromNestedJSON(fileStructure, root) + + await expect(findRootFolderCandidates()).resolves.toEqual([`.${sep}app`]) + }) + + test('when both src and app are present', async () => { + const fileStructure = { + src: {}, + app: {}, + } + + vol.fromNestedJSON(fileStructure, root) + + await expect(findRootFolderCandidates()).resolves.toEqual([`.${sep}src`, `.${sep}app`]) + }) + + test('when neither src nor app are present', async () => { + const fileStructure = { + dist: {}, + } + + vol.fromNestedJSON(fileStructure, root) + + await expect(findRootFolderCandidates()).resolves.toEqual([]) + }) + }) +} diff --git a/packages/steiger/src/features/choose-root-folder/choose-from-similar.ts b/packages/steiger/src/features/choose-root-folder/choose-from-similar.ts new file mode 100644 index 0000000..db18859 --- /dev/null +++ b/packages/steiger/src/features/choose-root-folder/choose-from-similar.ts @@ -0,0 +1,122 @@ +import { readdir } from 'node:fs/promises' +import { parse, relative, sep, join, dirname } from 'node:path' +import pc from 'picocolors' +import { isGitIgnored } from 'globby' +import * as find from 'empathic/find' + +import { distance } from 'fastest-levenshtein' +import { isCancel, outro, select, confirm } from '@clack/prompts' +import { formatCommand } from './format-command' +import { ExitException } from './exit-exception' + +/** The maximum Levenshtein distance between the input and the reference for the input to be considered a typo. */ +const typoThreshold = 5 +const gitFolder = find.up('.git') +const isIgnored = await isGitIgnored({ cwd: gitFolder ? dirname(gitFolder) : undefined }) + +/** Present the user with a choice of folders based on similarity to a given input. */ +export async function chooseFromSimilar(input: string): Promise { + const resolved = relative('.', input) + const { dir, base } = parse(resolved) + const existingDir = await resolveWithCorrections(dir || '.') + + const candidates = (await readdir(existingDir, { withFileTypes: true })) + .filter((entry) => entry.isDirectory() && entry.name !== '.git' && !isIgnored(join(existingDir, entry.name))) + .map((entry) => entry.name) + const withDistances = candidates.map((candidate) => [candidate, distance(candidate, base)] as const) + const suggestions = withDistances + .filter(([_candidate, distance]) => distance <= typoThreshold) + .sort((a, b) => a[1] - b[1]) + + let answer: string | undefined + if (suggestions.length === 1) { + const confirmation = await confirm({ + message: `${pc.red(input)} is not a folder. Did you mean ${pc.green(`.${sep}${join(existingDir, suggestions[0][0])}`)}?`, + inactive: 'No, exit', + }) + + if (confirmation === true) { + answer = join(existingDir, suggestions[0][0]) + } else { + answer = '' + } + } else { + const selection = await select({ + message: `${pc.red(input)} is not a folder. Did you mean one of the following?`, + options: suggestions + .map(([candidate, _distance]) => ({ value: candidate, label: `.${sep}${join(existingDir, candidate)}` })) + .concat({ value: '', label: 'No, exit' }), + }) + + if (selection !== '' && !isCancel(selection)) { + answer = join(existingDir, selection) + } else { + answer = '' + } + } + + if (answer !== '') { + outro(`Running ${formatCommand(`steiger .${sep}${answer}`)}`) + return answer + } else { + outro(`Alright! To run checks on a specific folder, run ${formatCommand(`steiger .${sep}your-folder`)}.`) + throw new ExitException() + } +} + +/** + * Take a relative path that might contain typos and resolve each typo to the best matching candidate. + * + * @example + * // For a folder structure like: + * // - src + * // - app + * // - shared + * // - dist + * resolveWithCorrections('src/app') // 'src/app' + * resolveWithCorrections('scr/shad') // 'src/shared' + */ +async function resolveWithCorrections(path: string) { + let finalPath = '.' + for (const part of path.split(sep)) { + if (part === '.') { + continue + } else if (part === '..') { + finalPath = join(finalPath, part) + } else { + const candidates = (await readdir(finalPath, { withFileTypes: true })) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + const distances = candidates.map((candidate) => distance(candidate, part)) + const bestMatch = candidates[distances.indexOf(Math.min(...distances))] + finalPath = join(finalPath, bestMatch) + } + } + + return finalPath +} + +if (import.meta.vitest) { + const { test, expect, vi } = import.meta.vitest + const { vol } = await import('memfs') + const { joinFromRoot } = await import('@steiger/toolkit') + + vi.mock('node:fs/promises', () => import('memfs').then((memfs) => memfs.fs.promises)) + + test('resolveWithCorrections', async () => { + const root = joinFromRoot('home', 'project') + const fileStructure = { + src: { + app: {}, + shared: {}, + }, + dist: {}, + } + + vi.spyOn(process, 'cwd').mockReturnValue(root) + vol.fromNestedJSON(fileStructure, root) + + expect(await resolveWithCorrections(join('src', 'app'))).toBe(join('src', 'app')) + expect(await resolveWithCorrections(join('scr', 'shad'))).toBe(join('src', 'shared')) + }) +} diff --git a/packages/steiger/src/features/choose-root-folder/exit-exception.ts b/packages/steiger/src/features/choose-root-folder/exit-exception.ts new file mode 100644 index 0000000..81007c9 --- /dev/null +++ b/packages/steiger/src/features/choose-root-folder/exit-exception.ts @@ -0,0 +1 @@ +export class ExitException extends Error {} diff --git a/packages/steiger/src/features/choose-root-folder/format-command.ts b/packages/steiger/src/features/choose-root-folder/format-command.ts new file mode 100644 index 0000000..c54e216 --- /dev/null +++ b/packages/steiger/src/features/choose-root-folder/format-command.ts @@ -0,0 +1,5 @@ +import pc from 'picocolors' + +export function formatCommand(command: string): string { + return pc.green(`\`${command}\``) +} diff --git a/packages/steiger/src/features/choose-root-folder/index.ts b/packages/steiger/src/features/choose-root-folder/index.ts new file mode 100644 index 0000000..7488677 --- /dev/null +++ b/packages/steiger/src/features/choose-root-folder/index.ts @@ -0,0 +1,3 @@ +export { chooseFromGuesses as chooseRootFolderFromGuesses } from './choose-from-guesses' +export { chooseFromSimilar as chooseRootFolderFromSimilar } from './choose-from-similar' +export { ExitException } from './exit-exception' diff --git a/packages/steiger/src/features/transfer-fs-to-vfs.ts b/packages/steiger/src/features/transfer-fs-to-vfs.ts index 181e9ec..426a695 100644 --- a/packages/steiger/src/features/transfer-fs-to-vfs.ts +++ b/packages/steiger/src/features/transfer-fs-to-vfs.ts @@ -1,27 +1,11 @@ -import { join, sep, resolve, parse, dirname } from 'node:path' -import { existsSync } from 'node:fs' +import { dirname, join, sep } from 'node:path' import chokidar from 'chokidar' +import * as find from 'empathic/find' import type { Folder } from '@steiger/types' import { isGitIgnored } from 'globby' import { createVfsRoot } from '../models/vfs' -function findGitRoot(startDir: string): string | null { - let currentDir = resolve(startDir) - - while (currentDir !== parse(currentDir).root) { - const gitFolderOrLinkPath = join(currentDir, '.git') - - if (existsSync(gitFolderOrLinkPath)) { - return currentDir - } - // Move up one directory - currentDir = dirname(currentDir) - } - - return null -} - /** * Start watching a given path with chokidar. * @@ -29,10 +13,11 @@ function findGitRoot(startDir: string): string | null { */ export async function createWatcher(path: string) { const vfs = createVfsRoot(path) - const isIgnored = await isGitIgnored({ cwd: findGitRoot(path) || path }) + const gitFolder = find.up('.git', { cwd: path }) + const isIgnored = await isGitIgnored({ cwd: gitFolder ? dirname(gitFolder) : path }) const watcher = chokidar.watch(path, { - ignored: (path) => path.split(sep).includes('node_modules') || isIgnored(path), + ignored: (path) => path.split(sep).includes('node_modules') || path.split(sep).includes('.git') || isIgnored(path), ignoreInitial: false, alwaysStat: true, awaitWriteFinish: true, diff --git a/packages/steiger/src/shared/file-system.ts b/packages/steiger/src/shared/file-system.ts index a01661b..59109c0 100644 --- a/packages/steiger/src/shared/file-system.ts +++ b/packages/steiger/src/shared/file-system.ts @@ -1,3 +1,4 @@ +import { access, stat } from 'node:fs/promises' import { File, Folder } from '@steiger/types' export function isPathInTree(vfs: Folder, paths: string | Array) { @@ -29,3 +30,13 @@ export function isPathInTree(vfs: Folder, paths: string | Array) { return typeof paths === 'string' ? results[0] : results } + +/** Check if a given path exists and is a folder. */ +export async function existsAndIsFolder(path: string) { + try { + await access(path) + return (await stat(path)).isDirectory() + } catch { + return false + } +} diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 84b7dde..1bfe728 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -30,7 +30,7 @@ } ], "peerDependencies": { - "vitest": "^2.1.8" + "vitest": "^1.6.0 || ^2.1.8 || ^3.0.0-beta.2" }, "peerDependenciesMeta": { "vitest": { @@ -44,6 +44,6 @@ "@total-typescript/ts-reset": "^0.6.1", "tsup": "^8.3.5", "typescript": "^5.7.2", - "vitest": "^2.1.8" + "vitest": "^3.0.0-beta.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06f679d..7701599 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: packages/steiger: dependencies: + '@clack/prompts': + specifier: ^0.8.2 + version: 0.8.2 '@feature-sliced/steiger-plugin': specifier: workspace:* version: link:../steiger-plugin-fsd @@ -90,6 +93,12 @@ importers: effector: specifier: ^23.2.3 version: 23.2.3 + empathic: + specifier: ^1.0.0 + version: 1.0.0 + fastest-levenshtein: + specifier: ^1.0.16 + version: 1.0.16 globby: specifier: ^14.0.2 version: 14.0.2 @@ -105,6 +114,9 @@ importers: patronum: specifier: ^2.3.0 version: 2.3.0(effector@23.2.3) + picocolors: + specifier: ^1.1.1 + version: 1.1.1 prexit: specifier: ^2.3.0 version: 2.3.0 @@ -142,6 +154,9 @@ importers: '@types/yargs': specifier: ^17.0.33 version: 17.0.33 + memfs: + specifier: ^4.15.0 + version: 4.15.0 tsup: specifier: ^8.3.5 version: 8.3.5(@microsoft/api-extractor@7.47.7(@types/node@22.10.1))(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.5.1) @@ -152,8 +167,8 @@ importers: specifier: ^5.7.2 version: 5.7.2 vitest: - specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.1) + specifier: ^3.0.0-beta.2 + version: 3.0.0-beta.2(@types/node@22.10.1) packages/steiger-plugin-fsd: dependencies: @@ -201,8 +216,8 @@ importers: specifier: ^5.7.2 version: 5.7.2 vitest: - specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.1) + specifier: ^3.0.0-beta.2 + version: 3.0.0-beta.2(@types/node@22.10.1) packages/toolkit: devDependencies: @@ -225,8 +240,8 @@ importers: specifier: ^5.7.2 version: 5.7.2 vitest: - specifier: ^2.1.8 - version: 2.1.8(@types/node@22.10.1) + specifier: ^3.0.0-beta.2 + version: 3.0.0-beta.2(@types/node@22.10.1) packages/types: devDependencies: @@ -351,6 +366,12 @@ packages: '@changesets/write@0.3.2': resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@dependents/detective-less@5.0.0': resolution: {integrity: sha512-D/9dozteKcutI5OdxJd8rU+fL6XgaaRg60sPPJWkT33OCiRfkCu5wO5B/yXTaaL2e6EB0lcCBGe5E0XscZCvvQ==} engines: {node: '>=18'} @@ -869,6 +890,24 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.1.1': + resolution: {integrity: sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.5.0': + resolution: {integrity: sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@manypkg/cli@0.23.0': resolution: {integrity: sha512-9N0GuhUZhrDbOS2rer1/ZWaO8RvPOUI+kKTwlq74iQXomL+725E9Vfvl9U64FYwnLkQCxCmPZ9nBs/u8JwFnSw==} engines: {node: '>=18.0.0'} @@ -1237,11 +1276,11 @@ packages: resolution: {integrity: sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@2.1.8': - resolution: {integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw==} + '@vitest/expect@3.0.0-beta.2': + resolution: {integrity: sha512-xdywwsqHOTZ66dBr8sQ+l3c0ZQs/wQY48fBRgLDrUqTU8OlDir6H1JMIOeV+Jb85Ov1XBGXBrSVlPDIo/fN5EQ==} - '@vitest/mocker@2.1.8': - resolution: {integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA==} + '@vitest/mocker@3.0.0-beta.2': + resolution: {integrity: sha512-rSYrjKX8RwiKLw9MoZ8FDjos90C//AVphNVVYsv8QJn6brSkJLAOTFjTn13E8mF8kh3Bx8NKNgyDrx48ioJFXQ==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 @@ -1251,20 +1290,20 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.8': - resolution: {integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ==} + '@vitest/pretty-format@3.0.0-beta.2': + resolution: {integrity: sha512-vMCmIdShOz2vjMCyxk+SoexZxsIbwrRc/weTctKxnQAYv3NubehpwCOaT8nhirmYQtdW+8r079wz1s7cKxNmCA==} - '@vitest/runner@2.1.8': - resolution: {integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg==} + '@vitest/runner@3.0.0-beta.2': + resolution: {integrity: sha512-Ytyub2tBCGrROrGfVlB8SuWdQjFYzJTTR969CGJF/xkIgdkLE9SiQzBZy4td2VidypntLXAVHYjeGr75pvw93w==} - '@vitest/snapshot@2.1.8': - resolution: {integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg==} + '@vitest/snapshot@3.0.0-beta.2': + resolution: {integrity: sha512-6INaNxXyYBmFGHhjmSyoz+/P3F+e6sHZPXLYt2OAa6Zt1v1O91FoGUTwdNHj2ASxMQeVpK/7snxNaeyr2INVOg==} - '@vitest/spy@2.1.8': - resolution: {integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg==} + '@vitest/spy@3.0.0-beta.2': + resolution: {integrity: sha512-tSxQfS/wDWRtyx/a3smGuQr/YFaZk1iUsPbKkEvd6jIsrWBb747MSpdn9xfLgIhI68tXquCzruXiMQG0kHdILA==} - '@vitest/utils@2.1.8': - resolution: {integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA==} + '@vitest/utils@3.0.0-beta.2': + resolution: {integrity: sha512-Jkib9LoI9Xm3gmzwI+9KgEAJVZNgJQFrR1RAyqBN7k9O3qezOTUjqyYBnvyz3UcPywygP1jEjZWBxUKx4ELpxw==} '@vue/compiler-core@3.4.27': resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} @@ -1595,6 +1634,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + empathic@1.0.0: + resolution: {integrity: sha512-qtKgI1Mv8rTacvpaTkh28HM2Lbf+IOjXb7rhpt/42kZxRm8TBb/IVlo5iL2ztT19kc/EHAFN0fZ641avlXAgdg==} + engines: {node: '>=16'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -1887,6 +1930,10 @@ packages: engines: {node: '>=18'} hasBin: true + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2093,6 +2140,10 @@ packages: magic-string@0.30.15: resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} + memfs@4.15.0: + resolution: {integrity: sha512-q9MmZXd2rRWHS6GU3WEm3HyiXZyyoA1DqdOhEq0lxPBmKb5S7IAOwX0RgUCwJfqjelDCySa5h8ujOy24LqsWcw==} + engines: {node: '>= 4.0.0'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -2491,6 +2542,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -2615,6 +2669,12 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2652,6 +2712,12 @@ packages: tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tree-dump@1.0.2: + resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2681,6 +2747,9 @@ packages: typescript: optional: true + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.3.5: resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} engines: {node: '>=18'} @@ -2785,9 +2854,9 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - vite-node@2.1.8: - resolution: {integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg==} - engines: {node: ^18.0.0 || >=20.0.0} + vite-node@3.0.0-beta.2: + resolution: {integrity: sha512-ofTf6cfRdL30Wbl9n/BX81EyIR5s4PReLmSurrxQ+koLaWUNOEo8E0lCM53OJkb8vpa2URM2nSrxZsIFyvY1rg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true vite@5.4.11: @@ -2821,15 +2890,15 @@ packages: terser: optional: true - vitest@2.1.8: - resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} - engines: {node: ^18.0.0 || >=20.0.0} + vitest@3.0.0-beta.2: + resolution: {integrity: sha512-ZP0FVJ4tNJJOsjzZSuadEW0BPBgO7DMMen3mIE8TPPiPUMwz9YoS1U5bcqMYZ61r34xGsaYPe1h0l1MXt50f7g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' - '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.8 - '@vitest/ui': 2.1.8 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.0.0-beta.2 + '@vitest/ui': 3.0.0-beta.2 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -3085,6 +3154,17 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@dependents/detective-less@5.0.0': dependencies: gonzales-pe: 4.3.0 @@ -3394,6 +3474,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.1.1(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.5.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.5.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + '@manypkg/cli@0.23.0': dependencies: '@manypkg/get-packages': 2.2.2 @@ -3785,43 +3881,43 @@ snapshots: '@typescript-eslint/types': 8.18.0 eslint-visitor-keys: 4.2.0 - '@vitest/expect@2.1.8': + '@vitest/expect@3.0.0-beta.2': dependencies: - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 + '@vitest/spy': 3.0.0-beta.2 + '@vitest/utils': 3.0.0-beta.2 chai: 5.1.2 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.8(vite@5.4.11(@types/node@22.10.1))': + '@vitest/mocker@3.0.0-beta.2(vite@5.4.11(@types/node@22.10.1))': dependencies: - '@vitest/spy': 2.1.8 + '@vitest/spy': 3.0.0-beta.2 estree-walker: 3.0.3 magic-string: 0.30.15 optionalDependencies: vite: 5.4.11(@types/node@22.10.1) - '@vitest/pretty-format@2.1.8': + '@vitest/pretty-format@3.0.0-beta.2': dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@2.1.8': + '@vitest/runner@3.0.0-beta.2': dependencies: - '@vitest/utils': 2.1.8 + '@vitest/utils': 3.0.0-beta.2 pathe: 1.1.2 - '@vitest/snapshot@2.1.8': + '@vitest/snapshot@3.0.0-beta.2': dependencies: - '@vitest/pretty-format': 2.1.8 + '@vitest/pretty-format': 3.0.0-beta.2 magic-string: 0.30.15 pathe: 1.1.2 - '@vitest/spy@2.1.8': + '@vitest/spy@3.0.0-beta.2': dependencies: tinyspy: 3.0.2 - '@vitest/utils@2.1.8': + '@vitest/utils@3.0.0-beta.2': dependencies: - '@vitest/pretty-format': 2.1.8 + '@vitest/pretty-format': 3.0.0-beta.2 loupe: 3.1.2 tinyrainbow: 1.2.0 @@ -4139,6 +4235,8 @@ snapshots: emoji-regex@9.2.2: {} + empathic@1.0.0: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -4506,6 +4604,8 @@ snapshots: husky@9.1.7: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -4690,6 +4790,13 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + memfs@4.15.0: + dependencies: + '@jsonjoy.com/json-pack': 1.1.1(tslib@2.8.1) + '@jsonjoy.com/util': 1.5.0(tslib@2.8.1) + tree-dump: 1.0.2(tslib@2.8.1) + tslib: 2.8.1 + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -5073,6 +5180,8 @@ snapshots: signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + slash@3.0.0: {} slash@5.1.0: {} @@ -5191,6 +5300,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thingies@1.21.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tinybench@2.9.0: {} tinyexec@0.3.1: {} @@ -5220,6 +5333,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.0.2(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} ts-api-utils@1.3.0(typescript@5.7.2): @@ -5236,6 +5353,8 @@ snapshots: optionalDependencies: typescript: 5.7.2 + tslib@2.8.1: {} + tsup@8.3.5(@microsoft/api-extractor@7.47.7(@types/node@22.10.1))(postcss@8.4.47)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.5.1): dependencies: bundle-require: 5.0.0(esbuild@0.24.0) @@ -5362,7 +5481,7 @@ snapshots: validate-npm-package-name@5.0.1: {} - vite-node@2.1.8(@types/node@22.10.1): + vite-node@3.0.0-beta.2(@types/node@22.10.1): dependencies: cac: 6.7.14 debug: 4.4.0 @@ -5389,15 +5508,15 @@ snapshots: '@types/node': 22.10.1 fsevents: 2.3.3 - vitest@2.1.8(@types/node@22.10.1): + vitest@3.0.0-beta.2(@types/node@22.10.1): dependencies: - '@vitest/expect': 2.1.8 - '@vitest/mocker': 2.1.8(vite@5.4.11(@types/node@22.10.1)) - '@vitest/pretty-format': 2.1.8 - '@vitest/runner': 2.1.8 - '@vitest/snapshot': 2.1.8 - '@vitest/spy': 2.1.8 - '@vitest/utils': 2.1.8 + '@vitest/expect': 3.0.0-beta.2 + '@vitest/mocker': 3.0.0-beta.2(vite@5.4.11(@types/node@22.10.1)) + '@vitest/pretty-format': 3.0.0-beta.2 + '@vitest/runner': 3.0.0-beta.2 + '@vitest/snapshot': 3.0.0-beta.2 + '@vitest/spy': 3.0.0-beta.2 + '@vitest/utils': 3.0.0-beta.2 chai: 5.1.2 debug: 4.4.0 expect-type: 1.1.0 @@ -5409,7 +5528,7 @@ snapshots: tinypool: 1.0.2 tinyrainbow: 1.2.0 vite: 5.4.11(@types/node@22.10.1) - vite-node: 2.1.8(@types/node@22.10.1) + vite-node: 3.0.0-beta.2(@types/node@22.10.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.10.1 diff --git a/tooling/eslint-config/eslint.config.mjs b/tooling/eslint-config/eslint.config.mjs index 32fd84f..a0ef3b6 100644 --- a/tooling/eslint-config/eslint.config.mjs +++ b/tooling/eslint-config/eslint.config.mjs @@ -9,4 +9,9 @@ export default [ ...tseslint.configs.recommended, eslintConfigPrettier, { ignores: ['**/node_modules', '**/dist'] }, + { + rules: { + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + }, + }, ]