├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── index.ts ├── tea.yaml ├── test ├── e2e │ ├── server.ts │ └── webpack.config.ts └── integration │ ├── __snapshots__ │ └── test.ts.snap │ ├── fixtures │ ├── async.ts │ ├── index-no-import.ts │ └── index.ts │ ├── test.ts │ └── utils │ └── webpack.ts ├── tsconfig.build.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .idea 4 | test/e2e/dist 5 | dist/ 6 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json'), 3 | "bracketSpacing": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - '12' 5 | - '14' 6 | - '16' 7 | 8 | script: npm run ci 9 | 10 | notifications: 11 | email: false 12 | 13 | cache: 14 | directories: 15 | - node_modules 16 | 17 | after_success: 18 | - npm run codecov 19 | 20 | dist: trusty 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Matt Lewis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webpack-retry-chunk-load-plugin 2 | 3 | [![Build Status](https://travis-ci.org/mattlewis92/webpack-retry-chunk-load-plugin.svg?branch=master)](https://travis-ci.org/mattlewis92/webpack-retry-chunk-load-plugin) 4 | [![codecov](https://codecov.io/gh/mattlewis92/webpack-retry-chunk-load-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/mattlewis92/webpack-retry-chunk-load-plugin) 5 | [![npm version](https://badge.fury.io/js/webpack-retry-chunk-load-plugin.svg)](http://badge.fury.io/js/webpack-retry-chunk-load-plugin) 6 | [![GitHub issues](https://img.shields.io/github/issues/mattlewis92/webpack-retry-chunk-load-plugin.svg)](https://github.com/mattlewis92/webpack-retry-chunk-load-plugin/issues) 7 | [![GitHub stars](https://img.shields.io/github/stars/mattlewis92/webpack-retry-chunk-load-plugin.svg)](https://github.com/mattlewis92/webpack-retry-chunk-load-plugin/stargazers) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/mattlewis92/webpack-retry-chunk-load-plugin/master/LICENSE) 9 | 10 | A webpack plugin to retry loading of async chunks that failed to load 11 | 12 | screenshot 2018-10-24 at 21 47 39 13 | 14 | ## Usage 15 | 16 | ```javascript 17 | // webpack.config.js 18 | const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin'); 19 | 20 | plugins: [ 21 | new RetryChunkLoadPlugin({ 22 | // optional stringified function to get the cache busting query string appended to the script src 23 | // if not set will default to appending the string `?cache-bust=true` 24 | cacheBust: `function() { 25 | return Date.now(); 26 | }`, 27 | // optional value to set the amount of time in milliseconds before trying to load the chunk again. Default is 0 28 | // if string, value must be code to generate a delay value. Receives retryCount as argument 29 | // e.g. `function(retryAttempt) { return retryAttempt * 1000 }` 30 | retryDelay: 3000, 31 | // optional value to set the maximum number of retries to load the chunk. Default is 1 32 | maxRetries: 5, 33 | // optional list of chunks to which retry script should be injected 34 | // if not set will add retry script to all chunks that have webpack script loading 35 | chunks: ['chunkName'], 36 | // optional code to be executed in the browser context if after all retries chunk is not loaded. 37 | // if not set - nothing will happen and error will be returned to the chunk loader. 38 | lastResortScript: "window.location.href='/500.html';", 39 | }), 40 | ]; 41 | ``` 42 | 43 | ### Webpack compatibility 44 | 45 | | Webpack version | webpack-retry-chunk-load-plugin version | 46 | | --------------- | --------------------------------------- | 47 | | 5.x | 2.x | 48 | | 4.x | 1.x | 49 | 50 | ### angular cli 51 | 52 | To use this with the angular CLI you can use the fantastic [`angular-builders`](https://github.com/meltedspark/angular-builders) project to extend the built in webpack config 53 | 54 | ## License 55 | 56 | MIT 57 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | coveragePathIgnorePatterns: ['/test/', '/node_modules/'], 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | testTimeout: 60000, 7 | modulePathIgnorePatterns: ['/dist/'], 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-retry-chunk-load-plugin", 3 | "version": "3.1.1", 4 | "description": "A webpack plugin to retry loading of chunks that failed to load", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "peerDependencies": { 11 | "webpack": ">=5.0.0" 12 | }, 13 | "devDependencies": { 14 | "@types/express": "^4.17.13", 15 | "@types/jest": "^27.5.0", 16 | "@types/memory-fs": "^0.3.3", 17 | "@types/node": "^16.11.33", 18 | "@types/webpack": "^5.28.0", 19 | "codecov": "^3.8.1", 20 | "express": "^4.18.1", 21 | "gts": "^3.1.0", 22 | "html-webpack-plugin": "^5.3.1", 23 | "husky": "^5.2.0", 24 | "jest": "^27.5.1", 25 | "memory-fs": "^0.5.0", 26 | "nodemon": "^2.0.16", 27 | "pretty-quick": "^3.1.3", 28 | "ts-jest": "^27.1.4", 29 | "ts-loader": "^9.3.0", 30 | "ts-node": "^10.7.0", 31 | "typescript": "^4.6.4", 32 | "webpack": "^5.72.1", 33 | "webpack-cli": "^4.9.2" 34 | }, 35 | "scripts": { 36 | "lint": "gts lint", 37 | "test": "jest", 38 | "test:watch": "npm t -- --watch", 39 | "test:ci": "npm t -- --runInBand --coverage", 40 | "codecov": "cat coverage/lcov.info | codecov", 41 | "test:e2e": "webpack --config test/e2e/webpack.config.ts && nodemon test/e2e/server.ts", 42 | "ci": "npm run lint && npm run test:ci", 43 | "prepare": "npm run compile", 44 | "clean": "gts clean", 45 | "compile": "tsc -p tsconfig.build.json", 46 | "fix": "gts fix", 47 | "pretest": "npm run compile", 48 | "posttest": "npm run lint" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/mattlewis92/webpack-retry-chunk-load-plugin.git" 53 | }, 54 | "keywords": [ 55 | "webpack", 56 | "webpack plugin" 57 | ], 58 | "author": "Matt Lewis", 59 | "license": "MIT", 60 | "bugs": { 61 | "url": "https://github.com/mattlewis92/webpack-retry-chunk-load-plugin/issues" 62 | }, 63 | "homepage": "https://github.com/mattlewis92/webpack-retry-chunk-load-plugin#readme", 64 | "dependencies": { 65 | "prettier": "^2.6.2" 66 | }, 67 | "files": [ 68 | "dist" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from 'prettier'; 2 | import { Compiler, RuntimeGlobals } from 'webpack'; 3 | 4 | const pluginName = 'RetryChunkLoadPlugin'; 5 | 6 | export interface RetryChunkLoadPluginOptions { 7 | /** 8 | * optional stringified function to get the cache busting query string appended to the script src 9 | * if not set will default to appending the string `?cache-bust=true` 10 | */ 11 | cacheBust?: string; 12 | /** 13 | * optional list of chunks to which retry script should be injected 14 | * if not set will add retry script to all chunks that have webpack script loading 15 | */ 16 | chunks?: string[]; 17 | /** 18 | * optional code to be executed in the browser context if after all retries chunk is not loaded. 19 | * if not set - nothing will happen and error will be returned to the chunk loader. 20 | */ 21 | lastResortScript?: string; 22 | /** 23 | * optional value to set the maximum number of retries to load the chunk. Default is 1 24 | */ 25 | maxRetries?: number; 26 | /** 27 | * optional number value to set the amount of time in milliseconds before trying to load the chunk again. Default is 0 28 | * if string, value must be code to generate a delay value. Receives retryCount as argument 29 | * e.g. `function(retryAttempt) { return retryAttempt * 1000 }` 30 | */ 31 | retryDelay?: number | string; 32 | } 33 | 34 | export class RetryChunkLoadPlugin { 35 | options: RetryChunkLoadPluginOptions; 36 | 37 | constructor(options: RetryChunkLoadPluginOptions = {}) { 38 | this.options = Object.assign({}, options); 39 | } 40 | 41 | apply(compiler: Compiler) { 42 | compiler.hooks.thisCompilation.tap(pluginName, compilation => { 43 | const { mainTemplate, runtimeTemplate } = compilation; 44 | const maxRetryValueFromOptions = Number(this.options.maxRetries); 45 | const maxRetries = 46 | Number.isInteger(maxRetryValueFromOptions) && 47 | maxRetryValueFromOptions > 0 48 | ? maxRetryValueFromOptions 49 | : 1; 50 | const getCacheBustString = () => 51 | this.options.cacheBust 52 | ? ` 53 | (${this.options.cacheBust})(); 54 | ` 55 | : '"cache-bust=true"'; 56 | mainTemplate.hooks.localVars.tap( 57 | { name: pluginName, stage: 1 }, 58 | (source, chunk) => { 59 | const currentChunkName = chunk.name; 60 | const addRetryCode = 61 | !this.options.chunks || 62 | this.options.chunks.includes(currentChunkName); 63 | 64 | const getRetryDelay = 65 | typeof this.options.retryDelay === 'string' 66 | ? this.options.retryDelay 67 | : `function() { return ${this.options.retryDelay || 0} }`; 68 | 69 | if (!addRetryCode) return source; 70 | const script = runtimeTemplate.iife( 71 | '', 72 | ` 73 | if(typeof ${RuntimeGlobals.require} !== "undefined") { 74 | var oldGetScript = ${RuntimeGlobals.getChunkScriptFilename}; 75 | var oldLoadScript = ${RuntimeGlobals.ensureChunk}; 76 | var queryMap = {}; 77 | var countMap = {}; 78 | var getRetryDelay = ${getRetryDelay} 79 | ${RuntimeGlobals.getChunkScriptFilename} = function(chunkId){ 80 | var result = oldGetScript(chunkId); 81 | return result + (queryMap.hasOwnProperty(chunkId) ? '?' + queryMap[chunkId] : ''); 82 | }; 83 | ${RuntimeGlobals.ensureChunk} = function(chunkId){ 84 | var result = oldLoadScript(chunkId); 85 | return result.catch(function(error){ 86 | var retries = countMap.hasOwnProperty(chunkId) ? countMap[chunkId] : ${maxRetries}; 87 | if (retries < 1) { 88 | var realSrc = oldGetScript(chunkId); 89 | error.message = 'Loading chunk ' + chunkId + ' failed after ${maxRetries} retries.\\n(' + realSrc + ')'; 90 | error.request = realSrc;${ 91 | this.options.lastResortScript 92 | ? this.options.lastResortScript 93 | : '' 94 | } 95 | throw error; 96 | } 97 | return new Promise(function (resolve) { 98 | var retryAttempt = ${maxRetries} - retries + 1; 99 | setTimeout(function () { 100 | var retryAttemptString = '&retry-attempt=' + retryAttempt; 101 | var cacheBust = ${getCacheBustString()} + retryAttemptString; 102 | queryMap[chunkId] = cacheBust; 103 | countMap[chunkId] = retries - 1; 104 | resolve(${RuntimeGlobals.ensureChunk}(chunkId)); 105 | }, getRetryDelay(retryAttempt)) 106 | }) 107 | }); 108 | }; 109 | }` 110 | ); 111 | return ( 112 | source + 113 | prettier.format(script, { 114 | trailingComma: 'es5', 115 | singleQuote: true, 116 | parser: 'babel', 117 | }) 118 | ); 119 | } 120 | ); 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xf0ce7f58D34fF3301B03989abFB4e42533ccDf78' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /test/e2e/server.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as express from 'express'; 3 | 4 | const app = express(); 5 | const port = 3000; 6 | 7 | app.get('/test_integration_fixtures_async_ts.js', (request, response, next) => { 8 | if ( 9 | request.query['cache-bust'] === 'true' && 10 | request.query['retry-attempt'] === '5' 11 | ) { 12 | next(); 13 | } else { 14 | response.status(500).send('fail'); 15 | } 16 | }); 17 | 18 | app.use(express.static(path.join(__dirname, 'dist'))); 19 | 20 | app.listen(port, () => console.log(`Example app listening on port ${port}!`)); 21 | -------------------------------------------------------------------------------- /test/e2e/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import type { Configuration } from 'webpack'; 3 | import { RetryChunkLoadPlugin } from '../../src'; 4 | import HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | module.exports = { 7 | devtool: false, 8 | entry: path.join(__dirname, '..', 'integration', 'fixtures', 'index.ts'), 9 | output: { path: path.join(__dirname, 'dist') }, 10 | mode: 'development', 11 | plugins: [ 12 | new HtmlWebpackPlugin(), 13 | new RetryChunkLoadPlugin({ maxRetries: 5 }), 14 | ], 15 | resolve: { extensions: ['.ts'] }, 16 | } as Configuration; 17 | -------------------------------------------------------------------------------- /test/integration/__snapshots__/test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`given config {"chunks":["main"],"lastResortScript":"window.location.href='/500.html'"}, match snapshot 1`] = ` 4 | "/******/ (() => { // webpackBootstrap 5 | /******/ \\"use strict\\"; 6 | /******/ var __webpack_modules__ = ({ 7 | 8 | /***/ \\"./test/integration/fixtures/async.ts\\": 9 | /*!********************************************!*\\\\ 10 | !*** ./test/integration/fixtures/async.ts ***! 11 | \\\\********************************************/ 12 | /***/ ((__unused_webpack_module, exports) => { 13 | 14 | 15 | Object.defineProperty(exports, \\"__esModule\\", ({ value: true })); 16 | exports.foo = void 0; 17 | exports.foo = 'foo'; 18 | console.log('foo loaded!'); 19 | 20 | 21 | /***/ }) 22 | 23 | /******/ }); 24 | /************************************************************************/ 25 | /******/ // The module cache 26 | /******/ var __webpack_module_cache__ = {}; 27 | /******/ 28 | /******/ // The require function 29 | /******/ function __webpack_require__(moduleId) { 30 | /******/ // Check if module is in cache 31 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 32 | /******/ if (cachedModule !== undefined) { 33 | /******/ return cachedModule.exports; 34 | /******/ } 35 | /******/ // Create a new module (and put it into the cache) 36 | /******/ var module = __webpack_module_cache__[moduleId] = { 37 | /******/ // no module.id needed 38 | /******/ // no module.loaded needed 39 | /******/ exports: {} 40 | /******/ }; 41 | /******/ 42 | /******/ // Execute the module function 43 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 44 | /******/ 45 | /******/ // Return the exports of the module 46 | /******/ return module.exports; 47 | /******/ } 48 | /******/ 49 | /************************************************************************/ 50 | /******/ /* webpack/runtime/compat */ 51 | /******/ (() => { 52 | /******/ if (typeof __webpack_require__ !== 'undefined') { 53 | /******/ var oldGetScript = __webpack_require__.u; 54 | /******/ var oldLoadScript = __webpack_require__.e; 55 | /******/ var queryMap = {}; 56 | /******/ var countMap = {}; 57 | /******/ var getRetryDelay = function () { 58 | /******/ return 0; 59 | /******/ }; 60 | /******/ __webpack_require__.u = function (chunkId) { 61 | /******/ var result = oldGetScript(chunkId); 62 | /******/ return ( 63 | /******/ result + 64 | /******/ (queryMap.hasOwnProperty(chunkId) ? '?' + queryMap[chunkId] : '') 65 | /******/ ); 66 | /******/ }; 67 | /******/ __webpack_require__.e = function (chunkId) { 68 | /******/ var result = oldLoadScript(chunkId); 69 | /******/ return result.catch(function (error) { 70 | /******/ var retries = countMap.hasOwnProperty(chunkId) ? countMap[chunkId] : 1; 71 | /******/ if (retries < 1) { 72 | /******/ var realSrc = oldGetScript(chunkId); 73 | /******/ error.message = 74 | /******/ 'Loading chunk ' + 75 | /******/ chunkId + 76 | /******/ ' failed after 1 retries.\\\\n(' + 77 | /******/ realSrc + 78 | /******/ ')'; 79 | /******/ error.request = realSrc; 80 | /******/ window.location.href = '/500.html'; 81 | /******/ throw error; 82 | /******/ } 83 | /******/ return new Promise(function (resolve) { 84 | /******/ var retryAttempt = 1 - retries + 1; 85 | /******/ setTimeout(function () { 86 | /******/ var retryAttemptString = '&retry-attempt=' + retryAttempt; 87 | /******/ var cacheBust = 'cache-bust=true' + retryAttemptString; 88 | /******/ queryMap[chunkId] = cacheBust; 89 | /******/ countMap[chunkId] = retries - 1; 90 | /******/ resolve(__webpack_require__.e(chunkId)); 91 | /******/ }, getRetryDelay(retryAttempt)); 92 | /******/ }); 93 | /******/ }); 94 | /******/ }; 95 | /******/ } 96 | /******/ })(); 97 | /******/ 98 | /******/ 99 | /************************************************************************/ 100 | var __webpack_exports__ = {}; 101 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 102 | (() => { 103 | /*!********************************************!*\\\\ 104 | !*** ./test/integration/fixtures/index.ts ***! 105 | \\\\********************************************/ 106 | 107 | async function run() { 108 | console.log(await Promise.resolve().then(() => __webpack_require__(/*! ./async */ \\"./test/integration/fixtures/async.ts\\"))); 109 | } 110 | run(); 111 | 112 | })(); 113 | 114 | /******/ })() 115 | ;" 116 | `; 117 | 118 | exports[`given config {"chunks":["main"],"retryDelay":"function(retryAttempt) { return retryAttempt * 1000 }"}, match snapshot 1`] = ` 119 | "/******/ (() => { // webpackBootstrap 120 | /******/ \\"use strict\\"; 121 | /******/ var __webpack_modules__ = ({ 122 | 123 | /***/ \\"./test/integration/fixtures/async.ts\\": 124 | /*!********************************************!*\\\\ 125 | !*** ./test/integration/fixtures/async.ts ***! 126 | \\\\********************************************/ 127 | /***/ ((__unused_webpack_module, exports) => { 128 | 129 | 130 | Object.defineProperty(exports, \\"__esModule\\", ({ value: true })); 131 | exports.foo = void 0; 132 | exports.foo = 'foo'; 133 | console.log('foo loaded!'); 134 | 135 | 136 | /***/ }) 137 | 138 | /******/ }); 139 | /************************************************************************/ 140 | /******/ // The module cache 141 | /******/ var __webpack_module_cache__ = {}; 142 | /******/ 143 | /******/ // The require function 144 | /******/ function __webpack_require__(moduleId) { 145 | /******/ // Check if module is in cache 146 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 147 | /******/ if (cachedModule !== undefined) { 148 | /******/ return cachedModule.exports; 149 | /******/ } 150 | /******/ // Create a new module (and put it into the cache) 151 | /******/ var module = __webpack_module_cache__[moduleId] = { 152 | /******/ // no module.id needed 153 | /******/ // no module.loaded needed 154 | /******/ exports: {} 155 | /******/ }; 156 | /******/ 157 | /******/ // Execute the module function 158 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 159 | /******/ 160 | /******/ // Return the exports of the module 161 | /******/ return module.exports; 162 | /******/ } 163 | /******/ 164 | /************************************************************************/ 165 | /******/ /* webpack/runtime/compat */ 166 | /******/ (() => { 167 | /******/ if (typeof __webpack_require__ !== 'undefined') { 168 | /******/ var oldGetScript = __webpack_require__.u; 169 | /******/ var oldLoadScript = __webpack_require__.e; 170 | /******/ var queryMap = {}; 171 | /******/ var countMap = {}; 172 | /******/ var getRetryDelay = function (retryAttempt) { 173 | /******/ return retryAttempt * 1000; 174 | /******/ }; 175 | /******/ __webpack_require__.u = function (chunkId) { 176 | /******/ var result = oldGetScript(chunkId); 177 | /******/ return ( 178 | /******/ result + 179 | /******/ (queryMap.hasOwnProperty(chunkId) ? '?' + queryMap[chunkId] : '') 180 | /******/ ); 181 | /******/ }; 182 | /******/ __webpack_require__.e = function (chunkId) { 183 | /******/ var result = oldLoadScript(chunkId); 184 | /******/ return result.catch(function (error) { 185 | /******/ var retries = countMap.hasOwnProperty(chunkId) ? countMap[chunkId] : 1; 186 | /******/ if (retries < 1) { 187 | /******/ var realSrc = oldGetScript(chunkId); 188 | /******/ error.message = 189 | /******/ 'Loading chunk ' + 190 | /******/ chunkId + 191 | /******/ ' failed after 1 retries.\\\\n(' + 192 | /******/ realSrc + 193 | /******/ ')'; 194 | /******/ error.request = realSrc; 195 | /******/ throw error; 196 | /******/ } 197 | /******/ return new Promise(function (resolve) { 198 | /******/ var retryAttempt = 1 - retries + 1; 199 | /******/ setTimeout(function () { 200 | /******/ var retryAttemptString = '&retry-attempt=' + retryAttempt; 201 | /******/ var cacheBust = 'cache-bust=true' + retryAttemptString; 202 | /******/ queryMap[chunkId] = cacheBust; 203 | /******/ countMap[chunkId] = retries - 1; 204 | /******/ resolve(__webpack_require__.e(chunkId)); 205 | /******/ }, getRetryDelay(retryAttempt)); 206 | /******/ }); 207 | /******/ }); 208 | /******/ }; 209 | /******/ } 210 | /******/ })(); 211 | /******/ 212 | /******/ 213 | /************************************************************************/ 214 | var __webpack_exports__ = {}; 215 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 216 | (() => { 217 | /*!********************************************!*\\\\ 218 | !*** ./test/integration/fixtures/index.ts ***! 219 | \\\\********************************************/ 220 | 221 | async function run() { 222 | console.log(await Promise.resolve().then(() => __webpack_require__(/*! ./async */ \\"./test/integration/fixtures/async.ts\\"))); 223 | } 224 | run(); 225 | 226 | })(); 227 | 228 | /******/ })() 229 | ;" 230 | `; 231 | 232 | exports[`given config {"chunks":["main"],"retryDelay":3000}, match snapshot 1`] = ` 233 | "/******/ (() => { // webpackBootstrap 234 | /******/ \\"use strict\\"; 235 | /******/ var __webpack_modules__ = ({ 236 | 237 | /***/ \\"./test/integration/fixtures/async.ts\\": 238 | /*!********************************************!*\\\\ 239 | !*** ./test/integration/fixtures/async.ts ***! 240 | \\\\********************************************/ 241 | /***/ ((__unused_webpack_module, exports) => { 242 | 243 | 244 | Object.defineProperty(exports, \\"__esModule\\", ({ value: true })); 245 | exports.foo = void 0; 246 | exports.foo = 'foo'; 247 | console.log('foo loaded!'); 248 | 249 | 250 | /***/ }) 251 | 252 | /******/ }); 253 | /************************************************************************/ 254 | /******/ // The module cache 255 | /******/ var __webpack_module_cache__ = {}; 256 | /******/ 257 | /******/ // The require function 258 | /******/ function __webpack_require__(moduleId) { 259 | /******/ // Check if module is in cache 260 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 261 | /******/ if (cachedModule !== undefined) { 262 | /******/ return cachedModule.exports; 263 | /******/ } 264 | /******/ // Create a new module (and put it into the cache) 265 | /******/ var module = __webpack_module_cache__[moduleId] = { 266 | /******/ // no module.id needed 267 | /******/ // no module.loaded needed 268 | /******/ exports: {} 269 | /******/ }; 270 | /******/ 271 | /******/ // Execute the module function 272 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 273 | /******/ 274 | /******/ // Return the exports of the module 275 | /******/ return module.exports; 276 | /******/ } 277 | /******/ 278 | /************************************************************************/ 279 | /******/ /* webpack/runtime/compat */ 280 | /******/ (() => { 281 | /******/ if (typeof __webpack_require__ !== 'undefined') { 282 | /******/ var oldGetScript = __webpack_require__.u; 283 | /******/ var oldLoadScript = __webpack_require__.e; 284 | /******/ var queryMap = {}; 285 | /******/ var countMap = {}; 286 | /******/ var getRetryDelay = function () { 287 | /******/ return 3000; 288 | /******/ }; 289 | /******/ __webpack_require__.u = function (chunkId) { 290 | /******/ var result = oldGetScript(chunkId); 291 | /******/ return ( 292 | /******/ result + 293 | /******/ (queryMap.hasOwnProperty(chunkId) ? '?' + queryMap[chunkId] : '') 294 | /******/ ); 295 | /******/ }; 296 | /******/ __webpack_require__.e = function (chunkId) { 297 | /******/ var result = oldLoadScript(chunkId); 298 | /******/ return result.catch(function (error) { 299 | /******/ var retries = countMap.hasOwnProperty(chunkId) ? countMap[chunkId] : 1; 300 | /******/ if (retries < 1) { 301 | /******/ var realSrc = oldGetScript(chunkId); 302 | /******/ error.message = 303 | /******/ 'Loading chunk ' + 304 | /******/ chunkId + 305 | /******/ ' failed after 1 retries.\\\\n(' + 306 | /******/ realSrc + 307 | /******/ ')'; 308 | /******/ error.request = realSrc; 309 | /******/ throw error; 310 | /******/ } 311 | /******/ return new Promise(function (resolve) { 312 | /******/ var retryAttempt = 1 - retries + 1; 313 | /******/ setTimeout(function () { 314 | /******/ var retryAttemptString = '&retry-attempt=' + retryAttempt; 315 | /******/ var cacheBust = 'cache-bust=true' + retryAttemptString; 316 | /******/ queryMap[chunkId] = cacheBust; 317 | /******/ countMap[chunkId] = retries - 1; 318 | /******/ resolve(__webpack_require__.e(chunkId)); 319 | /******/ }, getRetryDelay(retryAttempt)); 320 | /******/ }); 321 | /******/ }); 322 | /******/ }; 323 | /******/ } 324 | /******/ })(); 325 | /******/ 326 | /******/ 327 | /************************************************************************/ 328 | var __webpack_exports__ = {}; 329 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 330 | (() => { 331 | /*!********************************************!*\\\\ 332 | !*** ./test/integration/fixtures/index.ts ***! 333 | \\\\********************************************/ 334 | 335 | async function run() { 336 | console.log(await Promise.resolve().then(() => __webpack_require__(/*! ./async */ \\"./test/integration/fixtures/async.ts\\"))); 337 | } 338 | run(); 339 | 340 | })(); 341 | 342 | /******/ })() 343 | ;" 344 | `; 345 | 346 | exports[`given config {"chunks":["main"]}, match snapshot 1`] = ` 347 | "/******/ (() => { // webpackBootstrap 348 | /******/ \\"use strict\\"; 349 | /******/ var __webpack_modules__ = ({ 350 | 351 | /***/ \\"./test/integration/fixtures/async.ts\\": 352 | /*!********************************************!*\\\\ 353 | !*** ./test/integration/fixtures/async.ts ***! 354 | \\\\********************************************/ 355 | /***/ ((__unused_webpack_module, exports) => { 356 | 357 | 358 | Object.defineProperty(exports, \\"__esModule\\", ({ value: true })); 359 | exports.foo = void 0; 360 | exports.foo = 'foo'; 361 | console.log('foo loaded!'); 362 | 363 | 364 | /***/ }) 365 | 366 | /******/ }); 367 | /************************************************************************/ 368 | /******/ // The module cache 369 | /******/ var __webpack_module_cache__ = {}; 370 | /******/ 371 | /******/ // The require function 372 | /******/ function __webpack_require__(moduleId) { 373 | /******/ // Check if module is in cache 374 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 375 | /******/ if (cachedModule !== undefined) { 376 | /******/ return cachedModule.exports; 377 | /******/ } 378 | /******/ // Create a new module (and put it into the cache) 379 | /******/ var module = __webpack_module_cache__[moduleId] = { 380 | /******/ // no module.id needed 381 | /******/ // no module.loaded needed 382 | /******/ exports: {} 383 | /******/ }; 384 | /******/ 385 | /******/ // Execute the module function 386 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 387 | /******/ 388 | /******/ // Return the exports of the module 389 | /******/ return module.exports; 390 | /******/ } 391 | /******/ 392 | /************************************************************************/ 393 | /******/ /* webpack/runtime/compat */ 394 | /******/ (() => { 395 | /******/ if (typeof __webpack_require__ !== 'undefined') { 396 | /******/ var oldGetScript = __webpack_require__.u; 397 | /******/ var oldLoadScript = __webpack_require__.e; 398 | /******/ var queryMap = {}; 399 | /******/ var countMap = {}; 400 | /******/ var getRetryDelay = function () { 401 | /******/ return 0; 402 | /******/ }; 403 | /******/ __webpack_require__.u = function (chunkId) { 404 | /******/ var result = oldGetScript(chunkId); 405 | /******/ return ( 406 | /******/ result + 407 | /******/ (queryMap.hasOwnProperty(chunkId) ? '?' + queryMap[chunkId] : '') 408 | /******/ ); 409 | /******/ }; 410 | /******/ __webpack_require__.e = function (chunkId) { 411 | /******/ var result = oldLoadScript(chunkId); 412 | /******/ return result.catch(function (error) { 413 | /******/ var retries = countMap.hasOwnProperty(chunkId) ? countMap[chunkId] : 1; 414 | /******/ if (retries < 1) { 415 | /******/ var realSrc = oldGetScript(chunkId); 416 | /******/ error.message = 417 | /******/ 'Loading chunk ' + 418 | /******/ chunkId + 419 | /******/ ' failed after 1 retries.\\\\n(' + 420 | /******/ realSrc + 421 | /******/ ')'; 422 | /******/ error.request = realSrc; 423 | /******/ throw error; 424 | /******/ } 425 | /******/ return new Promise(function (resolve) { 426 | /******/ var retryAttempt = 1 - retries + 1; 427 | /******/ setTimeout(function () { 428 | /******/ var retryAttemptString = '&retry-attempt=' + retryAttempt; 429 | /******/ var cacheBust = 'cache-bust=true' + retryAttemptString; 430 | /******/ queryMap[chunkId] = cacheBust; 431 | /******/ countMap[chunkId] = retries - 1; 432 | /******/ resolve(__webpack_require__.e(chunkId)); 433 | /******/ }, getRetryDelay(retryAttempt)); 434 | /******/ }); 435 | /******/ }); 436 | /******/ }; 437 | /******/ } 438 | /******/ })(); 439 | /******/ 440 | /******/ 441 | /************************************************************************/ 442 | var __webpack_exports__ = {}; 443 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 444 | (() => { 445 | /*!********************************************!*\\\\ 446 | !*** ./test/integration/fixtures/index.ts ***! 447 | \\\\********************************************/ 448 | 449 | async function run() { 450 | console.log(await Promise.resolve().then(() => __webpack_require__(/*! ./async */ \\"./test/integration/fixtures/async.ts\\"))); 451 | } 452 | run(); 453 | 454 | })(); 455 | 456 | /******/ })() 457 | ;" 458 | `; 459 | 460 | exports[`given config {"chunks":[]}, match snapshot 1`] = ` 461 | "/******/ (() => { // webpackBootstrap 462 | /******/ \\"use strict\\"; 463 | /******/ var __webpack_modules__ = ({ 464 | 465 | /***/ \\"./test/integration/fixtures/async.ts\\": 466 | /*!********************************************!*\\\\ 467 | !*** ./test/integration/fixtures/async.ts ***! 468 | \\\\********************************************/ 469 | /***/ ((__unused_webpack_module, exports) => { 470 | 471 | 472 | Object.defineProperty(exports, \\"__esModule\\", ({ value: true })); 473 | exports.foo = void 0; 474 | exports.foo = 'foo'; 475 | console.log('foo loaded!'); 476 | 477 | 478 | /***/ }) 479 | 480 | /******/ }); 481 | /************************************************************************/ 482 | /******/ // The module cache 483 | /******/ var __webpack_module_cache__ = {}; 484 | /******/ 485 | /******/ // The require function 486 | /******/ function __webpack_require__(moduleId) { 487 | /******/ // Check if module is in cache 488 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 489 | /******/ if (cachedModule !== undefined) { 490 | /******/ return cachedModule.exports; 491 | /******/ } 492 | /******/ // Create a new module (and put it into the cache) 493 | /******/ var module = __webpack_module_cache__[moduleId] = { 494 | /******/ // no module.id needed 495 | /******/ // no module.loaded needed 496 | /******/ exports: {} 497 | /******/ }; 498 | /******/ 499 | /******/ // Execute the module function 500 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 501 | /******/ 502 | /******/ // Return the exports of the module 503 | /******/ return module.exports; 504 | /******/ } 505 | /******/ 506 | /************************************************************************/ 507 | /******/ /************************************************************************/ 508 | var __webpack_exports__ = {}; 509 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 510 | (() => { 511 | /*!********************************************!*\\\\ 512 | !*** ./test/integration/fixtures/index.ts ***! 513 | \\\\********************************************/ 514 | 515 | async function run() { 516 | console.log(await Promise.resolve().then(() => __webpack_require__(/*! ./async */ \\"./test/integration/fixtures/async.ts\\"))); 517 | } 518 | run(); 519 | 520 | })(); 521 | 522 | /******/ })() 523 | ;" 524 | `; 525 | 526 | exports[`given config undefined, match snapshot 1`] = ` 527 | "/******/ (() => { // webpackBootstrap 528 | /******/ \\"use strict\\"; 529 | /******/ var __webpack_modules__ = ({ 530 | 531 | /***/ \\"./test/integration/fixtures/async.ts\\": 532 | /*!********************************************!*\\\\ 533 | !*** ./test/integration/fixtures/async.ts ***! 534 | \\\\********************************************/ 535 | /***/ ((__unused_webpack_module, exports) => { 536 | 537 | 538 | Object.defineProperty(exports, \\"__esModule\\", ({ value: true })); 539 | exports.foo = void 0; 540 | exports.foo = 'foo'; 541 | console.log('foo loaded!'); 542 | 543 | 544 | /***/ }) 545 | 546 | /******/ }); 547 | /************************************************************************/ 548 | /******/ // The module cache 549 | /******/ var __webpack_module_cache__ = {}; 550 | /******/ 551 | /******/ // The require function 552 | /******/ function __webpack_require__(moduleId) { 553 | /******/ // Check if module is in cache 554 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 555 | /******/ if (cachedModule !== undefined) { 556 | /******/ return cachedModule.exports; 557 | /******/ } 558 | /******/ // Create a new module (and put it into the cache) 559 | /******/ var module = __webpack_module_cache__[moduleId] = { 560 | /******/ // no module.id needed 561 | /******/ // no module.loaded needed 562 | /******/ exports: {} 563 | /******/ }; 564 | /******/ 565 | /******/ // Execute the module function 566 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 567 | /******/ 568 | /******/ // Return the exports of the module 569 | /******/ return module.exports; 570 | /******/ } 571 | /******/ 572 | /************************************************************************/ 573 | /******/ /* webpack/runtime/compat */ 574 | /******/ (() => { 575 | /******/ if (typeof __webpack_require__ !== 'undefined') { 576 | /******/ var oldGetScript = __webpack_require__.u; 577 | /******/ var oldLoadScript = __webpack_require__.e; 578 | /******/ var queryMap = {}; 579 | /******/ var countMap = {}; 580 | /******/ var getRetryDelay = function () { 581 | /******/ return 0; 582 | /******/ }; 583 | /******/ __webpack_require__.u = function (chunkId) { 584 | /******/ var result = oldGetScript(chunkId); 585 | /******/ return ( 586 | /******/ result + 587 | /******/ (queryMap.hasOwnProperty(chunkId) ? '?' + queryMap[chunkId] : '') 588 | /******/ ); 589 | /******/ }; 590 | /******/ __webpack_require__.e = function (chunkId) { 591 | /******/ var result = oldLoadScript(chunkId); 592 | /******/ return result.catch(function (error) { 593 | /******/ var retries = countMap.hasOwnProperty(chunkId) ? countMap[chunkId] : 1; 594 | /******/ if (retries < 1) { 595 | /******/ var realSrc = oldGetScript(chunkId); 596 | /******/ error.message = 597 | /******/ 'Loading chunk ' + 598 | /******/ chunkId + 599 | /******/ ' failed after 1 retries.\\\\n(' + 600 | /******/ realSrc + 601 | /******/ ')'; 602 | /******/ error.request = realSrc; 603 | /******/ throw error; 604 | /******/ } 605 | /******/ return new Promise(function (resolve) { 606 | /******/ var retryAttempt = 1 - retries + 1; 607 | /******/ setTimeout(function () { 608 | /******/ var retryAttemptString = '&retry-attempt=' + retryAttempt; 609 | /******/ var cacheBust = 'cache-bust=true' + retryAttemptString; 610 | /******/ queryMap[chunkId] = cacheBust; 611 | /******/ countMap[chunkId] = retries - 1; 612 | /******/ resolve(__webpack_require__.e(chunkId)); 613 | /******/ }, getRetryDelay(retryAttempt)); 614 | /******/ }); 615 | /******/ }); 616 | /******/ }; 617 | /******/ } 618 | /******/ })(); 619 | /******/ 620 | /******/ 621 | /************************************************************************/ 622 | var __webpack_exports__ = {}; 623 | // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. 624 | (() => { 625 | /*!********************************************!*\\\\ 626 | !*** ./test/integration/fixtures/index.ts ***! 627 | \\\\********************************************/ 628 | 629 | async function run() { 630 | console.log(await Promise.resolve().then(() => __webpack_require__(/*! ./async */ \\"./test/integration/fixtures/async.ts\\"))); 631 | } 632 | run(); 633 | 634 | })(); 635 | 636 | /******/ })() 637 | ;" 638 | `; 639 | -------------------------------------------------------------------------------- /test/integration/fixtures/async.ts: -------------------------------------------------------------------------------- 1 | export const foo = 'foo'; 2 | 3 | console.log('foo loaded!'); 4 | -------------------------------------------------------------------------------- /test/integration/fixtures/index-no-import.ts: -------------------------------------------------------------------------------- 1 | console.log('welcome to my module'); 2 | -------------------------------------------------------------------------------- /test/integration/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | async function run() { 2 | console.log(await import('./async')); 3 | } 4 | 5 | run(); 6 | -------------------------------------------------------------------------------- /test/integration/test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { RetryChunkLoadPluginOptions } from '../../src'; 3 | import webpack from './utils/webpack'; 4 | 5 | const mainOutputFile = path.join(__dirname, 'fixtures', 'dist', 'main.js'); 6 | 7 | const cases: (RetryChunkLoadPluginOptions | undefined)[] = [ 8 | undefined, 9 | { chunks: [] }, 10 | { chunks: ['main'] }, 11 | { chunks: ['main'], retryDelay: 3000 }, 12 | { 13 | chunks: ['main'], 14 | retryDelay: 'function(retryAttempt) { return retryAttempt * 1000 }', 15 | }, 16 | { 17 | chunks: ['main'], 18 | lastResortScript: "window.location.href='/500.html'", 19 | }, 20 | ]; 21 | 22 | test.each(cases)('given config %j, match snapshot', async config => { 23 | const { result, fs } = webpack(config); 24 | await result; 25 | const mainContents = fs.readFileSync(mainOutputFile).toString(); 26 | expect(mainContents).toMatchSnapshot(); 27 | }); 28 | -------------------------------------------------------------------------------- /test/integration/utils/webpack.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as webpack from 'webpack'; 3 | import { RetryChunkLoadPlugin } from '../../../src'; 4 | import MemoryFileSystem = require('memory-fs'); 5 | 6 | export default function ( 7 | pluginOptions = {}, 8 | { fixture = 'index.ts', extend = {} } = {} 9 | ) { 10 | const fs = new MemoryFileSystem(); 11 | 12 | const fixturesDir = path.join(__dirname, '../fixtures'); 13 | 14 | const result = new Promise((resolve, reject) => { 15 | const compiler = webpack({ 16 | mode: 'development', 17 | devtool: false, 18 | entry: path.join(fixturesDir, fixture), 19 | output: { 20 | path: path.join(fixturesDir, 'dist'), 21 | }, 22 | plugins: [new RetryChunkLoadPlugin(pluginOptions)], 23 | resolve: { 24 | extensions: ['.ts', '.js'], 25 | }, 26 | module: { 27 | rules: [{ test: /\.ts?$/, loader: 'ts-loader' }], 28 | }, 29 | ...extend, 30 | }); 31 | 32 | compiler.outputFileSystem = fs; 33 | 34 | compiler.run((error, stats) => { 35 | if (error) { 36 | return reject(error); 37 | } 38 | 39 | return resolve(stats); 40 | }); 41 | }); 42 | 43 | return { fs, result }; 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src/index.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json" 3 | } 4 | --------------------------------------------------------------------------------