Skip to content

Commit

Permalink
fix: workaround to self ref module error in prod
Browse files Browse the repository at this point in the history
  • Loading branch information
ScriptedAlchemy committed Aug 12, 2023
1 parent 17433c9 commit e134caa
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 10 deletions.
2 changes: 1 addition & 1 deletion apps/3000-home/remote-delegate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import importDelegatedModule from "@module-federation/nextjs-mf/importDelegatedModule";
import { importDelegatedModule } from "@module-federation/nextjs-mf/importDelegatedModule";
/* eslint-disable no-undef */
// eslint-disable-next-line no-async-promise-executor
module.exports = new Promise(async (resolve, reject) => {
Expand Down
3 changes: 0 additions & 3 deletions packages/nextjs-mf/importDelegatedModule.ts

This file was deleted.

286 changes: 285 additions & 1 deletion packages/nextjs-mf/src/default-delegate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,288 @@
import { importDelegatedModule } from '@module-federation/utilities/src/utils/importDelegatedModule'
import {
AsyncContainer,
RemoteVars,
RuntimeRemote,
RuntimeRemotesMap,
WebpackRemoteContainer
} from '@module-federation/utilities/src/types/index';

let pure = {} as RemoteVars;
try {
// @ts-ignore
pure = process.env['REMOTES'] || {};
} catch (e) {
// not in webpack bundle
}
const remoteVars = pure as RemoteVars;

const extractUrlAndGlobal = (urlAndGlobal: string): [string, string] => {
const index = urlAndGlobal.indexOf('@');
if (index <= 0 || index === urlAndGlobal.length - 1) {
throw new Error(`Invalid request "${urlAndGlobal}"`);
}
return [urlAndGlobal.substring(index + 1), urlAndGlobal.substring(0, index)];
};

const loadScript = (keyOrRuntimeRemoteItem: string | RuntimeRemote) => {
const runtimeRemotes = getRuntimeRemotes();

// 1) Load remote container if needed
let asyncContainer: RuntimeRemote['asyncContainer'];
const reference =
typeof keyOrRuntimeRemoteItem === 'string'
? runtimeRemotes[keyOrRuntimeRemoteItem]
: keyOrRuntimeRemoteItem;

if (reference.asyncContainer) {
asyncContainer =
typeof reference.asyncContainer.then === 'function'
? reference.asyncContainer
: // @ts-ignore
reference.asyncContainer();
} else {
// This casting is just to satisfy typescript,
// In reality remoteGlobal will always be a string;
const remoteGlobal = reference.global as unknown as string;

// Check if theres an override for container key if not use remote global
const containerKey = reference.uniqueKey
? (reference.uniqueKey as unknown as string)
: remoteGlobal;

const __webpack_error__ = new Error() as Error & {
type: string;
request: string | null;
};

// @ts-ignore
if (!globalThis.__remote_scope__) {
// create a global scope for container, similar to how remotes are set on window in the browser
// @ts-ignore
globalThis.__remote_scope__ = {
// @ts-ignore
_config: {},
};
}
// @ts-ignore
const globalScope =
// @ts-ignore
typeof window !== 'undefined' ? window : globalThis.__remote_scope__;

if (typeof window === 'undefined') {
globalScope['_config'][containerKey] = reference.url;
} else {
// to match promise template system, can be removed once promise template is gone
if (!globalScope['remoteLoading']) {
globalScope['remoteLoading'] = {};
}
if (globalScope['remoteLoading'][containerKey]) {
return globalScope['remoteLoading'][containerKey];
}
}
// @ts-ignore
asyncContainer = new Promise(function (resolve, reject) {
function resolveRemoteGlobal() {
const asyncContainer = globalScope[
remoteGlobal
] as unknown as AsyncContainer;
return resolve(asyncContainer);
}

if (typeof globalScope[remoteGlobal] !== 'undefined') {
return resolveRemoteGlobal();
}

(__webpack_require__ as any).l(
reference.url,
function (event: Event) {
if (typeof globalScope[remoteGlobal] !== 'undefined') {
return resolveRemoteGlobal();
}

const errorType =
event && (event.type === 'load' ? 'missing' : event.type);
const realSrc =
event && event.target && (event.target as HTMLScriptElement).src;

__webpack_error__.message =
'Loading script failed.\n(' +
errorType +
': ' +
realSrc +
' or global var ' +
remoteGlobal +
')';

__webpack_error__.name = 'ScriptExternalLoadError';
__webpack_error__.type = errorType;
__webpack_error__.request = realSrc;

reject(__webpack_error__);
},
containerKey
);
}).catch(function (err) {
console.error('container is offline, returning fake remote');
console.error(err);

return {
fake: true,
// @ts-ignore
get: (arg) => {
console.warn('faking', arg, 'module on, its offline');

return Promise.resolve(() => {
return {
__esModule: true,
default: () => {
return null;
},
};
});
},
//eslint-disable-next-line
init: () => {},
};
});
if (typeof window !== 'undefined') {
globalScope['remoteLoading'][containerKey] = asyncContainer;
}
}

return asyncContainer;
};

const getRuntimeRemotes = () => {
try {
const runtimeRemotes = Object.entries(remoteVars).reduce(function (
acc,
item
) {
const [key, value] = item;
// if its an object with a thenable (eagerly executing function)
if (typeof value === 'object' && typeof value.then === 'function') {
acc[key] = { asyncContainer: value };
}
// if its a function that must be called (lazily executing function)
else if (typeof value === 'function') {
// @ts-ignore
acc[key] = { asyncContainer: value };
}
// if its a delegate module, skip it
else if (typeof value === 'string' && value.startsWith('internal ')) {
const [request, query] = value.replace('internal ', '').split('?');
if (query) {
const remoteSyntax = new URLSearchParams(query).get('remote');
if (remoteSyntax) {
const [url, global] = extractUrlAndGlobal(remoteSyntax);
acc[key] = { global, url };
}
}
}
// if its just a string (global@url)
else if (typeof value === 'string') {
const [url, global] = extractUrlAndGlobal(value);
acc[key] = { global, url };
}
// we dont know or currently support this type
else {
//@ts-ignore
console.warn('remotes process', process.env.REMOTES);
throw new Error(
`[mf] Invalid value received for runtime_remote "${key}"`
);
}
return acc;
},
{} as RuntimeRemotesMap);

return runtimeRemotes;
} catch (err) {
console.warn('Unable to retrieve runtime remotes: ', err);
}

return {} as RuntimeRemotesMap;
};



const importDelegatedModule = async (
keyOrRuntimeRemoteItem: string | RuntimeRemote
) => {
// @ts-ignore
return loadScript(keyOrRuntimeRemoteItem)
.then((asyncContainer: WebpackRemoteContainer) => {
return asyncContainer;
})
.then((asyncContainer: WebpackRemoteContainer) => {
// most of this is only needed because of legacy promise based implementation
// can remove proxies once we remove promise based implementations
if (typeof window === 'undefined') {
if (!Object.hasOwnProperty.call(keyOrRuntimeRemoteItem, 'globalThis')) {
return asyncContainer;
}

// return asyncContainer;

//TODO: need to solve chunk flushing with delegated modules
return {
get: function (arg: string) {
//@ts-ignore
return asyncContainer.get(arg).then((f) => {
const m = f();
const result = {
__esModule: m.__esModule,
};
for (const prop in m) {
if (typeof m[prop] === 'function') {
Object.defineProperty(result, prop, {
get: function () {
return function () {
//@ts-ignore
if (globalThis.usedChunks)
//@ts-ignore
globalThis.usedChunks.add(
//@ts-ignore
`${keyOrRuntimeRemoteItem.global}->${arg}`
);
//eslint-disable-next-line prefer-rest-params
return m[prop](...arguments);
};
},
enumerable: true,
});
} else {
Object.defineProperty(result, prop, {
get: () => {
//@ts-ignore
if (globalThis.usedChunks)
//@ts-ignore
globalThis.usedChunks.add(
//@ts-ignore
`${keyOrRuntimeRemoteItem.global}->${arg}`
);

return m[prop];
},
enumerable: true,
});
}
}

if (m.then) {
return Promise.resolve(() => result);
}

return () => result;
});
},
init: asyncContainer.init,
};
} else {
return asyncContainer;
}
});
};

// eslint-disable-next-line no-async-promise-executor
module.exports = new Promise(async (resolve, reject) => {
Expand Down
10 changes: 5 additions & 5 deletions packages/utilities/src/plugins/DelegateModulesPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ class DelegateModulesPlugin {
dependencyModule &&
!compilation.chunkGraph.isModuleInChunk(dependencyModule, chunk)
) {
this.addModuleAndDependenciesToChunk(
dependencyModule,
chunk,
compilation
);
// this.addModuleAndDependenciesToChunk(
// dependencyModule,
// chunk,
// compilation
// );
}
}
}
Expand Down

0 comments on commit e134caa

Please sign in to comment.