Skip to content

Commit

Permalink
feat: performance improvement via cache
Browse files Browse the repository at this point in the history
  • Loading branch information
JounQin committed Apr 26, 2021
1 parent f9b7c8d commit ea3acf7
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 58 deletions.
149 changes: 93 additions & 56 deletions packages/eslint-plugin-mdx/src/rules/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import fs from 'fs'
import path from 'path'

import { cosmiconfigSync } from 'cosmiconfig'
import type { CosmiconfigResult } from 'cosmiconfig/dist/types'
import { arrayify } from 'eslint-mdx'
import remarkMdx from 'remark-mdx'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import type { FrozenProcessor } from 'unified'
import unified from 'unified'

import type { RemarkConfig, RemarkPlugin } from './types'
Expand Down Expand Up @@ -39,74 +40,110 @@ export const requirePkg = <T>(
throw error
}

let searchSync: (searchFrom?: string) => CosmiconfigResult
/**
* Given a filepath, get the nearest path that is a regular file.
* The filepath provided by eslint may be a virtual filepath rather than a file
* on disk. This attempts to transform a virtual path into an on-disk path
*/
export const getPhysicalFilename = (filename: string): string => {
try {
if (fs.statSync(filename).isFile()) {
return filename
}
} catch (err) {
// https://github.com/eslint/eslint/issues/11989
if ((err as { code: string }).code === 'ENOTDIR') {
return getPhysicalFilename(path.dirname(filename))
}
}

return filename
}

export const remarkProcessor = unified().use(remarkParse).freeze()

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

// @internal - exported for testing
export const processorCache = new Map<string, FrozenProcessor>()

// eslint-disable-next-line sonarjs/cognitive-complexity
export const getRemarkProcessor = (searchFrom: string, isMdx: boolean) => {
if (!searchSync) {
searchSync = cosmiconfigSync('remark', {
packageProp: 'remarkConfig',
}).search
const initCacheKey = `${String(isMdx)}-${searchFrom}`

let cacheKey = initCacheKey
let cachedProcessor = processorCache.get(cacheKey)

if (cachedProcessor) {
return cachedProcessor
}

let result: Partial<CosmiconfigResult>
const result = explorer.search(searchFrom)

try {
result = searchSync(searchFrom)
} catch (err) {
// https://github.com/eslint/eslint/issues/11989
/* istanbul ignore if */
if (
(err as { code?: string }).code !== 'ENOTDIR' ||
!/[/\\]\d+_[^/\\]*\.[\da-z]+$/i.test(searchFrom)
) {
throw err
}
try {
result = searchSync(path.dirname(searchFrom))
} catch {
/* istanbul ignore next */
throw err
}
cacheKey = result ? `${String(isMdx)}-${result.filepath}` : ''

cachedProcessor = processorCache.get(cacheKey)

if (cachedProcessor) {
return cachedProcessor
}

/* istanbul ignore next */
const { plugins = [], settings } = (result?.config ||
{}) as Partial<RemarkConfig>
if (result) {
/* istanbul ignore next */
const { plugins = [], settings } = (result.config ||
{}) as Partial<RemarkConfig>

// 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 `result` could be null inside `plugins.reduce`
/* istanbul ignore else */
if (plugins.length > 0) {
try {
// eslint-disable-next-line node/no-extraneous-require
plugins.push([require.resolve('remark-lint-file-extension'), false])
} catch {
// just ignore if the package does not exist
// 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 {
// eslint-disable-next-line node/no-extraneous-require
plugins.push([require.resolve('remark-lint-file-extension'), false])
} catch {
// just ignore if the package does not exist
}
}

const initProcessor = remarkProcessor()
.use({ settings })
.use(remarkStringify)

if (isMdx) {
initProcessor.use(remarkMdx)
}
}

const initProcessor = remarkProcessor().use({ settings }).use(remarkStringify)
cachedProcessor = plugins
.reduce((processor, pluginWithSettings) => {
const [plugin, ...pluginSettings] = arrayify(pluginWithSettings) as [
RemarkPlugin,
...unknown[]
]
return processor.use(
/* istanbul ignore next */
typeof plugin === 'string'
? requirePkg(plugin, 'remark', result.filepath)
: plugin,
...pluginSettings,
)
}, initProcessor)
.freeze()
} else {
const initProcessor = remarkProcessor().use(remarkStringify)

if (isMdx) {
initProcessor.use(remarkMdx)
if (isMdx) {
initProcessor.use(remarkMdx)
}

cachedProcessor = initProcessor.freeze()
}

return plugins
.reduce((processor, pluginWithSettings) => {
const [plugin, ...pluginSettings] = arrayify(pluginWithSettings) as [
RemarkPlugin,
...unknown[]
]
return processor.use(
/* istanbul ignore next */
typeof plugin === 'string'
? requirePkg(plugin, 'remark', result.filepath)
: plugin,
...pluginSettings,
)
}, initProcessor)
.freeze()
processorCache
.set(cacheKey, cachedProcessor)
.set(initCacheKey, cachedProcessor)

return cachedProcessor
}
7 changes: 5 additions & 2 deletions packages/eslint-plugin-mdx/src/rules/remark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ParserOptions } from 'eslint-mdx'
import { DEFAULT_EXTENSIONS, MARKDOWN_EXTENSIONS } from 'eslint-mdx'
import vfile from 'vfile'

import { getRemarkProcessor } from './helpers'
import { getPhysicalFilename, getRemarkProcessor } from './helpers'
import type { RemarkLintMessage } from './types'

export const remark: Rule.RuleModule = {
Expand Down Expand Up @@ -37,7 +37,10 @@ export const remark: Rule.RuleModule = {
return
}
const sourceText = sourceCode.getText(node)
const remarkProcessor = getRemarkProcessor(filename, isMdx)
const remarkProcessor = getRemarkProcessor(
getPhysicalFilename(filename),
isMdx,
)
const file = vfile({
path: filename,
contents: sourceText,
Expand Down
Empty file added test/fixtures/dir.mdx/.gitkeep
Empty file.
72 changes: 72 additions & 0 deletions test/remark.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import path from 'path'

import { DEFAULT_PARSER_OPTIONS as parserOptions } from 'eslint-mdx'
// @ts-ignore - processorCache is an internal API
import { processorCache, remark } from 'eslint-plugin-mdx'
import { homedir } from 'os'

import { parser, ruleTester } from './helpers'

const userDir = homedir()

ruleTester.run('remark 1', remark, {
valid: [
{
code: '<header>Header1</header>',
parser,
parserOptions,
filename: 'remark.mdx',
},
{
code: '<header>Header2</header>',
parser,
parserOptions,
filename: path.resolve(__filename, '0-fake.mdx'),
},
{
code: '<header>Header3</header>',
parser,
parserOptions,
filename: path.resolve(__dirname, 'fixtures/dir.mdx'),
},
{
code: '<header>Header4</header>',
parser,
parserOptions,
filename: path.resolve(userDir, '../test.mdx'),
},
{
code: '<header>Header5</header>',
parser,
parserOptions,
// dark hack
get filename() {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
processorCache.clear()
return path.resolve(userDir, '../test.md')
},
},
],
invalid: [
{
code: '<header>Header</header>',
parser,
parserOptions,
filename: path.resolve(__filename, '0_fake_virtual_filename.mdx'),
errors: [
{
message: JSON.stringify({
reason: 'Do not use `_` in a file name',
source: 'remark-lint',
ruleId: 'no-file-name-irregular-characters',
severity: 1,
}),
line: null,
column: 0,
endLine: null,
endColumn: 0,
},
],
},
],
})

0 comments on commit ea3acf7

Please sign in to comment.