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
24 changes: 24 additions & 0 deletions docs/errors/OXDT0004.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
outline: deep
---
# OXDT0004: Oxlint Config Inspection Failed

## Message

> Failed to inspect Oxlint config "`{configPath}`": `{reason}`

## Cause

The inspected config is outside the workspace, uses an unsupported format, cannot be loaded, or Oxlint did not return its built-in rule categories.

## Example

The RPC receives `../oxlint.config.ts` or an `oxlint.config.mts` file that the inspector cannot process.

## Fix

Select a supported Oxlint config inside the workspace and verify that Oxlint can read its rules.

## Source

- [`packages/oxc/src/node/rpc/functions/oxlint-inspect-config.ts`](https://github.com/vitejs/devtools/blob/main/packages/oxc/src/node/rpc/functions/oxlint-inspect-config.ts) — validates the config path and builds the complete rule configuration overview.
1 change: 1 addition & 0 deletions docs/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ Emitted by `@vitejs/devtools-oxc`.
| [OXDT0001](./OXDT0001) | error | Failed to Create Lint Result |
| [OXDT0002](./OXDT0002) | error | Invalid Lint Result ID |
| [OXDT0003](./OXDT0003) | error | Failed to Delete Lint Result |
| [OXDT0004](./OXDT0004) | error | Oxlint Config Inspection Failed |
5 changes: 5 additions & 0 deletions packages/oxc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
DevTools for Oxc — a built-in Vite DevTools integration that inspects the Oxc toolchain (oxlint and oxfmt).

📖 [Documentation](https://devtools.vite.dev/oxc/)

# Credits

- [nelsonlaidev/oxlint-config-inspector](https://github.com/nelsonlaidev/oxlint-config-inspector)
- [eslint/config-inspector](https://github.com/eslint/config-inspector)
1 change: 1 addition & 0 deletions packages/oxc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"prepack": "pnpm build"
},
"dependencies": {
"@oxlint-config-inspector/core": "catalog:deps",
"@vitejs/devtools-kit": "workspace:*",
"cac": "catalog:deps",
"devframe": "catalog:deps",
Expand Down
9 changes: 8 additions & 1 deletion packages/oxc/src/app/components/SummaryCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,16 @@ const configFiles = computed<OxcConfigFile[]>(() => {
</button>
</template>
<template #title>
<div class="flex items-center gap-1">
<div class="flex items-center gap-2">
<DisplayFileIcon class="flex-none" :filename="configFile.path" />
<div>{{ configFile.path }}</div>
<NuxtLink
:to="{ path: '/oxlint/config', query: { config: configFile.path } }"
class="btn-action-sm ml-2"
>
Inspect config
<span class="i-ph-arrow-right" />
</NuxtLink>
</div>
</template>
<div class="w-150 max-w-full font-mono">
Expand Down
38 changes: 38 additions & 0 deletions packages/oxc/src/app/components/config/ConfigOptionSelectGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
defineProps<{
options: readonly string[]
titles?: readonly string[]
classes?: readonly string[]
}>()

const value = defineModel<string>({ required: true })
</script>

<template>
<fieldset class="inline-flex flex-wrap gap-1 of-hidden text-sm">
<label
v-for="(option, index) in options"
:key="option"
class="relative cursor-pointer border border-base rounded-full px2.5 py0.5 hover:bg-active"
:class="option === value ? 'bg-active' : 'saturate-0 hover:saturate-100'"
>
<span
:class="[
option === value ? '' : 'op50',
titles?.[index] ? '' : 'capitalize',
classes?.[index] ?? '',
]"
>
<slot :value="option" :title="titles?.[index]">
{{ titles?.[index] ?? option }}
</slot>
</span>
<input
v-model="value"
type="radio"
:value="option"
class="absolute inset-0 cursor-pointer op0"
/>
</label>
</fieldset>
</template>
183 changes: 183 additions & 0 deletions packages/oxc/src/app/components/config/ConfigOverrides.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<script setup lang="ts">
import type { InspectConfigResult } from '@oxlint-config-inspector/core'
import DisplayBadge from '@vitejs/devtools-ui/components/Display/DisplayBadge.vue'
import DisplayNumberBadge from '@vitejs/devtools-ui/components/Display/DisplayNumberBadge.vue'
import VisualEmptyState from '@vitejs/devtools-ui/components/Visual/VisualEmptyState.vue'
import { getPluginColor } from '@vitejs/devtools-ui/utils/color'
import { computed } from 'vue'

const emit = defineEmits<{
selectRule: [ruleId: string]
}>()

const props = defineProps<{
config: InspectConfigResult
}>()

const rulesById = computed(() => new Map(props.config.rules.map(rule => [rule.ruleId, rule])))
</script>

<template>
<VisualEmptyState
v-if="config.overrideGroups.length === 0"
icon="i-ph-files-duotone"
title="No overrides"
description="No override configuration was found."
/>
<div v-else class="flex flex-col gap-2">
<details
v-for="group in config.overrideGroups"
:key="group.index"
class="bg-base border border-base rounded"
>
<summary
class="flex cursor-pointer list-none items-center gap-4 px2 py1 font-mono text-sm hover:bg-active"
>
<span class="w-8 flex-none text-right text-xs op50">#{{ group.index + 1 }}</span>
<span class="min-w-0 flex flex-1 items-center gap-2" :title="group.files.join(', ')">
<span class="truncate">{{ group.files[0] }}</span>
<span v-if="group.files.length > 1" class="flex-none text-xs op50">
+{{ group.files.length - 1 }}
</span>
</span>
<span class="flex items-center gap-3 text-xs">
<span
class="flex items-center gap-1"
:class="group.files.length > 1 ? 'text-yellow-600 dark:text-yellow-400' : 'op25'"
>
<span class="i-ph-file-search-duotone" />
<DisplayNumberBadge :number="group.files.length" color="" />
</span>
<span
class="flex items-center gap-1"
:class="(group.plugins?.length ?? 0) > 1 ? 'text-teal-600 dark:text-teal-400' : 'op25'"
>
<span class="i-ph-plugs-duotone" />
<DisplayNumberBadge :number="group.plugins?.length ?? 0" color="" />
</span>
<span
class="flex items-center gap-1"
:class="group.rules.length > 1 ? 'text-blue-600 dark:text-blue-400' : 'op25'"
>
<span class="i-ph-list-bullets-duotone" />
<DisplayNumberBadge :number="group.rules.length" color="" />
</span>
</span>
</summary>

<div class="grid gap-4 border-t border-base p4">
<div class="flex items-start gap-2">
<span class="i-ph-file-search-duotone my1 flex-none" />
<div class="flex flex-col gap-2">
<div>Applies to files matching</div>
<div class="flex flex-wrap items-center gap-2">
<DisplayBadge
v-for="file in group.files"
:key="file"
:text="file"
:color="false"
size="md"
class="inline-flex items-center border border-base bg-transparent px2 py1 text-sm leading-none font-mono"
/>
</div>
</div>
</div>

<div v-if="group.excludeFiles?.length" class="flex items-start gap-2">
<span class="i-ph-eye-closed-duotone my1 flex-none" />
<div class="flex flex-col gap-2">
<div>Exclude</div>
<div class="flex flex-wrap items-center gap-2">
<DisplayBadge
v-for="file in group.excludeFiles"
:key="file"
:text="file"
:color="false"
size="md"
class="inline-flex items-center border border-base bg-transparent px2 py1 text-sm leading-none font-mono"
/>
</div>
</div>
</div>

<div v-if="group.plugins?.length" class="flex items-start gap-2">
<span class="i-ph-plugs-duotone my1 flex-none" />
<div class="flex flex-col gap-2">
<div>Plugins ({{ group.plugins.length }})</div>
<div class="flex flex-wrap items-center gap-2">
<DisplayBadge
v-for="plugin in group.plugins"
:key="plugin"
:text="plugin"
size="md"
class="px3 py1 text-sm leading-none font-mono"
/>
</div>
</div>
</div>

<div v-if="group.rules.length" class="flex items-start gap-2">
<span class="i-ph-list-bullets-duotone my1 flex-none" />
<div class="min-w-0 flex flex-1 flex-col gap-2">
<div>Rules ({{ group.rules.length }})</div>
<div
class="grid grid-cols-[max-content_max-content_max-content_minmax(20rem,1fr)] items-center gap-x-3 gap-y-2 of-x-auto"
>
<button
v-for="rule in group.rules"
:key="rule.ruleId"
type="button"
class="col-span-4 grid grid-cols-subgrid items-center text-left focus-visible:bg-active outline-none cursor-pointer"
@click="emit('selectRule', rule.ruleId)"
>
<span
:class="{
'i-ph-warning-circle-duotone color-scale-critical': rule.severity === 'error',
'i-ph-warning-duotone color-scale-medium': rule.severity === 'warn',
'i-ph-circle-half-tilt-duotone op-fade': rule.severity === 'off',
'i-ph-question-duotone op-fade': rule.severity === 'unknown',
}"
:title="rule.severity"
/>
<span class="ws-nowrap border border-base rounded py-1 px-2 font-mono">
<span
v-if="rulesById.get(rule.ruleId)?.pluginName"
:style="{
color: getPluginColor(rulesById.get(rule.ruleId)!.pluginName!),
}"
>
{{ rulesById.get(rule.ruleId)!.pluginName }}
</span>
<span v-if="rulesById.get(rule.ruleId)?.pluginName" class="op-fade">/</span>
<span>{{ rulesById.get(rule.ruleId)?.name ?? rule.ruleId }}</span>
</span>
<span class="grid grid-cols-2 items-center gap-1 op-fade">
<span
v-if="rulesById.get(rule.ruleId)?.recommended"
class="i-ph-check-square-duotone"
title="Recommended"
/>
<span v-else />
<span
v-if="rulesById.get(rule.ruleId)?.fixable"
class="i-ph-wrench-duotone"
title="Fixable"
/>
<span v-else />
</span>
<span class="truncate op-fade">
{{
rulesById.get(rule.ruleId)?.description ??
rulesById.get(rule.ruleId)?.category ??
'-'
}}
</span>
</button>
</div>
</div>
</div>
<p v-else class="text-sm op-fade">No rules configured for this override.</p>
</div>
</details>
</div>
</template>
109 changes: 109 additions & 0 deletions packages/oxc/src/app/components/config/ConfigOverview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<script setup lang="ts">
import type { InspectConfigResult } from '@oxlint-config-inspector/core'
import type { OxcConfigFile } from '../../../types'
import ContainerCard from '@vitejs/devtools-ui/components/Container/ContainerCard.vue'
import { computed } from 'vue'

const props = defineProps<{
config: InspectConfigResult
configFiles: OxcConfigFile[]
}>()

const selectedPath = defineModel<string>('selectedPath', { required: true })
const selectedConfig = computed(() =>
props.configFiles.find(file => file.path === selectedPath.value),
)

const metrics = computed(
() =>
[
['Total rules', props.config.stats.totalRules, 'i-ph-list-numbers-duotone'],
['Enabled rules', props.config.stats.enabledRules, 'i-ph-list-checks-duotone'],
['Configured rules', props.config.stats.configuredRules, 'i-ph-sliders-duotone'],
['Override rules', props.config.stats.overrideRules, 'i-ph-files-duotone'],
['Builtin rules', props.config.stats.builtinRules, 'i-ph-package-duotone'],
['JS plugin rules', props.config.stats.jsPluginRules, 'i-ph-plugs-duotone'],
['Recommended', props.config.stats.recommendedRules, 'i-ph-check-square-duotone'],
['Fixable', props.config.stats.fixableRules, 'i-ph-wrench-duotone'],
['Unknown rules', props.config.stats.unknownRules, 'i-ph-question-duotone'],
['Deprecated', props.config.stats.deprecatedRules, 'i-ph-prohibit-inset-duotone'],
['Plugin errors', props.config.pluginErrors.length, 'i-ph-warning-circle-duotone'],
] as const,
)
</script>

<template>
<div class="grid gap-4">
<ContainerCard v-if="config.pluginErrors.length">
<template #header>
<h2 class="flex items-center gap-2 font-medium">
<span class="i-ph-warning-duotone color-scale-critical" />
Plugin load errors
</h2>
</template>
<div class="grid gap-3 p4">
<div
v-for="pluginError in config.pluginErrors"
:key="`${pluginError.specifier}:${pluginError.message}`"
class="border border-base rounded p3"
>
<code class="font-medium">{{ pluginError.specifier }}</code>
<p class="mt2 text-sm op-fade">{{ pluginError.message }}</p>
</div>
</div>
</ContainerCard>

<section class="grid gap-2">
<div
class="border border-base rounded p4 grid grid-cols-[max-content_160px_2fr] gap-2 items-center"
>
<template v-for="[label, value, icon] in metrics" :key="label">
<div :class="icon" />
<div>{{ label }}</div>
<div>{{ value }}</div>
</template>
</div>
</section>

<ContainerCard>
<template #header>
<label class="block">
<span class="relative block">
<select
v-model="selectedPath"
:title="selectedPath"
aria-label="Config"
class="w-full appearance-none pl7 pr8 font-mono bg-transparent color-base outline-none"
>
<option v-for="file in configFiles" :key="file.path" :value="file.path">
{{ file.path }}
</option>
</select>
<span
class="i-ph-file-code-duotone pointer-events-none absolute left-0 top-1/2 translate-y--1/2 op60"
/>
<span
class="i-ph-caret-down pointer-events-none absolute right-0 top-1/2 translate-y--1/2 op60"
/>
</span>
</label>
</template>
<div
v-if="selectedConfig"
class="config-code max-h-96 overflow-auto bg-code p4 font-mono text-sm"
>
<Shiki :code="selectedConfig.content" :ext="`.${selectedConfig.format}`" />
</div>
</ContainerCard>
</div>
</template>

<style scoped>
.config-code {
scrollbar-width: none;
}

.config-code::-webkit-scrollbar {
display: none;
}
</style>
Loading
Loading