@amritk/mjst 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  };