@atlaspack/runtime-js 2.12.1-dev.3566 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/lib/JSRuntime.js +48 -17
  3. package/lib/helpers/browser/analytics/analytics.js +11 -0
  4. package/lib/helpers/browser/css-loader.js +3 -3
  5. package/lib/helpers/browser/esm-js-loader-retry.js +66 -45
  6. package/lib/helpers/browser/esm-js-loader-shards.js +7 -0
  7. package/lib/helpers/browser/html-loader.js +1 -1
  8. package/lib/helpers/browser/import-polyfill.js +7 -7
  9. package/lib/helpers/browser/js-loader.js +4 -4
  10. package/lib/helpers/browser/prefetch-loader.js +1 -1
  11. package/lib/helpers/browser/preload-loader.js +1 -1
  12. package/lib/helpers/browser/wasm-loader.js +1 -1
  13. package/lib/helpers/bundle-manifest.js +2 -1
  14. package/lib/helpers/bundle-url.js +36 -18
  15. package/lib/helpers/cacheLoader.js +4 -4
  16. package/lib/helpers/conditional-loader-dev.js +1 -1
  17. package/lib/helpers/get-worker-url.js +1 -1
  18. package/lib/helpers/node/html-loader.js +2 -2
  19. package/lib/helpers/node/js-loader.js +2 -2
  20. package/lib/helpers/node/wasm-loader.js +2 -2
  21. package/lib/helpers/worker/js-loader.js +1 -1
  22. package/lib/helpers/worker/wasm-loader.js +1 -1
  23. package/package.json +8 -8
  24. package/src/JSRuntime.js +98 -26
  25. package/src/helpers/browser/analytics/analytics.d.ts +6 -0
  26. package/src/helpers/browser/analytics/analytics.js +6 -0
  27. package/src/helpers/browser/analytics/analytics.js.flow +8 -0
  28. package/src/helpers/browser/esm-js-loader-retry.js +61 -14
  29. package/src/helpers/browser/esm-js-loader-shards.js +11 -0
  30. package/src/helpers/bundle-manifest.js +4 -1
  31. package/src/helpers/bundle-url.js +37 -27
  32. package/test/analytics.test.ts +47 -0
  33. package/test/bundle-url.test.js +62 -0
  34. package/test/esm-js-loader-retry.test.ts +65 -0
  35. package/LICENSE +0 -201
  36. package/src/helpers/.babelrc +0 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/runtime-js",
3
- "version": "2.12.1-dev.3566+facdfb05f",
3
+ "version": "2.13.0",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -12,15 +12,15 @@
12
12
  "main": "lib/JSRuntime.js",
13
13
  "source": "src/JSRuntime.js",
14
14
  "engines": {
15
- "atlaspack": "^2.12.1-dev.3566+facdfb05f",
15
+ "atlaspack": "^2.12.0",
16
16
  "node": ">= 16.0.0"
17
17
  },
18
18
  "dependencies": {
19
- "@atlaspack/diagnostic": "2.12.1-dev.3566+facdfb05f",
20
- "@atlaspack/feature-flags": "2.12.1-dev.3566+facdfb05f",
21
- "@atlaspack/plugin": "2.12.1-dev.3566+facdfb05f",
22
- "@atlaspack/utils": "2.12.1-dev.3566+facdfb05f",
19
+ "@atlaspack/diagnostic": "2.13.0",
20
+ "@atlaspack/domain-sharding": "2.13.0",
21
+ "@atlaspack/feature-flags": "2.13.0",
22
+ "@atlaspack/plugin": "2.13.0",
23
+ "@atlaspack/utils": "2.13.0",
23
24
  "nullthrows": "^1.1.1"
24
- },
25
- "gitHead": "facdfb05f693e50037a82a4afa101adf093fd8c9"
25
+ }
26
26
  }
package/src/JSRuntime.js CHANGED
@@ -74,6 +74,9 @@ let bundleDependencies = new WeakMap<
74
74
 
75
75
  type JSRuntimeConfig = {|
76
76
  splitManifestThreshold: number,
77
+ domainSharding?: {|
78
+ maxShards: number,
79
+ |},
77
80
  |};
78
81
 
79
82
  let defaultConfig: JSRuntimeConfig = {
@@ -86,6 +89,16 @@ const CONFIG_SCHEMA: SchemaEntity = {
86
89
  splitManifestThreshold: {
87
90
  type: 'number',
88
91
  },
92
+ domainSharding: {
93
+ type: 'object',
94
+ properties: {
95
+ maxShards: {
96
+ type: 'number',
97
+ },
98
+ },
99
+ additionalProperties: false,
100
+ required: ['maxShards'],
101
+ },
89
102
  },
90
103
  additionalProperties: false,
91
104
  };
@@ -181,6 +194,7 @@ export default (new Runtime({
181
194
  bundleGraph,
182
195
  bundleGroup: resolved.value,
183
196
  options,
197
+ shardingConfig: config.domainSharding,
184
198
  });
185
199
 
186
200
  if (loaderRuntime != null) {
@@ -271,7 +285,15 @@ export default (new Runtime({
271
285
  }
272
286
 
273
287
  // URL dependency or not, fall back to including a runtime that exports the url
274
- assets.push(getURLRuntime(dependency, bundle, mainBundle, options));
288
+ assets.push(
289
+ getURLRuntime(
290
+ dependency,
291
+ bundle,
292
+ mainBundle,
293
+ options,
294
+ config.domainSharding,
295
+ ),
296
+ );
275
297
  }
276
298
 
277
299
  // In development, bundles can be created lazily. This means that the parent bundle may not
@@ -297,7 +319,11 @@ export default (new Runtime({
297
319
  );
298
320
  let loaderCode = `require(${JSON.stringify(
299
321
  loader,
300
- )})( ${getAbsoluteUrlExpr(relativePathExpr, bundle)})`;
322
+ )})(${getAbsoluteUrlExpr(
323
+ relativePathExpr,
324
+ bundle,
325
+ config.domainSharding,
326
+ )})`;
301
327
  assets.push({
302
328
  filePath: __filename,
303
329
  code: loaderCode,
@@ -376,12 +402,14 @@ function getLoaderRuntime({
376
402
  bundleGroup,
377
403
  bundleGraph,
378
404
  options,
405
+ shardingConfig,
379
406
  }: {|
380
407
  bundle: NamedBundle,
381
408
  dependency: Dependency,
382
409
  bundleGroup: BundleGroup,
383
410
  bundleGraph: BundleGraph<NamedBundle>,
384
411
  options: PluginOptions,
412
+ shardingConfig: JSRuntimeConfig['domainSharding'],
385
413
  |}): ?RuntimeAsset {
386
414
  let loaders = getLoaders(bundle.env);
387
415
  if (loaders == null) {
@@ -423,6 +451,7 @@ function getLoaderRuntime({
423
451
  function getLoaderForBundle(
424
452
  bundle: NamedBundle,
425
453
  to: NamedBundle,
454
+ shardingConfig: JSRuntimeConfig['domainSharding'],
426
455
  ): string | void {
427
456
  let loader = loaders[to.type];
428
457
  if (!loader) {
@@ -455,11 +484,22 @@ function getLoaderRuntime({
455
484
  return `Promise.resolve(__parcel__require__("./" + ${relativePathExpr}))`;
456
485
  }
457
486
 
458
- let absoluteUrlExpr = shouldUseRuntimeManifest(bundle, options)
459
- ? `require('./helpers/bundle-manifest').resolve(${JSON.stringify(
460
- to.publicId,
461
- )})`
462
- : getAbsoluteUrlExpr(relativePathExpr, bundle);
487
+ let absoluteUrlExpr;
488
+ if (shouldUseRuntimeManifest(bundle, options)) {
489
+ let publicId = JSON.stringify(to.publicId);
490
+ absoluteUrlExpr = `require('./helpers/bundle-manifest').resolve(${publicId})`;
491
+
492
+ if (shardingConfig) {
493
+ absoluteUrlExpr = `require('@atlaspack/domain-sharding').shardUrl(${absoluteUrlExpr}, ${shardingConfig.maxShards})`;
494
+ }
495
+ } else {
496
+ absoluteUrlExpr = getAbsoluteUrlExpr(
497
+ relativePathExpr,
498
+ bundle,
499
+ shardingConfig,
500
+ );
501
+ }
502
+
463
503
  let code = `require(${JSON.stringify(loader)})(${absoluteUrlExpr})`;
464
504
 
465
505
  // In development, clear the require cache when an error occurs so the
@@ -483,22 +523,32 @@ function getLoaderRuntime({
483
523
  bundle,
484
524
  )) {
485
525
  // This bundle has a conditional dependency, we need to load the bundle group
486
-
487
- // Load conditional bundles with helper (and a dev mode with additional hints)
488
- loaderModules.push(
489
- `require('./helpers/conditional-loader${
490
- options.mode === 'development' ? '-dev' : ''
491
- }')('${cond.key}', function (){return Promise.all([${cond.ifTrueBundles
492
- .map((targetBundle) => getLoaderForBundle(bundle, targetBundle))
493
- .join(',')}]);}, function (){return Promise.all([${cond.ifFalseBundles
494
- .map((targetBundle) => getLoaderForBundle(bundle, targetBundle))
495
- .join(',')}]);})`,
526
+ const ifTrueLoaders = cond.ifTrueBundles.map((targetBundle) =>
527
+ getLoaderForBundle(bundle, targetBundle),
496
528
  );
529
+ const ifFalseLoaders = cond.ifFalseBundles.map((targetBundle) =>
530
+ getLoaderForBundle(bundle, targetBundle),
531
+ );
532
+
533
+ if (ifTrueLoaders.length > 0 || ifFalseLoaders.length > 0) {
534
+ // Load conditional bundles with helper (and a dev mode with additional hints)
535
+ loaderModules.push(
536
+ `require('./helpers/conditional-loader${
537
+ options.mode === 'development' ? '-dev' : ''
538
+ }')('${
539
+ cond.key
540
+ }', function (){return Promise.all([${ifTrueLoaders.join(
541
+ ',',
542
+ )}]);}, function (){return Promise.all([${ifFalseLoaders.join(
543
+ ',',
544
+ )}]);})`,
545
+ );
546
+ }
497
547
  }
498
548
  }
499
549
 
500
550
  for (let to of externalBundles) {
501
- let loaderModule = getLoaderForBundle(bundle, to);
551
+ let loaderModule = getLoaderForBundle(bundle, to, shardingConfig);
502
552
  if (loaderModule !== undefined) loaderModules.push(loaderModule);
503
553
  }
504
554
 
@@ -573,7 +623,11 @@ function getLoaderRuntime({
573
623
  let code = [];
574
624
 
575
625
  if (needsEsmLoadPrelude) {
576
- code.push(`let load = require('./helpers/browser/esm-js-loader');`);
626
+ let preludeLoad = shardingConfig
627
+ ? `let load = require('./helpers/browser/esm-js-loader-shards')(${shardingConfig.maxShards});`
628
+ : `let load = require('./helpers/browser/esm-js-loader');`;
629
+
630
+ code.push(preludeLoad);
577
631
  }
578
632
 
579
633
  code.push(`module.exports = ${loaderCode};`);
@@ -673,6 +727,7 @@ function getURLRuntime(
673
727
  from: NamedBundle,
674
728
  to: NamedBundle,
675
729
  options: PluginOptions,
730
+ shardingConfig: JSRuntimeConfig['domainSharding'],
676
731
  ): RuntimeAsset {
677
732
  let relativePathExpr = getRelativePathExpr(from, to, options);
678
733
  let code;
@@ -690,12 +745,19 @@ function getURLRuntime(
690
745
  } else {
691
746
  code += `let bundleURL = require('./helpers/bundle-url');\n`;
692
747
  code += `let url = bundleURL.getBundleURL('${from.publicId}') + ${relativePathExpr};`;
748
+ if (shardingConfig) {
749
+ code += `url = require('@atlaspack/domain-sharding').shardUrl(url, ${shardingConfig.maxShards});`;
750
+ }
693
751
  code += `module.exports = workerURL(url, bundleURL.getOrigin(url), ${String(
694
752
  from.env.outputFormat === 'esmodule',
695
753
  )});`;
696
754
  }
697
755
  } else {
698
- code = `module.exports = ${getAbsoluteUrlExpr(relativePathExpr, from)};`;
756
+ code = `module.exports = ${getAbsoluteUrlExpr(
757
+ relativePathExpr,
758
+ from,
759
+ shardingConfig,
760
+ )};`;
699
761
  }
700
762
 
701
763
  return {
@@ -765,17 +827,27 @@ function getRelativePathExpr(
765
827
  return res;
766
828
  }
767
829
 
768
- function getAbsoluteUrlExpr(relativePathExpr: string, bundle: NamedBundle) {
830
+ function getAbsoluteUrlExpr(
831
+ relativePathExpr: string,
832
+ fromBundle: NamedBundle,
833
+ shardingConfig: JSRuntimeConfig['domainSharding'],
834
+ ) {
769
835
  if (
770
- (bundle.env.outputFormat === 'esmodule' &&
771
- bundle.env.supports('import-meta-url')) ||
772
- bundle.env.outputFormat === 'commonjs'
836
+ (fromBundle.env.outputFormat === 'esmodule' &&
837
+ fromBundle.env.supports('import-meta-url')) ||
838
+ fromBundle.env.outputFormat === 'commonjs'
773
839
  ) {
774
840
  // This will be compiled to new URL(url, import.meta.url) or new URL(url, 'file:' + __filename).
775
841
  return `new __parcel__URL__(${relativePathExpr}).toString()`;
776
- } else {
777
- return `require('./helpers/bundle-url').getBundleURL('${bundle.publicId}') + ${relativePathExpr}`;
778
842
  }
843
+
844
+ const regularBundleUrl = `require('./helpers/bundle-url').getBundleURL('${fromBundle.publicId}') + ${relativePathExpr}`;
845
+
846
+ if (!shardingConfig) {
847
+ return regularBundleUrl;
848
+ }
849
+
850
+ return `require('@atlaspack/domain-sharding').shardUrl(${regularBundleUrl}, ${shardingConfig.maxShards})`;
779
851
  }
780
852
 
781
853
  function shouldUseRuntimeManifest(
@@ -0,0 +1,6 @@
1
+ export type AtlaspackAnalyticsEvent = {
2
+ action: string;
3
+ attributes?: {[key: string]: string | number | boolean};
4
+ };
5
+
6
+ export function sendAnalyticsEvent(event: AtlaspackAnalyticsEvent): void;
@@ -0,0 +1,6 @@
1
+ function sendAnalyticsEvent(detail) {
2
+ const ev = new globalThis.CustomEvent('atlaspack:analytics', {detail});
3
+ globalThis.dispatchEvent(ev);
4
+ }
5
+
6
+ module.exports = {sendAnalyticsEvent};
@@ -0,0 +1,8 @@
1
+ // @flow
2
+
3
+ export type AtlaspackAnalyticsEvent = {|
4
+ action: string,
5
+ attributes?: { [key: string]: string | number | boolean },
6
+ |};
7
+
8
+ declare export function sendAnalyticsEvent(event: AtlaspackAnalyticsEvent): void;
@@ -1,26 +1,73 @@
1
1
  async function load(id) {
2
- if (!parcelRequire.retryState) {
3
- parcelRequire.retryState = {};
4
- }
2
+ // Global state maps the initial url to the completed task.
3
+ // This ensures the same URL is used in subsequent imports
4
+ if (!parcelRequire._retryState) parcelRequire._retryState = {};
5
+ /** @type {Record<string, Promise<void>>} */
6
+ const retryState = parcelRequire._retryState;
7
+
8
+ // The number of retries before rethrowing the error
9
+ const maxRetries = 6;
5
10
 
11
+ // Resolve the request URL from the bundle ID
12
+ const url = require('../bundle-manifest').resolve(id);
13
+
14
+ // Wait for the user to go online before making a request
6
15
  if (!globalThis.navigator.onLine) {
7
- await new Promise((res) =>
8
- globalThis.addEventListener('online', res, {once: true}),
16
+ await new Promise((resolve) =>
17
+ globalThis.addEventListener('online', resolve, {once: true}),
9
18
  );
10
19
  }
11
20
 
12
- let url = require('../bundle-manifest').resolve(id);
21
+ // If the import has not run or is not currently running
22
+ // then start the import retry task. Otherwise reuse the
23
+ // existing result or wait for the current task to complete
24
+ if (!retryState[url]) {
25
+ retryState[url] = (async () => {
26
+ // Try first request with normal import circuit
27
+ try {
28
+ // eslint-disable-next-line no-undef
29
+ return await __parcel__import__(url);
30
+ } catch {
31
+ /**/
32
+ }
13
33
 
14
- if (parcelRequire.retryState[id] != undefined) {
15
- url = `${url}?retry=${parcelRequire.retryState[id]}`;
16
- }
34
+ // Attempt to retry request
35
+ for (let i = 1; i <= maxRetries; i++) {
36
+ try {
37
+ // Wait for an increasing delay time
38
+ const jitter = Math.round(Math.random() * 100);
39
+ const delay = Math.min(Math.pow(2, i), 8) * 1000;
40
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
17
41
 
18
- try {
19
- // eslint-disable-next-line no-undef
20
- return await __parcel__import__(url);
21
- } catch (error) {
22
- parcelRequire.retryState[id] = Date.now();
42
+ // Append the current time to the request URL
43
+ // to ensure it has not been cached by the browser
44
+ // eslint-disable-next-line no-undef
45
+ const result = await __parcel__import__(`${url}?t=${Date.now()}`);
46
+ sendAnalyticsEvent('recovered', url, i);
47
+ return result;
48
+ } catch (error) {
49
+ if (i === maxRetries) {
50
+ sendAnalyticsEvent('failure', url, i);
51
+ throw error;
52
+ }
53
+ sendAnalyticsEvent('progress', url, i);
54
+ }
55
+ }
56
+ })();
23
57
  }
58
+
59
+ return retryState[url];
60
+ }
61
+
62
+ function sendAnalyticsEvent(status, targetUrl, attempt) {
63
+ require('./analytics/analytics.js').sendAnalyticsEvent({
64
+ action: 'importRetry',
65
+ attributes: {
66
+ status,
67
+ targetUrl,
68
+ attempt,
69
+ },
70
+ });
24
71
  }
25
72
 
26
73
  module.exports = load;
@@ -0,0 +1,11 @@
1
+ let load = (maxShards) => (id) => {
2
+ // eslint-disable-next-line no-undef
3
+ return __parcel__import__(
4
+ require('@atlaspack/domain-sharding').shardUrl(
5
+ require('../bundle-manifest').resolve(id),
6
+ maxShards,
7
+ ),
8
+ );
9
+ };
10
+
11
+ module.exports = load;
@@ -1,6 +1,9 @@
1
1
  var mapping = new Map();
2
2
 
3
- function register(baseUrl, manifest) {
3
+ function register(
4
+ /** @type {string} */ baseUrl,
5
+ /** @type {Array<string>} */ manifest, // ['id', 'path', 'id2', 'path2']
6
+ ) {
4
7
  for (var i = 0; i < manifest.length - 1; i += 2) {
5
8
  mapping.set(manifest[i], {
6
9
  baseUrl: baseUrl,
@@ -1,21 +1,42 @@
1
- var bundleURL = {};
2
- function getBundleURLCached(id) {
3
- var value = bundleURL[id];
1
+ const stackTraceUrlRegexp =
2
+ /(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g;
3
+
4
+ const bundleURL = {};
5
+
6
+ /**
7
+ * Retrieves the cached bundle URL for a given identifier.
8
+ * If the URL is not cached, it computes and stores it in the cache.
9
+ *
10
+ * @param {string} id - The identifier for the bundle.
11
+ * @param {Error?} inputError - An error object to extract the stack trace from
12
+ * (for testing purposes).
13
+ * @returns {string} The URL of the bundle, without file name.
14
+ */
15
+ function getBundleURLCached(id, inputError) {
16
+ let value = bundleURL[id];
17
+
4
18
  if (!value) {
5
- value = getBundleURL();
19
+ value = getBundleURL(inputError);
6
20
  bundleURL[id] = value;
7
21
  }
8
22
 
9
23
  return value;
10
24
  }
11
25
 
12
- function getBundleURL() {
26
+ /** Get the URL without the filename (last / segment)
27
+ *
28
+ * @param {string} url
29
+ * @returns {string} The URL with the file name removed
30
+ */
31
+ function getBaseURL(url) {
32
+ return url.slice(0, url.lastIndexOf('/')) + '/';
33
+ }
34
+
35
+ function getBundleURL(inputError) {
13
36
  try {
14
- throw new Error();
37
+ throw inputError ?? new Error();
15
38
  } catch (err) {
16
- var matches = ('' + err.stack).match(
17
- /(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^)\n]+/g,
18
- );
39
+ var matches = ('' + err.stack).match(stackTraceUrlRegexp);
19
40
  if (matches) {
20
41
  // The first two stack frames will be this function and getBundleURLCached.
21
42
  // Use the 3rd one, which will be a runtime in the original bundle.
@@ -26,26 +47,15 @@ function getBundleURL() {
26
47
  return '/';
27
48
  }
28
49
 
29
- function getBaseURL(url) {
30
- return (
31
- ('' + url).replace(
32
- /^((?:https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/.+)\/[^/]+$/,
33
- '$1',
34
- ) + '/'
35
- );
36
- }
37
-
38
- // TODO: Replace uses with `new URL(url).origin` when ie11 is no longer supported.
50
+ /**
51
+ * @param {string} url
52
+ * @returns {string}
53
+ */
39
54
  function getOrigin(url) {
40
- let matches = ('' + url).match(
41
- /(https?|file|ftp|(chrome|moz|safari-web)-extension):\/\/[^/]+/,
42
- );
43
- if (!matches) {
44
- throw new Error('Origin not found');
45
- }
46
- return matches[0];
55
+ return new URL(url).origin;
47
56
  }
48
57
 
58
+ // TODO: convert this file to ESM once HMR issues are resolved
59
+ exports.getOrigin = getOrigin;
49
60
  exports.getBundleURL = getBundleURLCached;
50
61
  exports.getBaseURL = getBaseURL;
51
- exports.getOrigin = getOrigin;
@@ -0,0 +1,47 @@
1
+ import {sendAnalyticsEvent} from '../src/helpers/browser/analytics/analytics.js';
2
+ import {mock} from 'node:test';
3
+ import type {Mock} from 'node:test';
4
+ import assert from 'node:assert';
5
+
6
+ describe('@atlaspack/analytics', () => {
7
+ let dispatchEventMock: Mock<Window['dispatchEvent']>;
8
+
9
+ beforeEach(() => {
10
+ // @ts-expect-error
11
+ globalThis.CustomEvent = MockCustomEvent;
12
+ dispatchEventMock = mock.fn();
13
+ globalThis.dispatchEvent = dispatchEventMock;
14
+ });
15
+
16
+ it('should not throw', () => {
17
+ assert.doesNotThrow(() =>
18
+ sendAnalyticsEvent({
19
+ action: 'test',
20
+ }),
21
+ );
22
+ });
23
+
24
+ it('should raise event on window', () => {
25
+ sendAnalyticsEvent({
26
+ action: 'test',
27
+ });
28
+
29
+ assert.equal(dispatchEventMock.mock.callCount(), 1);
30
+ assert.deepEqual(dispatchEventMock.mock.calls[0].arguments[0], {
31
+ eventName: 'atlaspack:analytics',
32
+ detail: {
33
+ action: 'test',
34
+ },
35
+ });
36
+ });
37
+ });
38
+
39
+ class MockCustomEvent {
40
+ eventName: string;
41
+ detail: any;
42
+
43
+ constructor(eventName: string, options: any = {}) {
44
+ this.eventName = eventName;
45
+ this.detail = options.detail;
46
+ }
47
+ }
@@ -0,0 +1,62 @@
1
+ // @flow
2
+ import assert from 'assert';
3
+
4
+ import {getBaseURL, getBundleURL} from '../src/helpers/bundle-url';
5
+
6
+ const createErrorStack = (url) => {
7
+ // This error stack is copied from a local dev, with a bunch
8
+ // of lines trimmed off the end so it's not unnecessarily long
9
+ return `
10
+ Error
11
+ at Object.getBundleURL (http://localhost:8081/main-bundle.1a2fa8b7.js:15688:29)
12
+ at Object.getBundleURLCached (http://localhost:8081/main-bundle.1a2fa8b7.js:15688:29)
13
+ at a7u9v.6d3ceb6ac67fea50 (${url}:361466:46)
14
+ at newRequire (http://localhost:8081/main-bundle.1a2fa8b7.js:71:24)
15
+ at localRequire (http://localhost:8081/main-bundle.1a2fa8b7.js:84:35)
16
+ at 7H8wc.react-intl-next (http://localhost:8081/main-bundle.1a2fa8b7.js:279746:28)
17
+ at newRequire (http://localhost:8081/main-bundle.1a2fa8b7.js:71:24)
18
+ at localRequire (http://localhost:8081/main-bundle.1a2fa8b7.js:84:35)
19
+ at 1nL5S../manifest (http://localhost:8081/main-bundle.1a2fa8b7.js:279714:17)
20
+ at newRequire (http://localhost:8081/main-bundle.1a2fa8b7.js:71:24)
21
+ at localRequire (http://localhost:8081/main-bundle.1a2fa8b7.js:84:35)
22
+ `.trim();
23
+ };
24
+
25
+ describe('getBaseUrl', () => {
26
+ it('should return the URL with the filename removed', () => {
27
+ const testUrl =
28
+ 'https://bundle-shard-3.assets.example.com/assets/testBundle.123abc.js';
29
+
30
+ assert.equal(
31
+ getBaseURL(testUrl),
32
+ 'https://bundle-shard-3.assets.example.com/assets/',
33
+ );
34
+ });
35
+
36
+ it('should handle domains with no .', () => {
37
+ const testUrl = 'http://localhost/assets/testBundle.123abc.js';
38
+
39
+ assert.equal(getBaseURL(testUrl), 'http://localhost/assets/');
40
+ });
41
+
42
+ it('should handle domains with ports', () => {
43
+ const testUrl = 'http://localhost:8081/assets/testBundle.123abc.js';
44
+
45
+ assert.equal(getBaseURL(testUrl), 'http://localhost:8081/assets/');
46
+ });
47
+ });
48
+
49
+ describe('getBundleURL', () => {
50
+ it('should return the URL of the bundle without the file name', () => {
51
+ const testUrl =
52
+ 'https://bundle-domain.assets.example.com/assets/testBundle.123abc.js';
53
+
54
+ const errorStack = createErrorStack(testUrl);
55
+ const error = new Error();
56
+ error.stack = errorStack;
57
+
58
+ const result = getBundleURL('test-asset', error);
59
+
60
+ assert.equal(result, 'https://bundle-domain.assets.example.com/assets/');
61
+ });
62
+ });
@@ -0,0 +1,65 @@
1
+ import load from '../src/helpers/browser/esm-js-loader-retry.js';
2
+ import bundleManifest from '../src/helpers/bundle-manifest.js';
3
+ import {mock} from 'node:test';
4
+ import type {Mock} from 'node:test';
5
+ import assert from 'node:assert';
6
+
7
+ declare var globalThis: Window & {[key: string]: any};
8
+
9
+ describe('esm-js-loader-retry', () => {
10
+ let mockSetTimeout: Mock<Window['setTimeout']>;
11
+ let mockParcelImport: Mock<() => Promise<void>>;
12
+
13
+ // eslint-disable-next-line require-await
14
+ const importError = async () => {
15
+ throw new Error('TypeError: Failed to fetch dynamically imported module');
16
+ };
17
+
18
+ before(() => {
19
+ bundleManifest.register('http://localhost', ['1', 'foo.js']);
20
+ });
21
+
22
+ beforeEach(() => {
23
+ mockSetTimeout = mock.fn((callback: any, duration: any, ...args: any[]) =>
24
+ callback(),
25
+ );
26
+ globalThis.setTimeout = mockSetTimeout;
27
+
28
+ mockParcelImport = mock.fn(() => Promise.resolve());
29
+ globalThis.__parcel__import__ = mockParcelImport;
30
+
31
+ globalThis.parcelRequire = mock.fn();
32
+ // @ts-expect-error
33
+ globalThis.navigator = {onLine: true};
34
+ globalThis.CustomEvent = globalThis.CustomEvent || class {};
35
+ globalThis.dispatchEvent = mock.fn();
36
+ });
37
+
38
+ it('should not throw', async () => {
39
+ await assert.doesNotReject(() => load('1'));
40
+ });
41
+
42
+ it('should throw if all requests fail', async () => {
43
+ mockParcelImport.mock.mockImplementationOnce(importError, 0);
44
+ mockParcelImport.mock.mockImplementationOnce(importError, 1);
45
+ mockParcelImport.mock.mockImplementationOnce(importError, 2);
46
+ mockParcelImport.mock.mockImplementationOnce(importError, 3);
47
+ mockParcelImport.mock.mockImplementationOnce(importError, 4);
48
+ mockParcelImport.mock.mockImplementationOnce(importError, 5);
49
+ mockParcelImport.mock.mockImplementationOnce(importError, 6);
50
+ await assert.rejects(() => load('1'));
51
+ });
52
+
53
+ it('should resolve if the first request fails', async () => {
54
+ mockParcelImport.mock.mockImplementationOnce(importError, 0);
55
+ await assert.doesNotReject(() => load('1'));
56
+ });
57
+
58
+ it('should resolve if the first few requests fails', async () => {
59
+ mockParcelImport.mock.mockImplementationOnce(importError, 0);
60
+ mockParcelImport.mock.mockImplementationOnce(importError, 1);
61
+ mockParcelImport.mock.mockImplementationOnce(importError, 2);
62
+ mockParcelImport.mock.mockImplementationOnce(importError, 3);
63
+ await assert.doesNotReject(() => load('1'));
64
+ });
65
+ });