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 22e585ac..0f32cd67 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -25,6 +25,7 @@ import { Parent, SchemaSchema, SchemaType, + IsExternalSchema, } from './types/JSONSchema' import {generateName, log, maybeStripDefault, maybeStripNameHints} from './utils' @@ -56,22 +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( - { - [Parent]: schema[Parent], - $id: schema.$id, - additionalProperties: schema.additionalProperties, - allOf: [], - description: schema.description, - required: schema.required, - 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) @@ -116,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', } @@ -129,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', @@ -151,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)), @@ -161,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), } @@ -168,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)), @@ -177,6 +179,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'BOOLEAN', @@ -185,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), @@ -194,6 +198,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition ?? keyName, usedNames, options)!, params: (schema as EnumJSONSchema).enum!.map((_, n) => ({ @@ -208,6 +213,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NEVER', @@ -216,6 +222,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NULL', @@ -224,6 +231,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'NUMBER', @@ -231,6 +239,7 @@ function parseNonLiteral( case 'OBJECT': return { comment: schema.description, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'OBJECT', @@ -240,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)), @@ -251,6 +261,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), type: 'STRING', @@ -263,6 +274,7 @@ function parseNonLiteral( const arrayType: TTuple = { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, maxItems, minItems, @@ -280,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), @@ -290,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 => { @@ -307,6 +321,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), params: (schema as EnumJSONSchema).enum!.map(_ => parseLiteral(_, undefined)), @@ -323,6 +338,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, maxItems: schema.maxItems, minItems, @@ -338,6 +354,7 @@ function parseNonLiteral( return { comment: schema.description, deprecated: schema.deprecated, + isExternalSchema: schema[IsExternalSchema], keyName, params, standaloneName: standaloneName(schema, keyNameFromDefinition, usedNames, options), @@ -374,6 +391,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 f9be9524..7ce14130 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 { @@ -77,6 +78,10 @@ export interface LinkedJSONSchema extends JSONSchema { */ export interface NormalizedJSONSchema extends Omit { [Parent]: NormalizedJSONSchema | null + /** + * Indicates whether this schema was an external $ref. + */ + [IsExternalSchema]: boolean additionalItems?: boolean | NormalizedJSONSchema additionalProperties: boolean | NormalizedJSONSchema 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