diff --git a/.eslintrc b/.eslintrc index 3d022cc..0b06fb1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,7 +15,9 @@ "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/camelcase": "off", "no-bitwise": "off", + "eqeqeq": ["error", "always"], "no-restricted-syntax": "off", + "no-underscore-dangle": "off", "func-names": "off", "no-return-assign": "off", "generator-star-spacing": "off", @@ -39,6 +41,7 @@ } ], "padding-line-between-statements": "off", + "default-param-last": "off", "implicit-arrow-linebreak": "off", "no-plusplus": "off", "max-classes-per-file": "off", @@ -50,6 +53,7 @@ "function-paren-newline": "off", "no-param-reassign": "off", "no-shadow": "warn", + "no-eval": "warn", "consistent-return": "off", "prettier/prettier": "error", "@typescript-eslint/explicit-function-return-type": "off", @@ -66,6 +70,7 @@ "react/sort-comp": "off", "react/no-array-index-key": "off", "react-hooks/exhaustive-deps": "warn", + "react-hooks/rules-of-hooks": "off", "import/prefer-default-export": "off", "import/order": [ "error", diff --git a/package.json b/package.json index d29c1e5..362fad4 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-import": "2.27.5", "eslint-plugin-jest": "27.2.2", - "eslint-plugin-prettier": "4.2.1", + "eslint-plugin-prettier": "5.0.0", "eslint-plugin-react": "7.32.2", "eslint-plugin-react-hooks": "4.6.0", "husky": "8.0.3", diff --git a/packages/perftool/lib/api/external.ts b/packages/perftool/lib/api/external.ts new file mode 100644 index 0000000..4125d64 --- /dev/null +++ b/packages/perftool/lib/api/external.ts @@ -0,0 +1,3 @@ +import type { InterceptParams } from './intercept'; + +export const intercept = async (params: InterceptParams): Promise => window._perftool_intercept?.(params); diff --git a/packages/perftool/lib/api/intercept.ts b/packages/perftool/lib/api/intercept.ts new file mode 100644 index 0000000..a3532be --- /dev/null +++ b/packages/perftool/lib/api/intercept.ts @@ -0,0 +1,152 @@ +import { Page, HTTPRequest } from 'puppeteer'; +import { minimatch } from 'minimatch'; +import mime from 'mime'; +import { LRUCache } from 'lru-cache'; +import fsPromises from 'fs/promises'; +import path from 'path'; + +import { JSONSerializable } from '../utils/types'; +import { debug, error } from '../utils/logger'; +import CWD from '../utils/cwd'; + +type Method = string; + +const fileCache = new LRUCache({ + maxSize: 100 * 1024 ** 2, + sizeCalculation: (value: Buffer) => { + return value.length; + }, +}); + +export const enum FakeResponseType { + JSON = 'json', + File = 'file', + Abort = 'abort', +} + +export type InterceptParams = { + method?: Method; + source: string; +} & ( + | { responseType: FakeResponseType.Abort; response: undefined } + | { responseType: FakeResponseType.File; response: string } + | { responseType?: FakeResponseType.JSON; response: JSONSerializable } +); + +type FakeResponse = { mimeType: string; data: string | Buffer }; + +const ANY_METHOD = 'ANY'; + +export async function useInterceptApi(page: Page): Promise { + const requestReplacementByMethodMap: Map> = new Map(); + + function getRequestReplacement(url: string, method: Method): FakeResponse | null | void { + if (!requestReplacementByMethodMap.has(method)) { + return; + } + + if (requestReplacementByMethodMap.get(method)!.has(url)) { + return requestReplacementByMethodMap.get(method)!.get(url); + } + + for (const [source, response] of requestReplacementByMethodMap.get(method)!) { + if (minimatch(url, source)) { + return response; + } + } + } + + async function setRequestReplacement({ + method = ANY_METHOD, + response, + responseType = FakeResponseType.File, + source, + }: InterceptParams) { + if (!requestReplacementByMethodMap.has(method)) { + requestReplacementByMethodMap.set(method, new Map()); + } + + if (responseType === FakeResponseType.Abort) { + debug(`[Intercept] Set up ABORT ${method} ${source}`); + requestReplacementByMethodMap.get(method)!.set(source, null); + } + + if (responseType === FakeResponseType.JSON) { + debug(`[Intercept] Set up JSON response ${method} ${source}`); + requestReplacementByMethodMap.get(method)!.set(source, { + mimeType: 'application/json', + data: JSON.stringify(response), + }); + } + + if (responseType === FakeResponseType.File) { + if (typeof response !== 'string') { + error( + `[Intercept] Error while setting up file response ${method} ${source}: request replacement file path is not a string`, + ); + return; + } + + const mimeType = mime.getType(response); + + if (!mimeType) { + error( + `[Intercept] Error while setting up file response ${method} ${source}: could not get a mime type from file extension (path: ${response})`, + ); + return; + } + + let data: Buffer; + + if (fileCache.has(response)) { + data = fileCache.get(response)!; + } else { + try { + data = await fsPromises.readFile(path.resolve(CWD, response)); + fileCache.set(response, data); + } catch (e) { + error('[Intercept] Error while opening file', e); + return; + } + } + + debug(`[Intercept] Set up FILE response ${method} ${source} -> ${response}`); + + requestReplacementByMethodMap.get(method)!.set(source, { + mimeType, + data, + }); + } + } + + async function handleInterceptedRequest(req: HTTPRequest) { + if (req.isInterceptResolutionHandled()) { + return; + } + + const response = getRequestReplacement(req.url(), req.method()) || getRequestReplacement(req.url(), ANY_METHOD); + + if (response === null) { + debug(`[Intercept] ${req.method()} ${req.url()} aborted`); + await req.abort(); + return; + } + + if (!response) { + await req.continue(); + return; + } + + await req.respond({ + contentType: response.mimeType, + body: response.data, + }); + + debug(`[Intercept] ${req.method()} ${req.url()} intercepted, respond ${response.mimeType}`); + } + + await page.exposeFunction('_perftool_intercept', setRequestReplacement); + await page.setRequestInterception(true); + + page.on('request', handleInterceptedRequest); +} diff --git a/packages/perftool/lib/client/__spec__/index.test.ts b/packages/perftool/lib/client/__spec__/index.test.ts index b355a48..e08e545 100644 --- a/packages/perftool/lib/client/__spec__/index.test.ts +++ b/packages/perftool/lib/client/__spec__/index.test.ts @@ -24,10 +24,10 @@ describe('client/createPerfToolClient', () => { })); jest.unstable_mockModule('../input', () => ({ - resolveTests: (resolveTestsMock = jest.fn(() => [fakeTest])), + resolveTest: (resolveTestsMock = jest.fn(() => fakeTest)), })); - window.finish = jest.fn() as typeof window.finish; + window._perftool_finish = jest.fn() as typeof window._perftool_finish; const config = {} as Config; const subjects = [{}] as Subject[]; @@ -48,7 +48,7 @@ describe('client/createPerfToolClient', () => { state: fakeTest.state, }); - expect(window.finish).toHaveBeenCalledTimes(1); - expect(window.finish).toHaveBeenCalledWith([fakeResult]); + expect(window._perftool_finish).toHaveBeenCalledTimes(1); + expect(window._perftool_finish).toHaveBeenCalledWith(fakeResult); }); }); diff --git a/packages/perftool/lib/client/__spec__/input.test.ts b/packages/perftool/lib/client/__spec__/input.test.ts index d84ef64..3df834a 100644 --- a/packages/perftool/lib/client/__spec__/input.test.ts +++ b/packages/perftool/lib/client/__spec__/input.test.ts @@ -3,7 +3,7 @@ import { jest } from '@jest/globals'; import type { Task } from '../measurement/types'; import type { Subject } from '../measurement/runner'; -describe('client/input/getTests', () => { +describe('client/input/getTest', () => { let assertMock = {} as jest.Mock; beforeEach(() => { jest.unstable_mockModule('../../utils/assert', () => ({ @@ -20,95 +20,88 @@ describe('client/input/getTests', () => { jest.resetModules(); }); - it('should transform raw tests with ids into tests with component and task', async () => { + it('should transform raw test with ids into test with component and task', async () => { const fakeTasks = [{ id: 'fakeTaskId1' }, { id: 'fakeTaskId2' }, { id: 'fakeTaskId3' }] as Task[]; const fakeSubjects = [ { id: 'fakeSubjectId1' }, { id: 'fakeSubjectId2' }, { id: 'fakeSubjectId3' }, ] as Subject[]; - const state1 = { state1: '' }; - const state2 = { state2: '' }; - const fakeRawTests = [ - { subjectId: fakeSubjects[0].id, taskId: fakeTasks[0].id, state: state1 }, - { subjectId: fakeSubjects[1].id, taskId: fakeTasks[2].id, state: state2 }, - ]; + const state = { state: '' }; + const fakeRawTest = { subjectId: fakeSubjects[0].id, taskId: fakeTasks[0].id, state }; - const { getTests } = await import('../input'); + const { getTest } = await import('../input'); - const result = getTests(fakeRawTests, { tasks: fakeTasks, subjects: fakeSubjects }); + const result = getTest(fakeRawTest, { tasks: fakeTasks, subjects: fakeSubjects }); - expect(result).toEqual([ - { subject: fakeSubjects[0], task: fakeTasks[0], state: state1 }, - { subject: fakeSubjects[1], task: fakeTasks[2], state: state2 }, - ]); - - expect(assertMock).toHaveBeenCalledTimes(fakeRawTests.length); + expect(result).toEqual({ subject: fakeSubjects[0], task: fakeTasks[0], state }); + expect(assertMock).toHaveBeenCalledTimes(1); }); it('should throw if task not found', async () => { const fakeTasks = [{ id: 'fakeTaskId1' }] as Task[]; const fakeSubjects = [{ id: 'fakeSubjectId1' }] as Subject[]; - const fakeRawTests = [{ subjectId: fakeSubjects[0].id, taskId: 'fakeTaskId2', state: {} }]; + const fakeRawTest = { subjectId: fakeSubjects[0].id, taskId: 'fakeTaskId2', state: {} }; - const { getTests } = await import('../input'); + const { getTest } = await import('../input'); expect(() => { - getTests(fakeRawTests, { tasks: fakeTasks, subjects: fakeSubjects }); + getTest(fakeRawTest, { tasks: fakeTasks, subjects: fakeSubjects }); }).toThrow(); - expect(assertMock).toHaveBeenCalledTimes(fakeRawTests.length); + expect(assertMock).toHaveBeenCalledTimes(1); }); it('should throw if subject not found', async () => { const fakeTasks = [{ id: 'fakeTaskId1' }] as Task[]; const fakeSubjects = [{ id: 'fakeSubjectId1' }] as Subject[]; - const fakeRawTests = [{ subjectId: 'fakeSubjectId2', taskId: fakeTasks[0].id, state: {} }]; + const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: fakeTasks[0].id, state: {} }; - const { getTests } = await import('../input'); + const { getTest } = await import('../input'); expect(() => { - getTests(fakeRawTests, { tasks: fakeTasks, subjects: fakeSubjects }); + getTest(fakeRawTest, { tasks: fakeTasks, subjects: fakeSubjects }); }).toThrow(); - expect(assertMock).toHaveBeenCalledTimes(fakeRawTests.length); + expect(assertMock).toHaveBeenCalledTimes(1); }); }); -describe('client/input/resolveTests', () => { +describe('client/input/resolveTest', () => { afterEach(() => { jest.restoreAllMocks(); jest.resetModules(); - delete window.tests; + delete window._perftool_api_ready; + delete window._perftool_test; }); - it('should call getTests if window.tests is present', async () => { - const tasks = [] as Task[]; - const subjects = [] as Subject[]; - const fakeRawTests = [] as any[]; - const fakeResult = [] as any[]; + it('should call getTests if window._perftool_test is present', async () => { + const tasks = [{ id: 'fakeTaskId' }] as any[]; + const subjects = [{ id: 'fakeSubjectId2' }] as any[]; + const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} }; + const fakeResult = { subject: subjects[0], task: tasks[0], state: fakeRawTest.state }; - window.tests = fakeRawTests; + window._perftool_test = fakeRawTest; - const { resolveTests } = await import('../input'); - const result = await resolveTests({ tasks, subjects }); + const { resolveTest } = await import('../input'); + const result = await resolveTest({ tasks, subjects }); expect(result).toEqual(fakeResult); - - expect(window.tests).toEqual(fakeRawTests); + expect(window._perftool_test).toEqual(fakeRawTest); }); - it("should imitate an array in window.tests if it's not present", async () => { - const tasks = [] as Task[]; - const subjects = [] as Subject[]; - const fakeRawTests = [] as any[]; - const fakeResult = [] as any[]; + it("should create window._perftool_api_ready if test isn't present", async () => { + const tasks = [{ id: 'fakeTaskId' }] as any[]; + const subjects = [{ id: 'fakeSubjectId2' }] as any[]; + const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} }; + const fakeResult = { subject: subjects[0], task: tasks[0], state: fakeRawTest.state }; - const { resolveTests } = await import('../input'); - const resultPromise = resolveTests({ tasks, subjects }); + const { resolveTest } = await import('../input'); + const resultPromise = resolveTest({ tasks, subjects }); - window.tests?.push(...fakeRawTests); + window._perftool_test = fakeRawTest; + window._perftool_api_ready?.(); expect(resultPromise).resolves.toEqual(fakeResult); }); @@ -116,12 +109,13 @@ describe('client/input/resolveTests', () => { it('should reject if error while transforming tests', async () => { const tasks = [] as Task[]; const subjects = [] as Subject[]; - const fakeRawTests = [{ subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} }]; + const fakeRawTest = { subjectId: 'fakeSubjectId2', taskId: 'fakeTaskId', state: {} }; - const { resolveTests } = await import('../input'); - const resultPromise = resolveTests({ tasks, subjects }); + const { resolveTest } = await import('../input'); + const resultPromise = resolveTest({ tasks, subjects }); - window.tests?.push(...fakeRawTests); + window._perftool_test = fakeRawTest; + window._perftool_api_ready?.(); expect(resultPromise).rejects.toThrow(); }); diff --git a/packages/perftool/lib/client/index.ts b/packages/perftool/lib/client/index.ts index 3a09cf8..efc724a 100644 --- a/packages/perftool/lib/client/index.ts +++ b/packages/perftool/lib/client/index.ts @@ -3,7 +3,7 @@ import { debug } from '../utils/logger'; import type { Task } from './measurement/types'; import { runTask, Subject } from './measurement/runner'; -import { resolveTests } from './input'; +import { resolveTest } from './input'; type CreatePerfToolClientParams[]> = { subjects: Subject[]; @@ -21,19 +21,10 @@ export async function createPerfToolClient[]>({ debug('Available tasks: ', tasks); debug('Config: ', config); - const tests = await resolveTests({ tasks, subjects }); - const resultPromises = []; + const { task, subject, state } = await resolveTest({ tasks, subjects }); - debug(`Running ${tests.length} tests`); - for (const { task, subject, state } of tests) { - const resultPromise = runTask({ task, subject, config, state }); + debug(`Running...`); - resultPromises.push(resultPromise); - } - - debug('Waiting for all tests to complete...'); - const results = await Promise.all(resultPromises); - - debug('All tests complete, calling window.finish'); - await window.finish(results); + const result = await runTask({ task, subject, config, state }); + await window._perftool_finish!(result); } diff --git a/packages/perftool/lib/client/input.ts b/packages/perftool/lib/client/input.ts index d8f8452..7172987 100644 --- a/packages/perftool/lib/client/input.ts +++ b/packages/perftool/lib/client/input.ts @@ -15,39 +15,35 @@ type ResolveTestsParams[]> = { subjects: Subject[]; }; -export function getTests[]>( - rawTests: RawTest[], +export function getTest[]>( + { subjectId, taskId, state }: RawTest, { tasks, subjects }: ResolveTestsParams, -): Test[] { - return rawTests.map(({ subjectId, taskId, state }) => { - const subject = subjects.find(({ id }) => subjectId === id); - const task = tasks.find(({ id }) => taskId === id); +): Test { + const subject = subjects.find(({ id }) => subjectId === id); + const task = tasks.find(({ id }) => taskId === id); - assert(subject && task); + assert(subject && task); - return { subject, task, state }; - }); + return { subject, task, state }; } -export async function resolveTests[]>( +export async function resolveTest[]>( params: ResolveTestsParams, -): Promise[]> { +): Promise> { /** * @see utils/window.d.ts */ - if (Array.isArray(window.tests)) { - return getTests(window.tests, params); + if (window._perftool_test) { + return getTest(window._perftool_test, params); } return new Promise((resolve, reject) => { - window.tests = { - push: (...items) => { - try { - resolve(getTests(items, params)); - } catch (err) { - reject(err); - } - }, + window._perftool_api_ready = () => { + try { + resolve(getTest(window._perftool_test!, params)); + } catch (err) { + reject(err); + } }; }); } diff --git a/packages/perftool/lib/client/measurement/runner.ts b/packages/perftool/lib/client/measurement/runner.ts index 93733f8..18c712f 100644 --- a/packages/perftool/lib/client/measurement/runner.ts +++ b/packages/perftool/lib/client/measurement/runner.ts @@ -13,7 +13,7 @@ class TimeoutError extends BaseError {} export type Subject = { id: string; - Component: ComponentType; + Component: ComponentType & { beforeTest?: () => Promise | void }; }; type RunTaskParams> = { @@ -32,7 +32,7 @@ export type RunTaskResult> = { state?: TaskState; } & (SuccessResult | ErrorResult); -export function runTask>({ +export async function runTask>({ task, state, subject, @@ -46,6 +46,11 @@ export function runTask>({ debug('Running test\n', `TaskId: ${meta.taskId}\n`, `SubjectId: ${meta.subjectId}`); debug('Task config: ', config); + if (typeof subject.Component.beforeTest === 'function') { + debug('Running beforeTest'); + await subject.Component.beforeTest(); + } + return Promise.race([ task .run({ Subject: subject.Component, config, container, state }) diff --git a/packages/perftool/lib/controller/__spec__/clientScript.test.ts b/packages/perftool/lib/controller/__spec__/clientScript.test.ts index 4ff74d4..50fc40f 100644 --- a/packages/perftool/lib/controller/__spec__/clientScript.test.ts +++ b/packages/perftool/lib/controller/__spec__/clientScript.test.ts @@ -1,35 +1,30 @@ -import { insertTests, createInsertionScriptContent } from '../clientScript'; +import { bootstrapTest, createInsertionScriptContent } from '../clientScript'; -const tests = [ - { subjectId: '1', taskId: '1', state: {} }, - { subjectId: '3', taskId: 'fake', state: {} }, -]; +const test = { subjectId: '3', taskId: 'fake', state: {} }; describe('controller/insertTests', () => { afterEach(() => { - delete window.tests; + delete window._perftool_test; }); - it('should parse JSON serialized tests and push in window.tests if present', () => { - window.tests = []; + it('should call window._perftool_api_ready if present', () => { + bootstrapTest(JSON.stringify(test)); - insertTests(JSON.stringify(tests)); - - expect(window.tests).toEqual(tests); + expect(window._perftool_test).toEqual(test); }); - it('should parse JSON serialized tests and push in window.tests if not present', () => { - insertTests(JSON.stringify(tests)); + it('should parse JSON serialized test and set in window._perftool_test', () => { + bootstrapTest(JSON.stringify(test)); - expect(window.tests).toEqual(tests); + expect(window._perftool_test).toEqual(test); }); }); describe('controller/createInsertionScriptContent', () => { it('should have insertTests body and call with serialized tests', () => { - const insertionScript = createInsertionScriptContent(tests); + const insertionScript = createInsertionScriptContent(test); - expect(insertionScript.includes(insertTests.toString())).toEqual(true); - expect(insertionScript.includes(`insertTests('${JSON.stringify(tests)}')`)).toEqual(true); + expect(insertionScript.includes(bootstrapTest.toString())).toEqual(true); + expect(insertionScript.includes(`bootstrapTest('${JSON.stringify(test)}')`)).toEqual(true); }); }); diff --git a/packages/perftool/lib/controller/__spec__/controller.test.ts b/packages/perftool/lib/controller/__spec__/controller.test.ts index b9c86e6..86a8e2c 100644 --- a/packages/perftool/lib/controller/__spec__/controller.test.ts +++ b/packages/perftool/lib/controller/__spec__/controller.test.ts @@ -8,18 +8,11 @@ import BaseError from '../../utils/baseError'; describe('controller/TestController', () => { it('should request a schedule from planner and execute tasks in parallel yielding the result', async () => { - const tests: Test[][] = [ - [ - { subjectId: 'fakeId1', taskId: 'fakeTask1' }, - { subjectId: 'fakeId2', taskId: 'fakeTask2' }, - { subjectId: 'fakeId3', taskId: 'fakeTask3' }, - ], - [ - { subjectId: 'fakeId2', taskId: 'fakeTask2' }, - { subjectId: 'fakeId3', taskId: 'fakeTask3' }, - ], - [{ subjectId: 'fakeId3', taskId: 'fakeTask3' }], - [{ subjectId: 'fakeId4', taskId: 'fakeTask5' }], + const tests: Test[] = [ + { subjectId: 'fakeId1', taskId: 'fakeTask1' }, + { subjectId: 'fakeId2', taskId: 'fakeTask2' }, + { subjectId: 'fakeId3', taskId: 'fakeTask3' }, + { subjectId: 'fakeId4', taskId: 'fakeTask5' }, ]; const config = { @@ -57,18 +50,11 @@ describe('controller/TestController', () => { }); it('should not pass dry runs to the result', async () => { - const tests: Test[][] = [ - [ - { subjectId: 'fakeId1', taskId: 'fakeTask1', type: 'dry' }, - { subjectId: 'fakeId2', taskId: 'fakeTask2' }, - { subjectId: 'fakeId3', taskId: 'fakeTask3' }, - ], - [ - { subjectId: 'fakeId2', taskId: 'fakeTask2', type: 'dry' }, - { subjectId: 'fakeId3', taskId: 'fakeTask3' }, - ], - [{ subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' }], - [{ subjectId: 'fakeId4', taskId: 'fakeTask5' }], + const tests: Test[] = [ + { subjectId: 'fakeId1', taskId: 'fakeTask1', type: 'dry' }, + { subjectId: 'fakeId2', taskId: 'fakeTask2' }, + { subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' }, + { subjectId: 'fakeId4', taskId: 'fakeTask5' }, ]; const config = { @@ -96,13 +82,13 @@ describe('controller/TestController', () => { generatorResult.push(result); } - expect(new Set(generatorResult)).toEqual(new Set(tests.filter((test) => test[0]?.type !== 'dry'))); + expect(new Set(generatorResult)).toEqual(new Set(tests.filter((test) => test?.type !== 'dry'))); }); it('should throw if execute returns an error', async () => { - const tests: Test[][] = [ - [{ subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' }], - [{ subjectId: 'fakeId4', taskId: 'fakeTask5' }], + const tests: Test[] = [ + { subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' }, + { subjectId: 'fakeId4', taskId: 'fakeTask5' }, ]; const config = { diff --git a/packages/perftool/lib/controller/__spec__/executor.test.ts b/packages/perftool/lib/controller/__spec__/executor.test.ts index 53939f8..8b97771 100644 --- a/packages/perftool/lib/controller/__spec__/executor.test.ts +++ b/packages/perftool/lib/controller/__spec__/executor.test.ts @@ -50,8 +50,9 @@ describe('controller/Executor', () => { let addScriptTagMock = {} as jest.Mock; let closeMock = {} as jest.Mock; let createInsertionScriptContentMock = {} as jest.Mock; - const tests: Test[] = [{ subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' }]; - const result = [{ ...tests[0], state: {} }]; + let useInterceptApiMock = {} as jest.Mock; + const test: Test = { subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' }; + const result = { ...test, state: {} }; const insertionScriptContent = 'some script content'; jest.unstable_mockModule('puppeteer', () => ({ default: { @@ -70,6 +71,9 @@ describe('controller/Executor', () => { jest.unstable_mockModule('../clientScript', () => ({ createInsertionScriptContent: (createInsertionScriptContentMock = jest.fn(() => insertionScriptContent)), })); + jest.unstable_mockModule('../../api/intercept', () => ({ + useInterceptApi: (useInterceptApiMock = jest.fn()), + })); const { default: Executor } = await import('../executor'); @@ -81,7 +85,7 @@ describe('controller/Executor', () => { const cache = createFakeCache(); const executor = await Executor.create(config, cache, port); - const execResult = await executor.execute(tests); + const execResult = await executor.execute(test); expect(newPageMock).toHaveBeenCalledTimes(1); @@ -89,25 +93,25 @@ describe('controller/Executor', () => { expect(gotoMock).toHaveBeenCalledWith(`http://localhost:${port}/`); expect(exposeFunctionMock).toHaveBeenCalledTimes(1); - expect(exposeFunctionMock.mock.calls[0][0]).toEqual('finish'); + expect(exposeFunctionMock.mock.calls[0][0]).toEqual('_perftool_finish'); + + expect(useInterceptApiMock).toHaveBeenCalledTimes(1); expect(addScriptTagMock).toHaveBeenCalledTimes(1); expect(addScriptTagMock).toHaveBeenCalledWith({ content: insertionScriptContent }); expect(createInsertionScriptContentMock).toHaveBeenCalledTimes(1); - expect(createInsertionScriptContentMock).toHaveBeenCalledWith([{ ...tests[0], state: {} }]); + expect(createInsertionScriptContentMock).toHaveBeenCalledWith({ ...test, state: {} }); expect(closeMock).toHaveBeenCalledTimes(1); expect(execResult).toEqual(result); - result[0].state = { changedState: true }; + result.state = { changedState: true }; - await executor.execute(tests); + await executor.execute(test); - expect(createInsertionScriptContentMock).toHaveBeenLastCalledWith([ - { ...tests[0], state: { changedState: true } }, - ]); + expect(createInsertionScriptContentMock).toHaveBeenLastCalledWith({ ...test, state: { changedState: true } }); }); it('should return error on page timeout', async () => { @@ -138,7 +142,7 @@ describe('controller/Executor', () => { const cache = createFakeCache(); const executor = await Executor.create(config, cache, port); - const execResult = await executor.execute([]); + const execResult = await executor.execute({ subjectId: 'fakeId3', taskId: 'fakeTask3' }); expect(closeMock).toHaveBeenCalledTimes(1); expect(execResult).toBeInstanceOf(Error); @@ -169,7 +173,7 @@ describe('controller/Executor', () => { await executor.finalize(); expect(closeMock).toHaveBeenCalledTimes(1); - expect(executor.execute([])).rejects.toThrow(); + expect(executor.execute({ subjectId: 'fakeId3', taskId: 'fakeTask3', type: 'dry' })).rejects.toThrow(); expect(newPageMock).toHaveBeenCalledTimes(0); await executor.finalize(); diff --git a/packages/perftool/lib/controller/__spec__/planner.test.ts b/packages/perftool/lib/controller/__spec__/planner.test.ts index e9120c8..14d657c 100644 --- a/packages/perftool/lib/controller/__spec__/planner.test.ts +++ b/packages/perftool/lib/controller/__spec__/planner.test.ts @@ -70,7 +70,7 @@ describe('controller/Planner', () => { jest.resetModules(); }); - it('should yield idempotent tasks once and in one group', async () => { + it('should yield idempotent tasks once', async () => { const config = { dryRunTimes: 3, retries: 10, @@ -96,13 +96,9 @@ describe('controller/Planner', () => { const planner = new Planner(config, tasks, modules); - const result = []; - - for (const testGroup of planner.plan()) { - result.push(testGroup); - } + const result = [...planner.plan()]; - expect(new Set(result[0])).toEqual(new Set(expectedResult)); + expect(new Set(result)).toEqual(new Set(expectedResult)); }); it('should yield non-idempotent tasks given number of times', async () => { @@ -120,14 +116,10 @@ describe('controller/Planner', () => { const planner = new Planner(config, tasks, modules); - const result = []; - - for (const testGroup of planner.plan()) { - result.push(testGroup); - } + const result = [...planner.plan()]; - expect(result.filter((tests) => tests[0].taskId === tasks[0].id)).toHaveLength(1); - expect(result.filter((tests) => tests[0].taskId === tasks[1].id && tests[0].type !== 'dry')).toHaveLength( + expect(result.filter((test) => test.taskId === tasks[0].id)).toHaveLength(1); + expect(result.filter((test) => test.taskId === tasks[1].id && test.type !== 'dry')).toHaveLength( config.retries, ); }); @@ -153,10 +145,10 @@ describe('controller/Planner', () => { result.push(testGroup); } - expect(result.filter((tests) => tests[0].taskId === tasks[0].id && tests[0].type !== 'dry')).toHaveLength( + expect(result.filter((test) => test.taskId === tasks[0].id && test.type !== 'dry')).toHaveLength( config.retries, ); - expect(result.filter((tests) => tests[0].taskId === tasks[0].id && tests[0].type === 'dry')).toHaveLength( + expect(result.filter((test) => test.taskId === tasks[0].id && test.type === 'dry')).toHaveLength( config.dryRunTimes, ); }); diff --git a/packages/perftool/lib/controller/clientScript.ts b/packages/perftool/lib/controller/clientScript.ts index 74c0e34..8059284 100644 --- a/packages/perftool/lib/controller/clientScript.ts +++ b/packages/perftool/lib/controller/clientScript.ts @@ -1,18 +1,21 @@ import type { RawTest } from '../client/input'; import { Task } from '../client/measurement/types'; -export function insertTests(serializedTests: string) { - const tests: RawTest[] = JSON.parse(serializedTests); +export function bootstrapTest(serializedTest: string) { + const test: RawTest = JSON.parse(serializedTest); /** * @see utils/window.d.ts */ - window.tests = window.tests || []; - window.tests.push(...tests); + window._perftool_test = test; + + if (window._perftool_api_ready) { + window._perftool_api_ready(); + } } -export function createInsertionScriptContent>(tests: RawTest[]) { - return `${insertTests.toString()} - insertTests('${JSON.stringify(tests)}'); +export function createInsertionScriptContent>(test: RawTest) { + return `${bootstrapTest.toString()} + bootstrapTest('${JSON.stringify(test)}'); `; } diff --git a/packages/perftool/lib/controller/controller.ts b/packages/perftool/lib/controller/controller.ts index 34b56ef..258a255 100644 --- a/packages/perftool/lib/controller/controller.ts +++ b/packages/perftool/lib/controller/controller.ts @@ -1,4 +1,3 @@ -import assert from '../utils/assert'; import { Config } from '../config'; import { Task } from '../client/measurement/types'; import { RunTaskResult } from '../client/measurement/runner'; @@ -21,27 +20,19 @@ export default class TestController[]> { this.planner = planner; } - async *run(): AsyncGenerator[], undefined> { + async *run(): AsyncGenerator, undefined> { const { executor } = this; const schedule = this.planner.plan(); const start = async function* start() { - while (true) { - const { done, value } = schedule.next(); - - if (done) { - return; - } - - assert(value); - + for (const value of schedule) { const result = await executor.execute(value); if (result instanceof Error) { throw result; } - if (value[0]?.type === 'dry') { + if (value.type === 'dry') { debug('[controller]', 'current run is dry, skipping'); continue; } diff --git a/packages/perftool/lib/controller/executor.ts b/packages/perftool/lib/controller/executor.ts index cd60c32..436649b 100644 --- a/packages/perftool/lib/controller/executor.ts +++ b/packages/perftool/lib/controller/executor.ts @@ -8,13 +8,14 @@ import { RunTaskResult } from '../client/measurement/runner'; import { Task, TaskState } from '../client/measurement/types'; import { debug } from '../utils/logger'; import { RawTest } from '../client/input'; +import { useInterceptApi } from '../api/intercept'; import { createInsertionScriptContent } from './clientScript'; export type Test = { taskId: string; subjectId: string; type?: 'dry' }; export type IExecutor[]> = { - execute(tests: Test[]): Promise[] | Error>; + execute(test: Test): Promise | Error>; }; const PUPPETEER_MYSTERY_ERROR_RETRIES = 5; @@ -108,8 +109,8 @@ export default class Executor[]> implements IExecu await this.browserInstance.close(); } - async execute(tests: Test[]): Promise[] | Error> { - debug('[executor]', 'running tests', tests); + async execute(test: Test): Promise | Error> { + debug('[executor]', 'running test', test); assert(this.workable); let page: Page = undefined as unknown as Page; @@ -125,21 +126,23 @@ export default class Executor[]> implements IExecu } } - const results = new Deferred[]>(); + const result = new Deferred>(); await page.goto(`http://localhost:${this.port}/`); - await page.exposeFunction('finish', (taskResults: RunTaskResult[]) => { - taskResults.forEach(this.setState); + await page.exposeFunction('_perftool_finish', (taskResult: RunTaskResult) => { + this.setState(taskResult); - results.resolve(taskResults); + result.resolve(taskResult); }); - await page.addScriptTag({ content: createInsertionScriptContent(tests.map(this.decorateWithState)) }); + await useInterceptApi(page); + await page.addScriptTag({ content: createInsertionScriptContent(this.decorateWithState(test)) }); return Promise.race([ - results.promise, - defer(this.config.runWaitTimeout).then(() => { - return new Error(`timeout ${this.config.runWaitTimeout}ms reached waiting for run to end`); - }), + result.promise, + defer( + this.config.runWaitTimeout, + () => new Error(`timeout ${this.config.runWaitTimeout}ms reached waiting for run to end`), + ), ]).finally(() => { return page.close(); }); diff --git a/packages/perftool/lib/controller/index.ts b/packages/perftool/lib/controller/index.ts index 0351a95..e808c80 100644 --- a/packages/perftool/lib/controller/index.ts +++ b/packages/perftool/lib/controller/index.ts @@ -23,7 +23,7 @@ export async function* runTests[]>({ port, tasks, testModules, -}: RunTestsParams): AsyncGenerator[], undefined> { +}: RunTestsParams): AsyncGenerator, undefined> { info('Running performance tests...'); const executor = await Executor.create(config, cache, port); diff --git a/packages/perftool/lib/controller/planner.ts b/packages/perftool/lib/controller/planner.ts index f17a3a4..3957d8a 100644 --- a/packages/perftool/lib/controller/planner.ts +++ b/packages/perftool/lib/controller/planner.ts @@ -8,7 +8,7 @@ import type { TestModule } from '../build/collect'; import type { Test } from './executor'; export type IPlanner = { - plan(): Generator; + plan(): Generator; }; export function getTests[]>( @@ -52,7 +52,7 @@ export default class Planner[]> implements IPlanner { this.testModules = testModules; } - *plan(): Generator { + *plan(): Generator { const idempotentTasks = this.tasks.filter(({ isIdempotent }) => isIdempotent); const nonIdempotentTasks = this.tasks.filter(({ isIdempotent }) => !isIdempotent); @@ -60,7 +60,9 @@ export default class Planner[]> implements IPlanner { debug('[planner]', 'running idempotent tasks'); const tests = getTests(this.config, idempotentTasks, this.testModules); - yield tests; + for (const test of tests) { + yield test; + } } if (nonIdempotentTasks.length) { @@ -74,7 +76,7 @@ export default class Planner[]> implements IPlanner { } for (let i = 0; i < this.config.dryRunTimes; ++i) { for (const test of tests) { - yield [{ ...test, type: 'dry' }]; + yield { ...test, type: 'dry' }; } } @@ -82,7 +84,7 @@ export default class Planner[]> implements IPlanner { for (let i = 0; i < this.config.retries; ++i) { for (const test of tests) { - yield [test]; + yield test; } } } diff --git a/packages/perftool/lib/index.ts b/packages/perftool/lib/index.ts index 7ced9fc..f05ec2b 100755 --- a/packages/perftool/lib/index.ts +++ b/packages/perftool/lib/index.ts @@ -16,10 +16,11 @@ import Cache from './cache'; import openBrowser from './utils/openBrowser'; import { waitForSigint } from './utils/interrupt'; -const cli = createCommand('perftool'); - +export { intercept } from './api/external'; export type { ProjectConfig as Config }; +const cli = createCommand('perftool'); + cli.addArgument(createArgument('[include...]', 'Modules to run perftest on')) .addOption(createOption('-l, --logLevel ', 'Log level').choices(['quiet', 'normal', 'verbose'])) .addOption(createOption('-c, --configPath ', 'Config path')) diff --git a/packages/perftool/lib/statistics/index.ts b/packages/perftool/lib/statistics/index.ts index 99afcc6..0beca92 100644 --- a/packages/perftool/lib/statistics/index.ts +++ b/packages/perftool/lib/statistics/index.ts @@ -63,17 +63,15 @@ export default class Statistics[]> { this.computableObservations.get(subjectId)?.get(taskId)?.push(rest.result); } - async consume(source: AsyncGenerator[], undefined>): Promise { + async consume(source: AsyncGenerator, undefined>): Promise { this.isConsuming = true; - for await (const resultGroup of source) { + for await (const result of source) { if (!this.isConsuming) { break; } - for (const result of resultGroup) { - this.addObservation(result); - } + this.addObservation(result); } this.isConsuming = false; diff --git a/packages/perftool/lib/typings/window.d.ts b/packages/perftool/lib/typings/window.d.ts index 8d3dc14..9afb819 100644 --- a/packages/perftool/lib/typings/window.d.ts +++ b/packages/perftool/lib/typings/window.d.ts @@ -1,12 +1,15 @@ import type { Task } from '../client/measurement/types'; import type { RunTaskResult } from '../client/measurement/runner'; import type { RawTest } from '../client/input'; +import { InterceptParams } from '../api/intercept'; declare global { interface Window { // TODO comment - tests?: Array>> | { push: (...args: RawTest>[]) => void }; - finish: []>(results: RunTaskResult[]) => Promise; + _perftool_test?: RawTest>; + _perftool_finish?: []>(result: RunTaskResult) => Promise; + _perftool_intercept?: (params: InterceptParams) => Promise; + _perftool_api_ready?: () => void; } } diff --git a/packages/perftool/package.json b/packages/perftool/package.json index 447a61d..4c5ea34 100644 --- a/packages/perftool/package.json +++ b/packages/perftool/package.json @@ -54,6 +54,9 @@ "jstat": "1.9.6", "loglevel": "1.8.1", "loglevel-plugin-prefix": "0.8.4", + "lru-cache": "10.0.0", + "mime": "3.0.0", + "minimatch": "9.0.3", "morgan": "1.10.0", "open": "9.1.0", "puppeteer": "19.9.1", @@ -66,6 +69,7 @@ "@jest/globals": "29.6.1", "@types/express": "4.17.17", "@types/jest": "29.5.0", + "@types/mime": "3.0.1", "@types/morgan": "1.9.4", "@types/node": "20.4.3", "@types/react": "18.2.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dcf798..52d78e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: 6.7.1 version: 6.7.1(eslint@8.45.0) eslint-plugin-prettier: - specifier: 4.2.1 - version: 4.2.1(eslint-config-prettier@8.8.0)(eslint@8.45.0)(prettier@3.0.0) + specifier: 5.0.0 + version: 5.0.0(eslint-config-prettier@8.8.0)(eslint@8.45.0)(prettier@3.0.0) eslint-plugin-react: specifier: 7.32.2 version: 7.32.2(eslint@8.45.0) @@ -122,6 +122,15 @@ importers: loglevel-plugin-prefix: specifier: 0.8.4 version: 0.8.4 + lru-cache: + specifier: 10.0.0 + version: 10.0.0 + mime: + specifier: 3.0.0 + version: 3.0.0 + minimatch: + specifier: 9.0.3 + version: 9.0.3 morgan: specifier: 1.10.0 version: 1.10.0 @@ -153,6 +162,9 @@ importers: '@types/jest': specifier: 29.5.0 version: 29.5.0 + '@types/mime': + specifier: 3.0.1 + version: 3.0.1 '@types/morgan': specifier: 1.9.4 version: 1.9.4 @@ -1903,7 +1915,7 @@ packages: lodash.get: 4.4.2 make-error: 1.3.6 ts-node: 9.1.1(typescript@5.0.4) - tslib: 2.5.0 + tslib: 2.6.1 transitivePeerDependencies: - typescript dev: true @@ -2651,7 +2663,7 @@ packages: nx: 15.9.2 semver: 7.3.4 tmp: 0.2.1 - tslib: 2.5.0 + tslib: 2.6.1 dev: true /@nrwl/nx-darwin-arm64@15.9.2: @@ -3016,6 +3028,18 @@ packages: node-gyp-build: 4.6.0 dev: true + /@pkgr/utils@2.4.2: + resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + fast-glob: 3.3.1 + is-glob: 4.0.3 + open: 9.1.0 + picocolors: 1.0.0 + tslib: 2.6.1 + dev: true + /@puppeteer/browsers@0.4.1(typescript@5.0.4): resolution: {integrity: sha512-4IICvy1McAkT/HyNZHIs7sp8ngBX1dmO0TPQ+FWq9ATQMqI8p+Ulm5A3kS2wYDh5HDHHkYrrETOu6rlj64VuTw==} engines: {node: '>=14.1.0'} @@ -3728,7 +3752,7 @@ packages: engines: {node: '>=14.15.0'} dependencies: js-yaml: 3.14.1 - tslib: 2.5.0 + tslib: 2.6.1 dev: true /@zkochan/js-yaml@0.0.6: @@ -4295,7 +4319,6 @@ packages: /balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -4314,7 +4337,6 @@ packages: /big-integer@1.6.51: resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} engines: {node: '>=0.6'} - dev: false /bin-links@4.0.1: resolution: {integrity: sha512-bmFEM39CyX336ZGGRsGPlc6jZHriIoHacOQcTt72MktIjpPhZoP4te2jOyUXF3BLILmJ8aNLncoPVeIIFlrDeA==} @@ -4366,7 +4388,6 @@ packages: engines: {node: '>= 5.10.0'} dependencies: big-integer: 1.6.51 - dev: false /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -4379,7 +4400,6 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: true /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -4455,7 +4475,6 @@ packages: engines: {node: '>=12'} dependencies: run-applescript: 5.0.0 - dev: false /byte-size@7.0.0: resolution: {integrity: sha512-NNiBxKgxybMBtWdmvx7ZITJi4ZG+CYUgwOSZTfqB1qogkRHrhbQE/R2r5Fh94X+InN5MCYz6SvB/ejHMj/HbsQ==} @@ -4528,7 +4547,7 @@ packages: resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} dependencies: pascal-case: 3.1.2 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /camelcase-keys@6.2.2: @@ -5246,7 +5265,6 @@ packages: dependencies: bplist-parser: 0.2.0 untildify: 4.0.0 - dev: false /default-browser@4.0.0: resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} @@ -5256,7 +5274,6 @@ packages: default-browser-id: 3.0.0 execa: 7.1.1 titleize: 3.0.0 - dev: false /defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} @@ -5272,7 +5289,6 @@ packages: /define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - dev: false /define-properties@1.2.0: resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==} @@ -5413,7 +5429,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /dot-prop@5.3.0: @@ -5831,14 +5847,17 @@ packages: semver: 6.3.0 dev: true - /eslint-plugin-prettier@4.2.1(eslint-config-prettier@8.8.0)(eslint@8.45.0)(prettier@3.0.0): - resolution: {integrity: sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==} - engines: {node: '>=12.0.0'} + /eslint-plugin-prettier@5.0.0(eslint-config-prettier@8.8.0)(eslint@8.45.0)(prettier@3.0.0): + resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: - eslint: '>=7.28.0' + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' eslint-config-prettier: '*' - prettier: '>=2.0.0' + prettier: '>=3.0.0' peerDependenciesMeta: + '@types/eslint': + optional: true eslint-config-prettier: optional: true dependencies: @@ -5846,6 +5865,7 @@ packages: eslint-config-prettier: 8.8.0(eslint@8.45.0) prettier: 3.0.0 prettier-linter-helpers: 1.0.0 + synckit: 0.8.5 dev: true /eslint-plugin-react-hooks@4.6.0(eslint@8.45.0): @@ -6166,6 +6186,17 @@ packages: micromatch: 4.0.5 dev: true + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fast-json-parse@1.0.3: resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==} dev: true @@ -7152,7 +7183,6 @@ packages: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - dev: false /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -7184,7 +7214,6 @@ packages: hasBin: true dependencies: is-docker: 3.0.0 - dev: false /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} @@ -8398,7 +8427,12 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.5.0 + tslib: 2.6.1 + dev: false + + /lru-cache@10.0.0: + resolution: {integrity: sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==} + engines: {node: 14 || >=16.14} dev: false /lru-cache@5.1.1: @@ -8571,6 +8605,12 @@ packages: hasBin: true dev: false + /mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: false + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -8624,6 +8664,13 @@ packages: brace-expansion: 2.0.1 dev: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + /minimist-options@4.1.0: resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} engines: {node: '>= 6'} @@ -8822,7 +8869,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /node-addon-api@3.2.1: @@ -9158,7 +9205,7 @@ packages: tar-stream: 2.2.0 tmp: 0.2.1 tsconfig-paths: 4.2.0 - tslib: 2.5.0 + tslib: 2.6.1 v8-compile-cache: 2.3.0 yargs: 17.7.1 yargs-parser: 21.1.1 @@ -9298,7 +9345,6 @@ packages: define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 is-wsl: 2.2.0 - dev: false /optionator@0.8.3: resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} @@ -9527,7 +9573,7 @@ packages: resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} dependencies: dot-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /parent-module@1.0.1: @@ -9607,7 +9653,7 @@ packages: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} dependencies: no-case: 3.0.4 - tslib: 2.5.0 + tslib: 2.6.1 dev: false /path-exists@3.0.0: @@ -10353,7 +10399,6 @@ packages: engines: {node: '>=12'} dependencies: execa: 5.1.1 - dev: false /run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} @@ -10368,7 +10413,7 @@ packages: /rxjs@7.8.0: resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} dependencies: - tslib: 2.5.0 + tslib: 2.6.1 dev: true /safe-buffer@5.1.2: @@ -10931,6 +10976,14 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true + /synckit@0.8.5: + resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} + engines: {node: ^14.18.0 || >=16.0.0} + dependencies: + '@pkgr/utils': 2.4.2 + tslib: 2.5.0 + dev: true + /table-layout@1.0.2: resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} engines: {node: '>=8.0.0'} @@ -11105,7 +11158,6 @@ packages: /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} - dev: false /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} @@ -11314,6 +11366,10 @@ packages: /tslib@2.5.0: resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==} + dev: true + + /tslib@2.6.1: + resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} /tsutils@3.21.0(typescript@5.0.4): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} @@ -11539,7 +11595,6 @@ packages: /untildify@4.0.0: resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} engines: {node: '>=8'} - dev: false /upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==}