├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ ├── feature_request.md │ └── question.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .nuxtrc ├── .release-it.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── docs ├── .npmrc ├── app.config.ts ├── components │ └── Logo.vue ├── content │ └── 0.index.md ├── nuxt.config.ts ├── package.json ├── public │ ├── icon.png │ ├── logo-dark.svg │ ├── logo-light.svg │ ├── preview-dark.png │ └── preview.png ├── tokens.config.ts └── tsconfig.json ├── eslint.config.js ├── netlify.toml ├── package.json ├── playground ├── nuxt.config.js ├── package.json ├── pages │ └── index.vue ├── server │ └── plugins │ │ └── html.ts └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── renovate.json ├── src ├── config.ts ├── module.ts ├── runtime │ ├── nitro.ts │ ├── types.d.ts │ └── validator.ts └── type.d.ts ├── test ├── __snapshots__ │ └── validator.test.ts.snap ├── checker.test.ts ├── custom.test.ts ├── module-dev.test.ts ├── module-prerender.test.ts └── validator.test.ts ├── tsconfig.json ├── validator.d.ts └── vitest.config.mjs /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [danielroe] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug report to help us improve the module. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Version 11 | module: 12 | nuxt: 13 | 14 | ### Nuxt configuration 15 | #### [mode](https://nuxtjs.org/api/configuration-mode): 16 | - [ ] universal 17 | - [ ] spa 18 | 19 | ### Nuxt configuration 20 | 26 | 27 | ## Reproduction 28 | > :warning: without a minimal reproduction we wont be able to look into your issue 29 | 30 | **Link:** 31 | [ ] https:///codesandbox.io/ 32 | [ ] GitHub repository 33 | 34 | #### What is expected? 35 | #### What is actually happening? 36 | #### Steps to reproduce 37 | ## Additional information 38 | ## Checklist 39 | * [ ] I have tested with the latest Nuxt version and the issue still occurs 40 | * [ ] I have tested with the latest module version and the issue still occurs 41 | * [ ] I have searched the issue tracker and this issue hasn't been reported yet 42 | 43 | ### Steps to reproduce 44 | 45 | 46 | ### What is expected? 47 | 48 | 49 | ### What is actually happening? 50 | 51 | 52 | ### Performance analysis? 53 | 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Nuxt Community Discord 4 | url: https://discord.nuxtjs.org/ 5 | about: Consider asking questions about the module here. 6 | # - name: Documentation 7 | # url: /README.md 8 | # about: Check our documentation before reporting issues or questions. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea or enhancement for this project. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Is your feature request related to a problem? Please describe. 11 | 12 | 13 | ### Describe the solution you'd like to see 14 | 15 | 16 | ### Describe alternatives you've considered 17 | 18 | 19 | ### Additional context 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about the module. 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - renovate/* 8 | - renovate/* 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | ci: 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest] 20 | 21 | steps: 22 | 23 | - uses: actions/checkout@v4 24 | - run: npm i -g --force corepack && corepack enable 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: 18 28 | cache: "pnpm" 29 | 30 | - name: 📦 Install dependencies 31 | run: pnpm install 32 | 33 | - name: 🚧 Set up project 34 | run: pnpm dev:prepare 35 | 36 | - name: 🔠 Lint project 37 | run: pnpm run lint 38 | 39 | - name: Test 40 | run: pnpm run test --coverage 41 | 42 | - name: Coverage 43 | if: ${{ matrix.os == 'ubuntu-latest' }} 44 | uses: codecov/codecov-action@v5 45 | env: 46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - 'v*' 10 | 11 | jobs: 12 | release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 18 23 | 24 | - run: npx changelogithub 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .output 7 | .vscode 8 | .DS_Store 9 | coverage 10 | dist 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | # enable TypeScript bundler module resolution - https://www.typescriptlang.org/docs/handbook/modules/reference.html#bundler 2 | experimental.typescriptBundlerResolution=false 3 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "commitMessage": "chore: release v${version}" 4 | }, 5 | "github": { 6 | "release": true, 7 | "releaseName": "v${version}" 8 | }, 9 | "npm": { 10 | "skipChecks": true 11 | }, 12 | "plugins": { 13 | "@release-it/conventional-changelog": { 14 | "preset": "conventionalcommits", 15 | "infile": "CHANGELOG.md" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @danielroe 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![@nuxtjs/html-validator](https://html-validator.nuxtjs.org/preview.png)](https://html-validator.nuxtjs.org) 2 | 3 | # @nuxtjs/html-validator 4 | 5 | [![npm version][npm-version-src]][npm-version-href] 6 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 7 | [![Github Actions CI][github-actions-ci-src]][github-actions-ci-href] 8 | [![Codecov][codecov-src]][codecov-href] 9 | [![License][license-src]][license-href] 10 | 11 | > HTML validation using [html-validate](https://html-validate.org/) for [NuxtJS](https://nuxtjs.org) 12 | 13 | - [✨  Release Notes](https://html-validator.nuxtjs.org/releases) 14 | - [📖  Documentation](https://html-validator.nuxtjs.org) 15 | 16 | ## Features 17 | 18 | - Zero-configuration required 19 | - Helps reduce hydration errors 20 | - Detects common accessibility mistakes 21 | 22 | [📖  Read more](https://html-validator.nuxtjs.org) 23 | 24 | ## Quick setup 25 | 26 | Add `@nuxtjs/html-validator` to your project 27 | 28 | ```bash 29 | npx nuxi@latest module add html-validator 30 | ``` 31 | 32 | ## Development 33 | 34 | 1. Clone this repository 35 | 2. Install dependencies using `yarn install` 36 | 3. Start development server using `yarn dev` 37 | 38 | ## License 39 | 40 | [MIT License](./LICENSE) 41 | 42 | 43 | [npm-version-src]: https://img.shields.io/npm/v/@nuxtjs/html-validator/latest.svg 44 | [npm-version-href]: https://npmjs.com/package/@nuxtjs/html-validator 45 | 46 | [npm-downloads-src]: https://img.shields.io/npm/dm/@nuxtjs/html-validator.svg 47 | [npm-downloads-href]: https://npm.chart.dev/@nuxtjs/html-validator 48 | 49 | [github-actions-ci-src]: https://github.com/nuxt-modules/html-validator/workflows/ci/badge.svg 50 | [github-actions-ci-href]: https://github.com/nuxt-modules/html-validator/actions?query=workflow%3Aci 51 | 52 | [codecov-src]: https://img.shields.io/codecov/c/github/nuxt-modules/html-validator.svg 53 | [codecov-href]: https://codecov.io/gh/nuxt-modules/html-validator 54 | 55 | [license-src]: https://img.shields.io/npm/l/@nuxtjs/html-validator.svg 56 | [license-href]: https://npmjs.com/package/@nuxtjs/html-validator 57 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | title: '@nuxtjs/html-validator', 4 | url: 'https://html-validator.nuxtjs.org', 5 | description: 'The best place to start your documentation.', 6 | socials: { 7 | github: 'nuxt-modules/html-validator', 8 | }, 9 | image: 'https://html-validator.nuxtjs.org/preview.png', 10 | aside: { 11 | level: 0, 12 | exclude: [], 13 | }, 14 | header: { 15 | logo: true, 16 | }, 17 | footer: { 18 | iconLinks: [ 19 | { 20 | href: 'https://nuxt.com', 21 | icon: 'simple-icons:nuxtdotjs', 22 | }, 23 | ], 24 | }, 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /docs/components/Logo.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | -------------------------------------------------------------------------------- /docs/content/0.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: 'Automatically validate Nuxt server-rendered HTML (SSR and SSG) to detect common issues with HTML that can lead to hydration errors, as well as improve accessibility and best practice.' 4 | category: Getting started 5 | 6 | --- 7 | 8 | 9 | 10 | 11 | ## Key features 12 | 13 | ::list 14 | - Zero-configuration required 15 | - Helps reduce hydration errors 16 | - Detects common accessibility mistakes 17 | :: 18 | 19 | This module configures [`html-validate`](https://html-validate.org/) to automatically validate Nuxt server-rendered HTML (SSR and SSG) to detect common issues with HTML that can lead to hydration errors, as well as improve accessibility and best practice. 20 | 21 | ## Quick start 22 | 23 | ### Install 24 | ```bash 25 | npx nuxi@latest module add html-validator 26 | ``` 27 | 28 | ### nuxt.config.js 29 | 30 | ::code-group 31 | ```js [Nuxt 3] 32 | defineNuxtConfig({ 33 | modules: ['@nuxtjs/html-validator'] 34 | }) 35 | ``` 36 | ```js {}[Nuxt 2.9+] 37 | export default { 38 | buildModules: ['@nuxtjs/html-validator'] 39 | } 40 | ``` 41 | ```js [Nuxt < 2.9"> 42 | export default { 43 | // Install @nuxtjs/html-validator as dependency instead of devDependency 44 | modules: ['@nuxtjs/html-validator'] 45 | } 46 | ``` 47 | :: 48 | 49 | ::alert{type="info"} 50 | `html-validator` won't be added to your production bundle - it's just used in development and at build/generate time. 51 | :: 52 | 53 | ### Configuration (optional) 54 | 55 | `@nuxtjs/html-validator` takes four options. 56 | 57 | - `usePrettier` enables prettier printing of your source code to show errors in-context. 58 | 59 | ::alert 60 | 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. 61 | :: 62 | 63 | - `logLevel` sets the verbosity to one of `verbose`, `warning` or `error`. It defaults to `verbose` in dev, and `warning` when generating. 64 | 65 | ::alert 66 | You can use this configuration option to turn off console logging for the `No HTML validation errors found for ...` message. 67 | :: 68 | 69 | - `failOnError` will throw an error after running `nuxt generate` if there are any validation errors with the generated pages. 70 | 71 | ::alert 72 | Useful in continuous integration. 73 | :: 74 | 75 | - `options` allows you to pass in `html-validate` options that will be merged with the default configuration 76 | 77 | ::alert{type="info"} 78 | You can find more about configuring `html-validate` [here](https://html-validate.org/rules/index.html). 79 | :: 80 | 81 | **Defaults** 82 | 83 | ```js{}[nuxt.config.js] 84 | { 85 | htmlValidator: { 86 | usePrettier: false, 87 | logLevel: 'verbose', 88 | failOnError: false, 89 | /** A list of routes to ignore (that is, not check validity for). */ 90 | ignore: [/\.(xml|rss|json)$/], 91 | options: { 92 | extends: [ 93 | 'html-validate:document', 94 | 'html-validate:recommended', 95 | 'html-validate:standard' 96 | ], 97 | rules: { 98 | 'svg-focusable': 'off', 99 | 'no-unknown-elements': 'error', 100 | // Conflicts or not needed as we use prettier formatting 101 | 'void-style': 'off', 102 | 'no-trailing-whitespace': 'off', 103 | // Conflict with Nuxt defaults 104 | 'require-sri': 'off', 105 | 'attribute-boolean-style': 'off', 106 | 'doctype-style': 'off', 107 | // Unreasonable rule 108 | 'no-inline-style': 'off' 109 | } 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | **You're good to go!** 116 | 117 | Every time you hard-refresh (server-render) a page in Nuxt, you will see any HTML validation issues printed in your server console. 118 | -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | extends: '@nuxt-themes/docus', 3 | compatibilityDate: '2024-08-19', 4 | }) 5 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxi preview", 10 | "lint": "eslint ." 11 | }, 12 | "dependencies": { 13 | "@nuxt-themes/docus": "1.15.1", 14 | "nuxt": "3.17.5", 15 | "pinceau": "^0.20.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt-modules/html-validator/2365bc5f3f2395328372e6dd045a5dccdc399029/docs/public/icon.png -------------------------------------------------------------------------------- /docs/public/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/public/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/public/preview-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt-modules/html-validator/2365bc5f3f2395328372e6dd045a5dccdc399029/docs/public/preview-dark.png -------------------------------------------------------------------------------- /docs/public/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nuxt-modules/html-validator/2365bc5f3f2395328372e6dd045a5dccdc399029/docs/public/preview.png -------------------------------------------------------------------------------- /docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from 'pinceau' 2 | 3 | // E6F0F0 4 | 5 | export default defineTheme({ 6 | color: { 7 | primary: { 8 | 50: '#eefbf5', 9 | 100: '#d7f4e5', 10 | 200: '#b1e9cf', 11 | 300: '#7fd6b4', 12 | 400: '#41b38a', 13 | 500: '#27a27a', 14 | 600: '#198262', 15 | 700: '#146850', 16 | 800: '#125341', 17 | 900: '#104436', 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createConfigForNuxt } from '@nuxt/eslint-config/flat' 3 | 4 | export default createConfigForNuxt({ 5 | features: { 6 | tooling: true, 7 | stylistic: true, 8 | }, 9 | dirs: { 10 | src: [ 11 | './playground', 12 | './docs', 13 | ], 14 | }, 15 | }).append( 16 | { 17 | files: ['test/**'], 18 | rules: { 19 | '@typescript-eslint/no-explicit-any': 'off', 20 | }, 21 | }, 22 | { 23 | files: ['docs/**'], 24 | rules: { 25 | 'vue/multi-word-component-names': 'off', 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # Global settings applied to the whole site. 2 | # 3 | # “base” is the directory to change to before starting build. If you set base: 4 | # that is where we will look for package.json/.nvmrc/etc, not repo root! 5 | # “command” is your build command. 6 | # “publish” is the directory to publish (relative to the root of your repo). 7 | 8 | [build] 9 | command = "pnpm i && pnpm dev:prepare && pnpm --filter docs generate" 10 | publish = "docs/dist" 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nuxtjs/html-validator", 3 | "version": "2.1.0", 4 | "description": "html-validate integration for Nuxt", 5 | "keywords": [ 6 | "nuxt", 7 | "module", 8 | "nuxt-module", 9 | "html-validator", 10 | "validation", 11 | "html" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/nuxt-modules/html-validator.git" 16 | }, 17 | "license": "MIT", 18 | "type": "module", 19 | "exports": { 20 | ".": "./dist/module.mjs" 21 | }, 22 | "main": "./dist/module.mjs", 23 | "typesVersions": { 24 | "*": { 25 | ".": [ 26 | "./dist/module.d.mts" 27 | ] 28 | } 29 | }, 30 | "files": [ 31 | "dist", 32 | "validator.d.ts" 33 | ], 34 | "scripts": { 35 | "prepack": "nuxt-module-build build", 36 | "dev": "nuxt dev playground", 37 | "dev:build": "nuxt build playground", 38 | "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground", 39 | "prepare": "simple-git-hooks", 40 | "lint": "eslint .", 41 | "release": "pnpm test && bumpp && npm publish", 42 | "test": "pnpm vitest run" 43 | }, 44 | "dependencies": { 45 | "@nuxt/kit": "^3.15.4", 46 | "consola": "^3.4.0", 47 | "html-validate": "~9.5.0", 48 | "knitwork": "^1.2.0", 49 | "pathe": "^2.0.3", 50 | "prettier": "^3.5.2", 51 | "std-env": "^3.8.0" 52 | }, 53 | "devDependencies": { 54 | "@nuxt/eslint-config": "1.4.1", 55 | "@nuxt/module-builder": "1.0.1", 56 | "@nuxt/test-utils": "3.19.1", 57 | "@vitest/coverage-v8": "3.1.2", 58 | "bumpp": "10.1.1", 59 | "eslint": "9.28.0", 60 | "husky": "9.1.7", 61 | "lint-staged": "16.1.0", 62 | "nuxt": "3.17.5", 63 | "simple-git-hooks": "2.13.0", 64 | "vitest": "3.1.2" 65 | }, 66 | "simple-git-hooks": { 67 | "pre-commit": "npx lint-staged" 68 | }, 69 | "lint-staged": { 70 | "*.{js,ts,mjs,cjs,json,.*rc}": [ 71 | "npx eslint --fix" 72 | ] 73 | }, 74 | "resolutions": { 75 | "@nuxt/content": "2.13.4", 76 | "@nuxt/kit": "3.17.5", 77 | "@nuxtjs/html-validator": "link:./" 78 | }, 79 | "publishConfig": { 80 | "access": "public" 81 | }, 82 | "packageManager": "pnpm@10.11.1" 83 | } 84 | -------------------------------------------------------------------------------- /playground/nuxt.config.js: -------------------------------------------------------------------------------- 1 | import { defineNuxtConfig } from 'nuxt/config' 2 | 3 | export default defineNuxtConfig({ 4 | modules: ['@nuxtjs/html-validator'], 5 | routeRules: { 6 | '/redirect': { 7 | redirect: '/', 8 | }, 9 | }, 10 | compatibilityDate: '2024-08-19', 11 | }) 12 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-validator-playground", 3 | "dependencies": { 4 | "@nuxtjs/html-validator": "latest", 5 | "nuxt": "latest" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /playground/server/plugins/html.ts: -------------------------------------------------------------------------------- 1 | export default defineNitroPlugin((nitro) => { 2 | nitro.hooks.hook('html-validator:check', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "docs" 3 | - "playground" 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>danielroe/renovate" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigData } from 'html-validate' 2 | 3 | export const defaultHtmlValidateConfig: ConfigData = { 4 | extends: [ 5 | 'html-validate:document', 6 | 'html-validate:recommended', 7 | 'html-validate:standard', 8 | ], 9 | rules: { 10 | // 11 | 'svg-focusable': 'off', 12 | 'no-unknown-elements': 'error', 13 | // Conflicts or not needed as we use prettier formatting 14 | 'void-style': 'off', 15 | 'no-trailing-whitespace': 'off', 16 | // Conflict with Nuxt defaults 17 | 'require-sri': 'off', 18 | 'attribute-boolean-style': 'off', 19 | 'doctype-style': 'off', 20 | // Unreasonable rule 21 | 'no-inline-style': 'off', 22 | }, 23 | } 24 | 25 | export type LogLevel = 'verbose' | 'warning' | 'error' 26 | 27 | export interface ModuleOptions { 28 | /** Explicitly set this to false to disable validation. */ 29 | enabled?: boolean 30 | usePrettier?: boolean 31 | logLevel?: LogLevel 32 | failOnError?: boolean 33 | options?: ConfigData 34 | /** 35 | * A list of routes to ignore (that is, not check validity for) 36 | * @default [/\.(xml|rss|json)$/] 37 | */ 38 | ignore?: Array 39 | /** 40 | * allow to hook into `html-validator` 41 | * enabling this option block the response until the HTML check and the hook has finished 42 | * 43 | * @default false 44 | */ 45 | hookable?: boolean 46 | } 47 | 48 | export const DEFAULTS: Required> & { logLevel?: LogLevel } = { 49 | usePrettier: false, 50 | enabled: true, 51 | failOnError: false, 52 | options: defaultHtmlValidateConfig, 53 | hookable: false, 54 | ignore: [/\.(xml|rss|json)$/], 55 | } 56 | 57 | export const NuxtRedirectHtmlRegex = /<\/head><\/html>/ 58 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, pathToFileURL } from 'node:url' 2 | import { colors } from 'consola/utils' 3 | import { normalize } from 'pathe' 4 | import { isWindows } from 'std-env' 5 | import { genArrayFromRaw, genObjectFromRawEntries } from 'knitwork' 6 | 7 | import { createResolver, defineNuxtModule, logger, resolvePath } from '@nuxt/kit' 8 | import { DEFAULTS, NuxtRedirectHtmlRegex } from './config' 9 | import type { ModuleOptions } from './config' 10 | 11 | export type { ModuleOptions } 12 | 13 | export default defineNuxtModule({ 14 | meta: { 15 | name: '@nuxtjs/html-validator', 16 | configKey: 'htmlValidator', 17 | compatibility: { 18 | nuxt: '>=3.0.0-rc.7', 19 | }, 20 | }, 21 | defaults: nuxt => ({ 22 | ...DEFAULTS, 23 | logLevel: nuxt.options.dev ? 'verbose' : 'warning', 24 | }), 25 | async setup(moduleOptions, nuxt) { 26 | const resolver = createResolver(import.meta.url, 27 | 28 | ) 29 | nuxt.hook('prepare:types', ({ references }) => { 30 | const types = resolver.resolve('./runtime/types.d.ts') 31 | references.push({ path: types }) 32 | }) 33 | 34 | if (nuxt.options._prepare || moduleOptions.enabled === false) { 35 | return 36 | } 37 | 38 | logger.info(`Using ${colors.bold('html-validate')} to validate server-rendered HTML`) 39 | 40 | const { usePrettier, failOnError, options, logLevel } = moduleOptions as Required 41 | 42 | if (nuxt.options.htmlValidator?.options?.extends) { 43 | options.extends = nuxt.options.htmlValidator.options.extends 44 | } 45 | 46 | if (nuxt.options.dev) { 47 | nuxt.hook('nitro:config', (config) => { 48 | // Transpile the nitro plugin we're injecting 49 | config.externals = config.externals || {} 50 | config.externals.inline = config.externals.inline || [] 51 | config.externals.inline.push('@nuxtjs/html-validator') 52 | 53 | // Add a nitro plugin that will run the validator for us on each request 54 | config.plugins = config.plugins || [] 55 | config.plugins.push(normalize(fileURLToPath(new URL('./runtime/nitro', import.meta.url)))) 56 | config.virtual = config.virtual || {} 57 | const serialisedOptions = genObjectFromRawEntries(Object.entries(moduleOptions).map(([key, value]) => { 58 | if (key !== 'ignore') return [key, JSON.stringify(value, null, 2)] 59 | const ignore = value as ModuleOptions['ignore'] || [] 60 | return [key, genArrayFromRaw(ignore.map(v => typeof v === 'string' ? JSON.stringify(v) : v.toString()))] 61 | })) 62 | config.virtual['#html-validator-config'] = `export default ${serialisedOptions}` 63 | }) 64 | } 65 | 66 | if (!nuxt.options.dev) { 67 | const validatorPath = await resolvePath(fileURLToPath(new URL('./runtime/validator', import.meta.url))) 68 | const { useChecker, getValidator } = await import(isWindows ? pathToFileURL(validatorPath).href : validatorPath) 69 | const validator = getValidator(options) 70 | const { checkHTML, invalidPages } = useChecker(validator, usePrettier, logLevel) 71 | 72 | if (failOnError) { 73 | const errorIfNeeded = () => { 74 | if (invalidPages.length) { 75 | throw new Error('html-validator found errors') 76 | } 77 | } 78 | nuxt.hook('close', errorIfNeeded) 79 | } 80 | 81 | // Prerendering 82 | 83 | nuxt.hook('nitro:init', (nitro) => { 84 | nitro.hooks.hook('prerender:generate', (route) => { 85 | if (!route.contents || !route.fileName?.endsWith('.html')) { 86 | return 87 | } 88 | if (route.contents.match(NuxtRedirectHtmlRegex)) { 89 | return 90 | } 91 | checkHTML(route.route, route.contents) 92 | }) 93 | }) 94 | } 95 | }, 96 | }) 97 | -------------------------------------------------------------------------------- /src/runtime/nitro.ts: -------------------------------------------------------------------------------- 1 | import type { NitroAppPlugin, RenderResponse } from 'nitropack' 2 | import { useChecker, getValidator, isIgnored } from './validator' 3 | // @ts-expect-error virtual module 4 | import config from '#html-validator-config' 5 | 6 | export default function (nitro) { 7 | const validator = getValidator(config.options) 8 | const { checkHTML } = useChecker(validator, config.usePrettier, config.logLevel) 9 | 10 | nitro.hooks.hook('render:response', async (response: Partial, { event }) => { 11 | if (typeof response.body === 'string' && (response.headers?.['Content-Type'] || response.headers?.['content-type'])?.includes('html') && !isIgnored(event.path, config.ignore)) { 12 | // Block the response only if it's not hookable 13 | const promise = checkHTML(event.path, response.body) 14 | if (config.hookable) { 15 | await nitro.hooks.callHook('html-validator:check', await promise, response, { event }) 16 | } 17 | } 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/runtime/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Result } from 'html-validate' 2 | import type { H3Event } from 'h3' 3 | import type { RenderResponse } from 'nitropack' 4 | 5 | declare module 'nitropack' { 6 | interface NitroRuntimeHooks { 7 | 'html-validator:check': (result: { valid: boolean, results: Result[] }, response: Partial, context: { 8 | event: H3Event 9 | }) => void 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/runtime/validator.ts: -------------------------------------------------------------------------------- 1 | import { colors } from 'consola/utils' 2 | 3 | import type { ConfigData } from 'html-validate' 4 | import { HtmlValidate, formatterFactory } from 'html-validate' 5 | import type { LogLevel } from '../config' 6 | 7 | export const getValidator = (options: ConfigData = {}) => { 8 | return new HtmlValidate(options) 9 | } 10 | 11 | export const useChecker = ( 12 | validator: HtmlValidate, 13 | usePrettier = false, 14 | logLevel: LogLevel = 'verbose', 15 | ) => { 16 | const invalidPages: string[] = [] 17 | 18 | const checkHTML = async (url: string, html: string) => { 19 | let couldFormat = false 20 | try { 21 | if (usePrettier) { 22 | const { format } = await import('prettier') 23 | html = await format(html, { parser: 'html' }) 24 | couldFormat = true 25 | } 26 | } 27 | catch (e) { 28 | console.error(e) 29 | } 30 | 31 | // Clean up Vue scoped style attributes 32 | html = typeof html === 'string' ? html.replace(/ ?data-v-[-A-Za-z0-9]+(=["'][^"']*["'])?/g, '') : html 33 | const { valid, results } = await validator.validateString(html) 34 | 35 | if (valid && !results.length) { 36 | if (logLevel === 'verbose') { 37 | console.log(`No HTML validation errors found for ${colors.bold(url)}`) 38 | } 39 | 40 | return { valid, results } 41 | } 42 | 43 | if (!valid) { 44 | invalidPages.push(url) 45 | } 46 | 47 | const formatter = couldFormat ? formatterFactory('codeframe') : formatterFactory('stylish') 48 | 49 | const formattedResult = formatter?.(results) 50 | const message = [ 51 | `HTML validation errors found for ${colors.bold(url)}`, 52 | formattedResult, 53 | ].filter(Boolean).join('\n') 54 | 55 | if (valid) { 56 | if (logLevel === 'verbose' || logLevel === 'warning') { 57 | console.warn(message) 58 | } 59 | } 60 | else { 61 | console.error(message) 62 | } 63 | 64 | return { valid, results } 65 | } 66 | 67 | return { checkHTML, invalidPages } 68 | } 69 | 70 | export function isIgnored(path: string, ignore: Array = []) { 71 | return ignore.some(i => typeof i === 'string' ? path === i : i.test(path)) 72 | } 73 | -------------------------------------------------------------------------------- /src/type.d.ts: -------------------------------------------------------------------------------- 1 | import './runtime/types' 2 | -------------------------------------------------------------------------------- /test/__snapshots__/validator.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 | 3 | exports[`useValidator > returns a valid htmlValidate instance 1`] = ` 4 | [ 5 | { 6 | "errorCount": 1, 7 | "filePath": "inline", 8 | "messages": [ 9 | { 10 | "column": 42, 11 | "context": { 12 | "ancestor": "", 13 | "child": "", 14 | "kind": "descendant", 15 | }, 16 | "line": 1, 17 | "message": " element is not permitted as a descendant of ", 18 | "offset": 41, 19 | "ruleId": "element-permitted-content", 20 | "ruleUrl": "https://html-validate.org/rules/element-permitted-content.html", 21 | "selector": "body > a > a", 22 | "severity": 2, 23 | "size": 1, 24 | }, 25 | ], 26 | "source": "xTest", 27 | "warningCount": 0, 28 | }, 29 | ] 30 | `; 31 | -------------------------------------------------------------------------------- /test/checker.test.ts: -------------------------------------------------------------------------------- 1 | import { colors } from 'consola/utils' 2 | import { describe, it, expect, vi, afterEach } from 'vitest' 3 | 4 | import { useChecker } from '../src/runtime/validator' 5 | 6 | vi.mock('prettier', () => ({ 7 | format: vi.fn().mockImplementation((str: string) => { 8 | if (typeof str !== 'string') { 9 | throw new TypeError('invalid') 10 | } 11 | return 'valid' 12 | }), 13 | })) 14 | 15 | vi.mock('html-validate') 16 | 17 | vi.spyOn(console, 'error') 18 | vi.spyOn(console, 'log') 19 | vi.spyOn(console, 'warn') 20 | 21 | describe('useChecker', () => { 22 | afterEach(() => { 23 | vi.clearAllMocks() 24 | }) 25 | 26 | it('logs valid output', async () => { 27 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [] })) 28 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false) 29 | 30 | await checker('https://test.com/', Symbol as any) 31 | expect(console.log).toHaveBeenCalledWith( 32 | `No HTML validation errors found for ${colors.bold('https://test.com/')}`, 33 | ) 34 | expect(console.warn).not.toHaveBeenCalled() 35 | expect(console.error).not.toHaveBeenCalled() 36 | }) 37 | 38 | it('logs valid output', async () => { 39 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [] })) 40 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, 'verbose') 41 | 42 | await checker('https://test.com/', Symbol as any) 43 | expect(console.log).toHaveBeenCalledWith( 44 | `No HTML validation errors found for ${colors.bold('https://test.com/')}`, 45 | ) 46 | expect(console.warn).not.toHaveBeenCalled() 47 | expect(console.error).not.toHaveBeenCalled() 48 | }) 49 | 50 | for (const logLevel of ['warning', 'error'] as const) { 51 | it(`does not log valid output when logging on level ${logLevel}`, async () => { 52 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [] })) 53 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, logLevel) 54 | 55 | await checker('https://test.com/', Symbol as any) 56 | expect(console.log).not.toHaveBeenCalled() 57 | expect(console.warn).not.toHaveBeenCalled() 58 | expect(console.error).not.toHaveBeenCalled() 59 | }) 60 | } 61 | 62 | for (const logLevel of [undefined, 'verbose', 'warning'] as const) { 63 | it(`logs a warning when valid html is provided with warnings and log level is set to ${logLevel}`, async () => { 64 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [{ messages: [{ message: '' }] }] })) 65 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, logLevel) 66 | 67 | await checker('https://test.com/', Symbol as any) 68 | expect(console.log).not.toHaveBeenCalled() 69 | expect(console.warn).toHaveBeenCalled() 70 | expect(console.error).not.toHaveBeenCalled() 71 | }) 72 | } 73 | 74 | it('does not log a warning when valid html is provided with warnings and log level is set to error', async () => { 75 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [{ messages: [{ message: '' }] }] })) 76 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, 'error') 77 | 78 | await checker('https://test.com/', Symbol as any) 79 | expect(console.log).not.toHaveBeenCalled() 80 | expect(console.warn).not.toHaveBeenCalled() 81 | expect(console.error).not.toHaveBeenCalled() 82 | }) 83 | 84 | for (const logLevel of [undefined, 'verbose', 'warning', 'error'] as const) { 85 | it(`logs an error when invalid html is provided and log level is set to ${logLevel}`, async () => { 86 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: false, results: [{ messages: [{ message: '' }] }] })) 87 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false, logLevel) 88 | 89 | await checker('https://test.com/', 'Link') 90 | expect(mockValidator).toHaveBeenCalled() 91 | expect(console.log).not.toHaveBeenCalled() 92 | expect(console.warn).not.toHaveBeenCalled() 93 | expect(console.error).toHaveBeenCalledWith( 94 | `HTML validation errors found for ${colors.bold('https://test.com/')}`, 95 | ) 96 | }) 97 | } 98 | 99 | it('records urls when invalid html is provided', async () => { 100 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: false, results: [] })) 101 | const { checkHTML: checker, invalidPages } = useChecker({ validateString: mockValidator } as any, false) 102 | 103 | await checker('https://test.com/', 'Link') 104 | expect(invalidPages).toContain('https://test.com/') 105 | }) 106 | 107 | it('ignores Vue-generated scoped data attributes', async () => { 108 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [] })) 109 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false) 110 | 111 | await checker( 112 | 'https://test.com/', 113 | 'Link', 114 | ) 115 | expect(mockValidator).toHaveBeenCalledWith( 116 | 'Link', 117 | ) 118 | expect(console.log).toHaveBeenCalledWith( 119 | `No HTML validation errors found for ${colors.bold('https://test.com/')}`, 120 | ) 121 | expect(console.warn).not.toHaveBeenCalled() 122 | expect(console.error).not.toHaveBeenCalled() 123 | }) 124 | 125 | it('ignores vite-plugin-inspect generated data attributes', async () => { 126 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: true, results: [] })) 127 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, false) 128 | 129 | await checker( 130 | 'https://test.com/', 131 | 'Link', 132 | ) 133 | expect(mockValidator).toHaveBeenCalledWith( 134 | 'Link', 135 | ) 136 | expect(console.log).toHaveBeenCalledWith( 137 | `No HTML validation errors found for ${colors.bold('https://test.com/')}`, 138 | ) 139 | expect(console.warn).not.toHaveBeenCalled() 140 | expect(console.error).not.toHaveBeenCalled() 141 | }) 142 | 143 | it('formats HTML with prettier when asked to do so', async () => { 144 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: false, results: [] })) 145 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, true) 146 | 147 | await checker('https://test.com/', 'Link') 148 | const { format } = await import('prettier') 149 | expect(format).toHaveBeenCalledWith('Link', { parser: 'html' }) 150 | 151 | const formatterFactory = await import('html-validate').then(r => r.formatterFactory) 152 | expect(formatterFactory).toHaveBeenCalledWith('codeframe') 153 | }) 154 | 155 | it('falls back gracefully when prettier cannot format', async () => { 156 | const mockValidator = vi.fn().mockImplementation(() => ({ valid: false, results: [] })) 157 | const { checkHTML: checker } = useChecker({ validateString: mockValidator } as any, true) 158 | 159 | await checker('https://test.com/', Symbol as any) 160 | const { format } = await import('prettier') 161 | expect(format).toHaveBeenCalledWith(Symbol, { parser: 'html' }) 162 | expect(console.log).not.toHaveBeenCalled() 163 | expect(console.warn).not.toHaveBeenCalled() 164 | expect(console.error).toHaveBeenCalledWith( 165 | `HTML validation errors found for ${colors.bold('https://test.com/')}`, 166 | ) 167 | 168 | const validate = await import('html-validate') 169 | expect(validate.formatterFactory).not.toHaveBeenCalledWith('codeframe') 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /test/custom.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { describe, it, expect, vi } from 'vitest' 3 | import { setup, $fetch } from '@nuxt/test-utils' 4 | 5 | const error = vi.fn() 6 | Object.defineProperty(console, 'error', { 7 | get() { 8 | return error 9 | }, 10 | set() {}, 11 | }) 12 | 13 | await setup({ 14 | rootDir: fileURLToPath(new URL('../playground', import.meta.url)), 15 | nuxtConfig: { 16 | htmlValidator: { 17 | options: { 18 | extends: [], 19 | }, 20 | }, 21 | }, 22 | }) 23 | 24 | describe('custom options', () => { 25 | it('overriding extends does not merge array', async () => { 26 | await $fetch('/') 27 | expect(console.error).not.toHaveBeenCalled() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/module-dev.test.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { describe, it, expect, vi } from 'vitest' 3 | import { setup, $fetch } from '@nuxt/test-utils' 4 | 5 | vi.spyOn(console, 'error') 6 | 7 | await setup({ 8 | rootDir: fileURLToPath(new URL('../playground', import.meta.url)), 9 | dev: true, 10 | }) 11 | 12 | describe.todo('Nuxt dev', () => { 13 | it('should add hook to server response', async () => { 14 | const body = await $fetch('/') 15 | 16 | expect(body).toContain('This is an invalid nested anchor tag') 17 | expect(console.error).toHaveBeenCalledWith( 18 | expect.stringContaining('element-permitted-content'), 19 | ) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /test/module-prerender.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsp } from 'node:fs' 2 | import { resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import { describe, it, expect, vi } from 'vitest' 5 | import { setup, useTestContext } from '@nuxt/test-utils' 6 | import { useNuxt } from '@nuxt/kit' 7 | 8 | const error = vi.fn() 9 | Object.defineProperty(console, 'error', { 10 | get() { 11 | return error 12 | }, 13 | set() {}, 14 | }) 15 | 16 | await setup({ 17 | rootDir: fileURLToPath(new URL('../playground', import.meta.url)), 18 | build: true, 19 | nuxtConfig: { 20 | hooks: { 21 | 'modules:before'() { 22 | const nuxt = useNuxt() 23 | nuxt.options.nitro.prerender = { routes: ['/', '/redirect'] } 24 | }, 25 | }, 26 | }, 27 | }) 28 | 29 | describe('Nuxt prerender', () => { 30 | it('should add hook for generating HTML', async () => { 31 | const ctx = useTestContext() 32 | const html = await fsp.readFile( 33 | resolve(ctx.nuxt!.options.nitro.output?.dir || '', 'public/index.html'), 34 | 'utf-8', 35 | ) 36 | 37 | expect(html).toContain('This is an invalid nested anchor tag') 38 | expect(console.error).toHaveBeenCalledWith( 39 | expect.stringContaining('HTML validation errors'), 40 | ) 41 | expect(console.error).toHaveBeenCalledWith( 42 | expect.stringContaining( 43 | ' element is not permitted as a descendant of ', 44 | ), 45 | ) 46 | }) 47 | 48 | it('ignores redirect pages', async () => { 49 | const ctx = useTestContext() 50 | const html = await fsp.readFile( 51 | resolve(ctx.nuxt!.options.nitro.output?.dir || '', 'public/redirect/index.html'), 52 | 'utf-8', 53 | ) 54 | 55 | expect(html).toMatchInlineSnapshot(`""`) 56 | expect(console.error).not.toHaveBeenCalledWith( 57 | expect.stringContaining(' element must have as content'), 58 | ) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /test/validator.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { defaultHtmlValidateConfig } from '../src/config' 3 | import { getValidator } from '../src/runtime/validator' 4 | 5 | describe('useValidator', () => { 6 | it('generates a new validator for each set of options', () => { 7 | const option1 = { extends: [] } 8 | const validator1 = getValidator(option1) 9 | const validator2 = getValidator({ extends: ['html-validate:document'] }) 10 | const validator3 = getValidator(option1) 11 | const validator4 = getValidator() 12 | const validator5 = getValidator() 13 | 14 | expect(validator1).not.toEqual(validator2) 15 | expect(validator1).toEqual(validator3) 16 | expect(validator4).toEqual(validator5) 17 | }) 18 | 19 | it('returns a valid htmlValidate instance', async () => { 20 | const validator = getValidator({ extends: ['html-validate:standard'] }) 21 | 22 | const { valid, results } = await validator.validateString('x') 23 | expect(valid).toBeTruthy() 24 | expect(results).toEqual([]) 25 | 26 | const { valid: invalid, results: invalidResults } = await validator.validateString('xTest') 27 | expect(invalid).toBeFalsy() 28 | expect(invalidResults).toMatchSnapshot() 29 | }) 30 | 31 | it('works with default config', async () => { 32 | const validator = getValidator(defaultHtmlValidateConfig) 33 | const { valid, results } = await validator.validateString('x') 34 | expect(valid).toBeTruthy() 35 | expect(results).toEqual([]) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json", 3 | } 4 | -------------------------------------------------------------------------------- /validator.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/runtime/validator' 2 | -------------------------------------------------------------------------------- /vitest.config.mjs: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { defineConfig } from 'vitest/config' 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | '@nuxtjs/html-validator': fileURLToPath(new URL('./src/module', import.meta.url)), 8 | }, 9 | }, 10 | test: { 11 | coverage: { 12 | include: ['src'], 13 | reporter: ['text', 'json', 'html', 'lcov'], 14 | }, 15 | }, 16 | }) 17 | --------------------------------------------------------------------------------