build: use webpack-retry-chunk-load-plugin (#6885)

* build: use webpack retry chunk load plugin

* fix

* dedupe

* lint

* retry backoff

* reduce from 1000 to 500ms

* add cache bust query

* rm cache bust

* 3

* cache bust

* Update craco.config.cjs

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
This commit is contained in:
Vignesh Mohankumar 2023-07-06 17:54:49 -04:00 committed by GitHub
parent 07eb9eb9a2
commit 3a0f6920d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 33 additions and 111 deletions

@ -13,7 +13,6 @@ module.exports = {
files: ['**/*'],
rules: {
'multiline-comment-style': ['error', 'separate-lines'],
'rulesdir/enforce-retry-on-import': 'error',
'rulesdir/no-undefined-or': 'error',
},
},

@ -6,6 +6,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
const commitHash = execSync('git rev-parse HEAD').toString().trim()
const isProduction = process.env.NODE_ENV === 'production'
@ -93,6 +94,16 @@ module.exports = {
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
new VanillaExtractPlugin({ identifiers: 'short' }),
new RetryChunkLoadPlugin({
cacheBust: `function() {
return 'cache-bust=' + Date.now();
}`,
// Retries with exponential backoff (500ms, 1000ms, 2000ms).
retryDelay: `function(retryAttempt) {
return 2 ** (retryAttempt - 1) * 500;
}`,
maxRetries: 3,
}),
],
configure: (webpackConfig) => {
// Configure webpack plugins:

@ -1,36 +0,0 @@
/* eslint-env node */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce use of retry() for dynamic imports',
category: 'Best Practices',
recommended: false,
},
schema: [],
},
create(context) {
return {
ImportExpression(node) {
const grandParent = node.parent.parent
if (
!(
grandParent &&
grandParent.type === 'CallExpression' &&
// Technically, we are only checking that a function named `retry` wraps the dynamic import.
// We do not go as far as enforcing that it is import('utils/retry').retry
grandParent.callee.name === 'retry' &&
grandParent.arguments.length === 1 &&
grandParent.arguments[0].type === 'ArrowFunctionExpression'
)
) {
context.report({
node,
message: 'Dynamic import should be wrapped in retry (see `utils/retry.ts`): `retry(() => import(...))`',
})
}
},
}
},
}

@ -127,6 +127,7 @@
"ts-transform-graphql-tag": "^0.2.1",
"typechain": "^5.0.0",
"typescript": "^4.4.3",
"webpack-retry-chunk-load-plugin": "^3.1.1",
"yarn-deduplicate": "^6.0.0"
},
"dependencies": {

@ -8,11 +8,10 @@ import useAccountRiskCheck from 'hooks/useAccountRiskCheck'
import { lazy } from 'react'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import { retry } from 'utils/retry'
const Bag = lazy(() => retry(() => import('nft/components/bag/Bag')))
const TransactionCompleteModal = lazy(() => retry(() => import('nft/components/collection/TransactionCompleteModal')))
const AirdropModal = lazy(() => retry(() => import('components/AirdropModal')))
const Bag = lazy(() => import('nft/components/bag/Bag'))
const TransactionCompleteModal = lazy(() => import('nft/components/collection/TransactionCompleteModal'))
const AirdropModal = lazy(() => import('components/AirdropModal'))
export default function TopLevelModals() {
const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM)

@ -35,7 +35,6 @@ import {
} from 'make-plural/plurals'
import { PluralCategory } from 'make-plural/plurals'
import { ReactNode, useEffect } from 'react'
import { retry } from 'utils/retry'
type LocalePlural = {
[key in SupportedLocale]: (n: number | string, ord?: boolean) => PluralCategory
@ -80,7 +79,7 @@ const plurals: LocalePlural = {
export async function dynamicActivate(locale: SupportedLocale) {
i18n.loadLocaleData(locale, { plurals: () => plurals[locale] })
try {
const catalog = await retry(() => import(`locales/${locale}.js`))
const catalog = await import(`locales/${locale}.js`)
// Bundlers will either export it as default or as a named export named default.
i18n.load(locale, catalog.messages || catalog.default.messages)
} catch (error: unknown) {

@ -18,7 +18,6 @@ import { flexRowNoWrap } from 'theme/styles'
import { Z_INDEX } from 'theme/zIndex'
import { STATSIG_DUMMY_KEY } from 'tracing'
import { getEnvName } from 'utils/env'
import { retry } from 'utils/retry'
import { getCurrentPageFromLocation } from 'utils/urlRoutes'
import { getCLS, getFCP, getFID, getLCP, Metric } from 'web-vitals'
@ -46,12 +45,12 @@ import Swap from './Swap'
import { RedirectPathToSwapOnly } from './Swap/redirects'
import Tokens from './Tokens'
const TokenDetails = lazy(() => retry(() => import('./TokenDetails')))
const Vote = lazy(() => retry(() => import('./Vote')))
const NftExplore = lazy(() => retry(() => import('nft/pages/explore')))
const Collection = lazy(() => retry(() => import('nft/pages/collection')))
const Profile = lazy(() => retry(() => import('nft/pages/profile/profile')))
const Asset = lazy(() => retry(() => import('nft/pages/asset/Asset')))
const TokenDetails = lazy(() => import('./TokenDetails'))
const Vote = lazy(() => import('./Vote'))
const NftExplore = lazy(() => import('nft/pages/explore'))
const Collection = lazy(() => import('nft/pages/collection'))
const Profile = lazy(() => import('nft/pages/profile/profile'))
const Asset = lazy(() => import('nft/pages/asset/Asset'))
const BodyWrapper = styled.div`
display: flex;

@ -1,26 +0,0 @@
import { retry } from './retry'
describe('retry function', () => {
it('should resolve when function is successful', async () => {
const expectedResult = 'Success'
const mockFn = jest.fn().mockResolvedValue(expectedResult)
const result = await retry(mockFn)
expect(result).toEqual(expectedResult)
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('should retry the specified number of times before rejecting', async () => {
const error = new Error('Failure')
const mockFn = jest.fn().mockRejectedValue(error)
await expect(retry(mockFn, 3, 1)).rejects.toEqual(error)
expect(mockFn).toHaveBeenCalledTimes(3)
})
it('should resolve when function is successful on the second attempt', async () => {
const expectedResult = 'Success'
const mockFn = jest.fn().mockRejectedValueOnce(new Error('Failure')).mockResolvedValue(expectedResult)
const result = await retry(mockFn, 3, 1)
expect(result).toEqual(expectedResult)
expect(mockFn).toHaveBeenCalledTimes(2)
})
})

@ -1,29 +0,0 @@
/**
* Executes a Promise-based function multiple times with exponential backoff (doubling).
* @returns the result of the original function's final attempt.
*
* @example
* ```ts
* const fetchWithRetry = retry(fetchData, 5, 2000);
* fetchWithRetry.then(data => console.log(data)).catch(error => console.error(error));
* ```
*/
export function retry<T>(fn: () => Promise<T>, retries = 3, delay = 1000): Promise<T> {
return new Promise((resolve, reject) => {
const attempt = async (attempts: number, currentDelay: number) => {
try {
const result = await fn()
resolve(result)
} catch (error) {
if (attempts === retries) {
reject(error)
} else {
const exponentialBackoffDelay = currentDelay * 2
setTimeout(() => attempt(attempts + 1, exponentialBackoffDelay), currentDelay)
}
}
}
attempt(1, delay)
})
}

@ -1,8 +1,6 @@
import type { TokenInfo, TokenList } from '@uniswap/token-lists'
import type { ValidateFunction } from 'ajv'
import { retry } from './retry'
enum ValidationSchema {
LIST = 'list',
TOKENS = 'tokens',
@ -19,16 +17,16 @@ async function validate(schema: ValidationSchema, data: unknown): Promise<unknow
let validatorImport
switch (schema) {
case ValidationSchema.LIST:
validatorImport = await retry(() => import('utils/__generated__/validateTokenList'))
validatorImport = await import('utils/__generated__/validateTokenList')
break
case ValidationSchema.TOKENS:
validatorImport = await retry(() => import('utils/__generated__/validateTokens'))
validatorImport = await import('utils/__generated__/validateTokens')
break
default:
throw new Error('No validation function specified for token list schema')
}
const [, validatorModule] = await Promise.all([retry(() => import('ajv')), validatorImport])
const [, validatorModule] = await Promise.all([import('ajv'), validatorImport])
const validator = validatorModule.default as ValidateFunction
if (validator?.(data)) {
return data

@ -16093,7 +16093,7 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^2.1.2, prettier@^2.8.0, prettier@^2.8.8:
prettier@^2.1.2, prettier@^2.6.2, prettier@^2.8.0, prettier@^2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
@ -19439,6 +19439,13 @@ webpack-merge@^5.8.0:
clone-deep "^4.0.1"
wildcard "^2.0.0"
webpack-retry-chunk-load-plugin@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/webpack-retry-chunk-load-plugin/-/webpack-retry-chunk-load-plugin-3.1.1.tgz#44aefc21abd01769ecd07f9a200a58a78caf930c"
integrity sha512-BKq/7EcelyWUUI6SeBaUKB1G+fSZP0rlxIwRQ+aO6mK5tffljaHdpJ4I2q54rpaaKjSbwbZRQlaITXe93SL9nA==
dependencies:
prettier "^2.6.2"
webpack-sources@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"