From 264c58f7b3ea9107018d330ddf52b4e2f4570058 Mon Sep 17 00:00:00 2001 From: Anthony Fu Date: Fri, 5 Apr 2024 23:37:23 +0200 Subject: [PATCH] feat: improve how configures are resolved (#26) --- app/server/api/payload.json.ts | 5 ++- package.json | 1 + pnpm-lock.yaml | 10 ++--- src/cli.ts | 30 +++++++++++--- src/configs.ts | 72 +++++++++++++++++++++++++++++----- src/server.ts | 6 +-- src/ws.ts | 14 +++---- types.ts | 1 + 8 files changed, 106 insertions(+), 33 deletions(-) diff --git a/app/server/api/payload.json.ts b/app/server/api/payload.json.ts index 5194199..49bbd9d 100644 --- a/app/server/api/payload.json.ts +++ b/app/server/api/payload.json.ts @@ -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() diff --git a/package.json b/package.json index 4cc3eb0..58d59a7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41b602a..783b130 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ dependencies: fast-glob: specifier: ^3.3.2 version: 3.3.2 + find-up: + specifier: ^7.0.0 + version: 7.0.0 get-port-please: specifier: ^3.1.2 version: 3.1.2 @@ -5358,7 +5361,6 @@ packages: locate-path: 7.2.0 path-exists: 5.0.0 unicorn-magic: 0.1.0 - dev: true /flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} @@ -6373,7 +6375,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: p-locate: 6.0.0 - dev: true /lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -7370,7 +7371,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: yocto-queue: 1.0.0 - dev: true /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} @@ -7391,7 +7391,6 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dependencies: p-limit: 4.0.0 - dev: true /p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} @@ -7502,7 +7501,6 @@ packages: /path-exists@5.0.0: resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -8991,7 +8989,6 @@ packages: /unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} - dev: true /unimport@3.7.1(rollup@3.29.4): resolution: {integrity: sha512-V9HpXYfsZye5bPPYUgs0Otn3ODS1mDUciaBlXljI4C2fTwfFpvFZRywmlOu943puN9sncxROMZhsZCjNXEpzEQ==} @@ -9794,7 +9791,6 @@ packages: /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} - dev: true /zhead@2.2.4: resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} diff --git a/src/cli.ts b/src/cli.ts index 774e984..d2be46e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -13,20 +13,31 @@ const cli = cac() cli .command('build', 'Build inspector with current config file for static hosting') - .option('--config ', 'Config file path', { default: process.env.ESLINT_CONFIG }) + .option('--config ', 'Config file path') + .option('--basePath ', 'Base directory for globs to resolve. Default to directory of config file if not provided') .option('--out-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)}`) @@ -35,19 +46,26 @@ cli cli .command('', 'Start dev inspector') - .option('--config ', 'Config file path', { default: process.env.ESLINT_CONFIG }) + .option('--config ', 'Config file path') + .option('--basePath ', 'Base directory for globs to resolve. Default to directory of config file if not provided') .option('--host ', 'Host', { default: process.env.HOST || '127.0.0.1' }) .option('--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) diff --git a/src/configs.ts b/src/configs.ts index c2b5763..29a4dc3 100644 --- a/src/configs.ts +++ b/src/configs.ts @@ -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', @@ -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() const eslintRules = await import(['eslint', 'use-at-your-own-risk'].join('/')).then(r => r.default.builtinRules) @@ -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/**', @@ -89,11 +141,13 @@ export async function readConfig( files, meta: { lastUpdate: Date.now(), + rootPath, configPath, }, } return { + configs: rawConfigs, dependencies, payload, } diff --git a/src/server.ts b/src/server.ts index b90100d..68de26a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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') diff --git a/src/ws.ts b/src/ws.ts index 67dd149..9952bf0 100644 --- a/src/ws.ts +++ b/src/ws.ts @@ -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({ @@ -28,7 +28,7 @@ export async function createWsServer() { const watcher = chokidar.watch([], { ignoreInitial: true, - cwd: process.cwd(), + cwd: options.cwd, disableGlobbing: true, }) @@ -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 }) diff --git a/types.ts b/types.ts index 6e5425b..94dbf35 100644 --- a/types.ts +++ b/types.ts @@ -41,6 +41,7 @@ export interface ResolvedPayload extends Payload { export interface PayloadMeta { wsPort?: number lastUpdate: number + rootPath: string configPath: string }