From 443b107223a2eea1049970e7baef574e1c3dcede Mon Sep 17 00:00:00 2001 From: "Valentin D. Pinkman" Date: Wed, 24 Jan 2024 15:43:35 +0100 Subject: [PATCH] chore: add purify-ts in the template module --- packages/core/jest.config.ts | 1 + packages/core/package.json | 5 +- packages/core/src/di.ts | 5 +- .../internal/config/data/ConfigDataSource.ts | 6 +- packages/core/src/internal/config/data/Dto.ts | 2 +- .../config/data/LocalConfigDataSource.stub.ts | 7 +- .../config/data/LocalConfigDataSource.test.ts | 47 ++++-- .../config/data/LocalConfigDataSource.ts | 30 +++- .../data/RemoteConfigDataSource.stub.ts | 13 +- .../data/RemoteConfigDataSource.test.ts | 146 ++++++++++++++++-- .../config/data/RemoteConfigDataSource.ts | 83 +++++++--- .../src/internal/config/di/configTypes.ts | 39 +++++ .../service/DefaultConfigService.test.ts | 36 +++-- .../config/service/DefaultConfigService.ts | 22 ++- .../usecase/GetSdkVersionUseCase.test.ts | 4 +- pnpm-lock.yaml | 10 +- 16 files changed, 372 insertions(+), 84 deletions(-) diff --git a/packages/core/jest.config.ts b/packages/core/jest.config.ts index e96a4dbad..c7a18213d 100644 --- a/packages/core/jest.config.ts +++ b/packages/core/jest.config.ts @@ -3,6 +3,7 @@ import type { JestConfigWithTsJest } from "@ledgerhq/jest-config-dsdk"; const config: JestConfigWithTsJest = { preset: "@ledgerhq/jest-config-dsdk", setupFiles: ["/jest.setup.ts"], + testPathIgnorePatterns: ["/lib/"], collectCoverageFrom: [ // TODO: remove internal when the rest of the files are setup "src/internal/**/*.ts", diff --git a/packages/core/package.json b/packages/core/package.json index d41b3a187..86cbec626 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,13 +12,16 @@ "dev": "tsc --watch", "lint": "eslint --cache --ext .ts \"src\"", "lint:fix": "eslint --cache --fix --ext .ts \"src\"", - "test:unit": "jest --coverage src", + "test": "jest src", + "test:watch": "pnpm test -- --watch", + "test:coverage": "pnpm test -- --coverage", "module:init": "zx scripts/add-module.mjs", "run:index": "ts-node src/index.ts" }, "dependencies": { "inversify": "^6.0.2", "inversify-logger-middleware": "^3.1.0", + "purify-ts": "^2.0.3", "reflect-metadata": "^0.2.1" }, "devDependencies": { diff --git a/packages/core/src/di.ts b/packages/core/src/di.ts index 4df17acf9..6fd46ab40 100644 --- a/packages/core/src/di.ts +++ b/packages/core/src/di.ts @@ -12,7 +12,10 @@ export const makeContainer = ({ }: Partial = {}) => { const container = new Container(); container.applyMiddleware(logger); - container.load(configModuleFactory({ mock })); + container.load( + configModuleFactory({ mock }) + // modules go here + ); return container; }; diff --git a/packages/core/src/internal/config/data/ConfigDataSource.ts b/packages/core/src/internal/config/data/ConfigDataSource.ts index 1faf0389a..58f8a3630 100644 --- a/packages/core/src/internal/config/data/ConfigDataSource.ts +++ b/packages/core/src/internal/config/data/ConfigDataSource.ts @@ -1,11 +1,13 @@ +import { Either } from "purify-ts"; +import { LocalConfigFailure, RemoteConfigFailure } from "../di/configTypes"; import { Config } from "../model/Config"; // Describe the different data sources interfaces our application could have export interface LocalConfigDataSource { - getConfig(): Config; + getConfig(): Either; } export interface RemoteConfigDataSource { - getConfig(): Promise; + getConfig(): Promise>; } diff --git a/packages/core/src/internal/config/data/Dto.ts b/packages/core/src/internal/config/data/Dto.ts index 8869cbe7d..328b5e045 100644 --- a/packages/core/src/internal/config/data/Dto.ts +++ b/packages/core/src/internal/config/data/Dto.ts @@ -1,6 +1,6 @@ // Types used by the data sources (here an example for the remote data source) // They will usually be the response of an API call that will need to be parsed -// into an object used by the application. (in our example: DTOConfig => Config) +// into an object used by the application. (in our example: ConfigDto => Config) export type ConfigDto = { version: string; diff --git a/packages/core/src/internal/config/data/LocalConfigDataSource.stub.ts b/packages/core/src/internal/config/data/LocalConfigDataSource.stub.ts index 51659fc48..8c18bfe09 100644 --- a/packages/core/src/internal/config/data/LocalConfigDataSource.stub.ts +++ b/packages/core/src/internal/config/data/LocalConfigDataSource.stub.ts @@ -1,6 +1,7 @@ import { injectable } from "inversify"; import { Config } from "../model/Config"; import { LocalConfigDataSource } from "./ConfigDataSource"; +import { Either } from "purify-ts"; /** * @@ -10,10 +11,10 @@ import { LocalConfigDataSource } from "./ConfigDataSource"; @injectable() export class StubLocalConfigDataSource implements LocalConfigDataSource { - getConfig(): Config { - return { + getConfig(): Either { + return Either.of({ name: "DeviceSDK", version: "0.0.0-mock.1", - }; + }); } } diff --git a/packages/core/src/internal/config/data/LocalConfigDataSource.test.ts b/packages/core/src/internal/config/data/LocalConfigDataSource.test.ts index 59485444f..21b3d1482 100644 --- a/packages/core/src/internal/config/data/LocalConfigDataSource.test.ts +++ b/packages/core/src/internal/config/data/LocalConfigDataSource.test.ts @@ -1,43 +1,64 @@ import fs from "fs"; import { LocalConfigDataSource } from "./ConfigDataSource"; import { FileLocalConfigDataSource } from "./LocalConfigDataSource"; -import { StubLocalConfigDataSource } from "./LocalConfigDataSource.stub"; +import { Either, Left } from "purify-ts"; +import { JSONParseError, ReadFileError } from "../di/configTypes"; const readFileSyncSpy = jest.spyOn(fs, "readFileSync"); +const jsonParse = jest.spyOn(JSON, "parse"); let datasource: LocalConfigDataSource; describe("LocalConfigDataSource", () => { describe("FileLocalConfigDataSource", () => { beforeEach(() => { readFileSyncSpy.mockClear(); + jsonParse.mockClear(); datasource = new FileLocalConfigDataSource(); + }); + + it("should return an Either", () => { readFileSyncSpy.mockReturnValue( JSON.stringify({ name: "DeviceSDK", version: "0.0.0-spied.1" }) ); - }); - it("should return the config", () => { - expect(datasource.getConfig()).toEqual({ + jsonParse.mockReturnValue({ name: "DeviceSDK", version: "0.0.0-spied.1", }); + + expect(datasource.getConfig()).toStrictEqual( + Either.of({ + name: "DeviceSDK", + version: "0.0.0-spied.1", + }) + ); }); - }); - describe("StubLocalConfigDataSource", () => { - beforeEach(() => { - datasource = new StubLocalConfigDataSource(); + it("should return an Either if readFileSync throws", () => { + const err = new Error("readFileSync error"); + readFileSyncSpy.mockImplementation(() => { + throw err; + }); + + expect(datasource.getConfig()).toEqual(Left(new ReadFileError(err))); }); - it("should return the config", () => { - expect(datasource.getConfig()).toEqual({ - name: "DeviceSDK", - version: "0.0.0-mock.1", + it("should return an Either if JSON.parse throws", () => { + const err = new Error("JSON.parse error"); + readFileSyncSpy.mockReturnValue( + JSON.stringify({ name: "DeviceSDK", version: "0.0.0-spied.1" }) + ); + + jsonParse.mockImplementation(() => { + throw err; }); + + expect(datasource.getConfig()).toEqual(Left(new JSONParseError(err))); }); }); afterAll(() => { - readFileSyncSpy.mockClear(); + readFileSyncSpy.mockRestore(); + jsonParse.mockRestore(); }); }); diff --git a/packages/core/src/internal/config/data/LocalConfigDataSource.ts b/packages/core/src/internal/config/data/LocalConfigDataSource.ts index 89726302d..150a1de97 100644 --- a/packages/core/src/internal/config/data/LocalConfigDataSource.ts +++ b/packages/core/src/internal/config/data/LocalConfigDataSource.ts @@ -1,8 +1,14 @@ import fs from "fs"; import { injectable } from "inversify"; +import path from "path"; +import { Either } from "purify-ts"; +import { + ReadFileError, + JSONParseError, + LocalConfigFailure, +} from "../di/configTypes"; import { Config } from "../model/Config"; import { LocalConfigDataSource } from "./ConfigDataSource"; -import path from "path"; /** * @@ -12,11 +18,21 @@ import path from "path"; */ @injectable() export class FileLocalConfigDataSource implements LocalConfigDataSource { - getConfig(): Config { - const version = fs.readFileSync( - path.join(__dirname, "version.json"), - "utf-8" - ); - return JSON.parse(version) as Config; + getConfig(): Either { + return Either.encase(() => + fs.readFileSync(path.join(__dirname, "version.json"), "utf-8") + ) + .mapLeft((error) => { + console.log("readFileSync error"); + return new ReadFileError(error); + }) + .chain((str) => { + return Either.encase(() => JSON.parse(str) as Config).mapLeft( + (error) => { + console.log("JSON.parse error"); + return new JSONParseError(error); + } + ); + }); } } diff --git a/packages/core/src/internal/config/data/RemoteConfigDataSource.stub.ts b/packages/core/src/internal/config/data/RemoteConfigDataSource.stub.ts index 448b9f3c6..0b9732f85 100644 --- a/packages/core/src/internal/config/data/RemoteConfigDataSource.stub.ts +++ b/packages/core/src/internal/config/data/RemoteConfigDataSource.stub.ts @@ -1,6 +1,7 @@ import { injectable } from "inversify"; import { RemoteConfigDataSource } from "./ConfigDataSource"; import { Config } from "../model/Config"; +import { Either } from "purify-ts"; /** * class RemoteRestConfigDataSource @@ -8,12 +9,14 @@ import { Config } from "../model/Config"; */ @injectable() export class StubRemoteConfigDataSource implements RemoteConfigDataSource { - async getConfig(): Promise { + async getConfig(): Promise> { return new Promise((res) => - res({ - name: "DeviceSDK", - version: "0.0.0-fake.2", - }) + res( + Either.of({ + name: "DeviceSDK", + version: "0.0.0-fake.2", + }) + ) ); } } diff --git a/packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts b/packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts index fb70ae21a..477718e07 100644 --- a/packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts +++ b/packages/core/src/internal/config/data/RemoteConfigDataSource.test.ts @@ -1,30 +1,148 @@ +import { Either, Left } from "purify-ts"; import { RemoteConfigDataSource } from "./ConfigDataSource"; import { RestRemoteConfigDataSource } from "./RemoteConfigDataSource"; -import { StubRemoteConfigDataSource } from "./RemoteConfigDataSource.stub"; +import { + ApiCallError, + JSONParseError, + ParseResponseError, +} from "../di/configTypes"; let datasource: RemoteConfigDataSource; +const callApiSpy = jest.spyOn( + RestRemoteConfigDataSource.prototype as any, + "_callApi" +); +const parseResponseSpy = jest.spyOn( + RestRemoteConfigDataSource.prototype as any, + "_parseResponse" +); + describe("RemoteRestConfigDataSource", () => { describe("RestRemoteConfigDataSource", () => { - beforeAll(() => { + beforeEach(() => { + callApiSpy.mockClear(); + parseResponseSpy.mockClear(); datasource = new RestRemoteConfigDataSource(); }); - it("should return the config", async () => { - expect(await datasource.getConfig()).toStrictEqual({ - name: "DeviceSDK", - version: "0.0.0-fake.1", + it("should return an Either", async () => { + callApiSpy.mockResolvedValue( + Either.of({ + ok: true, + json: () => + Promise.resolve( + Either.of({ name: "DeviceSDK", version: "0.0.0-fake.1" }) + ), + }) + ); + + parseResponseSpy.mockReturnValue( + Either.of({ + name: "DeviceSDK", + version: "0.0.0-fake.1", + }) + ); + + expect(await datasource.getConfig()).toStrictEqual( + Either.of({ + name: "DeviceSDK", + version: "0.0.0-fake.1", + }) + ); + }); + + it("should return an Either if _callApi throws", async () => { + const err = new Error("_callApi error"); + callApiSpy.mockResolvedValue(Left(err)); + + expect(await datasource.getConfig()).toStrictEqual( + Left(new ApiCallError(err)) + ); + }); + + it("should return an Either if _callApi returns a non-ok response", async () => { + callApiSpy.mockResolvedValue( + Either.of({ + ok: false, + json: () => + Promise.resolve( + Either.of({ name: "DeviceSDK", version: "0.0.0-fake.1" }) + ), + }) + ); + + expect(await datasource.getConfig()).toStrictEqual( + Left(new ApiCallError(new Error("response not ok"))) + ); + }); + + it("should return an Either if deserializing json fails", async () => { + const err = new Error("deserializing json failure"); + callApiSpy.mockResolvedValue( + Either.of({ + ok: true, + json: () => Promise.resolve(Left(err)), + }) + ); + + expect(await datasource.getConfig()).toStrictEqual( + Left(new JSONParseError()) + ); + }); + + it("should return an Either if _parseResponse throws", async () => { + callApiSpy.mockResolvedValue( + Either.of({ + ok: true, + json: () => + Promise.resolve( + Either.of({ name: "DeviceSDK", version: "0.0.0-fake.1" }) + ), + }) + ); + + parseResponseSpy.mockImplementation(() => { + return Left(new ParseResponseError()); }); + + expect(await datasource.getConfig()).toStrictEqual( + Left(new ParseResponseError()) + ); }); - }); - describe("MockRemoteConfigDataSource", () => { - beforeAll(() => { - datasource = new StubRemoteConfigDataSource(); + + it("should return an Either if `name` is missing in Dto", async () => { + parseResponseSpy.mockRestore(); + callApiSpy.mockResolvedValue( + Either.of({ + ok: true, + json: () => + Promise.resolve( + Either.of({ + version: "0.0.0-fake.1", + yolo: "yolo", + }) + ), + }) + ); + + expect(await datasource.getConfig()).toStrictEqual( + Left(new ParseResponseError()) + ); }); - it("should return the config", async () => { - expect(await datasource.getConfig()).toStrictEqual({ - name: "DeviceSDK", - version: "0.0.0-fake.2", + describe("without private methods spy", () => { + beforeEach(() => { + callApiSpy.mockRestore(); + parseResponseSpy.mockRestore(); + }); + + it("should return an Either", async () => { + expect(await datasource.getConfig()).toStrictEqual( + Either.of({ + name: "DeviceSDK", + version: "0.0.0-fake.1", + }) + ); }); }); }); diff --git a/packages/core/src/internal/config/data/RemoteConfigDataSource.ts b/packages/core/src/internal/config/data/RemoteConfigDataSource.ts index 06072aa99..88797ce88 100644 --- a/packages/core/src/internal/config/data/RemoteConfigDataSource.ts +++ b/packages/core/src/internal/config/data/RemoteConfigDataSource.ts @@ -2,6 +2,13 @@ import { injectable } from "inversify"; import { RemoteConfigDataSource } from "./ConfigDataSource"; import { ConfigDto } from "./Dto"; import { Config } from "../model/Config"; +import { + ApiCallError, + JSONParseError, + ParseResponseError, + RemoteConfigFailure, +} from "../di/configTypes"; +import { Either, Left } from "purify-ts"; /** * class RemoteRestConfigDataSource @@ -9,31 +16,65 @@ import { Config } from "../model/Config"; */ @injectable() export class RestRemoteConfigDataSource implements RemoteConfigDataSource { - async getConfig() { - // Fake API call - const v = await new Promise<{ json: () => Promise }>( - (resolve) => { - resolve({ - json: async () => - new Promise((res) => - res({ - name: "DeviceSDK", - version: "0.0.0-fake.1", - yolo: "yolo", - }) - ), - }); - } - ); - const json = await v.json(); - const config = this._parseResponse(json); - return config; + async getConfig(): Promise> { + const call = await this._callApi(); + if (call.isLeft()) { + console.error("ApiCallError"); + return Left(new ApiCallError(call.extract())); + } + + if (!call.extract().ok) { + console.error("ApiCallError"); + return Left(new ApiCallError(new Error("response not ok"))); + } + + const json = await call.extract().json(); + if (json.isLeft()) { + console.error("JSONParseError"); + return Left(new JSONParseError()); + } + + return json + .chain((dto) => this._parseResponse(dto)) + .map((config) => config); } // Parser for the Dto // parserResponse: ConfigDto => Config - private _parseResponse(dto: ConfigDto): Config { + private _parseResponse(dto: ConfigDto): Either { const { name, version } = dto; - return { name, version }; + if (!name || !version) { + console.log("missing stuff"); + return Left(new ParseResponseError()); + } + return Either.of({ name, version }); + } + + private _callApi(): Promise< + Either< + never, + { + ok: boolean; + json: () => Promise>; + } + > + > { + return new Promise((res) => { + res( + Either.of({ + ok: true, + json: async () => + new Promise((r) => { + r( + Either.of({ + name: "DeviceSDK", + version: "0.0.0-fake.1", + yolo: "yolo", + }) + ); + }), + }) + ); + }); } } diff --git a/packages/core/src/internal/config/di/configTypes.ts b/packages/core/src/internal/config/di/configTypes.ts index a0de0a651..c91af94fa 100644 --- a/packages/core/src/internal/config/di/configTypes.ts +++ b/packages/core/src/internal/config/di/configTypes.ts @@ -4,3 +4,42 @@ export const types = { ConfigService: Symbol.for("ConfigService"), GetSdkVersionUseCase: Symbol.for("GetSdkVersionUseCase"), }; + +export class ApiCallError { + readonly _tag = "ApiCallError"; + originalError?: Error; + constructor(readonly err?: Error) { + this.originalError = err; + } +} + +export class ParseResponseError { + readonly _tag = "ParseResponseError"; + originalError?: Error; + constructor(readonly err?: Error) { + this.originalError = err; + } +} + +export class JSONParseError { + readonly _tag = "JSONParseError"; + originalError?: Error; + constructor(readonly err?: Error) { + this.originalError = err; + } +} + +export type RemoteConfigFailure = + | ApiCallError + | ParseResponseError + | JSONParseError; + +export class ReadFileError { + readonly _tag = "ReadFileError"; + originalError?: Error; + constructor(readonly err?: Error) { + this.originalError = err; + } +} + +export type LocalConfigFailure = JSONParseError | ReadFileError; diff --git a/packages/core/src/internal/config/service/DefaultConfigService.test.ts b/packages/core/src/internal/config/service/DefaultConfigService.test.ts index e7628c35f..c42a4a0ee 100644 --- a/packages/core/src/internal/config/service/DefaultConfigService.test.ts +++ b/packages/core/src/internal/config/service/DefaultConfigService.test.ts @@ -1,5 +1,7 @@ +import { Either, Left } from "purify-ts"; import { ConfigService } from "./ConfigService"; import { DefaultConfigService } from "./DefaultConfigService"; +import { JSONParseError } from "../di/configTypes"; const localDataSource = { getConfig: jest.fn(), @@ -20,10 +22,12 @@ describe("DefaultConfigService", () => { describe("when the local config is available", () => { it("should return the `local` version", async () => { - localDataSource.getConfig.mockReturnValue({ - name: "DeviceSDK", - version: "1.0.0-local", - }); + localDataSource.getConfig.mockReturnValue( + Either.of({ + name: "DeviceSDK", + version: "1.0.0-local", + }) + ); expect(await service.getSdkConfig()).toStrictEqual({ name: "DeviceSDK", @@ -32,17 +36,31 @@ describe("DefaultConfigService", () => { }); }); - describe("when the local config is not available", () => { + describe("when the local config is not available, use remote", () => { it("should return the `remote` version", async () => { - localDataSource.getConfig.mockReturnValue(""); - remoteDataSource.getConfig.mockResolvedValue({ + localDataSource.getConfig.mockReturnValue(Left(new JSONParseError())); + remoteDataSource.getConfig.mockResolvedValue( + Either.of({ + name: "DeviceSDK", + version: "1.0.0-remote", + }) + ); + + expect(await service.getSdkConfig()).toStrictEqual({ name: "DeviceSDK", version: "1.0.0-remote", }); + }); + }); + + describe("when the local remote config are not available", () => { + it("should return the `default` version", async () => { + localDataSource.getConfig.mockReturnValue(Left(new JSONParseError())); + remoteDataSource.getConfig.mockResolvedValue(Left(new JSONParseError())); expect(await service.getSdkConfig()).toStrictEqual({ - name: "DeviceSDK", - version: "1.0.0-remote", + name: "DeadSdk", + version: "0.0.0-dead.1", }); }); }); diff --git a/packages/core/src/internal/config/service/DefaultConfigService.ts b/packages/core/src/internal/config/service/DefaultConfigService.ts index 59256fbfd..f5bda0f6a 100644 --- a/packages/core/src/internal/config/service/DefaultConfigService.ts +++ b/packages/core/src/internal/config/service/DefaultConfigService.ts @@ -20,11 +20,25 @@ export class DefaultConfigService implements ConfigService { } async getSdkConfig(): Promise { - const localConfig = this._local.getConfig(); - if (localConfig?.version) { - return this._local.getConfig(); + // Returns an Either + const localConfig = this._local.getConfig().ifLeft((err) => { + console.error("Local config not available"); + console.error(err); + }); + + if (localConfig.isRight()) { + return localConfig.extract(); } - return this._remote.getConfig().then((config) => config); + return this._remote.getConfig().then((config) => { + return config + .mapLeft((err) => { + // Here we handle the error and return a default value + console.error("Remote config not available"); + console.error(err); + return { name: "DeadSdk", version: "0.0.0-dead.1" }; + }) + .extract(); + }); } } diff --git a/packages/core/src/internal/config/usecase/GetSdkVersionUseCase.test.ts b/packages/core/src/internal/config/usecase/GetSdkVersionUseCase.test.ts index a6cfe70a5..d16f097ac 100644 --- a/packages/core/src/internal/config/usecase/GetSdkVersionUseCase.test.ts +++ b/packages/core/src/internal/config/usecase/GetSdkVersionUseCase.test.ts @@ -16,8 +16,8 @@ describe("GetSdkVersionUseCase", () => { it("should return the sdk version", async () => { getSdkConfigMock.mockResolvedValue({ name: "DeviceSDK", - version: "1.0.0-mock.1", + version: "1.0.0", }); - expect(await usecase.getSdkVersion()).toBe("1.0.0-mock.1"); + expect(await usecase.getSdkVersion()).toBe("1.0.0"); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2d988118..f22db0126 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: inversify-logger-middleware: specifier: ^3.1.0 version: 3.1.0 + purify-ts: + specifier: ^2.0.3 + version: 2.0.3 reflect-metadata: specifier: ^0.2.1 version: 0.2.1 @@ -1135,7 +1138,6 @@ packages: /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - dev: true /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -4913,6 +4915,12 @@ packages: resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} dev: true + /purify-ts@2.0.3: + resolution: {integrity: sha512-RiPOlX4L+eggnbEdwGV34t7iRSPK5d37nKPZXSu8G5mTUhxbEjPpThRFuEV4GL/T6zEJQ+ZeiuNoBk61VJvszg==} + dependencies: + '@types/json-schema': 7.0.15 + dev: false + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true