|
| 1 | +import fs from 'fs' |
1 | 2 | import path from 'path'
|
2 | 3 |
|
3 | 4 | import { cosmiconfigSync } from 'cosmiconfig'
|
4 |
| -import type { CosmiconfigResult } from 'cosmiconfig/dist/types' |
5 | 5 | import { arrayify } from 'eslint-mdx'
|
6 | 6 | import remarkMdx from 'remark-mdx'
|
7 | 7 | import remarkParse from 'remark-parse'
|
8 | 8 | import remarkStringify from 'remark-stringify'
|
| 9 | +import type { FrozenProcessor } from 'unified' |
9 | 10 | import unified from 'unified'
|
10 | 11 |
|
11 | 12 | import type { RemarkConfig, RemarkPlugin } from './types'
|
@@ -39,74 +40,109 @@ export const requirePkg = <T>(
|
39 | 40 | throw error
|
40 | 41 | }
|
41 | 42 |
|
42 |
| -let searchSync: (searchFrom?: string) => CosmiconfigResult |
| 43 | +/** |
| 44 | + * Given a filepath, get the nearest path that is a regular file. |
| 45 | + * The filepath provided by eslint may be a virtual filepath rather than a file |
| 46 | + * on disk. This attempts to transform a virtual path into an on-disk path |
| 47 | + */ |
| 48 | +export const getPhysicalFilename = (filename: string): string => { |
| 49 | + try { |
| 50 | + if (fs.statSync(filename).isFile()) { |
| 51 | + return filename |
| 52 | + } |
| 53 | + } catch (err) { |
| 54 | + // https://github.com/eslint/eslint/issues/11989 |
| 55 | + if ((err as { code: string }).code === 'ENOTDIR') { |
| 56 | + return getPhysicalFilename(path.dirname(filename)) |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + return filename |
| 61 | +} |
43 | 62 |
|
44 | 63 | export const remarkProcessor = unified().use(remarkParse).freeze()
|
45 | 64 |
|
| 65 | +const explorer = cosmiconfigSync('remark', { |
| 66 | + packageProp: 'remarkConfig', |
| 67 | +}) |
| 68 | + |
| 69 | +// @internal - exported for testing |
| 70 | +export const processorCache = new Map<string, FrozenProcessor>() |
| 71 | + |
| 72 | +// eslint-disable-next-line sonarjs/cognitive-complexity |
46 | 73 | export const getRemarkProcessor = (searchFrom: string, isMdx: boolean) => {
|
47 |
| - if (!searchSync) { |
48 |
| - searchSync = cosmiconfigSync('remark', { |
49 |
| - packageProp: 'remarkConfig', |
50 |
| - }).search |
| 74 | + const initCacheKey = `${String(isMdx)}-${searchFrom}` |
| 75 | + |
| 76 | + let cachedProcessor = processorCache.get(initCacheKey) |
| 77 | + |
| 78 | + if (cachedProcessor) { |
| 79 | + return cachedProcessor |
51 | 80 | }
|
52 | 81 |
|
53 |
| - let result: Partial<CosmiconfigResult> |
| 82 | + const result = explorer.search(searchFrom) |
54 | 83 |
|
55 |
| - try { |
56 |
| - result = searchSync(searchFrom) |
57 |
| - } catch (err) { |
58 |
| - // https://github.com/eslint/eslint/issues/11989 |
59 |
| - /* istanbul ignore if */ |
60 |
| - if ( |
61 |
| - (err as { code?: string }).code !== 'ENOTDIR' || |
62 |
| - !/[/\\]\d+_[^/\\]*\.[\da-z]+$/i.test(searchFrom) |
63 |
| - ) { |
64 |
| - throw err |
65 |
| - } |
66 |
| - try { |
67 |
| - result = searchSync(path.dirname(searchFrom)) |
68 |
| - } catch { |
69 |
| - /* istanbul ignore next */ |
70 |
| - throw err |
71 |
| - } |
| 84 | + const cacheKey = result ? `${String(isMdx)}-${result.filepath}` : '' |
| 85 | + |
| 86 | + cachedProcessor = processorCache.get(cacheKey) |
| 87 | + |
| 88 | + if (cachedProcessor) { |
| 89 | + return cachedProcessor |
72 | 90 | }
|
73 | 91 |
|
74 |
| - /* istanbul ignore next */ |
75 |
| - const { plugins = [], settings } = (result?.config || |
76 |
| - {}) as Partial<RemarkConfig> |
| 92 | + if (result) { |
| 93 | + /* istanbul ignore next */ |
| 94 | + const { plugins = [], settings } = (result.config || |
| 95 | + {}) as Partial<RemarkConfig> |
77 | 96 |
|
78 |
| - // disable this rule automatically since we already have a parser option `extensions` |
79 |
| - // only disable this plugin if there are at least one plugin enabled |
80 |
| - // otherwise `result` could be null inside `plugins.reduce` |
81 |
| - /* istanbul ignore else */ |
82 |
| - if (plugins.length > 0) { |
83 |
| - try { |
84 |
| - // eslint-disable-next-line node/no-extraneous-require |
85 |
| - plugins.push([require.resolve('remark-lint-file-extension'), false]) |
86 |
| - } catch { |
87 |
| - // just ignore if the package does not exist |
| 97 | + // disable this rule automatically since we already have a parser option `extensions` |
| 98 | + // only disable this plugin if there are at least one plugin enabled |
| 99 | + // otherwise it is redundant |
| 100 | + /* istanbul ignore else */ |
| 101 | + if (plugins.length > 0) { |
| 102 | + try { |
| 103 | + // eslint-disable-next-line node/no-extraneous-require |
| 104 | + plugins.push([require.resolve('remark-lint-file-extension'), false]) |
| 105 | + } catch { |
| 106 | + // just ignore if the package does not exist |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + const initProcessor = remarkProcessor() |
| 111 | + .use({ settings }) |
| 112 | + .use(remarkStringify) |
| 113 | + |
| 114 | + if (isMdx) { |
| 115 | + initProcessor.use(remarkMdx) |
88 | 116 | }
|
89 |
| - } |
90 | 117 |
|
91 |
| - const initProcessor = remarkProcessor().use({ settings }).use(remarkStringify) |
| 118 | + cachedProcessor = plugins |
| 119 | + .reduce((processor, pluginWithSettings) => { |
| 120 | + const [plugin, ...pluginSettings] = arrayify(pluginWithSettings) as [ |
| 121 | + RemarkPlugin, |
| 122 | + ...unknown[] |
| 123 | + ] |
| 124 | + return processor.use( |
| 125 | + /* istanbul ignore next */ |
| 126 | + typeof plugin === 'string' |
| 127 | + ? requirePkg(plugin, 'remark', result.filepath) |
| 128 | + : plugin, |
| 129 | + ...pluginSettings, |
| 130 | + ) |
| 131 | + }, initProcessor) |
| 132 | + .freeze() |
| 133 | + } else { |
| 134 | + const initProcessor = remarkProcessor().use(remarkStringify) |
92 | 135 |
|
93 |
| - if (isMdx) { |
94 |
| - initProcessor.use(remarkMdx) |
| 136 | + if (isMdx) { |
| 137 | + initProcessor.use(remarkMdx) |
| 138 | + } |
| 139 | + |
| 140 | + cachedProcessor = initProcessor.freeze() |
95 | 141 | }
|
96 | 142 |
|
97 |
| - return plugins |
98 |
| - .reduce((processor, pluginWithSettings) => { |
99 |
| - const [plugin, ...pluginSettings] = arrayify(pluginWithSettings) as [ |
100 |
| - RemarkPlugin, |
101 |
| - ...unknown[] |
102 |
| - ] |
103 |
| - return processor.use( |
104 |
| - /* istanbul ignore next */ |
105 |
| - typeof plugin === 'string' |
106 |
| - ? requirePkg(plugin, 'remark', result.filepath) |
107 |
| - : plugin, |
108 |
| - ...pluginSettings, |
109 |
| - ) |
110 |
| - }, initProcessor) |
111 |
| - .freeze() |
| 143 | + processorCache |
| 144 | + .set(initCacheKey, cachedProcessor) |
| 145 | + .set(cacheKey, cachedProcessor) |
| 146 | + |
| 147 | + return cachedProcessor |
112 | 148 | }
|
0 commit comments