@amritk/mjst 0.12.2 → 0.12.3
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/cli.js +280 -362
- package/dist/combine-files.js +24 -35
- 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.js +4 -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 +11 -11
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
|
};
|
package/dist/load-config.js
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import { resolve } from
|
|
3
|
-
const SOURCE_FORMATS = [
|
|
4
|
-
const isSourceFormat = (value) => typeof value ===
|
|
5
|
-
const isHelpersMode = (value) => value ===
|
|
6
|
-
const isImportExt = (value) => value ===
|
|
7
|
-
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry ===
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const SOURCE_FORMATS = ["json", "typebox", "zod", "valibot", "effect"];
|
|
4
|
+
const isSourceFormat = (value) => typeof value === "string" && SOURCE_FORMATS.includes(value);
|
|
5
|
+
const isHelpersMode = (value) => value === "package" || value === "embedded";
|
|
6
|
+
const isImportExt = (value) => value === "js" || value === "ts";
|
|
7
|
+
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
8
|
+
const loadConfig = async (configPath) => {
|
|
9
|
+
const absolutePath = resolve(configPath);
|
|
10
|
+
const raw = await readFile(absolutePath, "utf-8");
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
13
|
+
throw new Error(`Config file must be a JSON object: ${configPath}`);
|
|
14
|
+
}
|
|
15
|
+
const obj = parsed;
|
|
16
|
+
return {
|
|
17
|
+
...typeof obj["schema"] === "string" && { schema: obj["schema"] },
|
|
18
|
+
...typeof obj["schemaDir"] === "string" && { schemaDir: obj["schemaDir"] },
|
|
19
|
+
...typeof obj["outDir"] === "string" && { outDir: obj["outDir"] },
|
|
20
|
+
...typeof obj["outFile"] === "string" && { outFile: obj["outFile"] },
|
|
21
|
+
...isSourceFormat(obj["input"]) && { input: obj["input"] },
|
|
22
|
+
...typeof obj["export"] === "string" && { export: obj["export"] },
|
|
23
|
+
...typeof obj["validators"] === "boolean" && { validators: obj["validators"] },
|
|
24
|
+
...typeof obj["typesOnly"] === "boolean" && { typesOnly: obj["typesOnly"] },
|
|
25
|
+
...typeof obj["examples"] === "boolean" && { examples: obj["examples"] },
|
|
26
|
+
...typeof obj["build"] === "boolean" && { build: obj["build"] },
|
|
27
|
+
...typeof obj["logWarnings"] === "boolean" && { logWarnings: obj["logWarnings"] },
|
|
28
|
+
...typeof obj["strict"] === "boolean" && { strict: obj["strict"] },
|
|
29
|
+
...typeof obj["stripUnknown"] === "boolean" && { stripUnknown: obj["stripUnknown"] },
|
|
30
|
+
...typeof obj["caseInsensitive"] === "boolean" && { caseInsensitive: obj["caseInsensitive"] },
|
|
31
|
+
...typeof obj["readonly"] === "boolean" && { readonly: obj["readonly"] },
|
|
32
|
+
...isHelpersMode(obj["helpers"]) && { helpers: obj["helpers"] },
|
|
33
|
+
...isImportExt(obj["importExt"]) && { importExt: obj["importExt"] },
|
|
34
|
+
...typeof obj["rootType"] === "string" && { rootType: obj["rootType"] },
|
|
35
|
+
...typeof obj["typeSuffix"] === "string" && { typeSuffix: obj["typeSuffix"] },
|
|
36
|
+
// `banner` is a boolean toggle or a custom header string, mirroring the CLI flag.
|
|
37
|
+
...(typeof obj["banner"] === "boolean" || typeof obj["banner"] === "string") && { banner: obj["banner"] },
|
|
38
|
+
...typeof obj["resolveRemote"] === "boolean" && { resolveRemote: obj["resolveRemote"] },
|
|
39
|
+
...isStringArray(obj["allowedHosts"]) && { allowedHosts: obj["allowedHosts"] },
|
|
40
|
+
...typeof obj["allowPrivateHosts"] === "boolean" && { allowPrivateHosts: obj["allowPrivateHosts"] }
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
loadConfig
|
|
46
45
|
};
|
|
@@ -1,45 +1,30 @@
|
|
|
1
|
-
import { pathToFileURL } from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* with a loader such as `tsx`). We surface a friendly hint when the import fails
|
|
15
|
-
* for a `.ts` path so the cause is not a cryptic module-resolution error.
|
|
16
|
-
*/
|
|
17
|
-
export const loadSchemaModule = async (modulePath, exportName) => {
|
|
18
|
-
let mod;
|
|
19
|
-
try {
|
|
20
|
-
mod = (await import(pathToFileURL(modulePath).href));
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
const loadSchemaModule = async (modulePath, exportName) => {
|
|
3
|
+
let mod;
|
|
4
|
+
try {
|
|
5
|
+
mod = await import(pathToFileURL(modulePath).href);
|
|
6
|
+
} catch (error) {
|
|
7
|
+
const hint = modulePath.endsWith(".ts") ? " Importing a .ts schema requires a TypeScript-capable runtime \u2014 run mjst via `bunx`, or install `tsx` and run Node with `--import tsx`." : "";
|
|
8
|
+
throw new Error(`Failed to load schema module at ${modulePath}.${hint}
|
|
9
|
+
${String(error)}`);
|
|
10
|
+
}
|
|
11
|
+
if (exportName) {
|
|
12
|
+
if (!(exportName in mod)) {
|
|
13
|
+
throw new Error(`Schema module ${modulePath} has no export named '${exportName}'.`);
|
|
21
14
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const namedKeys = Object.keys(mod).filter((key) => key !== 'default');
|
|
38
|
-
if (namedKeys.length === 1) {
|
|
39
|
-
const key = namedKeys[0];
|
|
40
|
-
if (key)
|
|
41
|
-
return mod[key];
|
|
42
|
-
}
|
|
43
|
-
throw new Error(`Schema module ${modulePath} exports ${namedKeys.length} values (${namedKeys.join(', ') || 'none'}). ` +
|
|
44
|
-
`Specify which one to use with --export <name>.`);
|
|
15
|
+
return mod[exportName];
|
|
16
|
+
}
|
|
17
|
+
if ("default" in mod && mod["default"] !== void 0) {
|
|
18
|
+
return mod["default"];
|
|
19
|
+
}
|
|
20
|
+
const namedKeys = Object.keys(mod).filter((key) => key !== "default");
|
|
21
|
+
if (namedKeys.length === 1) {
|
|
22
|
+
const key = namedKeys[0];
|
|
23
|
+
if (key)
|
|
24
|
+
return mod[key];
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Schema module ${modulePath} exports ${namedKeys.length} values (${namedKeys.join(", ") || "none"}). Specify which one to use with --export <name>.`);
|
|
27
|
+
};
|
|
28
|
+
export {
|
|
29
|
+
loadSchemaModule
|
|
45
30
|
};
|
package/dist/load-schema.js
CHANGED
|
@@ -1,52 +1,36 @@
|
|
|
1
|
-
import { readFile } from
|
|
2
|
-
import { getAdapter } from
|
|
3
|
-
import { resolveRefsFromFile } from
|
|
4
|
-
import { hasExternalRefs } from
|
|
5
|
-
import { loadSchemaModule } from
|
|
6
|
-
/**
|
|
7
|
-
* Builds the `@amritk/resolve-refs` options from the CLI config, mirroring the
|
|
8
|
-
* safety posture of `mjst lint`: remote (`http(s)`) `$ref`s are refused unless
|
|
9
|
-
* opted into, a non-empty `allowedHosts` implies remote fetching, and private/
|
|
10
|
-
* loopback hosts stay blocked as an SSRF guard unless explicitly permitted.
|
|
11
|
-
*/
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { getAdapter } from "@amritk/adapters/get-adapter";
|
|
3
|
+
import { resolveRefsFromFile } from "@amritk/resolve-refs";
|
|
4
|
+
import { hasExternalRefs } from "./has-external-refs.js";
|
|
5
|
+
import { loadSchemaModule } from "./load-schema-module.js";
|
|
12
6
|
const buildResolveOptions = (config) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
};
|
|
7
|
+
const remote = (config.resolveRemote ?? false) || (config.allowedHosts?.length ?? 0) > 0;
|
|
8
|
+
return {
|
|
9
|
+
remote,
|
|
10
|
+
...config.allowedHosts ? { allowedHosts: [...config.allowedHosts] } : {},
|
|
11
|
+
...config.allowPrivateHosts ? { allowPrivateHosts: config.allowPrivateHosts } : {}
|
|
12
|
+
};
|
|
20
13
|
};
|
|
21
|
-
/** Renders resolver failures into a single CLI error message. */
|
|
22
14
|
const formatResolveErrors = (schemaPath, errors) => {
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
const details = errors.map((error) => ` - ${error.message}`).join("\n");
|
|
16
|
+
return `Failed to resolve $refs in ${schemaPath}:
|
|
17
|
+
${details}`;
|
|
25
18
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return data;
|
|
44
|
-
const { resolved, errors } = await resolveRefsFromFile(schemaPath, buildResolveOptions(config));
|
|
45
|
-
if (errors.length > 0)
|
|
46
|
-
throw new Error(formatResolveErrors(schemaPath, errors));
|
|
47
|
-
return resolved;
|
|
48
|
-
}
|
|
49
|
-
console.log(`Input format: ${inputFormat}`);
|
|
50
|
-
const source = await loadSchemaModule(schemaPath, config.export);
|
|
51
|
-
return getAdapter(inputFormat).toJSONSchema(source);
|
|
19
|
+
const loadSchema = async (config, schemaPath) => {
|
|
20
|
+
const inputFormat = config.input ?? "json";
|
|
21
|
+
if (inputFormat === "json") {
|
|
22
|
+
const data = JSON.parse(await readFile(schemaPath, "utf-8"));
|
|
23
|
+
if (!hasExternalRefs(data))
|
|
24
|
+
return data;
|
|
25
|
+
const { resolved, errors } = await resolveRefsFromFile(schemaPath, buildResolveOptions(config));
|
|
26
|
+
if (errors.length > 0)
|
|
27
|
+
throw new Error(formatResolveErrors(schemaPath, errors));
|
|
28
|
+
return resolved;
|
|
29
|
+
}
|
|
30
|
+
console.log(`Input format: ${inputFormat}`);
|
|
31
|
+
const source = await loadSchemaModule(schemaPath, config.export);
|
|
32
|
+
return getAdapter(inputFormat).toJSONSchema(source);
|
|
33
|
+
};
|
|
34
|
+
export {
|
|
35
|
+
loadSchema
|
|
52
36
|
};
|