Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improved errors when config can not be found #85

Merged
merged 2 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading