Commit bb94341e authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(htmlvalidate)!: use `StaticConfigLoader` by default

BREAKING CHANGE: This change only affects API users, the CLI tool continues to
work as before.

The default configuration loader has changed from `FileSystemConfigLoader` to
`StaticConfigLoader`, i.e. the directory traversal looking for
`.htmlvalidate.json` configuration files must now be explicitly enabled.

See (MIGRATION.md)[https://html-validate.org/migration/index.html] for details.
parent 4ab84991
Pipeline #377496935 passed with stages
in 11 minutes and 59 seconds
......@@ -169,6 +169,54 @@ If you used `requiredAttributes` or `deprecatedAttributes` these have now been i
### `ConfigReadyEvent`
**Only affects API users.**
If you have a rule or plugin listening to the `ConfigReadyEvent` event the datatype of the `config` property has changed from `ConfigData` to `ResolvedConfig`.
For most part it contains the same information but is normalized, for instance rules are now always passed as `Record<RuleID, [Severity, Options]>`.
Configured transformers, plugins etc are resolved instances and fields suchs as `root` and `extends` will never be present.
### `StaticConfigLoader`
**Only affects API users.**
The default configuration loader has changed from {@link dev/using-api#filesystemconfigloader `FileSystemConfigLoader`} to {@link dev/using-api#staticconfigloader- `StaticConfigLoader`}, i.e. the directory traversal looking for `.htmlvalidate.json` configuration files must now be explicitly enabled.
This will reduce the dependency on the NodeJS `fs` module and make it easier to use the library in browsers.
To restore the previous behaviour you must now enable `FileSystemConfigLoader`:
```diff
import { HtmlValidate, FileSystemConfigLoader } from "html-validate";
-const htmlvalidate = new HtmlValidate();
+const loader = new FileSystemConfigLoader();
+const htmlvalidate = new HtmlValidate(loader);
```
If you pass configuration to the constructor you now pass it to the loader instead:
```diff
import { HtmlValidate, FileSystemConfigLoader } from "html-validate";
-const htmlvalidate = new HtmlValidate({ ... });
+const loader = new FileSystemConfigLoader({ ... });
+const htmlvalidate = new HtmlValidate(loader);
```
If you use the `root` property as a workaround for the directory traversal you can now drop the workaround and rely on `StaticConfigLoader`:
```diff
import { HtmlValidate } from "html-validate";
-const htmlvalidate = new HtmlValidate({
- root: true,
-});
+const htmlvalidate = new HtmlValidate();
```
The CLI class is not affected as it will enable `FileSystemConfigLoader` automatically, so the following code will continue to work as expected:
```ts
const cli = new CLI();
const htmlvalidate = cli.getValidator();
```
......@@ -167,13 +167,15 @@ console.log(stylish(report.results));
## Configuration loaders
By default `HtmlValidate` traverses the file system looking for configuration files such as `.htmlvalidate.json`.
If this behaviour is not desired a custom loader can be used instead:
Since v6 the `HtmlValidate` API uses `StaticConfigLoader` by default which only loads static configuration (configuration passed to constructor or calls to validation functions).
The CLI tool uses `FileSystemConfigLoader` instead which traversess the file system looking for configuration files such as `.htmlvalidate.json`.
To specify a loader pass it as the first argument to constructor:
```ts
import { StaticConfigLoader, HtmlValidate } from "html-validate";
import { FileSystemConfigLoader, HtmlValidate } from "html-validate";
const loader = new StaticConfigLoader();
const loader = new FileSystemConfigLoader();
const htmlvalidate = new HtmlValidate(loader);
```
......@@ -206,7 +208,7 @@ class MyCustomLoader extends ConfigLoader {
The custom loader is used the same as builtin loaders:
```diff
-const loader = new StaticConfigLoader();
-const loader = new FileSystemConfigLoader();
+const loader = new MyCustomLoader();
const htmlvalidate = new HtmlValidate(loader);
```
......@@ -221,15 +223,15 @@ htmlvalidate.validateString("..", "my-fancy-handle");
This will generate calls to `getConfigFor("foo.html")` and `getConfigFor("my-fancy-handle")` respectively.
While `validateFile` requires the file to be readable, the second argument to `validateString` can be any handle the API user wants as long as the loader can understand it.
### `FileSystemConfigLoader([config: ConfigData])` (default)
### `FileSystemConfigLoader([config: ConfigData])`
Default loader which traverses filesystem looking for `.htmlvalidate.json` configuration files, starting at the directory of the target filename.
Loader which traverses filesystem looking for `.htmlvalidate.json` configuration files, starting at the directory of the target filename.
The result from the configuration files are merged both with a global configuration and optionally explicit overrides from the calls to `validateFile`, `validateString` and `validateSource`.
### `StaticConfigLoader([config: ConfigData])`
### `StaticConfigLoader([config: ConfigData])` (default)
Loads configuration only from the configuration passed to the constructor or explicit overrides to `validateString(..)`.
Default loader which loads configuration only from the configuration passed to the constructor or explicit overrides to `validateString(..)`.
```ts
const loader = StaticConfigLoader({
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HtmlValidate configuration smoketest test-files/config/cjs-config/file.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/directive/disable.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/directive/disable-block.html 1`] = `undefined`;
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`test-files/config/cjs-config/file.html 1`] = `
Object {
"errorCount": 0,
"results": Array [],
"valid": true,
"warningCount": 0,
}
`;
exports[`test-files/config/directive/disable.html 1`] = `
Object {
"errorCount": 2,
"results": Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/directive/disable.html",
"messages": Array [
Object {
"column": 3,
"context": "i",
"line": 1,
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 8,
"message": "<i> must not be self-closed",
"offset": 212,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(4)",
"severity": 2,
"size": 2,
},
],
"source": "<i/>Before disable, should trigger
<!-- [html-validate-disable no-self-closing] -->
<i/>After disable, should not trigger
<i/>After disable, should not trigger
<!-- [html-validate-enable no-self-closing] -->
<i/>Before after, should trigger
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/directive/disable-block.html 1`] = `
Object {
"errorCount": 4,
"results": Array [
Object {
"errorCount": 4,
"filePath": "test-files/config/directive/disable-block.html",
"messages": Array [
Object {
"column": 3,
"context": "i",
"line": 1,
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 4,
"context": "i",
"line": 3,
"message": "<i> must not be self-closed",
"offset": 43,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "div > i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 11,
"message": "<i> must not be self-closed",
"offset": 335,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(3)",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 16,
"message": "<i> must not be self-closed",
"offset": 439,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(5)",
"severity": 2,
"size": 2,
},
],
"source": "<i/>Outside block, should trigger
<div>
<i/>Inside block, before directive, should trigger
<!-- [html-validate-disable-block no-self-closing] -->
<i/>Inside block, after directive, shout not trigger
<div>
<i/>Inside block, after directive, shout not trigger
</div>
<i/>Inside block, after directive, shout not trigger
</div>
<i/>Outside block, should trigger
<div>
<!-- [html-validate-disable-block no-self-closing] -->
</div>
<i/>Outside block, should trigger
<!-- [html-validate-disable-block no-self-closing] -->
Should handle when root element is parent but no children
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/directive/disable-multiple.html 1`] = `
Object {
"errorCount": 2,
"results": Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/directive/disable-multiple.html",
"messages": Array [
Object {
"column": 2,
"context": Object {
"documentation": "\`<blink>\` has no direct replacement and blinking text is frowned upon by accessibility standards.",
"source": "non-standard",
"tagName": "blink",
},
"line": 1,
"message": "<blink> is deprecated",
"offset": 1,
"ruleId": "deprecated",
"ruleUrl": "https://html-validate.org/rules/deprecated.html",
"selector": "blink",
"severity": 2,
"size": 5,
},
Object {
"column": 7,
"context": "blink",
"line": 1,
"message": "<blink> must not be self-closed",
"offset": 6,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "blink",
"severity": 2,
"size": 2,
},
],
"source": "<blink/> Outside block, should trigger
<div>
<!-- [html-validate-disable-block no-self-closing, deprecated] -->
<blink/>Inside block, should not trigger
</div>
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/directive/disable-next.html 1`] = `
Object {
"errorCount": 2,
"results": Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/directive/disable-next.html",
"messages": Array [
Object {
"column": 3,
"context": "i",
"line": 1,
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 5,
"message": "<i> must not be self-closed",
"offset": 136,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(3)",
"severity": 2,
"size": 2,
},
],
"source": "<i/>Before disable, should trigger
<!-- [html-validate-disable-next no-self-closing] -->
<i/>First after disable, should not trigger
<i/>Second after disable, should trigger
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/elements/file.html 1`] = `
Object {
"errorCount": 2,
"results": Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/elements/file.html",
"messages": Array [
Object {
"column": 3,
"context": Object {
"tagName": "deprecated",
},
"line": 2,
"message": "<deprecated> is deprecated",
"offset": 8,
"ruleId": "deprecated",
"ruleUrl": "https://html-validate.org/rules/deprecated.html",
"selector": "div > deprecated",
"severity": 2,
"size": 10,
},
Object {
"column": 4,
"context": undefined,
"line": 5,
"message": "Element <li> is not permitted as content in <ul>",
"offset": 67,
"ruleId": "element-permitted-content",
"ruleUrl": "https://html-validate.org/rules/element-permitted-content.html",
"selector": "div > ul > li",
"severity": 2,
"size": 2,
},
],
"source": "<div>
<deprecated>new (deprecated) element</deprecated>
<ul>
<li>overridden so it is disallowed</li>
<p>overridden so it is allowed</p>
</ul>
</div>
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/file.html 1`] = `
Object {
"errorCount": 1,
"results": Array [
Object {
"errorCount": 1,
"filePath": "test-files/config/file.html",
"messages": Array [
Object {
"column": 6,
"context": "br",
"line": 2,
"message": "End tag for <br> must be omitted",
"offset": 49,
"ruleId": "void-content",
"ruleUrl": "https://html-validate.org/rules/void-content.html",
"selector": null,
"severity": 2,
"size": 3,
},
],
"source": "<!-- default configuration yields error -->
<br></br>
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/js-config/file.html 1`] = `
Object {
"errorCount": 0,
"results": Array [],
"valid": true,
"warningCount": 0,
}
`;
exports[`test-files/config/off/error/file.html 1`] = `
Object {
"errorCount": 1,
"results": Array [
Object {
"errorCount": 1,
"filePath": "test-files/config/off/error/file.html",
"messages": Array [
Object {
"column": 6,
"context": "br",
"line": 2,
"message": "End tag for <br> must be omitted",
"offset": 42,
"ruleId": "void-content",
"ruleUrl": "https://html-validate.org/rules/void-content.html",
"selector": null,
"severity": 2,
"size": 3,
},
],
"source": "<!-- reconfigured to yield error -->
<br></br>
",
"warningCount": 0,
},
],
"valid": false,
"warningCount": 0,
}
`;
exports[`test-files/config/off/file.html 1`] = `
Object {
"errorCount": 0,
"results": Array [],
"valid": true,
"warningCount": 0,
}
`;
exports[`test-files/config/warn/file.html 1`] = `
Object {
"errorCount": 0,
"results": Array [
Object {
"errorCount": 0,
"filePath": "test-files/config/warn/file.html",
"messages": Array [
Object {
"column": 6,
"context": "br",
"line": 2,
"message": "End tag for <br> must be omitted",
"offset": 44,
"ruleId": "void-content",
"ruleUrl": "https://html-validate.org/rules/void-content.html",
"selector": null,
"severity": 1,
"size": 3,
},
],
"source": "<!-- configured to yield a warning -->
<br></br>
",
"warningCount": 1,
},
],
"valid": true,
"warningCount": 1,
}
`;
import { readFileSync } from "fs";
import { ConfigData, UserError, HtmlValidate, Report } from "..";
import {
FileSystemConfigLoader,
ConfigData,
ConfigLoader,
UserError,
HtmlValidate,
Report,
} from "..";
import { expandFiles, ExpandOptions } from "./expand-files";
import { getFormatter } from "./formatter";
import { IsIgnored } from "./is-ignored";
......@@ -17,6 +24,7 @@ export interface CLIOptions {
export class CLI {
private options: CLIOptions;
private config: ConfigData;
private loader: ConfigLoader;
private ignored: IsIgnored;
/**
......@@ -28,6 +36,7 @@ export class CLI {
public constructor(options?: CLIOptions) {
this.options = options || {};
this.config = this.getConfig();
this.loader = new FileSystemConfigLoader(this.config);
this.ignored = new IsIgnored();
}
......@@ -70,15 +79,29 @@ export class CLI {
* or call [[HtmlValidate.flushConfigCache]].
*/
public clearCache(): void {
this.loader.flushCache();
this.ignored.clearCache();
}
/**
* Get HtmlValidate instance with configuration based on options passed to the
* constructor.
*
* @internal
*/
public getLoader(): ConfigLoader {
return this.loader;
}
/**
* Get HtmlValidate instance with configuration based on options passed to the
* constructor.
*
* @public
*/
public getValidator(): HtmlValidate {
return new HtmlValidate(this.config);
const loader = this.getLoader();
return new HtmlValidate(loader);
}
/**
......
import path from "path";
import { CLI } from "./cli";
const root = path.resolve(__dirname, "../../");
const cli = new CLI();
const files = cli.expandFiles(["test-files/config"]).map((it) => path.relative(root, it));
const htmlvalidate = cli.getValidator();
it.each(files)("%s", (filename) => {
expect.assertions(1);
const report = htmlvalidate.validateFile(filename);
expect(report).toMatchSnapshot();
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FileSystemConfigLoader smoketest test-files/config/cjs-config/file.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "off",
},
"transform": Object {},
}
`;
exports[`FileSystemConfigLoader smoketest test-files/config/directive/disable.html 1`] = `
Object {
"extends": Array [
......
......@@ -8,7 +8,7 @@ import { Parser } from "./parser";
import { Report, Reporter } from "./reporter";
import { RuleDocumentation } from "./rule";
import configurationSchema from "./schema/config.json";
import { FileSystemConfigLoader } from "./config/loaders/