diff --git a/src/cli.ts b/src/cli.ts index 8db3842..f70b364 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -14,7 +14,7 @@ async function main() { } const { loadSchema } = await import("./loader/loader"); - const schema = await loadSchema(entryPath, {}); + const schema = await loadSchema(entryPath, { inferDefaults: args.defaults }); if (args.write) { const json = JSON.stringify(schema, null, 2); const outfile = resolve( diff --git a/src/loader/loader.ts b/src/loader/loader.ts index 43cab11..2f362d4 100644 --- a/src/loader/loader.ts +++ b/src/loader/loader.ts @@ -1,5 +1,5 @@ -import jiti from "jiti"; import { defu } from "defu"; +import jiti from "jiti"; import { resolveSchema } from "../schema"; import type { Schema } from "../types"; import untypedPlugin from "./babel"; @@ -10,6 +10,7 @@ type JITIOptions = Parameters[1]; export interface LoaderOptions { jiti?: JITIOptions; defaults?: Record; + inferDefaults?: boolean; } export async function loadSchema( @@ -31,8 +32,11 @@ export async function loadSchema( const resolvedEntryPath = _jitiRequire.resolve(entryPath); const rawSchema = _jitiRequire(resolvedEntryPath); - - const schema = await resolveSchema(rawSchema, options.defaults); + const schema = await resolveSchema( + rawSchema, + options.defaults, + options.inferDefaults + ); return schema; } diff --git a/src/schema.ts b/src/schema.ts index 7a5f093..8827643 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,25 +1,31 @@ +import type { InputObject, InputValue, JSType, JSValue, Schema } from "./types"; import { getType, - isObject, - unique, getValue, - setValue, + isObject, joinPath, nonEmpty, + setValue, + unique, } from "./utils"; -import type { InputObject, InputValue, JSType, JSValue, Schema } from "./types"; interface _ResolveCtx { root: InputObject; defaults?: InputObject; resolveCache: Record; + inferDefaults: boolean; } -export async function resolveSchema(obj: InputObject, defaults?: InputObject) { +export async function resolveSchema( + obj: InputObject, + defaults?: InputObject, + inferDefaults = true +): Promise { const schema = await _resolveSchema(obj, "", { root: obj, defaults, resolveCache: {}, + inferDefaults, }); // TODO: Create meta-schema fror superset of Schema interface // schema.$schema = 'http://json-schema.org/schema#' @@ -41,23 +47,26 @@ async function _resolveSchema( // Node is plain value if (!isObject(input)) { + // Clone arrays to avoid mutation + const safeInput = Array.isArray(input) ? [...input] : (input as JSValue); const schema: Schema = { type: getType(input), id: schemaId, - // Clone arrays to avoid mutation - default: Array.isArray(input) ? [...input] : (input as JSValue), + default: ctx.inferDefaults ? safeInput : undefined, }; - normalizeSchema(schema); + + normalizeSchema(schema, ctx.inferDefaults); ctx.resolveCache[id] = schema; + if (ctx.defaults && getValue(ctx.defaults, id) === undefined) { setValue(ctx.defaults, id, schema.default); } + return schema; } // Clone to avoid mutation const node = { ...(input as any) } as InputObject; - const schema: Schema = (ctx.resolveCache[id] = { ...node.$schema, id: schemaId, @@ -87,19 +96,23 @@ async function _resolveSchema( } } - // Infer default value from $resolve and $default - if (ctx.defaults) { - schema.default = getValue(ctx.defaults, id); - } - if (schema.default === undefined && "$default" in node) { - schema.default = node.$default; - } - if (typeof node.$resolve === "function") { - schema.default = await node.$resolve(schema.default, async (key) => { - // eslint-disable-next-line unicorn/no-await-expression-member - return (await _resolveSchema(getValue(ctx.root, key), key, ctx)).default; - }); + if (ctx.inferDefaults) { + // Infer default value from $resolve and $default + if (ctx.defaults) { + schema.default = getValue(ctx.defaults, id); + } + if (schema.default === undefined && "$default" in node) { + schema.default = node.$default; + } + if (typeof node.$resolve === "function") { + schema.default = await node.$resolve(schema.default, async (key) => { + // eslint-disable-next-line unicorn/no-await-expression-member + return (await _resolveSchema(getValue(ctx.root, key), key, ctx)) + .default; + }); + } } + if (ctx.defaults) { setValue(ctx.defaults, id, schema.default); } @@ -110,7 +123,7 @@ async function _resolveSchema( getType(schema.default) || (schema.properties ? "object" : "any"); } - normalizeSchema(schema); + normalizeSchema(schema, ctx.inferDefaults); if (ctx.defaults && getValue(ctx.defaults, id) === undefined) { setValue(ctx.defaults, id, schema.default); } @@ -122,7 +135,10 @@ export async function applyDefaults(ref: InputObject, input: InputObject) { return input; } -function normalizeSchema(schema: Partial): asserts schema is Schema { +function normalizeSchema( + schema: Partial, + inferDefaults = true +): asserts schema is Schema { if (schema.type === "array" && !("items" in schema)) { schema.items = { type: nonEmpty(unique((schema.default as any[]).map((i) => getType(i)))), @@ -136,6 +152,7 @@ function normalizeSchema(schema: Partial): asserts schema is Schema { } } if ( + inferDefaults && schema.default === undefined && ("properties" in schema || schema.type === "object" || diff --git a/test/schema.test.ts b/test/schema.test.ts index 0237be2..c1076a3 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, expect, it } from "vitest"; import { resolveSchema } from "../src"; describe("resolveSchema", () => { @@ -43,6 +43,27 @@ describe("resolveSchema", () => { }); }); + it("without inferred defaults", async () => { + const schema = await resolveSchema( + { foo: { bar: 123 } }, + { foo: { bar: 123 } }, + false + ); + + expect(schema).toMatchObject({ + properties: { + foo: { + type: "object", + properties: { + bar: { + type: "number", + }, + }, + }, + }, + }); + }); + it("with $default", async () => { const schema = await resolveSchema({ foo: { $default: "bar" },