Skip to content

Commit

Permalink
feat: experiemental Files tab, close #24
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Apr 5, 2024
1 parent 6c25668 commit 2595b46
Show file tree
Hide file tree
Showing 15 changed files with 412 additions and 36 deletions.
6 changes: 3 additions & 3 deletions app/app.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { errorInfo } from './composables/payload'
import { ensureDataFetch, errorInfo } from '~/composables/payload'
import { version } from '~~/package.json'
import 'floating-vue/dist/style.css'
import './styles/global.css'
import './composables/dark'
import { version } from '~~/package.json'
import { ensureDataFetch } from '~/composables/payload'
useHead({
title: 'ESLint Config Inspector',
Expand Down
132 changes: 132 additions & 0 deletions app/components/FileGroupItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script setup lang="ts">
import { defineModel, ref, watchEffect } from 'vue'
import { payload } from '../composables/payload'
import { useRouter } from '#app/composables/router'
import type { FilesGroup } from '~~/types'
defineProps<{
index: number
group: FilesGroup
}>()
const open = defineModel('open', {
default: true,
})
const hasShown = ref(open.value)
if (!hasShown.value) {
const stop = watchEffect(() => {
if (open.value) {
hasShown.value = true
stop()
}
})
}
const router = useRouter()
function goToConfig(idx: number) {
router.push(`/configs?index=${idx + 1}`)
}
</script>

<template>
<details
class="flat-config-item"
:open="open"
border="~ base rounded-lg" relative
@toggle="open = $event.target.open"
>
<summary block>
<div class="absolute right-[calc(100%+10px)] top-1.5" text-right font-mono op35 lt-lg:hidden>
#{{ index + 1 }}
</div>
<div flex="~ gap-2 items-start wrap items-center" cursor-pointer select-none bg-hover px2 py2 text-sm font-mono>
<div i-ph-caret-right class="[details[open]_&]:rotate-90" transition op50 />
<div flex flex-col gap-3 md:flex-row flex-auto>
<span op50 flex-auto>
<span>Files group #{{ index + 1 }}</span>
</span>

<div flex="~ gap-2 items-start wrap">
<SummarizeItem
icon="i-ph-files-duotone"
:number="group.files?.length || 0"
color="text-yellow5"
title="Files"
/>
<SummarizeItem
icon="i-ph-stack-duotone"
:number="group.configs.length"
color="text-blue5 dark:text-blue4"
title="Rules"
mr-2
/>
</div>
</div>
</div>
</summary>

<div absolute right-2 top-2 text-right text-5em font-mono op5 pointer-events-none>
#{{ index + 1 }}
</div>

<div v-if="hasShown" px4 py4 flex="~ col gap-4" of-auto>
<div flex="~ gap-2 items-center">
<div i-ph-files-duotone flex-none />
<div>Matched Local Files ({{ group.files.length }})</div>
</div>

<div flex="~ col gap-1" ml7 mt--2>
<FileItem v-for="file of group.files" :key="file" font-mono :filepath="file" />
</div>

<div flex="~ gap-2 items-center">
<div i-ph-stack-duotone flex-none />
<div>Configs specific to files ({{ group.configs.length }})</div>
</div>
<div flex="~ col gap-1" ml6 mt--2>
<div v-for="configIdx of group.configs" :key="configIdx" font-mono flex="~ gap-2">
<VDropdown>
<button border="~ base rounded px2" flex="~ gap-2 items-center" hover="bg-active" px2 py0.5>
<ColorizedConfigName v-if="payload.configs[configIdx].name" :name="payload.configs[configIdx].name!" />
<div v-else op50 italic>
anonymous
</div>
<div op50 text-sm>
#{{ configIdx + 1 }}
</div>
</button>
<template #popper="{ shown }">
<div v-if="shown" max-h="50vh" min-w-100>
<div flex="~ items-center gap-2" p3>
<button
action-button
title="Copy"
@click="goToConfig(configIdx)"
>
<div i-ph-stack-duotone />
Go to this config
</button>
<slot name="popup-actions" />
</div>
<div p3 border="t base">
<div flex="~ gap-2 items-start">
<div i-ph-file-magnifying-glass-duotone my1 flex-none op75 />
<div flex="~ col gap-2">
<div op50>
Applies to files matching
</div>
<div flex="~ gap-2 items-center wrap">
<GlobItem v-for="glob, idx of payload.configs[configIdx].files" :key="idx" :glob="glob" />
</div>
</div>
</div>
</div>
</div>
</template>
</VDropdown>
</div>
</div>
</div>
</details>
</template>
34 changes: 34 additions & 0 deletions app/components/FileItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { computed } from 'vue'
import { filepathIconsMap } from '../composables/icons'
import { filtersConfigs } from '../composables/state'
import { useRouter } from '#app/composables/router'
const props = defineProps<{
filepath: string
}>()
const icon = computed(() => {
for (const rule of filepathIconsMap) {
if (rule.match.test(props.filepath))
return rule.icon
}
return 'i-ph-file-duotone'
})
const router = useRouter()
function searchFile() {
filtersConfigs.filepath = props.filepath
filtersConfigs.rule = undefined
router.push(`/configs`)
}
</script>

<template>
<div flex="~ gap-2 items-center">
<div :class="icon" flex-none h="1em" translate-y-1px />
<button text-gray hover="underline" @click="searchFile">
{{ filepath }}
</button>
</div>
</template>
8 changes: 8 additions & 0 deletions app/components/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ function toggleRuleView() {
<div i-ph-list-dashes-duotone flex-none />
Rules
</NuxtLink>
<NuxtLink
to="/files" active-class="op100! bg-active"
px3 py1 op50 border="~ base rounded"
flex="~ gap-2 items-center"
>
<div i-ph-files-duotone flex-none />
Files
</NuxtLink>
<button
title="Toggle Dark Mode"
i-ph-sun-dim-duotone dark:i-ph-moon-stars-duotone ml1 text-xl op50 hover:op75
Expand Down
6 changes: 5 additions & 1 deletion app/components/SummarizeItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ const props = defineProps<{
</script>

<template>
<div flex="~ gap-2" :title="`${props.number} ${props.title}`" :class="props.number ? props.color : 'op35'">
<div
v-tooltip="`${props.number} ${props.title}`"
flex="~ gap-2"
:class="props.number ? props.color : 'op35'"
>
<div :class="props.icon" />
<span min-w-6 :class="`text-${props.color}`">{{ props.number || '-' }}</span>
</div>
Expand Down
33 changes: 33 additions & 0 deletions app/composables/configs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Linter } from 'eslint'
import { minimatch } from 'minimatch'
import type { FlatESLintConfigItem } from '~~/types'

function matchGlob(file: string, glob: (Linter.FlatConfigFileSpec | Linter.FlatConfigFileSpec[])[]) {
const globs = (Array.isArray(glob) ? glob : [glob]).flat()
return globs.some(glob => typeof glob === 'function' ? glob(file) : minimatch(file, glob))
}

export function isIgnoreOnlyConfig(config: FlatESLintConfigItem) {
const keys = Object.keys(config).filter(i => !['name'].includes(i))
return keys.length === 1 && keys[0] === 'ignores'
}

export function getMatchedConfigs(filepath: string, configs: FlatESLintConfigItem[]): number[] {
const ignoreOnlyConfigs = configs.filter(isIgnoreOnlyConfig)
const isIgnored = ignoreOnlyConfigs.some(config => matchGlob(filepath, config.ignores!))
if (isIgnored)
return []
const isAnyIncluded = configs.some(config => matchGlob(filepath, config.files || []))
if (!isAnyIncluded)
return []

return configs
.map((config, index) => {
const isIncluded = config.files ? matchGlob(filepath, config.files) : true
const isExcluded = config.ignores ? matchGlob(filepath, config.ignores) : false
if (isIncluded && !isExcluded)
return index
return undefined
})
.filter(index => index !== undefined) as number[]
}
43 changes: 43 additions & 0 deletions app/composables/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// @unocss-include
export const filepathIconsMap = [
{
match: /package\.json$/,
icon: 'i-file-icons-npm text-red scale-110',
},
{
match: /eslint\.config\.\w+$/,
icon: 'i-file-icons-eslint text-primary',
},
{
match: /\.[mc]?jsx?$/,
icon: 'i-vscode-icons-file-type-js-official',
},
{
match: /\.[mc]?tsx?$/,
icon: 'i-file-icons-typescript-alt text-blue3',
},
{
match: /\.vue$/,
icon: 'i-logos-vue',
},
{
match: /\.svelte$/,
icon: 'i-logos-svelte-icon',
},
{
match: /\.html?$/,
icon: 'i-devicon-html5',
},
{
match: /\.md$/,
icon: 'i-simple-icons-markdown text-gray',
},
{
match: /\.json[c5]?$/,
icon: 'i-simple-icons-json text-gray',
},
{
match: /\.css$/,
icon: 'i-vscode-icons-file-type-css',
},
]
29 changes: 27 additions & 2 deletions app/composables/payload.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
import { $fetch } from 'ofetch'
import type { ErrorInfo, Payload, ResolvedPayload, RuleConfigStates, RuleInfo } from '~~/types'
import type { ErrorInfo, FilesGroup, Payload, ResolvedPayload, RuleConfigStates, RuleInfo } from '~~/types'

const LOG_NAME = '[ESLint Config Inspector]'

Expand Down Expand Up @@ -61,7 +61,7 @@ export function ensureDataFetch() {
return _promises
}

export const payload = computed(() => resolvePayload(data.value!))
export const payload = computed(() => Object.freeze(resolvePayload(data.value!)))

export function getRuleFromName(name: string): RuleInfo | undefined {
return payload.value.rules[name]
Expand Down Expand Up @@ -92,8 +92,33 @@ export function resolvePayload(payload: Payload): ResolvedPayload {
})
})

const generalConfigs = payload.configs
.map((config, idx) => (!config.files && !config.ignores) || isIgnoreOnlyConfig(config) ? idx : undefined)
.filter((idx): idx is number => idx !== undefined)

const filesMatchedConfigsMap = new Map<string, number[]>()
payload.files.forEach((file) => {
filesMatchedConfigsMap.set(file, getMatchedConfigs(file, payload.configs))
})

const filesGroupMap = new Map<string, FilesGroup>()
for (const [file, configs] of filesMatchedConfigsMap.entries()) {
const configIndex = configs.sort((a, b) => a - b).filter(i => !generalConfigs.includes(i))
const id = configIndex.join(',')
if (!filesGroupMap.has(id))
filesGroupMap.set(id, { id, files: [], configs: configIndex })
filesGroupMap.get(id)!.files.push(file)
}

configsOpenState.value = payload.configs.length >= 10
// collapse all if there are too many items
? payload.configs.map(() => false)
: payload.configs.map(() => true)

return {
...payload,
ruleStateMap,
filesMatchedConfigsMap,
filesGroup: [...filesGroupMap.values()],
}
}
4 changes: 4 additions & 0 deletions app/composables/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { breakpointsTailwind } from '@vueuse/core'
import { computed, reactive, ref } from 'vue'
import type { FiltersConfigsPage } from '~~/types'

export const filtersConfigs = reactive<FiltersConfigsPage>({
Expand All @@ -19,6 +20,7 @@ export const stateStorage = useLocalStorage(
{
viewType: 'list' as 'list' | 'grid',
viewFileMatchType: 'configs' as 'configs' | 'merged',
viewFilesTab: 'list' as 'list' | 'group',
showSpecificOnly: false,
},
{ mergeDefaults: true },
Expand All @@ -28,3 +30,5 @@ const bp = useBreakpoints(breakpointsTailwind)
const bpSm = bp.smallerOrEqual('md')

export const isGridView = computed(() => bpSm.value || stateStorage.value.viewType === 'grid')

export const configsOpenState = ref<boolean[]>([])

0 comments on commit 2595b46

Please sign in to comment.