style(eslint-config): add rules to restrict the imports of element-plus (#20959)

* style(eslint-config): add rules to restrict the imports of element-plus

* chore: added validation for tsx files

* chore: revert the shell
This commit is contained in:
qiang
2025-06-13 17:07:39 +08:00
committed by GitHub
parent bb5930e8b7
commit 9539c19b40
20 changed files with 384 additions and 120 deletions

View File

@@ -46,7 +46,6 @@ will start the local development environment.
<script setup lang="ts">
// make sure this component is registered in @element-plus/components
import { ComponentYouAreDeveloping } from '@element-plus/components'
</script>
```

View File

@@ -40,7 +40,7 @@ transitions/collapse
// collapse
import { ElCollapseTransition } from 'element-plus'
// fade/zoom
import 'element-plus/lib/theme-chalk/base.css'
import 'element-plus/theme-chalk/base.css'
import App from './App.vue'
const app = createApp(App)

View File

@@ -37,7 +37,7 @@
</template>
<script lang="ts" setup>
import ElIcon from '@element-plus/components/icon'
import { ElIcon } from 'element-plus'
import {
Back,
DArrowRight,

View File

@@ -19,7 +19,7 @@ import {
TableV2FixedDir,
TableV2SortOrder,
} from 'element-plus'
import type { Column, SortBy } from '@element-plus/components/table-v2'
import type { Column, SortBy } from 'element-plus'
const longText =
'Quaerat ipsam necessitatibus eum quibusdam est id voluptatem cumque mollitia.'

View File

@@ -9,10 +9,7 @@
</template>
<script lang="ts" setup>
import type {
TreeNode,
TreeNodeData,
} from 'element-plus/es/components/tree-v2/src/types'
import type { TreeNode, TreeNodeData } from 'element-plus'
interface Tree {
id?: string

View File

@@ -18,7 +18,7 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { ElTreeV2 } from 'element-plus'
import type { TreeNodeData } from 'element-plus/es/components/tree-v2/src/types'
import type { TreeNodeData } from 'element-plus'
interface Tree {
id: string

View File

@@ -21,14 +21,14 @@
<script lang="ts" setup>
import { ref } from 'vue'
import type { TreeInstance } from 'element-plus'
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { RenderContentContext, TreeInstance } from 'element-plus'
interface Tree {
id: number
label: string
children?: Tree[]
}
type Node = RenderContentContext['node']
const treeRef = ref<TreeInstance>()

View File

@@ -9,7 +9,7 @@
</template>
<script lang="ts" setup>
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { LoadFunction } from 'element-plus'
interface Tree {
name: string
@@ -22,7 +22,7 @@ const props = {
isLeaf: 'leaf',
}
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
const loadNode: LoadFunction = (node, resolve) => {
if (node.level === 0) {
return resolve([{ name: 'region' }])
}

View File

@@ -13,8 +13,7 @@
</template>
<script lang="ts" setup>
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { TreeNodeData } from 'element-plus/es/components/tree/src/tree.type'
import type { TreeNodeData } from 'element-plus'
interface Tree {
id: number
@@ -23,7 +22,7 @@ interface Tree {
children?: Tree[]
}
const customNodeClass = ({ isPenultimate }: TreeNodeData, node: Node) =>
const customNodeClass = ({ isPenultimate }: TreeNodeData) =>
isPenultimate ? 'is-penultimate' : ''
const data: Tree[] = [

View File

@@ -44,16 +44,19 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { RenderContentContext, RenderContentFunction } from 'element-plus'
interface Tree {
id: number
label: string
children?: Tree[]
}
type Node = RenderContentContext['node']
type Data = RenderContentContext['data']
let id = 1000
const append = (data: Tree) => {
const append = (data: Data) => {
const newChild = { id: id++, label: 'testtest', children: [] }
if (!data.children) {
data.children = []
@@ -62,7 +65,7 @@ const append = (data: Tree) => {
dataSource.value = [...dataSource.value]
}
const remove = (node: Node, data: Tree) => {
const remove = (node: Node, data: Data) => {
const parent = node.parent
const children: Tree[] = parent.data.children || parent.data
const index = children.findIndex((d) => d.id === data.id)
@@ -70,47 +73,36 @@ const remove = (node: Node, data: Tree) => {
dataSource.value = [...dataSource.value]
}
const renderContent = (
h,
{
node,
data,
store,
}: {
node: Node
data: Tree
store: Node['store']
}
) => {
const renderContent: RenderContentFunction = (h, { node, data }) => {
return h(
'div',
{
class: 'custom-tree-node',
},
h('span', null, node.label),
h(
'div',
null,
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => append(data),
},
'Append '
),
h(
ElButton,
{
type: 'danger',
link: true,
style: 'margin-left: 4px',
onClick: () => remove(node, data),
},
'Delete'
)
)
[
h('span', null, node.label),
h('div', null, [
h(
ElButton,
{
type: 'primary',
link: true,
onClick: () => append(data),
},
'Append '
),
h(
ElButton,
{
type: 'danger',
link: true,
style: 'margin-left: 4px',
onClick: () => remove(node, data),
},
'Delete'
),
]),
]
)
}

View File

@@ -17,12 +17,15 @@
</template>
<script lang="ts" setup>
import type Node from 'element-plus/es/components/tree/src/model/node'
// eslint-disable-next-line no-restricted-imports
import type { DragEvents } from 'element-plus/es/components/tree/src/model/useDragNode'
import type {
AllowDropType,
NodeDropType,
} from 'element-plus/es/components/tree/src/tree.type'
RenderContentContext,
} from 'element-plus'
type Node = RenderContentContext['node']
const handleDragStart = (node: Node, ev: DragEvents) => {
console.log('drag start', node)

View File

@@ -18,7 +18,7 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import type { TreeInstance } from 'element-plus'
import type { FilterNodeMethodFunction, TreeInstance } from 'element-plus'
interface Tree {
[key: string]: any
@@ -36,7 +36,7 @@ watch(filterText, (val) => {
treeRef.value!.filter(val)
})
const filterNode = (value: string, data: Tree) => {
const filterNode: FilterNodeMethodFunction = (value: string, data: Tree) => {
if (!value) return true
return data.label.includes(value)
}

View File

@@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { LoadFunction } from 'element-plus'
interface Tree {
name: string
@@ -17,11 +17,7 @@ const props = {
}
let time = 0
const loadNode = (
node: Node,
resolve: (data: Tree[]) => void,
reject: () => void
) => {
const loadNode: LoadFunction = (node, resolve, reject) => {
if (node.level === 0) {
return resolve([{ name: 'region' }])
}

View File

@@ -10,7 +10,7 @@
</template>
<script lang="ts" setup>
import type Node from 'element-plus/es/components/tree/src/model/node'
import type { LoadFunction } from 'element-plus'
interface Tree {
name: string
@@ -30,7 +30,7 @@ const handleCheckChange = (
console.log(data, checked, indeterminate)
}
const loadNode = (node: Node, resolve: (data: Tree[]) => void) => {
const loadNode: LoadFunction = (node, resolve) => {
if (node.level === 0) {
return resolve([{ name: 'Root1' }, { name: 'Root2' }])
}

View File

@@ -129,6 +129,178 @@ module.exports = defineConfig({
'@typescript-eslint/no-unused-vars': 'off',
},
},
{
files: [
'docs/examples/**/*.{js,jsx,ts,tsx,vue}',
'docs/en-US/**/*.md/*.{js,jsx,ts,tsx,vue}',
],
rules: {
'no-console': 'off',
'import/no-unresolved': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-restricted-imports': [
'error',
{
paths: [
{ name: '@element-plus', message: 'Use element-plus instead.' },
],
patterns: [
{
group: [
'@element-plus/*',
'!@element-plus/icons-vue',
'element-plus/es/*',
'!element-plus/es/locale',
'element-plus/lib/*',
'!element-plus/lib/locale',
],
message: 'Use element-plus instead.',
},
],
},
],
},
},
{
files: [
'internal/**/*.{js,ts}',
'packages/constants/**/*.{js,ts}',
'packages/locale/**/*.{js,ts}',
'packages/test-utils/**/*.{js,ts}',
'packages/theme-chalk/**/*.{js,ts}',
],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{ name: 'lodash', message: 'Use lodash-unified instead.' },
{ name: 'lodash-es', message: 'Use lodash-unified instead.' },
{ name: 'element-plus', message: 'Use @element-plus/* instead.' },
],
patterns: [
{
group: ['lodash/*', 'lodash-es/*'],
message: 'Use lodash-unified instead.',
},
{
group: ['element-plus/*'],
message: 'Use @element-plus/* instead.',
},
{
group: [
'@element-plus/components',
'@element-plus/constants',
'@element-plus/directives',
'@element-plus/element-plus',
'@element-plus/hooks',
'@element-plus/locale',
'@element-plus/test-utils',
'@element-plus/theme-chalk',
'@element-plus/utils',
],
message:
'Please do not use this dependency in the current file.',
},
],
},
],
},
},
{
files: [
'packages/directives/**/*.{js,jsx,ts,tsx,vue}',
'packages/hooks/**/*.{js,jsx,ts,tsx,vue}',
'packages/utils/**/*.{js,jsx,ts,tsx,vue}',
],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{ name: 'lodash', message: 'Use lodash-unified instead.' },
{ name: 'lodash-es', message: 'Use lodash-unified instead.' },
{ name: 'element-plus', message: 'Use @element-plus/* instead.' },
],
patterns: [
{
group: ['lodash/*', 'lodash-es/*'],
message: 'Use lodash-unified instead.',
},
{
group: ['element-plus/*'],
message: 'Use @element-plus/* instead.',
},
{
group: [
'@element-plus/components',
'@element-plus/element-plus',
'@element-plus/theme-chalk',
'@element-plus/build',
'@element-plus/build-constants',
'@element-plus/build-utils',
'@element-plus/eslint-config',
'@element-plus/metadata',
],
message:
'Please do not use this dependency in the current file.',
},
],
},
],
},
},
{
files: [
'packages/components/**/*.{js,jsx,ts,tsx,vue}',
'packages/element-plus/**/*.{js,jsx,ts,tsx,vue}',
],
rules: {
'no-restricted-imports': [
'error',
{
paths: [
{ name: 'lodash', message: 'Use lodash-unified instead.' },
{ name: 'lodash-es', message: 'Use lodash-unified instead.' },
{ name: 'element-plus', message: 'Use @element-plus/* instead.' },
],
patterns: [
{
group: ['lodash/*', 'lodash-es/*'],
message: 'Use lodash-unified instead.',
},
{
group: ['element-plus/*'],
message: 'Use @element-plus/* instead.',
},
{
group: ['@element-plus/theme-chalk/src/el-*.scss'],
message: 'Use @element-plus/theme-chalk/src/*.scss instead.',
},
{
group: [
'@element-plus/theme-chalk/*.css',
'!@element-plus/theme-chalk/el-*.css',
'!@element-plus/theme-chalk/base.css',
],
message: 'Use @element-plus/theme-chalk/src/el-*.css instead.',
},
{
group: [
'@element-plus/build',
'@element-plus/build-constants',
'@element-plus/build-utils',
'@element-plus/eslint-config',
'@element-plus/metadata',
],
message:
'Please do not use this dependency in the current file.',
},
],
},
],
},
},
],
rules: {
// js/ts

View File

@@ -1,7 +1,7 @@
import { defineComponent, nextTick, reactive, ref } from 'vue'
import { computed, defineComponent, nextTick, reactive, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { useLocale } from '@element-plus/hooks'
import { useLocale, useNamespace } from '@element-plus/hooks'
import Chinese from '@element-plus/locale/lang/zh-cn'
import English from '@element-plus/locale/lang/en'
import {
@@ -20,7 +20,7 @@ import {
} from '../src/hooks/use-global-config'
import ConfigProvider, { messageConfig } from '../src/config-provider'
import type { PropType } from 'vue'
import type { ComponentPublicInstance, PropType } from 'vue'
import type { VueWrapper } from '@vue/test-utils'
import type { Language } from '@element-plus/locale'
import type { ComponentSize } from '@element-plus/constants'
@@ -29,7 +29,9 @@ import type { ConfigProviderProps } from '../src/config-provider-props'
const TestComp = defineComponent({
setup() {
const { t } = useLocale()
return () => <div>{t('el.popconfirm.confirmButtonText')}</div>
return () => (
<div class="locale-manifest">{t('el.popconfirm.confirmButtonText')}</div>
)
},
})
@@ -412,4 +414,128 @@ describe('config-provider', () => {
expect(pagination.vm.$el.className.includes('large')).toBe(true)
})
})
describe('use-namespace', () => {
const TestComp = defineComponent({
setup() {
const ns = useNamespace('table')
const cssVar = ns.cssVar({
'border-style': 'solid',
'border-width': '',
})
const cssVarBlock = ns.cssVarBlock({
'text-color': '#409eff',
'active-color': '',
})
return () => (
<div
id="testId"
class={[
ns.b(), // return ns + block
ns.b('body'),
ns.e('content'),
ns.m('active'),
ns.be('content', 'active'),
ns.em('content', 'active'),
ns.bem('body', 'content', 'active'),
ns.is('focus'),
ns.e(), // return empty string
ns.m(), // return empty string
ns.be(), // return empty string
ns.em(), // return empty string
ns.bem(), // return empty string
ns.is('hover', undefined), // return empty string
ns.is('clicked', false), // return empty string
]}
style={{ ...cssVar, ...cssVarBlock }}
>
text
</div>
)
},
})
const Comp = defineComponent({
setup(_props, { slots }) {
provideGlobalConfig({ namespace: 'ep' })
return () => slots.default?.()
},
})
let wrapper: VueWrapper<InstanceType<typeof Comp>>
beforeEach(() => {
wrapper = mount(Comp, {
slots: { default: () => <TestComp /> },
})
})
afterEach(() => {
wrapper.unmount()
})
it('should provide bem correctly', async () => {
await nextTick()
expect(wrapper.find('#testId').classes()).toEqual([
'ep-table', // b()
'ep-table-body', // b('body')
'ep-table__content', // e('content')
'ep-table--active', // m('active')
'ep-table-content__active', // be('content', 'active')
'ep-table__content--active', // em('content', 'active')
'ep-table-body__content--active', // bem('body', 'content', 'active')
'is-focus', // is('focus')
])
const style = wrapper.find('#testId').attributes('style')
expect(style).toMatch('--ep-border-style: solid;')
expect(style).not.toMatch('--ep-border-width:')
expect(style).toMatch('--ep-table-text-color: #409eff;')
expect(style).not.toMatch('--ep-table-active-color:')
})
})
describe('use-locale', () => {
let wrapper: VueWrapper<ComponentPublicInstance>
beforeEach(() => {
wrapper = mount(
defineComponent({
props: {
locale: {
type: Object as PropType<Language>,
default: Chinese,
},
},
setup(props) {
provideGlobalConfig(computed(() => ({ locale: props.locale })))
return () => <TestComp />
},
})
)
})
afterEach(() => {
wrapper.unmount()
})
it('should provide locale correctly', async () => {
await nextTick()
expect(wrapper.find('.locale-manifest').text()).toBe(
Chinese.el.popconfirm.confirmButtonText
)
})
it('should update the text reactively', async () => {
await nextTick()
expect(wrapper.find('.locale-manifest').text()).toBe(
Chinese.el.popconfirm.confirmButtonText
)
await wrapper.setProps({
locale: English,
})
expect(wrapper.find('.locale-manifest').text()).toBe(
English.el.popconfirm.confirmButtonText
)
})
})
})

View File

@@ -2,16 +2,12 @@ import { defineComponent, provide } from 'vue'
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import { NOOP } from '@element-plus/utils'
import { ElButton, buttonGroupContextKey } from '@element-plus/components'
import {
formContextKey,
formItemContextKey,
} from '@element-plus/components/form'
import type {
FormContext,
FormItemContext,
} from '@element-plus/components/form'
ElButton,
buttonGroupContextKey,
} from '@element-plus/components/button'
import { formContextKey, formItemContextKey } from '../src/constants'
import type { FormContext, FormItemContext } from '../src/types'
const AXIOM = 'Rem is the best girl'

View File

@@ -3,15 +3,20 @@ import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import Chinese from '@element-plus/locale/lang/zh-cn'
import English from '@element-plus/locale/lang/en'
import { provideGlobalConfig } from '@element-plus/components/config-provider'
import { buildTranslator, useLocale } from '../use-locale'
import type { Language } from '@element-plus/locale'
import type { ComponentPublicInstance, PropType } from 'vue'
import type { VueWrapper } from '@vue/test-utils'
const TestComp = defineComponent({
setup() {
const { t } = useLocale()
props: {
locale: {
type: Object as PropType<Language>,
default: Chinese,
},
},
setup(props) {
const { t } = useLocale(computed(() => props.locale))
return () => (
<div class="locale-manifest">{t('el.popconfirm.confirmButtonText')}</div>
)
@@ -22,20 +27,7 @@ describe('use-locale', () => {
let wrapper: VueWrapper<ComponentPublicInstance>
beforeEach(() => {
wrapper = mount(
defineComponent({
props: {
locale: {
type: Object as PropType<Language>,
default: Chinese,
},
},
setup(props) {
provideGlobalConfig(computed(() => ({ locale: props.locale })))
return () => <TestComp />
},
})
)
wrapper = mount(TestComp)
})
afterEach(() => {

View File

@@ -1,7 +1,6 @@
import { computed, defineComponent, nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { provideGlobalConfig } from '@element-plus/components/config-provider'
import { useNamespace } from '..'
import type { VueWrapper } from '@vue/test-utils'
@@ -45,17 +44,9 @@ const TestComp = defineComponent({
})
describe('use-namespace', () => {
const Comp = defineComponent({
setup(_props, { slots }) {
provideGlobalConfig({ namespace: 'ep' })
return () => slots.default?.()
},
})
let wrapper: VueWrapper<InstanceType<typeof Comp>>
let wrapper: VueWrapper<InstanceType<typeof TestComp>>
beforeEach(() => {
wrapper = mount(Comp, {
slots: { default: () => <TestComp /> },
})
wrapper = mount(TestComp)
})
afterEach(() => {
@@ -65,21 +56,21 @@ describe('use-namespace', () => {
it('should provide bem correctly', async () => {
await nextTick()
expect(wrapper.find('#testId').classes()).toEqual([
'ep-table', // b()
'ep-table-body', // b('body')
'ep-table__content', // e('content')
'ep-table--active', // m('active')
'ep-table-content__active', // be('content', 'active')
'ep-table__content--active', // em('content', 'active')
'ep-table-body__content--active', // bem('body', 'content', 'active')
'el-table', // b()
'el-table-body', // b('body')
'el-table__content', // e('content')
'el-table--active', // m('active')
'el-table-content__active', // be('content', 'active')
'el-table__content--active', // em('content', 'active')
'el-table-body__content--active', // bem('body', 'content', 'active')
'is-focus', // is('focus')
])
const style = wrapper.find('#testId').attributes('style')
expect(style).toMatch('--ep-border-style: solid;')
expect(style).not.toMatch('--ep-border-width:')
expect(style).toMatch('--ep-table-text-color: #409eff;')
expect(style).not.toMatch('--ep-table-active-color:')
expect(style).toMatch('--el-border-style: solid;')
expect(style).not.toMatch('--el-border-width:')
expect(style).toMatch('--el-table-text-color: #409eff;')
expect(style).not.toMatch('--el-table-active-color:')
})
it('overrides namespace', () => {

View File

@@ -1,7 +1,8 @@
import { getCurrentInstance, onMounted, ref, shallowRef, watch } from 'vue'
import { useEventListener } from '@vueuse/core'
import { isElement, isFocusable, isFunction } from '@element-plus/utils'
import { useFormDisabled } from '@element-plus/components/form/src/hooks/use-form-common-props'
// eslint-disable-next-line no-restricted-imports
import { useFormDisabled } from '@element-plus/components/form/src/hooks/use-form-common-props' // TODO: remove this
import type { ShallowRef } from 'vue'
interface UseFocusControllerOptions {