├── .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 | [](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 |
2 |
6 |
10 |
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 |
7 |
--------------------------------------------------------------------------------
/docs/public/logo-light.svg:
--------------------------------------------------------------------------------
1 |
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 |
2 |
9 |
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 |
--------------------------------------------------------------------------------