diff --git a/biome.jsonc b/biome.jsonc index bb9ee99c..adfa2af0 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.1/schema.json", + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "files": { "include": ["**/**"], "ignore": [ diff --git a/src/bin/index.ts b/src/bin/index.ts index 42257581..9d3a34ef 100644 --- a/src/bin/index.ts +++ b/src/bin/index.ts @@ -2,7 +2,7 @@ import type { Configs } from '../@types/poku.js'; import { escapeRegExp } from '../modules/helpers/list-files.js'; -import { getArg, getPaths, hasArg, argToArray } from '../parsers/get-arg.js'; +import { positionals, values } from '../parsers/get-arg.js'; import { states } from '../configs/files.js'; import { platformIsValid } from '../parsers/get-runtime.js'; import { format } from '../services/format.js'; @@ -12,15 +12,27 @@ import { poku } from '../modules/essentials/poku.js'; import { Write } from '../services/write.js'; import { getConfigs } from '../parsers/options.js'; +const argToArray = ( + argValue: string | undefined, +): string[] | undefined => { + if (argValue === undefined) return undefined; + if (!argValue) return []; + + return argValue + .split(',') + .map((a) => a.trim()) + .filter((a) => a); +}; + (async () => { - if (hasArg('version') || hasArg('v', '-')) { + if (values.version) { const { VERSION } = require('../configs/poku.js'); Write.log(VERSION); return; } - if (hasArg('help') || hasArg('h', '-')) { + if (values.help) { const { help } = require('./help.js'); help(); @@ -28,56 +40,45 @@ import { getConfigs } from '../parsers/options.js'; return; } - const enforce = hasArg('enforce') || hasArg('x', '-'); - const configFile = getArg('config') || getArg('c', '-'); + const enforce = values.enforce; + const configFile = typeof values.config === 'boolean' ? undefined : values.config; const defaultConfigs = await getConfigs(configFile); - const dirs: string[] = (() => { + const dirs = ((): string[] => { /* c8 ignore next 2 */ // Deprecated - const includeArg = getArg('include'); - if (includeArg !== undefined) return includeArg.split(','); - - return ( - getPaths('-') ?? - (defaultConfigs?.include - ? Array.prototype.concat(defaultConfigs?.include) - : ['.']) - ); + const includeArg = values.include; + if (typeof includeArg === 'string') return includeArg.split(','); + if (positionals.length > 0) return positionals; + if (defaultConfigs?.include) return Array.prototype.concat(defaultConfigs?.include); + return ['.']; })(); - const platform = getArg('platform'); - const filter = getArg('filter') ?? defaultConfigs?.filter; - const exclude = getArg('exclude') ?? defaultConfigs?.exclude; - const killPort = getArg('killport'); - const killRange = getArg('killrange'); - const killPID = getArg('killpid'); + const platform = values.platform; + const filter = values.filter ?? defaultConfigs?.filter; + const exclude = values.exclude ?? defaultConfigs?.exclude; + const killPort = typeof values.killport === 'boolean' ? undefined : values.killport; + const killRange = typeof values.killrange === 'boolean' ? undefined : values.killrange; + const killPID = typeof values.killpid === 'boolean' ? undefined : values.killpid; /* c8 ignore start */ // Deno - const denoAllow = argToArray('denoallow') ?? defaultConfigs?.deno?.allow; - const denoDeny = argToArray('denodeny') ?? defaultConfigs?.deno?.deny; - const denoCJS = - getArg('denocjs') - ?.split(',') - .map((a) => a.trim()) - .filter((a) => a) || - hasArg('denocjs') || - defaultConfigs?.deno?.cjs; + const denoAllow = argToArray(typeof values.denoallow === 'boolean' ? undefined : values.denoallow) ?? defaultConfigs?.deno?.allow; + const denoDeny = argToArray(typeof values.denodeny === 'boolean' ? undefined : values.denodeny) ?? defaultConfigs?.deno?.deny; + const denoCJS = argToArray(typeof values.denocjs === 'boolean' ? undefined : values.denocjs) ?? defaultConfigs?.deno?.cjs; /* c8 ignore stop */ - const parallel = - hasArg('parallel') || hasArg('p', '-') || defaultConfigs?.parallel; - const quiet = hasArg('quiet') || hasArg('q', '-') || defaultConfigs?.quiet; - const debug = hasArg('debug') || hasArg('d', '-') || defaultConfigs?.debug; - const failFast = hasArg('failfast') || defaultConfigs?.failFast; - const watchMode = hasArg('watch') || hasArg('w', '-'); - const hasEnvFile = hasArg('envfile'); + const parallel = !!values.parallel || defaultConfigs?.parallel; + const quiet = !!values.quiet || defaultConfigs?.quiet; + const debug = !!values.debug || defaultConfigs?.debug; + const failFast = !!values.failfast || defaultConfigs?.failFast; + const watchMode = !!values.watch; + const hasEnvFile = values.envfile; const concurrency = (() => { if (!(parallel || defaultConfigs?.parallel)) return; - const value = Number(getArg('concurrency')); + const value = Number(values.concurrency); return Number.isNaN(value) ? defaultConfigs?.concurrency : value; })(); if (dirs.length === 1) states.isSinglePath = true; - if (hasArg('listfiles')) { + if (values.listfiles) { const { listFiles } = require('../modules/helpers/list-files.js'); const files: string[] = []; @@ -149,28 +150,25 @@ import { getConfigs } from '../parsers/options.js'; /* c8 ignore stop */ if (hasEnvFile || defaultConfigs?.envFile) { - const envFilePath = getArg('envfile') ?? defaultConfigs?.envFile; + const envFilePath = (typeof values.envfile === 'boolean' ? null : values.envfile) ?? defaultConfigs?.envFile; tasks.push(envFile(envFilePath)); } const options: Configs = { /* c8 ignore next 11 */ // Varies Platform - platform: platformIsValid(platform) - ? platform - : hasArg('node') - ? 'node' - : hasArg('bun') - ? 'bun' - : hasArg('deno') - ? 'deno' - : platformIsValid(defaultConfigs?.platform) - ? defaultConfigs?.platform - : undefined, + platform: (() => { + if (platformIsValid(platform)) return platform; + if (values.node) return 'node'; + if (values.bun) return 'bun'; + if (values.deno) return 'deno'; + if (platformIsValid(defaultConfigs?.platform)) return defaultConfigs?.platform; + return undefined; + })(), filter: - typeof filter === 'string' ? new RegExp(escapeRegExp(filter)) : filter, + typeof filter === 'string' ? new RegExp(escapeRegExp(filter)) : undefined, exclude: - typeof exclude === 'string' ? new RegExp(escapeRegExp(exclude)) : exclude, + typeof exclude === 'string' ? new RegExp(escapeRegExp(exclude)) : undefined, parallel, concurrency, quiet, diff --git a/src/bin/watch.ts b/src/bin/watch.ts index 35946c65..b307a8b6 100644 --- a/src/bin/watch.ts +++ b/src/bin/watch.ts @@ -5,7 +5,7 @@ import { Write } from '../services/write.js'; import process from 'node:process'; import type { Configs } from '../@types/poku.js'; import { format } from '../services/format.js'; -import { getArg } from '../parsers/get-arg.js'; +import { values } from '../parsers/get-arg.js'; import { fileResults } from '../configs/files.js'; import { availableParallelism } from '../polyfills/os.js'; @@ -14,7 +14,7 @@ export const startWatch = async (dirs: string[], options: Configs) => { const watchers: Set = new Set(); const executing = new Set(); - const interval = Number(getArg('watchinterval')) || 1500; + const interval = Number(values.watchinterval) || 1500; const setIsRunning = (value: boolean) => { isRunning = value; diff --git a/src/modules/helpers/container.ts b/src/modules/helpers/container.ts index 109cfeda..c34c1b12 100644 --- a/src/modules/helpers/container.ts +++ b/src/modules/helpers/container.ts @@ -5,9 +5,9 @@ import type { import { DockerCompose, DockerContainer } from '../../services/container.js'; /** A minimal API to assist tests that require containers or tests that run inside containers using a **Dockerfile**. */ -const dockerfile = (configs: DockerfileConfigs) => new DockerContainer(configs); +const dockerfile = (configs: DockerfileConfigs): DockerContainer => new DockerContainer(configs); /** A minimal API to assist tests that require containers or tests that run inside containers using a **docker-compose.yml**. */ -const compose = (configs: DockerComposeConfigs) => new DockerCompose(configs); +const compose = (configs: DockerComposeConfigs): DockerCompose => new DockerCompose(configs); export const docker = { dockerfile, compose }; diff --git a/src/modules/helpers/each.ts b/src/modules/helpers/each.ts index 059d23cf..d4813308 100644 --- a/src/modules/helpers/each.ts +++ b/src/modules/helpers/each.ts @@ -24,19 +24,19 @@ export const beforeEach = ( ): Control => { options?.immediate && callback(); - each.before.cb = () => { + each.before.cb = (): unknown => { if (each.before.status) return callback(); }; - const pause = () => { + const pause = (): void => { each.before.status = false; }; - const continueFunc = () => { + const continueFunc = (): void => { each.before.status = true; }; - const reset = () => { + const reset = (): void => { each.before.cb = undefined; }; @@ -61,19 +61,19 @@ export const beforeEach = ( * ``` */ export const afterEach = (callback: () => unknown): Control => { - each.after.cb = () => { + each.after.cb = (): unknown => { if (each.after.status) return callback(); }; - const pause = () => { + const pause = (): void => { each.after.status = false; }; - const continueFunc = () => { + const continueFunc = (): void => { each.after.status = true; }; - const reset = () => { + const reset = (): void => { each.after.cb = undefined; }; diff --git a/src/modules/helpers/list-files.ts b/src/modules/helpers/list-files.ts index 4576a21c..69e47db3 100644 --- a/src/modules/helpers/list-files.ts +++ b/src/modules/helpers/list-files.ts @@ -24,10 +24,10 @@ export const sanitizePath = (input: string, ensureTarget?: boolean): string => { : sanitizedPath; }; -export const isFile = async (fullPath: string) => +export const isFile = async (fullPath: string): Promise => (await fsStat(fullPath)).isFile(); -export const escapeRegExp = (string: string) => +export const escapeRegExp = (string: string): string => string.replace(regex.safeRegExp, '\\$&'); const envFilter = env.FILTER?.trim() diff --git a/src/modules/helpers/log.ts b/src/modules/helpers/log.ts index b621fb7f..ef2ffa96 100644 --- a/src/modules/helpers/log.ts +++ b/src/modules/helpers/log.ts @@ -2,7 +2,7 @@ import { parseResultType } from '../../parsers/assert.js'; import { Write } from '../../services/write.js'; /** By default **Poku** only shows outputs generated from itself. This helper allows you to use an alternative to `console.log` with **Poku**. */ -export const log = (...args: unknown[]) => { +export const log = (...args: unknown[]): void => { const parsedMessages = args .map((arg) => parseResultType(arg)) .join(' ') diff --git a/src/modules/helpers/skip.ts b/src/modules/helpers/skip.ts index fcc03e5d..fb101f7c 100644 --- a/src/modules/helpers/skip.ts +++ b/src/modules/helpers/skip.ts @@ -2,7 +2,7 @@ import { exit, env } from 'node:process'; import { Write } from '../../services/write.js'; import { format } from '../../services/format.js'; -export const skip = (message = 'Skipping') => { +export const skip = (message = 'Skipping'): void => { const isPoku = typeof env?.FILE === 'string' && env?.FILE.length > 0; const FILE = env.FILE; diff --git a/src/modules/index.ts b/src/modules/index.ts index b1479056..4d8e0385 100755 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -40,4 +40,4 @@ export type { export type { Configs as ListFilesConfigs } from '../@types/list-files.js'; /** 🐷 Auxiliary function to define the `poku` configurations */ -export const defineConfig = (options: ConfigFile) => options; +export const defineConfig = (options: ConfigFile): ConfigFile => options; diff --git a/src/parsers/get-arg.ts b/src/parsers/get-arg.ts index fda6a33f..65db37ec 100644 --- a/src/parsers/get-arg.ts +++ b/src/parsers/get-arg.ts @@ -1,70 +1,53 @@ import { argv } from 'node:process'; import { toDynamicCase } from './to-dynamic-case.js'; - -const [, , ...processArgs] = argv; -const regexQuotes = /''|""/; - -const processedArgs = processArgs.map(toDynamicCase); - -export const getArg = ( - arg: string, - prefix = '--', - baseArgs = processedArgs -): string | undefined => { - const argPattern = `${prefix}${arg}=`; - const argValue = baseArgs.find((a) => a.startsWith(argPattern)); - - if (!argValue) return; - - return argValue.slice(argPattern.length).replace(regexQuotes, ''); -}; - -export const hasArg = ( - arg: string, - prefix = '--', - baseArgs = processedArgs -): boolean => baseArgs.some((a) => a.startsWith(`${prefix}${arg}`)); - -export const getPaths = ( - prefix = '--', - baseArgs = processedArgs -): string[] | undefined => { - let hasPaths = false; - const paths: string[] = []; - - for (const arg of baseArgs) { - if (arg.startsWith(prefix)) continue; - - hasPaths = true; - const parts = arg.split(','); - - for (const part of parts) paths.push(part); - } - - return hasPaths ? paths : undefined; -}; - -export const argToArray = ( - arg: string, - prefix = '--', - baseArgs = processedArgs -): string[] | undefined => { - const hasArgument = hasArg(arg, prefix, baseArgs); - if (!hasArgument) return; - - const argValue = getArg(arg, prefix, baseArgs); - if (!argValue) return []; - - return argValue - .split(',') - .map((a) => a.trim()) - .filter((a) => a); -}; - -const only = getArg('only'); - -export const hasOnly = hasArg('only') && !only; +import { parseArgs as nodeParseArgs } from 'node:util'; + +function bool(short: string) { + return { + type: 'boolean', + short, + } as const; +} +function Options(type: TYPE, ...args: T[]) { + return Object.fromEntries(args.map(x => [x, { type }] as const)) as Record; +} +const options = { + version: bool('v'), + help: bool('h'), + parallel: bool('p'), + debug: bool('d'), + enforce: bool('x'), + quiet: bool('q'), + watch: bool('w'), + ...Options('string', 'concurrency', 'watchinterval', 'include', 'denocjs', 'platform', 'filter', 'exclude', 'killport', 'killrange', 'killpid', 'envfile', 'denoallow', 'denodeny'), + ...Options('boolean', 'listfiles', 'node', 'bun', 'deno', 'only', 'failfast'), + config: { + type: 'boolean', + short: 'c', + }, +} as const; + +function parseArgs(args = argv.slice(2).map(toDynamicCase)) { + const result = nodeParseArgs({ + allowPositionals: true, + strict: false, + options, + args, + }); + return { + values: result.values, + positionals: result.positionals.flatMap(x => x.split(',')), + }; +} + +export const { values, positionals } = parseArgs(); + +const only = typeof values.only === 'boolean' ? undefined : values.only; + +export const hasOnly = !!values.only && !only; export const hasDescribeOnly = only === 'describe'; export const hasItOnly = only && ['it', 'test'].includes(only); + +export const test = { parseArgs }; diff --git a/src/parsers/time.ts b/src/parsers/time.ts index 92de6714..d164895b 100644 --- a/src/parsers/time.ts +++ b/src/parsers/time.ts @@ -1,4 +1,4 @@ -const pad = (num: number) => String(num).padStart(2, '0'); +const pad = (num: number): string => String(num).padStart(2, '0'); export const parseTime = (date: Date): string => { const hours = pad(date.getHours()); diff --git a/test/unit/args.test.ts b/test/unit/args.test.ts index e09c769b..c37bc15a 100644 --- a/test/unit/args.test.ts +++ b/test/unit/args.test.ts @@ -1,69 +1,34 @@ import { describe } from '../../src/modules/helpers/describe.js'; import { it } from '../../src/modules/helpers/it/core.js'; import { assert } from '../../src/modules/essentials/assert.js'; -import { - getArg, - hasArg, - argToArray, - getPaths, -} from '../../src/parsers/get-arg.js'; +import { test } from '../../src/parsers/get-arg.js'; + +const { parseArgs } = test; describe('CLI Argument Handling Functions', () => { it('should get argument value', () => { - const args = ['--arg=value']; - const result = getArg('arg', '--', args); - assert.strictEqual(result, 'value', 'Argument value should be "value"'); - }); - - it('should return undefined for missing argument value', () => { - const args = ['--arg']; - const result = getArg('arg', '--', args); - assert.strictEqual(result, undefined, 'Argument value should be undefined'); + const args = ['--envfile=value']; + const result = parseArgs(args); + assert.strictEqual(result.values.envfile, 'value', 'Argument value should be "value"'); }); it('should check if argument exists', () => { - const args = ['--checkArg']; - const result = hasArg('checkArg', '--', args); - assert.strictEqual(result, true, 'Argument should exist'); + const args = ['--watch']; + const result = parseArgs(args); + assert.strictEqual(result.values.watch, true, 'Argument should exist'); }); it('should check if argument does not exist', () => { - const args = ['--anotherArg']; - const result = hasArg('checkArg', '--', args); - assert.strictEqual(result, false, 'Argument should not exist'); - }); - - it('should convert argument to array', () => { - const args = ['--array=1,2,3']; - const result = argToArray('array', '--', args); - assert.deepStrictEqual( - result, - ['1', '2', '3'], - 'Argument should be converted to array [1, 2, 3]' - ); - }); - - it('should return empty array for argument without value', () => { - const args = ['--array']; - const result = argToArray('array', '--', args); - assert.deepStrictEqual( - result, - [], - 'Argument should be converted to an empty array' - ); - }); - - it('should return undefined for non-existing argument to array', () => { - const args: string[] = []; - const result = argToArray('array', '--', args); - assert.strictEqual(result, undefined, 'Argument should be undefined'); + const args = ['--watch']; + const result = parseArgs(args); + assert.strictEqual(result.values.debug, undefined, 'Argument should not exist'); }); it('should return paths without prefix arguments', () => { const args = ['--arg=value', 'path1', 'path2']; - const result = getPaths('--', args); + const result = parseArgs(args); assert.deepStrictEqual( - result, + result.positionals, ['path1', 'path2'], 'Should return ["path1", "path2"]' ); @@ -71,9 +36,9 @@ describe('CLI Argument Handling Functions', () => { it('should split paths by comma', () => { const args = ['path1,path2,path3']; - const result = getPaths('--', args); + const result = parseArgs(args); assert.deepStrictEqual( - result, + result.positionals, ['path1', 'path2', 'path3'], 'Should split paths by comma' ); @@ -81,19 +46,19 @@ describe('CLI Argument Handling Functions', () => { it('should return undefined if no paths provided', () => { const args = ['--arg=value']; - const result = getPaths('--', args); - assert.strictEqual( - result, - undefined, + const result = parseArgs(args); + assert.deepStrictEqual( + result.positionals, + [], 'Should return undefined if no paths' ); }); it('should handle mixed arguments with and without prefix', () => { const args = ['--arg=value', 'path1', '--another=value', 'path2,path3']; - const result = getPaths('--', args); + const result = parseArgs(args); assert.deepStrictEqual( - result, + result.positionals, ['path1', 'path2', 'path3'], 'Should return ["path1", "path2", "path3"]' ); @@ -101,10 +66,10 @@ describe('CLI Argument Handling Functions', () => { it('should handle empty array', () => { const args: string[] = []; - const result = getPaths('--', args); - assert.strictEqual( - result, - undefined, + const result = parseArgs(args); + assert.deepStrictEqual( + result.positionals, + [], 'Should return undefined for empty array' ); }); diff --git a/test/unit/env-services.test.ts b/test/unit/env-services.test.ts index 2781a59a..838df5d5 100644 --- a/test/unit/env-services.test.ts +++ b/test/unit/env-services.test.ts @@ -89,10 +89,12 @@ describe('resolveEnvVariables', () => { it('should resolve environment variables', () => { const env = { MY_VAR: 'resolvedValue' }; assert.strictEqual( + // biome-ignore lint/nursery/noTemplateCurlyInString: resolveEnvVariables('value is ${MY_VAR}', env), 'value is resolvedValue' ); assert.strictEqual( + // biome-ignore lint/nursery/noTemplateCurlyInString: resolveEnvVariables('${MY_VAR} is set', env), 'resolvedValue is set' ); @@ -101,10 +103,12 @@ describe('resolveEnvVariables', () => { it('should handle missing environment variables', () => { const env = { MY_VAR: 'resolvedValue' }; assert.strictEqual( + // biome-ignore lint/nursery/noTemplateCurlyInString: resolveEnvVariables('value is ${UNKNOWN_VAR}', env), 'value is ' ); assert.strictEqual( + // biome-ignore lint/nursery/noTemplateCurlyInString: resolveEnvVariables('${UNKNOWN_VAR} is set', env), ' is set' ); diff --git a/test/unit/watch.test.ts b/test/unit/watch.test.ts index ee6dc5f9..400b099d 100644 --- a/test/unit/watch.test.ts +++ b/test/unit/watch.test.ts @@ -18,7 +18,7 @@ const runtime = getRuntime(); const tmpDir = path.resolve('.', 'test/__fixtures__/.temp/watch'); const humanDelay = 750; -const createTempDir = () => { +const createTempDir = (): void => { if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); }