@yoyoboot/web-lint-config 10.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/cli.mjs +27 -0
- package/eslint.mjs +42 -0
- package/package.json +50 -0
- package/quality.mjs +260 -0
- package/stylelint.mjs +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# `@yoyoboot/web-lint-config`
|
|
2
|
+
|
|
3
|
+
YoyoBoot 的公共 Web 语义检查预设,提供 ESLint flat config、Stylelint 16 config 与只减不增的历史债务门禁。Prettier 仍由消费者拥有格式偏好,项目架构规则留在项目本地配置。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// eslint.config.mjs
|
|
9
|
+
import { webLintConfig } from '@yoyoboot/web-lint-config/eslint'
|
|
10
|
+
export default webLintConfig({ environment: 'browser' })
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// stylelint.config.mjs
|
|
15
|
+
import { styleLintConfig } from '@yoyoboot/web-lint-config/stylelint'
|
|
16
|
+
export default styleLintConfig({ tailwind: true })
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Stylelint 预设使用字符串形式的 `postcss-html` / `postcss-less` custom syntax,兼容声明的 Stylelint 16.11+ peer 范围;Vue SFC 为兼容 `<style lang="less">` 不把 Less function 与 `//` comment 当作 CSS 语义错误,其它样式 correctness 规则仍保持启用。
|
|
20
|
+
|
|
21
|
+
消费者通过 `quality.config.mjs` 显式声明 eslint/style/format glob,并使用 `web-quality ci` 执行渐进门禁。首次基线只能在仓库历史中不存在该文件时用 `web-quality init` 建立;后续只能运行 `web-quality prune` 删除已消失条目,禁止手工扩容、总数额度或通配符忽略。
|
|
22
|
+
|
|
23
|
+
源 manifest 始终保持 `private: true` 与 `0.0.0-private`。产品版本由 Unified Release Train staging 注入;本地 tarball 或 prepare-only 结果不代表 registry 已发布。
|
package/cli.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { runQuality } from './quality.mjs'
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
const [mode = 'check', tool = 'all', flag] = args
|
|
7
|
+
try {
|
|
8
|
+
if (
|
|
9
|
+
args.length > 3 ||
|
|
10
|
+
!['check', 'ci', 'report', 'init', 'prune'].includes(mode) ||
|
|
11
|
+
!['all', 'eslint', 'style', 'format'].includes(tool) ||
|
|
12
|
+
(flag && flag !== '--fix') ||
|
|
13
|
+
(mode !== 'check' && (flag || tool !== 'all'))
|
|
14
|
+
) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
'Usage: web-quality check [all|eslint|style|format] [--fix] | ci | report | init | prune',
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
const result = await runQuality({ mode, tool, fix: flag === '--fix' })
|
|
20
|
+
if (mode === 'report') {
|
|
21
|
+
await mkdir('.quality', { recursive: true })
|
|
22
|
+
await writeFile('.quality/report.json', `${JSON.stringify(result, null, 2)}\n`)
|
|
23
|
+
} else if (!result.passed) process.exitCode = 1
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(error)
|
|
26
|
+
process.exitCode = 2
|
|
27
|
+
}
|
package/eslint.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import prettier from 'eslint-config-prettier/flat'
|
|
3
|
+
import vue from 'eslint-plugin-vue'
|
|
4
|
+
import globals from 'globals'
|
|
5
|
+
import ts from 'typescript-eslint'
|
|
6
|
+
import vueParser from 'vue-eslint-parser'
|
|
7
|
+
|
|
8
|
+
export const defaultIgnores = ['**/node_modules/**', '**/dist/**', '**/coverage/**']
|
|
9
|
+
|
|
10
|
+
/** Project architecture and naming exceptions belong in the caller's config. */
|
|
11
|
+
export function webLintConfig({ environment = 'browser', ignores = [] } = {}) {
|
|
12
|
+
if (!['browser', 'node'].includes(environment)) throw new Error('Unknown lint environment')
|
|
13
|
+
const code = ['**/*.{js,mjs,cjs,jsx,ts,tsx,mts,cts,vue}']
|
|
14
|
+
const typed = ['**/*.{ts,tsx,mts,cts,vue}']
|
|
15
|
+
return ts.config(
|
|
16
|
+
{ ignores: [...defaultIgnores, ...ignores] },
|
|
17
|
+
{
|
|
18
|
+
files: code,
|
|
19
|
+
extends: [js.configs.recommended],
|
|
20
|
+
languageOptions: { globals: globals[environment], ecmaVersion: 'latest' },
|
|
21
|
+
},
|
|
22
|
+
{ files: typed, extends: [...ts.configs.recommended] },
|
|
23
|
+
{
|
|
24
|
+
files: ['**/*.vue'],
|
|
25
|
+
extends: [...vue.configs['flat/recommended']],
|
|
26
|
+
languageOptions: {
|
|
27
|
+
parser: vueParser,
|
|
28
|
+
parserOptions: { parser: ts.parser, extraFileExtensions: ['.vue'] },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
files: [
|
|
33
|
+
'**/*.{mjs,cjs}',
|
|
34
|
+
'**/*.config.{js,ts}',
|
|
35
|
+
'**/tools/**/*.{js,ts}',
|
|
36
|
+
'**/scripts/**/*.{js,ts}',
|
|
37
|
+
],
|
|
38
|
+
languageOptions: { globals: globals.node },
|
|
39
|
+
},
|
|
40
|
+
prettier,
|
|
41
|
+
)
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yoyoboot/web-lint-config",
|
|
3
|
+
"version": "10.5.5",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "共享 Web 语义检查预设与渐进质量门禁",
|
|
6
|
+
"homepage": "https://github.com/yoyoboot/YoyoBoot/tree/main/packages/web-lint-config",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/yoyoboot/YoyoBoot.git",
|
|
10
|
+
"directory": "packages/web-lint-config"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/yoyoboot/YoyoBoot/issues"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
"./eslint": "./eslint.mjs",
|
|
17
|
+
"./stylelint": "./stylelint.mjs",
|
|
18
|
+
"./quality": "./quality.mjs"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"web-quality": "./cli.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"eslint.mjs",
|
|
25
|
+
"stylelint.mjs",
|
|
26
|
+
"quality.mjs",
|
|
27
|
+
"cli.mjs",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@eslint/js": "9.39.1",
|
|
35
|
+
"eslint-config-prettier": "10.1.8",
|
|
36
|
+
"eslint-plugin-vue": "10.9.2",
|
|
37
|
+
"fast-glob": "3.3.3",
|
|
38
|
+
"globals": "17.7.0",
|
|
39
|
+
"postcss-html": "1.8.0",
|
|
40
|
+
"postcss-less": "6.0.0",
|
|
41
|
+
"typescript-eslint": "8.63.0",
|
|
42
|
+
"vue-eslint-parser": "10.4.1"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"eslint": "^9.15.0 || ^10.0.0",
|
|
46
|
+
"prettier": "^3.3.3",
|
|
47
|
+
"stylelint": "^16.11.0",
|
|
48
|
+
"typescript": ">=4.8.4 <6.1.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/quality.mjs
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { execFileSync } from 'node:child_process'
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { relative, resolve } from 'node:path'
|
|
5
|
+
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import fg from 'fast-glob'
|
|
7
|
+
import { ESLint } from 'eslint'
|
|
8
|
+
import stylelint from 'stylelint'
|
|
9
|
+
import * as prettier from 'prettier'
|
|
10
|
+
|
|
11
|
+
const portable = (value) => value.replaceAll('\\', '/')
|
|
12
|
+
const normalized = (value) => value.replaceAll('\r\n', '\n')
|
|
13
|
+
const hash = (value) => createHash('sha256').update(normalized(value)).digest('hex')
|
|
14
|
+
|
|
15
|
+
export function fingerprint({ tool, file, rule, message, source }) {
|
|
16
|
+
return hash(JSON.stringify([tool, portable(file), rule, message, normalized(source).trim()]))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function collectEntries(issues) {
|
|
20
|
+
const entries = {}
|
|
21
|
+
for (const issue of issues) {
|
|
22
|
+
if (issue.fatal) throw new Error(`Cannot baseline fatal issue: ${issue.file}: ${issue.message}`)
|
|
23
|
+
const key = fingerprint(issue)
|
|
24
|
+
if (entries[key]) entries[key].count += 1
|
|
25
|
+
else
|
|
26
|
+
entries[key] = {
|
|
27
|
+
tool: issue.tool,
|
|
28
|
+
file: issue.file,
|
|
29
|
+
rule: issue.rule,
|
|
30
|
+
message: issue.message,
|
|
31
|
+
count: 1,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Object.fromEntries(Object.entries(entries).sort(([a], [b]) => a.localeCompare(b)))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compareBaseline(current, baseline) {
|
|
38
|
+
const added = Object.entries(current).filter(
|
|
39
|
+
([key, item]) => item.count > (baseline[key]?.count ?? 0),
|
|
40
|
+
)
|
|
41
|
+
const removed = Object.entries(baseline).filter(
|
|
42
|
+
([key, item]) => item.count > (current[key]?.count ?? 0),
|
|
43
|
+
)
|
|
44
|
+
return { added, removed }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function pruneBaseline(current, baseline) {
|
|
48
|
+
return Object.fromEntries(
|
|
49
|
+
Object.entries(baseline)
|
|
50
|
+
.filter(([key]) => current[key])
|
|
51
|
+
.map(([key, item]) => [key, { ...item, count: Math.min(item.count, current[key].count) }]),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function validateBaseline(value) {
|
|
56
|
+
if (
|
|
57
|
+
value?.schemaVersion !== 1 ||
|
|
58
|
+
!value.entries ||
|
|
59
|
+
typeof value.entries !== 'object' ||
|
|
60
|
+
Array.isArray(value.entries)
|
|
61
|
+
) {
|
|
62
|
+
throw new Error('Invalid quality baseline schema')
|
|
63
|
+
}
|
|
64
|
+
for (const [key, entry] of Object.entries(value.entries)) {
|
|
65
|
+
if (!/^[a-f0-9]{64}$/.test(key) || !Number.isSafeInteger(entry.count) || entry.count < 1) {
|
|
66
|
+
throw new Error('Invalid quality baseline fingerprint/count')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function assertBaselineDoesNotGrow(baseline, previous) {
|
|
73
|
+
if (compareBaseline(baseline.entries, previous.entries).added.length) {
|
|
74
|
+
throw new Error('Quality baseline grew relative to Git base; remove added exceptions')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function checkGitBaseline(cwd, baselinePath) {
|
|
79
|
+
const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
80
|
+
cwd,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
}).trim()
|
|
83
|
+
const locator = portable(relative(gitRoot, baselinePath))
|
|
84
|
+
const base = process.env.WEB_LINT_BASE_REF || 'HEAD'
|
|
85
|
+
// Missing/invalid history must not silently disable the ratchet.
|
|
86
|
+
execFileSync('git', ['rev-parse', '--verify', `${base}^{commit}`], { cwd, stdio: 'pipe' })
|
|
87
|
+
const files = execFileSync('git', ['ls-tree', '--name-only', base, '--', locator], {
|
|
88
|
+
cwd: gitRoot,
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
}).trim()
|
|
91
|
+
if (!files) return undefined // Initial baseline is a reviewable addition in this change.
|
|
92
|
+
return validateBaseline(
|
|
93
|
+
JSON.parse(execFileSync('git', ['show', `${base}:${locator}`], { cwd, encoding: 'utf8' })),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function scan({ cwd, config, tool = 'all', fix = false }) {
|
|
98
|
+
const issues = []
|
|
99
|
+
const counts = {}
|
|
100
|
+
const ignored = [
|
|
101
|
+
'**/node_modules/**',
|
|
102
|
+
'**/dist/**',
|
|
103
|
+
'**/coverage/**',
|
|
104
|
+
'**/.quality/**',
|
|
105
|
+
...(config.ignore ?? []),
|
|
106
|
+
]
|
|
107
|
+
const filesFor = async (kind) => {
|
|
108
|
+
const patterns = config[kind]
|
|
109
|
+
if (!Array.isArray(patterns)) throw new Error(`Missing explicit ${kind} scope`)
|
|
110
|
+
const files = (
|
|
111
|
+
await fg(patterns, { cwd, ignore: ignored, onlyFiles: true, unique: true, dot: true })
|
|
112
|
+
).sort()
|
|
113
|
+
counts[kind] = files.length
|
|
114
|
+
if (!files.length && kind === 'eslint') throw new Error('ESLint scope matched no files')
|
|
115
|
+
return files
|
|
116
|
+
}
|
|
117
|
+
const add = (toolName, file, diagnostic, source) => {
|
|
118
|
+
const { rule, message, line = 1, column = 1, fatal = false } = diagnostic
|
|
119
|
+
const lines = normalized(source).split('\n')
|
|
120
|
+
issues.push({
|
|
121
|
+
tool: toolName,
|
|
122
|
+
file: portable(file),
|
|
123
|
+
rule,
|
|
124
|
+
message,
|
|
125
|
+
line,
|
|
126
|
+
column,
|
|
127
|
+
fatal,
|
|
128
|
+
source: lines.slice(line - 1, diagnostic.endLine ?? line).join('\n'),
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
if (tool === 'all' || tool === 'eslint') {
|
|
132
|
+
const files = await filesFor('eslint')
|
|
133
|
+
const eslint = new ESLint({ cwd, fix, errorOnUnmatchedPattern: true })
|
|
134
|
+
const results = await eslint.lintFiles(files)
|
|
135
|
+
if (fix) await ESLint.outputFixes(results)
|
|
136
|
+
for (const result of results) {
|
|
137
|
+
const file = portable(relative(cwd, result.filePath))
|
|
138
|
+
const source = result.output ?? (await readFile(result.filePath, 'utf8'))
|
|
139
|
+
for (const item of result.messages)
|
|
140
|
+
add(
|
|
141
|
+
'eslint',
|
|
142
|
+
file,
|
|
143
|
+
{
|
|
144
|
+
...item,
|
|
145
|
+
rule: item.ruleId ?? 'parser',
|
|
146
|
+
fatal: item.fatal || !item.ruleId,
|
|
147
|
+
},
|
|
148
|
+
source,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (tool === 'all' || tool === 'style') {
|
|
153
|
+
for (const file of await filesFor('style')) {
|
|
154
|
+
const result = await stylelint.lint({
|
|
155
|
+
cwd,
|
|
156
|
+
files: resolve(cwd, file),
|
|
157
|
+
configFile: resolve(cwd, 'stylelint.config.mjs'),
|
|
158
|
+
fix,
|
|
159
|
+
})
|
|
160
|
+
for (const entry of result.results) {
|
|
161
|
+
if (entry.invalidOptionWarnings.length || entry.parseErrors.length) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Stylelint configuration/parse failure: ${file}: ${JSON.stringify([...entry.invalidOptionWarnings, ...entry.parseErrors])}`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
const source = await readFile(resolve(cwd, file), 'utf8')
|
|
167
|
+
for (const item of entry.warnings)
|
|
168
|
+
add(
|
|
169
|
+
'style',
|
|
170
|
+
file,
|
|
171
|
+
{
|
|
172
|
+
...item,
|
|
173
|
+
message: item.text,
|
|
174
|
+
fatal: ['CssSyntaxError', 'syntax-string'].includes(item.rule),
|
|
175
|
+
},
|
|
176
|
+
source,
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (tool === 'all' || tool === 'format') {
|
|
182
|
+
for (const file of await filesFor('format')) {
|
|
183
|
+
const filePath = resolve(cwd, file)
|
|
184
|
+
const info = await prettier.getFileInfo(filePath, {
|
|
185
|
+
ignorePath: resolve(cwd, '.prettierignore'),
|
|
186
|
+
})
|
|
187
|
+
if (info.ignored || !info.inferredParser) continue
|
|
188
|
+
const options = { ...(await prettier.resolveConfig(filePath)), filepath: filePath }
|
|
189
|
+
const source = await readFile(filePath, 'utf8')
|
|
190
|
+
// Git may materialize the same tracked file as CRLF on Windows and LF on Linux.
|
|
191
|
+
// Line-ending policy belongs to Git; the format ratchet must fingerprint semantic
|
|
192
|
+
// formatting debt consistently on both hosts.
|
|
193
|
+
if (!(await prettier.check(normalized(source), options))) {
|
|
194
|
+
if (fix) await writeFile(filePath, await prettier.format(source, options))
|
|
195
|
+
else
|
|
196
|
+
issues.push({
|
|
197
|
+
tool: 'format',
|
|
198
|
+
file,
|
|
199
|
+
rule: 'prettier',
|
|
200
|
+
message: 'File differs from local Prettier configuration',
|
|
201
|
+
source: hash(source),
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const fatal = issues.filter((issue) => issue.fatal)
|
|
207
|
+
if (fatal.length) throw new Error(`Unbaselinable lint failure: ${JSON.stringify(fatal)}`)
|
|
208
|
+
return { counts, issues, entries: collectEntries(issues) }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function runQuality({
|
|
212
|
+
cwd = process.cwd(),
|
|
213
|
+
mode = 'check',
|
|
214
|
+
tool = 'all',
|
|
215
|
+
fix = false,
|
|
216
|
+
} = {}) {
|
|
217
|
+
const config = (await import(pathToFileURL(resolve(cwd, 'quality.config.mjs')).href)).default
|
|
218
|
+
const report = await scan({ cwd, config, tool, fix })
|
|
219
|
+
console.log(JSON.stringify({ scanned: report.counts, issues: report.issues.length }))
|
|
220
|
+
if (mode === 'report') return report
|
|
221
|
+
if (mode === 'check') {
|
|
222
|
+
for (const issue of report.issues)
|
|
223
|
+
console.error(`${issue.file}:${issue.line ?? 1} ${issue.rule}: ${issue.message}`)
|
|
224
|
+
return { ...report, passed: !report.issues.length }
|
|
225
|
+
}
|
|
226
|
+
const baselinePath = resolve(cwd, 'quality-baseline.json')
|
|
227
|
+
if (mode === 'init') {
|
|
228
|
+
try {
|
|
229
|
+
await readFile(baselinePath, 'utf8')
|
|
230
|
+
throw new Error('Quality baseline already exists; only prune is allowed after bootstrap')
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error.code !== 'ENOENT') throw error
|
|
233
|
+
}
|
|
234
|
+
if (checkGitBaseline(cwd, baselinePath)) {
|
|
235
|
+
throw new Error('Quality baseline exists in Git history; restore it and prune instead')
|
|
236
|
+
}
|
|
237
|
+
await writeFile(
|
|
238
|
+
baselinePath,
|
|
239
|
+
`${JSON.stringify({ schemaVersion: 1, entries: report.entries }, null, 2)}\n`,
|
|
240
|
+
{ flag: 'wx' },
|
|
241
|
+
)
|
|
242
|
+
console.log(`Bootstrapped ${Object.keys(report.entries).length} historical fingerprints`)
|
|
243
|
+
return { ...report, passed: true }
|
|
244
|
+
}
|
|
245
|
+
const baseline = validateBaseline(JSON.parse(await readFile(baselinePath, 'utf8')))
|
|
246
|
+
const previous = checkGitBaseline(cwd, baselinePath)
|
|
247
|
+
if (previous) assertBaselineDoesNotGrow(baseline, previous)
|
|
248
|
+
if (mode === 'prune') {
|
|
249
|
+
baseline.entries = pruneBaseline(report.entries, baseline.entries)
|
|
250
|
+
await writeFile(baselinePath, `${JSON.stringify(baseline, null, 2)}\n`)
|
|
251
|
+
}
|
|
252
|
+
const { added, removed } = compareBaseline(report.entries, baseline.entries)
|
|
253
|
+
for (const [, entry] of added) console.error(`NEW ${entry.file}: ${entry.rule}: ${entry.message}`)
|
|
254
|
+
if (removed.length)
|
|
255
|
+
console.error(`${removed.length} stale baseline entries; run lint:baseline:prune`)
|
|
256
|
+
console.log(
|
|
257
|
+
`Historical issues: ${report.issues.length}; new fingerprints: ${added.length}; retired fingerprints: ${removed.length}`,
|
|
258
|
+
)
|
|
259
|
+
return { ...report, passed: !added.length && !removed.length }
|
|
260
|
+
}
|
package/stylelint.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Explicit semantic rules: no standard/style formatting preset. */
|
|
2
|
+
export function styleLintConfig({ tailwind = false } = {}) {
|
|
3
|
+
return {
|
|
4
|
+
ignoreFiles: ['**/node_modules/**', '**/dist/**', '**/coverage/**'],
|
|
5
|
+
rules: {
|
|
6
|
+
'annotation-no-unknown': true,
|
|
7
|
+
'at-rule-no-unknown': [
|
|
8
|
+
true,
|
|
9
|
+
{
|
|
10
|
+
ignoreAtRules: tailwind
|
|
11
|
+
? [
|
|
12
|
+
'theme',
|
|
13
|
+
'source',
|
|
14
|
+
'utility',
|
|
15
|
+
'variant',
|
|
16
|
+
'custom-variant',
|
|
17
|
+
'apply',
|
|
18
|
+
'reference',
|
|
19
|
+
'config',
|
|
20
|
+
'plugin',
|
|
21
|
+
'tailwind',
|
|
22
|
+
]
|
|
23
|
+
: [],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
'block-no-empty': true,
|
|
27
|
+
'color-no-invalid-hex': true,
|
|
28
|
+
'comment-no-empty': true,
|
|
29
|
+
'custom-property-no-missing-var-function': true,
|
|
30
|
+
'declaration-block-no-duplicate-properties': [
|
|
31
|
+
true,
|
|
32
|
+
{ ignore: ['consecutive-duplicates-with-different-syntaxes'] },
|
|
33
|
+
],
|
|
34
|
+
'declaration-block-no-shorthand-property-overrides': true,
|
|
35
|
+
'font-family-no-duplicate-names': true,
|
|
36
|
+
'font-family-no-missing-generic-family-keyword': true,
|
|
37
|
+
'function-no-unknown': [
|
|
38
|
+
true,
|
|
39
|
+
{ ignoreFunctions: tailwind ? ['--alpha', '--spacing', 'theme'] : [] },
|
|
40
|
+
],
|
|
41
|
+
'keyframe-block-no-duplicate-selectors': true,
|
|
42
|
+
'keyframe-declaration-no-important': true,
|
|
43
|
+
'media-feature-name-no-unknown': true,
|
|
44
|
+
'no-descending-specificity': null,
|
|
45
|
+
'no-duplicate-at-import-rules': true,
|
|
46
|
+
'no-duplicate-selectors': true,
|
|
47
|
+
'no-invalid-double-slash-comments': true,
|
|
48
|
+
'property-no-unknown': true,
|
|
49
|
+
'selector-pseudo-class-no-unknown': [
|
|
50
|
+
true,
|
|
51
|
+
{ ignorePseudoClasses: ['deep', 'global', 'slotted'] },
|
|
52
|
+
],
|
|
53
|
+
'selector-pseudo-element-no-unknown': [
|
|
54
|
+
true,
|
|
55
|
+
{ ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'] },
|
|
56
|
+
],
|
|
57
|
+
'selector-type-no-unknown': [true, { ignore: ['custom-elements'] }],
|
|
58
|
+
'string-no-newline': true,
|
|
59
|
+
'unit-no-unknown': true,
|
|
60
|
+
},
|
|
61
|
+
overrides: [
|
|
62
|
+
{
|
|
63
|
+
files: ['**/*.vue'],
|
|
64
|
+
customSyntax: 'postcss-html',
|
|
65
|
+
rules: {
|
|
66
|
+
// Vue SFC may embed Less; file globs cannot distinguish <style lang="less">.
|
|
67
|
+
'function-no-unknown': null,
|
|
68
|
+
'no-invalid-double-slash-comments': null,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
files: ['**/*.less'],
|
|
73
|
+
customSyntax: 'postcss-less',
|
|
74
|
+
// Less extends CSS with variables, functions and mixins.
|
|
75
|
+
rules: {
|
|
76
|
+
'at-rule-no-unknown': null,
|
|
77
|
+
'function-no-unknown': null,
|
|
78
|
+
'no-invalid-double-slash-comments': null,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
}
|
|
83
|
+
}
|