Skip to content

Commit

Permalink
feat: use unified-engine to load ESM configurations (#461)
Browse files Browse the repository at this point in the history
* feat: use unified-engine to load ESM configurations

close #436
  • Loading branch information
JounQin authored May 3, 2023
1 parent 9a71318 commit 296a319
Show file tree
Hide file tree
Showing 11 changed files with 87 additions and 153 deletions.
5 changes: 5 additions & 0 deletions .changeset/quick-dolphins-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eslint-mdx": minor
---

feat: use unified-engine to load ESM configurations
2 changes: 1 addition & 1 deletion packages/eslint-mdx/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
79 changes: 0 additions & 79 deletions packages/eslint-mdx/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -64,83 +62,6 @@ export const loadEsmModule = <T>(modulePath: URL | string): Promise<T> =>
modulePath,
) as Promise<T>

/**
* Loads CJS and ESM modules based on extension
* @param modulePath path to the module
* @returns
*/
export const loadModule = async <T>(modulePath: string): Promise<T> => {
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 <T>(
plugin: string,
prefix: string,
filePath?: string,
): Promise<T> => {
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<T>(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')
Expand Down
8 changes: 0 additions & 8 deletions packages/eslint-mdx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -23,13 +22,6 @@ export interface NormalPosition {
range: [number, number]
}

export type RemarkPlugin = Plugin | string

export interface RemarkConfig {
settings: Record<string, string>
plugins: Array<RemarkPlugin | [RemarkPlugin, ...unknown[]]>
}

export interface WorkerOptions {
fileOptions: VFileOptions
physicalFilename: string
Expand Down
85 changes: 42 additions & 43 deletions packages/eslint-mdx/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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: {
Expand All @@ -60,12 +53,29 @@ let tt: Record<string, TokenType> & typeof _tokTypes

let TokenTranslator: typeof import('espree/lib/token-translator')['default']

const explorer = cosmiconfig('remark', {
packageProp: 'remarkConfig',
})

export const processorCache = new Map<string, FrozenProcessor>()

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<Config>((resolve, reject) =>
config.load(searchFrom, (error, result) =>
error ? reject(error) : resolve(result),
),
)
}

const getRemarkMdxOptions = (tokens: Token[]): Options => ({
acorn: acornParser,
acornOptions: {
Expand All @@ -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}`

Expand All @@ -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)
Expand All @@ -120,19 +127,23 @@ 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<RemarkConfig>
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
// otherwise it is redundant
/* istanbul ignore else */
if (plugins.length > 0) {
try {
plugins.push([await requirePkg('lint-file-extension', 'remark'), false])
plugins.push([
(
await loadEsmModule<typeof import('remark-lint-file-extension')>(
'remark-lint-file-extension',
)
).default,
false,
])
} catch {
// just ignore if the package does not exist
}
Expand All @@ -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>(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)

Expand Down
30 changes: 30 additions & 0 deletions test/__snapshots__/fixtures.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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`] = `
[
{
Expand Down
1 change: 1 addition & 0 deletions test/fixtures/async/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[test](./test)
5 changes: 5 additions & 0 deletions test/fixtures/esm/.remarkrc.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import validateLinks from 'remark-validate-links'

export default {
plugins: [[validateLinks, 2]],
}
1 change: 1 addition & 0 deletions test/fixtures/esm/test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[test](./test)
22 changes: 1 addition & 21 deletions test/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -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'",
))
})
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==
Expand Down

0 comments on commit 296a319

Please sign in to comment.