@amritk/mjst 0.12.2 → 0.13.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/README.md +28 -1
- package/dist/cli.js +289 -362
- package/dist/combine-files.js +24 -35
- package/dist/compile-api/collect-route-contracts.d.ts +9 -0
- package/dist/compile-api/collect-route-contracts.js +15 -0
- package/dist/compile-api/default-routes-import.d.ts +9 -0
- package/dist/compile-api/default-routes-import.js +8 -0
- package/dist/compile-api/help-text.d.ts +6 -0
- package/dist/compile-api/help-text.js +27 -0
- package/dist/compile-api/load-routes-module.d.ts +8 -0
- package/dist/compile-api/load-routes-module.js +13 -0
- package/dist/compile-api/parse-compile-api-args.d.ts +25 -0
- package/dist/compile-api/parse-compile-api-args.js +73 -0
- package/dist/compile-api/read-compile-options.d.ts +9 -0
- package/dist/compile-api/read-compile-options.js +24 -0
- package/dist/compile-api/run.d.ts +9 -0
- package/dist/compile-api/run.js +75 -0
- package/dist/detect-helpers-mode.js +28 -41
- package/dist/emit-examples.js +18 -28
- package/dist/has-external-refs.js +16 -21
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +8 -7
- package/dist/lint/resolver.js +79 -112
- package/dist/lint/ruleset-loader.js +30 -28
- package/dist/lint/run.js +153 -194
- package/dist/load-config.js +44 -45
- package/dist/load-schema-module.js +28 -43
- package/dist/load-schema.js +31 -47
- package/dist/parse-cli-args.js +190 -229
- package/dist/read-version.js +8 -12
- package/dist/resolve-import-ext.js +4 -9
- package/package.json +12 -11
package/dist/lint/resolver.js
CHANGED
|
@@ -1,126 +1,93 @@
|
|
|
1
|
-
import { readFileSync } from
|
|
2
|
-
import { resolve as resolvePath } from
|
|
3
|
-
import { createDocument, DiagnosticSeverity, resolveSourceOriginFromMap
|
|
4
|
-
import { resolveRefs, resolveRefsFromFile } from
|
|
5
|
-
import { parse as parseYaml } from
|
|
6
|
-
import { hasExternalRefs } from
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import { createDocument, DiagnosticSeverity, resolveSourceOriginFromMap } from "@amritk/lint";
|
|
4
|
+
import { resolveRefs, resolveRefsFromFile } from "@amritk/resolve-refs";
|
|
5
|
+
import { parse as parseYaml } from "@amritk/yaml";
|
|
6
|
+
import { hasExternalRefs } from "../has-external-refs.js";
|
|
7
7
|
const isRemote = (location) => /^https?:\/\//i.test(location);
|
|
8
|
-
/**
|
|
9
|
-
* Parses a referenced document by extension: YAML for `.yaml`/`.yml`, JSON for
|
|
10
|
-
* `.json`, and for anything else (extensionless, remote) tries JSON first, then
|
|
11
|
-
* YAML. YAML is a JSON superset, so this accepts every document the linter does.
|
|
12
|
-
*/
|
|
13
8
|
const parseDoc = (content, location) => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
9
|
+
if (/\.ya?ml$/i.test(location))
|
|
10
|
+
return parseYaml(content);
|
|
11
|
+
if (/\.json$/i.test(location))
|
|
12
|
+
return JSON.parse(content);
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(content);
|
|
15
|
+
} catch {
|
|
16
|
+
return parseYaml(content);
|
|
17
|
+
}
|
|
24
18
|
};
|
|
25
|
-
/** Whether the file at `absolute` reads back byte-for-byte as `input` (the document we linted). */
|
|
26
19
|
const readsBackAs = (absolute, input) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
20
|
+
try {
|
|
21
|
+
return readFileSync(absolute, "utf8") === input;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
33
25
|
};
|
|
34
|
-
/**
|
|
35
|
-
* Builds the {@link ISourceSet} the runner uses to map a finding on a resolved
|
|
36
|
-
* (dereferenced) node back to the file it came from. Origins from the resolver
|
|
37
|
-
* attribute each inlined node to its source document + path; `get` lazily parses
|
|
38
|
-
* each source file (with a position map) so findings on cross-file nodes report
|
|
39
|
-
* that file's own `line:column`. The root document is already parsed by the
|
|
40
|
-
* linter, so it is reused directly. Remote documents are not re-fetched here, so
|
|
41
|
-
* a finding inlined from a URL falls back to the root document's position.
|
|
42
|
-
*/
|
|
43
26
|
const buildSourceSet = (rootDocument, resolved, origins, rootLocation) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
};
|
|
27
|
+
const documents = /* @__PURE__ */ new Map([[rootLocation, rootDocument]]);
|
|
28
|
+
const originMap = origins ?? /* @__PURE__ */ new Map();
|
|
29
|
+
const load = (location) => {
|
|
30
|
+
if (documents.has(location))
|
|
31
|
+
return documents.get(location);
|
|
32
|
+
let document;
|
|
33
|
+
if (location !== "" && !isRemote(location)) {
|
|
34
|
+
try {
|
|
35
|
+
document = createDocument(readFileSync(location, "utf8"), { source: location });
|
|
36
|
+
} catch {
|
|
37
|
+
document = void 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
documents.set(location, document);
|
|
41
|
+
return document;
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
get: load,
|
|
45
|
+
origin: (path) => resolveSourceOriginFromMap(resolved, originMap, rootLocation, path)
|
|
46
|
+
};
|
|
65
47
|
};
|
|
66
|
-
/** A zero-width range at the document start, for resolve errors with no recoverable position. */
|
|
67
48
|
const DOCUMENT_START = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
68
|
-
/**
|
|
69
|
-
* Maps resolver failures ({@link ResolveError}) into lint findings so an
|
|
70
|
-
* unresolvable `$ref` — a typo'd pointer, a missing file, a refused/failed
|
|
71
|
-
* remote fetch — surfaces as a diagnostic instead of being silently dropped. An
|
|
72
|
-
* error carrying a JSON path is anchored to that node's position in the root
|
|
73
|
-
* document (best effort, `closest`); otherwise the finding is document-level.
|
|
74
|
-
*/
|
|
75
49
|
const toFindings = (errors, root) => errors.map((error) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
50
|
+
const location = error.path.length > 0 ? root.getLocationForJsonPath(error.path, true) : void 0;
|
|
51
|
+
const finding = {
|
|
52
|
+
code: "unresolved-ref",
|
|
53
|
+
message: error.message,
|
|
54
|
+
path: error.path,
|
|
55
|
+
severity: DiagnosticSeverity.Error,
|
|
56
|
+
range: location?.range ?? DOCUMENT_START
|
|
57
|
+
};
|
|
58
|
+
if (root.source !== void 0)
|
|
59
|
+
finding.source = root.source;
|
|
60
|
+
return finding;
|
|
87
61
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
...(options.allowPrivateHosts ? { allowPrivateHosts: options.allowPrivateHosts } : {}),
|
|
103
|
-
parse: parseDoc,
|
|
104
|
-
trackOrigins: true,
|
|
105
|
-
};
|
|
106
|
-
return async (document, { input }) => {
|
|
107
|
-
const { source } = document;
|
|
108
|
-
if (source && hasExternalRefs(document.data)) {
|
|
109
|
-
const absolute = resolvePath(source);
|
|
110
|
-
if (readsBackAs(absolute, input)) {
|
|
111
|
-
const { resolved, origins, errors } = await resolveRefsFromFile(absolute, fromFileOptions);
|
|
112
|
-
return {
|
|
113
|
-
resolved,
|
|
114
|
-
sources: buildSourceSet(document, resolved, origins, absolute),
|
|
115
|
-
...(errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
const { resolved, origins, errors } = resolveRefs(document.data, { trackOrigins: true });
|
|
62
|
+
const createLintResolver = (options = {}) => {
|
|
63
|
+
const fromFileOptions = {
|
|
64
|
+
remote: options.remote ?? false,
|
|
65
|
+
...options.allowedHosts ? { allowedHosts: options.allowedHosts } : {},
|
|
66
|
+
...options.allowPrivateHosts ? { allowPrivateHosts: options.allowPrivateHosts } : {},
|
|
67
|
+
parse: parseDoc,
|
|
68
|
+
trackOrigins: true
|
|
69
|
+
};
|
|
70
|
+
return async (document, { input }) => {
|
|
71
|
+
const { source } = document;
|
|
72
|
+
if (source && hasExternalRefs(document.data)) {
|
|
73
|
+
const absolute = resolvePath(source);
|
|
74
|
+
if (readsBackAs(absolute, input)) {
|
|
75
|
+
const { resolved: resolved2, origins: origins2, errors: errors2 } = await resolveRefsFromFile(absolute, fromFileOptions);
|
|
120
76
|
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
77
|
+
resolved: resolved2,
|
|
78
|
+
sources: buildSourceSet(document, resolved2, origins2, absolute),
|
|
79
|
+
...errors2.length > 0 ? { diagnostics: toFindings(errors2, document) } : {}
|
|
124
80
|
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const { resolved, origins, errors } = resolveRefs(document.data, { trackOrigins: true });
|
|
84
|
+
return {
|
|
85
|
+
resolved,
|
|
86
|
+
sources: buildSourceSet(document, resolved, origins, ""),
|
|
87
|
+
...errors.length > 0 ? { diagnostics: toFindings(errors, document) } : {}
|
|
125
88
|
};
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export {
|
|
92
|
+
createLintResolver
|
|
126
93
|
};
|
|
@@ -1,31 +1,33 @@
|
|
|
1
|
-
import { existsSync } from
|
|
2
|
-
import { readFile } from
|
|
3
|
-
import { dirname, isAbsolute, join, resolve } from
|
|
4
|
-
import { pathToFileURL } from
|
|
5
|
-
import { parseWithPointers } from
|
|
6
|
-
const RULESET_FILENAMES = [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return parseWithPointers(content).data;
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { parseWithPointers } from "@amritk/lint";
|
|
6
|
+
const RULESET_FILENAMES = [".lint.yaml", ".lint.yml", ".lint.json", ".lint.js", ".lint.mjs"];
|
|
7
|
+
const loadRuleset = async (path) => {
|
|
8
|
+
const absolute = isAbsolute(path) ? path : resolve(process.cwd(), path);
|
|
9
|
+
if (/\.(c|m)?js$/.test(absolute)) {
|
|
10
|
+
const module = await import(pathToFileURL(absolute).href);
|
|
11
|
+
return module.default ?? module;
|
|
12
|
+
}
|
|
13
|
+
const content = await readFile(absolute, "utf8");
|
|
14
|
+
return parseWithPointers(content).data;
|
|
16
15
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return candidate;
|
|
25
|
-
}
|
|
26
|
-
const parent = dirname(dir);
|
|
27
|
-
if (parent === dir)
|
|
28
|
-
return undefined;
|
|
29
|
-
dir = parent;
|
|
16
|
+
const discoverRuleset = (startDir) => {
|
|
17
|
+
let dir = resolve(startDir);
|
|
18
|
+
while (true) {
|
|
19
|
+
for (const name of RULESET_FILENAMES) {
|
|
20
|
+
const candidate = join(dir, name);
|
|
21
|
+
if (existsSync(candidate))
|
|
22
|
+
return candidate;
|
|
30
23
|
}
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir)
|
|
26
|
+
return void 0;
|
|
27
|
+
dir = parent;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export {
|
|
31
|
+
discoverRuleset,
|
|
32
|
+
loadRuleset
|
|
31
33
|
};
|
package/dist/lint/run.js
CHANGED
|
@@ -1,208 +1,167 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import { dirname, isAbsolute, resolve } from
|
|
3
|
-
import { DiagnosticSeverity, lintDocument, validateRuleset
|
|
4
|
-
import fg from
|
|
5
|
-
import yargs from
|
|
6
|
-
import { createLintResolver } from
|
|
7
|
-
import { discoverRuleset, loadRuleset } from
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { DiagnosticSeverity, lintDocument, validateRuleset } from "@amritk/lint";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
import yargs from "yargs";
|
|
6
|
+
import { createLintResolver } from "./resolver.js";
|
|
7
|
+
import { discoverRuleset, loadRuleset } from "./ruleset-loader.js";
|
|
8
8
|
const SEVERITY_BY_NAME = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
error: DiagnosticSeverity.Error,
|
|
10
|
+
warn: DiagnosticSeverity.Warning,
|
|
11
|
+
info: DiagnosticSeverity.Information,
|
|
12
|
+
hint: DiagnosticSeverity.Hint
|
|
13
13
|
};
|
|
14
|
-
const SEVERITY_LABEL = [
|
|
15
|
-
/**
|
|
16
|
-
* Renders findings as a compact, dependency-free report — one `file:line:col`
|
|
17
|
-
* line per finding (1-based, editor-clickable), then a count summary. Structured
|
|
18
|
-
* output (JSON, SARIF, …) is a consumer concern: use the `@amritk/lint` library's
|
|
19
|
-
* `lintDocument`, which returns `IDiagnostic[]`.
|
|
20
|
-
*/
|
|
14
|
+
const SEVERITY_LABEL = ["error", "warning", "info", "hint"];
|
|
21
15
|
const formatReport = (findings) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
16
|
+
if (findings.length === 0)
|
|
17
|
+
return "No problems found\n";
|
|
18
|
+
const lines = findings.map((d) => {
|
|
19
|
+
const loc = `${d.source ?? "<stdin>"}:${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
20
|
+
return `${loc} ${SEVERITY_LABEL[d.severity] ?? "error"} ${d.code} ${d.message}`;
|
|
21
|
+
});
|
|
22
|
+
const errors = findings.filter((d) => d.severity === DiagnosticSeverity.Error).length;
|
|
23
|
+
const warnings = findings.filter((d) => d.severity === DiagnosticSeverity.Warning).length;
|
|
24
|
+
lines.push("", `\u2716 ${findings.length} problem(s) (${errors} error(s), ${warnings} warning(s))`);
|
|
25
|
+
return `${lines.join("\n")}
|
|
26
|
+
`;
|
|
32
27
|
};
|
|
33
28
|
const readStdin = async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
const chunks = [];
|
|
30
|
+
for await (const chunk of process.stdin)
|
|
31
|
+
chunks.push(chunk);
|
|
32
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
38
33
|
};
|
|
39
34
|
const filterBySeverity = (results, failSeverity) => results.filter((result) => result.severity <= failSeverity);
|
|
40
|
-
/**
|
|
41
|
-
* Maps `items` through `worker` with at most `limit` running at once, preserving
|
|
42
|
-
* input order in the result. Lets the CLI lint a directory of documents
|
|
43
|
-
* concurrently instead of strictly one at a time, without unbounded parallelism.
|
|
44
|
-
*/
|
|
45
35
|
const mapWithConcurrency = async (items, limit, worker) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
const results = new Array(items.length);
|
|
37
|
+
let next = 0;
|
|
38
|
+
const runners = new Array(Math.max(1, Math.min(limit, items.length))).fill(0).map(async () => {
|
|
39
|
+
while (true) {
|
|
40
|
+
const index = next++;
|
|
41
|
+
if (index >= items.length)
|
|
42
|
+
return;
|
|
43
|
+
results[index] = await worker(items[index]);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
await Promise.all(runners);
|
|
47
|
+
return results;
|
|
58
48
|
};
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
array: true,
|
|
101
|
-
describe: 'Restrict remote $ref fetches to these hosts (implies --resolve-remote)',
|
|
102
|
-
})
|
|
103
|
-
.option('allow-private-hosts', {
|
|
104
|
-
type: 'boolean',
|
|
105
|
-
default: false,
|
|
106
|
-
describe: 'Permit remote $refs to private/loopback hosts (SSRF guard, off by default)',
|
|
107
|
-
})
|
|
108
|
-
.help()
|
|
109
|
-
.alias('help', 'h')
|
|
110
|
-
.parse());
|
|
111
|
-
const failSeverity = SEVERITY_BY_NAME[parsed.failSeverity] ?? DiagnosticSeverity.Error;
|
|
112
|
-
// A non-empty --allowed-hosts is an explicit opt-in to remote fetching.
|
|
113
|
-
const allowRemote = parsed.resolveRemote || (parsed.allowedHosts?.length ?? 0) > 0;
|
|
114
|
-
const resolver = parsed.resolve
|
|
115
|
-
? createLintResolver({
|
|
116
|
-
remote: allowRemote,
|
|
117
|
-
...(parsed.allowedHosts ? { allowedHosts: parsed.allowedHosts } : {}),
|
|
118
|
-
allowPrivateHosts: parsed.allowPrivateHosts,
|
|
119
|
-
})
|
|
120
|
-
: undefined;
|
|
121
|
-
const reportRulesetProblems = (definition, label) => {
|
|
122
|
-
if (parsed.quiet)
|
|
123
|
-
return;
|
|
124
|
-
for (const problem of validateRuleset(definition)) {
|
|
125
|
-
const at = problem.path.length > 0 ? ` (at ${problem.path.join('.')})` : '';
|
|
126
|
-
err.push(`warning: ruleset ${label}: ${problem.message}${at}\n`);
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
const discoverAndLoad = async (dir) => {
|
|
130
|
-
const discovered = discoverRuleset(dir);
|
|
131
|
-
if (!discovered)
|
|
132
|
-
return undefined;
|
|
133
|
-
const definition = await loadRuleset(discovered);
|
|
134
|
-
reportRulesetProblems(definition, discovered);
|
|
135
|
-
return { definition, basePath: dirname(discovered) };
|
|
136
|
-
};
|
|
137
|
-
let rulesetDefinition;
|
|
138
|
-
let rulesetBasePath;
|
|
139
|
-
if (parsed.ruleset) {
|
|
140
|
-
rulesetDefinition = await loadRuleset(parsed.ruleset);
|
|
141
|
-
rulesetBasePath = dirname(isAbsolute(parsed.ruleset) ? parsed.ruleset : resolve(process.cwd(), parsed.ruleset));
|
|
142
|
-
reportRulesetProblems(rulesetDefinition, parsed.ruleset);
|
|
49
|
+
const run = async (argv, options = {}) => {
|
|
50
|
+
const out = [];
|
|
51
|
+
const err = [];
|
|
52
|
+
const parsed = await yargs(argv).scriptName("lint").parserConfiguration({ "greedy-arrays": false }).usage("$0 [documents..]", "Lint JSON/YAML documents against a ruleset").positional("documents", { describe: "Documents or globs to lint", type: "string", array: true }).option("ruleset", { alias: "r", type: "string", describe: "Path to a ruleset file" }).option("encoding", { type: "string", default: "utf8", describe: "Input encoding" }).option("fail-severity", { alias: "F", type: "string", default: "error", choices: Object.keys(SEVERITY_BY_NAME) }).option("display-only-failures", { alias: "D", type: "boolean", default: false }).option("verbose", { type: "boolean", default: false }).option("quiet", { alias: "q", type: "boolean", default: false }).option("stdin-filepath", {
|
|
53
|
+
type: "string",
|
|
54
|
+
describe: "Path to associate with stdin input (labels findings and enables ruleset discovery)"
|
|
55
|
+
}).option("concurrency", {
|
|
56
|
+
type: "number",
|
|
57
|
+
default: 8,
|
|
58
|
+
describe: "Maximum number of documents to lint in parallel"
|
|
59
|
+
}).option("resolve", {
|
|
60
|
+
type: "boolean",
|
|
61
|
+
default: true,
|
|
62
|
+
describe: "Dereference $ref / $dynamicRef / $recursiveRef before linting (use --no-resolve to disable)"
|
|
63
|
+
}).option("resolve-remote", {
|
|
64
|
+
type: "boolean",
|
|
65
|
+
default: false,
|
|
66
|
+
describe: "Allow fetching http(s) $refs while resolving (off by default; a lint run stays offline)"
|
|
67
|
+
}).option("allowed-hosts", {
|
|
68
|
+
type: "string",
|
|
69
|
+
array: true,
|
|
70
|
+
describe: "Restrict remote $ref fetches to these hosts (implies --resolve-remote)"
|
|
71
|
+
}).option("allow-private-hosts", {
|
|
72
|
+
type: "boolean",
|
|
73
|
+
default: false,
|
|
74
|
+
describe: "Permit remote $refs to private/loopback hosts (SSRF guard, off by default)"
|
|
75
|
+
}).help().alias("help", "h").parse();
|
|
76
|
+
const failSeverity = SEVERITY_BY_NAME[parsed.failSeverity] ?? DiagnosticSeverity.Error;
|
|
77
|
+
const allowRemote = parsed.resolveRemote || (parsed.allowedHosts?.length ?? 0) > 0;
|
|
78
|
+
const resolver = parsed.resolve ? createLintResolver({
|
|
79
|
+
remote: allowRemote,
|
|
80
|
+
...parsed.allowedHosts ? { allowedHosts: parsed.allowedHosts } : {},
|
|
81
|
+
allowPrivateHosts: parsed.allowPrivateHosts
|
|
82
|
+
}) : void 0;
|
|
83
|
+
const reportRulesetProblems = (definition, label) => {
|
|
84
|
+
if (parsed.quiet)
|
|
85
|
+
return;
|
|
86
|
+
for (const problem of validateRuleset(definition)) {
|
|
87
|
+
const at = problem.path.length > 0 ? ` (at ${problem.path.join(".")})` : "";
|
|
88
|
+
err.push(`warning: ruleset ${label}: ${problem.message}${at}
|
|
89
|
+
`);
|
|
143
90
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
source: stdinPath ?? '<stdin>',
|
|
166
|
-
};
|
|
167
|
-
allResults.push(...(await lintDocument(content, opts)));
|
|
91
|
+
};
|
|
92
|
+
const discoverAndLoad = async (dir) => {
|
|
93
|
+
const discovered = discoverRuleset(dir);
|
|
94
|
+
if (!discovered)
|
|
95
|
+
return void 0;
|
|
96
|
+
const definition = await loadRuleset(discovered);
|
|
97
|
+
reportRulesetProblems(definition, discovered);
|
|
98
|
+
return { definition, basePath: dirname(discovered) };
|
|
99
|
+
};
|
|
100
|
+
let rulesetDefinition;
|
|
101
|
+
let rulesetBasePath;
|
|
102
|
+
if (parsed.ruleset) {
|
|
103
|
+
rulesetDefinition = await loadRuleset(parsed.ruleset);
|
|
104
|
+
rulesetBasePath = dirname(isAbsolute(parsed.ruleset) ? parsed.ruleset : resolve(process.cwd(), parsed.ruleset));
|
|
105
|
+
reportRulesetProblems(rulesetDefinition, parsed.ruleset);
|
|
106
|
+
}
|
|
107
|
+
const targets = await fg(parsed.documents ?? [], { dot: true, onlyFiles: true });
|
|
108
|
+
const allResults = [];
|
|
109
|
+
if (targets.length === 0) {
|
|
110
|
+
if (options.stdin === void 0 && process.stdin.isTTY) {
|
|
111
|
+
return { code: 2, stdout: "", stderr: "No documents provided.\n" };
|
|
168
112
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (!definition && !parsed.ruleset) {
|
|
178
|
-
const dir = dirname(file);
|
|
179
|
-
let discovered = rulesetCache.get(dir);
|
|
180
|
-
if (!rulesetCache.has(dir)) {
|
|
181
|
-
discovered = await discoverAndLoad(dir);
|
|
182
|
-
rulesetCache.set(dir, discovered);
|
|
183
|
-
}
|
|
184
|
-
if (discovered)
|
|
185
|
-
({ definition, basePath } = discovered);
|
|
186
|
-
}
|
|
187
|
-
const opts = definition
|
|
188
|
-
? {
|
|
189
|
-
ruleset: definition,
|
|
190
|
-
...(basePath !== undefined ? { rulesetBasePath: basePath } : {}),
|
|
191
|
-
...(resolver ? { resolve: resolver } : {}),
|
|
192
|
-
source: file,
|
|
193
|
-
}
|
|
194
|
-
: { source: file };
|
|
195
|
-
return lintDocument(content, opts);
|
|
196
|
-
});
|
|
197
|
-
for (const remaining of perFile)
|
|
198
|
-
allResults.push(...remaining);
|
|
113
|
+
const content = options.stdin ?? await readStdin();
|
|
114
|
+
const stdinPath = parsed.stdinFilepath;
|
|
115
|
+
let definition = rulesetDefinition;
|
|
116
|
+
let basePath = rulesetBasePath;
|
|
117
|
+
if (!definition && !parsed.ruleset && stdinPath) {
|
|
118
|
+
const discovered = await discoverAndLoad(dirname(stdinPath));
|
|
119
|
+
if (discovered)
|
|
120
|
+
({ definition, basePath } = discovered);
|
|
199
121
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
stdout: out.join(''),
|
|
206
|
-
stderr: err.join(''),
|
|
122
|
+
const opts = {
|
|
123
|
+
...definition ? { ruleset: definition } : {},
|
|
124
|
+
...basePath !== void 0 ? { rulesetBasePath: basePath } : {},
|
|
125
|
+
...resolver && definition ? { resolve: resolver } : {},
|
|
126
|
+
source: stdinPath ?? "<stdin>"
|
|
207
127
|
};
|
|
128
|
+
allResults.push(...await lintDocument(content, opts));
|
|
129
|
+
} else {
|
|
130
|
+
const rulesetCache = /* @__PURE__ */ new Map();
|
|
131
|
+
const perFile = await mapWithConcurrency(targets, parsed.concurrency, async (file) => {
|
|
132
|
+
const content = await readFile(file, parsed.encoding);
|
|
133
|
+
let definition = rulesetDefinition;
|
|
134
|
+
let basePath = rulesetBasePath;
|
|
135
|
+
if (!definition && !parsed.ruleset) {
|
|
136
|
+
const dir = dirname(file);
|
|
137
|
+
let discovered = rulesetCache.get(dir);
|
|
138
|
+
if (!rulesetCache.has(dir)) {
|
|
139
|
+
discovered = await discoverAndLoad(dir);
|
|
140
|
+
rulesetCache.set(dir, discovered);
|
|
141
|
+
}
|
|
142
|
+
if (discovered)
|
|
143
|
+
({ definition, basePath } = discovered);
|
|
144
|
+
}
|
|
145
|
+
const opts = definition ? {
|
|
146
|
+
ruleset: definition,
|
|
147
|
+
...basePath !== void 0 ? { rulesetBasePath: basePath } : {},
|
|
148
|
+
...resolver ? { resolve: resolver } : {},
|
|
149
|
+
source: file
|
|
150
|
+
} : { source: file };
|
|
151
|
+
return lintDocument(content, opts);
|
|
152
|
+
});
|
|
153
|
+
for (const remaining of perFile)
|
|
154
|
+
allResults.push(...remaining);
|
|
155
|
+
}
|
|
156
|
+
const displayed = parsed.displayOnlyFailures ? filterBySeverity(allResults, failSeverity) : allResults;
|
|
157
|
+
if (!parsed.quiet)
|
|
158
|
+
out.push(formatReport(displayed));
|
|
159
|
+
return {
|
|
160
|
+
code: filterBySeverity(allResults, failSeverity).length > 0 ? 1 : 0,
|
|
161
|
+
stdout: out.join(""),
|
|
162
|
+
stderr: err.join("")
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
export {
|
|
166
|
+
run
|
|
208
167
|
};
|