diff --git a/docs/Caching.md b/docs/Caching.md index 9275efe5eb..bfda4f658e 100644 --- a/docs/Caching.md +++ b/docs/Caching.md @@ -20,7 +20,7 @@ The main option for configuring the Metro cache is [`cacheStores`](./Configurati Metro provides a number of built-in cache store implementations for use with the [`cacheStores`](./Configuration.md#cachestores) config option: * **`FileStore({root: string})`** will store cache entries as files under the directory specified by `root`. -* **`AutoCleanFileStore()`** is a `FileStore` that periodically cleans up old entries. It accepts the same options as `FileStore` plus the following: +* **`AutoCleanFileStore()`**
Deprecated
is a `FileStore` that periodically cleans up old entries. It accepts the same options as `FileStore` plus the following: * **`options.intervalMs: number`** is the time in milliseconds between cleanup attempts. Defaults to 10 minutes. * **`options.cleanupThresholdMs: number`** is the minimum time in milliseconds since the last modification of an entry before it can be deleted. Defaults to 3 days. * **`HttpStore(options)`** is a bare-bones remote cache client that reads (`GET`) and writes (`PUT`) compressed cache artifacts over HTTP or HTTPS. diff --git a/packages/metro-cache/package.json b/packages/metro-cache/package.json index de252800a7..1f1101dd47 100644 --- a/packages/metro-cache/package.json +++ b/packages/metro-cache/package.json @@ -23,7 +23,7 @@ "metro-core": "0.83.1" }, "devDependencies": { - "metro-memory-fs": "*" + "memfs": "^4.38.2" }, "license": "MIT", "engines": { diff --git a/packages/metro-cache/src/stores/AutoCleanFileStore.js b/packages/metro-cache/src/stores/AutoCleanFileStore.js index effde7df9b..cf064113ef 100644 --- a/packages/metro-cache/src/stores/AutoCleanFileStore.js +++ b/packages/metro-cache/src/stores/AutoCleanFileStore.js @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format - * @flow */ import type {Options} from './FileStore'; @@ -14,90 +14,72 @@ import FileStore from './FileStore'; import fs from 'fs'; import path from 'path'; -type CleanOptions = { +type CleanOptions = $ReadOnly<{ ...Options, intervalMs?: number, cleanupThresholdMs?: number, - ... -}; - -type FileList = { - path: string, - stats: fs.Stats, - ... -}; - -// List all files in a directory in Node.js recursively in a synchronous fashion -const walkSync = function ( - dir: string, - filelist: Array, -): Array { - const files = fs.readdirSync(dir); - filelist = filelist || []; - files.forEach(function (file) { - const fullPath = path.join(dir, file); - const stats = fs.statSync(fullPath); - if (stats.isDirectory()) { - filelist = walkSync(fullPath + path.sep, filelist); - } else { - filelist.push({path: fullPath, stats}); - } - }); - return filelist; -}; - -function get(property: ?T, defaultValue: T): T { - if (property == null) { - return defaultValue; - } - - return property; -} +}>; /** - * A FileStore that cleans itself up in a given interval + * A FileStore that, at a given interval, stats the content of the cache root + * and deletes any file last modified a set threshold in the past. + * + * @deprecated This is not efficiently implemented and may cause significant + * redundant I/O when caches are large. Prefer your own cleanup scripts, or a + * custom Metro cache that uses watches, hooks get/set, and/or implements LRU. */ export default class AutoCleanFileStore extends FileStore { - _intervalMs: number; - _cleanupThresholdMs: number; - _root: string; + +#intervalMs: number; + +#cleanupThresholdMs: number; + +#root: string; constructor(opts: CleanOptions) { super({root: opts.root}); - this._intervalMs = get(opts.intervalMs, 10 * 60 * 1000); // 10 minutes - this._cleanupThresholdMs = get( - opts.cleanupThresholdMs, - 3 * 24 * 60 * 60 * 1000, // 3 days - ); + this.#root = opts.root; + this.#intervalMs = opts.intervalMs ?? 10 * 60 * 1000; // 10 minutes + this.#cleanupThresholdMs = + opts.cleanupThresholdMs ?? 3 * 24 * 60 * 60 * 1000; // 3 days - this._scheduleCleanup(); + this.#scheduleCleanup(); } - _scheduleCleanup() { - // $FlowFixMe[method-unbinding] added when improving typing for this parameters - setTimeout(this._doCleanup.bind(this), this._intervalMs); + #scheduleCleanup() { + setTimeout(() => this.#doCleanup(), this.#intervalMs); } - _doCleanup() { - const files = walkSync(this._root, []); + #doCleanup() { + const dirents = fs.readdirSync(this.#root, { + recursive: true, + withFileTypes: true, + }); let warned = false; - files.forEach(file => { - if (file.stats.mtimeMs < Date.now() - this._cleanupThresholdMs) { + const minModifiedTime = Date.now() - this.#cleanupThresholdMs; + dirents + .filter(dirent => dirent.isFile()) + .forEach(dirent => { + const absolutePath = path.join( + // $FlowFixMe[prop-missing] - dirent.parentPath added in Node 20.12 + dirent.parentPath, + dirent.name.toString(), + ); try { - fs.unlinkSync(file.path); + if (fs.statSync(absolutePath).mtimeMs < minModifiedTime) { + fs.unlinkSync(absolutePath); + } } catch (e) { if (!warned) { console.warn( - 'Problem cleaning up cache for ' + file.path + ': ' + e.message, + 'Problem cleaning up cache for ' + + absolutePath + + ': ' + + e.message, ); warned = true; } } - } - }); - - this._scheduleCleanup(); + }); + this.#scheduleCleanup(); } } diff --git a/packages/metro-cache/src/stores/FileStore.js b/packages/metro-cache/src/stores/FileStore.js index fc37b558da..18978c7335 100644 --- a/packages/metro-cache/src/stores/FileStore.js +++ b/packages/metro-cache/src/stores/FileStore.js @@ -4,8 +4,8 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow + * @format */ import fs from 'fs'; @@ -14,20 +14,20 @@ import path from 'path'; const NULL_BYTE = 0x00; const NULL_BYTE_BUFFER = Buffer.from([NULL_BYTE]); -export type Options = { +export type Options = $ReadOnly<{ root: string, -}; +}>; export default class FileStore { - _root: string; + +#root: string; constructor(options: Options) { - this._root = options.root; + this.#root = options.root; } async get(key: Buffer): Promise { try { - const data = await fs.promises.readFile(this._getFilePath(key)); + const data = await fs.promises.readFile(this.#getFilePath(key)); if (data[0] === NULL_BYTE) { return (data.slice(1): any); @@ -44,20 +44,20 @@ export default class FileStore { } async set(key: Buffer, value: T): Promise { - const filePath = this._getFilePath(key); + const filePath = this.#getFilePath(key); try { - await this._set(filePath, value); + await this.#set(filePath, value); } catch (err) { if (err.code === 'ENOENT') { fs.mkdirSync(path.dirname(filePath), {recursive: true}); - await this._set(filePath, value); + await this.#set(filePath, value); } else { throw err; } } } - async _set(filePath: string, value: T): Promise { + async #set(filePath: string, value: T): Promise { let content; if (value instanceof Buffer) { content = Buffer.concat([NULL_BYTE_BUFFER, value]); @@ -68,20 +68,20 @@ export default class FileStore { } clear() { - this._removeDirs(); + this.#removeDirs(); } - _getFilePath(key: Buffer): string { + #getFilePath(key: Buffer): string { return path.join( - this._root, + this.#root, key.slice(0, 1).toString('hex'), key.slice(1).toString('hex'), ); } - _removeDirs() { + #removeDirs() { for (let i = 0; i < 256; i++) { - fs.rmSync(path.join(this._root, ('0' + i.toString(16)).slice(-2)), { + fs.rmSync(path.join(this.#root, ('0' + i.toString(16)).slice(-2)), { force: true, recursive: true, }); diff --git a/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js b/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js index af314226b8..c4a9380ee2 100644 --- a/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js +++ b/packages/metro-cache/src/stores/__tests__/AutoCleanFileStore-test.js @@ -9,7 +9,7 @@ * @oncall react_native */ -'use strict'; +import {memfs} from 'memfs'; describe('AutoCleanFileStore', () => { let AutoCleanFileStore; @@ -19,22 +19,23 @@ describe('AutoCleanFileStore', () => { jest .resetModules() .resetAllMocks() - .mock('fs', () => new (require('metro-memory-fs'))()); - + .mock('fs', () => memfs().fs); AutoCleanFileStore = require('../AutoCleanFileStore').default; fs = require('fs'); + jest.spyOn(fs, 'statSync'); jest.spyOn(fs, 'unlinkSync'); }); test('sets and writes into the cache', async () => { - // $FlowFixMe[underconstrained-implicit-instantiation] - const fileStore = new AutoCleanFileStore({ + const fileStore = new AutoCleanFileStore({ root: '/root', intervalMs: 49, - cleanupThresholdMs: 0, + cleanupThresholdMs: 90, }); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); + expect(fs.statSync).toHaveBeenCalledTimes(0); + await fileStore.set(cache, {foo: 42}); expect(await fileStore.get(cache)).toEqual({foo: 42}); @@ -43,17 +44,28 @@ describe('AutoCleanFileStore', () => { expect(await fileStore.get(cache)).toEqual({foo: 42}); + // And there should have been no cleanup + expect(fs.statSync).not.toHaveBeenCalled(); + // Run to 50ms so that we've exceeded the 49ms cleanup interval jest.advanceTimersByTime(20); - // mtime doesn't work very well in in-memory-store, so we couldn't test that - // functionality + expect(fs.statSync).toHaveBeenCalledTimes(1); + + // At 50ms we should have checked the file, but it's still fresh enough + expect(await fileStore.get(cache)).toEqual({foo: 42}); + expect(fs.unlinkSync).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(50); + + // After another 50ms, we should have checked the file again and deleted it + expect(fs.statSync).toHaveBeenCalledTimes(2); + expect(fs.unlinkSync).toHaveBeenCalledTimes(1); expect(await fileStore.get(cache)).toEqual(null); }); test('returns null when reading a non-existing file', async () => { - // $FlowFixMe[underconstrained-implicit-instantiation] - const fileStore = new AutoCleanFileStore({root: '/root'}); + const fileStore = new AutoCleanFileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); expect(await fileStore.get(cache)).toEqual(null); diff --git a/packages/metro-cache/src/stores/__tests__/FileStore-test.js b/packages/metro-cache/src/stores/__tests__/FileStore-test.js index 027e7d3eb4..5f7a70a420 100644 --- a/packages/metro-cache/src/stores/__tests__/FileStore-test.js +++ b/packages/metro-cache/src/stores/__tests__/FileStore-test.js @@ -4,13 +4,12 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @flow strict-local * @format * @oncall react_native */ -'use strict'; - -const {dirname} = require('path'); +import {memfs} from 'memfs'; describe('FileStore', () => { let FileStore; @@ -20,7 +19,7 @@ describe('FileStore', () => { jest .resetModules() .resetAllMocks() - .mock('fs', () => new (require('metro-memory-fs'))()); + .mock('fs', () => memfs().fs); FileStore = require('../FileStore').default; fs = require('fs'); @@ -28,7 +27,7 @@ describe('FileStore', () => { }); test('sets and writes into the cache', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); await fileStore.set(cache, {foo: 42}); @@ -36,23 +35,22 @@ describe('FileStore', () => { }); test('returns null when reading a non-existing file', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); expect(await fileStore.get(cache)).toEqual(null); }); test('returns null when reading a empty file', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); - const filePath = fileStore._getFilePath(cache); - fs.mkdirSync(dirname(filePath), {recursive: true}); - fs.writeFileSync(filePath, ''); + jest.spyOn(fs.promises, 'readFile').mockImplementation(async () => ''); expect(await fileStore.get(cache)).toEqual(null); + expect(fs.promises.readFile).toHaveBeenCalledWith(expect.any(String)); }); test('writes into cache if folder is missing', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); const data = Buffer.from([0xca, 0xc4, 0xe5]); @@ -62,7 +60,7 @@ describe('FileStore', () => { }); test('reads and writes binary data', async () => { - const fileStore = new FileStore({root: '/root'}); + const fileStore = new FileStore({root: '/root'}); const cache = Buffer.from([0xfa, 0xce, 0xb0, 0x0c]); const data = Buffer.from([0xca, 0xc4, 0xe5]); diff --git a/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts b/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts index 693b81a6f4..2d78a5c5d7 100644 --- a/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts +++ b/packages/metro-cache/types/stores/AutoCleanFileStore.d.ts @@ -10,4 +10,12 @@ import type FileStore from './FileStore'; +/** + * A FileStore that, at a given interval, stats the content of the cache root + * and deletes any file last modified a set threshold in the past. + * + * @deprecated This is not efficiently implemented and may cause significant + * redundant I/O when caches are large. Prefer your own cleanup scripts, or a + * custom Metro cache that uses watches, hooks get/set, and/or implements LRU. + */ export default class AutoCleanFileStore extends FileStore {} diff --git a/scripts/jestFilter.js b/scripts/jestFilter.js index 1e295fcce7..bd3de0aef9 100644 --- a/scripts/jestFilter.js +++ b/scripts/jestFilter.js @@ -26,7 +26,6 @@ const BROKEN_ON_WINDOWS = [ 'packages/metro-file-map/src/crawlers/__tests__/node-test.js', // resolveModulePath failed - 'packages/metro-cache/src/stores/__tests__/FileStore-test.js', 'packages/metro-resolver/src/__tests__/assets-test.js', 'packages/metro-resolver/src/__tests__/platform-extensions-test.js', 'packages/metro-resolver/src/__tests__/symlinks-test.js', diff --git a/yarn.lock b/yarn.lock index 3e7f8dd099..7abf2311d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1112,6 +1112,50 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jsonjoy.com/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/buffers@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz#ade6895b7d3883d70f87b5743efaa12c71dfef7a" + integrity sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q== + +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + +"@jsonjoy.com/json-pack@^1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.11.0.tgz#3d40d3d8042f5e9eeb005658a76b788e8ca84ac0" + integrity sha512-nLqSTAYwpk+5ZQIoVp7pfd/oSKNWlEdvTq2LzVA4r2wtWZg6v+5u0VgBOaDJuUfNOuw/4Ysq6glN5QKSrOCgrA== + dependencies: + "@jsonjoy.com/base64" "^1.1.2" + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/json-pointer" "^1.0.1" + "@jsonjoy.com/util" "^1.9.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + +"@jsonjoy.com/json-pointer@^1.0.1": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408" + integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + dependencies: + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/util" "^1.9.0" + +"@jsonjoy.com/util@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46" + integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + dependencies: + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2940,6 +2984,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-to-regex.js@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz#f71cc9cb8441471a9318626160bc8a35e1306b21" + integrity sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg== + glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -3104,6 +3153,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -4194,6 +4248,18 @@ makeerror@1.0.x: dependencies: tmpl "1.0.x" +memfs@^4.38.2: + version "4.38.2" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.38.2.tgz#e3a3a0362032c3ab7093cc7c179bd5fa8abc94c3" + integrity sha512-FpWsVHpAkoSh/LfY1BgAl72BVd374ooMRtDi2VqzBycX4XEfvC0XKACCe0C9VRZoYq5viuoyTv6lYXZ/Q7TrLQ== + dependencies: + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.0.1" + thingies "^2.5.0" + tree-dump "^1.0.3" + tslib "^2.0.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5206,6 +5272,11 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thingies@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.5.0.tgz#5f7b882c933b85989f8466b528a6247a6881e04f" + integrity sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw== + throat@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" @@ -5238,6 +5309,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +tree-dump@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" + integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" @@ -5253,6 +5329,11 @@ tsconfig-paths@^3.14.1: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^2.0.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.0.1: version "2.6.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"