@appshell/loader 0.0.1-alpha.0 → 0.0.1

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,25 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 0.0.1 (2023-06-15)
7
+
8
+ **Note:** Version bump only for package @appshell/loader
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.0.1-alpha.1](https://github.com/navaris/appshell/compare/@appshell/loader@0.0.1-alpha.0...@appshell/loader@0.0.1-alpha.1) (2023-06-15)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * hot module replacement support, example updates ([a67d230](https://github.com/navaris/appshell/commit/a67d2303b40f5911373225cc0b4ccf9b67c33d11))
20
+
21
+
22
+
23
+
24
+
6
25
  ## 0.0.1-alpha.0 (2023-03-19)
7
26
 
8
27
  **Note:** Version bump only for package @appshell/loader
@@ -8,14 +8,19 @@ const mockHead = (event: string) => {
8
8
  return element;
9
9
  });
10
10
  const removeChildSpy = jest.spyOn(document.head, 'removeChild').mockReturnThis();
11
+ const containsSpy = jest.spyOn(document.head, 'contains').mockReturnValue(true);
11
12
 
12
- return [appendChildSpy, removeChildSpy];
13
+ return [appendChildSpy, removeChildSpy, containsSpy];
13
14
  };
14
15
 
15
16
  describe('fetchDynamicScript', () => {
17
+ afterEach(() => {
18
+ jest.resetAllMocks();
19
+ });
20
+
16
21
  it('should successfully resolve when valid script url is provided', async () => {
17
22
  mockHead('load');
18
- const url = 'http://test.com/somescript.js';
23
+ const url = 'http://test.com/1/somescript.js';
19
24
  const success = await fetchDynamicScript(url);
20
25
 
21
26
  expect(success).toBe(true);
@@ -23,14 +28,17 @@ describe('fetchDynamicScript', () => {
23
28
 
24
29
  it('should reject when fetching the script results in an error', async () => {
25
30
  mockHead('error');
26
- const url = 'http://test.com/somescript.js';
31
+ const url = 'http://test.com/2/somescript.js';
27
32
 
28
33
  await expect(fetchDynamicScript(url)).rejects.toMatch(/Failed to fetch remote entry/i);
29
34
  });
30
35
 
31
36
  it('should cleanup script tags when finished', async () => {
32
- const [, removeChildSpy] = mockHead('load');
37
+ const [appendChildSpy, removeChildSpy] = mockHead('load');
38
+ const url = 'http://test.com/3/somescript.js';
39
+ await fetchDynamicScript(url);
33
40
 
41
+ expect(appendChildSpy).toHaveBeenCalled();
34
42
  expect(removeChildSpy).toHaveBeenCalled();
35
43
  });
36
44
  });
@@ -80,16 +80,6 @@ describe('loadFederatedComponent', () => {
80
80
  expect(Component).toBe(TestComponent);
81
81
  });
82
82
 
83
- it('should load the federated component from the share scope', async () => {
84
- const scope = 'TestModule2';
85
- const module = './TestComponent';
86
- const shareScope = 'test_scope';
87
-
88
- const Component = await loadFederatedComponent<ComponentType>(scope, module, shareScope);
89
-
90
- expect(Component).toBe(TestComponent);
91
- });
92
-
93
83
  it('should throw if the share scope does not exist', async () => {
94
84
  const scope = 'TestModule';
95
85
  const module = './TestComponent';
@@ -1,5 +1,6 @@
1
1
  /** @jest-environment jsdom */
2
2
  import { AppshellManifest } from '@appshell/config';
3
+ import * as fetchDynamicScript from '../src/fetchDynamicScript';
3
4
  import * as loadFederatedComponent from '../src/loadFederatedComponent';
4
5
  import remoteLoader from '../src/remoteLoader';
5
6
 
@@ -22,7 +23,10 @@ describe('remoteLoader', () => {
22
23
 
23
24
  it('should return the federated component if it is found in the manifest', async () => {
24
25
  const ExpectedComponent = () => 'test component';
25
- jest.spyOn(loadFederatedComponent, 'default').mockResolvedValue(ExpectedComponent);
26
+ jest.spyOn(fetchDynamicScript, 'default').mockReturnValueOnce(Promise.resolve(true));
27
+ jest
28
+ .spyOn(loadFederatedComponent, 'default')
29
+ .mockResolvedValue(Promise.resolve(ExpectedComponent));
26
30
 
27
31
  const loadRemote = remoteLoader(manifest);
28
32
  const ActualComponent = await loadRemote('TestModule/TestComponent');
@@ -30,6 +34,24 @@ describe('remoteLoader', () => {
30
34
  expect(ActualComponent).toEqual(ExpectedComponent);
31
35
  });
32
36
 
37
+ it('should not fetch remote entry more than once', async () => {
38
+ const ExpectedComponent = () => 'test component';
39
+ const fetchDynamicScriptSpy = jest
40
+ .spyOn(fetchDynamicScript, 'default')
41
+ .mockReturnValueOnce(Promise.resolve(true));
42
+ jest
43
+ .spyOn(loadFederatedComponent, 'default')
44
+ .mockResolvedValue(Promise.resolve(ExpectedComponent));
45
+
46
+ const loadRemote = remoteLoader(manifest);
47
+
48
+ await loadRemote('TestModule/TestComponent');
49
+ await loadRemote('TestModule/TestComponent');
50
+ await loadRemote('TestModule/TestComponent');
51
+
52
+ expect(fetchDynamicScriptSpy).toHaveBeenCalledTimes(1);
53
+ });
54
+
33
55
  it('should throw if remote key is not found in the manifest', async () => {
34
56
  const ExpectedComponent = () => 'test component';
35
57
  jest.spyOn(loadFederatedComponent, 'default').mockResolvedValue(ExpectedComponent);
package/dist/main.js CHANGED
@@ -1 +1 @@
1
- !function(e,o){if("object"==typeof exports&&"object"==typeof module)module.exports=o();else if("function"==typeof define&&define.amd)define([],o);else{var t=o();for(var r in t)("object"==typeof exports?exports:e)[r]=t[r]}}(self,(()=>(()=>{"use strict";var e={d:(o,t)=>{for(var r in t)e.o(t,r)&&!e.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:t[r]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}};(()=>{e.S={};var o={},t={};e.I=(r,n)=>{n||(n=[]);var a=t[r];if(a||(a=t[r]={}),!(n.indexOf(a)>=0)){if(n.push(a),o[r])return o[r];e.o(e.S,r)||(e.S[r]={}),e.S[r];var d=[];return o[r]=d.length?Promise.all(d).then((()=>o[r]=1)):1}}})();var o={};e.r(o),e.d(o,{default:()=>r});const t=new Set,r=o=>async r=>{let n;const a=o.remotes[r];if(!a)throw new Error(`Remote resource not found in manifest. Expected: ${r}`);try{return await(async e=>{const o=document.createElement("script");return new Promise(((r,n)=>{o.src=e,o.type="text/javascript",o.async=!0,o.onload=()=>{t.add(e),console.debug(`Remote entry fetched from '${e}'.`),r(!0)},o.onerror=()=>{const o=`Failed to fetch remote entry from '${e}'.`;console.error(o),n(o)},document.head.appendChild(o)})).finally((()=>{t.delete(e),document.head.removeChild(o)}))})(a.url),n=await(async(o,t,r="default")=>{console.debug(`loading federated component: { scope: ${o}, module: ${t}, shareScope: ${r} }`),await e.I(r);const n=window[o];if(!n)throw new Error(`Failed to find module container ${o}`);await n.init(e.S[r]);const a=await n.get(t);if(!a)throw new Error(`Invalid factory produced by container for module ${t}.`);const d=a();return console.debug(`federated component loaded: { scope: ${o}, module: ${t}, shareScope: ${r} }`),d[r]})(a.scope,a.module,a.shareScope),n}catch(e){throw new Error(`Failed to load component '${r}'. ${e?.toString()}`)}};return o})()));
1
+ !function(e,o){if("object"==typeof exports&&"object"==typeof module)module.exports=o();else if("function"==typeof define&&define.amd)define([],o);else{var t=o();for(var r in t)("object"==typeof exports?exports:e)[r]=t[r]}}(self,(()=>(()=>{"use strict";var e={d:(o,t)=>{for(var r in t)e.o(t,r)&&!e.o(o,r)&&Object.defineProperty(o,r,{enumerable:!0,get:t[r]})},o:(e,o)=>Object.prototype.hasOwnProperty.call(e,o),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}};(()=>{e.S={};var o={},t={};e.I=(r,n)=>{n||(n=[]);var a=t[r];if(a||(a=t[r]={}),!(n.indexOf(a)>=0)){if(n.push(a),o[r])return o[r];e.o(e.S,r)||(e.S[r]={}),e.S[r];var d=[];return o[r]=d.length?Promise.all(d).then((()=>o[r]=1)):1}}})();var o={};e.r(o),e.d(o,{default:()=>n});const t=new Set,r=new Set,n=o=>async n=>{let a;const d=o.remotes[n];if(!d)throw new Error(`Remote resource not found in manifest. Expected: ${n}`);try{return r.has(d.url)||await(async e=>{const o=document.createElement("script");return new Promise(((r,n)=>{t.has(e)?r(!1):(t.add(e),o.src=e,o.type="text/javascript",o.async=!0,o.onload=()=>{console.debug(`Remote entry fetched from '${e}'.`),r(!0)},o.onerror=()=>{const o=`Failed to fetch remote entry from '${e}'.`;console.error(o),n(o)},document.head.appendChild(o))})).finally((()=>{document.head.contains(o)&&document.head.removeChild(o)}))})(d.url)?(r.add(d.url),a=await(async(o,t,r="default")=>{console.debug(`loading federated component: { scope: ${o}, module: ${t}, shareScope: ${r} }`),await e.I(r);const n=window[o];if(!n)throw new Error(`Failed to find module container ${o}`);await n.init(e.S[r]);const a=await n.get(t);if(!a)throw new Error(`Invalid factory produced by container for module ${t}.`);const d=a();return console.debug(`federated component loaded: { scope: ${o}, module: ${t}, shareScope: ${r} }`),d.default})(d.scope,d.module,d.shareScope),a):null}catch(e){throw new Error(`Failed to load component '${n}'. ${e?.toString()}`)}};return o})()));
@@ -1 +1 @@
1
- {"version":3,"file":"fetchDynamicScript.d.ts","sourceRoot":"","sources":["../../../../src/fetchDynamicScript.ts"],"names":[],"mappings":"8BAE2B,MAAM;AAAjC,wBA8BE"}
1
+ {"version":3,"file":"fetchDynamicScript.d.ts","sourceRoot":"","sources":["../../../../src/fetchDynamicScript.ts"],"names":[],"mappings":"8BAE2B,MAAM;AAAjC,wBAoCE"}
@@ -1,4 +1,4 @@
1
1
  import { type AppshellManifest } from '@appshell/config';
2
- declare const _default: (manifest: AppshellManifest) => <TComponent>(key: string) => Promise<TComponent>;
2
+ declare const _default: (manifest: AppshellManifest) => <TComponent>(key: string) => Promise<TComponent | null>;
3
3
  export default _default;
4
4
  //# sourceMappingURL=remoteLoader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"remoteLoader.d.ts","sourceRoot":"","sources":["../../../../src/remoteLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;mCAI/B,gBAAgB,uBAChB,MAAM;AADhC,wBAqBI"}
1
+ {"version":3,"file":"remoteLoader.d.ts","sourceRoot":"","sources":["../../../../src/remoteLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;mCAM/B,gBAAgB,uBAChB,MAAM;AADhC,wBAyBI"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appshell/loader",
3
- "version": "0.0.1-alpha.0",
3
+ "version": "0.0.1",
4
4
  "description": "Utility for loading federated components",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/loader/src/index.d.ts",
@@ -20,5 +20,5 @@
20
20
  ],
21
21
  "author": "Robert Hamilton <rh@navaris.com>",
22
22
  "license": "MIT",
23
- "gitHead": "305b39fb81e7c267918dda7d16336e37b2b36643"
23
+ "gitHead": "b6692924e02579e28ed6399965cc6603dd1acb37"
24
24
  }
@@ -4,30 +4,36 @@ export default async (url: string) => {
4
4
  const element = document.createElement('script');
5
5
 
6
6
  const cleanup = (): void => {
7
- urlCache.delete(url);
8
- document.head.removeChild(element);
7
+ if (document.head.contains(element)) {
8
+ document.head.removeChild(element);
9
+ }
9
10
  };
10
11
 
11
12
  return new Promise<boolean>((resolve, reject) => {
12
- element.src = url;
13
- element.type = 'text/javascript';
14
- element.async = true;
15
-
16
- element.onload = () => {
13
+ if (urlCache.has(url)) {
14
+ resolve(false);
15
+ } else {
17
16
  urlCache.add(url);
18
- // eslint-disable-next-line no-console
19
- console.debug(`Remote entry fetched from '${url}'.`);
20
17
 
21
- resolve(true);
22
- };
18
+ element.src = url;
19
+ element.type = 'text/javascript';
20
+ element.async = true;
21
+
22
+ element.onload = () => {
23
+ // eslint-disable-next-line no-console
24
+ console.debug(`Remote entry fetched from '${url}'.`);
25
+
26
+ resolve(true);
27
+ };
23
28
 
24
- element.onerror = () => {
25
- const message = `Failed to fetch remote entry from '${url}'.`;
26
- // eslint-disable-next-line no-console
27
- console.error(message);
28
- reject(message);
29
- };
29
+ element.onerror = () => {
30
+ const message = `Failed to fetch remote entry from '${url}'.`;
31
+ // eslint-disable-next-line no-console
32
+ console.error(message);
33
+ reject(message);
34
+ };
30
35
 
31
- document.head.appendChild(element);
36
+ document.head.appendChild(element);
37
+ }
32
38
  }).finally(cleanup);
33
39
  };
@@ -42,7 +42,7 @@ export default async <TComponent>(scope: string, module: string, shareScope = 'd
42
42
  `federated component loaded: { scope: ${scope}, module: ${module}, shareScope: ${shareScope} }`,
43
43
  );
44
44
 
45
- const Component = Module[shareScope as ShareScopes];
45
+ const Component = Module.default;
46
46
 
47
47
  return Component;
48
48
  };
@@ -2,6 +2,8 @@ import { type AppshellManifest } from '@appshell/config';
2
2
  import fetchDynamicScript from './fetchDynamicScript';
3
3
  import loadFederatedComponent from './loadFederatedComponent';
4
4
 
5
+ const fetchedScriptCache = new Set<string>();
6
+
5
7
  export default (manifest: AppshellManifest) =>
6
8
  async <TComponent>(key: string) => {
7
9
  let Component: TComponent;
@@ -11,14 +13,18 @@ export default (manifest: AppshellManifest) =>
11
13
  }
12
14
 
13
15
  try {
14
- await fetchDynamicScript(remote.url);
15
- Component = await loadFederatedComponent<TComponent>(
16
- remote.scope,
17
- remote.module,
18
- remote.shareScope,
19
- );
16
+ const loaded = fetchedScriptCache.has(remote.url) || (await fetchDynamicScript(remote.url));
17
+ if (loaded) {
18
+ fetchedScriptCache.add(remote.url);
19
+ Component = await loadFederatedComponent<TComponent>(
20
+ remote.scope,
21
+ remote.module,
22
+ remote.shareScope,
23
+ );
20
24
 
21
- return Component;
25
+ return Component;
26
+ }
27
+ return null;
22
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
29
  } catch (err: any) {
24
30
  throw new Error(`Failed to load component '${key}'. ${err?.toString()}`);