Skip to content

Commit

Permalink
feat: improved errors when config can not be found (#85)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <[email protected]>
  • Loading branch information
voxpelli and antfu authored Aug 19, 2024
1 parent 7e66113 commit 748590e
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 17 deletions.
25 changes: 19 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import process from 'node:process'
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs'
import { relative, resolve } from 'node:path'

import open from 'open'
import { getPort } from 'get-port-please'
import cac from 'cac'
Expand All @@ -11,6 +12,7 @@ import { createHostServer } from './server'
import { distDir } from './dirs'
import { readConfig } from './configs'
import { MARK_CHECK, MARK_INFO } from './constants'
import { ConfigInspectorError } from './errors'

const cli = cac(
'eslint-config-inspector',
Expand All @@ -33,12 +35,23 @@ cli

const cwd = process.cwd()
const outDir = resolve(cwd, options.outDir)
const configs = await readConfig({
cwd,
userConfigPath: options.config,
userBasePath: options.basePath,
globMatchedFiles: options.files,
})

let configs
try {
configs = await readConfig({
cwd,
userConfigPath: options.config,
userBasePath: options.basePath,
globMatchedFiles: options.files,
})
}
catch (error) {
if (error instanceof ConfigInspectorError) {
error.prettyPrint()
process.exit(1)
}
throw error
}

let baseURL = options.base
if (!baseURL.endsWith('/'))
Expand Down
43 changes: 34 additions & 9 deletions src/configs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dirname, relative, resolve } from 'node:path'
import { basename, dirname, relative, resolve } from 'node:path'
import process from 'node:process'
import { ConfigArray } from '@eslint/config-array'
import { configArrayFindFiles } from '@voxpelli/config-array-find-files'
Expand All @@ -8,7 +8,8 @@ import c from 'picocolors'
import { resolve as resolveModule } from 'mlly'
import type { FlatConfigItem, MatchedFile, Payload, RuleInfo } from '../shared/types'
import { isIgnoreOnlyConfig, matchFile } from '../shared/configs'
import { MARK_CHECK, MARK_INFO, configFilenames } from './constants'
import { MARK_CHECK, MARK_INFO, configFilenames, legacyConfigFilenames } from './constants'
import { ConfigPathError, ConfigPathLegacyError } from './errors'

export interface ReadConfigOptions extends ResolveConfigPathOptions {
/**
Expand Down Expand Up @@ -55,24 +56,46 @@ export async function resolveConfigPath(options: ResolveConfigPathOptions) {
if (userBasePath)
userBasePath = resolve(cwd, userBasePath)

const configPath = userConfigPath
? resolve(cwd, userConfigPath)
: await findUp(configFilenames, { cwd: userBasePath || cwd })
const lookupBasePath = userBasePath || cwd

if (!configPath)
throw new Error('Cannot find ESLint config file')
let configPath = userConfigPath && resolve(cwd, userConfigPath)

if (!configPath) {
configPath = await findUp(configFilenames, { cwd: lookupBasePath })
}

if (!configPath) {
const legacyConfigPath = await findUp(legacyConfigFilenames, { cwd: lookupBasePath })

throw legacyConfigPath
? new ConfigPathLegacyError(
`${relative(cwd, dirname(legacyConfigPath))}/`,
basename(legacyConfigPath),
)
: new ConfigPathError(
`${relative(cwd, lookupBasePath)}/`,
configFilenames,
)
}

const basePath = userBasePath || (
userConfigPath
? cwd // When user explicit provide config path, use current working directory as root
: dirname(configPath) // Otherwise, use config file's directory as root
)

return {
basePath,
configPath,
}
}

export interface ESLintConfig {
configs: FlatConfigItem[]
payload: Payload
dependencies: string[]
}

/**
* Search and read the ESLint config file, processed into inspector payload with module dependencies
*
Expand All @@ -83,13 +106,15 @@ export async function resolveConfigPath(options: ResolveConfigPathOptions) {
*/
export async function readConfig(
options: ReadConfigOptions,
): Promise<{ configs: FlatConfigItem[], payload: Payload, dependencies: string[] }> {
): Promise<ESLintConfig> {
const {
chdir = true,
globMatchedFiles: globFiles = true,
} = options

const { basePath, configPath } = await resolveConfigPath(options)
const resolvedConfigPath = await resolveConfigPath(options)

const { basePath, configPath } = resolvedConfigPath
if (chdir && basePath !== process.cwd())
process.chdir(basePath)

Expand Down
9 changes: 9 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,14 @@ export const configFilenames = [
'eslint.config.cts',
]

export const legacyConfigFilenames = [
'.eslintrc.js',
'.eslintrc.cjs',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.json',
]

export const MARK_CHECK = c.green('✔')
export const MARK_INFO = c.blue('ℹ')
export const MARK_ERROR = c.red('✖')
49 changes: 49 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import c from 'picocolors'
import { MARK_ERROR } from './constants'

export class ConfigInspectorError extends Error {
prettyPrint() {
console.error(MARK_ERROR, this.message)
}
}

export class ConfigPathError extends ConfigInspectorError {
override name = 'ConfigPathError' as const

constructor(
public basePath: string,
public configFilenames: string[],
) {
super('Cannot find ESLint config file')
}

override prettyPrint() {
console.error(MARK_ERROR, this.message, c.dim(`
Looked in ${c.underline(this.basePath)} and parent folders for:
* ${this.configFilenames.join('\n * ')}`,
))
}
}

export class ConfigPathLegacyError extends ConfigInspectorError {
override name = 'ConfigPathLegacyError' as const

constructor(
public basePath: string,
public configFilename: string,
) {
super('Found ESLint legacy config file')
}

override prettyPrint() {
console.error(MARK_ERROR, this.message, c.dim(`
Encountered unsupported legacy config ${c.underline(this.configFilename)} in ${c.underline(this.basePath)}
\`@eslint/config-inspector\` only works with the new flat config format:
https://eslint.org/docs/latest/use/configure/configuration-files-new`,
))
}
}
27 changes: 25 additions & 2 deletions src/ws.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import process from 'node:process'

import chokidar from 'chokidar'
import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws'
import { getPort } from 'get-port-please'
import type { ReadConfigOptions } from './configs'
import { readConfig, resolveConfigPath } from './configs'
import { MARK_CHECK } from './constants'
import { ConfigInspectorError } from './errors'
import type { Payload } from '~~/shared/types'

const readErrorWarning = `Failed to load \`eslint.config.js\`.
Expand All @@ -27,7 +30,22 @@ export async function createWsServer(options: CreateWsServerOptions) {
ws.on('close', () => wsClients.delete(ws))
})

const { basePath } = await resolveConfigPath(options)
let resolvedConfigPath: Awaited<ReturnType<typeof resolveConfigPath>>
try {
resolvedConfigPath = await resolveConfigPath(options)
}
catch (e) {
if (e instanceof ConfigInspectorError) {
e.prettyPrint()
process.exit(1)
}
else {
throw e
}
}

const { basePath } = resolvedConfigPath

const watcher = chokidar.watch([], {
ignoreInitial: true,
cwd: basePath,
Expand Down Expand Up @@ -61,7 +79,12 @@ export async function createWsServer(options: CreateWsServerOptions) {
}
catch (e) {
console.error(readErrorWarning)
console.error(e)
if (e instanceof ConfigInspectorError) {
e.prettyPrint()
}
else {
console.error(e)
}
return {
message: readErrorWarning,
error: String(e),
Expand Down

0 comments on commit 748590e

Please sign in to comment.