diff --git a/docs/cli/lint.md b/docs/cli/lint.md index b45cb574f..e529dced2 100644 --- a/docs/cli/lint.md +++ b/docs/cli/lint.md @@ -62,6 +62,25 @@ you can limit this with the `--serial` flag. [GitHub autofixes] are enabled when CI and GitHub environment variables are present. +### Linting/formatting specific files + +`skuba format` and `skuba lint` accept optional glob patterns using glob syntax from the [fast-glob] module. which allow you to target specific files. + +```shell +skuba format src/index.ts + +Processed skuba lints in 0.03s. + +ESLint +Processed 1 file in 2.13s. + +Prettier +Processed 1 file in 0.11s. +✨ Done in 5.57s. +``` + +When using `skuba lint` with glob patterns, the `tsc` compiler will always run over the directories defined by your local project's `tsconfig.json`. + ### Annotations `skuba lint` can automatically emit annotations in CI. @@ -75,6 +94,7 @@ you can limit this with the `--serial` flag. [eslint deep dive]: ../deep-dives/eslint.md [eslint-config-seek]: https://github.com/seek-oss/eslint-config-seek [ESLint]: https://eslint.org/ +[fast-glob]: https://github.com/mrmlnc/fast-glob#pattern-syntax [GitHub annotations]: ../deep-dives/github.md#github-annotations [GitHub autofixes]: ../deep-dives/github.md#github-autofixes [prescribe ESLint]: https://myseek.atlassian.net/wiki/spaces/AA/pages/2358346041/#TypeScript diff --git a/package.json b/package.json index b34b15a5e..109a643cf 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "eslint": "^8.11.0", "eslint-config-skuba": "3.1.0", "execa": "^5.0.0", + "fast-glob": "^3.3.2", "fdir": "^6.0.0", "fs-extra": "^11.0.0", "function-arguments": "^1.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e64879a0..097f88d19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ dependencies: execa: specifier: ^5.0.0 version: 5.1.1 + fast-glob: + specifier: ^3.3.2 + version: 3.3.2 fdir: specifier: ^6.0.0 version: 6.1.1(picomatch@3.0.1) diff --git a/src/cli/adapter/eslint.ts b/src/cli/adapter/eslint.ts index 73ac50835..c8b51754b 100644 --- a/src/cli/adapter/eslint.ts +++ b/src/cli/adapter/eslint.ts @@ -29,6 +29,7 @@ export interface ESLintOutput { export const runESLint = async ( mode: 'format' | 'lint', logger: Logger, + inputFiles: string[], ): Promise => { logger.debug('Initialising ESLint...'); @@ -46,7 +47,7 @@ export const runESLint = async ( const [formatter, results] = await Promise.all([ engine.loadFormatter(), - engine.lintFiles('.'), + engine.lintFiles(inputFiles.length ? inputFiles : '.'), ]); const end = process.hrtime.bigint(); diff --git a/src/cli/adapter/prettier.test.ts b/src/cli/adapter/prettier.test.ts index 8224ee760..efab596a3 100644 --- a/src/cli/adapter/prettier.test.ts +++ b/src/cli/adapter/prettier.test.ts @@ -42,6 +42,7 @@ describe('runPrettier', () => { runPrettier( 'lint', log, + [], path.join(__dirname, '../../../integration/base/fixable'), ), ).resolves.toMatchInlineSnapshot(` @@ -77,6 +78,7 @@ describe('runPrettier', () => { runPrettier( 'lint', log, + [], path.join(__dirname, '../../../integration/base/fixable'), ), ).resolves.toMatchInlineSnapshot(` diff --git a/src/cli/adapter/prettier.ts b/src/cli/adapter/prettier.ts index 84297de34..2429e6177 100644 --- a/src/cli/adapter/prettier.ts +++ b/src/cli/adapter/prettier.ts @@ -155,6 +155,7 @@ export interface PrettierOutput { export const runPrettier = async ( mode: 'format' | 'lint', logger: Logger, + inputFiles: string[], cwd = process.cwd(), ): Promise => { logger.debug('Initialising Prettier...'); @@ -176,10 +177,9 @@ export const runPrettier = async ( // This avoids exhibiting different behaviour than a Prettier IDE integration, // though it may present headaches if `.gitignore` and `.prettierignore` rules // conflict. - const relativeFilepaths = await crawlDirectory(directory, [ - '.gitignore', - '.prettierignore', - ]); + const relativeFilepaths = inputFiles.length + ? inputFiles + : await crawlDirectory(directory, ['.gitignore', '.prettierignore']); logger.debug(`Discovered ${pluralise(relativeFilepaths.length, 'file')}.`); diff --git a/src/cli/format.ts b/src/cli/format.ts index 2e3ecb902..c9c34c700 100644 --- a/src/cli/format.ts +++ b/src/cli/format.ts @@ -1,29 +1,36 @@ import chalk from 'chalk'; -import { hasDebugFlag } from '../utils/args'; +import { hasDebugFlag, parseNonFlagArgs } from '../utils/args'; import { createLogger, log } from '../utils/logging'; import { runESLint } from './adapter/eslint'; import { runPrettier } from './adapter/prettier'; +import { getFiles } from './lint/files'; import { internalLint } from './lint/internal'; export const format = async (args = process.argv.slice(2)): Promise => { const debug = hasDebugFlag(args); + const fileInputs = parseNonFlagArgs(args); + const files = getFiles(fileInputs); log.plain(chalk.blueBright('skuba lints')); - const internal = await internalLint('format', { debug, serial: true }); + const internal = await internalLint('format', { + debug, + serial: true, + inputFiles: [], + }); const logger = createLogger(debug); log.newline(); log.plain(chalk.magenta('ESLint')); - const eslint = await runESLint('format', logger); + const eslint = await runESLint('format', logger, files); log.newline(); log.plain(chalk.cyan('Prettier')); - const prettier = await runPrettier('format', logger); + const prettier = await runPrettier('format', logger, files); if (eslint.ok && prettier.ok && internal.ok) { return; diff --git a/src/cli/init/index.ts b/src/cli/init/index.ts index dfd438bb0..26805a66b 100644 --- a/src/cli/init/index.ts +++ b/src/cli/init/index.ts @@ -99,7 +99,7 @@ export const init = async (args = process.argv.slice(2)) => { // Templating can initially leave certain files in an unformatted state; // consider a Markdown table with columns sized based on content length. - await runPrettier('format', createLogger(opts.debug), destinationDir); + await runPrettier('format', createLogger(opts.debug), [], destinationDir); depsInstalled = true; } catch (err) { diff --git a/src/cli/lint/autofix.ts b/src/cli/lint/autofix.ts index f0254e0ad..410013a42 100644 --- a/src/cli/lint/autofix.ts +++ b/src/cli/lint/autofix.ts @@ -137,12 +137,12 @@ export const autofix = async (params: AutofixParameters): Promise => { } if (params.eslint) { - await runESLint('format', logger); + await runESLint('format', logger, []); } // Unconditionally re-run Prettier; reaching here means we have pre-existing // format violations or may have created new ones through ESLint/internal fixes. - await runPrettier('format', logger); + await runPrettier('format', logger, []); if (process.env.GITHUB_ACTIONS) { // GitHub runners have Git installed locally diff --git a/src/cli/lint/eslint.ts b/src/cli/lint/eslint.ts index e644a9901..158d1dafd 100644 --- a/src/cli/lint/eslint.ts +++ b/src/cli/lint/eslint.ts @@ -11,8 +11,8 @@ import type { Input } from './types'; const LOG_PREFIX = chalk.magenta('ESLint │'); -export const runESLintInCurrentThread = ({ debug }: Input) => - runESLint('lint', createLogger(debug, LOG_PREFIX)); +export const runESLintInCurrentThread = ({ debug, inputFiles }: Input) => + runESLint('lint', createLogger(debug, LOG_PREFIX), inputFiles); export const runESLintInWorkerThread = (input: Input) => execWorkerThread( diff --git a/src/cli/lint/files.ts b/src/cli/lint/files.ts new file mode 100644 index 000000000..13d8b5a37 --- /dev/null +++ b/src/cli/lint/files.ts @@ -0,0 +1,17 @@ +import { globSync } from 'fast-glob'; + +import { log } from '../../utils/logging'; + +export const getFiles = (globPatterns: string[]): string[] => { + const files = globSync(globPatterns); + + if (globPatterns.length && !files.length) { + const error = `No files matching the patterns were found: ${globPatterns.join( + ', ', + )}`; + log.err(error); + process.exit(1); + } + + return files; +}; diff --git a/src/cli/lint/index.ts b/src/cli/lint/index.ts index 346b7b2c3..2db3b2dd0 100644 --- a/src/cli/lint/index.ts +++ b/src/cli/lint/index.ts @@ -1,7 +1,11 @@ import type { Writable } from 'stream'; import { inspect } from 'util'; -import { hasDebugFlag, hasSerialFlag } from '../../utils/args'; +import { + hasDebugFlag, + hasSerialFlag, + parseNonFlagArgs, +} from '../../utils/args'; import { log } from '../../utils/logging'; import { detectPackageManager } from '../../utils/packageManager'; import { throwOnTimeout } from '../../utils/wait'; @@ -9,6 +13,7 @@ import { throwOnTimeout } from '../../utils/wait'; import { createAnnotations } from './annotate'; import { autofix } from './autofix'; import { externalLint } from './external'; +import { getFiles } from './files'; import { internalLint } from './internal'; import type { Input } from './types'; @@ -22,6 +27,7 @@ export const lint = async ( serial: hasSerialFlag(args), tscOutputStream: tscWriteable, workerThreads, + inputFiles: getFiles(parseNonFlagArgs(args)), }; const { eslint, prettier, tscOk, tscOutputStream } = await externalLint(opts); diff --git a/src/cli/lint/prettier.ts b/src/cli/lint/prettier.ts index 2299e6def..8bc9b47df 100644 --- a/src/cli/lint/prettier.ts +++ b/src/cli/lint/prettier.ts @@ -11,8 +11,8 @@ import type { Input } from './types'; const LOG_PREFIX = chalk.cyan('Prettier │'); -export const runPrettierInCurrentThread = ({ debug }: Input) => - runPrettier('lint', createLogger(debug, LOG_PREFIX)); +export const runPrettierInCurrentThread = ({ debug, inputFiles }: Input) => + runPrettier('lint', createLogger(debug, LOG_PREFIX), inputFiles); export const runPrettierInWorkerThread = (input: Input) => execWorkerThread( diff --git a/src/cli/lint/types.ts b/src/cli/lint/types.ts index e3ab8f4b8..e1ac09abb 100644 --- a/src/cli/lint/types.ts +++ b/src/cli/lint/types.ts @@ -34,4 +34,11 @@ export interface Input { * Defaults to `true`. */ workerThreads?: boolean; + + /** + * Files specified by the user to be linted/formatted + * + * This may be an empty list, in which case all files in the current working directory will be linted/formatted + */ + inputFiles: string[]; } diff --git a/src/utils/args.test.ts b/src/utils/args.test.ts index eed16f398..40baf9cdd 100644 --- a/src/utils/args.test.ts +++ b/src/utils/args.test.ts @@ -1,6 +1,7 @@ import { hasDebugFlag, hasSerialFlag, + parseNonFlagArgs, parseProcessArgs, parseRunArgs, } from './args'; @@ -137,3 +138,20 @@ describe('parseRunArgs', () => { expect(parseRunArgs(input.split(' '))).toEqual(expected), ); }); + +describe('parseNonFlagArgs', () => { + interface TestCase { + input: string; + result: string[]; + } + + test.each` + input | result + ${'foo.ts bar.ts --flag'} | ${['foo.ts', 'bar.ts']} + ${'foo.ts --flag bar.ts'} | ${['foo.ts', 'bar.ts']} + ${'--flag foo.ts bar.ts'} | ${['foo.ts', 'bar.ts']} + ${'--flag'} | ${[]} + `('$input', ({ input, result }: TestCase) => + expect(parseNonFlagArgs(input.split(' '))).toEqual(result), + ); +}); diff --git a/src/utils/args.ts b/src/utils/args.ts index 3c941a77b..a099740d3 100644 --- a/src/utils/args.ts +++ b/src/utils/args.ts @@ -140,3 +140,8 @@ const parseRunArgsIteration = (state: RunArgs, args: string[]): string[] => { state.script.push(...args.slice(1)); return []; }; + +export const parseNonFlagArgs = (args: string[]): string[] => { + const files = args.filter((arg) => !arg.startsWith('-')); + return files; +};