viewlint 0.0.0 → 0.0.1
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/README.md +3 -0
- package/dist/bin/viewlint.d.ts +3 -0
- package/dist/bin/viewlint.d.ts.map +1 -0
- package/dist/bin/viewlint.js +11 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +150 -0
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +1 -0
- package/dist/config/views.d.ts +10 -0
- package/dist/config/views.d.ts.map +1 -0
- package/dist/config/views.js +43 -0
- package/dist/plugin/index.d.ts +8 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -0
- package/dist/src/binEntry.d.ts +22 -0
- package/dist/src/binEntry.d.ts.map +1 -0
- package/dist/src/binEntry.js +45 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +292 -0
- package/dist/src/configFile.d.ts +3 -0
- package/dist/src/configFile.d.ts.map +1 -0
- package/dist/src/configFile.js +27 -0
- package/dist/src/debug.d.ts +3 -0
- package/dist/src/debug.d.ts.map +1 -0
- package/dist/src/debug.js +2 -0
- package/dist/src/engine/collectIgnoredSelectors.d.ts +3 -0
- package/dist/src/engine/collectIgnoredSelectors.d.ts.map +1 -0
- package/dist/src/engine/collectIgnoredSelectors.js +42 -0
- package/dist/src/engine/evaluator.d.ts +26 -0
- package/dist/src/engine/evaluator.d.ts.map +1 -0
- package/dist/src/engine/evaluator.js +150 -0
- package/dist/src/engine/finder.d.ts +9 -0
- package/dist/src/engine/finder.d.ts.map +1 -0
- package/dist/src/engine/finder.js +33 -0
- package/dist/src/engine/scope.d.ts +15 -0
- package/dist/src/engine/scope.d.ts.map +1 -0
- package/dist/src/engine/scope.js +160 -0
- package/dist/src/engine.d.ts +25 -0
- package/dist/src/engine.d.ts.map +1 -0
- package/dist/src/engine.js +234 -0
- package/dist/src/formatter.d.ts +3 -0
- package/dist/src/formatter.d.ts.map +1 -0
- package/dist/src/formatter.js +15 -0
- package/dist/src/formatters/stylish.d.ts +3 -0
- package/dist/src/formatters/stylish.d.ts.map +1 -0
- package/dist/src/formatters/stylish.js +220 -0
- package/dist/src/helpers.d.ts +12 -0
- package/dist/src/helpers.d.ts.map +1 -0
- package/dist/src/helpers.js +71 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +55 -0
- package/dist/src/loadConfigFile.d.ts +3 -0
- package/dist/src/loadConfigFile.d.ts.map +1 -0
- package/dist/src/loadConfigFile.js +44 -0
- package/dist/src/resolveOptions.d.ts +15 -0
- package/dist/src/resolveOptions.d.ts.map +1 -0
- package/dist/src/resolveOptions.js +176 -0
- package/dist/src/setupOpts.d.ts +7 -0
- package/dist/src/setupOpts.d.ts.map +1 -0
- package/dist/src/setupOpts.js +19 -0
- package/dist/src/types.d.ts +185 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/src/vendor/medv.finder.js +336 -0
- package/package.json +50 -3
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewlint.d.ts","sourceRoot":"","sources":["../../bin/viewlint.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { sync } from "cross-spawn";
|
|
3
|
+
import debug from "debug";
|
|
4
|
+
import { createDefaultBinDeps, runBin } from "../src/binEntry.js";
|
|
5
|
+
import { runCli } from "../src/cli.js";
|
|
6
|
+
const argv = process.argv;
|
|
7
|
+
process.exitCode = await runBin(argv, createDefaultBinDeps({
|
|
8
|
+
spawnSync: sync,
|
|
9
|
+
runCli,
|
|
10
|
+
enableDebug: debug.enable,
|
|
11
|
+
}));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ConfigObject } from "../src/types.js";
|
|
2
|
+
import type { ConfigWithExtendsArray } from "./types.js";
|
|
3
|
+
export declare function defineConfig(...configs: ConfigWithExtendsArray): ConfigObject[];
|
|
4
|
+
export { findNearestViewlintConfigFile } from "../src/configFile.js";
|
|
5
|
+
export { defaultView, defineViewFromActions } from "./views.js";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../config/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAU,MAAM,iBAAiB,CAAA;AAE3D,OAAO,KAAK,EAEX,sBAAsB,EAGtB,MAAM,YAAY,CAAA;AAmMnB,wBAAgB,YAAY,CAC3B,GAAG,OAAO,EAAE,sBAAsB,GAChC,YAAY,EAAE,CAiBhB;AAED,OAAO,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAA;AACpE,OAAO,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
function resolveExtendsString(reference, plugins) {
|
|
2
|
+
const normalized = reference.trim();
|
|
3
|
+
const lastSlashIndex = normalized.lastIndexOf("/");
|
|
4
|
+
let pluginNamespace = "";
|
|
5
|
+
let configName = "";
|
|
6
|
+
if (lastSlashIndex === -1) {
|
|
7
|
+
const candidates = [...plugins.keys()].filter((knownNamespace) => {
|
|
8
|
+
return knownNamespace.endsWith(`/${reference}`);
|
|
9
|
+
});
|
|
10
|
+
if (candidates.length === 0) {
|
|
11
|
+
throw new Error(`Invalid extends reference '${reference}'. Expected '<configName>' or '<pluginNamespace>/<configName>'.`);
|
|
12
|
+
}
|
|
13
|
+
else if (candidates.length === 1) {
|
|
14
|
+
const candidate = candidates[0];
|
|
15
|
+
if (!candidate) {
|
|
16
|
+
throw new Error(`Invalid extends reference '${reference}'. Expected '<configName>' or '<pluginNamespace>/<configName>'.`);
|
|
17
|
+
}
|
|
18
|
+
pluginNamespace = candidate;
|
|
19
|
+
configName = reference;
|
|
20
|
+
}
|
|
21
|
+
else if (candidates.length > 1) {
|
|
22
|
+
candidates.sort();
|
|
23
|
+
throw new Error(`Ambiguous plugin '${reference}' in extends '${reference}'. Specify with a namespace. Matches: ${candidates
|
|
24
|
+
.map((candidate) => `'${candidate}'`)
|
|
25
|
+
.join(", ")}.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const pluginRef = normalized.slice(0, lastSlashIndex).trim();
|
|
30
|
+
configName = normalized.slice(lastSlashIndex + 1).trim();
|
|
31
|
+
if (pluginRef.length === 0 || configName.length === 0) {
|
|
32
|
+
throw new Error(`Invalid extends reference '${reference}'. Expected '<pluginNamespace>/<configName>'.`);
|
|
33
|
+
}
|
|
34
|
+
if (plugins.has(pluginRef)) {
|
|
35
|
+
pluginNamespace = pluginRef;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const knownPlugins = [...plugins.keys()].sort();
|
|
39
|
+
const knownPluginsMessage = knownPlugins.length === 0
|
|
40
|
+
? "No plugins are registered."
|
|
41
|
+
: `Known plugins: ${knownPlugins
|
|
42
|
+
.map((name) => `'${name}'`)
|
|
43
|
+
.join(", ")}.`;
|
|
44
|
+
throw new Error(`Unknown plugin referenced by extends '${reference}'. Ensure it is registered in plugins. ${knownPluginsMessage}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const plugin = plugins.get(pluginNamespace);
|
|
48
|
+
if (!plugin) {
|
|
49
|
+
throw new Error(`Unknown plugin '${pluginNamespace}' referenced by extends '${reference}'. Ensure it is registered in plugins.`);
|
|
50
|
+
}
|
|
51
|
+
if (!plugin.configs)
|
|
52
|
+
throw new Error(`No configuration found in plugin '${pluginNamespace}'.`);
|
|
53
|
+
const config = plugin.configs[configName];
|
|
54
|
+
if (!config) {
|
|
55
|
+
const availableConfigs = Object.keys(plugin.configs).sort();
|
|
56
|
+
const availableConfigsMessage = availableConfigs.length === 0
|
|
57
|
+
? "No configs are available in this plugin."
|
|
58
|
+
: `Available configs: ${availableConfigs
|
|
59
|
+
.map((name) => `'${name}'`)
|
|
60
|
+
.join(", ")}.`;
|
|
61
|
+
throw new Error(`Unknown config '${configName}' in plugin '${pluginNamespace}' (extends '${reference}'). ${availableConfigsMessage}`);
|
|
62
|
+
}
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
function applyExtendsEntry(entry, state, context) {
|
|
66
|
+
if (typeof entry === "string") {
|
|
67
|
+
const normalized = entry.trim();
|
|
68
|
+
const cycleIndex = context.stringStack.indexOf(normalized);
|
|
69
|
+
if (cycleIndex !== -1) {
|
|
70
|
+
const chain = [...context.stringStack.slice(cycleIndex), normalized].join(" -> ");
|
|
71
|
+
throw new Error(`Circular extends detected: ${chain}`);
|
|
72
|
+
}
|
|
73
|
+
context.stringStack.push(normalized);
|
|
74
|
+
try {
|
|
75
|
+
const resolved = resolveExtendsString(normalized, state.plugins);
|
|
76
|
+
applyConfig(resolved, state, context);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
context.stringStack.pop();
|
|
80
|
+
}
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
applyConfig(entry, state, context);
|
|
84
|
+
}
|
|
85
|
+
function applyConfig(config, state, context) {
|
|
86
|
+
if (Array.isArray(config)) {
|
|
87
|
+
if (context.activeArrays.has(config)) {
|
|
88
|
+
throw new Error("Circular extends detected: a config array was recursively extended.");
|
|
89
|
+
}
|
|
90
|
+
context.activeArrays.add(config);
|
|
91
|
+
try {
|
|
92
|
+
for (const item of config) {
|
|
93
|
+
applyConfig(item, state, context);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
context.activeArrays.delete(config);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (context.activeItems.has(config)) {
|
|
102
|
+
throw new Error("Circular extends detected: a config item was recursively extended.");
|
|
103
|
+
}
|
|
104
|
+
context.activeItems.add(config);
|
|
105
|
+
try {
|
|
106
|
+
if (config.plugins) {
|
|
107
|
+
for (const [pluginNamespace, plugin] of Object.entries(config.plugins)) {
|
|
108
|
+
state.plugins.set(pluginNamespace, plugin);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (config.extends) {
|
|
112
|
+
for (const extendsEntry of config.extends) {
|
|
113
|
+
applyExtendsEntry(extendsEntry, state, context);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const { extends: _extends, ...rest } = config;
|
|
117
|
+
const hasEntries = (value) => {
|
|
118
|
+
if (!value)
|
|
119
|
+
return false;
|
|
120
|
+
return Object.keys(value).length > 0;
|
|
121
|
+
};
|
|
122
|
+
if (hasEntries(rest.plugins) ||
|
|
123
|
+
hasEntries(rest.rules) ||
|
|
124
|
+
hasEntries(rest.options) ||
|
|
125
|
+
hasEntries(rest.views) ||
|
|
126
|
+
hasEntries(rest.scopes)) {
|
|
127
|
+
state.output.push(rest);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
context.activeItems.delete(config);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function defineConfig(...configs) {
|
|
135
|
+
const state = {
|
|
136
|
+
plugins: new Map(),
|
|
137
|
+
output: [],
|
|
138
|
+
};
|
|
139
|
+
const context = {
|
|
140
|
+
activeArrays: new WeakSet(),
|
|
141
|
+
activeItems: new WeakSet(),
|
|
142
|
+
stringStack: [],
|
|
143
|
+
};
|
|
144
|
+
for (const config of configs) {
|
|
145
|
+
applyConfig(config, state, context);
|
|
146
|
+
}
|
|
147
|
+
return state.output;
|
|
148
|
+
}
|
|
149
|
+
export { findNearestViewlintConfigFile } from "../src/configFile.js";
|
|
150
|
+
export { defaultView, defineViewFromActions } from "./views.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ConfigObject } from "../src/types.js";
|
|
2
|
+
export type InfiniteArray<T> = T | Array<InfiniteArray<T>>;
|
|
3
|
+
export type ExtendsElement = string | InfiniteArray<ConfigWithExtends>;
|
|
4
|
+
export type ConfigWithExtends = ConfigObject & {
|
|
5
|
+
extends?: readonly ExtendsElement[];
|
|
6
|
+
};
|
|
7
|
+
export type ConfigWithExtendsArray = InfiniteArray<ConfigWithExtends>[];
|
|
8
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../config/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEnD,MAAM,MAAM,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;AAE1D,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;AAEtE,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG;IAC9C,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,sBAAsB,GAAG,aAAa,CAAC,iBAAiB,CAAC,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Page } from "playwright";
|
|
2
|
+
import type { View } from "../src/types.js";
|
|
3
|
+
export type DefineViewAction = (args: {
|
|
4
|
+
page: Page;
|
|
5
|
+
}) => Promise<void> | void;
|
|
6
|
+
export declare const defineViewFromActions: (maybeActionArr: DefineViewAction | DefineViewAction[], opts?: {
|
|
7
|
+
name?: string;
|
|
8
|
+
}) => View;
|
|
9
|
+
export declare const defaultView: View;
|
|
10
|
+
//# sourceMappingURL=views.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"views.d.ts","sourceRoot":"","sources":["../../config/views.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAItC,OAAO,KAAK,EAAa,IAAI,EAAgB,MAAM,iBAAiB,CAAA;AAEpE,MAAM,MAAM,gBAAgB,GAAG,CAAC,IAAI,EAAE;IAAE,IAAI,EAAE,IAAI,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;AAE7E,eAAO,MAAM,qBAAqB,GACjC,gBAAgB,gBAAgB,GAAG,gBAAgB,EAAE,EACrD,OAAO;IACN,IAAI,CAAC,EAAE,MAAM,CAAA;CACb,KACC,IAgDF,CAAA;AAED,eAAO,MAAM,WAAW,EAAE,IAAqD,CAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { chromium } from "playwright";
|
|
2
|
+
import { toArray } from "../src/helpers.js";
|
|
3
|
+
export const defineViewFromActions = (maybeActionArr, opts) => {
|
|
4
|
+
return {
|
|
5
|
+
meta: {
|
|
6
|
+
name: opts?.name,
|
|
7
|
+
},
|
|
8
|
+
setup: async (opts) => {
|
|
9
|
+
const actions = toArray(maybeActionArr);
|
|
10
|
+
const baseURL = opts?.context?.baseURL;
|
|
11
|
+
if (!baseURL) {
|
|
12
|
+
throw new Error("Views created with defineView require options.context.baseURL to be set.");
|
|
13
|
+
}
|
|
14
|
+
const browser = await chromium.launch();
|
|
15
|
+
const context = await browser.newContext(opts?.context);
|
|
16
|
+
const page = await context.newPage();
|
|
17
|
+
const waitForSettled = async () => {
|
|
18
|
+
await page.waitForLoadState("load");
|
|
19
|
+
};
|
|
20
|
+
const runActions = async () => {
|
|
21
|
+
for (const action of actions) {
|
|
22
|
+
await action({ page });
|
|
23
|
+
await waitForSettled();
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const reset = async () => {
|
|
27
|
+
await page.goto(baseURL);
|
|
28
|
+
await waitForSettled();
|
|
29
|
+
await runActions();
|
|
30
|
+
};
|
|
31
|
+
await reset();
|
|
32
|
+
return {
|
|
33
|
+
page,
|
|
34
|
+
reset,
|
|
35
|
+
close: async () => {
|
|
36
|
+
await context.close();
|
|
37
|
+
await browser.close();
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export const defaultView = defineViewFromActions([], { name: "default" });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RuleDefinition, RuleMeta, RuleSchema } from "../src/types.js";
|
|
2
|
+
export declare function defineRule<Schema extends RuleSchema>(rule: RuleDefinition<Schema> & {
|
|
3
|
+
meta: RuleMeta<Schema> & {
|
|
4
|
+
schema: Schema;
|
|
5
|
+
};
|
|
6
|
+
}): RuleDefinition<Schema>;
|
|
7
|
+
export declare function defineRule(rule: RuleDefinition<undefined>): RuleDefinition<undefined>;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../plugin/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAE3E,wBAAgB,UAAU,CAAC,MAAM,SAAS,UAAU,EACnD,IAAI,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG;IAC9B,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;CAC3C,GACC,cAAc,CAAC,MAAM,CAAC,CAAA;AAEzB,wBAAgB,UAAU,CACzB,IAAI,EAAE,cAAc,CAAC,SAAS,CAAC,GAC7B,cAAc,CAAC,SAAS,CAAC,CAAA"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type SpawnSyncOptions = {
|
|
2
|
+
encoding: "utf8";
|
|
3
|
+
stdio: "inherit";
|
|
4
|
+
};
|
|
5
|
+
type SpawnSync = (command: string, args: string[], options: SpawnSyncOptions) => unknown;
|
|
6
|
+
type RunCli = (argv: string[]) => Promise<number>;
|
|
7
|
+
type EnableDebug = (namespaces: string) => void;
|
|
8
|
+
type BinDeps = {
|
|
9
|
+
spawnSync: SpawnSync;
|
|
10
|
+
runCli: RunCli;
|
|
11
|
+
enableDebug: EnableDebug;
|
|
12
|
+
writeStdout: (text: string) => void;
|
|
13
|
+
writeStderr: (text: string) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare function runBin(argv: string[], deps: BinDeps): Promise<number>;
|
|
16
|
+
export declare function createDefaultBinDeps(opts: {
|
|
17
|
+
spawnSync: SpawnSync;
|
|
18
|
+
runCli: RunCli;
|
|
19
|
+
enableDebug: EnableDebug;
|
|
20
|
+
}): BinDeps;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=binEntry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binEntry.d.ts","sourceRoot":"","sources":["../../src/binEntry.ts"],"names":[],"mappings":"AAAA,KAAK,gBAAgB,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,SAAS,CAAA;CAChB,CAAA;AAMD,KAAK,SAAS,GAAG,CAChB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,gBAAgB,KACrB,OAAO,CAAA;AAEZ,KAAK,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;AAEjD,KAAK,WAAW,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;AAE/C,KAAK,OAAO,GAAG;IACd,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,WAAW,CAAA;IACxB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC,CAAA;AAaD,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAgC3E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAC1C,SAAS,EAAE,SAAS,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,WAAW,CAAA;CACxB,GAAG,OAAO,CAQV"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
function isSpawnSyncResult(value) {
|
|
2
|
+
return typeof value === "object" && value !== null;
|
|
3
|
+
}
|
|
4
|
+
function coerceExitCode(result) {
|
|
5
|
+
if (!isSpawnSyncResult(result))
|
|
6
|
+
return 1;
|
|
7
|
+
if (typeof result.status !== "number")
|
|
8
|
+
return 1;
|
|
9
|
+
if (!Number.isFinite(result.status))
|
|
10
|
+
return 1;
|
|
11
|
+
return result.status;
|
|
12
|
+
}
|
|
13
|
+
export async function runBin(argv, deps) {
|
|
14
|
+
// Keep this entrypoint extremely lightweight, similar to ESLint.
|
|
15
|
+
// We intentionally scan argv for early flags instead of doing full parse.
|
|
16
|
+
if (argv.includes("--init")) {
|
|
17
|
+
deps.writeStderr("You can also run this command directly using 'npx @viewlint/create-config@latest'.\n");
|
|
18
|
+
const result = deps.spawnSync("npx", ["@viewlint/create-config@latest"], {
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
});
|
|
22
|
+
return coerceExitCode(result);
|
|
23
|
+
}
|
|
24
|
+
if (argv.includes("--mcp")) {
|
|
25
|
+
deps.writeStderr("You can also run this command directly using 'npx @viewlint/mcp@latest'.\n");
|
|
26
|
+
const result = deps.spawnSync("npx", ["@viewlint/mcp@latest"], {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
stdio: "inherit",
|
|
29
|
+
});
|
|
30
|
+
return coerceExitCode(result);
|
|
31
|
+
}
|
|
32
|
+
if (argv.includes("--verbose")) {
|
|
33
|
+
deps.enableDebug("viewlint*");
|
|
34
|
+
}
|
|
35
|
+
return deps.runCli(argv);
|
|
36
|
+
}
|
|
37
|
+
export function createDefaultBinDeps(opts) {
|
|
38
|
+
return {
|
|
39
|
+
spawnSync: opts.spawnSync,
|
|
40
|
+
runCli: opts.runCli,
|
|
41
|
+
enableDebug: opts.enableDebug,
|
|
42
|
+
writeStdout: (text) => process.stdout.write(text),
|
|
43
|
+
writeStderr: (text) => process.stderr.write(text),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":"AAwUA,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2E5D"}
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
4
|
+
import { defaultView } from "../config/views.js";
|
|
5
|
+
import { isRecord, toArray } from "./helpers.js";
|
|
6
|
+
import { ViewLint } from "./index.js";
|
|
7
|
+
function parseIntStrict(raw) {
|
|
8
|
+
const parsed = Number(raw);
|
|
9
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
|
|
10
|
+
throw new InvalidArgumentError(`Expected an integer but got '${raw}'.`);
|
|
11
|
+
}
|
|
12
|
+
return parsed;
|
|
13
|
+
}
|
|
14
|
+
function countSeverities(messages) {
|
|
15
|
+
let errorCount = 0;
|
|
16
|
+
let warningCount = 0;
|
|
17
|
+
let infoCount = 0;
|
|
18
|
+
for (const message of messages) {
|
|
19
|
+
if (message.severity === "error")
|
|
20
|
+
errorCount += 1;
|
|
21
|
+
if (message.severity === "warn")
|
|
22
|
+
warningCount += 1;
|
|
23
|
+
if (message.severity === "info")
|
|
24
|
+
infoCount += 1;
|
|
25
|
+
}
|
|
26
|
+
return { errorCount, warningCount, infoCount };
|
|
27
|
+
}
|
|
28
|
+
function sumCounts(results) {
|
|
29
|
+
let errorCount = 0;
|
|
30
|
+
let warningCount = 0;
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
errorCount += result.errorCount;
|
|
33
|
+
warningCount += result.warningCount;
|
|
34
|
+
}
|
|
35
|
+
return { errorCount, warningCount };
|
|
36
|
+
}
|
|
37
|
+
function computeExitCode(counts, maxWarnings) {
|
|
38
|
+
const tooManyWarnings = maxWarnings >= 0 && counts.warningCount > maxWarnings;
|
|
39
|
+
return counts.errorCount > 0 || tooManyWarnings ? 1 : 0;
|
|
40
|
+
}
|
|
41
|
+
async function isDirectory(filePath) {
|
|
42
|
+
try {
|
|
43
|
+
return (await fs.stat(filePath)).isDirectory();
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (isRecord(error) &&
|
|
47
|
+
"code" in error &&
|
|
48
|
+
typeof error.code === "string" &&
|
|
49
|
+
(error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function writeOrStdout(output, outputFile) {
|
|
56
|
+
if (!outputFile) {
|
|
57
|
+
process.stdout.write(output);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const filePath = path.resolve(process.cwd(), outputFile);
|
|
61
|
+
if (await isDirectory(filePath)) {
|
|
62
|
+
throw new Error(`Cannot write to output file path, it is a directory: ${outputFile}`);
|
|
63
|
+
}
|
|
64
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
65
|
+
await fs.writeFile(filePath, output, "utf8");
|
|
66
|
+
}
|
|
67
|
+
async function readPackageVersion() {
|
|
68
|
+
const candidatePaths = [
|
|
69
|
+
new URL("../package.json", import.meta.url),
|
|
70
|
+
new URL("../../package.json", import.meta.url),
|
|
71
|
+
];
|
|
72
|
+
for (const url of candidatePaths) {
|
|
73
|
+
try {
|
|
74
|
+
const raw = await fs.readFile(url, "utf8");
|
|
75
|
+
const parsed = JSON.parse(raw);
|
|
76
|
+
if (isRecord(parsed) && typeof parsed.version === "string") {
|
|
77
|
+
return parsed.version;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Try next candidate.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
throw new Error("packages/viewlint/package.json must include a string 'version' field for --version.");
|
|
85
|
+
}
|
|
86
|
+
function filterResultsForQuietMode(results) {
|
|
87
|
+
return results.map((result) => {
|
|
88
|
+
const messages = result.messages.filter((m) => m.severity === "error");
|
|
89
|
+
const counts = countSeverities(messages);
|
|
90
|
+
return {
|
|
91
|
+
...result,
|
|
92
|
+
messages,
|
|
93
|
+
suppressedMessages: [],
|
|
94
|
+
errorCount: counts.errorCount,
|
|
95
|
+
warningCount: 0,
|
|
96
|
+
infoCount: 0,
|
|
97
|
+
recommendCount: 0,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async function execute(options, urls) {
|
|
102
|
+
const viewlint = new ViewLint({
|
|
103
|
+
overrideConfigFile: options.config,
|
|
104
|
+
});
|
|
105
|
+
let targets;
|
|
106
|
+
try {
|
|
107
|
+
const resolved = await viewlint.getResolvedOptions();
|
|
108
|
+
const optionNames = options.option ?? [];
|
|
109
|
+
const scopeNames = options.scope ?? [];
|
|
110
|
+
const optionLayersFromRegistry = optionNames.flatMap((name) => {
|
|
111
|
+
const entry = resolved.optionsRegistry.get(name);
|
|
112
|
+
if (!entry) {
|
|
113
|
+
const known = [...resolved.optionsRegistry.keys()].sort();
|
|
114
|
+
const knownMessage = known.length === 0
|
|
115
|
+
? "No named options are defined in config."
|
|
116
|
+
: `Known options: ${known.map((x) => `'${x}'`).join(", ")}.`;
|
|
117
|
+
throw new Error(`Unknown option '${name}'. ${knownMessage}`);
|
|
118
|
+
}
|
|
119
|
+
const layers = toArray(entry);
|
|
120
|
+
return layers.map((layer) => {
|
|
121
|
+
if (layer.meta?.name)
|
|
122
|
+
return layer;
|
|
123
|
+
// Attempt to give the option a name from the key for better reporting.
|
|
124
|
+
return {
|
|
125
|
+
...layer,
|
|
126
|
+
meta: { ...(layer.meta ?? {}), name },
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
const scopesFromRegistry = scopeNames.flatMap((name) => {
|
|
131
|
+
const entry = resolved.scopeRegistry.get(name);
|
|
132
|
+
if (!entry) {
|
|
133
|
+
const known = [...resolved.scopeRegistry.keys()].sort();
|
|
134
|
+
const knownMessage = known.length === 0
|
|
135
|
+
? "No named scopes are defined in config."
|
|
136
|
+
: `Known scopes: ${known.map((x) => `'${x}'`).join(", ")}.`;
|
|
137
|
+
throw new Error(`Unknown scope '${name}'. ${knownMessage}`);
|
|
138
|
+
}
|
|
139
|
+
const scopes = toArray(entry);
|
|
140
|
+
return scopes.map((scope) => {
|
|
141
|
+
if (scope.meta?.name)
|
|
142
|
+
return scope;
|
|
143
|
+
// Attempt to give the scope a name from the key for better reporting.
|
|
144
|
+
return {
|
|
145
|
+
...scope,
|
|
146
|
+
meta: { ...(scope.meta ?? {}), name },
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
const selectorScopes = (options.selector ?? []).map((selector) => {
|
|
151
|
+
return {
|
|
152
|
+
meta: { name: selector },
|
|
153
|
+
getLocator: ({ page }) => page.locator(selector),
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
const resolvedScopes = [...scopesFromRegistry, ...selectorScopes];
|
|
157
|
+
const resolveView = () => {
|
|
158
|
+
if (options.view) {
|
|
159
|
+
const view = resolved.viewRegistry.get(options.view);
|
|
160
|
+
if (!view) {
|
|
161
|
+
const known = [...resolved.viewRegistry.keys()].sort();
|
|
162
|
+
const knownMessage = known.length === 0
|
|
163
|
+
? "No named views are defined in config."
|
|
164
|
+
: `Known views: ${known.map((x) => `'${x}'`).join(", ")}.`;
|
|
165
|
+
throw new Error(`Unknown view '${options.view}'. ${knownMessage}`);
|
|
166
|
+
}
|
|
167
|
+
if (view.meta?.name)
|
|
168
|
+
return view;
|
|
169
|
+
// Attempt to give the view a name from the key for better reporting.
|
|
170
|
+
return { ...view, meta: { ...(view.meta ?? {}), name: options.view } };
|
|
171
|
+
}
|
|
172
|
+
return defaultView;
|
|
173
|
+
};
|
|
174
|
+
const compileTarget = (url) => {
|
|
175
|
+
const urlLayer = url
|
|
176
|
+
? [
|
|
177
|
+
{
|
|
178
|
+
context: { baseURL: url },
|
|
179
|
+
},
|
|
180
|
+
]
|
|
181
|
+
: [];
|
|
182
|
+
return {
|
|
183
|
+
view: resolveView(),
|
|
184
|
+
options: urlLayer.length === 0 && optionLayersFromRegistry.length === 0
|
|
185
|
+
? undefined
|
|
186
|
+
: [...urlLayer, ...optionLayersFromRegistry],
|
|
187
|
+
scope: resolvedScopes.length === 0 ? undefined : resolvedScopes,
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
targets =
|
|
191
|
+
urls.length > 0
|
|
192
|
+
? urls.map((url) => compileTarget(url))
|
|
193
|
+
: [compileTarget(undefined)];
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
process.stderr.write(`${message}\n`);
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
if (targets.length === 0) {
|
|
201
|
+
process.stderr.write("No lint targets resolved. Provide a URL, or provide --option entries that set options.context.baseURL to use the default View.\n");
|
|
202
|
+
return 2;
|
|
203
|
+
}
|
|
204
|
+
let results;
|
|
205
|
+
try {
|
|
206
|
+
results = await viewlint.lintTargets(targets);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
210
|
+
process.stderr.write(`${message}\n`);
|
|
211
|
+
return 2;
|
|
212
|
+
}
|
|
213
|
+
const resultsForPrinting = options.quiet
|
|
214
|
+
? filterResultsForQuietMode(results)
|
|
215
|
+
: results;
|
|
216
|
+
let formatter;
|
|
217
|
+
try {
|
|
218
|
+
formatter = await viewlint.loadFormatter(options.format);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
222
|
+
process.stderr.write(`${message}\n`);
|
|
223
|
+
return 2;
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
const output = await formatter.format(resultsForPrinting);
|
|
227
|
+
await writeOrStdout(output, options.outputFile);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
231
|
+
process.stderr.write(`${message}\n`);
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
const counts = sumCounts(results);
|
|
235
|
+
return computeExitCode(counts, options.maxWarnings);
|
|
236
|
+
}
|
|
237
|
+
function isCommanderError(value) {
|
|
238
|
+
return isRecord(value) && typeof value.exitCode === "number";
|
|
239
|
+
}
|
|
240
|
+
export async function runCli(argv) {
|
|
241
|
+
const version = await readPackageVersion();
|
|
242
|
+
let exitCode = 0;
|
|
243
|
+
const program = new Command();
|
|
244
|
+
program
|
|
245
|
+
.name("viewlint")
|
|
246
|
+
.description("Lint accessibility and UI issues on web pages")
|
|
247
|
+
.usage("[options] <url> [url]")
|
|
248
|
+
.argument("[url...]", "One or more URLs to lint")
|
|
249
|
+
.showHelpAfterError()
|
|
250
|
+
.allowUnknownOption(false);
|
|
251
|
+
// Help group headings are part of the Commander-native help output.
|
|
252
|
+
program
|
|
253
|
+
.optionsGroup("Basic configuration:")
|
|
254
|
+
.option("-c, --config <path>", "Use this configuration instead of viewlint.config.ts, viewlint.config.mjs, or viewlint.config.js");
|
|
255
|
+
program
|
|
256
|
+
.optionsGroup("Targets:")
|
|
257
|
+
.option("--view <name>", "Use a named view from config")
|
|
258
|
+
.option("--option <name...>", "Apply named option layers from config (in order)")
|
|
259
|
+
.option("--scope <name...>", "Apply named scopes from config (in order)")
|
|
260
|
+
.option("--selector <css...>", "Use one or more ad-hoc CSS selectors as additional scope roots");
|
|
261
|
+
program
|
|
262
|
+
.optionsGroup("Handle Warnings:")
|
|
263
|
+
.option("--quiet", "Report errors only", false)
|
|
264
|
+
.option("--max-warnings <n>", "Number of warnings to trigger nonzero exit code", parseIntStrict, -1);
|
|
265
|
+
program
|
|
266
|
+
.optionsGroup("Output:")
|
|
267
|
+
.option("-f, --format <format>", "Use a specific output format", "stylish")
|
|
268
|
+
.option("-o, --output-file <path>", "Specify file to write report to");
|
|
269
|
+
program
|
|
270
|
+
.optionsGroup("Miscellaneous:")
|
|
271
|
+
.option("--verbose", "Log progress details to stderr", false)
|
|
272
|
+
.option("--init", "Run config initialization wizard", false)
|
|
273
|
+
.option("--mcp", "Start the ViewLint MCP server", false)
|
|
274
|
+
.version(version, "-v, --version", "Output the version number")
|
|
275
|
+
.helpOption("-h, --help", "Show help");
|
|
276
|
+
program.action(async (urls, options) => {
|
|
277
|
+
exitCode = await execute(options, urls);
|
|
278
|
+
});
|
|
279
|
+
program.exitOverride();
|
|
280
|
+
try {
|
|
281
|
+
await program.parseAsync(argv);
|
|
282
|
+
return exitCode;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (isCommanderError(error)) {
|
|
286
|
+
return error.exitCode === 0 ? 0 : 2;
|
|
287
|
+
}
|
|
288
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
289
|
+
process.stderr.write(`${message}\n`);
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configFile.d.ts","sourceRoot":"","sources":["../../src/configFile.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,sBAAsB,GAC/B,oBAAoB,GACpB,oBAAoB,GACpB,qBAAqB,CAAA;AAiBxB,wBAAgB,6BAA6B,CAC5C,YAAY,GAAE,MAAsB,GAClC,MAAM,GAAG,SAAS,CAYpB"}
|