Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions loadable-react-18/app1/@mf-types/app2/Content.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './compiled-types/src/client/components/Content';
export { default } from './compiled-types/src/client/components/Content';
2 changes: 2 additions & 0 deletions loadable-react-18/app1/@mf-types/app2/apis.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type RemoteKeys = 'app2/Content';
export type PackageType<T> = T extends 'app2/Content' ? typeof import('app2/Content') : any;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
export interface ContentProps {
content?: string;
}
declare const Content: React.FC<ContentProps>;
export default Content;
22 changes: 22 additions & 0 deletions loadable-react-18/app1/@mf-types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { PackageType as App2PackageType, RemoteKeys as App2RemoteKeys } from './app2/apis.d.ts';

declare module '@module-federation/runtime' {
type RemoteKeys = App2RemoteKeys;
type PackageType<T, Fallback = any> = T extends RemoteKeys ? App2PackageType<T> : Fallback;
export function loadRemote<T extends RemoteKeys, Fallback>(packageName: T): Promise<PackageType<T, Fallback>>;
export function loadRemote<T extends string, Fallback>(packageName: T): Promise<PackageType<T, Fallback>>;
}

declare module '@module-federation/enhanced/runtime' {
type RemoteKeys = App2RemoteKeys;
type PackageType<T, Fallback = any> = T extends RemoteKeys ? App2PackageType<T> : Fallback;
export function loadRemote<T extends RemoteKeys, Fallback>(packageName: T): Promise<PackageType<T, Fallback>>;
export function loadRemote<T extends string, Fallback>(packageName: T): Promise<PackageType<T, Fallback>>;
}

declare module '@module-federation/runtime-tools' {
type RemoteKeys = App2RemoteKeys;
type PackageType<T, Fallback = any> = T extends RemoteKeys ? App2PackageType<T> : Fallback;
export function loadRemote<T extends RemoteKeys, Fallback>(packageName: T): Promise<PackageType<T, Fallback>>;
export function loadRemote<T extends string, Fallback>(packageName: T): Promise<PackageType<T, Fallback>>;
}
65 changes: 55 additions & 10 deletions loadable-react-18/app1/src/server/mfFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,31 @@ const isMfComponent = component => mfAppNamesRegex.test(component);
* @return {string[]} chunk ids of the rendered components.
*/
export const getLoadableRequiredComponents = extractor => {
const loadableElement = extractor
.getScriptElements()
.find(el => el.key === '__LOADABLE_REQUIRED_CHUNKS___ext');
const scriptElements = extractor?.getScriptElements?.() ?? [];

const { namedChunks } = JSON.parse(loadableElement.props.dangerouslySetInnerHTML.__html);
const loadableElement = scriptElements.find(
el => el?.key === '__LOADABLE_REQUIRED_CHUNKS___ext',
);

return namedChunks;
if (!loadableElement) {
return [];
}

try {
const rawHtml = loadableElement.props?.dangerouslySetInnerHTML?.__html;

if (!rawHtml) {
return [];
}

const parsedData = JSON.parse(rawHtml);
const { namedChunks } = parsedData ?? {};

return Array.isArray(namedChunks) ? namedChunks : [];
} catch (error) {
console.error('[getLoadableRequiredComponents] Failed to parse required chunks', error);
return [];
}
};

const getMfRenderedComponents = loadableRequiredComponents => {
Expand All @@ -31,21 +49,48 @@ const getMfRenderedComponents = loadableRequiredComponents => {

const getMFStats = async () => {
const promises = Object.values(mfStatsUrlMap).map(url => axios.get(url));
return Promise.all(promises).then(responses => responses.map(response => response.data));

try {
const responses = await Promise.all(promises);

return responses.map(response => response.data);
} catch (error) {
console.error('[getMFStats] Failed to fetch remote federation stats', error);
return [];
}
};

export const getMfChunks = async extractor => {
const loadableRequiredComponents = getLoadableRequiredComponents(extractor);

if (!loadableRequiredComponents.length) {
return [[], []];
}

const mfRenderedComponents = getMfRenderedComponents(loadableRequiredComponents);

if (!mfRenderedComponents.length) {
return [[], []];
}

const mfChunks = await getMFStats();

const scriptsArr = [];
const stylesArr = [];
if (!mfChunks.length) {
return [[], []];
}

const scriptsArr: string[] = [];
const stylesArr: string[] = [];

mfRenderedComponents.forEach(([appName, component]) => {
const remoteStats = mfChunks.find(remote => remote.name === appName);
remoteStats.exposes[component].forEach(chunk => {
const remoteStats = mfChunks.find(remote => remote?.name === appName);
const exposeChunks = remoteStats?.exposes?.[component];

if (!Array.isArray(exposeChunks)) {
return;
}

exposeChunks.forEach(chunk => {
const url = 'http://localhost:3001/static/' + chunk;

url.endsWith('.css') ? stylesArr.push(url) : scriptsArr.push(url);
Expand Down
48 changes: 34 additions & 14 deletions loadable-react-18/app1/src/server/renderAndExtractContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,46 @@ export async function renderAndExtractContext({
// @loadable chunk extractor
chunkExtractor,
}: RenderAndExtractContextOptions) {
const { default: App } = await import('../client/components/App');
let markup = '';

// This not work, The ChunkExtractorManager context provider
// do not pass the chunkExtractor to the context consumer (ChunkExtractorManager)
// const markup = await renderToString(chunkExtractor.collectChunks(<App />));
try {
const { default: App } = await import('../client/components/App');

const markup = await renderToStaticMarkup(
<ChunkExtractorManager {...{ extractor: chunkExtractor }}>
<App />
</ChunkExtractorManager>,
);
// This not work, The ChunkExtractorManager context provider
// do not pass the chunkExtractor to the context consumer (ChunkExtractorManager)
// const markup = await renderToString(chunkExtractor.collectChunks(<App />));

const linkTags = chunkExtractor.getLinkTags();
const scriptTags = chunkExtractor.getScriptTags();
markup = await renderToStaticMarkup(
<ChunkExtractorManager {...{ extractor: chunkExtractor }}>
<App />
</ChunkExtractorManager>,
);
} catch (error) {
console.error('[renderAndExtractContext] Failed to render App component', error);
}

let linkTags = '';
let scriptTags = '';

try {
linkTags = chunkExtractor.getLinkTags();
scriptTags = chunkExtractor.getScriptTags();
} catch (error) {
console.error('[renderAndExtractContext] Failed to collect chunk tags', error);
}

// ================ WORKAROUND ================
const [mfRequiredScripts, mfRequiredStyles] = await getMfChunks(chunkExtractor);
let mfScriptTags = '';
let mfStyleTags = '';

try {
const [mfRequiredScripts, mfRequiredStyles] = await getMfChunks(chunkExtractor);

const mfScriptTags = mfRequiredScripts.map(createScriptTag).join('');
const mfStyleTags = mfRequiredStyles.map(createStyleTag).join('');
mfScriptTags = mfRequiredScripts.map(createScriptTag).join('');
mfStyleTags = mfRequiredStyles.map(createStyleTag).join('');
} catch (error) {
console.error('[renderAndExtractContext] Failed to collect module federation chunks', error);
}
// ================ WORKAROUND ================

console.log('mfScriptTags', mfScriptTags);
Expand Down
2 changes: 1 addition & 1 deletion loadable-react-18/app1/src/server/serverRender.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default async function serverRender(req, res, next) {
console.error('[renderAndExtractContext serverRender]', error);
}

const { markup, linkTags, scriptTags } = result as RenderAndExtractContextResult;
const { markup = '', linkTags = '', scriptTags = '' } = (result || {}) as Partial<RenderAndExtractContextResult>;

res.write(`<head>${linkTags}</head><body>`);
res.write(`<div id="root">${markup}</div>`);
Expand Down
2 changes: 1 addition & 1 deletion loadable-react-18/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"ignored": true,
"version": "0.0.1",
"scripts": {
"start": "pnpm --filter loadable-react-18_* --parallel start",
"start": "node scripts/start.js",
"build": "pnpm --filter loadable-react-18_* build",
"serve": "pnpm --filter loadable-react-18_* --parallel serve",
"clean": "pnpm --filter loadable-react-18_* --parallel clean",
Expand Down
83 changes: 83 additions & 0 deletions loadable-react-18/scripts/start.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const { spawn } = require('node:child_process');
const process = require('node:process');
const waitOn = require('wait-on');

const processes = new Set();
let shuttingDown = false;

function spawnProcess(command, args, name) {
const child = spawn(command, args, {
stdio: 'inherit',
});

processes.add(child);

child.on('exit', (code, signal) => {
processes.delete(child);

if (shuttingDown) {
return;
}

const exitCode = typeof code === 'number' ? code : 1;

if (exitCode === 0 && !signal) {
console.error(`${name} exited unexpectedly.`);
shutdown(1);
} else {
console.error(`${name} exited with code ${exitCode}${signal ? ` (signal: ${signal})` : ''}`);
shutdown(exitCode || 1);
}
});

child.on('error', error => {
if (shuttingDown) {
return;
}

console.error(`${name} failed to start`, error);
shutdown(1);
});

return child;
}

function shutdown(code = 0) {
if (shuttingDown) {
return;
}

shuttingDown = true;

for (const child of processes) {
if (!child.killed) {
child.kill('SIGINT');
}
}

process.exit(code);
}

['SIGINT', 'SIGTERM', 'SIGQUIT'].forEach(signal => {
process.on(signal, () => shutdown(0));
});

console.log('Starting App2...');
spawnProcess('pnpm', ['--filter', 'loadable-react-18_app2', 'start'], 'App2');

waitOn({
resources: ['http://localhost:3001/server/remoteEntry.js'],
timeout: 180_000,
})
.then(() => {
if (shuttingDown) {
return;
}

console.log('App2 is ready. Starting App1...');
spawnProcess('pnpm', ['--filter', 'loadable-react-18_app1', 'start'], 'App1');
})
.catch(error => {
console.error('Failed to detect App2 readiness', error);
shutdown(1);
});
Loading