├── .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 | [](https://travis-ci.org/mattlewis92/webpack-retry-chunk-load-plugin)
4 | [](https://codecov.io/gh/mattlewis92/webpack-retry-chunk-load-plugin)
5 | [](http://badge.fury.io/js/webpack-retry-chunk-load-plugin)
6 | [](https://github.com/mattlewis92/webpack-retry-chunk-load-plugin/issues)
7 | [](https://github.com/mattlewis92/webpack-retry-chunk-load-plugin/stargazers)
8 | [](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 |
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 |
--------------------------------------------------------------------------------