Skip to content

Commit

Permalink
fix(nextjs-mf): improve hot reloading (#3001)
Browse files Browse the repository at this point in the history
  • Loading branch information
ScriptedAlchemy authored Oct 23, 2024
1 parent 3d6b72c commit 1478f50
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 163 deletions.
5 changes: 5 additions & 0 deletions .changeset/ai-calm-cat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@module-federation/nextjs-mf": patch
---
- Added `globalThis.moduleGraphDirty = true` to mark the module graph as dirty when an error is detected.
- Replaced `new Function('return globalThis')()` with a direct reference to `globalThis`.
10 changes: 10 additions & 0 deletions .changeset/ai-eager-wolf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@module-federation/node": patch
---

Add global flag `moduleGraphDirty` to control forced revalidation in hot-reload.

- Introduced new global variable `moduleGraphDirty`.
- Initialized `moduleGraphDirty` to `false` in the global scope.
- Modified `revalidate` function to check `moduleGraphDirty` flag.
- Forces revalidation if `moduleGraphDirty` is `true`.
14 changes: 14 additions & 0 deletions .changeset/ai-noisy-lion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@module-federation/node": minor
---

Enhanced hot-reload functionality with module decaching and improved type safety.

- Added `callsite` package for resolving module paths.
- Implemented `decache` and `searchCache` functions to remove modules from cache safely.
- Ensure proper handling of relative module paths.
- Avoid issues with native modules during decaching.
- Refactored hot-reload logic to use the new decache functionality.
- Improved type definitions and type safety throughout `hot-reload.ts`.
- Properly typed function return values.
- Added TypeScript annotations for better clarity.
8 changes: 8 additions & 0 deletions .changeset/ai-noisy-owl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@module-federation/nextjs-mf": minor
---

Added the UniverseEntryChunkTrackerPlugin to track entry chunks in the server plugin.

- Applied UniverseEntryChunkTrackerPlugin in the applyServerPlugins function.
- This change aims to enhance tracking of entry chunks in the server environment for hot reloading prod instances
12 changes: 10 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,18 @@ jobs:
run: npx nx run-many --targets=build --projects=tag:type:pkg --skip-nx-cache

- name: Run Affected Test
run: npx nx affected -t test --parallel=2 --exclude='*,!tag:type:pkg' --skip-nx-cache
uses: nick-fields/retry@v3
with:
max_attempts: 2
timeout_minutes: 10
command: npx nx affected -t test --parallel=3 --exclude='*,!tag:type:pkg' --skip-nx-cache

- name: Run Affected Experimental Tests
run: npx nx affected -t test:experiments --parallel=2 --exclude='*,!tag:type:pkg' --skip-nx-cache
uses: nick-fields/retry@v3
with:
max_attempts: 2
timeout_minutes: 10
command: npx nx affected -t test:experiments --parallel=1 --exclude='*,!tag:type:pkg' --skip-nx-cache

e2e-modern:
needs: checkout-install
Expand Down
11 changes: 11 additions & 0 deletions packages/enhanced/test/ConfigTestCases.basictest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
if (globalThis.__FEDERATION__) {
globalThis.__GLOBAL_LOADING_REMOTE_ENTRY__ = {};
//@ts-ignore
globalThis.__FEDERATION__.__INSTANCES__.map((i) => {
i.moduleCache.clear();
if (globalThis[i.name]) {
delete globalThis[i.name];
}
});
globalThis.__FEDERATION__.__INSTANCES__ = [];
}
const { describeCases } = require('./ConfigTestCases.template');

describeCases({
Expand Down
11 changes: 11 additions & 0 deletions packages/enhanced/test/ConfigTestCases.embedruntime.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
if (globalThis.__FEDERATION__) {
globalThis.__GLOBAL_LOADING_REMOTE_ENTRY__ = {};
//@ts-ignore
globalThis.__FEDERATION__.__INSTANCES__.map((i) => {
i.moduleCache.clear();
if (globalThis[i.name]) {
delete globalThis[i.name];
}
});
globalThis.__FEDERATION__.__INSTANCES__ = [];
}
const { describeCases } = require('./ConfigTestCases.template');
jest.resetModules();
describeCases({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
import type { moduleFederationPlugin } from '@module-federation/sdk';
import path from 'path';
import InvertedContainerPlugin from '../container/InvertedContainerPlugin';
import UniverseEntryChunkTrackerPlugin from '@module-federation/node/universe-entry-chunk-tracker-plugin';

type EntryStaticNormalized = Awaited<
ReturnType<Extract<WebpackOptionsNormalized['entry'], () => any>>
Expand Down Expand Up @@ -74,7 +75,7 @@ export function applyServerPlugins(
suffix,
);
}

new UniverseEntryChunkTrackerPlugin().apply(compiler);
new InvertedContainerPlugin().apply(compiler);
}

Expand Down
80 changes: 38 additions & 42 deletions packages/nextjs-mf/src/plugins/container/runtimePlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ export default function (): FederationRuntimePlugin {
url: string;
attrs?: Record<string, any>;
}) {
// Updated type
var url = args.url;
var attrs = args.attrs;
const url = args.url;
const attrs = args.attrs;
if (typeof window !== 'undefined') {
var script = document.createElement('script');
const script = document.createElement('script');
script.src = url;
script.async = true;
delete attrs?.['crossorigin'];
Expand All @@ -26,20 +25,21 @@ export default function (): FederationRuntimePlugin {
from: string;
origin: any;
}) {
var id = args.id;
var error = args.error;
var from = args.from;
const id = args.id;
const error = args.error;
const from = args.from;
//@ts-ignore
globalThis.moduleGraphDirty = true;
console.error(id, 'offline');
var pg = function () {
const pg = function () {
console.error(id, 'offline', error);
return null;
};

(pg as any).getInitialProps = function (ctx: any) {
// Type assertion to add getInitialProps
return {};
};
var mod;
let mod;
if (from === 'build') {
mod = function () {
return {
Expand Down Expand Up @@ -70,15 +70,16 @@ export default function (): FederationRuntimePlugin {
return args;
}

var moduleCache = args.origin.moduleCache;
var name = args.origin.name;
var gs;
const moduleCache = args.origin.moduleCache;
const name = args.origin.name;
let gs;
try {
gs = new Function('return globalThis')();
} catch (e) {
gs = globalThis; // fallback for browsers without 'unsafe-eval' CSP policy enabled
}
var attachedRemote = gs[name];
//@ts-ignore
const attachedRemote = gs[name];
if (attachedRemote) {
moduleCache.set(name, attachedRemote);
}
Expand All @@ -89,10 +90,10 @@ export default function (): FederationRuntimePlugin {
return args;
},
beforeRequest: function (args: any) {
var options = args.options;
var id = args.id;
var remoteName = id.split('/').shift();
var remote = options.remotes.find(function (remote: any) {
const options = args.options;
const id = args.id;
const remoteName = id.split('/').shift();
const remote = options.remotes.find(function (remote: any) {
return remote.name === remoteName;
});
if (!remote) return args;
Expand All @@ -106,41 +107,41 @@ export default function (): FederationRuntimePlugin {
return args;
},
onLoad: function (args: any) {
var exposeModuleFactory = args.exposeModuleFactory;
var exposeModule = args.exposeModule;
var id = args.id;
var moduleOrFactory = exposeModuleFactory || exposeModule;
if (!moduleOrFactory) return args; // Ensure moduleOrFactory is defined
const exposeModuleFactory = args.exposeModuleFactory;
const exposeModule = args.exposeModule;
const id = args.id;
const moduleOrFactory = exposeModuleFactory || exposeModule;
if (!moduleOrFactory) return args;

if (typeof window === 'undefined') {
var exposedModuleExports: any;
let exposedModuleExports: any;
try {
exposedModuleExports = moduleOrFactory();
} catch (e) {
exposedModuleExports = moduleOrFactory;
}

var handler: ProxyHandler<any> = {
const handler: ProxyHandler<any> = {
get: function (target, prop, receiver) {
// Check if accessing a static property of the function itself
if (
target === exposedModuleExports &&
typeof exposedModuleExports[prop] === 'function'
) {
return function (this: unknown) {
globalThis.usedChunks.add(id);
//eslint-disable-next-line
return exposedModuleExports[prop].apply(this, arguments);
};
}

var originalMethod = target[prop];
const originalMethod = target[prop];
if (typeof originalMethod === 'function') {
var proxiedFunction = function (this: unknown) {
const proxiedFunction = function (this: unknown) {
globalThis.usedChunks.add(id);
//eslint-disable-next-line
return originalMethod.apply(this, arguments);
};

// Copy all enumerable properties from the original method to the proxied function
Object.keys(originalMethod).forEach(function (prop) {
Object.defineProperty(proxiedFunction, prop, {
value: originalMethod[prop],
Expand All @@ -158,12 +159,9 @@ export default function (): FederationRuntimePlugin {
};

if (typeof exposedModuleExports === 'function') {
// If the module export is a function, we create a proxy that can handle both its
// call (as a function) and access to its properties (including static methods).
exposedModuleExports = new Proxy(exposedModuleExports, handler);

// Proxy static properties specifically
var staticProps = Object.getOwnPropertyNames(exposedModuleExports);
const staticProps = Object.getOwnPropertyNames(exposedModuleExports);
staticProps.forEach(function (prop) {
if (typeof exposedModuleExports[prop] === 'function') {
exposedModuleExports[prop] = new Proxy(
Expand All @@ -176,7 +174,6 @@ export default function (): FederationRuntimePlugin {
return exposedModuleExports;
};
} else {
// For objects, just wrap the exported object itself
exposedModuleExports = new Proxy(exposedModuleExports, handler);
}

Expand All @@ -194,23 +191,22 @@ export default function (): FederationRuntimePlugin {
) {
return args;
}
var shareScopeMap = args.shareScopeMap;
var scope = args.scope;
var pkgName = args.pkgName;
var version = args.version;
var GlobalFederation = args.GlobalFederation;
var host = GlobalFederation['__INSTANCES__'][0];
const shareScopeMap = args.shareScopeMap;
const scope = args.scope;
const pkgName = args.pkgName;
const version = args.version;
const GlobalFederation = args.GlobalFederation;
const host = GlobalFederation['__INSTANCES__'][0];
if (!host) {
return args;
}

if (!host.options.shared[pkgName]) {
return args;
}
//handle react host next remote, disable resolving when not next host
args.resolver = function () {
shareScopeMap[scope][pkgName][version] =
host.options.shared[pkgName][0]; // replace local share scope manually with desired module
host.options.shared[pkgName][0];
return shareScopeMap[scope][pkgName][version];
};
return args;
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { default as StreamingTargetPlugin } from './plugins/StreamingTargetPlugin';
export { default as NodeFederationPlugin } from './plugins/NodeFederationPlugin';
export { default as UniversalFederationPlugin } from './plugins/UniversalFederationPlugin';
//@ts-ignore
export { default as ChunkCorrelationPlugin } from './plugins/ChunkCorrelationPlugin';
export { default as RemotePublicPathPlugin } from './plugins/RemotePublicPathRuntimeModule';
export { default as EntryChunkTrackerPlugin } from './plugins/EntryChunkTrackerPlugin';
Expand Down
88 changes: 0 additions & 88 deletions packages/node/src/plugins/StreamingTargetPlugin.md

This file was deleted.

Loading

0 comments on commit 1478f50

Please sign in to comment.