From 296a319bf7f385d97c6d647e093233552e7d99b3 Mon Sep 17 00:00:00 2001 From: JounQin Date: Wed, 3 May 2023 23:03:24 +0800 Subject: [PATCH] feat: use unified-engine to load ESM configurations (#461) * feat: use unified-engine to load ESM configurations close #436 --- .changeset/quick-dolphins-end.md | 5 ++ packages/eslint-mdx/package.json | 2 +- packages/eslint-mdx/src/helpers.ts | 79 ---------------------- packages/eslint-mdx/src/types.ts | 8 --- packages/eslint-mdx/src/worker.ts | 85 ++++++++++++------------ test/__snapshots__/fixtures.test.ts.snap | 30 +++++++++ test/fixtures/async/test.md | 1 + test/fixtures/esm/.remarkrc.mjs | 5 ++ test/fixtures/esm/test.md | 1 + test/helpers.test.ts | 22 +----- yarn.lock | 2 +- 11 files changed, 87 insertions(+), 153 deletions(-) create mode 100644 .changeset/quick-dolphins-end.md create mode 100644 test/fixtures/async/test.md create mode 100644 test/fixtures/esm/.remarkrc.mjs create mode 100644 test/fixtures/esm/test.md diff --git a/.changeset/quick-dolphins-end.md b/.changeset/quick-dolphins-end.md new file mode 100644 index 00000000..7b8e7d3e --- /dev/null +++ b/.changeset/quick-dolphins-end.md @@ -0,0 +1,5 @@ +--- +"eslint-mdx": minor +--- + +feat: use unified-engine to load ESM configurations diff --git a/packages/eslint-mdx/package.json b/packages/eslint-mdx/package.json index 88b677d1..fa2bef5e 100644 --- a/packages/eslint-mdx/package.json +++ b/packages/eslint-mdx/package.json @@ -32,7 +32,6 @@ "dependencies": { "acorn": "^8.8.2", "acorn-jsx": "^5.3.2", - "cosmiconfig": "^8.1.3", "espree": "^9.5.1", "estree-util-visit": "^1.2.1", "remark-mdx": "^2.3.0", @@ -41,6 +40,7 @@ "synckit": "^0.8.5", "tslib": "^2.5.0", "unified": "^10.1.2", + "unified-engine": "^10.1.0", "unist-util-visit": "^4.1.2", "uvu": "^0.5.6", "vfile": "^5.3.7" diff --git a/packages/eslint-mdx/src/helpers.ts b/packages/eslint-mdx/src/helpers.ts index 60f39c41..e4ac6676 100644 --- a/packages/eslint-mdx/src/helpers.ts +++ b/packages/eslint-mdx/src/helpers.ts @@ -1,7 +1,5 @@ -/* eslint-disable unicorn/no-await-expression-member */ import fs from 'node:fs' import path from 'node:path' -import { pathToFileURL } from 'node:url' import type { Position } from 'acorn' import type { Point } from 'unist' @@ -64,83 +62,6 @@ export const loadEsmModule = (modulePath: URL | string): Promise => modulePath, ) as Promise -/** - * Loads CJS and ESM modules based on extension - * @param modulePath path to the module - * @returns - */ -export const loadModule = async (modulePath: string): Promise => { - const esModulePath = path.isAbsolute(modulePath) - ? pathToFileURL(modulePath) - : modulePath - switch (path.extname(modulePath)) { - /* istanbul ignore next */ - case '.mjs': { - return (await loadEsmModule<{ default: T }>(esModulePath)).default - } - /* istanbul ignore next */ - case '.cjs': { - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return - return require(modulePath) - } - default: { - // The file could be either CommonJS or ESM. - // CommonJS is tried first then ESM if loading fails. - try { - // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unsafe-return - return require(modulePath) - } catch (err) { - const code = (err as { code: string }).code - /* istanbul ignore if */ - if ( - code === 'ERR_REQUIRE_ESM' || - // A pure ESM could have no `exports.require` and then throw the following error, - // related to #427. - code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' - ) { - // Load the ESM configuration file using the TypeScript dynamic import workaround. - // Once TypeScript provides support for keeping the dynamic import this workaround can be - // changed to a direct dynamic import. - return (await loadEsmModule<{ default: T }>(esModulePath)).default - } - - throw err - } - } - } -} - -export const requirePkg = async ( - plugin: string, - prefix: string, - filePath?: string, -): Promise => { - let packages: string[] - if (filePath && /^\.\.?(?:[/\\]|$)/.test(plugin)) { - packages = [path.resolve(path.dirname(filePath), plugin)] - } else { - prefix = prefix.endsWith('-') ? prefix : prefix + '-' - packages = [ - plugin, - /* istanbul ignore next */ - plugin.startsWith('@') - ? plugin.replace('/', '/' + prefix) - : prefix + plugin, - ] - } - let error: Error - for (const pkg of packages) { - try { - return await loadModule(pkg) - } catch (err) { - if (!error) { - error = err as Error - } - } - } - throw error -} - /* istanbul ignore next -- used in worker */ export const getPositionAtFactory = (text: string) => { const lines = text.split('\n') diff --git a/packages/eslint-mdx/src/types.ts b/packages/eslint-mdx/src/types.ts index fca72435..e413eb9f 100644 --- a/packages/eslint-mdx/src/types.ts +++ b/packages/eslint-mdx/src/types.ts @@ -2,7 +2,6 @@ import type { Position } from 'acorn' import type { AST, Linter } from 'eslint' import type { Program } from 'estree' import type { Root } from 'mdast' -import type { Plugin } from 'unified' import type { VFileOptions } from 'vfile' import type { VFileMessage } from 'vfile-message' @@ -23,13 +22,6 @@ export interface NormalPosition { range: [number, number] } -export type RemarkPlugin = Plugin | string - -export interface RemarkConfig { - settings: Record - plugins: Array -} - export interface WorkerOptions { fileOptions: VFileOptions physicalFilename: string diff --git a/packages/eslint-mdx/src/worker.ts b/packages/eslint-mdx/src/worker.ts index 3a8959a6..4417e823 100644 --- a/packages/eslint-mdx/src/worker.ts +++ b/packages/eslint-mdx/src/worker.ts @@ -4,8 +4,6 @@ import path from 'node:path' import { pathToFileURL } from 'node:url' import type { Token, TokenType, tokTypes as _tokTypes } from 'acorn' -import { cosmiconfig } from 'cosmiconfig' -import type { CosmiconfigResult } from 'cosmiconfig/dist/types' import type { AST } from 'eslint' import type { EsprimaToken } from 'espree/lib/token-translator' import type { @@ -26,27 +24,22 @@ import type { import type { Options } from 'micromark-extension-mdx-expression' import type { Root } from 'remark-mdx' import { extractProperties, runAsWorker } from 'synckit' -import type { FrozenProcessor, Plugin } from 'unified' +import type { FrozenProcessor } from 'unified' +import type { Config, Configuration } from 'unified-engine/lib/configuration' import type { Node } from 'unist' import { ok as assert } from 'uvu/assert' import type { VFileMessage } from 'vfile-message' import { - arrayify, loadEsmModule, nextCharOffsetFactory, normalizePosition, prevCharOffsetFactory, - requirePkg, } from './helpers' import { restoreTokens } from './tokens' -import type { - NormalPosition, - RemarkConfig, - RemarkPlugin, - WorkerOptions, - WorkerResult, -} from './types' +import type { NormalPosition, WorkerOptions, WorkerResult } from './types' + +let config: Configuration let acorn: typeof import('acorn') let acornJsx: { @@ -60,12 +53,29 @@ let tt: Record & typeof _tokTypes let TokenTranslator: typeof import('espree/lib/token-translator')['default'] -const explorer = cosmiconfig('remark', { - packageProp: 'remarkConfig', -}) - export const processorCache = new Map() +const getRemarkConfig = async (searchFrom: string) => { + if (!config) { + const { Configuration } = await loadEsmModule< + typeof import('unified-engine/lib/configuration') + >('unified-engine/lib/configuration.js') + config = new Configuration({ + cwd: process.cwd(), + packageField: 'remarkConfig', + pluginPrefix: 'remark', + rcName: '.remarkrc', + detectConfig: true, + }) + } + + return new Promise((resolve, reject) => + config.load(searchFrom, (error, result) => + error ? reject(error) : resolve(result), + ), + ) +} + const getRemarkMdxOptions = (tokens: Token[]): Options => ({ acorn: acornParser, acornOptions: { @@ -83,7 +93,6 @@ export const getRemarkProcessor = async ( searchFrom: string, isMdx: boolean, ignoreRemarkConfig?: boolean, - // eslint-disable-next-line sonarjs/cognitive-complexity ) => { const initCacheKey = `${String(isMdx)}-${searchFrom}` @@ -93,12 +102,10 @@ export const getRemarkProcessor = async ( return cachedProcessor } - const result: CosmiconfigResult = ignoreRemarkConfig - ? null - : await explorer.search(searchFrom) + const result = ignoreRemarkConfig ? null : await getRemarkConfig(searchFrom) - const cacheKey = result - ? `${String(isMdx)}-${result.filepath}` + const cacheKey = result?.filePath + ? `${String(isMdx)}-${result.filePath}` : String(isMdx) cachedProcessor = processorCache.get(cacheKey) @@ -120,11 +127,8 @@ export const getRemarkProcessor = async ( const remarkProcessor = unified().use(remarkParse).freeze() - if (result) { - /* istanbul ignore next */ - const { plugins = [], settings } = - // type-coverage:ignore-next-line -- cosmiconfig's typings issue - (result.config || {}) as Partial + if (result?.filePath) { + const { plugins, settings } = result // disable this rule automatically since we already have a parser option `extensions` // only disable this plugin if there are at least one plugin enabled @@ -132,7 +136,14 @@ export const getRemarkProcessor = async ( /* istanbul ignore else */ if (plugins.length > 0) { try { - plugins.push([await requirePkg('lint-file-extension', 'remark'), false]) + plugins.push([ + ( + await loadEsmModule( + 'remark-lint-file-extension', + ) + ).default, + false, + ]) } catch { // just ignore if the package does not exist } @@ -146,21 +157,9 @@ export const getRemarkProcessor = async ( initProcessor.use(remarkMdx, getRemarkMdxOptions(sharedTokens)) } - cachedProcessor = ( - await plugins.reduce(async (processor, pluginWithSettings) => { - const [plugin, ...pluginSettings] = arrayify(pluginWithSettings) as [ - RemarkPlugin, - ...unknown[], - ] - return (await processor).use( - /* istanbul ignore next */ - typeof plugin === 'string' - ? await requirePkg(plugin, 'remark', result.filepath) - : plugin, - ...pluginSettings, - ) - }, Promise.resolve(initProcessor)) - ).freeze() + cachedProcessor = plugins + .reduce((processor, plugin) => processor.use(...plugin), initProcessor) + .freeze() } else { const initProcessor = remarkProcessor().use(remarkStringify) diff --git a/test/__snapshots__/fixtures.test.ts.snap b/test/__snapshots__/fixtures.test.ts.snap index af025392..906a2b2b 100644 --- a/test/__snapshots__/fixtures.test.ts.snap +++ b/test/__snapshots__/fixtures.test.ts.snap @@ -1145,6 +1145,36 @@ exports[`fixtures should match all snapshots: remark.md 1`] = `[]`; exports[`fixtures should match all snapshots: remark.mdx 1`] = `[]`; +exports[`fixtures should match all snapshots: test.md 1`] = ` +[ + { + "column": 1, + "endColumn": 15, + "endLine": 1, + "line": 1, + "message": "Link to unknown file: \`test\`", + "nodeType": "Program", + "ruleId": "remark-validate-links-missing-file", + "severity": 1, + }, +] +`; + +exports[`fixtures should match all snapshots: test.md 2`] = ` +[ + { + "column": 1, + "endColumn": 15, + "endLine": 1, + "line": 1, + "message": "Link to unknown file: \`test\`", + "nodeType": "Program", + "ruleId": "remark-validate-links-missing-file", + "severity": 1, + }, +] +`; + exports[`fixtures should match all snapshots: unicorn.jsx 1`] = ` [ { diff --git a/test/fixtures/async/test.md b/test/fixtures/async/test.md new file mode 100644 index 00000000..f06058c7 --- /dev/null +++ b/test/fixtures/async/test.md @@ -0,0 +1 @@ +[test](./test) diff --git a/test/fixtures/esm/.remarkrc.mjs b/test/fixtures/esm/.remarkrc.mjs new file mode 100644 index 00000000..1c480f1f --- /dev/null +++ b/test/fixtures/esm/.remarkrc.mjs @@ -0,0 +1,5 @@ +import validateLinks from 'remark-validate-links' + +export default { + plugins: [[validateLinks, 2]], +} diff --git a/test/fixtures/esm/test.md b/test/fixtures/esm/test.md new file mode 100644 index 00000000..f06058c7 --- /dev/null +++ b/test/fixtures/esm/test.md @@ -0,0 +1 @@ +[test](./test) diff --git a/test/helpers.test.ts b/test/helpers.test.ts index 34447e17..c4772429 100644 --- a/test/helpers.test.ts +++ b/test/helpers.test.ts @@ -1,6 +1,4 @@ -import path from 'node:path' - -import { arrayify, requirePkg } from 'eslint-mdx' +import { arrayify } from 'eslint-mdx' import { getGlobals, getShortLang } from 'eslint-plugin-mdx' describe('Helpers', () => { @@ -25,22 +23,4 @@ describe('Helpers', () => { b: false, }) }) - - it('should resolve package correctly', async () => { - expect(await requirePkg('@1stg/config', 'commitlint')).toBeDefined() - // expect(await requirePkg('lint', 'remark')).toBeDefined() - // expect(await requirePkg('remark-parse', 'non-existed')).toBeDefined() - expect( - await requirePkg( - './.eslintrc', - 'non-existed', - path.resolve('package.json'), - ), - ).toBeDefined() - }) - - it('should throw on non existed package', () => - expect(requirePkg('@1stg/x-config', 'unexpected-')).rejects.toThrow( - "Cannot find module '@1stg/x-config'", - )) }) diff --git a/yarn.lock b/yarn.lock index bbf60da1..28700ed9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10871,7 +10871,7 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -unified-engine@^10.0.1: +unified-engine@^10.0.1, unified-engine@^10.1.0: version "10.1.0" resolved "https://registry.yarnpkg.com/unified-engine/-/unified-engine-10.1.0.tgz#6899f00d1f53ee9af94f7abd0ec21242aae3f56c" integrity sha512-5+JDIs4hqKfHnJcVCxTid1yBoI/++FfF/1PFdSMpaftZZZY+qg2JFruRbf7PaIwa9KgLotXQV3gSjtY0IdcFGQ==