@amritk/mjst 0.13.9 → 0.14.8
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 +34 -5
- package/dist/allowed-roots-hint.d.ts +8 -0
- package/dist/allowed-roots-hint.js +5 -0
- package/dist/cli-config.d.ts +25 -0
- package/dist/cli.js +99 -74
- package/dist/create-output-writer.d.ts +40 -0
- package/dist/create-output-writer.js +57 -0
- package/dist/emit-examples.d.ts +16 -5
- package/dist/emit-examples.js +7 -10
- package/dist/ensure-output-dir.d.ts +10 -0
- package/dist/ensure-output-dir.js +15 -0
- package/dist/help-text.d.ts +1 -1
- package/dist/help-text.js +3 -0
- package/dist/lint/resolver.d.ts +7 -0
- package/dist/lint/resolver.js +8 -3
- package/dist/lint/run.js +57 -26
- package/dist/load-config.d.ts +4 -0
- package/dist/load-config.js +13 -2
- package/dist/load-schema.js +7 -3
- package/dist/parse-cli-args.d.ts +9 -0
- package/dist/parse-cli-args.js +31 -12
- package/dist/parse-meta-request.d.ts +15 -0
- package/dist/parse-meta-request.js +22 -0
- package/dist/validate-config.d.ts +39 -0
- package/dist/validate-config.js +75 -0
- package/package.json +17 -13
package/dist/help-text.d.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* documents these flags (config.schema.json) is not shipped in the published
|
|
5
5
|
* package; the help-text test cross-checks that every flag is listed.
|
|
6
6
|
*/
|
|
7
|
-
export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect\n --export <name> Which export of the schema module to use when --input is not json\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
|
|
7
|
+
export declare const HELP_TEXT = "mjst \u2014 generate TypeScript parsers and type definitions from JSON Schemas\n\nUsage:\n mjst --schema <path> --out-dir <dir> [options]\n mjst --schema-dir <dir> --out-dir <dir> [options]\n mjst --schema <path> --out-file <file> --types-only [options]\n\nSubcommands (each with its own flags and --help):\n mjst lint <files> Lint JSON/YAML documents against a ruleset\n mjst compile-api <module> --out <file> Compile @amritk/api route contracts into a fetch-handler module\n\nInput:\n --schema <path> Schema to process: a JSON Schema file, or a module when --input is set\n --schema-dir <dir> Directory of JSON Schemas, processed recursively (instead of --schema)\n --input <format> Schema source format: json (default), typebox, zod, valibot, effect\n --export <name> Which export of the schema module to use when --input is not json\n\nOutput:\n --out-dir <dir> Output directory for the generated files\n --out-file <file> Single-file output instead of a directory (requires --types-only)\n --types-only Generate type definitions only, without parser functions\n --validators Also emit validateX/isX validators into a validators/ subdir\n --examples Also emit fast-check arbitraries + example values under examples/\n --build Compile the generated files to .js/.d.ts (implies --import-ext js)\n --force Overwrite pre-existing files in the output destination\n --import-ext <ext> Extension on generated relative imports: ts (default) or js\n --helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)\n --root-type <name> Root type name for a single --schema run (default: schema title or filename)\n --type-suffix <s> Suffix appended to every $ref-derived type name\n --banner [text] Prepend a header comment to every generated file\n --readonly Emit deeply readonly type definitions\n\nValidation:\n --strict Throw on type/shape mismatches instead of coercing to defaults\n --strip-unknown Silently drop undeclared input keys at every nesting level\n --case-insensitive Normalize mis-cased enum/const strings to their exact casing\n --log-warnings console.warn on input keys not declared in the schema\n\nReference resolution:\n --resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)\n --allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)\n --allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)\n --allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own\n (comma-separated; relative to the current directory)\n\nMisc:\n --config <path> JSON config file with the same keys; CLI flags take precedence\n --version, -v Print the CLI version\n --help, -h Print this help\n\nDocs: https://github.com/amritk/mjst/tree/main/packages/cli#readme\n";
|
package/dist/help-text.js
CHANGED
|
@@ -22,6 +22,7 @@ Output:
|
|
|
22
22
|
--validators Also emit validateX/isX validators into a validators/ subdir
|
|
23
23
|
--examples Also emit fast-check arbitraries + example values under examples/
|
|
24
24
|
--build Compile the generated files to .js/.d.ts (implies --import-ext js)
|
|
25
|
+
--force Overwrite pre-existing files in the output destination
|
|
25
26
|
--import-ext <ext> Extension on generated relative imports: ts (default) or js
|
|
26
27
|
--helpers <mode> Runtime helpers: package or embedded (default: auto-detect from package.json)
|
|
27
28
|
--root-type <name> Root type name for a single --schema run (default: schema title or filename)
|
|
@@ -39,6 +40,8 @@ Reference resolution:
|
|
|
39
40
|
--resolve-remote Fetch http(s) $refs while resolving cross-file/remote refs (off by default)
|
|
40
41
|
--allowed-hosts <h> Restrict remote $ref fetches to these hosts (comma-separated; implies --resolve-remote)
|
|
41
42
|
--allow-private-hosts Permit remote $refs to private/loopback hosts (SSRF guard, off by default)
|
|
43
|
+
--allowed-roots <d> Extra directories a local $ref may resolve into, on top of the schema's own
|
|
44
|
+
(comma-separated; relative to the current directory)
|
|
42
45
|
|
|
43
46
|
Misc:
|
|
44
47
|
--config <path> JSON config file with the same keys; CLI flags take precedence
|
package/dist/lint/resolver.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export type ResolverOptions = {
|
|
|
10
10
|
allowedHosts?: string[];
|
|
11
11
|
/** Permit remote refs to private/loopback/link-local hosts (off by default as an SSRF guard). */
|
|
12
12
|
allowPrivateHosts?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Extra directories a local `$ref` may resolve into, on top of the linted
|
|
15
|
+
* document's own directory (which is always allowed). Empty by default, which
|
|
16
|
+
* keeps a `$ref` from walking out of the tree the document belongs to.
|
|
17
|
+
* Relative entries are resolved against the process working directory.
|
|
18
|
+
*/
|
|
19
|
+
allowedRoots?: string[];
|
|
13
20
|
};
|
|
14
21
|
/**
|
|
15
22
|
* Builds a {@link LintResolver} backed by `@amritk/resolve-refs`, dereferencing
|
package/dist/lint/resolver.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { resolve as resolvePath } from "node:path";
|
|
2
|
+
import { dirname, resolve as resolvePath } from "node:path";
|
|
3
3
|
import { createDocument, resolveSourceOriginFromMap } from "@amritk/lint";
|
|
4
4
|
import { DiagnosticSeverity } from "@amritk/lint/types";
|
|
5
5
|
import { resolveRefs, resolveRefsFromFile } from "@amritk/resolve-refs";
|
|
6
6
|
import { parse as parseYaml } from "@amritk/yaml";
|
|
7
|
+
import { withAllowedRootsHint } from "../allowed-roots-hint.js";
|
|
7
8
|
import { hasExternalRefs } from "../has-external-refs.js";
|
|
8
9
|
const isRemote = (location) => /^https?:\/\//i.test(location);
|
|
9
10
|
const parseDoc = (content, location) => {
|
|
@@ -51,7 +52,7 @@ const toFindings = (errors, root) => errors.map((error) => {
|
|
|
51
52
|
const location = error.path.length > 0 ? root.getLocationForJsonPath(error.path, true) : void 0;
|
|
52
53
|
const finding = {
|
|
53
54
|
code: "unresolved-ref",
|
|
54
|
-
message: error.message,
|
|
55
|
+
message: withAllowedRootsHint(error.message),
|
|
55
56
|
path: error.path,
|
|
56
57
|
severity: DiagnosticSeverity.Error,
|
|
57
58
|
range: location?.range ?? DOCUMENT_START
|
|
@@ -68,12 +69,16 @@ const createLintResolver = (options = {}) => {
|
|
|
68
69
|
parse: parseDoc,
|
|
69
70
|
trackOrigins: true
|
|
70
71
|
};
|
|
72
|
+
const extraRoots = (options.allowedRoots ?? []).map((root) => resolvePath(root));
|
|
71
73
|
return async (document, { input }) => {
|
|
72
74
|
const { source } = document;
|
|
73
75
|
if (source && hasExternalRefs(document.data)) {
|
|
74
76
|
const absolute = resolvePath(source);
|
|
75
77
|
if (readsBackAs(absolute, input)) {
|
|
76
|
-
const { resolved: resolved2, origins: origins2, errors: errors2 } = await resolveRefsFromFile(absolute,
|
|
78
|
+
const { resolved: resolved2, origins: origins2, errors: errors2 } = await resolveRefsFromFile(absolute, {
|
|
79
|
+
...fromFileOptions,
|
|
80
|
+
...extraRoots.length > 0 ? { allowedRoots: [dirname(absolute), ...extraRoots] } : {}
|
|
81
|
+
});
|
|
77
82
|
return {
|
|
78
83
|
resolved: resolved2,
|
|
79
84
|
sources: buildSourceSet(document, resolved2, origins2, absolute),
|
package/dist/lint/run.js
CHANGED
|
@@ -47,39 +47,65 @@ const mapWithConcurrency = async (items, limit, worker) => {
|
|
|
47
47
|
await Promise.all(runners);
|
|
48
48
|
return results;
|
|
49
49
|
};
|
|
50
|
+
const parseArgs = async (argv) => {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = await yargs(argv).scriptName("lint").parserConfiguration({ "greedy-arrays": false }).strict().exitProcess(false).fail((message, error) => {
|
|
53
|
+
throw error ?? new Error(message);
|
|
54
|
+
}).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", {
|
|
55
|
+
type: "string",
|
|
56
|
+
describe: "Path to associate with stdin input (labels findings and enables ruleset discovery)"
|
|
57
|
+
}).option("concurrency", {
|
|
58
|
+
type: "number",
|
|
59
|
+
default: 8,
|
|
60
|
+
describe: "Maximum number of documents to lint in parallel"
|
|
61
|
+
}).option("resolve", {
|
|
62
|
+
type: "boolean",
|
|
63
|
+
default: true,
|
|
64
|
+
describe: "Dereference $ref / $dynamicRef / $recursiveRef before linting (use --no-resolve to disable)"
|
|
65
|
+
}).option("resolve-remote", {
|
|
66
|
+
type: "boolean",
|
|
67
|
+
default: false,
|
|
68
|
+
describe: "Allow fetching http(s) $refs while resolving (off by default; a lint run stays offline)"
|
|
69
|
+
}).option("allowed-hosts", {
|
|
70
|
+
type: "string",
|
|
71
|
+
array: true,
|
|
72
|
+
describe: "Restrict remote $ref fetches to these hosts (implies --resolve-remote)"
|
|
73
|
+
}).option("allow-private-hosts", {
|
|
74
|
+
type: "boolean",
|
|
75
|
+
default: false,
|
|
76
|
+
describe: "Permit remote $refs to private/loopback hosts (SSRF guard, off by default)"
|
|
77
|
+
}).option("allowed-roots", {
|
|
78
|
+
type: "string",
|
|
79
|
+
array: true,
|
|
80
|
+
describe: "Extra directories a local $ref may resolve into, beyond the document's own (repeat the flag; relative to the current directory)"
|
|
81
|
+
}).help().alias("help", "h").parse();
|
|
82
|
+
if (!Number.isFinite(parsed.concurrency) || parsed.concurrency < 1) {
|
|
83
|
+
return { error: "Invalid --concurrency value. Expected a positive number." };
|
|
84
|
+
}
|
|
85
|
+
return { args: parsed };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
88
|
+
}
|
|
89
|
+
};
|
|
50
90
|
const run = async (argv, options = {}) => {
|
|
51
91
|
const out = [];
|
|
52
92
|
const err = [];
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
default: true,
|
|
63
|
-
describe: "Dereference $ref / $dynamicRef / $recursiveRef before linting (use --no-resolve to disable)"
|
|
64
|
-
}).option("resolve-remote", {
|
|
65
|
-
type: "boolean",
|
|
66
|
-
default: false,
|
|
67
|
-
describe: "Allow fetching http(s) $refs while resolving (off by default; a lint run stays offline)"
|
|
68
|
-
}).option("allowed-hosts", {
|
|
69
|
-
type: "string",
|
|
70
|
-
array: true,
|
|
71
|
-
describe: "Restrict remote $ref fetches to these hosts (implies --resolve-remote)"
|
|
72
|
-
}).option("allow-private-hosts", {
|
|
73
|
-
type: "boolean",
|
|
74
|
-
default: false,
|
|
75
|
-
describe: "Permit remote $refs to private/loopback hosts (SSRF guard, off by default)"
|
|
76
|
-
}).help().alias("help", "h").parse();
|
|
93
|
+
const outcome = await parseArgs(argv);
|
|
94
|
+
if ("error" in outcome) {
|
|
95
|
+
return { code: 2, stdout: "", stderr: `${outcome.error}
|
|
96
|
+
Run \`mjst lint --help\` for the supported flags.
|
|
97
|
+
` };
|
|
98
|
+
}
|
|
99
|
+
const parsed = outcome.args;
|
|
100
|
+
if (parsed.help)
|
|
101
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
77
102
|
const failSeverity = SEVERITY_BY_NAME[parsed.failSeverity] ?? DiagnosticSeverity.Error;
|
|
78
103
|
const allowRemote = parsed.resolveRemote || (parsed.allowedHosts?.length ?? 0) > 0;
|
|
79
104
|
const resolver = parsed.resolve ? createLintResolver({
|
|
80
105
|
remote: allowRemote,
|
|
81
106
|
...parsed.allowedHosts ? { allowedHosts: parsed.allowedHosts } : {},
|
|
82
|
-
allowPrivateHosts: parsed.allowPrivateHosts
|
|
107
|
+
allowPrivateHosts: parsed.allowPrivateHosts,
|
|
108
|
+
...parsed.allowedRoots ? { allowedRoots: parsed.allowedRoots } : {}
|
|
83
109
|
}) : void 0;
|
|
84
110
|
const reportRulesetProblems = (definition, label) => {
|
|
85
111
|
if (parsed.quiet)
|
|
@@ -105,8 +131,13 @@ const run = async (argv, options = {}) => {
|
|
|
105
131
|
rulesetBasePath = dirname(isAbsolute(parsed.ruleset) ? parsed.ruleset : resolve(process.cwd(), parsed.ruleset));
|
|
106
132
|
reportRulesetProblems(rulesetDefinition, parsed.ruleset);
|
|
107
133
|
}
|
|
108
|
-
const
|
|
134
|
+
const documents = parsed.documents ?? [];
|
|
135
|
+
const targets = await fg(documents, { dot: true, onlyFiles: true });
|
|
109
136
|
const allResults = [];
|
|
137
|
+
if (targets.length === 0 && documents.length > 0) {
|
|
138
|
+
return { code: 2, stdout: "", stderr: `No files matched: ${documents.join(", ")}
|
|
139
|
+
` };
|
|
140
|
+
}
|
|
110
141
|
if (targets.length === 0) {
|
|
111
142
|
if (options.stdin === void 0 && process.stdin.isTTY) {
|
|
112
143
|
return { code: 2, stdout: "", stderr: "No documents provided.\n" };
|
package/dist/load-config.d.ts
CHANGED
|
@@ -2,5 +2,9 @@ import type { CliConfig } from './cli-config.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Loads a JSON config file and returns the relevant CLI config properties.
|
|
4
4
|
* The config file should have the same keys as the CLI flags (schema, outDir).
|
|
5
|
+
*
|
|
6
|
+
* Every key is validated up front (see {@link validateConfig}), so the `typeof`
|
|
7
|
+
* guards below are only here to narrow the types for TypeScript — by the time we
|
|
8
|
+
* reach them, nothing can fail them.
|
|
5
9
|
*/
|
|
6
10
|
export declare const loadConfig: (configPath: string) => Promise<Partial<CliConfig>>;
|
package/dist/load-config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { validateConfig } from "./validate-config.js";
|
|
3
4
|
const SOURCE_FORMATS = ["json", "typebox", "zod", "valibot", "effect"];
|
|
4
5
|
const isSourceFormat = (value) => typeof value === "string" && SOURCE_FORMATS.includes(value);
|
|
5
6
|
const isHelpersMode = (value) => value === "package" || value === "embedded";
|
|
@@ -7,12 +8,17 @@ const isImportExt = (value) => value === "js" || value === "ts";
|
|
|
7
8
|
const isStringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
8
9
|
const loadConfig = async (configPath) => {
|
|
9
10
|
const absolutePath = resolve(configPath);
|
|
10
|
-
const raw = await readFile(absolutePath, "utf-8")
|
|
11
|
+
const raw = await readFile(absolutePath, "utf-8").catch((error) => {
|
|
12
|
+
if (error.code === "ENOENT")
|
|
13
|
+
throw new Error(`Config file not found: ${absolutePath}`);
|
|
14
|
+
throw error;
|
|
15
|
+
});
|
|
11
16
|
const parsed = JSON.parse(raw);
|
|
12
17
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
13
18
|
throw new Error(`Config file must be a JSON object: ${configPath}`);
|
|
14
19
|
}
|
|
15
20
|
const obj = parsed;
|
|
21
|
+
validateConfig(obj, configPath);
|
|
16
22
|
return {
|
|
17
23
|
...typeof obj["schema"] === "string" && { schema: obj["schema"] },
|
|
18
24
|
...typeof obj["schemaDir"] === "string" && { schemaDir: obj["schemaDir"] },
|
|
@@ -24,6 +30,7 @@ const loadConfig = async (configPath) => {
|
|
|
24
30
|
...typeof obj["typesOnly"] === "boolean" && { typesOnly: obj["typesOnly"] },
|
|
25
31
|
...typeof obj["examples"] === "boolean" && { examples: obj["examples"] },
|
|
26
32
|
...typeof obj["build"] === "boolean" && { build: obj["build"] },
|
|
33
|
+
...typeof obj["force"] === "boolean" && { force: obj["force"] },
|
|
27
34
|
...typeof obj["logWarnings"] === "boolean" && { logWarnings: obj["logWarnings"] },
|
|
28
35
|
...typeof obj["strict"] === "boolean" && { strict: obj["strict"] },
|
|
29
36
|
...typeof obj["stripUnknown"] === "boolean" && { stripUnknown: obj["stripUnknown"] },
|
|
@@ -37,7 +44,11 @@ const loadConfig = async (configPath) => {
|
|
|
37
44
|
...(typeof obj["banner"] === "boolean" || typeof obj["banner"] === "string") && { banner: obj["banner"] },
|
|
38
45
|
...typeof obj["resolveRemote"] === "boolean" && { resolveRemote: obj["resolveRemote"] },
|
|
39
46
|
...isStringArray(obj["allowedHosts"]) && { allowedHosts: obj["allowedHosts"] },
|
|
40
|
-
...typeof obj["allowPrivateHosts"] === "boolean" && { allowPrivateHosts: obj["allowPrivateHosts"] }
|
|
47
|
+
...typeof obj["allowPrivateHosts"] === "boolean" && { allowPrivateHosts: obj["allowPrivateHosts"] },
|
|
48
|
+
// Kept as written: like `schema` and `outDir`, a relative entry is resolved
|
|
49
|
+
// against the process working directory rather than the config file's
|
|
50
|
+
// directory, so one rule covers both flags and config keys.
|
|
51
|
+
...isStringArray(obj["allowedRoots"]) && { allowedRoots: obj["allowedRoots"] }
|
|
41
52
|
};
|
|
42
53
|
};
|
|
43
54
|
export {
|
package/dist/load-schema.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
2
3
|
import { getAdapter } from "@amritk/adapters/get-adapter";
|
|
3
4
|
import { resolveRefsFromFile } from "@amritk/resolve-refs";
|
|
5
|
+
import { withAllowedRootsHint } from "./allowed-roots-hint.js";
|
|
4
6
|
import { hasExternalRefs } from "./has-external-refs.js";
|
|
5
7
|
import { loadSchemaModule } from "./load-schema-module.js";
|
|
6
|
-
const buildResolveOptions = (config) => {
|
|
8
|
+
const buildResolveOptions = (config, schemaPath) => {
|
|
7
9
|
const remote = (config.resolveRemote ?? false) || (config.allowedHosts?.length ?? 0) > 0;
|
|
10
|
+
const extraRoots = (config.allowedRoots ?? []).map((root) => resolve(root));
|
|
8
11
|
return {
|
|
9
12
|
remote,
|
|
13
|
+
...extraRoots.length > 0 ? { allowedRoots: [dirname(resolve(schemaPath)), ...extraRoots] } : {},
|
|
10
14
|
...config.allowedHosts ? { allowedHosts: [...config.allowedHosts] } : {},
|
|
11
15
|
...config.allowPrivateHosts ? { allowPrivateHosts: config.allowPrivateHosts } : {}
|
|
12
16
|
};
|
|
13
17
|
};
|
|
14
18
|
const formatResolveErrors = (schemaPath, errors) => {
|
|
15
|
-
const details = errors.map((error) => ` - ${error.message}`).join("\n");
|
|
19
|
+
const details = errors.map((error) => ` - ${withAllowedRootsHint(error.message)}`).join("\n");
|
|
16
20
|
return `Failed to resolve $refs in ${schemaPath}:
|
|
17
21
|
${details}`;
|
|
18
22
|
};
|
|
@@ -22,7 +26,7 @@ const loadSchema = async (config, schemaPath) => {
|
|
|
22
26
|
const data = JSON.parse(await readFile(schemaPath, "utf-8"));
|
|
23
27
|
if (!hasExternalRefs(data))
|
|
24
28
|
return data;
|
|
25
|
-
const { resolved, errors } = await resolveRefsFromFile(schemaPath, buildResolveOptions(config));
|
|
29
|
+
const { resolved, errors } = await resolveRefsFromFile(schemaPath, buildResolveOptions(config, schemaPath));
|
|
26
30
|
if (errors.length > 0)
|
|
27
31
|
throw new Error(formatResolveErrors(schemaPath, errors));
|
|
28
32
|
return resolved;
|
package/dist/parse-cli-args.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { CliConfig } from './cli-config.js';
|
|
2
|
+
/**
|
|
3
|
+
* True when `--<flagName>` consumes the argument that follows it (`--banner`
|
|
4
|
+
* takes an optional one, which still swallows the next non-flag token).
|
|
5
|
+
*
|
|
6
|
+
* Exported because whoever inspects raw argv before parsing — the version/help
|
|
7
|
+
* detection in `cli.ts` — has to know that the `-v` in `--type-suffix -v` is a
|
|
8
|
+
* *value*, not a request to print the version.
|
|
9
|
+
*/
|
|
10
|
+
export declare const flagTakesValue: (flagName: string) => boolean;
|
|
2
11
|
/**
|
|
3
12
|
* Parses command-line arguments into a partial CLI config.
|
|
4
13
|
* Every flag accepts both kebab-case and camelCase (e.g. `--out-dir` and `--outDir`)
|
package/dist/parse-cli-args.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const SOURCE_FORMATS = ["json", "typebox", "zod", "valibot", "effect"];
|
|
2
|
+
const LIST_KEYS = /* @__PURE__ */ new Set(["allowedHosts", "allowedRoots"]);
|
|
2
3
|
const BOOLEAN_KEYS = /* @__PURE__ */ new Set([
|
|
3
4
|
"typesOnly",
|
|
4
5
|
"validators",
|
|
5
6
|
"examples",
|
|
6
7
|
"build",
|
|
8
|
+
"force",
|
|
7
9
|
"logWarnings",
|
|
8
10
|
"strict",
|
|
9
11
|
"stripUnknown",
|
|
@@ -26,6 +28,10 @@ const VALUE_KEYS = /* @__PURE__ */ new Set([
|
|
|
26
28
|
]);
|
|
27
29
|
const EXTERNAL_VALUE_KEYS = /* @__PURE__ */ new Set(["config"]);
|
|
28
30
|
const toCamelCase = (key) => key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
31
|
+
const flagTakesValue = (flagName) => {
|
|
32
|
+
const key = toCamelCase(flagName);
|
|
33
|
+
return VALUE_KEYS.has(key) || LIST_KEYS.has(key) || EXTERNAL_VALUE_KEYS.has(key) || key === "banner";
|
|
34
|
+
};
|
|
29
35
|
const parseHelpersValue = (value) => {
|
|
30
36
|
if (value === "package" || value === "embedded")
|
|
31
37
|
return value;
|
|
@@ -37,11 +43,11 @@ const parseImportExtValue = (value) => {
|
|
|
37
43
|
return void 0;
|
|
38
44
|
};
|
|
39
45
|
const parseInputValue = (value) => SOURCE_FORMATS.includes(value) ? value : void 0;
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
46
|
+
const appendListValue = (config, key, value) => {
|
|
47
|
+
const entries = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
48
|
+
if (entries.length === 0)
|
|
43
49
|
return;
|
|
44
|
-
config
|
|
50
|
+
config[key] = [...config[key] ?? [], ...entries];
|
|
45
51
|
};
|
|
46
52
|
const assignValue = (config, key, value) => {
|
|
47
53
|
switch (key) {
|
|
@@ -108,6 +114,9 @@ const assignBoolean = (config, key, value) => {
|
|
|
108
114
|
case "build":
|
|
109
115
|
config.build = value;
|
|
110
116
|
return true;
|
|
117
|
+
case "force":
|
|
118
|
+
config.force = value;
|
|
119
|
+
return true;
|
|
111
120
|
case "logWarnings":
|
|
112
121
|
config.logWarnings = value;
|
|
113
122
|
return true;
|
|
@@ -137,8 +146,12 @@ const parseCliArgs = (args) => {
|
|
|
137
146
|
const config = {};
|
|
138
147
|
for (let i = 0; i < args.length; i++) {
|
|
139
148
|
const arg = args[i];
|
|
140
|
-
if (!arg
|
|
149
|
+
if (!arg)
|
|
141
150
|
continue;
|
|
151
|
+
if (arg === "--")
|
|
152
|
+
break;
|
|
153
|
+
if (!arg.startsWith("--")) {
|
|
154
|
+
throw new Error(`Unexpected argument "${arg}". mjst takes flags only \u2014 did you mean a subcommand (\`mjst lint\`, \`mjst compile-api\`)?`);
|
|
142
155
|
}
|
|
143
156
|
const equalsIndex = arg.indexOf("=");
|
|
144
157
|
if (equalsIndex !== -1) {
|
|
@@ -146,13 +159,16 @@ const parseCliArgs = (args) => {
|
|
|
146
159
|
const value = arg.slice(equalsIndex + 1);
|
|
147
160
|
if (key2 === "banner") {
|
|
148
161
|
config.banner = value === "false" ? false : value === "true" ? true : value;
|
|
149
|
-
} else if (key2
|
|
150
|
-
|
|
162
|
+
} else if (LIST_KEYS.has(key2)) {
|
|
163
|
+
appendListValue(config, key2, value);
|
|
151
164
|
} else if (BOOLEAN_KEYS.has(key2)) {
|
|
152
165
|
assignBoolean(config, key2, value !== "false");
|
|
153
166
|
} else if (VALUE_KEYS.has(key2)) {
|
|
154
167
|
assignValue(config, key2, value);
|
|
155
|
-
} else if (
|
|
168
|
+
} else if (EXTERNAL_VALUE_KEYS.has(key2)) {
|
|
169
|
+
if (value === "")
|
|
170
|
+
throw new Error(`Flag "--${arg.slice(2, equalsIndex)}" expects a value.`);
|
|
171
|
+
} else {
|
|
156
172
|
throw new Error(`Unknown flag "--${arg.slice(2, equalsIndex)}".`);
|
|
157
173
|
}
|
|
158
174
|
continue;
|
|
@@ -169,12 +185,12 @@ const parseCliArgs = (args) => {
|
|
|
169
185
|
}
|
|
170
186
|
continue;
|
|
171
187
|
}
|
|
172
|
-
if (key
|
|
188
|
+
if (LIST_KEYS.has(key)) {
|
|
173
189
|
const value = args[i + 1];
|
|
174
190
|
if (value === void 0 || value.startsWith("--")) {
|
|
175
191
|
throw new Error(`Flag "--${flagName}" expects a value.`);
|
|
176
192
|
}
|
|
177
|
-
|
|
193
|
+
appendListValue(config, key, value);
|
|
178
194
|
i++;
|
|
179
195
|
continue;
|
|
180
196
|
}
|
|
@@ -193,8 +209,10 @@ const parseCliArgs = (args) => {
|
|
|
193
209
|
}
|
|
194
210
|
if (EXTERNAL_VALUE_KEYS.has(key)) {
|
|
195
211
|
const value = args[i + 1];
|
|
196
|
-
if (value
|
|
197
|
-
|
|
212
|
+
if (value === void 0 || value.startsWith("--")) {
|
|
213
|
+
throw new Error(`Flag "--${flagName}" expects a value.`);
|
|
214
|
+
}
|
|
215
|
+
i++;
|
|
198
216
|
continue;
|
|
199
217
|
}
|
|
200
218
|
throw new Error(`Unknown flag "--${flagName}".`);
|
|
@@ -202,5 +220,6 @@ const parseCliArgs = (args) => {
|
|
|
202
220
|
return config;
|
|
203
221
|
};
|
|
204
222
|
export {
|
|
223
|
+
flagTakesValue,
|
|
205
224
|
parseCliArgs
|
|
206
225
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A request that short-circuits generation and just prints something. */
|
|
2
|
+
export type MetaRequest = 'version' | 'help';
|
|
3
|
+
/**
|
|
4
|
+
* Detects `mjst version` / `mjst help` and the `--version` / `-v` / `--help` /
|
|
5
|
+
* `-h` flags in raw argv.
|
|
6
|
+
*
|
|
7
|
+
* The subtlety is *where* the token sits. Scanning the whole argv with
|
|
8
|
+
* `includes('-v')` meant that any flag whose **value** happened to be `-v` — say
|
|
9
|
+
* `--type-suffix -v` — printed the version and exited 0 without generating
|
|
10
|
+
* anything, which CI reads as a successful build. So a token in the value
|
|
11
|
+
* position of a value-taking flag is skipped: it belongs to that flag.
|
|
12
|
+
*
|
|
13
|
+
* @returns What the args ask for, or `undefined` for a normal generation run.
|
|
14
|
+
*/
|
|
15
|
+
export declare const parseMetaRequest: (args: readonly string[]) => MetaRequest | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { flagTakesValue } from "./parse-cli-args.js";
|
|
2
|
+
const parseMetaRequest = (args) => {
|
|
3
|
+
if (args[0] === "version")
|
|
4
|
+
return "version";
|
|
5
|
+
if (args[0] === "help")
|
|
6
|
+
return "help";
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const previous = i > 0 ? args[i - 1] : void 0;
|
|
9
|
+
const consumedAsValue = previous?.startsWith("--") && !previous.includes("=") && flagTakesValue(previous.slice(2));
|
|
10
|
+
if (consumedAsValue)
|
|
11
|
+
continue;
|
|
12
|
+
const arg = args[i];
|
|
13
|
+
if (arg === "--version" || arg === "-v")
|
|
14
|
+
return "version";
|
|
15
|
+
if (arg === "--help" || arg === "-h")
|
|
16
|
+
return "help";
|
|
17
|
+
}
|
|
18
|
+
return void 0;
|
|
19
|
+
};
|
|
20
|
+
export {
|
|
21
|
+
parseMetaRequest
|
|
22
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The JSON type each config key accepts, mirroring `config.schema.json`.
|
|
3
|
+
* `boolean|string` is `banner`'s `"type": ["boolean", "string"]`.
|
|
4
|
+
*/
|
|
5
|
+
type ConfigValueType = 'string' | 'boolean' | 'boolean|string' | 'string[]';
|
|
6
|
+
/** One key's contract: its JSON type, plus the closed value set when it has one. */
|
|
7
|
+
type ConfigKeySpec = {
|
|
8
|
+
readonly type: ConfigValueType;
|
|
9
|
+
readonly enum?: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Every key a config file may carry, kept in lockstep with `config.schema.json`
|
|
13
|
+
* by `validate-config.test.ts`.
|
|
14
|
+
*
|
|
15
|
+
* The schema itself would be the obvious thing to validate against at runtime,
|
|
16
|
+
* but it is deliberately not shipped in the published package (see `help-text.ts`
|
|
17
|
+
* for the same trade-off), and `@amritk/runtime-validators` — the interpreter that
|
|
18
|
+
* would run it — is not a declared dependency of this package. So the contract
|
|
19
|
+
* lives here as data, and the test pins it to the schema so the two cannot drift.
|
|
20
|
+
* (Exported for exactly that test — nothing else should read it.)
|
|
21
|
+
*/
|
|
22
|
+
export declare const CONFIG_KEYS: Record<string, ConfigKeySpec>;
|
|
23
|
+
/**
|
|
24
|
+
* Checks a parsed config file against the key contract, throwing with every
|
|
25
|
+
* problem found (JSON Pointer + what was expected).
|
|
26
|
+
*
|
|
27
|
+
* Before this, each key was guarded by a `typeof` check whose failure branch was
|
|
28
|
+
* "drop it": `{"strcit": true, "strict": "true"}` generated non-strict output and
|
|
29
|
+
* exited 0, while the very same typo on the command line was rejected. A config
|
|
30
|
+
* file has to be at least as strict as the flags it stands in for.
|
|
31
|
+
*
|
|
32
|
+
* Note what this does *not* buy: a config is still trusted code. `schema` plus a
|
|
33
|
+
* non-`json` `input` makes `load-schema-module.ts` `import()` whatever path the
|
|
34
|
+
* config names, so an untrusted `mjst.config.json` still reaches arbitrary module
|
|
35
|
+
* evaluation. Validating the shape does not change that — do not run mjst against
|
|
36
|
+
* a config file you would not run as a script.
|
|
37
|
+
*/
|
|
38
|
+
export declare const validateConfig: (parsed: Record<string, unknown>, configPath: string) => void;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const CONFIG_KEYS = {
|
|
2
|
+
schema: { type: "string" },
|
|
3
|
+
schemaDir: { type: "string" },
|
|
4
|
+
input: { type: "string", enum: ["json", "typebox", "zod", "valibot", "effect"] },
|
|
5
|
+
export: { type: "string" },
|
|
6
|
+
outDir: { type: "string" },
|
|
7
|
+
outFile: { type: "string" },
|
|
8
|
+
typesOnly: { type: "boolean" },
|
|
9
|
+
validators: { type: "boolean" },
|
|
10
|
+
examples: { type: "boolean" },
|
|
11
|
+
build: { type: "boolean" },
|
|
12
|
+
force: { type: "boolean" },
|
|
13
|
+
logWarnings: { type: "boolean" },
|
|
14
|
+
strict: { type: "boolean" },
|
|
15
|
+
stripUnknown: { type: "boolean" },
|
|
16
|
+
caseInsensitive: { type: "boolean" },
|
|
17
|
+
readonly: { type: "boolean" },
|
|
18
|
+
helpers: { type: "string", enum: ["package", "embedded"] },
|
|
19
|
+
typeSuffix: { type: "string" },
|
|
20
|
+
banner: { type: "boolean|string" },
|
|
21
|
+
importExt: { type: "string", enum: ["js", "ts"] },
|
|
22
|
+
rootType: { type: "string" },
|
|
23
|
+
resolveRemote: { type: "boolean" },
|
|
24
|
+
allowedHosts: { type: "string[]" },
|
|
25
|
+
allowPrivateHosts: { type: "boolean" },
|
|
26
|
+
allowedRoots: { type: "string[]" },
|
|
27
|
+
config: { type: "string" }
|
|
28
|
+
};
|
|
29
|
+
const typeNameOf = (value) => {
|
|
30
|
+
if (value === null)
|
|
31
|
+
return "null";
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return "array";
|
|
34
|
+
return typeof value;
|
|
35
|
+
};
|
|
36
|
+
const matchesType = (value, type) => {
|
|
37
|
+
switch (type) {
|
|
38
|
+
case "string":
|
|
39
|
+
return typeof value === "string";
|
|
40
|
+
case "boolean":
|
|
41
|
+
return typeof value === "boolean";
|
|
42
|
+
case "boolean|string":
|
|
43
|
+
return typeof value === "boolean" || typeof value === "string";
|
|
44
|
+
case "string[]":
|
|
45
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const validateConfig = (parsed, configPath) => {
|
|
49
|
+
const problems = [];
|
|
50
|
+
const knownKeys = Object.keys(CONFIG_KEYS);
|
|
51
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
52
|
+
const spec = CONFIG_KEYS[key];
|
|
53
|
+
if (!spec) {
|
|
54
|
+
if (key === "$schema")
|
|
55
|
+
continue;
|
|
56
|
+
problems.push(` /${key}: unknown option. Known options: ${knownKeys.join(", ")}.`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!matchesType(value, spec.type)) {
|
|
60
|
+
problems.push(` /${key}: expected ${spec.type}, received ${typeNameOf(value)}.`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (spec.enum && typeof value === "string" && !spec.enum.includes(value)) {
|
|
64
|
+
problems.push(` /${key}: expected one of ${spec.enum.join(", ")}, received "${value}".`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (problems.length > 0) {
|
|
68
|
+
throw new Error(`Invalid config file ${configPath}:
|
|
69
|
+
${problems.join("\n")}`);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
export {
|
|
73
|
+
CONFIG_KEYS,
|
|
74
|
+
validateConfig
|
|
75
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/mjst",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.8",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -39,27 +39,31 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"dev": "bun run --conditions=development ./src/cli.ts",
|
|
41
41
|
"start": "bun run ./src/cli.ts",
|
|
42
|
-
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs",
|
|
42
|
+
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs && node ../../scripts/chmod-bin.mjs",
|
|
43
|
+
"prepublishOnly": "node ../../scripts/check-publishable.mjs",
|
|
43
44
|
"types:check": "tsgo -p . --noEmit",
|
|
44
45
|
"test": "NODE_ENV=production vitest run --root ../.. cli",
|
|
45
46
|
"generate-readme": "bun run ./scripts/generate-readme.ts"
|
|
46
47
|
},
|
|
48
|
+
"exports": {
|
|
49
|
+
"./package.json": "./package.json"
|
|
50
|
+
},
|
|
47
51
|
"dependencies": {
|
|
48
|
-
"@amritk/adapters": "0.3
|
|
49
|
-
"@amritk/api": "0.
|
|
50
|
-
"@amritk/generate-examples": "0.
|
|
51
|
-
"@amritk/generate-parsers": "0.
|
|
52
|
-
"@amritk/generate-validators": "0.
|
|
53
|
-
"@amritk/
|
|
54
|
-
"@amritk/
|
|
55
|
-
"@amritk/
|
|
56
|
-
"@amritk/
|
|
57
|
-
"@amritk/yaml": "0.4.0",
|
|
52
|
+
"@amritk/adapters": "^0.4.3",
|
|
53
|
+
"@amritk/api": "^0.15.1",
|
|
54
|
+
"@amritk/generate-examples": "^0.6.3",
|
|
55
|
+
"@amritk/generate-parsers": "^0.19.3",
|
|
56
|
+
"@amritk/generate-validators": "^0.13.0",
|
|
57
|
+
"@amritk/helpers": "^0.15.3",
|
|
58
|
+
"@amritk/lint": "^0.4.7",
|
|
59
|
+
"@amritk/resolve-refs": "^0.6.0",
|
|
60
|
+
"@amritk/yaml": "^0.7.1",
|
|
58
61
|
"fast-glob": "^3.3.2",
|
|
62
|
+
"json-schema-typed": "^8.0.1",
|
|
59
63
|
"yargs": "^17.7.2"
|
|
60
64
|
},
|
|
61
65
|
"devDependencies": {
|
|
62
|
-
"
|
|
66
|
+
"@amritk/generate-markdown": "^0.4.8",
|
|
63
67
|
"@types/yargs": "^17.0.33"
|
|
64
68
|
}
|
|
65
69
|
}
|