UNPKG

@nuxt/eslint

Version:

Generate ESLint config from current Nuxt settings

358 lines (350 loc) 10.9 kB
import { join as join$1, resolve as resolve$1, dirname as dirname$1 } from 'node:path'; import fs from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { resolvePath } from 'mlly'; import { join, dirname, relative, resolve } from 'pathe'; import { getPort } from 'get-port-please'; import { logger, createResolver } from '@nuxt/kit'; import { builtinModules } from 'node:module'; import { stringifyImports } from 'unimport'; function createAddonGlobals(nuxt) { let unimport; let nitroUnimport; nuxt.hook("imports:context", (context) => { unimport = context; }); nuxt.hook("nitro:init", (nitro) => { nitroUnimport = nitro.unimport; }); return { name: "nuxt:eslint:import-globals", async getConfigs() { const imports = [ ...await unimport?.getImports() || [], ...await nitroUnimport?.getImports() || [] ].sort(); return { configs: [ [ "// Set globals from imports registry", "{", ` name: 'nuxt/import-globals',`, " languageOptions: {", ` globals: Object.fromEntries(${JSON.stringify(imports.map((i) => i.as || i.name))}.map(i => [i, 'readonly'])),`, ` },`, "}" ].join("\n") ] }; } }; } async function setupDevToolsIntegration(options, nuxt) { const { enabled = "lazy", port } = (typeof options.config !== "boolean" ? options.config || {} : {}).devtools || {}; if (enabled === false) return; let viewerProcess; let viewerPort; let viewerUrl; let started = false; async function start() { if (started) return; started = true; const { startSubprocess } = await import('@nuxt/devtools-kit'); const inspectorBinPath = join( dirname(await resolvePath( "@eslint/config-inspector/package.json", { url: dirname(fileURLToPath(import.meta.url)) } )), "bin.mjs" ); viewerPort = port || await getPort({ portRange: [8123, 1e4], random: true }); viewerProcess = startSubprocess( { command: "node", args: [inspectorBinPath, "--no-open"], cwd: nuxt.options.rootDir, env: { PORT: viewerPort.toString() } }, { id: "eslint-config-inspector", name: "ESLint Config Viewer" }, nuxt ); nuxt.callHook("devtools:customTabs:refresh"); const url = `http://localhost:${viewerPort}`; for (let i = 0; i < 100; i++) { if (await fetch(url).then((r) => r.ok).catch(() => false)) break; await new Promise((resolve) => setTimeout(resolve, 500)); } await new Promise((resolve) => setTimeout(resolve, 2e3)); viewerUrl = url; } nuxt.hook("devtools:customTabs", (tabs) => { tabs.push({ name: "eslint-config", title: "ESLint Config", icon: "https://raw.githubusercontent.com/eslint/config-inspector/main/app/public/favicon.svg", view: viewerUrl ? { type: "iframe", src: viewerUrl } : { type: "launch", description: "Start ESLint config inspector to analyze the local ESLint configs", actions: [ { label: "Launch", pending: !!viewerProcess, handle: start } ] } }); }); if (enabled === true) start(); } async function initRootESLintConfig(nuxt, generateConfigPath) { const { findUp } = await import('find-up'); const hasFlatConfig = await findUp( [ "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs", "eslint.config.ts", "eslint.config.mts", "eslint.config.cts" ], { cwd: nuxt.options.rootDir } ); if (hasFlatConfig) return; const targetPath = join(nuxt.options.rootDir, "eslint.config.mjs"); let relativeDistPath = relative(nuxt.options.rootDir, generateConfigPath); if (!relativeDistPath.startsWith("./") && !relativeDistPath.startsWith("../")) relativeDistPath = "./" + relativeDistPath; await fs.writeFile( targetPath, [ "// @ts-check", `import withNuxt from '${relativeDistPath}'`, "", "export default withNuxt(", " // Your custom configs here", ")", "" ].join("\n"), "utf-8" ); logger.success(`ESLint config file created at ${targetPath}`); logger.info(`If you have .eslintrc or .eslintignore files, you might want to migrate them to the new config file`); } function getDirs(nuxt, options) { const rootDir = typeof options.config === "object" && options.config.rootDir || nuxt.options.rootDir; const dirs = { pages: [], composables: [], components: [], componentsPrefixed: [], layouts: [], plugins: [], middleware: [], modules: [], servers: [], root: [], src: [] }; for (const layer of nuxt.options._layers) { const r = (t) => relative(rootDir, resolve(layer.config.srcDir, t.replace(/^~[/\\]/, ""))); dirs.src.push(r("")); dirs.pages.push(r(nuxt.options.dir.pages || "pages")); dirs.layouts.push(r(nuxt.options.dir.layouts || "layouts")); dirs.plugins.push(r(nuxt.options.dir.plugins || "plugins")); dirs.middleware.push(r(nuxt.options.dir.middleware || "middleware")); dirs.modules.push(r(nuxt.options.dir.modules || "modules")); dirs.composables.push(r("composables")); dirs.composables.push(r("utils")); for (const dir of layer.config.imports?.dirs ?? []) { if (dir) dirs.composables.push(r(dir)); } if (layer.config.components && layer.config.components !== true) { const options2 = Array.isArray(layer.config.components) ? { dirs: layer.config.components } : layer.config.components; for (const dir of options2.dirs || []) { if (typeof dir === "string") dirs.components.push(r(dir)); else if (dir && "path" in dir && typeof dir.path === "string") { dirs.components.push(r(dir.path)); if (dir.prefix) dirs.componentsPrefixed.push(r(dir.path)); } } } else { dirs.components.push(r("components")); } } return dirs; } const r = createResolver(import.meta.url); async function generateESLintConfig(options, nuxt, addons) { const importLines = []; const configItems = []; const config = { standalone: true, ...typeof options.config !== "boolean" ? options.config || {} : {} }; let { configFile = join$1(nuxt.options.buildDir, "eslint.config.mjs") } = config; configFile = resolve$1(nuxt.options.rootDir, configFile); const configDir = dirname$1(configFile); importLines.push( { from: "eslint-typegen", name: "default", as: "typegen" }, { from: "@nuxt/eslint-config/flat", name: "createConfigForNuxt" }, { from: "@nuxt/eslint-config/flat", name: "defineFlatConfigs" }, { from: "@nuxt/eslint-config/flat", name: "resolveOptions" }, { from: "url", name: "fileURLToPath" } ); const dirs = getDirs(nuxt, options) || {}; for (const addon of addons) { const resolved = await addon.getConfigs(); if (resolved?.imports) importLines.push(...resolved.imports); if (resolved?.configs) configItems.push(...resolved.configs); } function relativeWithDot(path) { const r2 = relative(configDir, path); return r2.startsWith(".") ? r2 : "./" + r2; } const imports = await Promise.all(importLines.map(async (line) => { return { ...line, from: line.from.match(/^\w+:/) || builtinModules.includes(line.from) ? line.from : relativeWithDot(await r.resolvePath(line.from)) }; })); const code = [ "// ESLint config generated by Nuxt", '/// <reference path="./eslint-typegen.d.ts" />', "/* eslint-disable */", "// @ts-nocheck", "", stringifyImports(imports, false), "", "const r = (...args) => fileURLToPath(new URL(...args, import.meta.url))", "", "export { defineFlatConfigs }", "", `export const options = resolveOptions({`, ` features: ${JSON.stringify(config, null, 2)},`, ` dirs: {`, ...Object.entries(dirs).map(([key, value]) => { return ` ${key}: [${value.map( (v) => key === "root" ? `r(${JSON.stringify(relativeWithDot(v))})` : JSON.stringify(v) ).join(", ")}],`; }), `}`, `})`, "", `export const configs = createConfigForNuxt(options)`, ...configItems.length ? [ "", `configs.append(`, configItems.join(",\n\n"), `)`, "" ] : [], "export function withNuxt(...customs) {", " return configs", " .clone()", " .append(...customs)", ' .onResolved(configs => typegen(configs, { dtsPath: r("./eslint-typegen.d.ts"), augmentFlatConfigUtils: true }))', "}", "", "export default withNuxt" ].join("\n"); const [ pathToFlatConfigUtils, pathToESLintConfigFlat ] = await Promise.all([ r.resolvePath("eslint-flat-config-utils").then((r2) => relativeWithDot(r2)), r.resolvePath("@nuxt/eslint-config/flat").then((r2) => relativeWithDot(r2)) ]); const codeDts = [ "import type { FlatConfigComposer } from " + JSON.stringify(pathToFlatConfigUtils), "import { defineFlatConfigs } from " + JSON.stringify(pathToESLintConfigFlat), "import type { NuxtESLintConfigOptionsResolved } from " + JSON.stringify(pathToESLintConfigFlat), "", "declare const configs: FlatConfigComposer", "declare const options: NuxtESLintConfigOptionsResolved", "declare const withNuxt: typeof defineFlatConfigs", "export default withNuxt", "export { withNuxt, defineFlatConfigs, configs, options }" ].join("\n"); return { code, codeDts, configFile }; } async function setupConfigGen(options, nuxt) { const { autoInit = true } = typeof options.config !== "boolean" ? options.config || {} : {}; const defaultAddons = [ createAddonGlobals(nuxt) ]; nuxt.hook("prepare:types", ({ declarations }) => { declarations.push('/// <reference path="./eslint-typegen.d.ts" />'); }); let _configFile = void 0; async function writeConfigFile() { const addons = [ ...defaultAddons ]; await nuxt.callHook("eslint:config:addons", addons); const { code, codeDts, configFile } = await generateESLintConfig(options, nuxt, addons); await fs.mkdir(dirname$1(configFile), { recursive: true }); await fs.writeFile(configFile, code, "utf-8"); await fs.writeFile(configFile.replace(/\.mjs$/, ".d.mts"), codeDts, "utf-8"); _configFile = configFile; } setupDevToolsIntegration(options, nuxt); await writeConfigFile(); nuxt.hook("builder:generateApp", () => { writeConfigFile(); }); if (autoInit) { await initRootESLintConfig(nuxt, _configFile); } } export { setupConfigGen };