Skip to content

Commit

Permalink
feat: more testing trying to get remote component moved over
Browse files Browse the repository at this point in the history
  • Loading branch information
Emgem committed Jun 26, 2023
1 parent eef8221 commit b3e1e7a
Show file tree
Hide file tree
Showing 9 changed files with 1,927 additions and 59 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ useEffect(() => {

## Events API

Below are the details for all the events that are currently supported by Statflo.

### Outgoing Events

The following events can be published from the widget so that the app can trigger certain functionality.
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@statflo/widget-sdk",
"version": "0.4.5",
"version": "0.4.4",
"description": "SDK for building widgets with Statflo and beyond",
"main": "./dist/index.js",
"module": "./dist/index.js",
Expand Down Expand Up @@ -37,22 +37,27 @@
"react-error-boundary": "^4.0.10",
"typescript": "^4.9.3",
"uuid": "^9.0.0",
"webpack": "^5.88.0",
"zustand": "^4.3.3"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@types/jest": "^29.2.5",
"@types/react": "^18.2.13",
"@types/uuid": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0",
"babel-loader": "^9.1.2",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^4.4.1",
"eslint-plugin-import": "^2.26.0",
"jest": "^29.3.1",
"prettier": "^2.8.1",
"ts-jest": "^29.0.3"
"ts-jest": "^29.0.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"optionalDependencies": {
"use-sync-external-store": "1.2.0"
Expand Down
18 changes: 10 additions & 8 deletions src/components/RemoteComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ErrorBoundary, FallbackProps } from "react-error-boundary";
import { UseBoundStore } from "zustand/react";
import { Mutate, StoreApi } from "zustand/vanilla";

import { loadRemoteModule } from "../lib/module-federation";
import { Widget, WidgetState } from "../store";
import { loadComponent } from "../utils";

type WidgetProps = {
widget: Widget;
Expand Down Expand Up @@ -42,14 +42,16 @@ export const RemoteComponent: FC<WidgetProps> = ({

useEffect(() => {
if (widget.type === "native") {
if (widget.native && widget.native.module && widget.native.remote) {
const remote = widget.native.remote;
const module = widget.native.module;
const url = widget.url;
setComponent(
React.lazy(
loadComponent(
widget.native?.module,
widget.url,
widget.native?.module
)
)
);

const lazyFactory = () => loadRemoteModule(remote, module, url);
setComponent(React.lazy(lazyFactory));
}
setLoading(false);
}
}, []);
Expand Down
46 changes: 46 additions & 0 deletions src/hooks/useDynamicScript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect, useState } from "react";

const urlCache = new Set();

export const useDynamicScript = (url: string) => {
const [ready, setReady] = useState(false);
const [errorLoading, setErrorLoading] = useState(false);

useEffect(() => {
if (!url) return;

if (urlCache.has(url)) {
setReady(true);
setErrorLoading(false);
return;
}

setReady(false);
setErrorLoading(false);

const element = document.createElement("script");

element.src = url;
element.type = "text/javascript";
element.async = true;

element.onload = () => {
urlCache.add(url);
setReady(true);
};

element.onerror = () => {
setReady(false);
setErrorLoading(true);
};

document.head.appendChild(element);

return () => {
urlCache.delete(url);
document.head.removeChild(element);
};
}, [url]);

return { errorLoading, ready };
};
55 changes: 55 additions & 0 deletions src/hooks/useFederatedComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { lazy, useEffect, useState } from "react";

import { useDynamicScript } from "./useDynamicScript";

declare global {
interface Window {
[key: string]: any;
}
}

declare const __webpack_init_sharing__: (scope: string) => Promise<void>;
declare const __webpack_share_scopes__: any;

function loadComponent(remoteUrl: string, scope: string, module: string) {
return async () => {
await __webpack_init_sharing__("default");
const container = await import(remoteUrl);

await container.init(__webpack_share_scopes__.default);
const factory = await container.get(module);
const Module = factory();
return Module;
};
}

const componentCache = new Map();

export const useFederatedComponent = (
remoteUrl: string,
scope: string,
module?: string
) => {
if (!module) return { errorLoading: false, Component: null };

const key = `${remoteUrl}-${scope}-${module}`;
const [Component, setComponent] = useState<React.ComponentType<any> | null>(
null
);

const { ready, errorLoading } = useDynamicScript(remoteUrl);

useEffect(() => {
if (Component) setComponent(null);
}, [key]);

useEffect(() => {
if (ready && !Component) {
const Comp = lazy(loadComponent(remoteUrl, scope, module));
componentCache.set(key, Comp);
setComponent(Comp);
}
}, [Component, ready, key]);

return { errorLoading, Component };
};
31 changes: 0 additions & 31 deletions src/lib/module-federation.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "iframe-resizer/js/iframeResizer.contentWindow";
export interface Widget {
id: string;
name: string;
label: string;
label?: string;
url: string;
type: "iframe" | "native";
native?: {
Expand Down
50 changes: 50 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// @ts-nocheck

export const fetchRemote = (url, remoteName) => {
return new Promise((resolve, reject) => {
// We define a script tag to use the browser for fetching the remoteEntry.js file
const script = document.createElement("script");
script.src = url;
script.onerror = (err) => {
console.error(err);
reject(new Error(`Failed to fetch remote: ${remoteName}`));
};
// When the script is loaded we need to resolve the promise back to Module Federation
script.onload = () => {
// The script is now loaded on window using the name defined within the remote
const proxy = {
get: (request) => window[remoteName].get(request),
init: (arg) => {
try {
return window[remoteName].init(arg);
} catch (e) {
console.error(e);
console.error(`Failed to initialize remote: ${remoteName}`);
reject(e);
}
},
};
resolve(proxy);
};
// Lastly we inject the script tag into the document's head to trigger the script load
document.head.appendChild(script);
});
};

export const loadComponent =
(remoteModule, url, remoteComponent, scope = "default") =>
async () => {
if (!(remoteModule in window)) {
// Need to load the remote first
// Initializes the shared scope. Fills it with known provided modules from this build and all remotes

await __webpack_init_sharing__(scope); // TODO when would you use a different scope?
const fetchedContainer = await fetchRemote(url, remoteModule);

await fetchedContainer.init(__webpack_share_scopes__[scope]);
}
const container = window[remoteModule]; // Assuming the remote has been loaded using the above function
const factory = await container.get(remoteComponent);
const Module = factory();
return Module;
};
Loading

0 comments on commit b3e1e7a

Please sign in to comment.