Skip to content
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

Merged
merged 101 commits into from
Jul 23, 2024
Merged

feat: support modernjs ssr #2482

merged 101 commits into from
Jul 23, 2024

Conversation

2heal1
Copy link
Member

@2heal1 2heal1 commented May 13, 2024

Description

Support modern.js ssr .

start local demo

pnpm i 

nx build modern-js-plugin 

pnpm run app:modern:dev   

Related Issue

#2348

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have updated the documentation.

Copy link

changeset-bot bot commented May 13, 2024

🦋 Changeset detected

Latest commit: ef4a2b5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 35 packages
Name Type
@module-federation/devtools Minor
@module-federation/dts-plugin Minor
@module-federation/enhanced Minor
@module-federation/manifest Minor
@module-federation/modern-js Minor
@module-federation/runtime Minor
@module-federation/rspack Minor
@module-federation/node Minor
@module-federation/sdk Minor
@module-federation/nextjs-mf Patch
3008-runtime-remote Patch
host Patch
host-v5 Patch
host-vue3 Patch
remote1 Patch
remote2 Patch
remote3 Patch
@module-federation/modernjs Patch
modernjs-ssr-dynamic-nested-remote Patch
modernjs-ssr-dynamic-remote-new-version Patch
modernjs-ssr-dynamic-remote Patch
modernjs-ssr-host Patch
modernjs-ssr-nested-remote Patch
modernjs-ssr-remote-new-version Patch
modernjs-ssr-remote Patch
@module-federation/runtime-tools Minor
@module-federation/webpack-bundler-runtime Minor
@module-federation/esbuild Patch
@module-federation/managers Minor
@module-federation/utilities Patch
@module-federation/bridge-react-webpack-plugin Minor
@module-federation/third-party-dts-extractor Minor
@module-federation/bridge-react Minor
@module-federation/bridge-vue3 Minor
@module-federation/bridge-shared Minor

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

Copy link

netlify bot commented May 13, 2024

Deploy Preview for module-federation-docs ready!

Name Link
🔨 Latest commit ef4a2b5
🔍 Latest deploy log https://app.netlify.com/sites/module-federation-docs/deploys/669f21c9582e3c00087273a2
😎 Deploy Preview https://deploy-preview-2482--module-federation-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@2heal1 2heal1 force-pushed the feat/modernjs-ssr branch 3 times, most recently from 1139916 to 7f0efb3 Compare May 14, 2024 07:39
@2heal1 2heal1 requested a review from zhoushaw July 12, 2024 07:23
Copy link
Contributor

@squadronai squadronai bot left a 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:

  1. Updating the TypeScript configuration in the remote plugin to accommodate SSR requirements.
  2. Implementing a DTSManager class for generating and consuming TypeScript declaration files in a module federation setup.
  3. Enhancing the TypeScript compiler functionality to handle SSR-specific compilation and type definition generation.
  4. Adding support for creating and retrieving type archives for remote modules.
  5. Modifying compiler options for Next.js to optimize for server-side rendering.
  6. 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.

Comment on lines 56 to +60
'entry' in parsedInfo
? parsedInfo.entry
: parsedInfo.name === decodedRemote
? decodedRemote
: '';
? decodedRemote
: '';
Copy link
Contributor

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.

Suggested change
'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.

Comment on lines 50 to 55
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',
Copy link
Contributor

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:

Suggested change
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 = {};
}

Comment on lines 318 to 328
'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}" {
Copy link
Contributor

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:

Suggested change
'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>;
Copy link
Contributor

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.

Comment on lines 66 to 74
exit(): void {
this.rpcWorker?.terminate();
try {
this.rpcWorker?.terminate();
} catch (err) {
if (isDebugMode()) {
console.error(err);
}
}
}
Copy link
Contributor

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.

Suggested change
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
}
}
}
}

Comment on lines 430 to 564
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);
}
}
Copy link
Contributor

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.

Comment on lines +561 to 563
} catch (err) {
console.log('removeRemote fail: ', err);
}
Copy link
Contributor

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.

Suggested change
} 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 });
}

Comment on lines +86 to +116
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;
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +187 to +194
if (ssrRemoteEntry) {
const fullSSRRemoteEntry = simpleJoinRemoteEntry(
ssrRemoteEntry.path,
ssrRemoteEntry.name,
);
remoteSnapshot.ssrRemoteEntry = fullSSRRemoteEntry;
remoteSnapshot.ssrRemoteEntryType = 'commonjs-module';
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines 222 to +225

dev?: boolean | PluginDevOptions;
dts?: boolean | PluginDtsOptions;
async?: boolean | AsyncBoundaryOptions;
Copy link
Contributor

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.

Suggested change
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.

@zhoushaw zhoushaw merged commit fa37cc4 into main Jul 23, 2024
17 checks passed
@zhoushaw zhoushaw deleted the feat/modernjs-ssr branch July 23, 2024 07:23
@2heal1 2heal1 mentioned this pull request Jul 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants