| // @flow strict |
| |
| import objectValues from '../polyfills/objectValues'; |
| |
| import keyMap from '../jsutils/keyMap'; |
| import inspect from '../jsutils/inspect'; |
| import mapValue from '../jsutils/mapValue'; |
| import invariant from '../jsutils/invariant'; |
| import devAssert from '../jsutils/devAssert'; |
| |
| import { Kind } from '../language/kinds'; |
| import { TokenKind } from '../language/tokenKind'; |
| import { dedentBlockStringValue } from '../language/blockString'; |
| import { type DirectiveLocationEnum } from '../language/directiveLocation'; |
| import { |
| isTypeDefinitionNode, |
| isTypeExtensionNode, |
| } from '../language/predicates'; |
| import { |
| type Location, |
| type DocumentNode, |
| type StringValueNode, |
| type TypeNode, |
| type NamedTypeNode, |
| type SchemaDefinitionNode, |
| type SchemaExtensionNode, |
| type TypeDefinitionNode, |
| type InterfaceTypeDefinitionNode, |
| type InterfaceTypeExtensionNode, |
| type ObjectTypeDefinitionNode, |
| type ObjectTypeExtensionNode, |
| type UnionTypeDefinitionNode, |
| type UnionTypeExtensionNode, |
| type FieldDefinitionNode, |
| type InputObjectTypeDefinitionNode, |
| type InputObjectTypeExtensionNode, |
| type InputValueDefinitionNode, |
| type EnumTypeDefinitionNode, |
| type EnumTypeExtensionNode, |
| type EnumValueDefinitionNode, |
| type DirectiveDefinitionNode, |
| } from '../language/ast'; |
| |
| import { assertValidSDLExtension } from '../validation/validate'; |
| |
| import { getDirectiveValues } from '../execution/values'; |
| |
| import { specifiedScalarTypes, isSpecifiedScalarType } from '../type/scalars'; |
| import { introspectionTypes, isIntrospectionType } from '../type/introspection'; |
| import { |
| GraphQLDirective, |
| GraphQLDeprecatedDirective, |
| } from '../type/directives'; |
| import { |
| type GraphQLSchemaValidationOptions, |
| assertSchema, |
| GraphQLSchema, |
| type GraphQLSchemaNormalizedConfig, |
| } from '../type/schema'; |
| import { |
| type GraphQLType, |
| type GraphQLNamedType, |
| type GraphQLFieldConfigMap, |
| type GraphQLEnumValueConfigMap, |
| type GraphQLInputFieldConfigMap, |
| type GraphQLFieldConfigArgumentMap, |
| isScalarType, |
| isObjectType, |
| isInterfaceType, |
| isUnionType, |
| isListType, |
| isNonNullType, |
| isEnumType, |
| isInputObjectType, |
| GraphQLList, |
| GraphQLNonNull, |
| GraphQLScalarType, |
| GraphQLObjectType, |
| GraphQLInterfaceType, |
| GraphQLUnionType, |
| GraphQLEnumType, |
| GraphQLInputObjectType, |
| } from '../type/definition'; |
| |
| import { valueFromAST } from './valueFromAST'; |
| |
| type Options = {| |
| ...GraphQLSchemaValidationOptions, |
| |
| /** |
| * Descriptions are defined as preceding string literals, however an older |
| * experimental version of the SDL supported preceding comments as |
| * descriptions. Set to true to enable this deprecated behavior. |
| * This option is provided to ease adoption and will be removed in v16. |
| * |
| * Default: false |
| */ |
| commentDescriptions?: boolean, |
| |
| /** |
| * Set to true to assume the SDL is valid. |
| * |
| * Default: false |
| */ |
| assumeValidSDL?: boolean, |
| |}; |
| |
| /** |
| * Produces a new schema given an existing schema and a document which may |
| * contain GraphQL type extensions and definitions. The original schema will |
| * remain unaltered. |
| * |
| * Because a schema represents a graph of references, a schema cannot be |
| * extended without effectively making an entire copy. We do not know until it's |
| * too late if subgraphs remain unchanged. |
| * |
| * This algorithm copies the provided schema, applying extensions while |
| * producing the copy. The original schema remains unaltered. |
| * |
| * Accepts options as a third argument: |
| * |
| * - commentDescriptions: |
| * Provide true to use preceding comments as the description. |
| * |
| */ |
| export function extendSchema( |
| schema: GraphQLSchema, |
| documentAST: DocumentNode, |
| options?: Options, |
| ): GraphQLSchema { |
| assertSchema(schema); |
| |
| devAssert( |
| documentAST != null && documentAST.kind === Kind.DOCUMENT, |
| 'Must provide valid Document AST.', |
| ); |
| |
| if (options?.assumeValid !== true && options?.assumeValidSDL !== true) { |
| assertValidSDLExtension(documentAST, schema); |
| } |
| |
| const schemaConfig = schema.toConfig(); |
| const extendedConfig = extendSchemaImpl(schemaConfig, documentAST, options); |
| return schemaConfig === extendedConfig |
| ? schema |
| : new GraphQLSchema(extendedConfig); |
| } |
| |
| /** |
| * @internal |
| */ |
| export function extendSchemaImpl( |
| schemaConfig: GraphQLSchemaNormalizedConfig, |
| documentAST: DocumentNode, |
| options?: Options, |
| ): GraphQLSchemaNormalizedConfig { |
| // Collect the type definitions and extensions found in the document. |
| const typeDefs: Array<TypeDefinitionNode> = []; |
| const typeExtensionsMap = Object.create(null); |
| |
| // New directives and types are separate because a directives and types can |
| // have the same name. For example, a type named "skip". |
| const directiveDefs: Array<DirectiveDefinitionNode> = []; |
| |
| let schemaDef: ?SchemaDefinitionNode; |
| // Schema extensions are collected which may add additional operation types. |
| const schemaExtensions: Array<SchemaExtensionNode> = []; |
| |
| for (const def of documentAST.definitions) { |
| if (def.kind === Kind.SCHEMA_DEFINITION) { |
| schemaDef = def; |
| } else if (def.kind === Kind.SCHEMA_EXTENSION) { |
| schemaExtensions.push(def); |
| } else if (isTypeDefinitionNode(def)) { |
| typeDefs.push(def); |
| } else if (isTypeExtensionNode(def)) { |
| const extendedTypeName = def.name.value; |
| const existingTypeExtensions = typeExtensionsMap[extendedTypeName]; |
| typeExtensionsMap[extendedTypeName] = existingTypeExtensions |
| ? existingTypeExtensions.concat([def]) |
| : [def]; |
| } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { |
| directiveDefs.push(def); |
| } |
| } |
| |
| // If this document contains no new types, extensions, or directives then |
| // return the same unmodified GraphQLSchema instance. |
| if ( |
| Object.keys(typeExtensionsMap).length === 0 && |
| typeDefs.length === 0 && |
| directiveDefs.length === 0 && |
| schemaExtensions.length === 0 && |
| schemaDef == null |
| ) { |
| return schemaConfig; |
| } |
| |
| const typeMap = Object.create(null); |
| for (const existingType of schemaConfig.types) { |
| typeMap[existingType.name] = extendNamedType(existingType); |
| } |
| |
| for (const typeNode of typeDefs) { |
| const name = typeNode.name.value; |
| typeMap[name] = stdTypeMap[name] ?? buildType(typeNode); |
| } |
| |
| const operationTypes = { |
| // Get the extended root operation types. |
| query: schemaConfig.query && replaceNamedType(schemaConfig.query), |
| mutation: schemaConfig.mutation && replaceNamedType(schemaConfig.mutation), |
| subscription: |
| schemaConfig.subscription && replaceNamedType(schemaConfig.subscription), |
| // Then, incorporate schema definition and all schema extensions. |
| ...(schemaDef && getOperationTypes([schemaDef])), |
| ...getOperationTypes(schemaExtensions), |
| }; |
| |
| // Then produce and return a Schema config with these types. |
| return { |
| description: schemaDef?.description?.value, |
| ...operationTypes, |
| types: objectValues(typeMap), |
| directives: [ |
| ...schemaConfig.directives.map(replaceDirective), |
| ...directiveDefs.map(buildDirective), |
| ], |
| extensions: undefined, |
| astNode: schemaDef ?? schemaConfig.astNode, |
| extensionASTNodes: schemaConfig.extensionASTNodes.concat(schemaExtensions), |
| assumeValid: options?.assumeValid ?? false, |
| }; |
| |
| // Below are functions used for producing this schema that have closed over |
| // this scope and have access to the schema, cache, and newly defined types. |
| |
| function replaceType(type) { |
| if (isListType(type)) { |
| return new GraphQLList(replaceType(type.ofType)); |
| } else if (isNonNullType(type)) { |
| return new GraphQLNonNull(replaceType(type.ofType)); |
| } |
| return replaceNamedType(type); |
| } |
| |
| function replaceNamedType<T: GraphQLNamedType>(type: T): T { |
| // Note: While this could make early assertions to get the correctly |
| // typed values, that would throw immediately while type system |
| // validation with validateSchema() will produce more actionable results. |
| return ((typeMap[type.name]: any): T); |
| } |
| |
| function replaceDirective(directive: GraphQLDirective): GraphQLDirective { |
| const config = directive.toConfig(); |
| return new GraphQLDirective({ |
| ...config, |
| args: mapValue(config.args, extendArg), |
| }); |
| } |
| |
| function extendNamedType(type: GraphQLNamedType): GraphQLNamedType { |
| if (isIntrospectionType(type) || isSpecifiedScalarType(type)) { |
| // Builtin types are not extended. |
| return type; |
| } |
| if (isScalarType(type)) { |
| return extendScalarType(type); |
| } |
| if (isObjectType(type)) { |
| return extendObjectType(type); |
| } |
| if (isInterfaceType(type)) { |
| return extendInterfaceType(type); |
| } |
| if (isUnionType(type)) { |
| return extendUnionType(type); |
| } |
| if (isEnumType(type)) { |
| return extendEnumType(type); |
| } |
| if (isInputObjectType(type)) { |
| return extendInputObjectType(type); |
| } |
| |
| // Not reachable. All possible types have been considered. |
| invariant(false, 'Unexpected type: ' + inspect((type: empty))); |
| } |
| |
| function extendInputObjectType( |
| type: GraphQLInputObjectType, |
| ): GraphQLInputObjectType { |
| const config = type.toConfig(); |
| const extensions = typeExtensionsMap[config.name] ?? []; |
| |
| return new GraphQLInputObjectType({ |
| ...config, |
| fields: () => ({ |
| ...mapValue(config.fields, field => ({ |
| ...field, |
| type: replaceType(field.type), |
| })), |
| ...buildInputFieldMap(extensions), |
| }), |
| extensionASTNodes: config.extensionASTNodes.concat(extensions), |
| }); |
| } |
| |
| function extendEnumType(type: GraphQLEnumType): GraphQLEnumType { |
| const config = type.toConfig(); |
| const extensions = typeExtensionsMap[type.name] ?? []; |
| |
| return new GraphQLEnumType({ |
| ...config, |
| values: { |
| ...config.values, |
| ...buildEnumValueMap(extensions), |
| }, |
| extensionASTNodes: config.extensionASTNodes.concat(extensions), |
| }); |
| } |
| |
| function extendScalarType(type: GraphQLScalarType): GraphQLScalarType { |
| const config = type.toConfig(); |
| const extensions = typeExtensionsMap[config.name] ?? []; |
| |
| return new GraphQLScalarType({ |
| ...config, |
| extensionASTNodes: config.extensionASTNodes.concat(extensions), |
| }); |
| } |
| |
| function extendObjectType(type: GraphQLObjectType): GraphQLObjectType { |
| const config = type.toConfig(); |
| const extensions = typeExtensionsMap[config.name] ?? []; |
| |
| return new GraphQLObjectType({ |
| ...config, |
| interfaces: () => [ |
| ...type.getInterfaces().map(replaceNamedType), |
| ...buildInterfaces(extensions), |
| ], |
| fields: () => ({ |
| ...mapValue(config.fields, extendField), |
| ...buildFieldMap(extensions), |
| }), |
| extensionASTNodes: config.extensionASTNodes.concat(extensions), |
| }); |
| } |
| |
| function extendInterfaceType( |
| type: GraphQLInterfaceType, |
| ): GraphQLInterfaceType { |
| const config = type.toConfig(); |
| const extensions = typeExtensionsMap[config.name] ?? []; |
| |
| return new GraphQLInterfaceType({ |
| ...config, |
| interfaces: () => [ |
| ...type.getInterfaces().map(replaceNamedType), |
| ...buildInterfaces(extensions), |
| ], |
| fields: () => ({ |
| ...mapValue(config.fields, extendField), |
| ...buildFieldMap(extensions), |
| }), |
| extensionASTNodes: config.extensionASTNodes.concat(extensions), |
| }); |
| } |
| |
| function extendUnionType(type: GraphQLUnionType): GraphQLUnionType { |
| const config = type.toConfig(); |
| const extensions = typeExtensionsMap[config.name] ?? []; |
| |
| return new GraphQLUnionType({ |
| ...config, |
| types: () => [ |
| ...type.getTypes().map(replaceNamedType), |
| ...buildUnionTypes(extensions), |
| ], |
| extensionASTNodes: config.extensionASTNodes.concat(extensions), |
| }); |
| } |
| |
| function extendField(field) { |
| return { |
| ...field, |
| type: replaceType(field.type), |
| args: mapValue(field.args, extendArg), |
| }; |
| } |
| |
| function extendArg(arg) { |
| return { |
| ...arg, |
| type: replaceType(arg.type), |
| }; |
| } |
| |
| function getOperationTypes( |
| nodes: $ReadOnlyArray<SchemaDefinitionNode | SchemaExtensionNode>, |
| ): {| |
| query: ?GraphQLObjectType, |
| mutation: ?GraphQLObjectType, |
| subscription: ?GraphQLObjectType, |
| |} { |
| const opTypes = {}; |
| for (const node of nodes) { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const operationTypesNodes = node.operationTypes ?? []; |
| |
| for (const operationType of operationTypesNodes) { |
| opTypes[operationType.operation] = getNamedType(operationType.type); |
| } |
| } |
| |
| // Note: While this could make early assertions to get the correctly |
| // typed values below, that would throw immediately while type system |
| // validation with validateSchema() will produce more actionable results. |
| return (opTypes: any); |
| } |
| |
| function getNamedType(node: NamedTypeNode): GraphQLNamedType { |
| const name = node.name.value; |
| const type = stdTypeMap[name] ?? typeMap[name]; |
| |
| if (type === undefined) { |
| throw new Error(`Unknown type: "${name}".`); |
| } |
| return type; |
| } |
| |
| function getWrappedType(node: TypeNode): GraphQLType { |
| if (node.kind === Kind.LIST_TYPE) { |
| return new GraphQLList(getWrappedType(node.type)); |
| } |
| if (node.kind === Kind.NON_NULL_TYPE) { |
| return new GraphQLNonNull(getWrappedType(node.type)); |
| } |
| return getNamedType(node); |
| } |
| |
| function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { |
| const locations = node.locations.map( |
| ({ value }) => ((value: any): DirectiveLocationEnum), |
| ); |
| |
| return new GraphQLDirective({ |
| name: node.name.value, |
| description: getDescription(node, options), |
| locations, |
| isRepeatable: node.repeatable, |
| args: buildArgumentMap(node.arguments), |
| astNode: node, |
| }); |
| } |
| |
| function buildFieldMap( |
| nodes: $ReadOnlyArray< |
| | InterfaceTypeDefinitionNode |
| | InterfaceTypeExtensionNode |
| | ObjectTypeDefinitionNode |
| | ObjectTypeExtensionNode, |
| >, |
| ): GraphQLFieldConfigMap<mixed, mixed> { |
| const fieldConfigMap = Object.create(null); |
| for (const node of nodes) { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const nodeFields = node.fields ?? []; |
| |
| for (const field of nodeFields) { |
| fieldConfigMap[field.name.value] = { |
| // Note: While this could make assertions to get the correctly typed |
| // value, that would throw immediately while type system validation |
| // with validateSchema() will produce more actionable results. |
| type: (getWrappedType(field.type): any), |
| description: getDescription(field, options), |
| args: buildArgumentMap(field.arguments), |
| deprecationReason: getDeprecationReason(field), |
| astNode: field, |
| }; |
| } |
| } |
| return fieldConfigMap; |
| } |
| |
| function buildArgumentMap( |
| args: ?$ReadOnlyArray<InputValueDefinitionNode>, |
| ): GraphQLFieldConfigArgumentMap { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const argsNodes = args ?? []; |
| |
| const argConfigMap = Object.create(null); |
| for (const arg of argsNodes) { |
| // Note: While this could make assertions to get the correctly typed |
| // value, that would throw immediately while type system validation |
| // with validateSchema() will produce more actionable results. |
| const type: any = getWrappedType(arg.type); |
| |
| argConfigMap[arg.name.value] = { |
| type, |
| description: getDescription(arg, options), |
| defaultValue: valueFromAST(arg.defaultValue, type), |
| astNode: arg, |
| }; |
| } |
| return argConfigMap; |
| } |
| |
| function buildInputFieldMap( |
| nodes: $ReadOnlyArray< |
| InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, |
| >, |
| ): GraphQLInputFieldConfigMap { |
| const inputFieldMap = Object.create(null); |
| for (const node of nodes) { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const fieldsNodes = node.fields ?? []; |
| |
| for (const field of fieldsNodes) { |
| // Note: While this could make assertions to get the correctly typed |
| // value, that would throw immediately while type system validation |
| // with validateSchema() will produce more actionable results. |
| const type: any = getWrappedType(field.type); |
| |
| inputFieldMap[field.name.value] = { |
| type, |
| description: getDescription(field, options), |
| defaultValue: valueFromAST(field.defaultValue, type), |
| astNode: field, |
| }; |
| } |
| } |
| return inputFieldMap; |
| } |
| |
| function buildEnumValueMap( |
| nodes: $ReadOnlyArray<EnumTypeDefinitionNode | EnumTypeExtensionNode>, |
| ): GraphQLEnumValueConfigMap { |
| const enumValueMap = Object.create(null); |
| for (const node of nodes) { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const valuesNodes = node.values ?? []; |
| |
| for (const value of valuesNodes) { |
| enumValueMap[value.name.value] = { |
| description: getDescription(value, options), |
| deprecationReason: getDeprecationReason(value), |
| astNode: value, |
| }; |
| } |
| } |
| return enumValueMap; |
| } |
| |
| function buildInterfaces( |
| nodes: $ReadOnlyArray< |
| | InterfaceTypeDefinitionNode |
| | InterfaceTypeExtensionNode |
| | ObjectTypeDefinitionNode |
| | ObjectTypeExtensionNode, |
| >, |
| ): Array<GraphQLInterfaceType> { |
| const interfaces = []; |
| for (const node of nodes) { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const interfacesNodes = node.interfaces ?? []; |
| |
| for (const type of interfacesNodes) { |
| // Note: While this could make assertions to get the correctly typed |
| // values below, that would throw immediately while type system |
| // validation with validateSchema() will produce more actionable |
| // results. |
| interfaces.push((getNamedType(type): any)); |
| } |
| } |
| return interfaces; |
| } |
| |
| function buildUnionTypes( |
| nodes: $ReadOnlyArray<UnionTypeDefinitionNode | UnionTypeExtensionNode>, |
| ): Array<GraphQLObjectType> { |
| const types = []; |
| for (const node of nodes) { |
| /* istanbul ignore next (See https://github.com/graphql/graphql-js/issues/2203) */ |
| const typeNodes = node.types ?? []; |
| |
| for (const type of typeNodes) { |
| // Note: While this could make assertions to get the correctly typed |
| // values below, that would throw immediately while type system |
| // validation with validateSchema() will produce more actionable |
| // results. |
| types.push((getNamedType(type): any)); |
| } |
| } |
| return types; |
| } |
| |
| function buildType(astNode: TypeDefinitionNode): GraphQLNamedType { |
| const name = astNode.name.value; |
| const description = getDescription(astNode, options); |
| const extensionNodes = typeExtensionsMap[name] ?? []; |
| |
| switch (astNode.kind) { |
| case Kind.OBJECT_TYPE_DEFINITION: { |
| const extensionASTNodes = (extensionNodes: any); |
| const allNodes = [astNode, ...extensionASTNodes]; |
| |
| return new GraphQLObjectType({ |
| name, |
| description, |
| interfaces: () => buildInterfaces(allNodes), |
| fields: () => buildFieldMap(allNodes), |
| astNode, |
| extensionASTNodes, |
| }); |
| } |
| case Kind.INTERFACE_TYPE_DEFINITION: { |
| const extensionASTNodes = (extensionNodes: any); |
| const allNodes = [astNode, ...extensionASTNodes]; |
| |
| return new GraphQLInterfaceType({ |
| name, |
| description, |
| interfaces: () => buildInterfaces(allNodes), |
| fields: () => buildFieldMap(allNodes), |
| astNode, |
| extensionASTNodes, |
| }); |
| } |
| case Kind.ENUM_TYPE_DEFINITION: { |
| const extensionASTNodes = (extensionNodes: any); |
| const allNodes = [astNode, ...extensionASTNodes]; |
| |
| return new GraphQLEnumType({ |
| name, |
| description, |
| values: buildEnumValueMap(allNodes), |
| astNode, |
| extensionASTNodes, |
| }); |
| } |
| case Kind.UNION_TYPE_DEFINITION: { |
| const extensionASTNodes = (extensionNodes: any); |
| const allNodes = [astNode, ...extensionASTNodes]; |
| |
| return new GraphQLUnionType({ |
| name, |
| description, |
| types: () => buildUnionTypes(allNodes), |
| astNode, |
| extensionASTNodes, |
| }); |
| } |
| case Kind.SCALAR_TYPE_DEFINITION: { |
| const extensionASTNodes = (extensionNodes: any); |
| |
| return new GraphQLScalarType({ |
| name, |
| description, |
| astNode, |
| extensionASTNodes, |
| }); |
| } |
| case Kind.INPUT_OBJECT_TYPE_DEFINITION: { |
| const extensionASTNodes = (extensionNodes: any); |
| const allNodes = [astNode, ...extensionASTNodes]; |
| |
| return new GraphQLInputObjectType({ |
| name, |
| description, |
| fields: () => buildInputFieldMap(allNodes), |
| astNode, |
| extensionASTNodes, |
| }); |
| } |
| } |
| |
| // Not reachable. All possible type definition nodes have been considered. |
| invariant( |
| false, |
| 'Unexpected type definition node: ' + inspect((astNode: empty)), |
| ); |
| } |
| } |
| |
| const stdTypeMap = keyMap( |
| specifiedScalarTypes.concat(introspectionTypes), |
| type => type.name, |
| ); |
| |
| /** |
| * Given a field or enum value node, returns the string value for the |
| * deprecation reason. |
| */ |
| function getDeprecationReason( |
| node: EnumValueDefinitionNode | FieldDefinitionNode, |
| ): ?string { |
| const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); |
| return (deprecated?.reason: any); |
| } |
| |
| /** |
| * Given an ast node, returns its string description. |
| * @deprecated: provided to ease adoption and will be removed in v16. |
| * |
| * Accepts options as a second argument: |
| * |
| * - commentDescriptions: |
| * Provide true to use preceding comments as the description. |
| * |
| */ |
| export function getDescription( |
| node: { +description?: StringValueNode, +loc?: Location, ... }, |
| options: ?{ commentDescriptions?: boolean, ... }, |
| ): void | string { |
| if (node.description) { |
| return node.description.value; |
| } |
| if (options?.commentDescriptions === true) { |
| const rawValue = getLeadingCommentBlock(node); |
| if (rawValue !== undefined) { |
| return dedentBlockStringValue('\n' + rawValue); |
| } |
| } |
| } |
| |
| function getLeadingCommentBlock(node): void | string { |
| const loc = node.loc; |
| if (!loc) { |
| return; |
| } |
| const comments = []; |
| let token = loc.startToken.prev; |
| while ( |
| token != null && |
| token.kind === TokenKind.COMMENT && |
| token.next && |
| token.prev && |
| token.line + 1 === token.next.line && |
| token.line !== token.prev.line |
| ) { |
| const value = String(token.value); |
| comments.push(value); |
| token = token.prev; |
| } |
| return comments.length > 0 ? comments.reverse().join('\n') : undefined; |
| } |