Skip to content

Commit f6d8bb6

Browse files
authoredNov 16, 2024··
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
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export const GLOB_SRC_EXT = '?([cm])[jt]s?(x)'
2+
export const GLOB_SRC = '**/*.?([cm])[jt]s?(x)'
3+
4+
export const GLOB_JS = '**/*.?([cm])js'
5+
export const GLOB_JSX = '**/*.?([cm])jsx'
6+
7+
export const GLOB_TS = '**/*.?([cm])ts'
8+
export const GLOB_TSX = '**/*.?([cm])tsx'
9+
10+
export const GLOB_STYLE = '**/*.{c,le,sc}ss'
11+
export const GLOB_CSS = '**/*.css'
12+
export const GLOB_POSTCSS = '**/*.{p,post}css'
13+
export const GLOB_LESS = '**/*.less'
14+
export const GLOB_SCSS = '**/*.scss'
15+
16+
export const GLOB_JSON = '**/*.json'
17+
export const GLOB_JSON5 = '**/*.json5'
18+
export const GLOB_JSONC = '**/*.jsonc'
19+
20+
export const GLOB_MARKDOWN = '**/*.md'
21+
export const GLOB_MARKDOWN_IN_MARKDOWN = '**/*.md/*.md'
22+
export const GLOB_SVELTE = '**/*.svelte'
23+
export const GLOB_VUE = '**/*.vue'
24+
export const GLOB_YAML = '**/*.y?(a)ml'
25+
export const GLOB_TOML = '**/*.toml'
26+
export const GLOB_XML = '**/*.xml'
27+
export const GLOB_SVG = '**/*.svg'
28+
export const GLOB_HTML = '**/*.htm?(l)'
29+
export const GLOB_ASTRO = '**/*.astro'
30+
export const GLOB_ASTRO_TS = '**/*.astro/*.ts'
31+
export const GLOB_GRAPHQL = '**/*.{g,graph}ql'
32+
33+
export const GLOB_MARKDOWN_CODE = `${GLOB_MARKDOWN}/${GLOB_SRC}`
34+
35+
export const GLOB_TESTS = [
36+
`**/__tests__/**/*.${GLOB_SRC_EXT}`,
37+
`**/*.spec.${GLOB_SRC_EXT}`,
38+
`**/*.test.${GLOB_SRC_EXT}`,
39+
`**/*.bench.${GLOB_SRC_EXT}`,
40+
`**/*.benchmark.${GLOB_SRC_EXT}`,
41+
]
42+
43+
export const GLOB_ALL_SRC = [
44+
GLOB_SRC,
45+
GLOB_STYLE,
46+
GLOB_JSON,
47+
GLOB_JSON5,
48+
GLOB_MARKDOWN,
49+
GLOB_SVELTE,
50+
GLOB_VUE,
51+
GLOB_YAML,
52+
GLOB_XML,
53+
GLOB_HTML,
54+
]

‎packages/eslint-config/src/flat/index.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,22 @@ export function createConfigForNuxt(
6767
)
6868
}
6969

70-
if (resolved.features.stylistic) {
71-
const stylisticOptions = typeof resolved.features.stylistic === 'boolean'
72-
? {}
73-
: resolved.features.stylistic
70+
const stylisticOptions = typeof resolved.features.stylistic === 'boolean'
71+
? {}
72+
: resolved.features.stylistic
7473

74+
if (resolved.features.stylistic) {
7575
c.append(
7676
import('./configs/stylistic').then(m => m.default(stylisticOptions)),
7777
)
7878
}
7979

80+
if (resolved.features.formatters) {
81+
c.append(
82+
import('./configs/formatters').then(m => m.formatters(resolved.features.formatters, stylisticOptions)),
83+
)
84+
}
85+
8086
c.append(
8187
disables(resolved),
8288
)

‎packages/eslint-config/src/flat/types.ts

+68
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ export interface NuxtESLintFeaturesOptions {
5858
*/
5959
stylistic?: boolean | StylisticCustomizeOptions<true>
6060

61+
/**
62+
* Enable formatters to handling formatting for different file types
63+
*
64+
* Requires `eslint-plugin-format` to be installed
65+
*
66+
* @default false
67+
*/
68+
formatters?: boolean | OptionsFormatters
69+
6170
/**
6271
* Options for Nuxt specific rules
6372
*/
@@ -140,6 +149,65 @@ export interface NuxtESLintConfigOptions {
140149
}
141150
}
142151

152+
export interface OptionsFormatters {
153+
/**
154+
* Enable formatting support for CSS, Less, Sass, and SCSS.
155+
*
156+
* Currently only support Prettier.
157+
*/
158+
css?: 'prettier' | boolean
159+
160+
/**
161+
* Enable formatting support for HTML.
162+
*
163+
* Currently only support Prettier.
164+
*/
165+
html?: 'prettier' | boolean
166+
167+
/**
168+
* Enable formatting support for XML.
169+
*
170+
* Currently only support Prettier.
171+
*/
172+
xml?: 'prettier' | boolean
173+
174+
/**
175+
* Enable formatting support for SVG.
176+
*
177+
* Currently only support Prettier.
178+
*/
179+
svg?: 'prettier' | boolean
180+
181+
/**
182+
* Enable formatting support for Markdown.
183+
*
184+
* Support both Prettier and dprint.
185+
*
186+
* When set to `true`, it will use Prettier.
187+
*/
188+
markdown?: 'prettier' | 'dprint' | boolean
189+
190+
/**
191+
* Enable formatting support for GraphQL.
192+
*/
193+
graphql?: 'prettier' | boolean
194+
195+
/**
196+
* Custom options for Prettier.
197+
*
198+
* By default it's controlled by our own config.
199+
*/
200+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
201+
prettierOptions?: any
202+
203+
/**
204+
* Custom options for dprint.
205+
*
206+
* By default it's controlled by our own config.
207+
*/
208+
dprintOptions?: boolean
209+
}
210+
143211
type NotNill<T> = T extends null | undefined ? never : T
144212

145213
export interface NuxtESLintConfigOptionsResolved {

‎packages/eslint-config/src/flat/utils.ts

+46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
1+
import process from 'node:process'
12
import { isPackageExists } from 'local-pkg'
23
import type { NuxtESLintConfigOptions, NuxtESLintConfigOptionsResolved } from '../flat'
4+
import type { Awaitable } from './types'
5+
6+
export const parserPlain = {
7+
meta: {
8+
name: 'parser-plain',
9+
},
10+
parseForESLint: (code: string) => ({
11+
ast: {
12+
body: [],
13+
comments: [],
14+
loc: { end: code.length, start: 0 },
15+
range: [0, code.length],
16+
tokens: [],
17+
type: 'Program',
18+
},
19+
scopeManager: null,
20+
services: { isPlain: true },
21+
visitorKeys: {
22+
Program: [],
23+
},
24+
}),
25+
}
26+
27+
export async function ensurePackages(packages: (string | undefined)[]): Promise<void> {
28+
if (process.env.CI || process.stdout.isTTY === false)
29+
return
30+
31+
const nonExistingPackages = packages.filter(i => i && !isPackageExists(i)) as string[]
32+
if (nonExistingPackages.length === 0)
33+
return
34+
35+
const p = await import('@clack/prompts')
36+
const result = await p.confirm({
37+
message: `${nonExistingPackages.length === 1 ? 'Package is' : 'Packages are'} required for this config: ${nonExistingPackages.join(', ')}. Do you want to install them?`,
38+
})
39+
if (result)
40+
await import('@antfu/install-pkg').then(i => i.installPackage(nonExistingPackages, { dev: true }))
41+
}
42+
43+
export async function interopDefault<T>(m: Awaitable<T>): Promise<T extends { default: infer U } ? U : T> {
44+
const resolved = await m
45+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
46+
return (resolved as any).default || resolved
47+
}
348

449
export function removeUndefined<T extends object>(obj: T): T {
550
return Object.fromEntries(Object.entries(obj).filter(([, value]) => value !== undefined)) as T
@@ -34,6 +79,7 @@ export function resolveOptions(
3479
stylistic: false,
3580
typescript: isPackageExists('typescript'),
3681
tooling: false,
82+
formatters: false,
3783
nuxt: {},
3884
...config.features,
3985
},

‎pnpm-lock.yaml

+123
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)
Please sign in to comment.