|
| 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