Skip to content

Commit

Permalink
feat: allow throwing an error in ci if validation fails (#96)
Browse files Browse the repository at this point in the history
Co-authored-by: MALENFERT Yoan <[email protected]>
Co-authored-by: Daniel Roe <[email protected]>
  • Loading branch information
3 people authored Jul 26, 2021
1 parent 68e0d4c commit 5f7d650
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 31 deletions.
5 changes: 5 additions & 0 deletions docs/content/en/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ This module configures [`html-validate`](https://html-validate.org/) to automati

<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>

- `failOnError` will throw an error after running `nuxt generate` if there are any validation errors with the generated pages.

<alert>Useful in continuous integration.</alert>

- `options` allows you to pass in `html-validate` options that will be merged with the default configuration

<alert type="info">You can find more about configuring `html-validate` [here](https://html-validate.org/rules/index.html).</alert>
Expand All @@ -85,6 +89,7 @@ This module configures [`html-validate`](https://html-validate.org/) to automati
{
htmlValidator: {
usePrettier: false,
failOnError: false,
options: {
extends: [
'html-validate:document',
Expand Down
2 changes: 2 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ export const defaultHtmlValidateConfig: ConfigData = {

export interface ModuleOptions {
usePrettier?: boolean
failOnError?: boolean
options?: ConfigData
}

export const DEFAULTS: Required<ModuleOptions> = {
usePrettier: false,
failOnError: false,
options: defaultHtmlValidateConfig
}
9 changes: 7 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@ const nuxtModule: Module<ModuleOptions> = function (moduleOptions) {
)

const providedOptions = defu(this.options[CONFIG_KEY] || {}, moduleOptions)
const { usePrettier, options } = defu(providedOptions, DEFAULTS)
const { usePrettier, failOnError, options } = defu(providedOptions, DEFAULTS)
if (options && providedOptions.options && providedOptions.options.extends) {
options.extends = providedOptions.options.extends
}
const { validator } = useValidator(options)

const checkHTML = useChecker(validator, usePrettier)
const { checkHTML, invalidPages } = useChecker(validator, usePrettier)

this.nuxt.hook('render:route', (url: string, result: { html: string }) => checkHTML(url, result.html))
this.nuxt.hook('generate:page', ({ path, html }: { path: string, html: string }) => checkHTML(path, html))
this.nuxt.hook('generate:done', () => {
if (failOnError && invalidPages.length) {
throw new Error('html-validator found errors')
}
})
}

;(nuxtModule as any).meta = { name: '@nuxtjs/html-validator' }
Expand Down
54 changes: 31 additions & 23 deletions src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,38 +22,46 @@ export const useChecker = (
validator: HtmlValidate,
usePrettier = false,
reporter = consola.withTag('html-validate')
) => async (url: string, html: string) => {
let couldFormat = false
try {
if (usePrettier) {
const { format } = await import('prettier')
html = format(html, { parser: 'html' })
couldFormat = true
}
) => {
const invalidPages: string[] = []

const checkHTML = async (url: string, html: string) => {
let couldFormat = false
try {
if (usePrettier) {
const { format } = await import('prettier')
html = format(html, { parser: 'html' })
couldFormat = true
}
// eslint-disable-next-line
} catch (e) {
reporter.error(e)
}
reporter.error(e)
}

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

const { valid, results } = validator.validateString(html)
const { valid, results } = validator.validateString(html)

if (valid) {
return reporter.success(
if (valid) {
return reporter.success(
`No HTML validation errors found for ${chalk.bold(url)}`
)
}
)
}

invalidPages.push(url)

const formatter = couldFormat ? formatterFactory('codeframe') : formatterFactory('stylish')
const formatter = couldFormat ? formatterFactory('codeframe') : formatterFactory('stylish')

const formattedResult = formatter!(results)
const formattedResult = formatter!(results)

reporter.error(
[
reporter.error(
[
`HTML validation errors found for ${chalk.bold(url)}`,
formattedResult
].join('\n')
)
].join('\n')
)
}

return { checkHTML, invalidPages }
}
20 changes: 14 additions & 6 deletions test/checker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('useChecker', () => {

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

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

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

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

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

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

it('records urls when invalid html is provided', async () => {
const mockValidator = jest.fn().mockImplementation(() => ({ valid: false, results: [] }))
const { checkHTML: checker, invalidPages } = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)

await checker('https://test.com/', '<a>Link</a>')
expect(invalidPages).toContain('https://test.com/')
})

it('ignores Vue-generated scoped data attributes', async () => {
const mockValidator = jest.fn().mockImplementation(() => ({ valid: true, results: [] }))
const checker = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)
const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, mockReporter as any)

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

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

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

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

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

0 comments on commit 5f7d650

Please sign in to comment.