diff --git a/lib/gui/app.js b/lib/gui/app.js index 427b17e38..ae546c869 100644 --- a/lib/gui/app.js +++ b/lib/gui/app.js @@ -1,7 +1,7 @@ 'use strict'; const _ = require('lodash'); -const ToolRunner = require('./tool-runner'); +const {ToolRunner} = require('./tool-runner'); module.exports = class App { static create(paths, hermione, configs) { diff --git a/lib/gui/index.ts b/lib/gui/index.ts index f3938212a..31ab42468 100644 --- a/lib/gui/index.ts +++ b/lib/gui/index.ts @@ -1,20 +1,30 @@ +import type {CommanderStatic} from '@gemini-testing/commander'; import chalk from 'chalk'; import opener from 'opener'; + import server from './server'; import {logger} from '../common-utils'; import * as utils from '../server-utils'; +import {ReporterConfig} from '../types'; const {logError} = utils; +export interface GuiCliOptions { + autoRun: boolean; + open: unknown; +} + +export interface GuiConfigs { + options: GuiCliOptions; + program: CommanderStatic; + pluginConfig: ReporterConfig; +} + interface ServerArgs { paths: string[]; hermione: unknown; guiApi: unknown; - configs: { - options: { - open: unknown, - } - }; + configs: GuiConfigs; } export default (args: ServerArgs): void => { diff --git a/lib/gui/tool-runner/index.ts b/lib/gui/tool-runner/index.ts index 9b7a6b98f..030bffcf4 100644 --- a/lib/gui/tool-runner/index.ts +++ b/lib/gui/tool-runner/index.ts @@ -1,33 +1,80 @@ -'use strict'; - -const _ = require('lodash'); -const fs = require('fs-extra'); -const path = require('path'); -const chalk = require('chalk'); -const Promise = require('bluebird'); -const looksSame = require('looks-same'); - -const {createTestRunner} = require('./runner'); -const {subscribeOnToolEvents} = require('./report-subscriber'); -const {GuiReportBuilder} = require('../../report-builder/gui'); -const {EventSource} = require('../event-source'); -const {logger} = require('../../common-utils'); -const reporterHelper = require('../../reporter-helpers'); -const {UPDATED, SKIPPED, IDLE} = require('../../constants/test-statuses'); -const {DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} = require('../../constants/database'); -const {getShortMD5} = require('../../common-utils'); -const {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} = require('./utils'); -const {getTestsTreeFromDatabase} = require('../../db-utils/server'); -const {formatTestResult} = require('../../server-utils'); -const {ToolName} = require('../../constants'); - -module.exports = class ToolRunner { - static create(paths, hermione, configs) { - return new this(paths, hermione, configs); +import path from 'path'; + +import {CommanderStatic} from '@gemini-testing/commander'; +import chalk from 'chalk'; +import fs from 'fs-extra'; +import type Hermione from 'hermione'; +import type {TestCollection, Test as HermioneTest, Config as HermioneConfig} from 'hermione'; +import _ from 'lodash'; +import looksSame, {CoordBounds} from 'looks-same'; + +import {createTestRunner} from './runner'; +import {subscribeOnToolEvents} from './report-subscriber'; +import {GuiReportBuilder, GuiReportBuilderResult} from '../../report-builder/gui'; +import {EventSource} from '../event-source'; +import {logger, getShortMD5} from '../../common-utils'; +import * as reporterHelper from '../../reporter-helpers'; +import {UPDATED, SKIPPED, IDLE, TestStatus, ToolName, DATABASE_URLS_JSON_NAME, LOCAL_DATABASE_NAME} from '../../constants'; +import {formatId, mkFullTitle, mergeDatabasesForReuse, filterByEqualDiffSizes} from './utils'; +import {getTestsTreeFromDatabase} from '../../db-utils/server'; +import {formatTestResult} from '../../server-utils'; +import { + AssertViewResult, + HermioneTestResult, + HtmlReporterApi, + ImageData, + ImageInfoFail, + ReporterConfig +} from '../../types'; +import {GuiCliOptions, GuiConfigs} from '../index'; +import {Tree, TreeImage} from '../../tests-tree-builder/base'; +import {TestSpec} from './runner/runner'; +import {Response} from 'express'; +import {TestBranch, TestEqualDiffsData, TestRefUpdateData} from '../../tests-tree-builder/gui'; +import {ReporterTestResult} from '../../test-adapter'; +import {ImagesInfoFormatter} from '../../image-handler'; + +type ToolRunnerArgs = [paths: string[], hermione: Hermione & HtmlReporterApi, configs: GuiConfigs]; + +type ToolRunnerTree = GuiReportBuilderResult & Pick; + +interface HermioneTestExtended extends HermioneTest { + assertViewResults: {stateName: string, refImg: ImageData, currImg: ImageData}; + attempt: number; + imagesInfo: Pick[]; +} + +type HermioneTestPlain = Pick; + +interface UndoAcceptImagesResult { + updatedImages: TreeImage[]; + removedResults: string[]; +} + +// TODO: get rid of this function. It allows to format raw test, but is type-unsafe. +const formatTestResultUnsafe = (test: HermioneTest | HermioneTestExtended | HermioneTestPlain, status: TestStatus, {imageHandler}: {imageHandler: ImagesInfoFormatter}): ReporterTestResult => { + return formatTestResult(test as HermioneTestResult, status, {imageHandler}); +}; + +export class ToolRunner { + private _testFiles: string[]; + private _hermione: Hermione & HtmlReporterApi; + private _tree: ToolRunnerTree | null; + protected _collection: TestCollection | null; + private _globalOpts: CommanderStatic; + private _guiOpts: GuiCliOptions; + private _reportPath: string; + private _pluginConfig: ReporterConfig; + private _eventSource: EventSource; + protected _reportBuilder: GuiReportBuilder | null; + private _tests: Record; + + static create(this: new (...args: ToolRunnerArgs) => T, ...args: ToolRunnerArgs): T { + return new this(...args); } - constructor(paths, hermione, {program: globalOpts, pluginConfig, options: guiOpts}) { - this._testFiles = [].concat(paths); + constructor(...[paths, hermione, {program: globalOpts, pluginConfig, options: guiOpts}]: ToolRunnerArgs) { + this._testFiles = ([] as string[]).concat(paths); this._hermione = hermione; this._tree = null; this._collection = null; @@ -43,15 +90,15 @@ module.exports = class ToolRunner { this._tests = {}; } - get config() { + get config(): HermioneConfig { return this._hermione.config; } - get tree() { + get tree(): ToolRunnerTree | null { return this._tree; } - async initialize() { + async initialize(): Promise { await mergeDatabasesForReuse(this._reportPath); this._reportBuilder = GuiReportBuilder.create(this._hermione.htmlReporter, this._pluginConfig, {reuse: true}); @@ -66,81 +113,104 @@ module.exports = class ToolRunner { await this._handleRunnableCollection(); } - async _readTests() { + private _assertInitialized(): asserts this is {_reportBuilder: GuiReportBuilder; _collection: TestCollection} { + if (!this._reportBuilder || !this._collection) { + throw new Error('ToolRunner has to be initialized before usage'); + } + } + + async _readTests(): Promise { const {grep, set: sets, browser: browsers} = this._globalOpts; - return await this._hermione.readTests(this._testFiles, {grep, sets, browsers}); + return this._hermione.readTests(this._testFiles, {grep, sets, browsers}); } - finalize() { + async finalize(): Promise { + this._assertInitialized(); + return this._reportBuilder.finalize(); } - addClient(connection) { + addClient(connection: Response): void { this._eventSource.addConnection(connection); } - sendClientEvent(event, data) { + sendClientEvent(event: string, data: unknown): void { this._eventSource.emit(event, data); } - getTestsDataToUpdateRefs(imageIds) { + getTestsDataToUpdateRefs(imageIds: string[]): TestRefUpdateData[] { + this._assertInitialized(); + return this._reportBuilder.getTestsDataToUpdateRefs(imageIds); } - getImageDataToFindEqualDiffs(imageIds) { + getImageDataToFindEqualDiffs(imageIds: string[]): TestEqualDiffsData[] { + this._assertInitialized(); + const [selectedImage, ...comparedImages] = this._reportBuilder.getImageDataToFindEqualDiffs(imageIds); const imagesWithEqualBrowserName = comparedImages.filter((image) => image.browserName === selectedImage.browserName); - const imagesWithEqualDiffSizes = filterByEqualDiffSizes(imagesWithEqualBrowserName, selectedImage.diffClusters); + const imagesWithEqualDiffSizes = filterByEqualDiffSizes(imagesWithEqualBrowserName, (selectedImage as ImageInfoFail).diffClusters); return _.isEmpty(imagesWithEqualDiffSizes) ? [] : [selectedImage].concat(imagesWithEqualDiffSizes); } - updateReferenceImage(tests) { - return Promise.map(tests, (test) => { + async updateReferenceImage(tests: TestRefUpdateData[]): Promise { + return Promise.all(tests.map(async (test): Promise => { + this._assertInitialized(); + const updateResult = this._prepareTestResult(test); - const formattedResult = formatTestResult(updateResult, UPDATED, this._reportBuilder); + const formattedResult = formatTestResultUnsafe(updateResult, UPDATED, this._reportBuilder); const failResultId = formattedResult.id; const updateAttempt = this._reportBuilder.getUpdatedAttempt(formattedResult); formattedResult.attempt = updateAttempt; updateResult.attempt = updateAttempt; - return Promise.map(updateResult.imagesInfo, (imageInfo) => { + await Promise.all(updateResult.imagesInfo.map(async (imageInfo) => { const {stateName} = imageInfo; - return reporterHelper.updateReferenceImage(formattedResult, this._reportPath, stateName) - .then(() => { - const result = _.extend(updateResult, {refImg: imageInfo.expectedImg}); - this._emitUpdateReference(result, stateName); - }); - }) - .then(() => { - this._reportBuilder.addUpdated(formatTestResult(updateResult, UPDATED, this._reportBuilder), failResultId); - return this._reportBuilder.getTestBranch(formattedResult.id); - }); - }); + await reporterHelper.updateReferenceImage(formattedResult, this._reportPath, stateName); + + const result = _.extend(updateResult, {refImg: imageInfo.expectedImg}); + this._emitUpdateReference(result, stateName); + })); + + this._reportBuilder.addUpdated(formatTestResultUnsafe(updateResult, UPDATED, this._reportBuilder), failResultId); + + return this._reportBuilder.getTestBranch(formattedResult.id); + })); } - async undoAcceptImages(tests) { - const updatedImages = [], removedResults = []; + async undoAcceptImages(tests: TestRefUpdateData[]): Promise { + const updatedImages: TreeImage[] = [], removedResults: string[] = []; + + await Promise.all(tests.map(async (test) => { + this._assertInitialized(); - await Promise.map(tests, async (test) => { const updateResult = this._prepareTestResult(test); - const formattedResult = formatTestResult(updateResult, UPDATED, this._reportBuilder); + const formattedResult = formatTestResultUnsafe(updateResult, UPDATED, this._reportBuilder); formattedResult.attempt = updateResult.attempt; - await Promise.map(updateResult.imagesInfo, async (imageInfo) => { + await Promise.all(updateResult.imagesInfo.map(async (imageInfo) => { + this._assertInitialized(); + const {stateName} = imageInfo; + + const undoResultData = this._reportBuilder.undoAcceptImage(formattedResult, stateName); + if (undoResultData === null) { + return; + } + const { updatedImage, removedResult, previousExpectedPath, shouldRemoveReference, shouldRevertReference - } = await this._reportBuilder.undoAcceptImage(formattedResult, stateName); + } = undoResultData; updatedImage && updatedImages.push(updatedImage); removedResult && removedResults.push(removedResult); @@ -149,26 +219,32 @@ module.exports = class ToolRunner { await reporterHelper.removeReferenceImage(formattedResult, stateName); } - if (shouldRevertReference) { + if (shouldRevertReference && removedResult) { await reporterHelper.revertReferenceImage(removedResult, formattedResult, stateName); } - if (previousExpectedPath) { - this._reportBuilder.imageHandler.updateCacheExpectedPath(updateResult, stateName, previousExpectedPath); + if (previousExpectedPath && (updateResult as HermioneTest).fullTitle) { + this._reportBuilder.imageHandler.updateCacheExpectedPath({ + fullName: (updateResult as HermioneTest).fullTitle(), + browserId: (updateResult as HermioneTest).browserId + }, stateName, previousExpectedPath); } - }); - }); + })); + })); return {updatedImages, removedResults}; } - async findEqualDiffs(images) { - const [selectedImage, ...comparedImages] = images; + async findEqualDiffs(images: TestEqualDiffsData[]): Promise { + const [selectedImage, ...comparedImages] = images as (ImageInfoFail & {diffClusters: CoordBounds[]})[]; const {tolerance, antialiasingTolerance} = this.config; const compareOpts = {tolerance, antialiasingTolerance, stopOnFirstFail: true, shouldCluster: false}; - const equalImages = await Promise.filter(comparedImages, async (image) => { - try { - await Promise.mapSeries(image.diffClusters, async (diffCluster, i) => { + + const comparisons = await Promise.all(comparedImages.map(async (image) => { + for (let i = 0; i < image.diffClusters.length; i++) { + const diffCluster = image.diffClusters[i]; + + try { const refComparisonRes = await looksSame( {source: this._resolveImgPath(selectedImage.expectedImg.path), boundingBox: selectedImage.diffClusters[i]}, {source: this._resolveImgPath(image.expectedImg.path), boundingBox: diffCluster}, @@ -176,7 +252,7 @@ module.exports = class ToolRunner { ); if (!refComparisonRes.equal) { - return Promise.reject(false); + return false; } const actComparisonRes = await looksSame( @@ -186,28 +262,37 @@ module.exports = class ToolRunner { ); if (!actComparisonRes.equal) { - return Promise.reject(false); + return false; } - }); - - return true; - } catch (err) { - return err === false ? err : Promise.reject(err); + } catch (err) { + if (err !== false) { + throw err; + } + return false; + } } - }); - return equalImages.map((image) => image.id); + return image; + })); + + return comparisons.filter(Boolean).map(image => (image as TestEqualDiffsData).id); } - run(tests = []) { + async run(tests: TestSpec[] = []): Promise { + this._assertInitialized(); + const {grep, set: sets, browser: browsers} = this._globalOpts; return createTestRunner(this._collection, tests) .run((collection) => this._hermione.run(collection, {grep, sets, browsers})); } - async _handleRunnableCollection() { + protected async _handleRunnableCollection(): Promise { + this._assertInitialized(); + this._collection.eachTest((test, browserId) => { + this._assertInitialized(); + if (test.disabled || this._isSilentlySkipped(test)) { return; } @@ -217,38 +302,42 @@ module.exports = class ToolRunner { this._tests[testId] = _.extend(test, {browserId}); test.pending - ? this._reportBuilder.addSkipped(formatTestResult(test, SKIPPED, this._reportBuilder)) - : this._reportBuilder.addIdle(formatTestResult(test, IDLE, this._reportBuilder)); + ? this._reportBuilder.addSkipped(formatTestResultUnsafe(test, SKIPPED, this._reportBuilder)) + : this._reportBuilder.addIdle(formatTestResultUnsafe(test, IDLE, this._reportBuilder)); }); await this._fillTestsTree(); } - _isSilentlySkipped({silentSkip, parent}) { + protected _isSilentlySkipped({silentSkip, parent}: HermioneTest): boolean { return silentSkip || parent && this._isSilentlySkipped(parent); } - _subscribeOnEvents() { + protected _subscribeOnEvents(): void { + this._assertInitialized(); + subscribeOnToolEvents(this._hermione, this._reportBuilder, this._eventSource, this._reportPath); } - _prepareTestResult(test) { + protected _prepareTestResult(test: TestRefUpdateData): HermioneTestExtended | HermioneTestPlain { const {browserId, attempt} = test; const fullTitle = mkFullTitle(test); const testId = formatId(getShortMD5(fullTitle), browserId); const rawTest = this._tests[testId]; const {sessionId, url} = test.metaInfo; - const assertViewResults = []; + const assertViewResults: AssertViewResult[] = []; - const imagesInfo = test.imagesInfo.map((imageInfo) => { - const {stateName, actualImg} = imageInfo; - const path = this._hermione.config.browsers[browserId].getScreenshotPath(rawTest, stateName); - const refImg = {path, size: actualImg.size}; + const imagesInfo = test.imagesInfo + .filter(({stateName, actualImg}) => Boolean(stateName) && Boolean(actualImg)) + .map((imageInfo) => { + const {stateName, actualImg} = imageInfo as {stateName: string, actualImg: ImageData}; + const path = this._hermione.config.browsers[browserId].getScreenshotPath(rawTest, stateName); + const refImg = {path, size: actualImg.size}; - assertViewResults.push({stateName, refImg, currImg: actualImg}); + assertViewResults.push({stateName, refImg, currImg: actualImg}); - return _.extend(imageInfo, {expectedImg: refImg}); - }); + return _.extend(imageInfo, {expectedImg: refImg}); + }); const res = _.merge({}, rawTest, {assertViewResults, imagesInfo, sessionId, attempt, meta: {url}, updated: true}); @@ -259,14 +348,16 @@ module.exports = class ToolRunner { : res; } - _emitUpdateReference({refImg}, state) { + protected _emitUpdateReference({refImg}: {refImg: ImageData}, state: string): void { this._hermione.emit( this._hermione.events.UPDATE_REFERENCE, {refImg, state} ); } - async _fillTestsTree() { + async _fillTestsTree(): Promise { + this._assertInitialized(); + const {autoRun} = this._guiOpts; const testsTree = await this._loadDataFromDatabase(); @@ -277,7 +368,7 @@ module.exports = class ToolRunner { this._tree = {...this._reportBuilder.getResult(), autoRun}; } - async _loadDataFromDatabase() { + protected async _loadDataFromDatabase(): Promise { const dbPath = path.resolve(this._reportPath, LOCAL_DATABASE_NAME); if (await fs.pathExists(dbPath)) { @@ -286,10 +377,10 @@ module.exports = class ToolRunner { logger.warn(chalk.yellow(`Nothing to reuse in ${this._reportPath}: can not load data from ${DATABASE_URLS_JSON_NAME}`)); - return {}; + return null; } - _resolveImgPath(imgPath) { + protected _resolveImgPath(imgPath: string): string { return path.resolve(process.cwd(), this._pluginConfig.path, imgPath); } -}; +} diff --git a/lib/gui/tool-runner/runner/all-test-runner.ts b/lib/gui/tool-runner/runner/all-test-runner.ts index f181eb145..4800dddfd 100644 --- a/lib/gui/tool-runner/runner/all-test-runner.ts +++ b/lib/gui/tool-runner/runner/all-test-runner.ts @@ -1,4 +1,5 @@ -import {BaseRunner, TestCollection} from './runner'; +import type {TestCollection} from 'hermione'; +import {BaseRunner} from './runner'; export class AllTestRunner extends BaseRunner { override run(runHandler: (testCollection: TestCollection) => U): U { diff --git a/lib/gui/tool-runner/runner/index.ts b/lib/gui/tool-runner/runner/index.ts index df29746d1..f82cb8c20 100644 --- a/lib/gui/tool-runner/runner/index.ts +++ b/lib/gui/tool-runner/runner/index.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; +import type {TestCollection} from 'hermione'; -import {TestCollection, TestRunner, TestSpec} from './runner'; +import {TestRunner, TestSpec} from './runner'; import {AllTestRunner} from './all-test-runner'; import {SpecificTestRunner} from './specific-test-runner'; diff --git a/lib/gui/tool-runner/runner/runner.ts b/lib/gui/tool-runner/runner/runner.ts index a197f06fd..87e6a124a 100644 --- a/lib/gui/tool-runner/runner/runner.ts +++ b/lib/gui/tool-runner/runner/runner.ts @@ -1,7 +1,4 @@ -import Hermione from 'hermione'; -import {AsyncReturnType} from 'type-fest'; - -export type TestCollection = AsyncReturnType +import type {TestCollection} from 'hermione'; export interface TestRunner { run(handler: (testCollection: TestCollection) => U): U; diff --git a/lib/gui/tool-runner/runner/specific-test-runner.ts b/lib/gui/tool-runner/runner/specific-test-runner.ts index 9eba480d6..ff665af43 100644 --- a/lib/gui/tool-runner/runner/specific-test-runner.ts +++ b/lib/gui/tool-runner/runner/specific-test-runner.ts @@ -1,4 +1,5 @@ -import {BaseRunner, TestCollection, TestSpec} from './runner'; +import type {TestCollection} from 'hermione'; +import {BaseRunner, TestSpec} from './runner'; export class SpecificTestRunner extends BaseRunner { private _tests: TestSpec[]; diff --git a/lib/gui/tool-runner/utils.ts b/lib/gui/tool-runner/utils.ts index 723868208..c0732e838 100644 --- a/lib/gui/tool-runner/utils.ts +++ b/lib/gui/tool-runner/utils.ts @@ -50,15 +50,14 @@ export const mergeDatabasesForReuse = async (reportPath: string): Promise await Promise.all(dbPaths.map(p => fs.remove(p))); }; -export const filterByEqualDiffSizes = (imagesInfo: TestEqualDiffsData[], refDiffClusters: CoordBounds[]): TestEqualDiffsData[] => { - if (_.isEmpty(refDiffClusters)) { +export const filterByEqualDiffSizes = (imagesInfo: TestEqualDiffsData[], refDiffClusters?: CoordBounds[]): TestEqualDiffsData[] => { + if (!refDiffClusters || _.isEmpty(refDiffClusters)) { return []; } const refDiffSizes = refDiffClusters.map(getDiffClusterSizes); return _.filter(imagesInfo, (imageInfo) => { - // TODO: get rid of type assertion here const imageInfoFail = imageInfo as ImageInfoFail; const imageDiffSizes = imageInfoFail.diffClusters?.map(getDiffClusterSizes) ?? []; diff --git a/lib/image-handler.ts b/lib/image-handler.ts index aa988d54a..791d07a40 100644 --- a/lib/image-handler.ts +++ b/lib/image-handler.ts @@ -42,6 +42,11 @@ export interface ImageHandlerOptions { reportPath: string; } +interface TestSpec { + fullName: string; + browserId: string; +} + export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { private _imageStore: ImageStore; private _imagesSaver: ImagesSaver; @@ -235,7 +240,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { this._imagesSaver = newImagesSaver; } - updateCacheExpectedPath(testResult: ReporterTestResultPlain, stateName: string, expectedPath: string): void { + updateCacheExpectedPath(testResult: TestSpec, stateName: string, expectedPath: string): void { const key = this._getExpectedKey(testResult, stateName); if (expectedPath) { @@ -245,7 +250,7 @@ export class ImageHandler extends EventEmitter2 implements ImagesInfoFormatter { } } - private _getExpectedKey(testResult: ReporterTestResultPlain, stateName?: string): string { + private _getExpectedKey(testResult: TestSpec, stateName?: string): string { const shortTestId = getShortMD5(mkTestId(testResult.fullName, testResult.browserId)); return shortTestId + '#' + stateName; diff --git a/lib/report-builder/gui.ts b/lib/report-builder/gui.ts index 9f5a91a68..e7b42ef22 100644 --- a/lib/report-builder/gui.ts +++ b/lib/report-builder/gui.ts @@ -21,7 +21,7 @@ interface UndoAcceptImageResult { shouldRevertReference: boolean; } -interface GuiReportBuilderResult { +export interface GuiReportBuilderResult { tree: Tree; skips: SkipItem[]; config: ConfigForStaticFile & {customGui: ReporterConfig['customGui']}; diff --git a/package.json b/package.json index 776ea1038..9096a8995 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "@typescript-eslint/parser": "^5.60.0", "app-module-path": "^2.2.0", "babel-loader": "^9.1.3", + "buffer": "^6.0.3", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "classnames": "^2.2.5", @@ -134,6 +135,7 @@ "eslint": "^8.43.0", "eslint-config-gemini-testing": "^2.8.0", "eslint-plugin-react": "^7.32.2", + "events": "^3.3.0", "fork-ts-checker-webpack-plugin": "^9.0.0", "hermione": "^8.0.0-beta.2", "hermione-global-hook": "^1.0.1", diff --git a/webpack.common.js b/webpack.common.js index 15f58363a..c4de4521d 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -20,7 +20,9 @@ module.exports = { resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], fallback: { + buffer: require.resolve('buffer'), crypto: require.resolve('crypto-browserify'), + events: require.resolve('events'), path: require.resolve('path-browserify'), stream: require.resolve('stream-browserify') }