-
-
Notifications
You must be signed in to change notification settings - Fork 212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: support modernjs ssr #2482
Conversation
🦋 Changeset detectedLatest commit: ef4a2b5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 35 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for module-federation-docs ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
1139916
to
7f0efb3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary
This pull request implements support for Modern.js Server-Side Rendering (SSR) in the module federation system. The changes focus on enhancing TypeScript integration and type generation for remote modules in an SSR context.
Key modifications include:
- Updating the TypeScript configuration in the remote plugin to accommodate SSR requirements.
- Implementing a DTSManager class for generating and consuming TypeScript declaration files in a module federation setup.
- Enhancing the TypeScript compiler functionality to handle SSR-specific compilation and type definition generation.
- Adding support for creating and retrieving type archives for remote modules.
- Modifying compiler options for Next.js to optimize for server-side rendering.
- Introducing new constants and interfaces to support the SSR implementation.
The changes are thoroughly tested, with new test suites added to verify the functionality of type generation, consumption, and archive handling. The implementation seamlessly integrates with the existing codebase, providing a robust solution for Modern.js SSR support in module federation.
File Summaries
File | Summary |
---|---|
packages/dts-plugin/src/core/configurations/remotePlugin.test.ts | The changes implement support for Modern.js SSR by modifying the TypeScript configuration in the remote plugin. Two test cases are updated to reflect the new configuration, including adjustments to compiler options and file paths. |
packages/dts-plugin/src/core/configurations/remotePlugin.ts | The code implements a function to read and modify TypeScript configuration for remote module federation. It processes the tsconfig.json file, sets compiler options, and prepares file lists for compilation, focusing on exposed components and additional files. |
packages/dts-plugin/src/core/interfaces/TsConfigJson.ts | The code defines a TypeScript interface 'TsConfigJson' representing the structure of a tsconfig.json file. It includes optional properties for extending configurations, compiler options, and file inclusion/exclusion settings. |
packages/dts-plugin/src/core/lib/DTSManager.advance.spec.ts | The code adds tests for advanced usage of DTSManager, including generating types with API declaration files and consuming types from remote sources. It sets up test configurations, generates types, and verifies the structure of the generated type files. |
packages/dts-plugin/src/core/lib/DTSManager.spec.ts | The code implements tests for a DTSManager class, focusing on generating and consuming TypeScript declaration files for module federation. It includes tests for generating types, consuming types from remote sources, and updating types in different scenarios. |
packages/dts-plugin/src/core/lib/DTSManager.ts | The code implements type generation and consumption for a module federation system. It handles the generation of types for remote modules and the consumption of these types by host applications. |
packages/dts-plugin/src/core/lib/DtsWorker.spec.ts | The code implements a test suite for generating TypeScript declaration files in a child process. It sets up configuration options for both host and remote modules, initializes a DtsWorker, and verifies the generated type definitions structure. |
packages/dts-plugin/src/core/lib/archiveHandler.test.ts | The code implements test cases for an archive handling functionality. It sets up a temporary directory, configures TypeScript compilation options, and tests the 'createTypesArchive' function, including a test case for handling non-existent output directories. |
packages/dts-plugin/src/core/lib/archiveHandler.ts | The code introduces two new functions: retrieveTypesZipPath and createTypesArchive. These functions are related to handling types for a module federation setup, likely for TypeScript support in a distributed application architecture. |
packages/dts-plugin/src/core/lib/typeScriptCompiler.test.ts | The changes implement a test suite for a TypeScript compiler functionality. It sets up a temporary directory, configures TypeScript options, and tests various scenarios including compilation with empty and filled 'mapToExpose' objects, and handling of additional files to compile. |
packages/dts-plugin/src/core/lib/typeScriptCompiler.ts | The code implements TypeScript compilation and type definition generation for module federation. It includes functions for processing TypeScript files, extracting third-party types, and generating module federation API types. The main functionality revolves around compiling TypeScript, handling exposed components, and managing temporary configuration files. |
packages/dts-plugin/src/plugins/DevPlugin.ts | The code imports necessary modules and defines configuration options for a development plugin. It sets up a temporary directory for live reloading and prepares to modify the compiler's entry points if live reloading is enabled. |
packages/dts-plugin/src/plugins/GenerateTypesPlugin.ts | The code determines the zip prefix for type files based on the module federation configuration. It checks various properties of the config object to set the appropriate prefix, prioritizing manifest file path, manifest file name, or the config filename. |
packages/enhanced/src/lib/container/constant.ts | The code defines constants for module federation, including supported types and a temporary directory path. It imports from '@module-federation/sdk' and uses Node.js path manipulation. |
packages/nextjs-mf/src/plugins/NextFederationPlugin/apply-server-plugins.ts | The code modifies compiler options for server-side rendering. It sets the target to 'async-node', disables split chunks optimization, and prepares to configure the runtime chunk to address potential issues with Next.js. |
packages/sdk/src/constant.ts | Two new constants were added: MODULE_DEVTOOL_IDENTIFIER and ENCODE_NAME_PREFIX. The TEMP_DIR constant was also defined. |
Commits reviewed:
b7ae60138c44097af53c6c3dc0e273973bd22c3e...c00a609b64df2c3224931afa3de84b4681cbb888
b7ae60138c44097af53c6c3dc0e273973bd22c3e...c00a609b64df2c3224931afa3de84b4681cbb888
b7ae60138c44097af53c6c3dc0e273973bd22c3e...c00a609b64df2c3224931afa3de84b4681cbb888
b7ae60138c44097af53c6c3dc0e273973bd22c3e...c00a609b64df2c3224931afa3de84b4681cbb888
b7ae60138c44097af53c6c3dc0e273973bd22c3e...c00a609b64df2c3224931afa3de84b4681cbb888
b7ae60138c44097af53c6c3dc0e273973bd22c3e...ef4a2b561d74c4a16881004da12594db538b24ab
b7ae60138c44097af53c6c3dc0e273973bd22c3e...ef4a2b561d74c4a16881004da12594db538b24ab
b7ae60138c44097af53c6c3dc0e273973bd22c3e...ef4a2b561d74c4a16881004da12594db538b24ab
b7ae60138c44097af53c6c3dc0e273973bd22c3e...ef4a2b561d74c4a16881004da12594db538b24ab
b7ae60138c44097af53c6c3dc0e273973bd22c3e...ef4a2b561d74c4a16881004da12594db538b24ab
b7ae60138c44097af53c6c3dc0e273973bd22c3e...ef4a2b561d74c4a16881004da12594db538b24ab
Issues Reviewed:
Issue | Result | Reason |
---|---|---|
2348 | ✅ | The pull request addresses the issue 'Support Modern.js SSR' by implementing stream and string SSR modes, updating stats/manifest/snapshot, render mode, CSS handling, cache strategy, data fetch, downgrade strategy, dev features like LiveReload and dynamic remote type hints. |
'entry' in parsedInfo | ||
? parsedInfo.entry | ||
: parsedInfo.name === decodedRemote | ||
? decodedRemote | ||
: ''; | ||
? decodedRemote | ||
: ''; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplify the nested ternary operation for better readability. Consider using an if-else statement or switch case to determine the URL value.
'entry' in parsedInfo | |
? parsedInfo.entry | |
: parsedInfo.name === decodedRemote | |
? decodedRemote | |
: ''; | |
? decodedRemote | |
: ''; | |
let url = ''; | |
if ('entry' in parsedInfo) { | |
url = parsedInfo.entry; | |
} else if (parsedInfo.name === decodedRemote) { | |
url = decodedRemote; | |
} |
This change improves code clarity and makes it easier to maintain and debug.
updatedRemoteInfos: Record<string, Required<RemoteInfo>>; | ||
|
||
constructor(options: DTSManagerOptions) { | ||
this.options = cloneDeepWith(options, (_value, key) => { | ||
// moduleFederationConfig.manifest may have un serialization options | ||
if (key === 'manifest') { | ||
return false; | ||
} | ||
}); | ||
this.options = cloneDeepOptions(options); | ||
this.runtimePkgs = [ | ||
'@module-federation/runtime', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The updatedRemoteInfos
property is declared but not initialized in the constructor. Initialize this property in the constructor to ensure it's always properly set:
updatedRemoteInfos: Record<string, Required<RemoteInfo>>; | |
constructor(options: DTSManagerOptions) { | |
this.options = cloneDeepWith(options, (_value, key) => { | |
// moduleFederationConfig.manifest may have un serialization options | |
if (key === 'manifest') { | |
return false; | |
} | |
}); | |
this.options = cloneDeepOptions(options); | |
this.runtimePkgs = [ | |
'@module-federation/runtime', | |
constructor(options: DTSManagerOptions) { | |
this.options = cloneDeepOptions(options); | |
this.runtimePkgs = [ | |
'@module-federation/runtime', | |
'@module-federation/enhanced/', | |
]; | |
this.updatedRemoteInfos = {}; | |
} |
'Y', | ||
].join(' :\n')} ;`; | ||
|
||
const pkgsDeclareStr = this.runtimePkgs | ||
const runtimePkgs: Set<string> = new Set(); | ||
[...this.runtimePkgs, ...hostOptions.runtimePkgs].forEach((pkg) => { | ||
runtimePkgs.add(pkg); | ||
}); | ||
|
||
const pkgsDeclareStr = [...runtimePkgs] | ||
.map((pkg) => { | ||
return `declare module "${pkg}" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code concatenates an array with a string 'Y' which seems out of place and might lead to unexpected behavior. Remove the 'Y' if it's not needed:
'Y', | |
].join(' :\n')} ;`; | |
const pkgsDeclareStr = this.runtimePkgs | |
const runtimePkgs: Set<string> = new Set(); | |
[...this.runtimePkgs, ...hostOptions.runtimePkgs].forEach((pkg) => { | |
runtimePkgs.add(pkg); | |
}); | |
const pkgsDeclareStr = [...runtimePkgs] | |
.map((pkg) => { | |
return `declare module "${pkg}" { | |
].join(' : | |
')} ;`; | |
const runtimePkgs: Set<string> = new Set(); | |
[...this.runtimePkgs, ...hostOptions.runtimePkgs].forEach((pkg) => { | |
runtimePkgs.add(pkg); | |
}); | |
const pkgsDeclareStr = [...runtimePkgs] | |
.map((pkg) => { | |
return `declare module '${pkg}' { |
@@ -15,12 +14,8 @@ export class DtsWorker { | |||
private _res: Promise<any>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The '_res' property is declared but never used in the provided code snippet. If it's not used elsewhere in the class, consider removing it to improve code clarity.
exit(): void { | ||
this.rpcWorker?.terminate(); | ||
try { | ||
this.rpcWorker?.terminate(); | ||
} catch (err) { | ||
if (isDebugMode()) { | ||
console.error(err); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'exit' method catches all errors without distinction. Consider catching specific error types and handling them appropriately. For non-debug mode, you might want to log errors to a file or monitoring system instead of silently ignoring them.
exit(): void { | |
this.rpcWorker?.terminate(); | |
try { | |
this.rpcWorker?.terminate(); | |
} catch (err) { | |
if (isDebugMode()) { | |
console.error(err); | |
} | |
} | |
} | |
exit(): void { | |
try { | |
this.rpcWorker?.terminate(); | |
} catch (err) { | |
if (err instanceof Error) { | |
if (isDebugMode()) { | |
console.error('Error terminating worker:', err.message); | |
} else { | |
// Log to file or monitoring system | |
} | |
} | |
} | |
} |
remoteInsId = remoteIns.options.id || remoteInsId; | ||
const globalShareScopeMap = getGlobalShareScope(); | ||
|
||
let isAllSharedNotUsed = true; | ||
const needDeleteKeys: Array<[string, string, string, string]> = []; | ||
Object.keys(globalShareScopeMap).forEach((instId) => { | ||
const shareScopeMap = globalShareScopeMap[instId]; | ||
shareScopeMap && | ||
Object.keys(shareScopeMap).forEach((shareScope) => { | ||
const shareScopeVal = shareScopeMap[shareScope]; | ||
shareScopeVal && | ||
Object.keys(shareScopeVal).forEach((shareName) => { | ||
const sharedPkgs = shareScopeVal[shareName]; | ||
sharedPkgs && | ||
Object.keys(sharedPkgs).forEach((shareVersion) => { | ||
const shared = sharedPkgs[shareVersion]; | ||
if ( | ||
shared && | ||
typeof shared === 'object' && | ||
shared.from === remoteInfo.name | ||
) { | ||
if (shared.loaded || shared.loading) { | ||
shared.useIn = shared.useIn.filter( | ||
(usedHostName) => | ||
usedHostName !== remoteInfo.name, | ||
); | ||
if (shared.useIn.length) { | ||
isAllSharedNotUsed = false; | ||
} else { | ||
needDeleteKeys.push([ | ||
instId, | ||
shareScope, | ||
shareName, | ||
shareVersion, | ||
]); | ||
} | ||
} else { | ||
needDeleteKeys.push([ | ||
instId, | ||
shareScope, | ||
shareName, | ||
shareVersion, | ||
]); | ||
} | ||
} | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
|
||
if (isAllSharedNotUsed) { | ||
remoteIns.shareScopeMap = {}; | ||
delete globalShareScopeMap[remoteInsId]; | ||
if (isAllSharedNotUsed) { | ||
remoteIns.shareScopeMap = {}; | ||
delete globalShareScopeMap[remoteInsId]; | ||
} | ||
needDeleteKeys.forEach( | ||
([insId, shareScope, shareName, shareVersion]) => { | ||
delete globalShareScopeMap[insId]?.[shareScope]?.[shareName]?.[ | ||
shareVersion | ||
]; | ||
}, | ||
); | ||
globalThis.__FEDERATION__.__INSTANCES__.splice(remoteInsIndex, 1); | ||
} | ||
needDeleteKeys.forEach( | ||
([insId, shareScope, shareName, shareVersion]) => { | ||
delete globalShareScopeMap[insId]?.[shareScope]?.[shareName]?.[ | ||
shareVersion | ||
]; | ||
}, | ||
); | ||
globalThis.__FEDERATION__.__INSTANCES__.splice(remoteInsIndex, 1); | ||
|
||
const { hostGlobalSnapshot } = getGlobalRemoteInfo(remote, host); | ||
if (hostGlobalSnapshot) { | ||
const remoteKey = | ||
hostGlobalSnapshot && | ||
'remotesInfo' in hostGlobalSnapshot && | ||
hostGlobalSnapshot.remotesInfo && | ||
getInfoWithoutType(hostGlobalSnapshot.remotesInfo, remote.name).key; | ||
if (remoteKey) { | ||
delete hostGlobalSnapshot.remotesInfo[remoteKey]; | ||
if ( | ||
Boolean(Global.__FEDERATION__.__MANIFEST_LOADING__[remoteKey]) | ||
) { | ||
delete Global.__FEDERATION__.__MANIFEST_LOADING__[remoteKey]; | ||
} | ||
} | ||
} | ||
|
||
host.moduleCache.delete(remote.name); | ||
} | ||
host.moduleCache.delete(remote.name); | ||
} catch (err) { | ||
console.log('removeRemote fail: ', err); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The removeRemote method is quite complex and handles multiple responsibilities. Consider refactoring it into smaller, more focused methods to improve readability and maintainability. For example, separate the logic for cleaning up global state, shared dependencies, and module cache into distinct methods.
} catch (err) { | ||
console.log('removeRemote fail: ', err); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace the generic console.log error handling with a more robust error handling mechanism. Consider using a dedicated error logging service or throwing a custom error that can be caught and handled appropriately by the caller.
} catch (err) { | |
console.log('removeRemote fail: ', err); | |
} | |
} catch (err) { | |
this.host.logger.error('Failed to remove remote', { remote, error: err }); | |
throw new RemoteRemovalError('Failed to remove remote', { cause: err }); | |
} |
export function getRemoteEntryInfoFromSnapshot(snapshot: ModuleInfo): { | ||
url: string; | ||
type: RemoteEntryType; | ||
globalName: string; | ||
} { | ||
const defaultRemoteEntryInfo: { | ||
url: string; | ||
type: RemoteEntryType; | ||
globalName: string; | ||
} = { | ||
url: '', | ||
type: 'global', | ||
globalName: '', | ||
}; | ||
if (isBrowserEnv()) { | ||
return 'remoteEntry' in snapshot | ||
? { | ||
url: snapshot.remoteEntry, | ||
type: snapshot.remoteEntryType, | ||
globalName: snapshot.globalName, | ||
} | ||
: defaultRemoteEntryInfo; | ||
} | ||
if ('ssrRemoteEntry' in snapshot) { | ||
return { | ||
url: snapshot.ssrRemoteEntry || defaultRemoteEntryInfo.url, | ||
type: snapshot.ssrRemoteEntryType || defaultRemoteEntryInfo.type, | ||
globalName: snapshot.globalName, | ||
}; | ||
} | ||
return defaultRemoteEntryInfo; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The getRemoteEntryInfoFromSnapshot
function could be simplified and made more robust. Consider using object destructuring and providing default values to reduce repetition and improve readability.
export function getRemoteEntryInfoFromSnapshot(snapshot: ModuleInfo): { | |
url: string; | |
type: RemoteEntryType; | |
globalName: string; | |
} { | |
const defaultRemoteEntryInfo: { | |
url: string; | |
type: RemoteEntryType; | |
globalName: string; | |
} = { | |
url: '', | |
type: 'global', | |
globalName: '', | |
}; | |
if (isBrowserEnv()) { | |
return 'remoteEntry' in snapshot | |
? { | |
url: snapshot.remoteEntry, | |
type: snapshot.remoteEntryType, | |
globalName: snapshot.globalName, | |
} | |
: defaultRemoteEntryInfo; | |
} | |
if ('ssrRemoteEntry' in snapshot) { | |
return { | |
url: snapshot.ssrRemoteEntry || defaultRemoteEntryInfo.url, | |
type: snapshot.ssrRemoteEntryType || defaultRemoteEntryInfo.type, | |
globalName: snapshot.globalName, | |
}; | |
} | |
return defaultRemoteEntryInfo; | |
export function getRemoteEntryInfoFromSnapshot(snapshot: ModuleInfo) { | |
const defaultInfo = { url: '', type: 'global' as RemoteEntryType, globalName: '' }; | |
if (isBrowserEnv()) { | |
const { remoteEntry, remoteEntryType, globalName } = snapshot as RemoteWithEntry; | |
return remoteEntry ? { url: remoteEntry, type: remoteEntryType, globalName } : defaultInfo; | |
} | |
const { ssrRemoteEntry, ssrRemoteEntryType, globalName } = snapshot as RemoteWithEntry; | |
return ssrRemoteEntry ? { | |
url: ssrRemoteEntry, | |
type: ssrRemoteEntryType || defaultInfo.type, | |
globalName | |
} : defaultInfo; | |
} |
This refactored version reduces code duplication, uses type assertions to handle different snapshot structures, and provides a more concise implementation.
if (ssrRemoteEntry) { | ||
const fullSSRRemoteEntry = simpleJoinRemoteEntry( | ||
ssrRemoteEntry.path, | ||
ssrRemoteEntry.name, | ||
); | ||
remoteSnapshot.ssrRemoteEntry = fullSSRRemoteEntry; | ||
remoteSnapshot.ssrRemoteEntryType = 'commonjs-module'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add error handling for the SSR remote entry processing. If simpleJoinRemoteEntry
fails or returns an unexpected value, the current implementation might lead to runtime errors.
if (ssrRemoteEntry) { | |
const fullSSRRemoteEntry = simpleJoinRemoteEntry( | |
ssrRemoteEntry.path, | |
ssrRemoteEntry.name, | |
); | |
remoteSnapshot.ssrRemoteEntry = fullSSRRemoteEntry; | |
remoteSnapshot.ssrRemoteEntryType = 'commonjs-module'; | |
} | |
if (ssrRemoteEntry) { | |
try { | |
const fullSSRRemoteEntry = simpleJoinRemoteEntry( | |
ssrRemoteEntry.path, | |
ssrRemoteEntry.name, | |
); | |
if (fullSSRRemoteEntry) { | |
remoteSnapshot.ssrRemoteEntry = fullSSRRemoteEntry; | |
remoteSnapshot.ssrRemoteEntryType = 'commonjs-module'; | |
} else { | |
console.warn('Failed to generate SSR remote entry'); | |
} | |
} catch (error) { | |
console.error('Error processing SSR remote entry:', error); | |
} | |
} |
This suggestion adds try-catch block to handle potential errors and checks if fullSSRRemoteEntry
is truthy before assigning it.
|
||
dev?: boolean | PluginDevOptions; | ||
dts?: boolean | PluginDtsOptions; | ||
async?: boolean | AsyncBoundaryOptions; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider adding JSDoc comments for the new options (dev, dts, and async) to improve documentation and provide clarity on their usage and impact.
dev?: boolean | PluginDevOptions; | |
dts?: boolean | PluginDtsOptions; | |
async?: boolean | AsyncBoundaryOptions; | |
/** | |
* Options for development-specific features. | |
*/ | |
dev?: boolean | PluginDevOptions; | |
/** | |
* Options for generating and consuming TypeScript declaration files. | |
*/ | |
dts?: boolean | PluginDtsOptions; | |
/** | |
* Options for asynchronous loading of federated modules. | |
*/ | |
async?: boolean | AsyncBoundaryOptions; |
Adding these comments will help developers understand the purpose and functionality of each option, improving the overall usability of the plugin.
Description
Support modern.js ssr .
start local demo
Related Issue
#2348
Types of changes
Checklist