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

This commit is contained in:
Daniel Roe 2024-07-04 17:18:56 +01:00
parent d629b82b35
commit 1d1c180599
No known key found for this signature in database
GPG Key ID: 3714AB03996F442B
8 changed files with 158 additions and 58 deletions

View File

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

View File

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

View File

@ -75,13 +75,13 @@
</head>
<body class="visual-effects relative overflow-hidden min-h-screen bg-white dark:bg-black flex flex-col justify-center items-center text-center">
<div id="mouseLight" class="absolute top-0 rounded-full mouse-gradient transition-opacity h-[200px] w-[200px]"></div>
<a href="https://nuxt.com" target="_blank" rel="noopener" class="nuxt-logo z-20">
<a href="https://nuxt.com" target="_blank" rel="noopener" class="nuxt-logo z-20" aria-label="Nuxt">
<svg id="nuxtImg" xmlns="http://www.w3.org/2000/svg" width="214" height="53" 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="currentColor" d="M377 200a4 4 0 0 0 4-4v-93s5.244 8.286 15 25l38.707 66.961c1.789 3.119 5.084 5.039 8.649 5.039H470V50h-27a4 4 0 0 0-4 4v94l-17-30-36.588-62.98c-1.792-3.108-5.081-5.02-8.639-5.02H350v150h27ZM676.203 143.857 710.551 92h-25.73a9.972 9.972 0 0 0-8.333 4.522L660.757 120.5l-15.731-23.978A9.972 9.972 0 0 0 636.693 92h-25.527l34.348 51.643L608.524 200h24.966a9.969 9.969 0 0 0 8.29-4.458l19.18-28.756 18.981 28.72a9.968 9.968 0 0 0 8.313 4.494h24.736l-36.787-56.143ZM724.598 92h19.714V60.071h28.251V92H800v24.857h-27.437V159.5c0 10.5 5.284 15.429 14.43 15.429H800V200h-16.869c-23.576 0-38.819-14.143-38.819-39.214v-43.929h-19.714V92ZM590 92h-15c-3.489 0-6.218.145-8.5 2.523-2.282 2.246-2.5 3.63-2.5 7.066v52.486c0 8.058-.376 12.962-4 16.925-3.624 3.831-8.619 5-16 5-7.247 0-12.376-1.169-16-5-3.624-3.963-4-8.867-4-16.925v-52.486c0-3.435-.218-4.82-2.5-7.066C519.218 92.145 516.489 92 513 92h-15v62.422c0 14.004 3.892 25.101 11.676 33.292C517.594 195.905 529.103 200 544 200c14.897 0 26.204-4.095 34.123-12.286 7.918-8.191 11.877-19.288 11.877-33.292V92Z" />
</svg>
</a>
<button id="animation-toggle">Animation Enabled</button>
<button id="animation-toggle" type="button">Animation Enabled</button>
<div class="nuxt-loader-bar"></div>
<script>
const ANIMATION_KEY = 'nuxt-loading-enable-animation'

View File

@ -221,7 +221,7 @@
<body class="antialiased bg-white dark:bg-black text-black dark:text-white min-h-screen place-content-center flex flex-col items-center justify-center text-sm sm:text-base">
<div class="flex-1 flex flex-col gap-y-16 py-14">
<div class="flex flex-col gap-y-4 items-center justify-center">
<a href="https://nuxt.com" target="_blank">
<a href="https://nuxt.com" target="_blank" aria-label="Nuxt">
<svg width="61" height="42" viewBox="0 0 61 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M33.9869 41.2211H56.412C57.1243 41.2212 57.824 41.0336 58.4408 40.6772C59.0576 40.3209 59.5698 39.8083 59.9258 39.191C60.2818 38.5737 60.469 37.8736 60.4687 37.1609C60.4684 36.4482 60.2805 35.7482 59.924 35.1313L44.864 9.03129C44.508 8.41416 43.996 7.90168 43.3793 7.54537C42.7626 7.18906 42.063 7.00147 41.3509 7.00147C40.6387 7.00147 39.9391 7.18906 39.3225 7.54537C38.7058 7.90168 38.1937 8.41416 37.8377 9.03129L33.9869 15.7093L26.458 2.65061C26.1018 2.03354 25.5895 1.52113 24.9726 1.16489C24.3557 0.808639 23.656 0.621094 22.9438 0.621094C22.2316 0.621094 21.5318 0.808639 20.915 1.16489C20.2981 1.52113 19.7858 2.03354 19.4296 2.65061L0.689224 35.1313C0.332704 35.7482 0.144842 36.4482 0.144532 37.1609C0.144222 37.8736 0.331476 38.5737 0.687459 39.191C1.04344 39.8083 1.5556 40.3209 2.17243 40.6772C2.78925 41.0336 3.48899 41.2212 4.20126 41.2211H18.2778C23.8551 41.2211 27.9682 38.7699 30.7984 33.9876L37.6694 22.0813L41.3498 15.7093L52.3951 34.8492H37.6694L33.9869 41.2211ZM18.0484 34.8426L8.2247 34.8404L22.9504 9.32211L30.2979 22.0813L25.3784 30.6092C23.4989 33.7121 21.3637 34.8426 18.0484 34.8426Z" fill="#00DC82" />
</svg>
@ -235,8 +235,8 @@
<div class="get-started-gradient-left absolute left-0 inset-y-0 w-[20%] bg-gradient-to-r to-transparent from-green-400 rounded-xl z-1 transition-opacity duration-300"></div>
<div class="get-started-gradient-right absolute right-0 inset-y-0 w-[20%] bg-gradient-to-l to-transparent from-blue-400 rounded-xl z-1 transition-opacity duration-300"></div>
<div class="w-full absolute inset-x-0 flex justify-center -top-[58px]">
<img src="/icons/get-started.svg" class="hidden dark:block">
<img src="/icons/get-started-light.svg" class="dark:hidden">
<img alt="" src="/icons/get-started.svg" class="hidden dark:block">
<img alt="" src="/icons/get-started-light.svg" class="dark:hidden">
</div>
<div class="flex flex-col rounded-xl items-center gap-y-4 pt-[58px] px-4 sm:px-28 pb-6 z-10">
<h2 class="font-semibold text-2xl text-black dark:text-white">
@ -305,7 +305,7 @@
</div>
<footer class="relative border-t bg-white dark:bg-black border-gray-200 dark:border-gray-900 w-full h-[70px] flex items-center">
<div class="absolute inset-x-0 flex items-center justify-center -top-3">
<a href="https://nuxt.com" target="_blank">
<a href="https://nuxt.com" target="_blank" aria-label="Nuxt">
<svg width="70" height="20" viewBox="0 0 70 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="34.6528" cy="10.4209" rx="34.5" ry="9.5" fill="white" class="dark:hidden" />
<ellipse cx="34.6528" cy="10.4209" rx="34.5" ry="9.5" fill="black" class="hidden dark:block" />

View File

@ -1,6 +1,6 @@
// 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`] = `
".spotlight {
background: linear-gradient(45deg, #00dc82 0%, #36e4da 50%, #0047e1 100%);
filter: blur(20vh);
@ -218,7 +218,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,
:after {
@ -321,7 +321,7 @@ p {
"
`;
exports[`template > correctly outputs style blocks for error-500.vue 1`] = `
exports[`template > produces correct output for error-500 template 1`] = `
".spotlight {
background: linear-gradient(45deg, #00dc82 0%, #36e4da 50%, #0047e1 100%);
filter: blur(20vh);
@ -438,7 +438,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,
:after {
@ -537,7 +537,7 @@ p {
"
`;
exports[`template > correctly outputs style blocks for error-dev.vue 1`] = `
exports[`template > produces correct output for error-dev template 1`] = `
".spotlight {
background: linear-gradient(45deg, #00dc82 0%, #36e4da 50%, #0047e1 100%);
opacity: 0.8;
@ -670,7 +670,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,
:after {
@ -784,7 +784,7 @@ pre {
"
`;
exports[`template > correctly outputs style blocks for loading.vue 1`] = `
exports[`template > produces correct output for loading template 1`] = `
".nuxt-loader-bar {
background: repeating-linear-gradient(
to right,
@ -903,7 +903,8 @@ button {
button {
text-transform: none;
}
button {
button,
[type="button"] {
-webkit-appearance: button;
background-color: transparent;
background-image: none;
@ -1029,7 +1030,7 @@ svg {
"
`;
exports[`template > correctly outputs style blocks for loading.vue 2`] = `
exports[`template > produces correct output for loading template 2`] = `
"#animation-toggle {
position: fixed;
padding: 10px;
@ -1107,7 +1108,8 @@ button {
button {
text-transform: none;
}
button {
button,
[type="button"] {
-webkit-appearance: button;
background-color: transparent;
background-image: none;
@ -1176,7 +1178,7 @@ svg {
"
`;
exports[`template > correctly outputs style blocks for welcome.vue 1`] = `
exports[`template > produces correct output for welcome template 1`] = `
"@media (prefers-color-scheme: light) {
.get-started-gradient-border {
background: linear-gradient(to right, #ffffff, #ffffff),
@ -1784,7 +1786,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`] = `
"@property --gradient-angle {
syntax: "<angle>";
inherits: false;

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

@ -602,6 +602,9 @@ importers:
html-minifier:
specifier: 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:
specifier: 1.21.6
version: 1.21.6
@ -2001,6 +2004,10 @@ packages:
engines: {node: '>=6'}
hasBin: true
'@html-validate/stylish@4.2.0':
resolution: {integrity: sha512-Nl8HCv0hGRSLQ+n1OD4Hk3a+Urwk9HH0vQkAzzCarT4KlA7bRl+6xEiS5PZVwOmjtC7XiH/oNe3as9Fxcr2A1w==}
engines: {node: '>= 16'}
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
engines: {node: '>=12.22'}
@ -2660,6 +2667,12 @@ packages:
'@shikijs/vitepress-twoslash@1.10.0':
resolution: {integrity: sha512-Qvua0BIb5uSDryLBkSRx8EX7cNwvTa2GDq53Yh7NbqhwFlYPVp3pnBaRtiDiyYl3Ng+rR2UAakMFiF4PTdnMFg==}
'@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':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
@ -3869,7 +3882,7 @@ packages:
resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==}
engines: {node: '>=14'}
peerDependencies:
typescript: 5.5.3
typescript: '>=4.9.5'
peerDependenciesMeta:
typescript:
optional: true
@ -3878,7 +3891,7 @@ packages:
resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==}
engines: {node: '>=14'}
peerDependencies:
typescript: 5.5.3
typescript: '>=4.9.5'
peerDependenciesMeta:
typescript:
optional: true
@ -4855,6 +4868,25 @@ packages:
resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==}
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:
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
@ -5268,6 +5300,10 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
kleur@4.1.5:
resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==}
engines: {node: '>=6'}
klona@2.0.6:
resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
engines: {node: '>= 8'}
@ -7474,7 +7510,7 @@ packages:
vue@3.4.31:
resolution: {integrity: sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==}
peerDependencies:
typescript: 5.5.3
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
@ -8360,6 +8396,10 @@ snapshots:
protobufjs: 7.3.2
yargs: 17.7.2
'@html-validate/stylish@4.2.0':
dependencies:
kleur: 4.1.5
'@humanwhocodes/module-importer@1.0.1': {}
'@humanwhocodes/retry@0.3.0': {}
@ -9278,6 +9318,12 @@ snapshots:
- supports-color
- 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': {}
'@sindresorhus/is@4.6.0': {}
@ -12106,6 +12152,22 @@ snapshots:
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: {}
htmlparser2@8.0.2:
@ -12491,6 +12553,8 @@ snapshots:
kleur@3.0.3: {}
kleur@4.1.5: {}
klona@2.0.6: {}
knitwork@1.1.0: {}