Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-mdx-global-component-lint.md
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 35 additions & 4 deletions packages/doom/src/eslint.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<string, 'readonly'>

async function doom(
userConfig?: UserConfig | null,
): Promise<tseslint.ConfigArray>
Expand Down Expand Up @@ -64,7 +92,7 @@ async function doom(userConfigOrRoot?: UserConfig | string | null | URL) {
languageOptions: {
globals: globals.browser,
parserOptions: {
remarkConfigPath,
...(remarkConfigPath && { remarkConfigPath }),
},
},
rules: cspellEnabled
Expand All @@ -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',
Expand Down
27 changes: 27 additions & 0 deletions packages/doom/src/plugins/global/components.ts
Original file line number Diff line number Diff line change
@@ -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)))
15 changes: 3 additions & 12 deletions packages/doom/src/plugins/global/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
69 changes: 69 additions & 0 deletions packages/doom/test/eslint.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, test } from '@rstest/core'
import { ESLint } from 'eslint'

import doom from '#eslint.ts'

type ESLintOptions = NonNullable<ConstructorParameters<typeof ESLint>[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',
'',
'<Overview />',
'<Steps>',
' <Term name="product" />',
'</Steps>',
'',
].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<UnknownComponent />\n')

expect(messages).toContainEqual(
expect.objectContaining({
ruleId: 'no-undef',
message: "'UnknownComponent' is not defined.",
}),
)
})
})
Loading