dbdiagram 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/build/build-document.action.js +11 -4
- package/dist/actions/push.action.js +7 -4
- package/dist/actions/validate.action.js +51 -0
- package/dist/commands/command.loader.js +2 -0
- package/dist/commands/validate.command.js +11 -0
- package/dist/config.js +1 -0
- package/dist/constants/cli-error.constant.js +1 -0
- package/dist/errors/dbml-parse.error.js +9 -0
- package/dist/hooks/validate.hook.js +12 -0
- package/dist/services/dbml/dbml.service.js +28 -12
- package/dist/services/dbml/dbml.worker.js +56 -35
- package/dist/types/dbml-diagnostic.type.js +1 -0
- package/dist/utils/dbml.util.js +17 -21
- package/dist/utils/output.util.js +13 -0
- package/package.json +3 -1
|
@@ -3,14 +3,15 @@ import { log, spinner } from '@clack/prompts';
|
|
|
3
3
|
import { DBDOCS_CONFIGS } from '../../config.js';
|
|
4
4
|
import { getCredential } from '../../config/credential-manager.js';
|
|
5
5
|
import { DOCUMENT_SOURCE_TYPE } from '../../constants/document.constant.js';
|
|
6
|
-
import { AUTH_NOT_LOGGED_IN,
|
|
6
|
+
import { AUTH_NOT_LOGGED_IN, FILE_READ_ERROR, PROJECT_NAME_INVALID, PROJECT_NAME_MISSING, SOURCE_MISSING, VERSION_NAME_INVALID, WORKSPACE_NAME_INVALID, WORKSPACE_NAME_MISSING, } from '../../constants/cli-error.constant.js';
|
|
7
7
|
import { CliError } from '../../errors/cli.error.js';
|
|
8
8
|
import { portalIntegration } from '../../integrations/portal/portal.integration.js';
|
|
9
9
|
import { trackEvent } from '../../services/telemetry.service.js';
|
|
10
10
|
import { prepareBuildDocumentData } from '../../services/dbml/dbml.service.js';
|
|
11
11
|
import { getCliErrorMessageFromCode, toCliError } from '../../utils/cli-error.util.js';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
12
|
+
import { getDbmlVersion } from '../../utils/dbml.util.js';
|
|
13
|
+
import { DbmlParseError } from '../../errors/dbml-parse.error.js';
|
|
14
|
+
import { displayDbmlParseError, displayError, getOutputFormat, printJson } from '../../utils/output.util.js';
|
|
14
15
|
import { validateProjectUrlName, validateVersionName, validateWorkspaceUrlName, } from '../../utils/validation.util.js';
|
|
15
16
|
const DOCUMENT_SOURCE_CLIENT_TYPE = 'dbdiagram-cli';
|
|
16
17
|
export async function documentAction(options) {
|
|
@@ -84,7 +85,13 @@ export async function documentAction(options) {
|
|
|
84
85
|
}
|
|
85
86
|
catch (error) {
|
|
86
87
|
process.exitCode = 1;
|
|
87
|
-
|
|
88
|
+
if (error instanceof DbmlParseError) {
|
|
89
|
+
const label = options.source.type === DOCUMENT_SOURCE_TYPE.FILE
|
|
90
|
+
? options.source.value
|
|
91
|
+
: 'DBML';
|
|
92
|
+
return displayDbmlParseError(error.errors, label, getOutputFormat(options.json));
|
|
93
|
+
}
|
|
94
|
+
return displayError(toCliError(error), getOutputFormat(options.json));
|
|
88
95
|
}
|
|
89
96
|
const s = spinner();
|
|
90
97
|
s.start('Building document on dbdocs...');
|
|
@@ -9,9 +9,9 @@ import { preparePushDiagramData } from '../services/dbml/dbml.service.js';
|
|
|
9
9
|
import { DBDIAGRAM_CONFIGS } from '../config.js';
|
|
10
10
|
import { CliError } from '../errors/cli.error.js';
|
|
11
11
|
import { getCliErrorMessageFromCode, toCliError } from '../utils/cli-error.util.js';
|
|
12
|
-
import { AUTH_NOT_LOGGED_IN, FILE_PATH_MISSING, FILE_READ_ERROR,
|
|
13
|
-
import { displayError, getOutputFormat, printJson } from '../utils/output.util.js';
|
|
14
|
-
import {
|
|
12
|
+
import { AUTH_NOT_LOGGED_IN, FILE_PATH_MISSING, FILE_READ_ERROR, } from '../constants/cli-error.constant.js';
|
|
13
|
+
import { displayDbmlParseError, displayError, getOutputFormat, printJson } from '../utils/output.util.js';
|
|
14
|
+
import { DbmlParseError } from '../errors/dbml-parse.error.js';
|
|
15
15
|
export async function pushAction(filepath, options, thisCommand) {
|
|
16
16
|
const credential = getCredential();
|
|
17
17
|
if (!credential) {
|
|
@@ -38,7 +38,10 @@ export async function pushAction(filepath, options, thisCommand) {
|
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
40
|
process.exitCode = 1;
|
|
41
|
-
|
|
41
|
+
if (error instanceof DbmlParseError) {
|
|
42
|
+
return displayDbmlParseError(error.errors, resolvedFilepath, getOutputFormat(options.json));
|
|
43
|
+
}
|
|
44
|
+
return displayError(toCliError(error), getOutputFormat(options.json));
|
|
42
45
|
}
|
|
43
46
|
const vizPath = deriveDiagramVizPathFromDBML(resolvedFilepath);
|
|
44
47
|
const vizResult = await readDiagramVizFile(vizPath);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { log } from '@clack/prompts';
|
|
3
|
+
import { FILE_PATH_MISSING, FILE_READ_ERROR, VALIDATE_ERROR_HINT } from '../constants/cli-error.constant.js';
|
|
4
|
+
import { CliError } from '../errors/cli.error.js';
|
|
5
|
+
import { DbmlParseError } from '../errors/dbml-parse.error.js';
|
|
6
|
+
import { parseDbml } from '../services/dbml/dbml.service.js';
|
|
7
|
+
import { getCliErrorMessageFromCode, toCliError } from '../utils/cli-error.util.js';
|
|
8
|
+
import { countDiagnosticLabel, diagnosticRenderSection } from '../utils/dbml.util.js';
|
|
9
|
+
import { displayDbmlParseError, displayError, getOutputFormat, printJson } from '../utils/output.util.js';
|
|
10
|
+
export async function validateAction(filepath, options, thisCommand) {
|
|
11
|
+
const format = getOutputFormat(options.json);
|
|
12
|
+
const resolvedFilepath = thisCommand.args[0] || filepath;
|
|
13
|
+
if (!resolvedFilepath) {
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return displayError(new CliError(FILE_PATH_MISSING, getCliErrorMessageFromCode(FILE_PATH_MISSING)), format);
|
|
16
|
+
}
|
|
17
|
+
let dbml;
|
|
18
|
+
try {
|
|
19
|
+
dbml = await readFile(resolvedFilepath, 'utf8');
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return displayError(new CliError(FILE_READ_ERROR, `Failed to read file: ${message}`), format);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const { warnings, infos } = await parseDbml(dbml);
|
|
28
|
+
if (format === 'json') {
|
|
29
|
+
printJson({ valid: true, warnings, infos });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const counts = [];
|
|
33
|
+
if (warnings.length > 0)
|
|
34
|
+
counts.push(`${warnings.length} ${countDiagnosticLabel(warnings.length, 'warning')}`);
|
|
35
|
+
if (infos.length > 0)
|
|
36
|
+
counts.push(`${infos.length} ${countDiagnosticLabel(infos.length, 'info')}`);
|
|
37
|
+
const summary = counts.length > 0 ? `${counts.join(', ')}.` : 'No issues found.';
|
|
38
|
+
log.success(`${resolvedFilepath} is valid. ${summary}`);
|
|
39
|
+
if (warnings.length > 0)
|
|
40
|
+
log.warn(diagnosticRenderSection(warnings, 'warning'));
|
|
41
|
+
if (infos.length > 0)
|
|
42
|
+
log.info(diagnosticRenderSection(infos, 'info'));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
if (error instanceof DbmlParseError) {
|
|
47
|
+
return displayDbmlParseError(error.errors, resolvedFilepath, format, VALIDATE_ERROR_HINT);
|
|
48
|
+
}
|
|
49
|
+
return displayError(toCliError(error), format);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { registerAuthCommand } from './auth/auth.command.js';
|
|
2
2
|
import { registerBuildCommand } from './build/build.command.js';
|
|
3
3
|
import { registerPushCommand } from './push.command.js';
|
|
4
|
+
import { registerValidateCommand } from './validate.command.js';
|
|
4
5
|
import { registerPullCommand } from './pull.command.js';
|
|
5
6
|
import { registerDeleteCommand } from './delete.command.js';
|
|
6
7
|
import { registerListCommand } from './list/list.command.js';
|
|
@@ -10,6 +11,7 @@ export function load(program) {
|
|
|
10
11
|
registerInitCommand(program);
|
|
11
12
|
registerAuthCommand(program);
|
|
12
13
|
registerPushCommand(program);
|
|
14
|
+
registerValidateCommand(program);
|
|
13
15
|
registerPullCommand(program);
|
|
14
16
|
registerListCommand(program);
|
|
15
17
|
registerDeleteCommand(program);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { validateAction } from '../actions/validate.action.js';
|
|
2
|
+
import { validatePreAction } from '../hooks/validate.hook.js';
|
|
3
|
+
export function registerValidateCommand(program) {
|
|
4
|
+
program
|
|
5
|
+
.command('validate')
|
|
6
|
+
.description('Validate a local DBML file and report errors, warnings, and infos')
|
|
7
|
+
.argument('[filepath]', 'Path to the DBML file to validate')
|
|
8
|
+
.option('--json', 'Output as JSON')
|
|
9
|
+
.hook('preAction', validatePreAction)
|
|
10
|
+
.action(validateAction);
|
|
11
|
+
}
|
package/dist/config.js
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { getSettings } from '../config/settings-manager.js';
|
|
2
|
+
export function resolveValidateArgsFlags(cmd, settings) {
|
|
3
|
+
if (!cmd.args[0] && settings.dbml?.entry) {
|
|
4
|
+
cmd.args[0] = settings.dbml.entry;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function validatePreAction(thisCommand) {
|
|
8
|
+
const settings = getSettings();
|
|
9
|
+
if (!settings)
|
|
10
|
+
return;
|
|
11
|
+
resolveValidateArgsFlags(thisCommand, settings);
|
|
12
|
+
}
|
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import { DbmlParseError } from '../../errors/dbml-parse.error.js';
|
|
5
|
+
import { DBML_PARSE_TIMEOUT_MS } from '../../config.js';
|
|
4
6
|
const workerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'dbml.worker.js');
|
|
5
|
-
function parseDbml(content) {
|
|
7
|
+
export function parseDbml(content) {
|
|
6
8
|
return new Promise((resolve, reject) => {
|
|
7
9
|
const worker = new Worker(workerPath, { execArgv: process.execArgv });
|
|
10
|
+
let settled = false;
|
|
11
|
+
const timeout = setTimeout(() => {
|
|
12
|
+
settled = true;
|
|
13
|
+
worker.terminate();
|
|
14
|
+
reject(new Error(`DBML parser timed out after ${DBML_PARSE_TIMEOUT_MS}ms`));
|
|
15
|
+
}, DBML_PARSE_TIMEOUT_MS);
|
|
8
16
|
worker.postMessage(content);
|
|
9
|
-
worker.on('message', (
|
|
17
|
+
worker.on('message', (message) => {
|
|
18
|
+
settled = true;
|
|
19
|
+
clearTimeout(timeout);
|
|
10
20
|
worker.terminate();
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
if (message.errors)
|
|
22
|
+
reject(new DbmlParseError(message.errors));
|
|
23
|
+
else if (message.data)
|
|
24
|
+
resolve(message.data);
|
|
25
|
+
else
|
|
26
|
+
reject(new Error('DBML parser worker sent a message with neither data nor errors'));
|
|
27
|
+
});
|
|
28
|
+
worker.on('error', (error) => {
|
|
29
|
+
settled = true;
|
|
30
|
+
clearTimeout(timeout);
|
|
31
|
+
reject(error);
|
|
32
|
+
});
|
|
33
|
+
worker.on('exit', (code) => {
|
|
34
|
+
if (!settled) {
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
reject(new Error(`DBML parser worker exited unexpectedly with code ${code}`));
|
|
19
37
|
}
|
|
20
|
-
reject(err);
|
|
21
38
|
});
|
|
22
|
-
worker.on('error', reject);
|
|
23
39
|
});
|
|
24
40
|
}
|
|
25
41
|
export function prepareBuildDocumentData(content) {
|
|
@@ -1,40 +1,61 @@
|
|
|
1
1
|
import { parentPort } from 'node:worker_threads';
|
|
2
|
-
import { Parser } from '@dbml/core';
|
|
2
|
+
import { Parser, DEFAULT_ENTRY } from '@dbml/core';
|
|
3
3
|
import removeMd from 'remove-markdown';
|
|
4
|
+
function toDiagnostic(diagnostic) {
|
|
5
|
+
const { code, diagnostic: message, nodeOrToken: { startPos, endPos } } = diagnostic;
|
|
6
|
+
return {
|
|
7
|
+
code,
|
|
8
|
+
message,
|
|
9
|
+
start: {
|
|
10
|
+
line: startPos.line + 1,
|
|
11
|
+
column: startPos.column + 1,
|
|
12
|
+
},
|
|
13
|
+
end: {
|
|
14
|
+
line: endPos.line + 1,
|
|
15
|
+
column: endPos.column + 1,
|
|
16
|
+
},
|
|
17
|
+
fixes: (diagnostic.quickFixes ?? []).map((fix) => ({
|
|
18
|
+
title: fix.title,
|
|
19
|
+
edits: fix.edits.map((edit) => ({
|
|
20
|
+
start: edit.start,
|
|
21
|
+
end: edit.end,
|
|
22
|
+
newText: edit.newText,
|
|
23
|
+
})),
|
|
24
|
+
})),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
4
27
|
parentPort?.on('message', (content) => {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
name: schema.name,
|
|
13
|
-
tables: schema.tables.map((table) => table.name),
|
|
14
|
-
}));
|
|
15
|
-
const normalizedDatabase = database.normalize();
|
|
16
|
-
const diagramViews = database.diagramViews.map((view) => ({
|
|
17
|
-
name: view.name,
|
|
18
|
-
visibleEntities: view.visibleEntities,
|
|
19
|
-
}));
|
|
20
|
-
parentPort?.postMessage({
|
|
21
|
-
data: {
|
|
22
|
-
name,
|
|
23
|
-
description,
|
|
24
|
-
shallowSchema,
|
|
25
|
-
normalizedDatabase,
|
|
26
|
-
database,
|
|
27
|
-
diagramViews,
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
catch (error) {
|
|
32
|
-
const { diags } = error;
|
|
33
|
-
parentPort?.postMessage({
|
|
34
|
-
error: {
|
|
35
|
-
message: error instanceof Error ? error.message : String(error),
|
|
36
|
-
...(diags !== undefined ? { diags } : {}),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
28
|
+
const parser = new Parser();
|
|
29
|
+
parser.setDbmlSource(DEFAULT_ENTRY, content);
|
|
30
|
+
const report = parser.DBMLCompiler.interpretFile(DEFAULT_ENTRY);
|
|
31
|
+
const errors = report.getErrors();
|
|
32
|
+
if (errors.length > 0) {
|
|
33
|
+
parentPort?.postMessage({ errors: errors.map(toDiagnostic) });
|
|
34
|
+
return;
|
|
39
35
|
}
|
|
36
|
+
const database = Parser.parseJSONToDatabase(report.getValue() || {});
|
|
37
|
+
const { name: dbName = '', note: dbDescription = '' } = database;
|
|
38
|
+
const name = dbName.trim();
|
|
39
|
+
const description = (dbDescription ?? '').trim() !== '' ? removeMd(dbDescription).trim() : '';
|
|
40
|
+
const shallowSchema = database.schemas.map((schema) => ({
|
|
41
|
+
name: schema.name,
|
|
42
|
+
tables: schema.tables.map((table) => table.name),
|
|
43
|
+
}));
|
|
44
|
+
const normalizedDatabase = database.normalize();
|
|
45
|
+
const diagramViews = database.diagramViews.map((view) => ({
|
|
46
|
+
name: view.name,
|
|
47
|
+
visibleEntities: view.visibleEntities,
|
|
48
|
+
}));
|
|
49
|
+
parentPort?.postMessage({
|
|
50
|
+
data: {
|
|
51
|
+
name,
|
|
52
|
+
description,
|
|
53
|
+
shallowSchema,
|
|
54
|
+
normalizedDatabase,
|
|
55
|
+
database,
|
|
56
|
+
diagramViews,
|
|
57
|
+
warnings: report.getWarnings().map(toDiagnostic),
|
|
58
|
+
infos: report.getInfos().map(toDiagnostic),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
40
61
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils/dbml.util.js
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import pluralize from 'pluralize';
|
|
4
5
|
export function getDbmlVersion() {
|
|
5
6
|
const require = createRequire(import.meta.url);
|
|
6
7
|
const entry = require.resolve('@dbml/core');
|
|
7
8
|
const pkg = JSON.parse(readFileSync(path.resolve(path.dirname(entry), '..', 'package.json'), 'utf8'));
|
|
8
9
|
return pkg.version;
|
|
9
10
|
}
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
return
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return `\n - ${diags.join('\n - ')}`;
|
|
27
|
-
if (error instanceof Error)
|
|
28
|
-
return error.message;
|
|
29
|
-
return String(error);
|
|
30
|
-
}
|
|
11
|
+
export const countDiagnosticLabel = (count, noun) => {
|
|
12
|
+
return pluralize(noun, count);
|
|
13
|
+
};
|
|
14
|
+
export const diagnosticRenderLine = (diagnostic) => {
|
|
15
|
+
const location = `(line ${diagnostic.start.line}, column ${diagnostic.start.column})`;
|
|
16
|
+
return [
|
|
17
|
+
` - ${diagnostic.message} ${location} [${diagnostic.code}]`,
|
|
18
|
+
...diagnostic.fixes.map((fix) => ` fix: ${fix.title}`),
|
|
19
|
+
];
|
|
20
|
+
};
|
|
21
|
+
export const diagnosticRenderSection = (diagnostics, noun) => {
|
|
22
|
+
return [
|
|
23
|
+
`${countDiagnosticLabel(diagnostics.length, noun)}:`,
|
|
24
|
+
...diagnostics.flatMap(diagnosticRenderLine),
|
|
25
|
+
].join('\n');
|
|
26
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Table from 'cli-table3';
|
|
2
2
|
import { log } from '@clack/prompts';
|
|
3
|
+
import { DBML_PARSE_ERROR } from '../constants/cli-error.constant.js';
|
|
3
4
|
import { logConsole } from './logger.util.js';
|
|
5
|
+
import { countDiagnosticLabel, diagnosticRenderLine } from './dbml.util.js';
|
|
4
6
|
export const getOutputFormat = (json) => (json ? 'json' : 'plain');
|
|
5
7
|
export function printJson(data) {
|
|
6
8
|
logConsole(JSON.stringify(data, null, 2));
|
|
@@ -47,3 +49,14 @@ export function printTable(data, config) {
|
|
|
47
49
|
}
|
|
48
50
|
logConsole(table.toString());
|
|
49
51
|
}
|
|
52
|
+
export const displayDbmlParseError = (errors, label, format, hint) => {
|
|
53
|
+
const message = `Failed to parse ${label}. ${errors.length} ${countDiagnosticLabel(errors.length, 'error')}.`;
|
|
54
|
+
if (format === 'json') {
|
|
55
|
+
printJson({ errorCode: DBML_PARSE_ERROR, message, errors });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const lines = [message, ...errors.flatMap(diagnosticRenderLine)];
|
|
59
|
+
if (hint)
|
|
60
|
+
lines.push('', hint);
|
|
61
|
+
log.error(lines.join('\n'));
|
|
62
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dbdiagram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"dbdiagram": "dist/index.js"
|
|
@@ -39,12 +39,14 @@
|
|
|
39
39
|
"dotenv": "^16.4.0",
|
|
40
40
|
"luxon": "^3.7.2",
|
|
41
41
|
"open": "^11.0.0",
|
|
42
|
+
"pluralize": "^8.0.0",
|
|
42
43
|
"remove-markdown": "^0.6.4"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@eslint/js": "^10.0.1",
|
|
46
47
|
"@types/luxon": "^3.7.1",
|
|
47
48
|
"@types/node": "^25.7.0",
|
|
49
|
+
"@types/pluralize": "^0.0.33",
|
|
48
50
|
"@vitest/coverage-v8": "^4.1.0",
|
|
49
51
|
"danger": "^13.0.10",
|
|
50
52
|
"danger-plugin-istanbul-coverage": "^1.6.2",
|