diff --git a/src/generator.ts b/src/generator.ts index fb8d23b0..e188b30a 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -35,7 +35,10 @@ function declareEnums(ast: AST, options: Options, processed = new Set()): s } processed.add(ast) - let type = '' + + if (ast.isExternalSchema && !options.declareExternallyReferenced) { + return '' + } switch (ast.type) { case 'ENUM': @@ -46,7 +49,7 @@ function declareEnums(ast: AST, options: Options, processed = new Set()): s case 'INTERSECTION': return ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '') case 'TUPLE': - type = ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '') + let type = ast.params.reduce((prev, ast) => prev + declareEnums(ast, options, processed), '') if (ast.spreadParam) { type += declareEnums(ast.spreadParam, options, processed) } @@ -66,15 +69,17 @@ function declareNamedInterfaces(ast: AST, options: Options, rootASTName: string, processed.add(ast) let type = '' + if (ast.isExternalSchema && !options.declareExternallyReferenced) { + return '' + } + switch (ast.type) { case 'ARRAY': type = declareNamedInterfaces((ast as TArray).params, options, rootASTName, processed) break case 'INTERFACE': type = [ - hasStandaloneName(ast) && - (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && - generateStandaloneInterface(ast, options), + hasStandaloneName(ast) && generateStandaloneInterface(ast, options), getSuperTypesAndParams(ast) .map(ast => declareNamedInterfaces(ast, options, rootASTName, processed)) .filter(Boolean) @@ -108,6 +113,10 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc processed.add(ast) + if (ast.isExternalSchema && !options.declareExternallyReferenced) { + return '' + } + switch (ast.type) { case 'ARRAY': return [ @@ -120,11 +129,7 @@ function declareNamedTypes(ast: AST, options: Options, rootASTName: string, proc return '' case 'INTERFACE': return getSuperTypesAndParams(ast) - .map( - ast => - (ast.standaloneName === rootASTName || options.declareExternallyReferenced) && - declareNamedTypes(ast, options, rootASTName, processed), - ) + .map(ast => declareNamedTypes(ast, options, rootASTName, processed)) .filter(Boolean) .join('\n') case 'INTERSECTION': diff --git a/src/normalizer.ts b/src/normalizer.ts index fa3ec6ba..7a50d082 100644 --- a/src/normalizer.ts +++ b/src/normalizer.ts @@ -1,4 +1,4 @@ -import {JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema' +import {IsExternalSchema, JSONSchemaTypeName, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema' import {appendToDescription, escapeBlockComment, isSchemaLike, justName, toSafeString, traverse} from './utils' import {Options} from './' import {DereferencedPaths} from './resolver' @@ -73,6 +73,27 @@ rules.set('Transform id to $id', (schema, fileName) => { } }) +rules.set( + 'Add an ExternalRef flag to anything that needs it', + (schema, _fileName, _options, _key, dereferencedPaths) => { + if (!isSchemaLike(schema)) { + return + } + + // Top-level schema + if (!schema[Parent]) { + return + } + + const dereferencedName = dereferencedPaths.get(schema) + Object.defineProperty(schema, IsExternalSchema, { + enumerable: false, + value: dereferencedName && !dereferencedName.startsWith('#'), + writable: false, + }) + }, +) + rules.set('Add an $id to anything that needs it', (schema, fileName, _options, _key, dereferencedPaths) => { if (!isSchemaLike(schema)) { return @@ -95,10 +116,6 @@ rules.set('Add an $id to anything that needs it', (schema, fileName, _options, _ if (!schema.$id && !schema.title && dereferencedName) { schema.$id = toSafeString(justName(dereferencedName)) } - - if (dereferencedName) { - dereferencedPaths.delete(schema) - } }) rules.set('Escape closing JSDoc comment', schema => { diff --git a/src/parser.ts b/src/parser.ts index 7e82d7f6..b942e76d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -23,6 +23,9 @@ import { JSONSchemaWithDefinitions, SchemaSchema, SchemaType, + IsExternalSchema, + NormalizedJSONSchema, + Parent, } from './types/JSONSchema' import {generateName, log, maybeStripDefault, maybeStripNameHints} from './utils' @@ -31,7 +34,7 @@ export type Processed = Map> export type UsedNames = Set export function parse( - schema: LinkedJSONSchema | JSONSchema4Type, + schema: NormalizedJSONSchema | JSONSchema4Type, options: Options, keyName?: string, processed: Processed = new Map(), @@ -54,19 +57,17 @@ export function parse( // Be careful to first process the intersection before processing its params, // so that it gets first pick for standalone name. - const ast = parseAsTypeWithCache( - { - $id: schema.$id, - allOf: [], - description: schema.description, - title: schema.title, - }, - 'ALL_OF', - options, - keyName, - processed, - usedNames, - ) as TIntersection + const allOf: NormalizedJSONSchema = { + [IsExternalSchema]: schema[IsExternalSchema], + [Parent]: schema[Parent], + $id: schema.$id, + allOf: [], + description: schema.description, + title: schema.title, + additionalProperties: schema.additionalProperties, + required: schema.required, + } + const ast = parseAsTypeWithCache(allOf, 'ALL_OF', options, keyName, processed, usedNames) as TIntersection ast.params = types.map(type => // We hoist description (for comment) and id/title (for standaloneName) @@ -79,7 +80,7 @@ export function parse( } function parseAsTypeWithCache( - schema: LinkedJSONSchema, + schema: NormalizedJSONSchema, type: SchemaType, options: Options, keyName?: string, @@ -111,12 +112,14 @@ function parseAsTypeWithCache( function parseBooleanSchema(schema: boolean, keyName: string | undefined, options: Options): AST { if (schema) { return { + isExternalSchema: false, keyName, type: options.unknownAny ? 'UNKNOWN' : 'ANY', } } return { + isExternalSchema: false, keyName, type: 'NEVER', } @@ -124,6 +127,7 @@ function parseBooleanSchema(schema: boolean, keyName: string | undefined, option function parseLiteral(schema: JSONSchema4Type, keyName: string | undefined): AST { return { + isExternalSchema: false, keyName, params: schema, type: 'LITERAL', @@ -131,7 +135,7 @@ function parseLiteral(schema: JSONSchema4Type, keyName: string | undefined): AST } function parseNonLiteral( - schema: LinkedJSONSchema, + schema: NormalizedJSONSchema, type: SchemaType, options: Options, keyName: string | undefined, @@ -146,6 +150,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.allOf!.map(_ => parse(_, options, undefined, processed, usedNames)), @@ -156,6 +161,7 @@ function parseNonLiteral( ...(options.unknownAny ? T_UNKNOWN : T_ANY), comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), } @@ -163,6 +169,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.anyOf!.map(_ => parse(_, options, undefined, processed, usedNames)), @@ -172,6 +179,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'BOOLEAN', @@ -180,6 +188,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, params: schema.tsType!, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), @@ -189,9 +198,10 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames, options)!, - params: schema.enum!.map((_, n) => ({ + params: schema.enum!.map((_: any, n: number) => ({ ast: parseLiteral(_, undefined), keyName: schema.tsEnumNames![n], })), @@ -203,6 +213,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NEVER', @@ -211,6 +222,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NULL', @@ -219,6 +231,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NUMBER', @@ -226,6 +239,7 @@ function parseNonLiteral( case 'OBJECT': return { comment: schema.description, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'OBJECT', @@ -235,6 +249,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: schema.oneOf!.map(_ => parse(_, options, undefined, processed, usedNames)), @@ -246,6 +261,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'STRING', @@ -258,6 +274,7 @@ function parseNonLiteral( const arrayType: TTuple = { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, maxItems, minItems, @@ -275,6 +292,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: parse(schema.items!, options, `{keyNameFromDefinition}Items`, processed, usedNames), @@ -285,6 +303,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: (schema.type as JSONSchema4TypeName[]).map(type => { @@ -297,9 +316,10 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), - params: schema.enum!.map(_ => parseLiteral(_, undefined)), + params: schema.enum!.map((_: any) => parseLiteral(_, undefined)), type: 'UNION', } case 'UNNAMED_SCHEMA': @@ -313,6 +333,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, maxItems: schema.maxItems, minItems, @@ -328,6 +349,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, params, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), @@ -364,6 +386,7 @@ function newInterface( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, params: parseSchema(schema, options, processed, usedNames, name), standaloneName: name, diff --git a/src/types/AST.ts b/src/types/AST.ts index 4fb05afd..a4edf1ae 100644 --- a/src/types/AST.ts +++ b/src/types/AST.ts @@ -24,6 +24,7 @@ export type AST = export interface AbstractAST { comment?: string + isExternalSchema: boolean keyName?: string standaloneName?: string type: AST_TYPE @@ -154,18 +155,22 @@ export interface TCustomType extends AbstractAST { export const T_ANY: TAny = { type: 'ANY', + isExternalSchema: false, } export const T_ANY_ADDITIONAL_PROPERTIES: TAny & ASTWithName = { keyName: '[k: string]', type: 'ANY', + isExternalSchema: false, } export const T_UNKNOWN: TUnknown = { type: 'UNKNOWN', + isExternalSchema: false, } export const T_UNKNOWN_ADDITIONAL_PROPERTIES: TUnknown & ASTWithName = { keyName: '[k: string]', type: 'UNKNOWN', + isExternalSchema: false, } diff --git a/src/types/JSONSchema.ts b/src/types/JSONSchema.ts index 4c644f3f..3121027d 100644 --- a/src/types/JSONSchema.ts +++ b/src/types/JSONSchema.ts @@ -40,6 +40,7 @@ export interface JSONSchema extends JSONSchema4 { deprecated?: boolean } +export const IsExternalSchema = Symbol('IsExternalSchema') export const Parent = Symbol('Parent') export interface LinkedJSONSchema extends JSONSchema { @@ -70,7 +71,12 @@ export interface LinkedJSONSchema extends JSONSchema { not?: LinkedJSONSchema } -export interface NormalizedJSONSchema extends LinkedJSONSchema { +export type NormalizedJSONSchema = Omit & { + /** + * Indicates whether this schema was an external $ref. + */ + [IsExternalSchema]: boolean + additionalItems?: boolean | NormalizedJSONSchema additionalProperties: boolean | NormalizedJSONSchema extends?: string[] @@ -92,10 +98,6 @@ export interface NormalizedJSONSchema extends LinkedJSONSchema { oneOf?: NormalizedJSONSchema[] not?: NormalizedJSONSchema required: string[] - - // Removed by normalizer - definitions: never - id: never } export interface EnumJSONSchema extends NormalizedJSONSchema { diff --git a/src/utils.ts b/src/utils.ts index c7f1f215..2295af40 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import {deburr, isPlainObject, trim, upperFirst} from 'lodash' import {basename, dirname, extname, normalize, sep, posix} from 'path' -import {JSONSchema, LinkedJSONSchema, Parent} from './types/JSONSchema' +import {JSONSchema, LinkedJSONSchema, NormalizedJSONSchema, Parent} from './types/JSONSchema' import {JSONSchema4} from 'json-schema' import yaml from 'js-yaml' @@ -338,7 +338,7 @@ export function maybeStripDefault(schema: LinkedJSONSchema): LinkedJSONSchema { * * Mutates `schema`. */ -export function maybeStripNameHints(schema: JSONSchema): JSONSchema { +export function maybeStripNameHints(schema: NormalizedJSONSchema): NormalizedJSONSchema { if ('$id' in schema) { delete schema.$id } diff --git a/test/__snapshots__/test/test.ts.md b/test/__snapshots__/test/test.ts.md index e1c6e25d..4f06d5a6 100644 --- a/test/__snapshots__/test/test.ts.md +++ b/test/__snapshots__/test/test.ts.md @@ -1139,6 +1139,10 @@ Generated by [AVA](https://avajs.dev). export interface Extends extends Base1 {␊ foo: string;␊ }␊ + export interface Base1 {␊ + firstName: string;␊ + lastName: string;␊ + }␊ ` ## extends.2a.js @@ -449547,26 +449551,6 @@ Generated by [AVA](https://avajs.dev). }␊ ` -> Snapshot 5 - - './test/resources/MultiSchema/out/b.yaml.d.ts' - -> Snapshot 6 - - `/* eslint-disable */␊ - /**␊ - * This file was automatically generated by json-schema-to-typescript.␊ - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,␊ - * and run json-schema-to-typescript to regenerate this file.␊ - */␊ - ␊ - export interface BSchema {␊ - x?: string;␊ - y: number;␊ - [k: string]: unknown;␊ - }␊ - ` - ## files in (-i), pipe out > Snapshot 1 diff --git a/test/__snapshots__/test/test.ts.snap b/test/__snapshots__/test/test.ts.snap index 6640ff0a..f571a7e9 100644 Binary files a/test/__snapshots__/test/test.ts.snap and b/test/__snapshots__/test/test.ts.snap differ