Commit 6852d30d authored by David Sveningsson's avatar David Sveningsson

feat(cli): add `--init` to create initial configuration

parent efc621f7
......@@ -118,6 +118,15 @@ filesystem. Set the `root` property to `true` to prevent this behaviour:
}
```
### `--init`
Initialize project with a new configuration.
html-validate --init
The new configuration will be written to the current directory (in
`.htmlvalidate.json`).
### `--print-config`
Instead of validating file print the configuration generated.
......
......@@ -3471,6 +3471,16 @@
"@types/node": "*"
}
},
"@types/inquirer": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz",
"integrity": "sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==",
"dev": true,
"requires": {
"@types/through": "*",
"rxjs": "^6.4.0"
}
},
"@types/istanbul-lib-coverage": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
......@@ -3571,6 +3581,15 @@
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
"dev": true
},
"@types/through": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.29.tgz",
"integrity": "sha512-9a7C5VHh+1BKblaYiq+7Tfc+EOmjMdZaD1MYtkQjSoxgB69tBjW98ry6SKsi4zEIWztLOMRuL87A3bdT/Fc/4w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/unist": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
......@@ -7694,11 +7713,65 @@
"resolve-from": "^4.0.0"
}
},
"inquirer": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz",
"integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==",
"requires": {
"ansi-escapes": "^3.2.0",
"chalk": "^2.4.2",
"cli-cursor": "^2.1.0",
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.12",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
"string-width": "^2.1.0",
"strip-ansi": "^5.1.0",
"through": "^2.3.6"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"requires": {
"ansi-regex": "^4.1.0"
}
}
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mute-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
......@@ -8318,9 +8391,9 @@
}
},
"external-editor": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
"integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
"integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
"requires": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
......@@ -10668,53 +10741,95 @@
}
},
"inquirer": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.4.1.tgz",
"integrity": "sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz",
"integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==",
"requires": {
"ansi-escapes": "^3.2.0",
"ansi-escapes": "^4.2.1",
"chalk": "^2.4.2",
"cli-cursor": "^2.1.0",
"cli-cursor": "^3.1.0",
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
"lodash": "^4.17.11",
"mute-stream": "0.0.7",
"figures": "^3.0.0",
"lodash": "^4.17.15",
"mute-stream": "0.0.8",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
"string-width": "^2.1.0",
"string-width": "^4.1.0",
"strip-ansi": "^5.1.0",
"through": "^2.3.6"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
"ansi-escapes": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz",
"integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==",
"requires": {
"type-fest": "^0.5.2"
}
},
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"requires": {
"restore-cursor": "^3.1.0"
}
},
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"figures": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz",
"integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==",
"requires": {
"escape-string-regexp": "^1.0.5"
}
},
"is-fullwidth-code-point": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
},
"onetime": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
"integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
"requires": {
"mimic-fn": "^2.1.0"
}
},
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"string-width": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
"integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz",
"integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==",
"requires": {
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^4.0.0"
},
"dependencies": {
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "^3.0.0"
}
}
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^5.2.0"
}
},
"type-fest": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz",
"integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw=="
}
}
},
......@@ -14329,9 +14444,9 @@
}
},
"mute-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="
},
"nan": {
"version": "2.12.1",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should generate configuration for angularjs 1`] = `
"{
\\"elements\\": [
\\"html5\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.js$\\": \\"html-validate-angular/js\\",
\\"^.*\\\\\\\\.html$\\": \\"html-validate-angular/html\\"
}
}"
`;
exports[`should generate configuration for combined 1`] = `
"{
\\"elements\\": [
\\"html5\\",
\\"html-validate-vue/elements\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.js$\\": \\"html-validate-angular/js\\",
\\"^.*\\\\\\\\.html$\\": \\"html-validate-angular/html\\",
\\"^.*\\\\\\\\.vue$\\": \\"html-validate-vue\\",
\\"^.*\\\\\\\\.md$\\": \\"html-validate-markdown\\"
}
}"
`;
exports[`should generate configuration for default 1`] = `
"{
\\"elements\\": [
\\"html5\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
]
}"
`;
exports[`should generate configuration for markdown 1`] = `
"{
\\"elements\\": [
\\"html5\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.md$\\": \\"html-validate-markdown\\"
}
}"
`;
exports[`should generate configuration for vuejs 1`] = `
"{
\\"elements\\": [
\\"html5\\",
\\"html-validate-vue/elements\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.vue$\\": \\"html-validate-vue\\"
}
}"
`;
......@@ -6,6 +6,7 @@ import HtmlValidate from "../htmlvalidate";
import { Report } from "../reporter";
import { expandFiles, ExpandOptions } from "./expand-files";
import { getFormatter } from "./formatter";
import { init, InitResult } from "./init";
export interface CLIOptions {
configFile?: string;
......@@ -38,6 +39,16 @@ export class CLI {
return getFormatter(formatters);
}
/**
* Initialize project with a new configuration.
*
* A new `.htmlvalidate.json` file will be placed in the path provided by
* `cwd`.
*/
public init(cwd: string): Promise<InitResult> {
return init(cwd);
}
/**
* Get HtmlValidate instance with configuration based on options passed to the
* constructor.
......
......@@ -12,6 +12,7 @@ import { CLI } from "./cli";
enum Mode {
LINT,
INIT,
DUMP_EVENTS,
DUMP_TOKENS,
DUMP_TREE,
......@@ -19,6 +20,10 @@ enum Mode {
}
function getMode(argv: { [key: string]: any }): Mode {
if (argv.init) {
return Mode.INIT;
}
if (argv["dump-events"]) {
return Mode.DUMP_EVENTS;
}
......@@ -99,6 +104,7 @@ const argv: minimist.ParsedArgs = minimist(process.argv.slice(2), {
"stdin-filename",
],
boolean: [
"init",
"dump-events",
"dump-tokens",
"dump-tree",
......@@ -143,6 +149,7 @@ Debugging options:
Miscellaneous:
-c, --config=STRING use custom configuration file.
--init initialize project with a new configuration
--print-config output configuration for given file.
-h, --help show help.
--version show version.
......@@ -170,7 +177,7 @@ if (argv.version) {
process.exit();
}
if (argv.help || argv._.length === 0) {
if (argv.help || (argv._.length === 0 && !argv.init)) {
showUsage();
process.exit();
}
......@@ -198,7 +205,7 @@ const extensions = (argv.ext || "html").split(",").map((cur: string) => {
});
const files = cli.expandFiles(argv._, { extensions });
if (files.length === 0) {
if (files.length === 0 && mode !== Mode.INIT) {
console.error("No files matching patterns", argv._);
process.exit(1);
}
......@@ -222,6 +229,18 @@ try {
}
process.exit(result.valid ? 0 : 1);
} else if (mode === Mode.INIT) {
cli
.init(process.cwd())
.then(result => {
console.log(`Configuration written to "${result.filename}"`);
})
.catch(err => {
if (err) {
console.error(err);
}
process.exit(1);
});
} else if (mode === Mode.PRINT_CONFIG) {
const config = htmlvalidate.getConfigFor(files[0]);
const json = JSON.stringify(config.get(), null, 2);
......
const fs = {
existsSync: jest.fn(),
writeFile: jest.fn().mockImplementation((fn, data, cb) => cb()),
};
const inquirer = {
prompt: jest.fn(),
};
jest.mock("inquirer", () => inquirer);
jest.mock("fs", () => fs);
import { CLI } from "./cli";
let cli: CLI;
beforeEach(() => {
jest.clearAllMocks();
cli = new CLI();
});
it.each([
["default", []],
["angularjs", ["AngularJS"]],
["vuejs", ["Vue.js"]],
["markdown", ["Markdown"]],
["combined", ["AngularJS", "Vue.js", "Markdown"]],
])("should generate configuration for %s", async (name, frameworks) => {
expect.assertions(2);
fs.existsSync.mockReturnValue(false);
inquirer.prompt.mockResolvedValue({
write: true,
frameworks,
});
await cli.init(".");
expect(fs.writeFile).toHaveBeenCalledWith(
"./.htmlvalidate.json",
expect.anything(),
expect.anything()
);
expect(fs.writeFile.mock.calls[0][1]).toMatchSnapshot();
});
it("should not overwrite configuration unless requested", async () => {
expect.assertions(1);
fs.existsSync.mockReturnValue(true);
inquirer.prompt.mockResolvedValue({
write: false,
});
try {
await cli.init(".");
} catch (err) {
/* do nothing */
}
expect(fs.writeFile).not.toHaveBeenCalled();
});
it("should propagate errors from fs.writeFile", async () => {
expect.assertions(1);
fs.existsSync.mockReturnValue(false);
fs.writeFile.mockImplementationOnce((fn, data, cb) => cb("mock error"));
inquirer.prompt.mockResolvedValue({
write: true,
frameworks: [],
});
await expect(cli.init(".")).rejects.toEqual("mock error");
});
import deepmerge from "deepmerge";
import fs from "fs";
import inquirer from "inquirer";
import { ConfigData } from "../config";
export interface InitResult {
filename: string;
}
export enum Frameworks {
angularjs = "AngularJS",
vuejs = "Vue.js",
markdown = "Markdown",
}
const frameworkConfig: Record<string, ConfigData> = {
[Frameworks.angularjs]: {
transform: {
"^.*\\.js$": "html-validate-angular/js",
"^.*\\.html$": "html-validate-angular/html",
},
},
[Frameworks.vuejs]: {
elements: ["html-validate-vue/elements"],
transform: {
"^.*\\.vue$": "html-validate-vue",
},
},
[Frameworks.markdown]: {
transform: {
"^.*\\.md$": "html-validate-markdown",
},
},
};
function addFrameworks(src: ConfigData, frameworks: string[]): ConfigData {
let config = src;
for (const framework of frameworks) {
config = deepmerge(config, frameworkConfig[framework]);
}
return config;
}
function writeConfig(dst: string, config: ConfigData): Promise<void> {
return new Promise((resolve, reject) => {
fs.writeFile(dst, JSON.stringify(config, null, 2), err => {
if (err) reject(err);
resolve();
});
});
}
export async function init(cwd: string): Promise<InitResult> {
const filename = `${cwd}/.htmlvalidate.json`;
const exists = fs.existsSync(filename);
const initialConfig: ConfigData = {
elements: ["html5"],
extends: ["htmlvalidate:recommended"],
};
const when = /* istanbul ignore next */ (answers: any): boolean => {
return !exists || answers.write;
};
const questions: inquirer.QuestionCollection = [
{
name: "write",
type: "confirm",
default: false,
when: exists,
message:
"A .htmlvalidate.json file already exists, do you want to overwrite it?",
},
{
name: "frameworks",
type: "checkbox",
choices: [Frameworks.angularjs, Frameworks.vuejs, Frameworks.markdown],
message: "Support additional frameworks?",
when,
},
];
/* prompt user for questions */
const answers = await inquirer.prompt(questions);
if (!answers.write) {
return Promise.reject();
}
/* write configuration to file */
let config = initialConfig;
config = addFrameworks(config, answers.frameworks);
await writeConfig(filename, config);
return {
filename,
};
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment