diff --git a/loadable-react-18/app1/@mf-types/app2/Content.d.ts b/loadable-react-18/app1/@mf-types/app2/Content.d.ts new file mode 100644 index 00000000000..df1a07a087d --- /dev/null +++ b/loadable-react-18/app1/@mf-types/app2/Content.d.ts @@ -0,0 +1,2 @@ +export * from './compiled-types/src/client/components/Content'; +export { default } from './compiled-types/src/client/components/Content'; diff --git a/loadable-react-18/app1/@mf-types/app2/apis.d.ts b/loadable-react-18/app1/@mf-types/app2/apis.d.ts new file mode 100644 index 00000000000..a51accec443 --- /dev/null +++ b/loadable-react-18/app1/@mf-types/app2/apis.d.ts @@ -0,0 +1,2 @@ +export type RemoteKeys = 'app2/Content'; +export type PackageType = T extends 'app2/Content' ? typeof import('app2/Content') : any; diff --git a/loadable-react-18/app1/@mf-types/app2/compiled-types/src/client/components/Content.d.ts b/loadable-react-18/app1/@mf-types/app2/compiled-types/src/client/components/Content.d.ts new file mode 100644 index 00000000000..2e2de557dae --- /dev/null +++ b/loadable-react-18/app1/@mf-types/app2/compiled-types/src/client/components/Content.d.ts @@ -0,0 +1,6 @@ +import React from 'react'; +export interface ContentProps { + content?: string; +} +declare const Content: React.FC; +export default Content; diff --git a/loadable-react-18/app1/@mf-types/index.d.ts b/loadable-react-18/app1/@mf-types/index.d.ts new file mode 100644 index 00000000000..7d9293626d0 --- /dev/null +++ b/loadable-react-18/app1/@mf-types/index.d.ts @@ -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 extends RemoteKeys ? App2PackageType : Fallback; + export function loadRemote(packageName: T): Promise>; + export function loadRemote(packageName: T): Promise>; +} + +declare module '@module-federation/enhanced/runtime' { + type RemoteKeys = App2RemoteKeys; + type PackageType = T extends RemoteKeys ? App2PackageType : Fallback; + export function loadRemote(packageName: T): Promise>; + export function loadRemote(packageName: T): Promise>; +} + +declare module '@module-federation/runtime-tools' { + type RemoteKeys = App2RemoteKeys; + type PackageType = T extends RemoteKeys ? App2PackageType : Fallback; + export function loadRemote(packageName: T): Promise>; + export function loadRemote(packageName: T): Promise>; +} diff --git a/loadable-react-18/app1/src/server/mfFunctions.ts b/loadable-react-18/app1/src/server/mfFunctions.ts index a506f54eac6..70c7d34e6b9 100644 --- a/loadable-react-18/app1/src/server/mfFunctions.ts +++ b/loadable-react-18/app1/src/server/mfFunctions.ts @@ -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 => { @@ -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); diff --git a/loadable-react-18/app1/src/server/renderAndExtractContext.tsx b/loadable-react-18/app1/src/server/renderAndExtractContext.tsx index a518aaf2220..219822c7eb4 100644 --- a/loadable-react-18/app1/src/server/renderAndExtractContext.tsx +++ b/loadable-react-18/app1/src/server/renderAndExtractContext.tsx @@ -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()); + try { + const { default: App } = await import('../client/components/App'); - const markup = await renderToStaticMarkup( - - - , - ); + // This not work, The ChunkExtractorManager context provider + // do not pass the chunkExtractor to the context consumer (ChunkExtractorManager) + // const markup = await renderToString(chunkExtractor.collectChunks()); - const linkTags = chunkExtractor.getLinkTags(); - const scriptTags = chunkExtractor.getScriptTags(); + markup = await renderToStaticMarkup( + + + , + ); + } 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); diff --git a/loadable-react-18/app1/src/server/serverRender.tsx b/loadable-react-18/app1/src/server/serverRender.tsx index 3a42a3439ec..f7956d4527d 100644 --- a/loadable-react-18/app1/src/server/serverRender.tsx +++ b/loadable-react-18/app1/src/server/serverRender.tsx @@ -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; res.write(`${linkTags}`); res.write(`
${markup}
`); diff --git a/loadable-react-18/package.json b/loadable-react-18/package.json index cad9189e857..fddaac8a77c 100644 --- a/loadable-react-18/package.json +++ b/loadable-react-18/package.json @@ -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", diff --git a/loadable-react-18/scripts/start.js b/loadable-react-18/scripts/start.js new file mode 100644 index 00000000000..c5e5b00d63d --- /dev/null +++ b/loadable-react-18/scripts/start.js @@ -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); + });