fix(ui-templates): validate templates with html-validate (#28024)

This commit is contained in:
Daniel Roe 2024-07-04 17:18:56 +01:00 committed by GitHub
parent 3d55642aa5
commit 8271ea22df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 149 additions and 49 deletions

View File

@ -49,6 +49,7 @@ export const RenderPlugin = () => {
// Apply critters to inline styles // Apply critters to inline styles
html = await critters.process(html) html = await critters.process(html)
} }
html = html.replace(/<html[^>]*>/, '<html lang="en">')
// We no longer need references to external CSS // We no longer need references to external CSS
html = html.replace(/<link[^>]*>/g, '') html = html.replace(/<link[^>]*>/g, '')

View File

@ -12,7 +12,6 @@
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"dev": "vite", "dev": "vite",
"lint": "eslint --ext .ts,.js .",
"optimize-assets": "npx svgo public/assets/**/*.svg", "optimize-assets": "npx svgo public/assets/**/*.svg",
"postinstall": "pnpm build", "postinstall": "pnpm build",
"prerender": "pnpm build && jiti ./lib/prerender", "prerender": "pnpm build && jiti ./lib/prerender",
@ -25,6 +24,7 @@
"execa": "9.3.0", "execa": "9.3.0",
"globby": "14.0.2", "globby": "14.0.2",
"html-minifier": "4.0.0", "html-minifier": "4.0.0",
"html-validate": "^8.20.1",
"jiti": "2.0.0-beta.3", "jiti": "2.0.0-beta.3",
"knitwork": "1.1.0", "knitwork": "1.1.0",
"pathe": "1.1.2", "pathe": "1.1.2",

View File

@ -9,7 +9,7 @@
</head> </head>
<body class="antialiased bg-white dark:bg-[#020420] text-[#020420] dark:text-white min-h-screen place-content-center flex flex-col items-center justify-center text-sm sm:text-base"> <body class="antialiased bg-white dark:bg-[#020420] text-[#020420] dark:text-white min-h-screen place-content-center flex flex-col items-center justify-center text-sm sm:text-base">
<div class="flex flex-col mt-6 sm:mt-0"> <div class="flex flex-col mt-6 sm:mt-0">
<div class="flex flex-col gap-y-4 items-center justify-center"> <h1 class="flex flex-col gap-y-4 items-center justify-center">
<a href="https://nuxt.com?utm_source=nuxt-welcome" target="_blank" class="inline-flex items-end gap-4"> <a href="https://nuxt.com?utm_source=nuxt-welcome" target="_blank" class="inline-flex items-end gap-4">
<svg class="h-8 sm:h-12" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 800 200"> <svg class="h-8 sm:h-12" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 800 200">
<path fill="#00DC82" d="M168.303 200h111.522c3.543 0 7.022-.924 10.09-2.679A20.086 20.086 0 0 0 297.3 190a19.855 19.855 0 0 0 2.7-10.001 19.858 19.858 0 0 0-2.709-9.998L222.396 41.429a20.09 20.09 0 0 0-7.384-7.32 20.313 20.313 0 0 0-10.088-2.679c-3.541 0-7.02.925-10.087 2.68a20.082 20.082 0 0 0-7.384 7.32l-19.15 32.896L130.86 9.998a20.086 20.086 0 0 0-7.387-7.32A20.322 20.322 0 0 0 113.384 0c-3.542 0-7.022.924-10.09 2.679a20.091 20.091 0 0 0-7.387 7.319L2.709 170A19.853 19.853 0 0 0 0 179.999c-.002 3.511.93 6.96 2.7 10.001a20.091 20.091 0 0 0 7.385 7.321A20.322 20.322 0 0 0 20.175 200h70.004c27.737 0 48.192-12.075 62.266-35.633l34.171-58.652 18.303-31.389 54.93 94.285h-73.233L168.303 200Zm-79.265-31.421-48.854-.011 73.232-125.706 36.541 62.853-24.466 42.01c-9.347 15.285-19.965 20.854-36.453 20.854Z" /> <path fill="#00DC82" d="M168.303 200h111.522c3.543 0 7.022-.924 10.09-2.679A20.086 20.086 0 0 0 297.3 190a19.855 19.855 0 0 0 2.7-10.001 19.858 19.858 0 0 0-2.709-9.998L222.396 41.429a20.09 20.09 0 0 0-7.384-7.32 20.313 20.313 0 0 0-10.088-2.679c-3.541 0-7.02.925-10.087 2.68a20.082 20.082 0 0 0-7.384 7.32l-19.15 32.896L130.86 9.998a20.086 20.086 0 0 0-7.387-7.32A20.322 20.322 0 0 0 113.384 0c-3.542 0-7.022.924-10.09 2.679a20.091 20.091 0 0 0-7.387 7.319L2.709 170A19.853 19.853 0 0 0 0 179.999c-.002 3.511.93 6.96 2.7 10.001a20.091 20.091 0 0 0 7.385 7.321A20.322 20.322 0 0 0 20.175 200h70.004c27.737 0 48.192-12.075 62.266-35.633l34.171-58.652 18.303-31.389 54.93 94.285h-73.233L168.303 200Zm-79.265-31.421-48.854-.011 73.232-125.706 36.541 62.853-24.466 42.01c-9.347 15.285-19.965 20.854-36.453 20.854Z" />
@ -17,7 +17,7 @@
</svg> </svg>
<span class="inline-block font-mono leading-none text-[#00DC82] group-hover:border-[#00DC42] text-[12px] sm:text-[14px] font-semibold border-[#00DC42]/50 bg-[#00DC42]/10 group-hover:bg-[#00DC42]/15 px-2 sm:px-2.5 py-1 sm:py-1.5 border rounded">{{ version }}</span> <span class="inline-block font-mono leading-none text-[#00DC82] group-hover:border-[#00DC42] text-[12px] sm:text-[14px] font-semibold border-[#00DC42]/50 bg-[#00DC42]/10 group-hover:bg-[#00DC42]/15 px-2 sm:px-2.5 py-1 sm:py-1.5 border rounded">{{ version }}</span>
</a> </a>
</div> </h1>
<div class="max-w-[980px] w-full grid grid-cols-1 sm:grid-cols-3 mt-6 sm:mt-10 gap-4 sm:gap-6 px-4"> <div class="max-w-[980px] w-full grid grid-cols-1 sm:grid-cols-3 mt-6 sm:mt-10 gap-4 sm:gap-6 px-4">
<div class="sm:col-span-2 flex flex-col gap-1 border p-6 rounded-lg border-[#00DC42]/50 dark:bg-white/5 bg-gray-50/10"> <div class="sm:col-span-2 flex flex-col gap-1 border p-6 rounded-lg border-[#00DC42]/50 dark:bg-white/5 bg-gray-50/10">
<div class="w-[32px] h-[32px] bg-[#00DC82]/5 flex items-center justify-center border rounded border-[#00DC82] dark:border-[#00DC82]/80 dark:bg-[#020420] text-[#00DC82] dark:text-[#00DC82]"> <div class="w-[32px] h-[32px] bg-[#00DC82]/5 flex items-center justify-center border rounded border-[#00DC82] dark:border-[#00DC82]/80 dark:bg-[#020420] text-[#00DC82] dark:text-[#00DC82]">

View File

@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`template > correctly outputs style blocks for error-404.vue 1`] = ` exports[`template > produces correct output for error-404 template 1`] = `
".grid { ".grid {
display: grid; display: grid;
} }
@ -135,7 +135,7 @@ exports[`template > correctly outputs style blocks for error-404.vue 1`] = `
" "
`; `;
exports[`template > correctly outputs style blocks for error-404.vue 2`] = ` exports[`template > produces correct output for error-404 template 2`] = `
"*, "*,
:before, :before,
:after { :after {
@ -240,7 +240,7 @@ p {
" "
`; `;
exports[`template > correctly outputs style blocks for error-500.vue 1`] = ` exports[`template > produces correct output for error-500 template 1`] = `
".grid { ".grid {
display: grid; display: grid;
} }
@ -346,7 +346,7 @@ exports[`template > correctly outputs style blocks for error-500.vue 1`] = `
" "
`; `;
exports[`template > correctly outputs style blocks for error-500.vue 2`] = ` exports[`template > produces correct output for error-500 template 2`] = `
"*, "*,
:before, :before,
:after { :after {
@ -447,7 +447,7 @@ p {
" "
`; `;
exports[`template > correctly outputs style blocks for error-dev.vue 1`] = ` exports[`template > produces correct output for error-dev template 1`] = `
".absolute { ".absolute {
position: absolute; position: absolute;
} }
@ -606,7 +606,7 @@ exports[`template > correctly outputs style blocks for error-dev.vue 1`] = `
" "
`; `;
exports[`template > correctly outputs style blocks for error-dev.vue 2`] = ` exports[`template > produces correct output for error-dev template 2`] = `
"*, "*,
:before, :before,
:after { :after {
@ -724,7 +724,7 @@ pre {
" "
`; `;
exports[`template > correctly outputs style blocks for loading.vue 1`] = ` exports[`template > produces correct output for loading template 1`] = `
".nuxt-loader-bar { ".nuxt-loader-bar {
background: #00dc82; background: #00dc82;
position: fixed; position: fixed;
@ -887,7 +887,7 @@ exports[`template > correctly outputs style blocks for loading.vue 1`] = `
" "
`; `;
exports[`template > correctly outputs style blocks for loading.vue 2`] = ` exports[`template > produces correct output for loading template 2`] = `
"@keyframes nuxt-loading-move { "@keyframes nuxt-loading-move {
100% { 100% {
stroke-dashoffset: -128; stroke-dashoffset: -128;
@ -998,7 +998,7 @@ svg {
" "
`; `;
exports[`template > correctly outputs style blocks for welcome.vue 1`] = ` exports[`template > produces correct output for welcome template 1`] = `
".sr-only { ".sr-only {
position: absolute; position: absolute;
width: 1px; width: 1px;
@ -1358,7 +1358,7 @@ exports[`template > correctly outputs style blocks for welcome.vue 1`] = `
" "
`; `;
exports[`template > correctly outputs style blocks for welcome.vue 2`] = ` exports[`template > produces correct output for welcome template 2`] = `
"*, "*,
:before, :before,
:after { :after {
@ -1392,6 +1392,7 @@ body {
margin: 0; margin: 0;
line-height: inherit; line-height: inherit;
} }
h1,
h2 { h2 {
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
@ -1400,6 +1401,7 @@ a {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
h1,
h2, h2,
p { p {
margin: 0; margin: 0;

View File

@ -1,36 +0,0 @@
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { execaCommand } from 'execa'
import { format } from 'prettier'
const distDir = fileURLToPath(new URL('../node_modules/.temp/dist/templates', import.meta.url))
describe('template', () => {
beforeAll(async () => {
await execaCommand('pnpm build', {
cwd: fileURLToPath(new URL('..', import.meta.url)),
env: {
OUTPUT_DIR: './node_modules/.temp/dist',
},
})
})
afterAll(() => rm(distDir, { force: true, recursive: true }))
function formatCss (css: string) {
return format(css, {
parser: 'css',
})
}
it.each(['error-404.vue', 'error-500.vue', 'error-dev.vue', 'loading.vue', 'welcome.vue'])('correctly outputs style blocks for %s', async (file) => {
const contents = readFileSync(`${distDir}/${file}`, 'utf-8')
const scopedStyle = contents.match(/<style scoped>([\s\S]*)<\/style>/)
const globalStyle = contents.match(/style: \[[\s\S]*children: `([\s\S]*)`/)
expect(await formatCss(scopedStyle?.[1] || '')).toMatchSnapshot()
expect(await formatCss(globalStyle?.[1] || '')).toMatchSnapshot()
})
})

View File

@ -0,0 +1,69 @@
import { fileURLToPath } from 'node:url'
import { readFileSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { execaCommand } from 'execa'
import { format } from 'prettier'
import { createJiti } from 'jiti'
// @ts-expect-error types not valid for bundler resolution
import { HtmlValidate } from 'html-validate'
const distDir = fileURLToPath(new URL('../node_modules/.temp/dist/templates', import.meta.url))
describe('template', () => {
beforeAll(async () => {
await execaCommand('pnpm build', {
cwd: fileURLToPath(new URL('..', import.meta.url)),
env: {
OUTPUT_DIR: './node_modules/.temp/dist',
},
})
})
afterAll(() => rm(distDir, { force: true, recursive: true }))
function formatCss (css: string) {
return format(css, {
parser: 'css',
})
}
const jiti = createJiti(import.meta.url)
const validator = new HtmlValidate({
extends: [
'html-validate:document',
'html-validate:recommended',
'html-validate:standard',
],
rules: {
//
'svg-focusable': 'off',
'no-unknown-elements': 'error',
// Conflicts or not needed as we use prettier formatting
'void-style': 'off',
'no-trailing-whitespace': 'off',
// Conflict with Nuxt defaults
'require-sri': 'off',
'attribute-boolean-style': 'off',
'doctype-style': 'off',
// Unreasonable rule
'no-inline-style': 'off',
},
})
it.each(['error-404', 'error-500', 'error-dev', 'loading', 'welcome'])('produces correct output for %s template', async (file) => {
const contents = readFileSync(`${distDir}/${file}.vue`, 'utf-8')
const scopedStyle = contents.match(/<style scoped>([\s\S]*)<\/style>/)
const globalStyle = contents.match(/style: \[[\s\S]*children: `([\s\S]*)`/)
expect(await formatCss(scopedStyle?.[1] || '')).toMatchSnapshot()
expect(await formatCss(globalStyle?.[1] || '')).toMatchSnapshot()
const { template } = await jiti.import(`file://${distDir}/${file}.ts`) as { template: () => string }
const html = template()
const { valid, results } = await (validator as any).validateString(html)
expect.soft(valid).toBe(true)
expect.soft(results).toEqual([])
})
})

View File

@ -601,6 +601,9 @@ importers:
html-minifier: html-minifier:
specifier: 4.0.0 specifier: 4.0.0
version: 4.0.0 version: 4.0.0
html-validate:
specifier: ^8.20.1
version: 8.20.1(vitest@1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(sass@1.69.4)(terser@5.27.0))
jiti: jiti:
specifier: 2.0.0-beta.3 specifier: 2.0.0-beta.3
version: 2.0.0-beta.3 version: 2.0.0-beta.3
@ -1733,6 +1736,10 @@ packages:
'@floating-ui/utils@0.2.1': '@floating-ui/utils@0.2.1':
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
'@html-validate/stylish@4.2.0':
resolution: {integrity: sha512-Nl8HCv0hGRSLQ+n1OD4Hk3a+Urwk9HH0vQkAzzCarT4KlA7bRl+6xEiS5PZVwOmjtC7XiH/oNe3as9Fxcr2A1w==}
engines: {node: '>= 16'}
'@humanwhocodes/module-importer@1.0.1': '@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'} engines: {node: '>=12.22'}
@ -2312,6 +2319,12 @@ packages:
'@shikijs/vitepress-twoslash@1.10.1': '@shikijs/vitepress-twoslash@1.10.1':
resolution: {integrity: sha512-bBFHGKMGW0ACa8jFjDK2V9sWSh6wh1R1/U5VbVUr0HBm7kLR/H0bbr9RZeD91wKZb9JI3zJRwiNrTCueLuBw8A==} resolution: {integrity: sha512-bBFHGKMGW0ACa8jFjDK2V9sWSh6wh1R1/U5VbVUr0HBm7kLR/H0bbr9RZeD91wKZb9JI3zJRwiNrTCueLuBw8A==}
'@sidvind/better-ajv-errors@2.1.3':
resolution: {integrity: sha512-lWuod/rh7Xz5uXiEGSfm2Sd5PG7K/6yJfoAZVqzsEswjPJhUz15R7Gn/o8RczA041QS15hBd/BCSeu9vwPArkA==}
engines: {node: '>= 16.14'}
peerDependencies:
ajv: 4.11.8 - 8
'@sinclair/typebox@0.27.8': '@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@ -4494,6 +4507,25 @@ packages:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
html-validate@8.20.1:
resolution: {integrity: sha512-EawDiHzvZtnbBIfxE90lvKOWqNsmZGqRXTy+utxlGo525Vqjowg+RK42q1AeJ6zm1AyVTFIDSah1eBe9tc6YHg==}
engines: {node: '>= 16.14'}
hasBin: true
peerDependencies:
jest: ^27.1 || ^28.1.3 || ^29.0.3
jest-diff: ^27.1 || ^28.1.3 || ^29.0.3
jest-snapshot: ^27.1 || ^28.1.3 || ^29.0.3
vitest: ^0.34 || ^1
peerDependenciesMeta:
jest:
optional: true
jest-diff:
optional: true
jest-snapshot:
optional: true
vitest:
optional: true
html-void-elements@3.0.0: html-void-elements@3.0.0:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@ -4920,6 +4952,10 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'} engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
klona@2.0.6: klona@2.0.6:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -7870,6 +7906,10 @@ snapshots:
'@floating-ui/utils@0.2.1': {} '@floating-ui/utils@0.2.1': {}
'@html-validate/stylish@4.2.0':
dependencies:
kleur: 4.1.5
'@humanwhocodes/module-importer@1.0.1': {} '@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.3.0': {} '@humanwhocodes/retry@0.3.0': {}
@ -8692,6 +8732,12 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
'@sidvind/better-ajv-errors@2.1.3(ajv@8.12.0)':
dependencies:
'@babel/code-frame': 7.24.7
ajv: 8.12.0
chalk: 4.1.2
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
'@sindresorhus/is@4.6.0': {} '@sindresorhus/is@4.6.0': {}
@ -11477,6 +11523,22 @@ snapshots:
html-tags@3.3.1: {} html-tags@3.3.1: {}
html-validate@8.20.1(vitest@1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(sass@1.69.4)(terser@5.27.0)):
dependencies:
'@babel/code-frame': 7.24.7
'@html-validate/stylish': 4.2.0
'@sidvind/better-ajv-errors': 2.1.3(ajv@8.12.0)
ajv: 8.12.0
deepmerge: 4.3.1
glob: 10.4.1
ignore: 5.3.1
kleur: 4.1.5
minimist: 1.2.8
prompts: 2.4.2
semver: 7.6.2
optionalDependencies:
vitest: 1.6.0(@types/node@20.14.9)(happy-dom@14.12.3)(sass@1.69.4)(terser@5.27.0)
html-void-elements@3.0.0: {} html-void-elements@3.0.0: {}
htmlparser2@8.0.2: htmlparser2@8.0.2:
@ -11868,6 +11930,8 @@ snapshots:
kleur@3.0.3: {} kleur@3.0.3: {}
kleur@4.1.5: {}
klona@2.0.6: {} klona@2.0.6: {}
knitwork@1.1.0: {} knitwork@1.1.0: {}