@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/lint/run.js CHANGED
@@ -1,208 +1,167 @@
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';
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
- error: DiagnosticSeverity.Error,
10
- warn: DiagnosticSeverity.Warning,
11
- info: DiagnosticSeverity.Information,
12
- hint: DiagnosticSeverity.Hint,
9
+ error: DiagnosticSeverity.Error,
10
+ warn: DiagnosticSeverity.Warning,
11
+ info: DiagnosticSeverity.Information,
12
+ hint: DiagnosticSeverity.Hint
13
13
  };
14
- const SEVERITY_LABEL = ['error', 'warning', 'info', 'hint'];
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
- if (findings.length === 0)
23
- return 'No problems found\n';
24
- const lines = findings.map((d) => {
25
- const loc = `${d.source ?? '<stdin>'}:${d.range.start.line + 1}:${d.range.start.character + 1}`;
26
- return `${loc} ${SEVERITY_LABEL[d.severity] ?? 'error'} ${d.code} ${d.message}`;
27
- });
28
- const errors = findings.filter((d) => d.severity === DiagnosticSeverity.Error).length;
29
- const warnings = findings.filter((d) => d.severity === DiagnosticSeverity.Warning).length;
30
- lines.push('', `✖ ${findings.length} problem(s) (${errors} error(s), ${warnings} warning(s))`);
31
- return `${lines.join('\n')}\n`;
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
- const chunks = [];
35
- for await (const chunk of process.stdin)
36
- chunks.push(chunk);
37
- return Buffer.concat(chunks).toString('utf8');
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
- const results = new Array(items.length);
47
- let next = 0;
48
- const runners = new Array(Math.max(1, Math.min(limit, items.length))).fill(0).map(async () => {
49
- while (true) {
50
- const index = next++;
51
- if (index >= items.length)
52
- return;
53
- results[index] = await worker(items[index]);
54
- }
55
- });
56
- await Promise.all(runners);
57
- return results;
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
- * Runs the linter over `argv`, returning the exit code and the text it would
61
- * print (rather than writing to the process streams) so it can be driven
62
- * in-process by tests. `stdin` supplies the piped document when there are no file
63
- * targets; when omitted it is read from `process.stdin`.
64
- */
65
- export const run = async (argv, options = {}) => {
66
- const out = [];
67
- const err = [];
68
- const parsed = (await yargs(argv)
69
- .scriptName('lint')
70
- .parserConfiguration({ 'greedy-arrays': false })
71
- .usage('$0 [documents..]', 'Lint JSON/YAML documents against a ruleset')
72
- .positional('documents', { describe: 'Documents or globs to lint', type: 'string', array: true })
73
- .option('ruleset', { alias: 'r', type: 'string', describe: 'Path to a ruleset file' })
74
- .option('encoding', { type: 'string', default: 'utf8', describe: 'Input encoding' })
75
- .option('fail-severity', { alias: 'F', type: 'string', default: 'error', choices: Object.keys(SEVERITY_BY_NAME) })
76
- .option('display-only-failures', { alias: 'D', type: 'boolean', default: false })
77
- .option('verbose', { type: 'boolean', default: false })
78
- .option('quiet', { alias: 'q', type: 'boolean', default: false })
79
- .option('stdin-filepath', {
80
- type: 'string',
81
- describe: 'Path to associate with stdin input (labels findings and enables ruleset discovery)',
82
- })
83
- .option('concurrency', {
84
- type: 'number',
85
- default: 8,
86
- describe: 'Maximum number of documents to lint in parallel',
87
- })
88
- .option('resolve', {
89
- type: 'boolean',
90
- default: true,
91
- describe: 'Dereference $ref / $dynamicRef / $recursiveRef before linting (use --no-resolve to disable)',
92
- })
93
- .option('resolve-remote', {
94
- type: 'boolean',
95
- default: false,
96
- describe: 'Allow fetching http(s) $refs while resolving (off by default; a lint run stays offline)',
97
- })
98
- .option('allowed-hosts', {
99
- type: 'string',
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
- const targets = await fg(parsed.documents ?? [], { dot: true, onlyFiles: true });
145
- const allResults = [];
146
- if (targets.length === 0) {
147
- if (options.stdin === undefined && process.stdin.isTTY) {
148
- return { code: 2, stdout: '', stderr: 'No documents provided.\n' };
149
- }
150
- const content = options.stdin ?? (await readStdin());
151
- // With --stdin-filepath, label findings with that path and discover a
152
- // `.lint.*` ruleset by walking up from its directory.
153
- const stdinPath = parsed.stdinFilepath;
154
- let definition = rulesetDefinition;
155
- let basePath = rulesetBasePath;
156
- if (!definition && !parsed.ruleset && stdinPath) {
157
- const discovered = await discoverAndLoad(dirname(stdinPath));
158
- if (discovered)
159
- ({ definition, basePath } = discovered);
160
- }
161
- const opts = {
162
- ...(definition ? { ruleset: definition } : {}),
163
- ...(basePath !== undefined ? { rulesetBasePath: basePath } : {}),
164
- ...(resolver && definition ? { resolve: resolver } : {}),
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
- else {
170
- // Cache discovered/loaded rulesets by directory so a directory of documents
171
- // that share a `.lint.*` file parses it once, not once per file.
172
- const rulesetCache = new Map();
173
- const perFile = await mapWithConcurrency(targets, parsed.concurrency, async (file) => {
174
- const content = await readFile(file, parsed.encoding);
175
- let definition = rulesetDefinition;
176
- let basePath = rulesetBasePath;
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 displayed = parsed.displayOnlyFailures ? filterBySeverity(allResults, failSeverity) : allResults;
201
- if (!parsed.quiet)
202
- out.push(formatReport(displayed));
203
- return {
204
- code: filterBySeverity(allResults, failSeverity).length > 0 ? 1 : 0,
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
  };
@@ -1,46 +1,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
- /**
9
- * Loads a JSON config file and returns the relevant CLI config properties.
10
- * The config file should have the same keys as the CLI flags (schema, outDir).
11
- */
12
- export const loadConfig = async (configPath) => {
13
- const absolutePath = resolve(configPath);
14
- const raw = await readFile(absolutePath, 'utf-8');
15
- const parsed = JSON.parse(raw);
16
- if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
17
- throw new Error(`Config file must be a JSON object: ${configPath}`);
18
- }
19
- const obj = parsed;
20
- return {
21
- ...(typeof obj['schema'] === 'string' && { schema: obj['schema'] }),
22
- ...(typeof obj['schemaDir'] === 'string' && { schemaDir: obj['schemaDir'] }),
23
- ...(typeof obj['outDir'] === 'string' && { outDir: obj['outDir'] }),
24
- ...(typeof obj['outFile'] === 'string' && { outFile: obj['outFile'] }),
25
- ...(isSourceFormat(obj['input']) && { input: obj['input'] }),
26
- ...(typeof obj['export'] === 'string' && { export: obj['export'] }),
27
- ...(typeof obj['validators'] === 'boolean' && { validators: obj['validators'] }),
28
- ...(typeof obj['typesOnly'] === 'boolean' && { typesOnly: obj['typesOnly'] }),
29
- ...(typeof obj['examples'] === 'boolean' && { examples: obj['examples'] }),
30
- ...(typeof obj['build'] === 'boolean' && { build: obj['build'] }),
31
- ...(typeof obj['logWarnings'] === 'boolean' && { logWarnings: obj['logWarnings'] }),
32
- ...(typeof obj['strict'] === 'boolean' && { strict: obj['strict'] }),
33
- ...(typeof obj['stripUnknown'] === 'boolean' && { stripUnknown: obj['stripUnknown'] }),
34
- ...(typeof obj['caseInsensitive'] === 'boolean' && { caseInsensitive: obj['caseInsensitive'] }),
35
- ...(typeof obj['readonly'] === 'boolean' && { readonly: obj['readonly'] }),
36
- ...(isHelpersMode(obj['helpers']) && { helpers: obj['helpers'] }),
37
- ...(isImportExt(obj['importExt']) && { importExt: obj['importExt'] }),
38
- ...(typeof obj['rootType'] === 'string' && { rootType: obj['rootType'] }),
39
- ...(typeof obj['typeSuffix'] === 'string' && { typeSuffix: obj['typeSuffix'] }),
40
- // `banner` is a boolean toggle or a custom header string, mirroring the CLI flag.
41
- ...((typeof obj['banner'] === 'boolean' || typeof obj['banner'] === 'string') && { banner: obj['banner'] }),
42
- ...(typeof obj['resolveRemote'] === 'boolean' && { resolveRemote: obj['resolveRemote'] }),
43
- ...(isStringArray(obj['allowedHosts']) && { allowedHosts: obj['allowedHosts'] }),
44
- ...(typeof obj['allowPrivateHosts'] === 'boolean' && { allowPrivateHosts: obj['allowPrivateHosts'] }),
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 'node:url';
2
- /**
3
- * Loads a schema authored as a JS/TS module and returns the exported schema value.
4
- *
5
- * Export selection, in order of preference:
6
- * 1. The export named by `exportName`, when provided (`--export <name>`).
7
- * 2. The module's default export.
8
- * 3. The sole named export, when there is exactly one.
9
- *
10
- * Anything else is ambiguous and throws, so the user can disambiguate explicitly
11
- * rather than have mjst guess wrong.
12
- *
13
- * Importing a `.ts` module requires a TypeScript-capable runtime (Bun, or Node
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
- catch (error) {
23
- const hint = modulePath.endsWith('.ts')
24
- ? ' Importing a .ts schema requires a TypeScript-capable runtime — run mjst via `bunx`, or install `tsx` and run Node with `--import tsx`.'
25
- : '';
26
- throw new Error(`Failed to load schema module at ${modulePath}.${hint}\n${String(error)}`);
27
- }
28
- if (exportName) {
29
- if (!(exportName in mod)) {
30
- throw new Error(`Schema module ${modulePath} has no export named '${exportName}'.`);
31
- }
32
- return mod[exportName];
33
- }
34
- if ('default' in mod && mod['default'] !== undefined) {
35
- return mod['default'];
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
  };
@@ -1,52 +1,36 @@
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';
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
- // A non-empty allow-list is itself an opt-in to remote fetching.
14
- const remote = (config.resolveRemote ?? false) || (config.allowedHosts?.length ?? 0) > 0;
15
- return {
16
- remote,
17
- ...(config.allowedHosts ? { allowedHosts: [...config.allowedHosts] } : {}),
18
- ...(config.allowPrivateHosts ? { allowPrivateHosts: config.allowPrivateHosts } : {}),
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
- const details = errors.map((error) => ` - ${error.message}`).join('\n');
24
- return `Failed to resolve $refs in ${schemaPath}:\n${details}`;
15
+ const details = errors.map((error) => ` - ${error.message}`).join("\n");
16
+ return `Failed to resolve $refs in ${schemaPath}:
17
+ ${details}`;
25
18
  };
26
- /**
27
- * Reads a JSON Schema off disk, or loads a module and converts it via its adapter.
28
- *
29
- * For JSON input, a schema whose only references are same-document `#/...`
30
- * pointers is parsed as-is — the generator resolves those internal `$ref`s
31
- * itself into named type files, so inlining them here would collapse that
32
- * structure. When a cross-file or remote `$ref` is present (which the generator
33
- * cannot follow on its own), the schema is dereferenced with
34
- * `@amritk/resolve-refs`, inlining every external target into a single document.
35
- * Any resolve failures are surfaced as a CLI error rather than silently yielding
36
- * a half-resolved schema.
37
- */
38
- export const loadSchema = async (config, schemaPath) => {
39
- const inputFormat = config.input ?? 'json';
40
- if (inputFormat === 'json') {
41
- const data = JSON.parse(await readFile(schemaPath, 'utf-8'));
42
- if (!hasExternalRefs(data))
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
  };