Skip to content

Commit

Permalink
feat: improve how configures are resolved (#26)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu authored Apr 5, 2024
1 parent fd35b20 commit 264c58f
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 33 deletions.
5 changes: 4 additions & 1 deletion app/server/api/payload.json.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import process from 'node:process'
import { createWsServer } from '~~/src/ws'

export default lazyEventHandler(async () => {
const ws = await createWsServer()
const ws = await createWsServer({
cwd: process.cwd(),
})

return defineEventHandler(async () => {
return await ws.getData()
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"connect": "^3.7.0",
"esbuild": "^0.20.2",
"fast-glob": "^3.3.2",
"find-up": "^7.0.0",
"get-port-please": "^3.1.2",
"minimatch": "^9.0.4",
"open": "^10.1.0",
Expand Down
10 changes: 3 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 24 additions & 6 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,31 @@ const cli = cac()

cli
.command('build', 'Build inspector with current config file for static hosting')
.option('--config <configFile>', 'Config file path', { default: process.env.ESLINT_CONFIG })
.option('--config <configFile>', 'Config file path')
.option('--basePath <basePath>', 'Base directory for globs to resolve. Default to directory of config file if not provided')
.option('--out-dir <dir>', 'Output directory', { default: '.eslint-config-inspector' })
.action(async (options) => {
console.log('Building static ESLint config inspector...')

if (process.env.ESLINT_CONFIG)
options.config ||= process.env.ESLINT_CONFIG

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

if (existsSync(outDir))
await fs.rm(outDir, { recursive: true })
await fs.mkdir(outDir, { recursive: true })
await fs.cp(distDir, outDir, { recursive: true })
await fs.mkdir(resolve(outDir, 'api'), { recursive: true })

configs.payload.meta.configPath = ''
configs.payload.meta.rootPath = ''
await fs.writeFile(resolve(outDir, 'api/payload.json'), JSON.stringify(configs.payload, null, 2), 'utf-8')

console.log(`Built to ${relative(cwd, outDir)}`)
Expand All @@ -35,19 +46,26 @@ cli

cli
.command('', 'Start dev inspector')
.option('--config <configFile>', 'Config file path', { default: process.env.ESLINT_CONFIG })
.option('--config <configFile>', 'Config file path')
.option('--basePath <basePath>', 'Base directory for globs to resolve. Default to directory of config file if not provided')
.option('--host <host>', 'Host', { default: process.env.HOST || '127.0.0.1' })
.option('--port <port>', 'Port', { default: process.env.PORT || 7777 })
.option('--open', 'Open browser', { default: true })
.action(async (options) => {
const host = options.host
const port = await getPort({ port: options.port })
if (options.config)
process.env.ESLINT_CONFIG = options.config

if (process.env.ESLINT_CONFIG)
options.config ||= process.env.ESLINT_CONFIG

console.log(`Starting ESLint config inspector at http://${host}:${port}`)

const server = await createHostServer()
const cwd = process.cwd()
const server = await createHostServer({
cwd,
userConfigPath: options.config,
userBasePath: options.basePath,
})

server.listen(port, host)

Expand Down
72 changes: 63 additions & 9 deletions src/configs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import fs from 'node:fs'
import { dirname, resolve } from 'node:path'
import process from 'node:process'
import { resolve } from 'node:path'
import { bundleRequire } from 'bundle-require'
import type { Linter } from 'eslint'
import fg from 'fast-glob'
import type { Payload, RuleInfo } from '../types'
import { findUp } from 'find-up'
import type { FlatESLintConfigItem, Payload, RuleInfo } from '../types'

const configFilenames = [
'eslint.config.js',
Expand All @@ -15,17 +15,69 @@ const configFilenames = [
'eslint.config.cts',
]

export interface ReadConfigOptions {
/**
* Current working directory
*/
cwd: string
/**
* Override config file path.
* When not provided, will try to find config file in current working directory or userBasePath if provided.
*/
userConfigPath?: string
/**
* Override base path. When not provided, will use directory of discovered config file.
*/
userBasePath?: string
/**
* Change current working directory to basePath
* @default true
*/
chdir?: boolean
}

/**
* Search and read the ESLint config file, processed into inspector payload with module dependencies
*
* Accpet an options object to specify the working directory path and overrides.
*
* It uses `bundle-requires` load the config file and find it's dependencies.
* It always get the latest version of the config file (no ESM cache).
*/
export async function readConfig(
cwd: string,
configPathOverride = process.env.ESLINT_CONFIG,
): Promise<{ payload: Payload, dependencies: string[] }> {
const configPath = resolve(cwd, configPathOverride || configFilenames.find(i => fs.existsSync(resolve(cwd, i))) || configFilenames[0])
{
cwd,
userConfigPath,
userBasePath,
chdir = true,
}: ReadConfigOptions,
): Promise<{ configs: FlatESLintConfigItem[], payload: Payload, dependencies: string[] }> {
if (userBasePath)
userBasePath = resolve(cwd, userBasePath)

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

if (!configPath)
throw new Error('Cannot find ESLint config file')

const rootPath = 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
)

if (chdir && rootPath !== process.cwd())
process.chdir(rootPath)

console.log('Reading ESLint configs from', configPath)
const { mod, dependencies } = await bundleRequire({
filepath: configPath,
cwd: rootPath,
})

const rawConfigs = await (mod.default ?? mod) as Linter.FlatConfig[]
const rawConfigs = await (mod.default ?? mod) as FlatESLintConfigItem[]

const rulesMap = new Map<string, RuleInfo>()
const eslintRules = await import(['eslint', 'use-at-your-own-risk'].join('/')).then(r => r.default.builtinRules)
Expand Down Expand Up @@ -71,7 +123,7 @@ export async function readConfig(
const files = await fg(
configs.flatMap(i => i.files ?? []).filter(i => typeof i === 'string') as string[],
{
cwd,
cwd: rootPath,
onlyFiles: true,
ignore: [
'**/node_modules/**',
Expand All @@ -89,11 +141,13 @@ export async function readConfig(
files,
meta: {
lastUpdate: Date.now(),
rootPath,
configPath,
},
}

return {
configs: rawConfigs,
dependencies,
payload,
}
Expand Down
6 changes: 3 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createServer } from 'node:http'
import connect from 'connect'
import sirv from 'sirv'
import { createWsServer } from './ws'
import { type CreateWsServerOptions, createWsServer } from './ws'
import { distDir } from './dirs'

export async function createHostServer() {
export async function createHostServer(options: CreateWsServerOptions) {
const app = connect()

const ws = await createWsServer()
const ws = await createWsServer(options)

app.use('/api/payload.json', async (_req, res) => {
res.setHeader('Content-Type', 'application/json')
Expand Down
14 changes: 7 additions & 7 deletions src/ws.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import process from 'node:process'
import { relative } from 'node:path'
import chokidar from 'chokidar'
import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws'
import { getPort } from 'get-port-please'
import { readConfig } from './configs'
import { type ReadConfigOptions, readConfig } from './configs'
import type { Payload } from '~~/types'

const readErrorWarning = `Failed to load \`eslint.config.js\`.
Note that \`@eslint/config-inspector\` only works with the flat config format:
https://eslint.org/docs/latest/use/configure/configuration-files-new`

export async function createWsServer() {
const cwd = process.cwd()
export interface CreateWsServerOptions extends ReadConfigOptions {}

export async function createWsServer(options: CreateWsServerOptions) {
let payload: Payload | undefined
const port = await getPort({ port: 7811, random: true })
const wss = new WebSocketServer({
Expand All @@ -28,7 +28,7 @@ export async function createWsServer() {

const watcher = chokidar.watch([], {
ignoreInitial: true,
cwd: process.cwd(),
cwd: options.cwd,
disableGlobbing: true,
})

Expand All @@ -46,11 +46,11 @@ export async function createWsServer() {
async function getData() {
try {
if (!payload) {
return await readConfig(cwd)
return await readConfig(options)
.then((res) => {
const _payload = payload = res.payload
_payload.meta.wsPort = port
console.log(`Read ESLint config from \`${relative(cwd, _payload.meta.configPath)}\` with`, _payload.configs.length, 'configs and', Object.keys(_payload.rules).length, 'rules')
console.log(`Read ESLint config from \`${relative(options.cwd, _payload.meta.configPath)}\` with`, _payload.configs.length, 'configs and', Object.keys(_payload.rules).length, 'rules')
watcher.add(res.dependencies)
return payload
})
Expand Down
1 change: 1 addition & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface ResolvedPayload extends Payload {
export interface PayloadMeta {
wsPort?: number
lastUpdate: number
rootPath: string
configPath: string
}

Expand Down

0 comments on commit 264c58f

Please sign in to comment.