Skip to content

Commit 5f7d650

Browse files
MoogyGMALENFERT Yoandanielroe
authored
feat: allow throwing an error in ci if validation fails (#96)
Co-authored-by: MALENFERT Yoan <[email protected]> Co-authored-by: Daniel Roe <[email protected]>
1 parent 68e0d4c commit 5f7d650

File tree

5 files changed

+59
-31
lines changed

5 files changed

+59
-31
lines changed

docs/content/en/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ This module configures [`html-validate`](https://html-validate.org/) to automati
7575

7676
<alert>Consider not enabling this if you are using TailwindCSS, as prettier will struggle to cope with parsing the size of your HTML in development mode.</alert>
7777

78+
- `failOnError` will throw an error after running `nuxt generate` if there are any validation errors with the generated pages.
79+
80+
<alert>Useful in continuous integration.</alert>
81+
7882
- `options` allows you to pass in `html-validate` options that will be merged with the default configuration
7983

8084
<alert type="info">You can find more about configuring `html-validate` [here](https://html-validate.org/rules/index.html).</alert>
@@ -85,6 +89,7 @@ This module configures [`html-validate`](https://html-validate.org/) to automati
8589
{
8690
htmlValidator: {
8791
usePrettier: false,
92+
failOnError: false,
8893
options: {
8994
extends: [
9095
'html-validate:document',

src/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ export const defaultHtmlValidateConfig: ConfigData = {
2424

2525
export interface ModuleOptions {
2626
usePrettier?: boolean
27+
failOnError?: boolean
2728
options?: ConfigData
2829
}
2930

3031
export const DEFAULTS: Required<ModuleOptions> = {
3132
usePrettier: false,
33+
failOnError: false,
3234
options: defaultHtmlValidateConfig
3335
}

src/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,21 @@ const nuxtModule: Module<ModuleOptions> = function (moduleOptions) {
1414
)
1515

1616
const providedOptions = defu(this.options[CONFIG_KEY] || {}, moduleOptions)
17-
const { usePrettier, options } = defu(providedOptions, DEFAULTS)
17+
const { usePrettier, failOnError, options } = defu(providedOptions, DEFAULTS)
1818
if (options && providedOptions.options && providedOptions.options.extends) {
1919
options.extends = providedOptions.options.extends
2020
}
2121
const { validator } = useValidator(options)
2222

23-
const checkHTML = useChecker(validator, usePrettier)
23+
const { checkHTML, invalidPages } = useChecker(validator, usePrettier)
2424

2525
this.nuxt.hook('render:route', (url: string, result: { html: string }) => checkHTML(url, result.html))
2626
this.nuxt.hook('generate:page', ({ path, html }: { path: string, html: string }) => checkHTML(path, html))
27+
this.nuxt.hook('generate:done', () => {
28+
if (failOnError && invalidPages.length) {
29+
throw new Error('html-validator found errors')
30+
}
31+
})
2732
}
2833

2934
;(nuxtModule as any).meta = { name: '@nuxtjs/html-validator' }

src/validator.ts

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,46 @@ export const useChecker = (
2222
validator: HtmlValidate,
2323
usePrettier = false,
2424
reporter = consola.withTag('html-validate')
25-
) => async (url: string, html: string) => {
26-
let couldFormat = false
27-
try {
28-
if (usePrettier) {
29-
const { format } = await import('prettier')
30-
html = format(html, { parser: 'html' })
31-
couldFormat = true
32-
}
25+
) => {
26+
const invalidPages: string[] = []
27+
28+
const checkHTML = async (url: string, html: string) => {
29+
let couldFormat = false
30+
try {
31+
if (usePrettier) {
32+
const { format } = await import('prettier')
33+
html = format(html, { parser: 'html' })
34+
couldFormat = true
35+
}
3336
// eslint-disable-next-line
3437
} catch (e) {
35-
reporter.error(e)
36-
}
38+
reporter.error(e)
39+
}
3740

38-
// Clean up Vue scoped style attributes
39-
html = typeof html === 'string' ? html.replace(/ ?data-v-[a-z0-9]+\b/g, '') : html
41+
// Clean up Vue scoped style attributes
42+
html = typeof html === 'string' ? html.replace(/ ?data-v-[a-z0-9]+\b/g, '') : html
4043

41-
const { valid, results } = validator.validateString(html)
44+
const { valid, results } = validator.validateString(html)
4245

43-
if (valid) {
44-
return reporter.success(
46+
if (valid) {
47+
return reporter.success(
4548
`No HTML validation errors found for ${chalk.bold(url)}`
46-
)
47-
}
49+
)
50+
}
51+
52+
invalidPages.push(url)
4853

49-
const formatter = couldFormat ? formatterFactory('codeframe') : formatterFactory('stylish')
54+
const formatter = couldFormat ? formatterFactory('codeframe') : formatterFactory('stylish')
5055

51-
const formattedResult = formatter!(results)
56+
const formattedResult = formatter!(results)
5257

53-
reporter.error(
54-
[
58+
reporter.error(
59+
[
5560
`HTML validation errors found for ${chalk.bold(url)}`,
5661
formattedResult
57-
].join('\n')
58-
)
62+
].join('\n')
63+
)
64+
}
65+
66+
return { checkHTML, invalidPages }
5967
}

test/checker.test.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('useChecker', () => {
2929

3030
it('works with a consola reporter', async () => {
3131
const mockValidator = jest.fn().mockImplementation(() => ({ valid: false, results: [] }))
32-
const checker = useChecker({ validateString: mockValidator } as any)
32+
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any)
3333

3434
await checker('https://test.com/', '<a>Link</a>')
3535
expect(mockValidator).toHaveBeenCalled()
@@ -38,7 +38,7 @@ describe('useChecker', () => {
3838

3939
it('calls the provided validator', async () => {
4040
const mockValidator = jest.fn().mockImplementation(() => ({ valid: true, results: [] }))
41-
const checker = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
41+
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
4242

4343
await checker('https://test.com/', '<a>Link</a>')
4444
expect(mockValidator).toHaveBeenCalled()
@@ -47,16 +47,24 @@ describe('useChecker', () => {
4747

4848
it('prints an error message when invalid html is provided', async () => {
4949
const mockValidator = jest.fn().mockImplementation(() => ({ valid: false, results: [] }))
50-
const checker = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
50+
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
5151

5252
await checker('https://test.com/', '<a>Link</a>')
5353
expect(mockValidator).toHaveBeenCalled()
5454
expect(mockReporter.error).toHaveBeenCalled()
5555
})
5656

57+
it('records urls when invalid html is provided', async () => {
58+
const mockValidator = jest.fn().mockImplementation(() => ({ valid: false, results: [] }))
59+
const { checkHTML: checker, invalidPages } = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
60+
61+
await checker('https://test.com/', '<a>Link</a>')
62+
expect(invalidPages).toContain('https://test.com/')
63+
})
64+
5765
it('ignores Vue-generated scoped data attributes', async () => {
5866
const mockValidator = jest.fn().mockImplementation(() => ({ valid: true, results: [] }))
59-
const checker = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
67+
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
6068

6169
await checker(
6270
'https://test.com/',
@@ -70,7 +78,7 @@ describe('useChecker', () => {
7078

7179
it('formats HTML with prettier when asked to do so', async () => {
7280
const mockValidator = jest.fn().mockImplementation(() => ({ valid: false, results: [] }))
73-
const checker = useChecker({ validateString: mockValidator } as any, true, mockReporter as any)
81+
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, true, mockReporter as any)
7482

7583
await checker('https://test.com/', '<a>Link</a>')
7684
expect(prettier.format).toHaveBeenCalledWith('<a>Link</a>', { parser: 'html' })
@@ -79,7 +87,7 @@ describe('useChecker', () => {
7987

8088
it('falls back gracefully when prettier cannot format', async () => {
8189
const mockValidator = jest.fn().mockImplementation(() => ({ valid: false, results: [] }))
82-
const checker = useChecker({ validateString: mockValidator } as any, true, mockReporter as any)
90+
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, true, mockReporter as any)
8391

8492
await checker('https://test.com/', Symbol as any)
8593
expect(prettier.format).toHaveBeenCalledWith(Symbol, { parser: 'html' })

0 commit comments

Comments
 (0)