From 9516556f5958fb771751c9024928b0e9a7f39a3d Mon Sep 17 00:00:00 2001 From: ysyou Date: Wed, 24 Jun 2026 17:54:16 +0800 Subject: [PATCH] fix: declare MDX global components for lint --- .changeset/fix-mdx-global-component-lint.md | 5 ++ packages/doom/src/eslint.ts | 39 +++++++++-- .../doom/src/plugins/global/components.ts | 27 ++++++++ packages/doom/src/plugins/global/index.ts | 15 +--- packages/doom/test/eslint.spec.ts | 69 +++++++++++++++++++ 5 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 .changeset/fix-mdx-global-component-lint.md create mode 100644 packages/doom/src/plugins/global/components.ts create mode 100644 packages/doom/test/eslint.spec.ts diff --git a/.changeset/fix-mdx-global-component-lint.md b/.changeset/fix-mdx-global-component-lint.md new file mode 100644 index 000000000..b22ff80ac --- /dev/null +++ b/.changeset/fix-mdx-global-component-lint.md @@ -0,0 +1,5 @@ +--- +"@alauda/doom": patch +--- + +Declare Doom's MDX global components in the built-in ESLint config so `doom lint` does not report them as undefined JSX identifiers. diff --git a/packages/doom/src/eslint.ts b/packages/doom/src/eslint.ts index 8dd98046b..d91d37449 100644 --- a/packages/doom/src/eslint.ts +++ b/packages/doom/src/eslint.ts @@ -1,4 +1,6 @@ +import fs from 'node:fs' import { createRequire } from 'node:module' +import path from 'node:path' import { fileURLToPath } from 'node:url' import type { Options } from '@cspell/eslint-plugin' @@ -13,17 +15,43 @@ import globals from 'globals' import tseslint from 'typescript-eslint' import { loadConfig } from './cli/load-config.js' +import { getGlobalComponentNames } from './plugins/global/components.js' +import { pkgResolve } from './utils/index.js' const cjsRequire = createRequire(import.meta.url) -let remarkConfigPath: string +let remarkConfigPath: string | undefined + +const isLoadableRemarkConfigPath = (filepath: string) => + ['.cjs', '.js', '.mjs'].includes(path.extname(filepath)) try { - remarkConfigPath = fileURLToPath(import.meta.resolve('./remarkrc.js')) + const resolvedRemarkConfigPath = fileURLToPath( + import.meta.resolve('./remarkrc.js'), + ) + if (isLoadableRemarkConfigPath(resolvedRemarkConfigPath)) { + remarkConfigPath = resolvedRemarkConfigPath + } } catch { - remarkConfigPath = cjsRequire.resolve('./remarkrc.js') + try { + const resolvedRemarkConfigPath = cjsRequire.resolve('./remarkrc.js') + if (isLoadableRemarkConfigPath(resolvedRemarkConfigPath)) { + remarkConfigPath = resolvedRemarkConfigPath + } + } catch { + remarkConfigPath = undefined + } +} + +const builtRemarkConfigPath = pkgResolve('lib/remarkrc.js') +if (!remarkConfigPath && fs.existsSync(builtRemarkConfigPath)) { + remarkConfigPath = builtRemarkConfigPath } +const globalComponentGlobals = Object.fromEntries( + getGlobalComponentNames().map((name) => [name, 'readonly']), +) as Record + async function doom( userConfig?: UserConfig | null, ): Promise @@ -64,7 +92,7 @@ async function doom(userConfigOrRoot?: UserConfig | string | null | URL) { languageOptions: { globals: globals.browser, parserOptions: { - remarkConfigPath, + ...(remarkConfigPath && { remarkConfigPath }), }, }, rules: cspellEnabled @@ -81,6 +109,9 @@ async function doom(userConfigOrRoot?: UserConfig | string | null | URL) { }, { files: ['**/*.mdx'], + languageOptions: { + globals: globalComponentGlobals, + }, rules: { '@eslint-react/jsx-no-children-prop': 'off', '@eslint-react/rules-of-hooks': 'off', diff --git a/packages/doom/src/plugins/global/components.ts b/packages/doom/src/plugins/global/components.ts new file mode 100644 index 000000000..94b4f70d1 --- /dev/null +++ b/packages/doom/src/plugins/global/components.ts @@ -0,0 +1,27 @@ +import fs from 'node:fs' +import path from 'node:path' + +import { baseResolve } from '../../utils/index.ts' + +const componentsDir = baseResolve('runtime/components') + +const isGlobalComponentFile = (file: string) => { + const basename = path.basename(file, path.extname(file)) + return ( + !basename.startsWith('_') && + !basename.endsWith('.d') && + basename !== 'index' + ) +} + +export const getGlobalComponentFiles = () => + fs + .readdirSync(componentsDir) + .filter(isGlobalComponentFile) + .map((file) => path.resolve(componentsDir, file)) + +export const getGlobalComponentNames = () => + fs + .readdirSync(componentsDir) + .filter(isGlobalComponentFile) + .map((file) => path.basename(file, path.extname(file))) diff --git a/packages/doom/src/plugins/global/index.ts b/packages/doom/src/plugins/global/index.ts index dec61c47b..a6b37e33b 100644 --- a/packages/doom/src/plugins/global/index.ts +++ b/packages/doom/src/plugins/global/index.ts @@ -8,8 +8,9 @@ import { ACP_BASE, type DoomSite } from '../../shared/index.ts' import type { ExportItem } from '../../types.ts' import { baseResolve, pkgResolve } from '../../utils/index.ts' +import { getGlobalComponentFiles } from './components.ts' + const globalComponentsDir = baseResolve('global') -const componentsDir = baseResolve('runtime/components') export interface GlobalPluginOptions { version?: string @@ -39,17 +40,7 @@ export const globalPlugin = ({ .readdirSync(globalComponentsDir, 'utf8') .map((component) => path.resolve(globalComponentsDir, component)), markdown: { - globalComponents: fs - .readdirSync(componentsDir) - .filter((file) => { - const basename = path.basename(file, path.extname(file)) - return ( - !basename.startsWith('_') && - !basename.endsWith('.d') && - basename !== 'index' - ) - }) - .map((file) => path.resolve(componentsDir, file)), + globalComponents: getGlobalComponentFiles(), }, addRuntimeModules(config, isProd) { return { diff --git a/packages/doom/test/eslint.spec.ts b/packages/doom/test/eslint.spec.ts new file mode 100644 index 000000000..db67ef770 --- /dev/null +++ b/packages/doom/test/eslint.spec.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from '@rstest/core' +import { ESLint } from 'eslint' + +import doom from '#eslint.ts' + +type ESLintOptions = NonNullable[0]> + +const lintMdx = async (value: string) => { + const overrideConfig = [ + ...(await doom(null)), + { + files: ['**/*.mdx'], + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ignoreRemarkConfig: true, + }, + }, + rules: { + 'mdx/remark': 'off', + }, + }, + ] as unknown as ESLintOptions['overrideConfig'] + + const eslint = new ESLint({ + cwd: process.cwd(), + overrideConfigFile: true, + overrideConfig, + }) + + const [result] = await eslint.lintText(value, { + filePath: 'docs/en/test.mdx', + }) + + return result.messages +} + +describe('doom eslint config', () => { + test('allows doom global MDX components without local imports', async () => { + const messages = await lintMdx( + [ + '# Title', + '', + '', + '', + ' ', + '', + '', + ].join('\n'), + ) + + expect(messages).not.toContainEqual( + expect.objectContaining({ ruleId: 'no-undef' }), + ) + }) + + test('still reports unknown MDX component references', async () => { + const messages = await lintMdx('# Title\n\n\n') + + expect(messages).toContainEqual( + expect.objectContaining({ + ruleId: 'no-undef', + message: "'UnknownComponent' is not defined.", + }), + ) + }) +})