Skip to content

Commit f6d8bb6

Browse files
authored
feat: support features.formatters to format CSS and HTML (#520)
1 parent b413fbd commit f6d8bb6

File tree

12 files changed

+595
-16
lines changed

12 files changed

+595
-16
lines changed

docs/pages/index.vue

+1-5
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,7 @@ watch(projectsSectionVisible, () => {
129129
top: 25vh;
130130
width: 100%;
131131
height: 30vh;
132-
background: radial-gradient(
133-
50% 50% at 50% 50%,
134-
#00dc82 0%,
135-
rgba(0, 220, 130, 0) 100%
136-
);
132+
background: radial-gradient(50% 50% at 50% 50%, #00dc82 0%, rgba(0, 220, 130, 0) 100%);
137133
filter: blur(180px);
138134
opacity: 0.6;
139135
z-index: -1;

eslint.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export default createConfigForNuxt({
55
features: {
66
stylistic: true,
77
tooling: true,
8+
formatters: true,
89
},
910
dirs: {
1011
src: [

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@vueuse/nuxt": "catalog:",
3636
"bumpp": "catalog:",
3737
"eslint": "catalog:",
38+
"eslint-plugin-format": "^0.1.2",
3839
"fast-glob": "catalog:",
3940
"nuxt": "catalog:",
4041
"nuxt-og-image": "catalog:",

packages/eslint-config/package.json

+11-1
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,17 @@
4848
"prepack": "pnpm run build"
4949
},
5050
"peerDependencies": {
51-
"eslint": "^8.57.0 || ^9.0.0"
51+
"eslint": "^8.57.0 || ^9.0.0",
52+
"eslint-plugin-format": "*"
53+
},
54+
"peerDependenciesMeta": {
55+
"eslint-plugin-format": {
56+
"optional": true
57+
}
5258
},
5359
"dependencies": {
60+
"@antfu/install-pkg": "^0.4.1",
61+
"@clack/prompts": "^0.8.1",
5462
"@eslint/js": "catalog:",
5563
"@nuxt/eslint-plugin": "workspace:*",
5664
"@stylistic/eslint-plugin": "catalog:",
@@ -63,6 +71,8 @@
6371
"eslint-plugin-regexp": "catalog:",
6472
"eslint-plugin-unicorn": "catalog:",
6573
"eslint-plugin-vue": "catalog:",
74+
"eslint-processor-vue-blocks": "^0.1.2",
75+
"eslint-merge-processors": "^0.1.0",
6676
"globals": "catalog:",
6777
"local-pkg": "catalog:",
6878
"pathe": "catalog:",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import type { StylisticCustomizeOptions } from '@stylistic/eslint-plugin'
2+
import type { Linter } from 'eslint'
3+
import { isPackageExists } from 'local-pkg'
4+
import type { OptionsFormatters } from '../types'
5+
import { ensurePackages, interopDefault, parserPlain } from '../utils'
6+
import { GLOB_CSS, GLOB_GRAPHQL, GLOB_HTML, GLOB_LESS, GLOB_MARKDOWN, GLOB_POSTCSS, GLOB_SCSS, GLOB_SVG, GLOB_XML } from '../globs'
7+
8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
9+
type VendoredPrettierOptions = any
10+
11+
function mergePrettierOptions(
12+
options: VendoredPrettierOptions,
13+
overrides: VendoredPrettierOptions = {},
14+
): VendoredPrettierOptions {
15+
return {
16+
...options,
17+
...overrides,
18+
plugins: [
19+
...(overrides.plugins || []),
20+
...(options.plugins || []),
21+
],
22+
}
23+
}
24+
25+
export async function formatters(
26+
options: OptionsFormatters | boolean = {},
27+
stylistic: StylisticCustomizeOptions<true>,
28+
): Promise<Linter.Config[]> {
29+
if (!options)
30+
return []
31+
32+
if (options === true) {
33+
const isPrettierPluginXmlInScope = isPackageExists('@prettier/plugin-xml')
34+
options = {
35+
css: true,
36+
graphql: true,
37+
html: true,
38+
// Markdown is disabled by default as many Nuxt projects use MDC with @nuxt/content,
39+
// where Prettier doesn't fully understand.
40+
markdown: false,
41+
svg: isPrettierPluginXmlInScope,
42+
xml: isPrettierPluginXmlInScope,
43+
}
44+
}
45+
46+
await ensurePackages([
47+
'eslint-plugin-format',
48+
(options.xml || options.svg) ? '@prettier/plugin-xml' : undefined,
49+
])
50+
51+
const {
52+
indent,
53+
quotes,
54+
semi,
55+
} = {
56+
indent: 2,
57+
quotes: 'single',
58+
semi: false,
59+
...stylistic,
60+
}
61+
62+
const prettierOptions: VendoredPrettierOptions = Object.assign(
63+
{
64+
endOfLine: 'auto',
65+
printWidth: 120,
66+
semi,
67+
singleQuote: quotes === 'single',
68+
tabWidth: typeof indent === 'number' ? indent : 2,
69+
trailingComma: 'all',
70+
useTabs: indent === 'tab',
71+
} satisfies VendoredPrettierOptions,
72+
options.prettierOptions || {},
73+
)
74+
75+
const prettierXmlOptions: VendoredPrettierOptions = {
76+
xmlQuoteAttributes: 'double',
77+
xmlSelfClosingSpace: true,
78+
xmlSortAttributesByKey: false,
79+
xmlWhitespaceSensitivity: 'ignore',
80+
}
81+
82+
const dprintOptions = Object.assign(
83+
{
84+
indentWidth: typeof indent === 'number' ? indent : 2,
85+
quoteStyle: quotes === 'single' ? 'preferSingle' : 'preferDouble',
86+
useTabs: indent === 'tab',
87+
},
88+
options.dprintOptions || {},
89+
)
90+
91+
const pluginFormat = await interopDefault(import('eslint-plugin-format'))
92+
93+
const configs: Linter.Config[] = [
94+
{
95+
name: 'nuxt/formatter/setup',
96+
plugins: {
97+
format: pluginFormat,
98+
},
99+
},
100+
]
101+
102+
if (options.css) {
103+
configs.push(
104+
{
105+
files: [GLOB_CSS, GLOB_POSTCSS],
106+
languageOptions: {
107+
parser: parserPlain,
108+
},
109+
name: 'nuxt/formatter/css',
110+
rules: {
111+
'format/prettier': [
112+
'error',
113+
mergePrettierOptions(prettierOptions, {
114+
parser: 'css',
115+
}),
116+
],
117+
},
118+
},
119+
{
120+
files: [GLOB_SCSS],
121+
languageOptions: {
122+
parser: parserPlain,
123+
},
124+
name: 'nuxt/formatter/scss',
125+
rules: {
126+
'format/prettier': [
127+
'error',
128+
mergePrettierOptions(prettierOptions, {
129+
parser: 'scss',
130+
}),
131+
],
132+
},
133+
},
134+
{
135+
files: [GLOB_LESS],
136+
languageOptions: {
137+
parser: parserPlain,
138+
},
139+
name: 'nuxt/formatter/less',
140+
rules: {
141+
'format/prettier': [
142+
'error',
143+
mergePrettierOptions(prettierOptions, {
144+
parser: 'less',
145+
}),
146+
],
147+
},
148+
},
149+
)
150+
}
151+
152+
if (options.html) {
153+
configs.push({
154+
files: [GLOB_HTML],
155+
languageOptions: {
156+
parser: parserPlain,
157+
},
158+
name: 'nuxt/formatter/html',
159+
rules: {
160+
'format/prettier': [
161+
'error',
162+
mergePrettierOptions(prettierOptions, {
163+
parser: 'html',
164+
}),
165+
],
166+
},
167+
})
168+
}
169+
170+
if (options.xml) {
171+
configs.push({
172+
files: [GLOB_XML],
173+
languageOptions: {
174+
parser: parserPlain,
175+
},
176+
name: 'nuxt/formatter/xml',
177+
rules: {
178+
'format/prettier': [
179+
'error',
180+
mergePrettierOptions({ ...prettierXmlOptions, ...prettierOptions }, {
181+
parser: 'xml',
182+
plugins: [
183+
'@prettier/plugin-xml',
184+
],
185+
}),
186+
],
187+
},
188+
})
189+
}
190+
if (options.svg) {
191+
configs.push({
192+
files: [GLOB_SVG],
193+
languageOptions: {
194+
parser: parserPlain,
195+
},
196+
name: 'nuxt/formatter/svg',
197+
rules: {
198+
'format/prettier': [
199+
'error',
200+
mergePrettierOptions({ ...prettierXmlOptions, ...prettierOptions }, {
201+
parser: 'xml',
202+
plugins: [
203+
'@prettier/plugin-xml',
204+
],
205+
}),
206+
],
207+
},
208+
})
209+
}
210+
211+
if (options.markdown) {
212+
const formater = options.markdown === true
213+
? 'prettier'
214+
: options.markdown
215+
216+
configs.push({
217+
files: [GLOB_MARKDOWN],
218+
languageOptions: {
219+
parser: parserPlain,
220+
},
221+
name: 'nuxt/formatter/markdown',
222+
rules: {
223+
[`format/${formater}`]: [
224+
'error',
225+
formater === 'prettier'
226+
? mergePrettierOptions(prettierOptions, {
227+
embeddedLanguageFormatting: 'off',
228+
parser: 'markdown',
229+
})
230+
: {
231+
...dprintOptions,
232+
language: 'markdown',
233+
},
234+
],
235+
},
236+
})
237+
}
238+
239+
if (options.graphql) {
240+
configs.push({
241+
files: [GLOB_GRAPHQL],
242+
languageOptions: {
243+
parser: parserPlain,
244+
},
245+
name: 'nuxt/formatter/graphql',
246+
rules: {
247+
'format/prettier': [
248+
'error',
249+
mergePrettierOptions(prettierOptions, {
250+
parser: 'graphql',
251+
}),
252+
],
253+
},
254+
})
255+
}
256+
257+
return configs
258+
}

packages/eslint-config/src/flat/configs/javascript.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// @ts-expect-error missing types
21
import pluginESLint from '@eslint/js'
32
import type { Linter } from 'eslint'
43
import globals from 'globals'

packages/eslint-config/src/flat/configs/vue.ts

+22-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as parserVue from 'vue-eslint-parser'
22
import pluginVue from 'eslint-plugin-vue'
3+
import processorVueBlocks from 'eslint-processor-vue-blocks'
34
import type { Linter } from 'eslint'
5+
import { mergeProcessors } from 'eslint-merge-processors'
46
import type { NuxtESLintConfigOptions } from '../types'
57
import { removeUndefined, resolveOptions } from '../utils'
68

@@ -20,7 +22,7 @@ export default async function vue(options: NuxtESLintConfigOptions): Promise<Lin
2022
commaDangle = 'always-multiline',
2123
} = typeof resolved.features.stylistic === 'boolean' ? {} : resolved.features.stylistic
2224

23-
return [
25+
const configs: Linter.Config[] = [
2426
{
2527
name: 'nuxt/vue/setup',
2628
plugins: {
@@ -65,9 +67,17 @@ export default async function vue(options: NuxtESLintConfigOptions): Promise<Lin
6567
languageOptions: {
6668
parser: parserVue,
6769
},
68-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69-
processor: pluginVue.processors['.vue'] as any,
70-
rules: removeUndefined({
70+
processor: options.features?.formatters
71+
? mergeProcessors([
72+
pluginVue.processors['.vue'],
73+
processorVueBlocks({
74+
blocks: {
75+
styles: true,
76+
},
77+
}),
78+
])
79+
: pluginVue.processors['.vue'],
80+
rules: {
7181
...pluginVue.configs.base.rules,
7282
...pluginVue.configs['vue3-essential'].rules,
7383
...pluginVue.configs['vue3-strongly-recommended'].rules,
@@ -134,7 +144,14 @@ export default async function vue(options: NuxtESLintConfigOptions): Promise<Lin
134144
'vue/no-spaces-around-equal-signs-in-attribute': undefined,
135145
'vue/singleline-html-element-content-newline': undefined,
136146
}),
137-
}),
147+
},
138148
},
139149
]
150+
151+
for (const config of configs) {
152+
if (config.rules)
153+
config.rules = removeUndefined(config.rules)
154+
}
155+
156+
return configs
140157
}

0 commit comments

Comments
 (0)