Skip to content

Instantly share code, notes, and snippets.

@ScriptedAlchemy
Created March 24, 2022 04:18
Show Gist options
  • Save ScriptedAlchemy/7c1c7b25665524fbb0dfb4f06db7ebff to your computer and use it in GitHub Desktop.
Save ScriptedAlchemy/7c1c7b25665524fbb0dfb4f06db7ebff to your computer and use it in GitHub Desktop.
Chunk Flushing
const React = require("react");
const { Head } = require("next/document");
const path = require("path");
const crypto = require("crypto");
const generateDynamicRemoteScript = (remoteContainer) => {
const [name, path] = remoteContainer.path.split("@");
const remoteUrl = new URL(path.replace("ssr", "chunks"));
remoteUrl.searchParams.set("cbust", Date.now());
return {
[name]: React.createElement("script", {
"data-webpack": name,
src: remoteUrl.toString(),
async: true,
key: name,
}),
};
};
const extractChunkCorrelation = (remoteContainer, lookup, request) => {
if (
remoteContainer &&
remoteContainer.chunkMap &&
remoteContainer.chunkMap.federatedModules
) {
const path = remoteContainer.path.split("@")[1];
const [baseurl] = path.split("static/ssr");
remoteContainer.chunkMap.federatedModules.map((federatedRemote) => {
federatedRemote.exposes[request].forEach((remoteChunks) => {
remoteChunks.chunks.map((chunk) => {
if (!lookup.files.includes(new URL(chunk, baseurl).href)) {
lookup.files.push(new URL(chunk, baseurl).href);
}
});
});
});
} else {
console.warn(
"Module Federation:",
"no federated modules in chunk map OR experiments.flushChunks is disabled"
);
}
};
const requireMethod =
typeof __non_webpack_require__ !== "undefined"
? __non_webpack_require__
: require;
const requestPath = path.join(
process.cwd(),
".next",
"server/pages",
"../../react-loadable-manifest.json"
);
let remotes = {};
const loadableManifest = requireMethod(requestPath);
requireMethod.cache[requestPath].exports = new Proxy(loadableManifest, {
get(target, prop, receiver) {
if (!target[prop]) {
let remoteImport = prop.split("->")[1];
if (remoteImport) {
remoteImport = remoteImport.trim();
const [remote, module] = remoteImport.split("/");
if (!remotes[remote]) {
Object.assign(
remotes,
generateDynamicRemoteScript(global.loadedRemotes[remote])
);
}
const dynamicLoadableManifestItem = {
id: prop,
files: [],
};
// TODO: figure out who is requesting module
let remoteModuleContainerId;
Object.values(global.loadedRemotes).find((remote) => {
if (
remote.chunkMap &&
remote.chunkMap.federatedModules[0] &&
remote.chunkMap.federatedModules[0].remoteModules
) {
if (
remote.chunkMap.federatedModules[0].remoteModules[remoteImport]
) {
remoteModuleContainerId =
remote.chunkMap.federatedModules[0].remoteModules[remoteImport];
return true;
}
}
});
if (remoteModuleContainerId && process.env.NODE_ENV !== "development") {
dynamicLoadableManifestItem.id = remoteModuleContainerId;
}
extractChunkCorrelation(
global.loadedRemotes[remote],
dynamicLoadableManifestItem,
`./${module}`
);
return dynamicLoadableManifestItem;
}
}
return target[prop];
},
});
const flushChunks = async (remoteEnvVar = process.env.REMOTES) => {
remotes = {};
const remoteKeys = Object.keys(remoteEnvVar);
const preload = [];
try {
for (const key in loadableManifest) {
const [where, what] = key.split("->");
const request = what.trim();
const foundFederatedImport = remoteKeys.find((remoteKey) => {
return request.startsWith(`${remoteKey}/`);
});
if (foundFederatedImport) {
const remotePreload = remoteEnvVar[foundFederatedImport]().then(
(remoteContainer) => {
Object.assign(
remotes,
generateDynamicRemoteScript(remoteContainer)
);
const inferRequest = what.split(`${foundFederatedImport}/`)[1];
const request = `./${inferRequest}`;
extractChunkCorrelation(
remoteContainer,
loadableManifest[key],
request
);
}
);
preload.push(remotePreload);
}
}
await Promise.all(preload);
return remotes;
} catch (e) {
console.error("Module Federation: Could not flush chunks", e);
}
return [];
};
export class ExtendedHead extends Head {
constructor(props, context) {
super(props, context);
this.context = context;
}
getCssLinks(files) {
const cssLinks = super.getCssLinks(files);
if (Array.isArray(cssLinks)) {
return cssLinks.map((chunk) => {
if (!chunk) return null;
const [prefix, asset] = chunk.props.href.split(
this.context.assetPrefix
);
if (
chunk.props.href &&
chunk.props.href.startsWith("/") &&
chunk.props.href.includes("http")
) {
return React.cloneElement(chunk, {
...chunk.props,
href: `http${chunk.props.href.split("http")[1]}`,
});
} else if (
chunk.props.href &&
chunk.props.href.includes("-fed.") &&
this.context.assetPrefix
) {
const replacedArg = this.context.assetPrefix.endsWith("/")
? chunk.props.href.replace(`${this.context.assetPrefix}_next/`, "")
: chunk.props.href.replace(
`${this.context.assetPrefix}/_next/`,
""
);
return React.cloneElement(chunk, {
...chunk.props,
href: replacedArg,
});
} else if (asset.includes("http") && asset.startsWith("/")) {
return React.cloneElement(chunk, {
...chunk.props,
href: `http${asset.split("http")[1]}`,
});
} else return chunk;
});
}
return cssLinks;
}
getDynamicChunks(files) {
const dynamicChunks = super.getDynamicChunks(files);
return dynamicChunks.map((chunk) => {
if (!chunk) return null;
if (chunk.props.src.startsWith("/") && chunk.props.src.includes("http")) {
return React.cloneElement(chunk, {
...chunk.props,
src: `http${chunk.props.src.split("http")[1]}`,
});
} else if (
chunk.props.src.includes("-fed.") &&
this.context.assetPrefix
) {
const replacedArg = this.context.assetPrefix.endsWith("/")
? chunk.props.src.replace(`${this.context.assetPrefix}_next/`, "")
: chunk.props.src.replace(`${this.context.assetPrefix}/_next/`, "");
return React.cloneElement(chunk, {
...chunk.props,
src: replacedArg,
});
}
return chunk;
});
}
}
var interval;
const hashmap = {};
const revalidate = (options) => {
if (global.REMOTE_CONFIG) {
return new Promise((res) => {
const { poll, timeout } = Object.assign(
{
poll: process.env.NODE_ENV === "development",
timeout: 3000,
},
options
);
if (poll && interval) {
clearInterval(interval);
}
if (poll) {
interval = setInterval(() => {
revalidate(options);
}, timeout);
}
for (const property in global.REMOTE_CONFIG) {
const [name, url] = global.REMOTE_CONFIG[property].split("@");
fetch(url)
.then((re) => re.text())
.then((contents) => {
var hash = crypto.createHash("md5").update(contents).digest("hex");
if (hashmap[name]) {
if (hashmap[name] !== hash) {
hashmap[name] = hash;
console.log(name, "hash is different - must hot reload server");
res();
}
} else {
hashmap[name] = hash;
}
})
.catch((e) => {
console.log(
"Remote",
name,
url,
"Failed to load or is not online",
e
);
});
}
}).then(() => {
let req;
if (typeof __non_webpack_require__ === "undefined") {
req = require;
} else {
req = __non_webpack_require__;
}
if (global.hotLoad) {
global.hotLoad();
}
global.loadedRemotes = {};
// do hot reload things
});
}
return new Promise((res, rej) => {});
};
module.exports = { flushChunks, ExtendedHead, revalidate };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment