1
1
mirror of https://github.com/theoludwig/html-w3c-validator.git synced 2025-05-21 23:21:29 +02:00

9 Commits

40 changed files with 11248 additions and 47992 deletions

View File

@ -1,2 +0,0 @@
ARG VARIANT="16"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

View File

@ -1,15 +0,0 @@
{
"name": "html-w3c-validator",
"dockerComposeFile": "./docker-compose.yml",
"service": "workspace",
"workspaceFolder": "/workspace",
"extensions": [
"editorconfig.editorconfig",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker"
],
"postAttachCommand": ["npm", "install"],
"remoteUser": "node"
}

View File

@ -1,10 +0,0 @@
version: '3.0'
services:
workspace:
build:
context: './'
dockerfile: './Dockerfile'
volumes:
- '..:/workspace:cached'
command: 'sleep infinity'

View File

@ -1,5 +0,0 @@
build
node_modules
coverage
package.json
package-lock.json

View File

@ -1,21 +1,16 @@
{ {
"extends": ["standard-with-typescript", "prettier"], "extends": ["conventions", "prettier"],
"plugins": ["unicorn", "import", "prettier"], "plugins": ["prettier", "import", "unicorn"],
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"env": { "env": {
"node": true, "node": true
"jest": true
}, },
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "error",
"import/order": [
"error",
{ "groups": ["builtin", "external", "internal"] }
],
"import/extensions": ["error", "always"], "import/extensions": ["error", "always"],
"unicorn/prefer-node-protocol": "error", "unicorn/prevent-abbreviations": "error",
"unicorn/prevent-abbreviations": "error" "unicorn/prefer-node-protocol": "error"
} }
} }

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

View File

@ -1,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
## What changes this PR introduce? # What changes this PR introduce?
## List any relevant issue numbers ## List any relevant issue numbers

View File

@ -2,7 +2,7 @@ name: 'Build'
on: on:
push: push:
branches: [master, develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [master, develop]
@ -10,16 +10,18 @@ jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v2' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v2.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build' - name: 'Build'
run: 'npm run build' run: 'npm run build'
- run: 'npm run build:typescript'

View File

@ -2,7 +2,7 @@ name: 'Lint'
on: on:
push: push:
branches: [master, develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [master, develop]
@ -10,28 +10,19 @@ jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v2' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v2.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'lint:commit' - run: 'npm run lint:commit -- --to "${{ github.sha }}"'
run: 'npm run lint:commit -- --to "${{ github.sha }}"' - run: 'npm run lint:editorconfig'
- run: 'npm run lint:markdown'
- name: 'lint:editorconfig' - run: 'npm run lint:eslint'
run: 'npm run lint:editorconfig' - run: 'npm run lint:prettier'
- name: 'lint:markdown'
run: 'npm run lint:markdown'
- name: 'lint:typescript'
run: 'npm run lint:typescript'
- name: 'lint:prettier'
run: 'npm run lint:prettier'

View File

@ -7,24 +7,34 @@ on:
jobs: jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
permissions:
contents: 'write'
issues: 'write'
pull-requests: 'write'
id-token: 'write'
steps: steps:
- uses: 'actions/checkout@v2.3.4' - uses: 'actions/checkout@v3.5.2'
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v2.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build' - name: 'Build'
run: 'npm run build' run: 'npm run build'
- run: 'npm run build:typescript'
- name: 'Verify the integrity of provenance attestations and registry signatures for installed dependencies'
run: 'npm audit signatures'
- name: 'Release' - name: 'Release'
run: 'npm run release' run: 'npm run release'
env: env:

View File

@ -2,7 +2,7 @@ name: 'Test'
on: on:
push: push:
branches: [master, develop] branches: [develop]
pull_request: pull_request:
branches: [master, develop] branches: [master, develop]
@ -10,16 +10,16 @@ jobs:
test: test:
runs-on: 'ubuntu-latest' runs-on: 'ubuntu-latest'
steps: steps:
- uses: 'actions/checkout@v2' - uses: 'actions/checkout@v3.5.2'
- name: 'Use Node.js' - name: 'Setup Node.js'
uses: 'actions/setup-node@v2.5.1' uses: 'actions/setup-node@v3.6.0'
with: with:
node-version: '16.x' node-version: 'lts/*'
cache: 'npm' cache: 'npm'
- name: 'Install' - name: 'Install dependencies'
run: 'npm install' run: 'npm clean-install'
- name: 'Build' - name: 'Build'
run: 'npm run build' run: 'npm run build'

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ node_modules
# testing # testing
coverage coverage
.nyc_output
# envs # envs
.env .env

View File

@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:commit -- --edit

View File

@ -1,5 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint:staged
npm run build

View File

@ -1,10 +1,6 @@
{ {
"*": ["editorconfig-checker"], "*": ["editorconfig-checker"],
"*.{js,ts,jsx,tsx}": [ "*.{js,ts,jsx,tsx}": ["prettier --write", "eslint --fix"],
"prettier --write",
"eslint --fix",
"jest --findRelatedTests"
],
"*.{yml,json}": ["prettier --write"], "*.{yml,json}": ["prettier --write"],
"*.{md}": ["prettier --write", "markdownlint --dot --fix"] "*.md": ["prettier --write", "markdownlint --dot --fix"]
} }

11
.markdownlint-cli2.jsonc Normal file
View File

@ -0,0 +1,11 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true,
"MD033": false
},
"globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
}

View File

@ -1,6 +0,0 @@
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}

2
.npmrc
View File

@ -1,2 +1,2 @@
save-exact=true save-exact=true
engine-strict=true provenance=true

View File

@ -1,5 +0,0 @@
/build
node_modules
coverage
package.json
package-lock.json

2
.swcrc
View File

@ -13,7 +13,7 @@
"loose": true "loose": true
}, },
"module": { "module": {
"type": "commonjs", "type": "es6",
"strict": false, "strict": false,
"strictMode": true, "strictMode": true,
"lazy": false, "lazy": false,

9
.taprc Normal file
View File

@ -0,0 +1,9 @@
ts: false
jsx: false
flow: false
check-coverage: false
coverage: false
timeout: 120000
files:
- 'build/**/*.test.js'

View File

@ -3,7 +3,6 @@
"editorconfig.editorconfig", "editorconfig.editorconfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint"
"ms-azuretools.vscode-docker"
] ]
} }

27
.vscode/settings.json vendored
View File

@ -1,33 +1,14 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.importModuleSpecifierEnding": "js", "typescript.preferences.importModuleSpecifierEnding": "js",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.configPath": ".prettierrc.json", "prettier.configPath": ".prettierrc.json",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": true
}, },
"[markdown]": { "eslint.options": {
"editor.autoClosingBrackets": "always", "ignorePath": ".gitignore"
"editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[yaml]": { "prettier.ignorePath": ".gitignore"
"editor.autoClosingBrackets": "always",
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.autoClosingBrackets": "always",
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.autoClosingBrackets": "always",
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.autoClosingBrackets": "always",
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.autoClosingBrackets": "always",
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
} }

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
contact@divlo.fr. <contact@divlo.fr>.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -2,6 +2,14 @@
Thanks a lot for your interest in contributing to **html-w3c-validator**! 🎉 Thanks a lot for your interest in contributing to **html-w3c-validator**! 🎉
## Code of Conduct
**html-w3c-validator** adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
## Open Development
All work on **html-w3c-validator** happens directly on this repository. Both core team members and external contributors send pull requests which go through the same review process.
## Types of contributions ## Types of contributions
- Reporting a bug. - Reporting a bug.
@ -21,26 +29,4 @@ If you're adding new features to **html-w3c-validator**, please include tests.
## Commits ## Commits
The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases. The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.

View File

@ -24,7 +24,7 @@
**html-w3c-validator** is a CLI tool to validate multiple html pages using [validator.w3.org](https://validator.w3.org/). **html-w3c-validator** is a CLI tool to validate multiple html pages using [validator.w3.org](https://validator.w3.org/).
You might use a JavaScript framework or simply use HTML: **you should validate your production HTML** and this validation should be part of your CI/CD pipeline (tests, linting, etc.). You might use a JavaScript framework or simply use HTML but **you should always validate your production HTML** and this validation should be part of your CI/CD pipeline (tests, linting, etc.).
### Why should I validate my HTML pages? ### Why should I validate my HTML pages?
@ -56,19 +56,22 @@ npm install --save-dev html-w3c-validator start-server-and-test
{ {
"scripts": { "scripts": {
// Command to start the server serving your HTML pages (e.g: using vercel/serve) // Command to start the server serving your HTML pages (e.g: using vercel/serve)
"start": "serve ./build", "start": "serve \"./build\"",
// Command to validate your HTML pages // Command to validate your HTML pages
"test:html-w3c-validator": "start-server-and-test 'start' 'http://localhost:3000' 'html-w3c-validator'" "test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\""
} }
} }
``` ```
### `.html-w3c-validatorrc.json` ### `.html-w3c-validatorrc.json`
```json ```jsonc
{ {
"urls": ["http://localhost:3000/", "http://localhost:3000/about"] "urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"],
// You can also specify HTML files instead of URLs
"files": ["./index.html", "./about.html"]
} }
``` ```
@ -81,8 +84,8 @@ npm run test:html-w3c-validator
Example of output (in case of success): Example of output (in case of success):
```txt ```txt
✔ Validating http://localhost:3000/ ✔ Validating http://127.0.0.1:3000/
✔ Validating http://localhost:3000/about ✔ Validating http://127.0.0.1:3000/about
Success: HTML validation (W3C) passed! 🎉 Success: HTML validation (W3C) passed! 🎉
``` ```

View File

@ -1,3 +1,4 @@
{ {
"urls": ["http://localhost:3000/", "http://localhost:3000/about"] "urls": ["http://127.0.0.1:3000/", "http://127.0.0.1:3000/about"],
"files": ["./build/index.html", "./build/about.html"]
} }

27811
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,12 @@
"name": "example", "name": "example",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "serve ./build", "start": "serve \"./build\"",
"test:html-w3c-validator": "start-server-and-test 'start' 'http://localhost:3000' 'html-w3c-validator'" "test:html-w3c-validator": "start-server-and-test \"start\" \"http://127.0.0.1:3000\" \"html-w3c-validator\""
}, },
"devDependencies": { "devDependencies": {
"serve": "13.0.2", "serve": "14.2.0",
"start-server-and-test": "1.14.0", "start-server-and-test": "2.0.0",
"html-w3c-validator": "file:.." "html-w3c-validator": "file:.."
} }
} }

View File

@ -1,9 +0,0 @@
{
"testEnvironment": "node",
"resolver": "jest-ts-webcompat-resolver",
"transform": {
"^.+\\.(t|j)sx?$": ["@swc/jest"]
},
"rootDir": "./src",
"setupFiles": ["<rootDir>/__test__/setup.ts"]
}

30806
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
{ {
"name": "html-w3c-validator", "name": "html-w3c-validator",
"version": "0.0.0-development", "version": "0.0.0-development",
"public": true,
"description": "CLI for validating multiple html pages using validator.w3.org.", "description": "CLI for validating multiple html pages using validator.w3.org.",
"type": "module",
"author": "Divlo <contact@divlo.fr>", "author": "Divlo <contact@divlo.fr>",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
@ -20,68 +22,67 @@
], ],
"main": "build/index.js", "main": "build/index.js",
"bin": "build/index.js", "bin": "build/index.js",
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"files": [ "files": [
"build" "build"
], ],
"publishConfig": {
"access": "public",
"provenance": true
},
"engines": {
"node": ">=16.0.0",
"npm": ">=9.0.0"
},
"scripts": { "scripts": {
"build": "rimraf ./build && swc ./src --out-dir ./build && tsc", "build": "rimraf ./build && swc ./src --out-dir ./build",
"build:typescript": "tsc",
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules", "lint:markdown": "markdownlint-cli2",
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'", "lint:eslint": "eslint \".\" --ignore-path \".gitignore\"",
"lint:prettier": "prettier '.' --check", "lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "jest", "test": "tap",
"release": "semantic-release", "release": "semantic-release"
"postinstall": "husky install",
"prepublishOnly": "pinst --disable",
"postpublish": "pinst --enable"
}, },
"dependencies": { "dependencies": {
"chalk": "4.1.2", "chalk": "5.2.0",
"clipanion": "3.1.0", "clipanion": "3.2.0",
"html-validator": "6.0.0", "html-validator": "6.0.1",
"ora": "5.4.1", "log-symbols": "5.1.0",
"read-pkg": "5.2.0", "ora": "6.3.0",
"table": "6.8.0" "read-pkg": "8.0.0",
"table": "6.8.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "16.0.1", "@commitlint/cli": "17.6.3",
"@commitlint/config-conventional": "16.0.0", "@commitlint/config-conventional": "17.6.3",
"@swc/cli": "0.1.55", "@swc/cli": "0.1.62",
"@swc/core": "1.2.127", "@swc/core": "1.3.57",
"@swc/jest": "0.2.15", "@tsconfig/strictest": "2.0.1",
"@types/html-validator": "5.0.2", "@types/html-validator": "5.0.3",
"@types/jest": "27.4.0",
"@types/mock-fs": "4.13.1", "@types/mock-fs": "4.13.1",
"@types/node": "17.0.8", "@types/node": "20.1.3",
"editorconfig-checker": "4.0.2", "@types/tap": "15.0.8",
"eslint": "7.32.0", "@typescript-eslint/eslint-plugin": "5.59.5",
"eslint-config-prettier": "8.3.0", "editorconfig-checker": "5.0.1",
"eslint-config-standard-with-typescript": "21.0.1", "eslint": "8.40.0",
"eslint-plugin-import": "2.25.4", "eslint-config-conventions": "9.0.0",
"eslint-plugin-node": "11.1.0", "eslint-config-prettier": "8.8.0",
"eslint-plugin-prettier": "4.0.0", "eslint-plugin-import": "2.27.5",
"eslint-plugin-promise": "5.1.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unicorn": "40.0.0", "eslint-plugin-promise": "6.1.1",
"execa": "5.1.1", "eslint-plugin-unicorn": "47.0.0",
"husky": "7.0.4", "execa": "7.1.1",
"jest": "27.4.7", "lint-staged": "13.2.2",
"jest-mock-extended": "2.0.4", "markdownlint-cli2": "0.7.1",
"jest-ts-webcompat-resolver": "1.0.0", "markdownlint-rule-relative-links": "1.2.0",
"lint-staged": "12.1.5", "mock-fs": "5.2.0",
"markdownlint-cli": "0.30.0", "prettier": "2.8.8",
"mock-fs": "5.1.2", "rimraf": "5.0.0",
"npm": "8.3.0", "semantic-release": "21.0.2",
"pinst": "2.1.6", "serve": "14.2.0",
"prettier": "2.5.1", "tap": "16.3.4",
"rimraf": "3.0.2", "typescript": "5.0.4"
"semantic-release": "18.0.1",
"serve": "13.0.2",
"typescript": "4.5.4"
} }
} }

View File

@ -4,7 +4,12 @@ import fs from 'node:fs'
import { Command } from 'clipanion' import { Command } from 'clipanion'
import chalk from 'chalk' import chalk from 'chalk'
import ora from 'ora' import ora from 'ora'
import validateHTML, { ValidationMessageLocationObject } from 'html-validator' import logSymbols from 'log-symbols'
import type {
ValidationMessageLocationObject,
ParsedJsonAsValidationResults
} from 'html-validator'
import validateHTML from 'html-validator'
import { table } from 'table' import { table } from 'table'
import { isExistingPath } from './utils/isExistingPath.js' import { isExistingPath } from './utils/isExistingPath.js'
@ -13,21 +18,37 @@ const CURRENT_DIRECTORY = process.cwd()
const CONFIG_FILE_NAME = '.html-w3c-validatorrc.json' const CONFIG_FILE_NAME = '.html-w3c-validatorrc.json'
interface Config { interface Config {
urls: string[] urls?: string[]
files?: string[]
} }
interface Error { interface Error {
url: string data: string
messagesTable: string[][] messagesTable: string[][]
} }
interface Result {
data: string
isSuccess: boolean
}
const printResults = (results: Result[]): void => {
for (const result of results) {
if (result.isSuccess) {
console.log(logSymbols.success, result.data)
} else {
console.log(logSymbols.error, result.data)
}
}
}
export class HTMLValidatorCommand extends Command { export class HTMLValidatorCommand extends Command {
static usage = { static override usage = {
description: description:
'CLI for validating multiple html pages using <https://validator.w3.org/>.' 'CLI for validating multiple html pages using <https://validator.w3.org/>.'
} }
async execute(): Promise<number> { public async execute(): Promise<number> {
const configPath = path.join(CURRENT_DIRECTORY, CONFIG_FILE_NAME) const configPath = path.join(CURRENT_DIRECTORY, CONFIG_FILE_NAME)
try { try {
if (!(await isExistingPath(configPath))) { if (!(await isExistingPath(configPath))) {
@ -35,73 +56,120 @@ export class HTMLValidatorCommand extends Command {
`No config file found at ${configPath}. Please create ${CONFIG_FILE_NAME}.` `No config file found at ${configPath}. Please create ${CONFIG_FILE_NAME}.`
) )
} }
const configData = await fs.promises.readFile(configPath, { const configData = await fs.promises.readFile(configPath, {
encoding: 'utf-8' encoding: 'utf-8'
}) })
let config: Config = { urls: [] } let config: Config = { urls: [], files: [] }
let isValidConfig = true let isValidConfig = true
try { try {
config = JSON.parse(configData) config = JSON.parse(configData)
} catch { } catch {
isValidConfig = false isValidConfig = false
} }
isValidConfig = isValidConfig && Array.isArray(config.urls) isValidConfig =
isValidConfig &&
(Array.isArray(config.urls) || Array.isArray(config.urls))
if (!isValidConfig) { if (!isValidConfig) {
throw new Error( throw new Error(
`Invalid config file at ${configPath}. Please check the syntax.` `Invalid config file at ${configPath}. Please check the syntax.`
) )
} }
const urls =
config.urls == null
? []
: config.urls.map((url) => {
return { type: 'url', data: url }
})
const files =
config.files == null
? []
: config.files.map((file) => {
return { type: 'file', data: file }
})
const dataToValidate = [...urls, ...files]
const errors: Error[] = [] const errors: Error[] = []
let isValid = true let isValid = true
for (const url of config.urls) { const loader = ora(`Validating HTML (W3C)...`).start()
const loader = ora(`Validating ${url}`).start() const results: Result[] = []
const result = await validateHTML({ await Promise.all(
url, dataToValidate.map(async ({ data, type }) => {
format: 'json', try {
isLocal: true const options = {
}) format: 'json' as 'json' | undefined
const isValidHTML = result.messages.length === 0 }
if (isValidHTML) { let result: ParsedJsonAsValidationResults | undefined
loader.succeed() if (type === 'url') {
} else { result = await validateHTML({
loader.fail() url: data,
const messagesTable: string[][] = [] isLocal: true,
for (const message of result.messages) { ...options
const row: string[] = [] })
if (message.type === 'error') { } else if (type === 'file') {
row.push(chalk.red(message.type)) const htmlPath = path.resolve(CURRENT_DIRECTORY, data)
if (!(await isExistingPath(htmlPath))) {
throw new Error(
`No file found at ${htmlPath}. Please check the path.`
)
}
const html = await fs.promises.readFile(htmlPath, {
encoding: 'utf-8'
})
result = await validateHTML({
data: html,
...options
})
} else { } else {
row.push(chalk.yellow(message.type)) throw new Error('Invalid type')
} }
row.push(message.message) const hasErrors = result.messages.some((message) => {
const violation = message as ValidationMessageLocationObject return message.type === 'error'
if (violation.extract != null) { })
row.push( if (!hasErrors) {
`line: ${violation.lastLine}, column: ${violation.firstColumn}-${violation.lastColumn}` results.push({ data, isSuccess: true })
) } else {
results.push({ data, isSuccess: false })
const messagesTable: string[][] = []
for (const message of result.messages) {
if (message.type === 'error') {
const row: string[] = []
row.push(chalk.red(message.type))
row.push(message.message)
const violation = message as ValidationMessageLocationObject
if (violation.extract != null) {
row.push(
`line: ${violation.lastLine}, column: ${violation.firstColumn}-${violation.lastColumn}`
)
}
messagesTable.push(row)
}
}
errors.push({ data, messagesTable })
isValid = false
}
} catch (error) {
isValid = false
if (error instanceof Error) {
const messagesTable: string[][] = [[error.message]]
errors.push({ data, messagesTable })
} }
messagesTable.push(row)
} }
errors.push({ url, messagesTable }) })
isValid = false )
}
}
if (!isValid) { if (!isValid) {
loader.fail()
printResults(results)
for (const error of errors) { for (const error of errors) {
console.error(`\n${error.url}`) console.error(`\n${error.data}`)
console.error(table(error.messagesTable)) console.error(table(error.messagesTable))
console.error('------------------------------') console.error('------------------------------')
} }
console.error() console.error()
throw new Error('HTML validation (W3C) failed!') throw new Error('HTML validation (W3C) failed!')
} }
console.log() loader.succeed(
console.log(
`${chalk.bold.green('Success:')} HTML validation (W3C) passed! 🎉` `${chalk.bold.green('Success:')} HTML validation (W3C) passed! 🎉`
) )
printResults(results)
return 0 return 0
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {

View File

@ -1,26 +1,21 @@
import path from 'node:path' import tap from 'tap'
import { execa } from 'execa'
import execa from 'execa'
import { cli } from '../cli.js' import { cli } from '../cli.js'
import { HTMLValidatorCommand } from '../HTMLValidatorCommand.js' import { HTMLValidatorCommand } from '../HTMLValidatorCommand.js'
describe('html-w3c-validator', () => { await tap.test('html-w3c-validator', async (t) => {
afterEach(() => { await t.test('should be instance of the command', async (t) => {
jest.clearAllMocks()
})
it('should be instance of the command', async () => {
const command = cli.process([]) const command = cli.process([])
expect(command).toBeInstanceOf(HTMLValidatorCommand) t.equal(command instanceof HTMLValidatorCommand, true)
}) })
it('succeeds and validate the html correctly', async () => { await t.test('succeeds and validate the html correctly', async (t) => {
const examplePath = path.join(__dirname, '..', '..', 'example') const exampleURL = new URL('../../example', import.meta.url)
process.chdir(examplePath) process.chdir(exampleURL.pathname)
await execa('rimraf', ['node_modules']) await execa('rimraf', ['node_modules'])
await execa('npm', ['install']) await execa('npm', ['install'])
const { exitCode } = await execa('npm', ['run', 'test:html-w3c-validator']) const { exitCode } = await execa('npm', ['run', 'test:html-w3c-validator'])
expect(exitCode).toEqual(0) t.equal(exitCode, 0)
}) })
}) })

View File

@ -1 +0,0 @@
jest.setTimeout(60000)

View File

@ -1,11 +1,7 @@
import path from 'node:path'
import { Builtins, Cli } from 'clipanion' import { Builtins, Cli } from 'clipanion'
import readPackage from 'read-pkg'
import { HTMLValidatorCommand } from './HTMLValidatorCommand.js' import { HTMLValidatorCommand } from './HTMLValidatorCommand.js'
import { packageJSON } from './packageJSON.js'
const packageJSON = readPackage.sync({ cwd: path.join(__dirname, '..') })
export const cli = new Cli({ export const cli = new Cli({
binaryLabel: packageJSON.name, binaryLabel: packageJSON.name,

View File

@ -1,12 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
import chalk from 'chalk'
import { Cli } from 'clipanion' import { Cli } from 'clipanion'
import { cli } from './cli.js' import { cli } from './cli.js'
const [, , ...arguments_] = process.argv const [, , ...arguments_] = process.argv
cli.runExit(arguments_, Cli.defaultContext).catch(() => { await cli.runExit(arguments_, Cli.defaultContext)
console.error(chalk.red('Error occurred...'))
process.exit(1)
})

5
src/packageJSON.ts Normal file
View File

@ -0,0 +1,5 @@
import { readPackage } from 'read-pkg'
export const packageJSON = await readPackage({
cwd: new URL('..', import.meta.url)
})

View File

@ -1,29 +1,24 @@
import fsMock from 'mock-fs' import fsMock from 'mock-fs'
import tap from 'tap'
import { isExistingPath } from '../isExistingPath.js' import { isExistingPath } from '../isExistingPath.js'
describe('utils/isExistingFile', () => { await tap.test('utils/isExistingPath', async (t) => {
afterEach(async () => { t.afterEach(() => {
fsMock.restore() fsMock.restore()
}) })
it('should return true if the file exists', async () => { await t.test('should return true if the file exists', async () => {
fsMock( fsMock({
{ '/file.txt': ''
'/file.txt': '' })
}, t.equal(await isExistingPath('/file.txt'), true)
{ createCwd: false }
)
expect(await isExistingPath('/file.txt')).toBeTruthy()
}) })
it("should return false if the file doesn't exists", async () => { await t.test("should return false if the file doesn't exists", async () => {
fsMock( fsMock({
{ '/file.txt': ''
'/file.txt': '' })
}, t.equal(await isExistingPath('/randomfile.txt'), false)
{ createCwd: false }
)
expect(await isExistingPath('/randomfile.txt')).toBeFalsy()
}) })
}) })

View File

@ -1,14 +1,13 @@
{ {
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "commonjs", "module": "ESNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./build", "outDir": "./build",
"rootDir": "./src", "rootDir": "./src",
"noEmit": true, "noEmit": true,
"strict": true, "exactOptionalPropertyTypes": false
"skipLibCheck": true,
"esModuleInterop": true
} }
} }