diff --git a/lib/base-mercurius.module.ts b/lib/base-mercurius.module.ts index 47f2292..f20c644 100644 --- a/lib/base-mercurius.module.ts +++ b/lib/base-mercurius.module.ts @@ -1,14 +1,12 @@ -import { MercuriusModuleOptions } from './interfaces'; import { loadPackage } from '@nestjs/common/utils/load-package.util'; import { FastifyInstance } from 'fastify'; -import { MercuriusOptions } from 'mercurius'; import { normalizeRoutePath } from '@nestjs/graphql/dist/utils'; import { ApplicationConfig, HttpAdapterHost } from '@nestjs/core'; import { BaseMercuriusModuleOptions } from './interfaces/base-mercurius-module-options.interface'; import { HookExplorerService } from './services'; export abstract class BaseMercuriusModule< - Opts extends BaseMercuriusModuleOptions + Opts extends BaseMercuriusModuleOptions, > { constructor( protected readonly httpAdapterHost: HttpAdapterHost, diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index 904e4b9..981181f 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -1,4 +1,3 @@ export * from './resolve-loader.decorator'; -export * from './validation-rule.decorator'; export * from './resolve-reference-loader.decorator'; export * from './hook.decorator'; diff --git a/lib/decorators/validation-rule.decorator.ts b/lib/decorators/validation-rule.decorator.ts deleted file mode 100644 index 5672cf1..0000000 --- a/lib/decorators/validation-rule.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { SetMetadata } from '@nestjs/common'; -import { VALIDATOR_METADATA } from '../constants'; - -export function ValidationRule(): ClassDecorator { - return (target: Function) => { - SetMetadata(VALIDATOR_METADATA, true)(target); - }; -} diff --git a/lib/factories/graphql.factory.ts b/lib/factories/graphql.factory.ts index 2bdbdbb..860e714 100644 --- a/lib/factories/graphql.factory.ts +++ b/lib/factories/graphql.factory.ts @@ -12,11 +12,7 @@ import { } from '@nestjs/graphql/dist/services'; import { GraphQLSchemaBuilder } from '@nestjs/graphql/dist/graphql-schema.builder'; import { MercuriusModuleOptions } from '../interfaces'; -import { ValidationRules } from '../interfaces/base-mercurius-module-options.interface'; -import { - LoadersExplorerService, - ValidationRuleExplorerService, -} from '../services'; +import { LoadersExplorerService } from '../services'; import { transformFederatedSchema } from '../utils/faderation-factory.util'; @Injectable() @@ -31,7 +27,6 @@ export class GraphQLFactory extends NestGraphQLFactory { gqlSchemaBuilder: GraphQLSchemaBuilder, gqlSchemaHost: GraphQLSchemaHost, protected readonly loaderExplorerService: LoadersExplorerService, - protected readonly validationRuleExplorerService: ValidationRuleExplorerService, ) { super( resolversExplorerService, @@ -68,25 +63,10 @@ export class GraphQLFactory extends NestGraphQLFactory { } delete parentOptions.plugins; parentOptions.loaders = this.loaderExplorerService.explore(); - parentOptions.validationRules = this.mergeValidationRules( - options.validationRules, - ); - if (options.federationMetadata) { parentOptions.schema = transformFederatedSchema(parentOptions.schema); } return parentOptions; } - - mergeValidationRules(existingValidationRules?: ValidationRules) { - const rules = this.validationRuleExplorerService.explore(); - if (rules.length === 0 && !existingValidationRules) { - return; - } - return (params) => [ - ...(existingValidationRules ? existingValidationRules(params) : []), - ...rules.map((rule) => (context) => rule.validate(params, context)), - ]; - } } diff --git a/lib/interfaces/base-mercurius-module-options.interface.ts b/lib/interfaces/base-mercurius-module-options.interface.ts index 2e2860d..878a3ac 100644 --- a/lib/interfaces/base-mercurius-module-options.interface.ts +++ b/lib/interfaces/base-mercurius-module-options.interface.ts @@ -1,11 +1,9 @@ import { MercuriusCommonOptions } from 'mercurius'; -import { ValidationRule } from 'graphql'; export interface BaseMercuriusModuleOptions extends MercuriusCommonOptions { path?: string; useGlobalPrefix?: boolean; uploads?: boolean | FileUploadOptions; - validationRules?: ValidationRules; altair?: boolean | any; } @@ -17,9 +15,3 @@ export interface FileUploadOptions { //Max allowed number of files (default: Infinity). maxFiles?: number; } - -export type ValidationRules = (params: { - source: string; - variables?: Record; - operationName?: string; -}) => ValidationRule[]; diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index ac49d20..bcc4a22 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -1,5 +1,4 @@ export * from './mercurius-module-options.interface'; export * from './mercurius-gateway-module-options.interface'; export * from './loader.interface'; -export * from './validation-rule-host'; export * from './reference.interface'; diff --git a/lib/interfaces/validation-rule-host.ts b/lib/interfaces/validation-rule-host.ts deleted file mode 100644 index 0553510..0000000 --- a/lib/interfaces/validation-rule-host.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ASTVisitor, ValidationContext } from 'graphql'; - -export interface ValidationRuleHost { - validate( - params: { - source: string; - variables?: Record; - operationName?: string; - }, - context: ValidationContext, - ): ASTVisitor; -} diff --git a/lib/mercurius.module.ts b/lib/mercurius.module.ts index 204090d..25bb885 100644 --- a/lib/mercurius.module.ts +++ b/lib/mercurius.module.ts @@ -30,11 +30,7 @@ import { MercuriusModuleOptions, MercuriusOptionsFactory, } from './interfaces'; -import { - HookExplorerService, - LoadersExplorerService, - ValidationRuleExplorerService, -} from './services'; +import { HookExplorerService, LoadersExplorerService } from './services'; import { mergeDefaults } from './utils/merge-defaults'; import { MercuriusCoreModule } from './mercurius-core.module'; import { GraphQLFactory } from './factories/graphql.factory'; @@ -53,13 +49,13 @@ import { BaseMercuriusModule } from './base-mercurius.module'; GraphQLSchemaBuilder, GraphQLSchemaHost, LoadersExplorerService, - ValidationRuleExplorerService, HookExplorerService, ], }) export class MercuriusModule extends BaseMercuriusModule - implements OnModuleInit { + implements OnModuleInit +{ constructor( private readonly graphqlFactory: GraphQLFactory, private readonly graphqlTypesLoader: GraphQLTypesLoader, @@ -147,10 +143,10 @@ export class MercuriusModule )) || []; const mergedTypeDefs = extend(typeDefs, this.options.typeDefs); - const mercuriusOptions = ((await this.graphqlFactory.mergeOptions({ + const mercuriusOptions = (await this.graphqlFactory.mergeOptions({ ...this.options, typeDefs: mergedTypeDefs, - } as any)) as unknown) as MercuriusModuleOptions; + } as any)) as unknown as MercuriusModuleOptions; if ( this.options.definitions && diff --git a/lib/services/index.ts b/lib/services/index.ts index 45941e8..ded6a4a 100644 --- a/lib/services/index.ts +++ b/lib/services/index.ts @@ -1,5 +1,4 @@ export * from './loaders-explorer.service'; -export * from './validation-rule-explorer.service'; export * from './gql-execution-context'; export * from './gql-arguments-host'; export * from './pub-sub-host'; diff --git a/lib/services/validation-rule-explorer.service.ts b/lib/services/validation-rule-explorer.service.ts deleted file mode 100644 index 8af2004..0000000 --- a/lib/services/validation-rule-explorer.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { BaseExplorerService } from '@nestjs/graphql/dist/services'; -import { GRAPHQL_MODULE_OPTIONS } from '@nestjs/graphql/dist/graphql.constants'; -import { MercuriusModuleOptions } from '../interfaces'; -import { ModulesContainer } from '@nestjs/core/injector/modules-container'; -import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; -import { VALIDATOR_METADATA } from '../constants'; -import { ValidationRuleHost } from '../interfaces'; - -@Injectable() -export class ValidationRuleExplorerService extends BaseExplorerService { - constructor( - private readonly modulesContainer: ModulesContainer, - @Inject(GRAPHQL_MODULE_OPTIONS) - private readonly gqlOptions: MercuriusModuleOptions, - ) { - super(); - } - - explore() { - const modules = this.getModules( - this.modulesContainer, - this.gqlOptions.include || [], - ); - return this.flatMap(modules, (instance) => - this.filterValidationRules(instance), - ); - } - - filterValidationRules(wrapper: InstanceWrapper) { - const { instance } = wrapper; - if (!instance) { - return undefined; - } - const metadata = Reflect.getMetadata( - VALIDATOR_METADATA, - instance.constructor, - ); - return metadata ? instance : undefined; - } -} diff --git a/package.json b/package.json index 7dae5c3..9ff8d53 100644 --- a/package.json +++ b/package.json @@ -44,12 +44,11 @@ "eslint-plugin-import": "^2.22.1", "fastify": "^3.15.1", "graphql": "^15.5.0", - "graphql-query-complexity": "^0.8.0", "graphql-scalars": "^1.9.0", "graphql-tag": "^2.11.0", "husky": "6", "lint-staged": "^11.0.0", - "mercurius": "^7.3.0", + "mercurius": "^7.9.0", "mercurius-integration-testing": "^3.1.3", "mercurius-upload": "^1.1.1", "pinst": "^2.1.6", @@ -60,7 +59,8 @@ "supertest": "^6.1.3", "ts-node": "^9.1.1", "typescript": "^4.2.4", - "uvu": "^0.5.1" + "uvu": "^0.5.1", + "ws": "^7.4.6" }, "dependencies": { "@nestjs/graphql": "~7.10.6" @@ -73,7 +73,7 @@ "altair-fastify-plugin": "^3.2.2", "fastify": "^3.15.1", "graphql": "^15.5.0", - "mercurius": "^7.3.0", + "mercurius": "^7.7.0", "mercurius-upload": "^1.1.1", "reflect-metadata": "^0.1.13" }, diff --git a/tests/code-first/app.module.ts b/tests/code-first/app.module.ts index a870beb..720aa9e 100644 --- a/tests/code-first/app.module.ts +++ b/tests/code-first/app.module.ts @@ -13,6 +13,7 @@ import { UpperCaseDirective } from '../example/directives/upper-case.directive'; schemaDirectives: { uppercase: UpperCaseDirective, }, + subscription: true, }), ], providers: [CatService, DogService, AnimalResolver, CatResolver], diff --git a/tests/code-first/resolvers/cat.resolver.ts b/tests/code-first/resolvers/cat.resolver.ts index c464e8d..2459bdb 100644 --- a/tests/code-first/resolvers/cat.resolver.ts +++ b/tests/code-first/resolvers/cat.resolver.ts @@ -1,8 +1,17 @@ -import { Args, ID, Parent, Query, Resolver } from '@nestjs/graphql'; +import { + Args, + Context, + ID, + Parent, + Query, + Resolver, + Subscription, +} from '@nestjs/graphql'; import { Cat } from '../types/cat'; import { CatService } from '../services/cat.service'; import { ParseIntPipe } from '@nestjs/common'; -import { LoaderQuery, ResolveLoader } from '../../../lib'; +import { LoaderQuery, ResolveLoader, toAsyncIterator } from '../../../lib'; +import { PubSub } from 'mercurius'; @Resolver(() => Cat) export class CatResolver { @@ -22,4 +31,11 @@ export class CatResolver { hasFur(@Parent() queries: LoaderQuery[]) { return queries.map(({ obj }) => obj.lives > 1); } + + @Subscription(() => Cat, { + resolve: (payload) => payload, + }) + onCatSub(@Context('pubsub') pubSub: PubSub) { + return toAsyncIterator(pubSub.subscribe('CAT_SUB_TOPIC')); + } } diff --git a/tests/e2e/code-first.spec.ts b/tests/e2e/code-first.spec.ts index 0e5f05d..83d0c27 100644 --- a/tests/e2e/code-first.spec.ts +++ b/tests/e2e/code-first.spec.ts @@ -7,6 +7,9 @@ import { suite } from 'uvu'; import * as assert from 'uvu/assert'; import { Test } from '@nestjs/testing'; import { AppModule } from '../code-first/app.module'; +import * as Websocket from 'ws'; +import { FastifyInstance } from 'fastify'; +import { cats } from '../code-first/services/cat.service'; interface Context { app: NestFastifyApplication; @@ -262,4 +265,72 @@ gqlSuite( }, ); +gqlSuite('subscriber should work', async ({ app }) => { + return new Promise((resolve, reject) => { + app.listen(0, (err) => { + if (err) { + return reject(err); + } + const port = app.getHttpServer().address().port; + const fastifyApp = app.getHttpAdapter().getInstance() as FastifyInstance; + + const ws = new Websocket(`ws://localhost:${port}/graphql`, 'graphql-ws'); + + const client = Websocket.createWebSocketStream(ws, { + encoding: 'utf8', + objectMode: true, + }); + client.setEncoding('utf8'); + client.write( + JSON.stringify({ + type: 'connection_init', + }), + ); + + client.write( + JSON.stringify({ + id: 1, + type: 'start', + payload: { + query: ` + subscription { + onCatSub { + id + lives + name + hasFur + } + } + `, + }, + }), + ); + + client.on('data', (chunk) => { + const data = JSON.parse(chunk); + + if (data.type === 'connection_ack') { + fastifyApp.graphql.pubsub.publish({ + topic: 'CAT_SUB_TOPIC', + payload: cats[0], + }); + } else if (data.id === 1) { + const expectedCat = expectedCats[0]; + const receivedCat = data.payload.data?.onCatSub; + assert.ok(receivedCat); + assert.equal(expectedCat.id, receivedCat.id); + assert.type(receivedCat.hasFur, 'boolean'); + + client.end(); + } + }); + + client.on('end', () => { + client.destroy(); + app.close().then(resolve).catch(reject); + }); + }); + }); +}); + gqlSuite.run(); diff --git a/tests/example/app.module.ts b/tests/example/app.module.ts index d3ef41e..a014ebd 100644 --- a/tests/example/app.module.ts +++ b/tests/example/app.module.ts @@ -5,7 +5,6 @@ import { ImageResolver } from './resolvers/image.resolver'; import { HashScalar } from './scalars/hash.scalar'; import { JSONResolver } from 'graphql-scalars'; import { UpperCaseDirective } from './directives/upper-case.directive'; -import { ComplexityModule } from './modules/complexity/complexity.module'; import { UserModule } from './modules/user/user.module'; @Module({ @@ -27,8 +26,8 @@ import { UserModule } from './modules/user/user.module'; const value = await next(); const { info } = ctx; - const extensions = info?.parentType.getFields()[info.fieldName] - .extensions; + const extensions = + info?.parentType.getFields()[info.fieldName].extensions; //... return value; @@ -51,7 +50,6 @@ import { UserModule } from './modules/user/user.module'; }; }, }), - ComplexityModule, UserModule, ], controllers: [AppController], diff --git a/tests/example/modules/complexity/complexity.module.ts b/tests/example/modules/complexity/complexity.module.ts deleted file mode 100644 index daaaf27..0000000 --- a/tests/example/modules/complexity/complexity.module.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ConfigService } from './config.service'; -import { ComplexityValidator } from './validation/complexity.validator'; - -@Module({ - providers: [ConfigService, ComplexityValidator], -}) -export class ComplexityModule {} diff --git a/tests/example/modules/complexity/config.service.ts b/tests/example/modules/complexity/config.service.ts deleted file mode 100644 index edd40bc..0000000 --- a/tests/example/modules/complexity/config.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { GraphQLHook } from '../../../../lib'; - -@Injectable() -export class ConfigService { - get maxComplexity() { - return 100; - } - - @GraphQLHook('preParsing') - async preParse() {} -} diff --git a/tests/example/modules/complexity/validation/complexity.validator.ts b/tests/example/modules/complexity/validation/complexity.validator.ts deleted file mode 100644 index dd85e04..0000000 --- a/tests/example/modules/complexity/validation/complexity.validator.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { ASTVisitor, ValidationContext } from 'graphql'; -import QueryComplexity from 'graphql-query-complexity/dist/QueryComplexity'; -import { - fieldExtensionsEstimator, - simpleEstimator, -} from 'graphql-query-complexity'; -import { ValidationRuleHost, ValidationRule } from '../../../../../lib'; -import { ConfigService } from '../config.service'; - -@ValidationRule() -export class ComplexityValidator implements ValidationRuleHost { - constructor(private readonly configService: ConfigService) {} - - validate( - params: { - source: string; - variables?: Record; - operationName?: string; - }, - context: ValidationContext, - ): ASTVisitor { - return new QueryComplexity(context, { - maximumComplexity: this.configService.maxComplexity, - variables: params.variables, - operationName: params.operationName, - estimators: [ - fieldExtensionsEstimator(), - simpleEstimator({ defaultComplexity: 1 }), - ], - onComplete(complexity) { - console.log('Test validator complexity:', complexity); - }, - }); - } -} diff --git a/tests/example/modules/user/services/user.service.ts b/tests/example/modules/user/services/user.service.ts index b809d80..793ce44 100644 --- a/tests/example/modules/user/services/user.service.ts +++ b/tests/example/modules/user/services/user.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { UserType } from '../../../types/user.type'; import { CreateUserInput } from '../inputs/create-user.input'; -import { PubSubHost } from '../../../../../lib'; +import { GraphQLHook, PubSubHost } from '../../../../../lib'; export const users: UserType[] = [ { @@ -35,6 +35,9 @@ let nextId = 5; export class UserService { constructor(private readonly pubSubHost: PubSubHost) {} + @GraphQLHook('preParsing') + async preParse() {} + users() { return users; } diff --git a/tests/federation/userService/user.resolver.ts b/tests/federation/userService/user.resolver.ts index 448c3b4..eaba5f4 100644 --- a/tests/federation/userService/user.resolver.ts +++ b/tests/federation/userService/user.resolver.ts @@ -6,7 +6,6 @@ import { OmitType, Query, Resolver, - ResolveReference, Subscription, } from '@nestjs/graphql'; import { User } from './user'; diff --git a/tests/schema.graphql b/tests/schema.graphql index c6624bf..cc9628e 100644 --- a/tests/schema.graphql +++ b/tests/schema.graphql @@ -2,86 +2,25 @@ # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # ------------------------------------------------------ -type User implements IPerson { +type User { id: ID! - name: String - uniqueName: String! - uniqueId: String! - lastName: String! - birthDay: DateTime! - secret: Hash - meta: JSON - age: Int! - fullName(filter: String): String - posts: [PostType!]! -} - -interface IPerson { - id: ID! - name: String - uniqueName: String! - uniqueId: String! -} - -""" -A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. -""" -scalar DateTime - -"""Hash scalar""" -scalar Hash - -""" -The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). -""" -scalar JSON - -type PostType { - id: ID! - title: String! - authorId: String! - status: Status! -} - -enum Status { - PUBLISHED - DRAFT -} - -type Customer implements IPerson { - id: ID! - name: String - uniqueName: String! - uniqueId: String! + name: String! + isActive: Boolean! } type Query { users: [User!]! - user(id: ID!): User - search: [SearchUnion!]! - customers: [Customer!]! } -union SearchUnion = PostType | User - type Mutation { - uploadImage(file: Upload!): Boolean! createUser(input: CreateUserInput!): User! } -"""The `Upload` scalar type represents a file upload.""" -scalar Upload - input CreateUserInput { - name: String - lastName: String = "noone" - birthDay: DateTime! - secret: Hash - meta: JSON + name: String! + isActive: Boolean = true } type Subscription { - userAdded: User! - userAddedId: String! - specificUserAdded(id: ID!): User! + onCreateUser: User! } diff --git a/yarn.lock b/yarn.lock index 97a45b2..51d7759 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3289,13 +3289,6 @@ graphql-jit@^0.5.0: lodash.merge "4.6.2" lodash.mergewith "4.6.2" -graphql-query-complexity@^0.8.0: - version "0.8.1" - resolved "https://registry.yarnpkg.com/graphql-query-complexity/-/graphql-query-complexity-0.8.1.tgz#a08e30497b136c8d9dd425936ec92db7f26fbfe7" - integrity sha512-EGD77GBjbONFBkmvHmTO/6Ig+9E9PwtRF32vDn/MmTwVxT93MaGg8GD6e3M85hBlCkyRoJvFu+0ua1xWUKOaFw== - dependencies: - lodash.get "^4.4.2" - graphql-scalars@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/graphql-scalars/-/graphql-scalars-1.9.3.tgz#84d27e42d280a16821ab0ef59cddaac84f798575" @@ -4151,11 +4144,6 @@ lodash.clonedeep@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -4356,10 +4344,10 @@ mercurius-upload@^1.1.1: fastify-plugin "^2.3.4" graphql-upload "^11.0.0" -mercurius@^7.3.0: - version "7.6.1" - resolved "https://registry.yarnpkg.com/mercurius/-/mercurius-7.6.1.tgz#354afb09640ea7edcbe4fe3589a995c9cbf14eae" - integrity sha512-GLY6sO7OkHD85pBPQh4sPw6IxYuwRaHi0FhjV6PaqML0wrD2QyfRP8VrrGUwMLRUeW8YdQWonTtaQo+nuqBv+w== +mercurius@^7.9.0: + version "7.9.0" + resolved "https://registry.yarnpkg.com/mercurius/-/mercurius-7.9.0.tgz#62ebb7fcd443f148e8a2a0c7d4b7d689b649585c" + integrity sha512-o+PYgPx9bvwvnHJEiqyaKMO5RZhq/TFsV+CVG6a2BrG+BNSTpm3eTwVNRmvAd6F5cuBh7VniBONRWd6REKQwWw== dependencies: "@types/isomorphic-form-data" "^2.0.0" end-of-stream "^1.4.4" @@ -6515,6 +6503,11 @@ ws@^7.4.2, ws@^7.4.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.5.tgz#a484dd851e9beb6fdb420027e3885e8ce48986c1" integrity sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g== +ws@^7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + xdg-basedir@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"