1
1
mirror of https://github.com/theoludwig/html-w3c-validator.git synced 2024-12-08 00:45:37 +01:00

feat: add html-w3c-validator command

This commit is contained in:
Divlo 2022-01-06 22:34:58 +01:00
parent 39b05c66a7
commit 77cefcd53f
No known key found for this signature in database
GPG Key ID: 8F9478F220CE65E9
18 changed files with 34634 additions and 449 deletions

View File

@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'daily'
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'daily'

2
.gitignore vendored
View File

@ -3,7 +3,7 @@ node_modules
.npm
# production
build
/build
.swc
# testing

View File

@ -1,4 +1,4 @@
build
/build
node_modules
coverage
package.json

View File

@ -1,5 +1,5 @@
{
"branches": [{ "name": "master", "channel": "beta", "prerelease": true }],
"branches": ["master"],
"plugins": [
[
"@semantic-release/commit-analyzer",

View File

@ -65,7 +65,7 @@ npm install --save-dev html-w3c-validator start-server-and-test
}
```
### `.html-w3c-validator.json`
### `.html-w3c-validatorrc.json`
```json
{
@ -79,18 +79,22 @@ npm install --save-dev html-w3c-validator start-server-and-test
npm run test:html-w3c-validator
```
Example of output:
Example of output (in case of success):
```txt
✔ Validating http://localhost:3000/
✔ Validating http://localhost:3000/about
Success: HTML validation (W3C) passed! 🎉
```
See the [./example](./example) folder for practical usage.
### Options
```text
-V, --version Output the version number.
-h, --help Display help for command.
--config Path to a configuration file (default: `.html-w3c-validator.json`).
```
## 💡 Contributing

View File

@ -0,0 +1,3 @@
{
"urls": ["http://localhost:3000/", "http://localhost:3000/about"]
}

11
example/build/about.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About</title>
</head>
<body>
</body>
</html>

9
example/build/index.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Home</title>
</head>
<body></body>
</html>

27688
example/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
example/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "example",
"private": true,
"scripts": {
"start": "serve ./build",
"test:html-w3c-validator": "start-server-and-test 'start' 'http://localhost:3000' 'html-w3c-validator'"
},
"devDependencies": {
"serve": "13.0.2",
"start-server-and-test": "1.14.0",
"html-w3c-validator": "file:.."
}
}

View File

@ -5,7 +5,5 @@
"^.+\\.(t|j)sx?$": ["@swc/jest"]
},
"rootDir": "./src",
"collectCoverage": true,
"coverageDirectory": "../coverage/",
"coverageReporters": ["text", "cobertura"]
"setupFiles": ["<rootDir>/__test__/setup.ts"]
}

15570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,9 @@
"chalk": "4.1.2",
"clipanion": "3.1.0",
"html-validator": "6.0.0",
"read-pkg": "5.2.0"
"ora": "5.4.1",
"read-pkg": "5.2.0",
"table": "6.8.0"
},
"devDependencies": {
"@commitlint/cli": "16.0.1",
@ -54,7 +56,9 @@
"@swc/cli": "0.1.55",
"@swc/core": "1.2.127",
"@swc/jest": "0.2.15",
"@types/html-validator": "5.0.2",
"@types/jest": "27.4.0",
"@types/mock-fs": "4.13.1",
"@types/node": "17.0.8",
"editorconfig-checker": "4.0.2",
"eslint": "7.32.0",
@ -65,16 +69,20 @@
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-promise": "5.1.1",
"eslint-plugin-unicorn": "40.0.0",
"execa": "5.1.1",
"husky": "7.0.4",
"jest": "27.4.7",
"jest-mock-extended": "2.0.4",
"jest-ts-webcompat-resolver": "1.0.0",
"lint-staged": "12.1.5",
"markdownlint-cli": "0.30.0",
"mock-fs": "5.1.2",
"npm": "8.3.0",
"pinst": "2.1.6",
"prettier": "2.5.1",
"rimraf": "3.0.2",
"semantic-release": "18.0.1",
"serve": "13.0.2",
"typescript": "4.5.4"
}
}

View File

@ -1,6 +1,25 @@
import { Command } from 'clipanion'
import path from 'node:path'
import fs from 'node:fs'
// const CURRENT_DIRECTORY = process.cwd()
import { Command } from 'clipanion'
import chalk from 'chalk'
import ora from 'ora'
import validateHTML, { ValidationMessageLocationObject } from 'html-validator'
import { table } from 'table'
import { isExistingPath } from './utils/isExistingPath.js'
const CURRENT_DIRECTORY = process.cwd()
const CONFIG_FILE_NAME = '.html-w3c-validatorrc.json'
interface Config {
urls: string[]
}
interface Error {
url: string
messagesTable: string[][]
}
export class HTMLValidatorCommand extends Command {
static usage = {
@ -9,7 +28,90 @@ export class HTMLValidatorCommand extends Command {
}
async execute(): Promise<number> {
console.log('html-w3c-validator')
const configPath = path.join(CURRENT_DIRECTORY, CONFIG_FILE_NAME)
try {
if (!(await isExistingPath(configPath))) {
throw new Error(
`No config file found at ${configPath}. Please create ${CONFIG_FILE_NAME}.`
)
}
const configData = await fs.promises.readFile(configPath, {
encoding: 'utf-8'
})
let config: Config = { urls: [] }
let isValidConfig = true
try {
config = JSON.parse(configData)
} catch {
isValidConfig = false
}
isValidConfig = isValidConfig && Array.isArray(config.urls)
if (!isValidConfig) {
throw new Error(
`Invalid config file at ${configPath}. Please check the syntax.`
)
}
const errors: Error[] = []
let isValid = true
for (const url of config.urls) {
const loader = ora(`Validating ${url}`).start()
const result = await validateHTML({
url,
format: 'json',
isLocal: true
})
const isValidHTML = result.messages.length === 0
if (isValidHTML) {
loader.succeed()
} else {
loader.fail()
const messagesTable: string[][] = []
for (const message of result.messages) {
const row: string[] = []
if (message.type === 'error') {
row.push(chalk.red(message.type))
} else {
row.push(chalk.yellow(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({ url, messagesTable })
isValid = false
}
}
if (!isValid) {
for (const error of errors) {
console.error(`\n${error.url}`)
console.error(table(error.messagesTable))
console.error('------------------------------')
}
console.error()
throw new Error('HTML validation (W3C) failed!')
}
console.log()
console.log(
`${chalk.bold.green('Success:')} HTML validation (W3C) passed! 🎉`
)
return 0
} catch (error) {
if (error instanceof Error) {
console.error(`${chalk.bold.red('Error:')} ${error.message}`)
} else {
console.error(
`${chalk.bold.red('Error:')} HTML validation (W3C) failed!`
)
}
return 1
}
}
}

View File

@ -1,3 +1,7 @@
import path from 'node:path'
import execa from 'execa'
import { cli } from '../cli.js'
import { HTMLValidatorCommand } from '../HTMLValidatorCommand.js'
@ -12,11 +16,11 @@ describe('html-w3c-validator', () => {
})
it('succeeds and validate the html correctly', async () => {
console.log = jest.fn()
const exitCode = await cli.run([], {
stdin: process.stdin
})
expect(console.log).toHaveBeenCalledWith('html-w3c-validator')
const examplePath = path.join(__dirname, '..', '..', 'example')
process.chdir(examplePath)
await execa('rimraf', ['node_modules'])
await execa('npm', ['install'])
const { exitCode } = await execa('npm', ['run', 'test:html-w3c-validator'])
expect(exitCode).toEqual(0)
})
})

1
src/__test__/setup.ts Normal file
View File

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

View File

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

View File

@ -0,0 +1,10 @@
import fs from 'node:fs'
export const isExistingPath = async (path: string): Promise<boolean> => {
try {
await fs.promises.access(path, fs.constants.F_OK)
return true
} catch {
return false
}
}