mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-27 01:22:49 +00:00
1373 lines
52 KiB
JavaScript
1373 lines
52 KiB
JavaScript
import { a as MF_SERVER_IDENTIFIER, n as ActionKind, o as UpdateMode, r as DEFAULT_TAR_NAME, t as Action } from "./Action-DNNg2YDh.mjs";
|
|
import { a as getIdentifier, c as logger$1, i as getFreePort, l as LogKind, n as UpdateKind, o as getIPV4, r as fib, s as fileLog, t as Broker, u as APIKind } from "./Broker-z82OgzMe.mjs";
|
|
import { createRequire } from "node:module";
|
|
import fs, { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
import path, { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "fs/promises";
|
|
import { utils } from "@module-federation/managers";
|
|
import typescript from "typescript";
|
|
import { ENCODE_NAME_PREFIX, MANIFEST_EXT, TEMP_DIR, decodeName, getProcessEnv, inferAutoPublicPath, parseEntry } from "@module-federation/sdk";
|
|
import ansiColors from "ansi-colors";
|
|
import { Agent } from "undici";
|
|
import { ThirdPartyExtractor } from "@module-federation/third-party-dts-extractor";
|
|
import AdmZip from "adm-zip";
|
|
import crypto from "crypto";
|
|
import { TYPE_001, typeDescMap } from "@module-federation/error-codes";
|
|
import { logAndReport } from "@module-federation/error-codes/node";
|
|
import { execFile, fork } from "child_process";
|
|
import util from "util";
|
|
import WebSocket from "isomorphic-ws";
|
|
import http from "http";
|
|
import process$1 from "process";
|
|
|
|
//#region \0rolldown/runtime.js
|
|
var __defProp = Object.defineProperty;
|
|
var __exportAll = (all, no_symbols) => {
|
|
let target = {};
|
|
for (var name in all) {
|
|
__defProp(target, name, {
|
|
get: all[name],
|
|
enumerable: true
|
|
});
|
|
}
|
|
if (!no_symbols) {
|
|
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
}
|
|
return target;
|
|
};
|
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
|
//#endregion
|
|
//#region src/server/message/Action/AddPublisher.ts
|
|
var AddPublisherAction = class extends Action {
|
|
constructor(payload) {
|
|
super({ payload }, ActionKind.ADD_PUBLISHER);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/message/Action/AddSubscriber.ts
|
|
var AddSubscriberAction = class extends Action {
|
|
constructor(payload) {
|
|
super({ payload }, ActionKind.ADD_SUBSCRIBER);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/message/Action/ExitSubscriber.ts
|
|
var ExitSubscriberAction = class extends Action {
|
|
constructor(payload) {
|
|
super({ payload }, ActionKind.EXIT_SUBSCRIBER);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/message/Action/ExitPublisher.ts
|
|
var ExitPublisherAction = class extends Action {
|
|
constructor(payload) {
|
|
super({ payload }, ActionKind.EXIT_PUBLISHER);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/message/Action/NotifyWebClient.ts
|
|
var NotifyWebClientAction = class extends Action {
|
|
constructor(payload) {
|
|
super({ payload }, ActionKind.NOTIFY_WEB_CLIENT);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/message/Action/UpdatePublisher.ts
|
|
var UpdatePublisherAction = class extends Action {
|
|
constructor(payload) {
|
|
super({ payload }, ActionKind.UPDATE_PUBLISHER);
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/broker/createBroker.ts
|
|
function createBroker() {
|
|
const sub = fork(path.resolve(__dirname, "./start-broker.js"), [], {
|
|
detached: true,
|
|
stdio: "ignore",
|
|
env: process.env
|
|
});
|
|
sub.send("start");
|
|
sub.unref();
|
|
return sub;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/server/DevServer.ts
|
|
var ModuleFederationDevServer = class {
|
|
constructor(ctx) {
|
|
this._publishWebSocket = null;
|
|
this._subscriberWebsocketMap = {};
|
|
this._reconnect = true;
|
|
this._reconnectTimes = 0;
|
|
this._isConnected = false;
|
|
this._isReconnecting = false;
|
|
this._updateCallback = () => Promise.resolve(void 0);
|
|
const { name, remotes, remoteTypeTarPath, updateCallback } = ctx;
|
|
this._ip = getIPV4();
|
|
this._name = name;
|
|
this._remotes = remotes;
|
|
this._remoteTypeTarPath = remoteTypeTarPath;
|
|
this._updateCallback = updateCallback;
|
|
this._stopWhenSIGTERMOrSIGINT();
|
|
this._handleUnexpectedExit();
|
|
this._connectPublishToServer();
|
|
}
|
|
_connectPublishToServer() {
|
|
if (!this._reconnect) return;
|
|
fileLog(`Publisher:${this._name} Trying to connect to ws://${this._ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}...`, MF_SERVER_IDENTIFIER, "info");
|
|
this._publishWebSocket = new WebSocket(`ws://${this._ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}?WEB_SOCKET_CONNECT_MAGIC_ID=${Broker.WEB_SOCKET_CONNECT_MAGIC_ID}`);
|
|
this._publishWebSocket.on("open", () => {
|
|
fileLog(`Current pid: ${process.pid}, publisher:${this._name} connected to ws://${this._ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}, starting service...`, MF_SERVER_IDENTIFIER, "info");
|
|
this._isConnected = true;
|
|
const addPublisherAction = new AddPublisherAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
remoteTypeTarPath: this._remoteTypeTarPath
|
|
});
|
|
this._publishWebSocket?.send(JSON.stringify(addPublisherAction));
|
|
this._connectSubscribers();
|
|
});
|
|
this._publishWebSocket.on("message", async (message) => {
|
|
try {
|
|
const parsedMessage = JSON.parse(message.toString());
|
|
if (parsedMessage.type === "Log") {
|
|
if (parsedMessage.kind === LogKind.BrokerExitLog) {
|
|
fileLog(`Receive broker exit signal, ${this._name} service will exit...`, MF_SERVER_IDENTIFIER, "warn");
|
|
this._exit();
|
|
}
|
|
}
|
|
if (parsedMessage.type === "API") {
|
|
if (parsedMessage.kind === APIKind.FETCH_TYPES) {
|
|
const { payload: { remoteInfo } } = parsedMessage;
|
|
fileLog(`${this._name} Receive broker FETCH_TYPES, payload as follows: ${JSON.stringify(remoteInfo, null, 2)}.`, MF_SERVER_IDENTIFIER, "info");
|
|
await this.fetchDynamicRemoteTypes({ remoteInfo });
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
const exitPublisher = new ExitPublisherAction({
|
|
name: this._name,
|
|
ip: this._ip
|
|
});
|
|
const exitSubscriber = new ExitSubscriberAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
publishers: this._remotes.map((remote) => ({
|
|
name: remote.name,
|
|
ip: remote.ip
|
|
}))
|
|
});
|
|
this._publishWebSocket?.send(JSON.stringify(exitPublisher));
|
|
this._publishWebSocket?.send(JSON.stringify(exitSubscriber));
|
|
fileLog("Parse messages error, ModuleFederationDevServer will exit...", MF_SERVER_IDENTIFIER, "fatal");
|
|
this._exit();
|
|
}
|
|
});
|
|
this._publishWebSocket.on("close", (code) => {
|
|
fileLog(`Connection closed with code ${code}.`, MF_SERVER_IDENTIFIER, "warn");
|
|
this._publishWebSocket && this._publishWebSocket.close();
|
|
this._publishWebSocket = null;
|
|
if (!this._reconnect) return;
|
|
const reconnectTime = fib(++this._reconnectTimes);
|
|
fileLog(`start reconnecting to server after ${reconnectTime}s.`, MF_SERVER_IDENTIFIER, "info");
|
|
setTimeout(() => this._connectPublishToServer(), reconnectTime * 1e3);
|
|
});
|
|
this._publishWebSocket.on("error", this._tryCreateBackgroundBroker.bind(this));
|
|
}
|
|
_connectSubscriberToServer(remote) {
|
|
const { name, ip } = remote;
|
|
fileLog(`remote module:${name} trying to connect to ws://${ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}...`, MF_SERVER_IDENTIFIER, "info");
|
|
const identifier = getIdentifier({
|
|
name,
|
|
ip
|
|
});
|
|
this._subscriberWebsocketMap[identifier] = new WebSocket(`ws://${ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}?WEB_SOCKET_CONNECT_MAGIC_ID=${Broker.WEB_SOCKET_CONNECT_MAGIC_ID}`);
|
|
this._subscriberWebsocketMap[identifier].on("open", () => {
|
|
fileLog(`Current pid: ${process.pid} remote module: ${name} connected to ws://${ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}, starting service...`, MF_SERVER_IDENTIFIER, "info");
|
|
const addSubscriber = new AddSubscriberAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
publishers: [{
|
|
name,
|
|
ip
|
|
}]
|
|
});
|
|
this._subscriberWebsocketMap[identifier].send(JSON.stringify(addSubscriber));
|
|
});
|
|
this._subscriberWebsocketMap[identifier].on("message", async (message) => {
|
|
try {
|
|
const parsedMessage = JSON.parse(message.toString());
|
|
if (parsedMessage.type === "Log") {
|
|
if (parsedMessage.kind === LogKind.BrokerExitLog) {
|
|
fileLog(`${identifier}'s Server exit, thus ${identifier} will no longer has reload ability.`, MF_SERVER_IDENTIFIER, "warn");
|
|
this._exit();
|
|
}
|
|
}
|
|
if (parsedMessage.type === "API") {
|
|
if (parsedMessage.kind === APIKind.UPDATE_SUBSCRIBER) {
|
|
const { payload: { updateKind, updateSourcePaths, name: subscribeName, remoteTypeTarPath, updateMode } } = parsedMessage;
|
|
await this._updateSubscriber({
|
|
remoteTypeTarPath,
|
|
name: subscribeName,
|
|
updateKind,
|
|
updateMode,
|
|
updateSourcePaths
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
const exitSubscriber = new ExitSubscriberAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
publishers: [{
|
|
name,
|
|
ip
|
|
}]
|
|
});
|
|
this._subscriberWebsocketMap[identifier].send(JSON.stringify(exitSubscriber));
|
|
fileLog(`${identifier} exit,
|
|
error: ${err instanceof Error ? err.toString() : JSON.stringify(err)}
|
|
`, MF_SERVER_IDENTIFIER, "warn");
|
|
}
|
|
});
|
|
this._subscriberWebsocketMap[identifier].on("close", (code) => {
|
|
fileLog(`Connection closed with code ${code}.`, MF_SERVER_IDENTIFIER, "warn");
|
|
this._subscriberWebsocketMap[identifier]?.close();
|
|
delete this._subscriberWebsocketMap[identifier];
|
|
});
|
|
this._subscriberWebsocketMap[identifier].on("error", (err) => {
|
|
if ("code" in err && err.code === "ETIMEDOUT") fileLog(`Can not connect ${JSON.stringify(remote)}, please make sure this remote is started locally.`, MF_SERVER_IDENTIFIER, "warn");
|
|
else console.error(err);
|
|
this._subscriberWebsocketMap[identifier]?.close();
|
|
delete this._subscriberWebsocketMap[identifier];
|
|
});
|
|
}
|
|
_connectSubscribers() {
|
|
this._remotes.forEach((remote) => {
|
|
this._connectSubscriberToServer(remote);
|
|
});
|
|
}
|
|
async _updateSubscriber(options) {
|
|
const { updateMode, updateKind, updateSourcePaths, name, remoteTypeTarPath, remoteInfo } = options;
|
|
fileLog(`[_updateSubscriber] run, options: ${JSON.stringify(options, null, 2)}`, MF_SERVER_IDENTIFIER, "warn");
|
|
if (updateMode === UpdateMode.PASSIVE && updateSourcePaths.includes(this._name)) {
|
|
fileLog(`[_updateSubscriber] run, updateSourcePaths:${updateSourcePaths} includes ${this._name}, update ignore!`, MF_SERVER_IDENTIFIER, "warn");
|
|
return;
|
|
}
|
|
if (updateSourcePaths.slice(-1)[0] === this._name) {
|
|
fileLog(`[_updateSubscriber] run, updateSourcePaths:${updateSourcePaths} ends is ${this._name}, update ignore!`, MF_SERVER_IDENTIFIER, "warn");
|
|
return;
|
|
}
|
|
fileLog(`[_updateSubscriber] run, updateSourcePaths:${updateSourcePaths}, current module:${this._name}, update start...`, MF_SERVER_IDENTIFIER, "info");
|
|
await this._updateCallback({
|
|
name,
|
|
updateMode,
|
|
updateKind,
|
|
updateSourcePaths,
|
|
remoteTypeTarPath,
|
|
remoteInfo
|
|
});
|
|
const newUpdateSourcePaths = updateSourcePaths.concat(this._name);
|
|
const updatePublisher = new UpdatePublisherAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
updateMode: UpdateMode.PASSIVE,
|
|
updateKind,
|
|
updateSourcePaths: newUpdateSourcePaths,
|
|
remoteTypeTarPath: this._remoteTypeTarPath
|
|
});
|
|
fileLog(`[_updateSubscriber] run, updateSourcePaths:${newUpdateSourcePaths}, update publisher ${this._name} start...`, MF_SERVER_IDENTIFIER, "info");
|
|
this._publishWebSocket?.send(JSON.stringify(updatePublisher));
|
|
}
|
|
_tryCreateBackgroundBroker(err) {
|
|
if (!((err?.code === "ECONNREFUSED" || err?.code === "ETIMEDOUT") && err.port === Broker.DEFAULT_WEB_SOCKET_PORT)) {
|
|
fileLog(`websocket error: ${err.stack}`, MF_SERVER_IDENTIFIER, "fatal");
|
|
return;
|
|
}
|
|
fileLog(`Failed to connect to ws://${this._ip}:${Broker.DEFAULT_WEB_SOCKET_PORT}...`, MF_SERVER_IDENTIFIER, "fatal");
|
|
this._isReconnecting = true;
|
|
setTimeout(() => {
|
|
this._isReconnecting = false;
|
|
if (this._reconnect === false) return;
|
|
fileLog("Creating new background broker...", MF_SERVER_IDENTIFIER, "warn");
|
|
createBroker().on("message", (message) => {
|
|
if (message === "ready") {
|
|
fileLog("background broker started.", MF_SERVER_IDENTIFIER, "info");
|
|
this._reconnectTimes = 1;
|
|
if (process.send) process.send("ready");
|
|
}
|
|
});
|
|
}, Math.ceil(100 * Math.random()));
|
|
}
|
|
_stopWhenSIGTERMOrSIGINT() {
|
|
process.on("SIGTERM", () => {
|
|
fileLog(`Process(${process.pid}) SIGTERM, ModuleFederationDevServer will exit...`, MF_SERVER_IDENTIFIER, "warn");
|
|
this._exit();
|
|
});
|
|
process.on("SIGINT", () => {
|
|
fileLog(`Process(${process.pid}) SIGINT, ModuleFederationDevServer will exit...`, MF_SERVER_IDENTIFIER, "warn");
|
|
this._exit();
|
|
});
|
|
}
|
|
_handleUnexpectedExit() {
|
|
process.on("unhandledRejection", (error) => {
|
|
if (this._isReconnecting) return;
|
|
console.error("Unhandled Rejection Error: ", error);
|
|
fileLog(`Process(${process.pid}) unhandledRejection, garfishModuleServer will exit...`, MF_SERVER_IDENTIFIER, "error");
|
|
this._exit();
|
|
});
|
|
process.on("uncaughtException", (error) => {
|
|
if (this._isReconnecting) return;
|
|
console.error("Unhandled Exception Error: ", error);
|
|
fileLog(`Process(${process.pid}) uncaughtException, garfishModuleServer will exit...`, MF_SERVER_IDENTIFIER, "error");
|
|
this._exit();
|
|
});
|
|
}
|
|
_exit() {
|
|
this._reconnect = false;
|
|
if (this._publishWebSocket) {
|
|
const exitPublisher = new ExitPublisherAction({
|
|
name: this._name,
|
|
ip: this._ip
|
|
});
|
|
this._publishWebSocket.send(JSON.stringify(exitPublisher));
|
|
this._publishWebSocket.on("message", (message) => {
|
|
const parsedMessage = JSON.parse(message.toString());
|
|
fileLog(`[${parsedMessage.kind}]: ${JSON.stringify(parsedMessage)}`, MF_SERVER_IDENTIFIER, "info");
|
|
});
|
|
}
|
|
if (this._publishWebSocket) {
|
|
this._publishWebSocket.close();
|
|
this._publishWebSocket = null;
|
|
}
|
|
process.exit(0);
|
|
}
|
|
exit() {
|
|
this._exit();
|
|
}
|
|
update(options) {
|
|
if (!this._publishWebSocket || !this._isConnected) return;
|
|
const { updateKind, updateMode, updateSourcePaths, clientName } = options;
|
|
fileLog(`update run, ${this._name} module update, updateKind: ${updateKind}, updateMode: ${updateMode}, updateSourcePaths: ${updateSourcePaths}`, MF_SERVER_IDENTIFIER, "info");
|
|
if (updateKind === UpdateKind.RELOAD_PAGE) {
|
|
const notifyWebClient = new NotifyWebClientAction({
|
|
name: clientName || this._name,
|
|
updateMode
|
|
});
|
|
this._publishWebSocket.send(JSON.stringify(notifyWebClient));
|
|
return;
|
|
}
|
|
const updatePublisher = new UpdatePublisherAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
updateMode,
|
|
updateKind,
|
|
updateSourcePaths: [this._name],
|
|
remoteTypeTarPath: this._remoteTypeTarPath
|
|
});
|
|
this._publishWebSocket.send(JSON.stringify(updatePublisher));
|
|
}
|
|
async fetchDynamicRemoteTypes(options) {
|
|
const { remoteInfo, once } = options;
|
|
const updateMode = UpdateMode.PASSIVE;
|
|
const updateKind = UpdateKind.UPDATE_TYPE;
|
|
fileLog(`fetchDynamicRemoteTypes: remoteInfo: ${JSON.stringify(remoteInfo)}`, MF_SERVER_IDENTIFIER, "info");
|
|
await this._updateCallback({
|
|
name: this._name,
|
|
updateMode,
|
|
updateKind,
|
|
updateSourcePaths: [],
|
|
remoteTypeTarPath: "",
|
|
remoteInfo,
|
|
once
|
|
});
|
|
const updatePublisher = new UpdatePublisherAction({
|
|
name: this._name,
|
|
ip: this._ip,
|
|
updateMode,
|
|
updateKind,
|
|
updateSourcePaths: [this._name],
|
|
remoteTypeTarPath: this._remoteTypeTarPath
|
|
});
|
|
this._publishWebSocket.send(JSON.stringify(updatePublisher));
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/server/createHttpServer.ts
|
|
async function createHttpServer(options) {
|
|
const { typeTarPath } = options;
|
|
const freeport = await getFreePort();
|
|
const server = http.createServer((req, res) => {
|
|
if ((req.url?.split("?")[0] ?? "/") === `/${DEFAULT_TAR_NAME}`) {
|
|
res.statusCode = 200;
|
|
res.setHeader("Content-Type", "application/x-gzip");
|
|
if (req.method === "HEAD") {
|
|
res.end();
|
|
return;
|
|
}
|
|
const stream = fs.createReadStream(typeTarPath);
|
|
stream.on("error", () => {
|
|
if (!res.headersSent) res.statusCode = 500;
|
|
res.end();
|
|
});
|
|
res.on("close", () => {
|
|
stream.destroy();
|
|
});
|
|
stream.pipe(res);
|
|
return;
|
|
}
|
|
res.statusCode = 404;
|
|
res.end();
|
|
});
|
|
server.listen(freeport);
|
|
return {
|
|
server,
|
|
serverAddress: `http://${getIPV4()}:${freeport}`
|
|
};
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/core/lib/typeScriptCompiler.ts
|
|
const STARTS_WITH_SLASH = /^\//;
|
|
const DEFINITION_FILE_EXTENSION = ".d.ts";
|
|
const retrieveMfTypesPath = (tsConfig, remoteOptions) => normalize(tsConfig.compilerOptions.outDir.replace(remoteOptions.compiledTypesFolder, ""));
|
|
const retrieveOriginalOutDir = (tsConfig, remoteOptions) => normalize(tsConfig.compilerOptions.outDir.replace(remoteOptions.compiledTypesFolder, "").replace(remoteOptions.typesFolder, ""));
|
|
const retrieveMfAPITypesPath = (tsConfig, remoteOptions) => join(retrieveOriginalOutDir(tsConfig, remoteOptions), `${remoteOptions.typesFolder}.d.ts`);
|
|
function writeTempTsConfig(tsConfig, context, name, cwd) {
|
|
const createHash = (contents) => {
|
|
return crypto.createHash("md5").update(contents).digest("hex");
|
|
};
|
|
const hash = createHash(`${JSON.stringify(tsConfig)}${name}${Date.now()}`);
|
|
const tempTsConfigJsonPath = resolve(cwd ?? context, "node_modules", TEMP_DIR, `tsconfig.${hash}.json`);
|
|
mkdirSync(dirname(tempTsConfigJsonPath), { recursive: true });
|
|
writeFileSync(tempTsConfigJsonPath, JSON.stringify(tsConfig, null, 2));
|
|
return tempTsConfigJsonPath;
|
|
}
|
|
const removeExt = (f) => {
|
|
const vueExt = ".vue";
|
|
const ext = extname(f);
|
|
if (ext === vueExt) return f;
|
|
const regexPattern = new RegExp(`\\${ext}$`);
|
|
return f.replace(regexPattern, "");
|
|
};
|
|
function getExposeKey(options) {
|
|
const { filePath, rootDir, outDir, mapExposeToEntry } = options;
|
|
return mapExposeToEntry[relative(outDir, filePath.replace(new RegExp(`\\.d.ts$`), ""))];
|
|
}
|
|
const processTypesFile = async (options) => {
|
|
const { outDir, filePath, rootDir, cb, mapExposeToEntry, mfTypePath } = options;
|
|
if (!existsSync(filePath)) return;
|
|
if ((await stat(filePath)).isDirectory()) {
|
|
const files = await readdir(filePath);
|
|
await Promise.all(files.map((file) => processTypesFile({
|
|
...options,
|
|
filePath: join(filePath, file)
|
|
})));
|
|
} else if (filePath.endsWith(".d.ts")) {
|
|
const exposeKey = getExposeKey({
|
|
filePath,
|
|
rootDir,
|
|
outDir,
|
|
mapExposeToEntry
|
|
});
|
|
if (exposeKey) {
|
|
const mfeTypeEntry = join(mfTypePath, `${exposeKey === "." ? "index" : exposeKey}${DEFINITION_FILE_EXTENSION}`);
|
|
const mfeTypeEntryDirectory = dirname(mfeTypeEntry);
|
|
const relativePathToOutput = relative(mfeTypeEntryDirectory, filePath).replace(DEFINITION_FILE_EXTENSION, "").replace(STARTS_WITH_SLASH, "").split(sep).join("/");
|
|
mkdirSync(mfeTypeEntryDirectory, { recursive: true });
|
|
await writeFile(mfeTypeEntry, `export * from './${relativePathToOutput}';\nexport { default } from './${relativePathToOutput}';`);
|
|
}
|
|
cb(await readFile(filePath, "utf8"));
|
|
}
|
|
};
|
|
const getPMFromUserAgent = () => {
|
|
const userAgent = process.env["npm_config_user_agent"];
|
|
if (userAgent == null) return "null";
|
|
return userAgent.split("/")[0];
|
|
};
|
|
const resolvePackageManagerExecutable = () => {
|
|
switch (getPMFromUserAgent()) {
|
|
case "yarn": return "yarn";
|
|
default: return "npx";
|
|
}
|
|
};
|
|
const splitCommandArgs = (value) => {
|
|
const args = [];
|
|
let current = "";
|
|
let quote = null;
|
|
let escaped = false;
|
|
for (const char of value) {
|
|
if (escaped) {
|
|
current += char;
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if (char === "\\") {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (quote) {
|
|
if (char === quote) quote = null;
|
|
else current += char;
|
|
continue;
|
|
}
|
|
if (char === "\"" || char === "'") {
|
|
quote = char;
|
|
continue;
|
|
}
|
|
if (char.trim() === "") {
|
|
if (current) {
|
|
args.push(current);
|
|
current = "";
|
|
}
|
|
continue;
|
|
}
|
|
current += char;
|
|
}
|
|
if (current) args.push(current);
|
|
return args;
|
|
};
|
|
const formatCommandForDisplay = (executable, args) => {
|
|
const formatArg = (arg) => {
|
|
if (/[\s'"]/.test(arg)) return JSON.stringify(arg);
|
|
return arg;
|
|
};
|
|
return [executable, ...args].map(formatArg).join(" ");
|
|
};
|
|
const compileTs = async (mapComponentsToExpose, tsConfig, remoteOptions) => {
|
|
if (!Object.keys(mapComponentsToExpose).length) return;
|
|
const { compilerOptions } = tsConfig;
|
|
const tempTsConfigJsonPath = writeTempTsConfig(tsConfig, remoteOptions.context, remoteOptions.moduleFederationConfig.name || "mf", typeof remoteOptions.moduleFederationConfig.dts !== "boolean" ? remoteOptions.moduleFederationConfig.dts?.cwd ?? void 0 : void 0);
|
|
logger$1.debug(`tempTsConfigJsonPath: ${tempTsConfigJsonPath}`);
|
|
try {
|
|
const mfTypePath = retrieveMfTypesPath(tsConfig, remoteOptions);
|
|
const thirdPartyExtractor = new ThirdPartyExtractor({
|
|
destDir: resolve(mfTypePath, "node_modules"),
|
|
context: remoteOptions.context,
|
|
exclude: typeof remoteOptions.extractThirdParty === "object" ? remoteOptions.extractThirdParty.exclude : void 0
|
|
});
|
|
const execPromise = util.promisify(execFile);
|
|
const pmExecutable = resolvePackageManagerExecutable();
|
|
const compilerArgs = splitCommandArgs(remoteOptions.compilerInstance);
|
|
const cmdArgs = [
|
|
...compilerArgs.length > 0 ? compilerArgs : [remoteOptions.compilerInstance],
|
|
"--project",
|
|
tempTsConfigJsonPath
|
|
];
|
|
const cmd = formatCommandForDisplay(pmExecutable, cmdArgs);
|
|
try {
|
|
await execPromise(pmExecutable, cmdArgs, {
|
|
cwd: typeof remoteOptions.moduleFederationConfig.dts !== "boolean" ? remoteOptions.moduleFederationConfig.dts?.cwd ?? void 0 : void 0,
|
|
shell: process.platform === "win32"
|
|
});
|
|
} catch (err) {
|
|
if (compilerOptions.tsBuildInfoFile) try {
|
|
await rm(compilerOptions.tsBuildInfoFile);
|
|
} catch (e) {}
|
|
logAndReport(TYPE_001, typeDescMap, { cmd }, (msg) => {
|
|
throw new Error(msg);
|
|
}, void 0);
|
|
}
|
|
const mapExposeToEntry = Object.fromEntries(Object.entries(mapComponentsToExpose).map(([exposed, filename]) => {
|
|
const normalizedFileName = normalize(filename);
|
|
let relativeFileName = "";
|
|
if (isAbsolute(normalizedFileName)) relativeFileName = relative(tsConfig.compilerOptions.rootDir, normalizedFileName);
|
|
else relativeFileName = relative(tsConfig.compilerOptions.rootDir, resolve(remoteOptions.context, normalizedFileName));
|
|
return [removeExt(relativeFileName), exposed];
|
|
}));
|
|
const cb = remoteOptions.extractThirdParty ? thirdPartyExtractor.collectPkgs.bind(thirdPartyExtractor) : () => void 0;
|
|
await processTypesFile({
|
|
outDir: compilerOptions.outDir,
|
|
filePath: compilerOptions.outDir,
|
|
rootDir: compilerOptions.rootDir,
|
|
mfTypePath,
|
|
cb,
|
|
mapExposeToEntry
|
|
});
|
|
if (remoteOptions.extractThirdParty) await thirdPartyExtractor.copyDts();
|
|
if (remoteOptions.deleteTsConfig) await rm(tempTsConfigJsonPath);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/core/lib/archiveHandler.ts
|
|
const retrieveTypesZipPath = (mfTypesPath, remoteOptions) => join(mfTypesPath.replace(remoteOptions.typesFolder, ""), `${remoteOptions.typesFolder}.zip`);
|
|
const createTypesArchive = async (tsConfig, remoteOptions) => {
|
|
const mfTypesPath = retrieveMfTypesPath(tsConfig, remoteOptions);
|
|
const zip = new AdmZip();
|
|
zip.addLocalFolder(mfTypesPath);
|
|
return zip.writeZipPromise(retrieveTypesZipPath(mfTypesPath, remoteOptions));
|
|
};
|
|
const downloadErrorLogger = (destinationFolder, fileToDownload) => (reason) => {
|
|
throw {
|
|
...reason,
|
|
message: `Network error: Unable to download federated mocks for '${destinationFolder}' from '${fileToDownload}' because '${reason.message}'`
|
|
};
|
|
};
|
|
const retrieveTypesArchiveDestinationPath = (hostOptions, destinationFolder) => {
|
|
return resolve(hostOptions.context, hostOptions.typesFolder, destinationFolder);
|
|
};
|
|
const downloadTypesArchive = (hostOptions) => {
|
|
let retries = 0;
|
|
return async ([destinationFolder, fileToDownload]) => {
|
|
const destinationPath = retrieveTypesArchiveDestinationPath(hostOptions, destinationFolder);
|
|
while (retries++ < hostOptions.maxRetries) try {
|
|
const url = new URL(fileToDownload).href;
|
|
const response = await nativeFetch(url, {
|
|
responseType: "arraybuffer",
|
|
timeout: hostOptions.timeout,
|
|
family: hostOptions.family
|
|
}).catch(downloadErrorLogger(destinationFolder, url));
|
|
if (typeof response.headers?.["content-type"] === "string" && response.headers["content-type"].includes("text/html")) throw new Error(`${url} receives invalid content-type: ${response.headers["content-type"]}`);
|
|
try {
|
|
if (hostOptions.deleteTypesFolder) await rm(destinationPath, {
|
|
recursive: true,
|
|
force: true
|
|
});
|
|
} catch (error) {
|
|
fileLog(`Unable to remove types folder, ${error}`, "downloadTypesArchive", "error");
|
|
}
|
|
new AdmZip(Buffer.from(response.data)).extractAllTo(destinationPath, true);
|
|
fileLog(`zip.extractAllTo success destinationPath: ${destinationPath}; url: ${url}`, "downloadTypesArchive", "info");
|
|
return [destinationFolder, destinationPath];
|
|
} catch (error) {
|
|
fileLog(`Error during types archive download: ${error?.message || "unknown error"}`, "downloadTypesArchive", "error");
|
|
if (retries >= hostOptions.maxRetries) {
|
|
logger$1.error(`Failed to download types archive from "${fileToDownload}". Set FEDERATION_DEBUG=true for details.`);
|
|
if (hostOptions.abortOnError !== false) throw error;
|
|
return;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/core/configurations/hostPlugin.ts
|
|
const defaultOptions$1 = {
|
|
typesFolder: "@mf-types",
|
|
remoteTypesFolder: "@mf-types",
|
|
deleteTypesFolder: true,
|
|
maxRetries: 3,
|
|
implementation: "",
|
|
context: process.cwd(),
|
|
abortOnError: true,
|
|
consumeAPITypes: false,
|
|
runtimePkgs: [],
|
|
remoteTypeUrls: {},
|
|
timeout: 6e4,
|
|
typesOnBuild: false,
|
|
family: 4
|
|
};
|
|
const buildZipUrl = (hostOptions, url) => {
|
|
const remoteUrl = new URL(url, "file:");
|
|
remoteUrl.pathname = `${remoteUrl.pathname.split("/").slice(0, -1).join("/")}/${hostOptions.remoteTypesFolder}.zip`;
|
|
return remoteUrl.protocol === "file:" ? remoteUrl.pathname : remoteUrl.href;
|
|
};
|
|
const buildApiTypeUrl = (zipUrl) => {
|
|
if (!zipUrl) return;
|
|
return zipUrl.replace(".zip", ".d.ts");
|
|
};
|
|
const retrieveRemoteInfo = (options) => {
|
|
const { hostOptions, remoteAlias, remote } = options;
|
|
const { remoteTypeUrls } = hostOptions;
|
|
let decodedRemote = remote;
|
|
if (decodedRemote.startsWith(ENCODE_NAME_PREFIX)) decodedRemote = decodeName(decodedRemote, ENCODE_NAME_PREFIX);
|
|
const parsedInfo = parseEntry(decodedRemote, void 0, "@");
|
|
const url = "entry" in parsedInfo ? parsedInfo.entry : parsedInfo.name === decodedRemote ? decodedRemote : "";
|
|
let zipUrl = "";
|
|
let apiTypeUrl = "";
|
|
const name = parsedInfo.name || remoteAlias;
|
|
const remoteTypeUrl = typeof remoteTypeUrls === "object" && remoteTypeUrls[name];
|
|
if (remoteTypeUrl) {
|
|
zipUrl = remoteTypeUrl.zip;
|
|
apiTypeUrl = remoteTypeUrl.api;
|
|
}
|
|
const shouldResolveTypeUrlsByConvention = Boolean(url && !url.includes(MANIFEST_EXT));
|
|
if (!zipUrl && shouldResolveTypeUrlsByConvention) zipUrl = buildZipUrl(hostOptions, url);
|
|
if (!apiTypeUrl && zipUrl && (remoteTypeUrl || shouldResolveTypeUrlsByConvention)) apiTypeUrl = buildApiTypeUrl(zipUrl);
|
|
return {
|
|
name,
|
|
url,
|
|
zipUrl,
|
|
apiTypeUrl,
|
|
alias: remoteAlias
|
|
};
|
|
};
|
|
const resolveRemotes = (hostOptions) => {
|
|
const parsedOptions = utils.parseOptions(hostOptions.moduleFederationConfig.remotes || {}, (item, key) => ({
|
|
remote: Array.isArray(item) ? item[0] : item,
|
|
key
|
|
}), (item, key) => ({
|
|
remote: Array.isArray(item.external) ? item.external[0] : item.external,
|
|
key
|
|
}));
|
|
const remoteTypeUrls = hostOptions.remoteTypeUrls ?? {};
|
|
if (typeof remoteTypeUrls !== "object") throw new Error("remoteTypeUrls must be consumed before resolveRemotes");
|
|
const remoteInfos = Object.keys(remoteTypeUrls).reduce((sum, remoteName) => {
|
|
const { zip, api, alias } = remoteTypeUrls[remoteName];
|
|
sum[alias] = {
|
|
name: remoteName,
|
|
url: "",
|
|
zipUrl: zip,
|
|
apiTypeUrl: api,
|
|
alias: alias || remoteName
|
|
};
|
|
return sum;
|
|
}, {});
|
|
return parsedOptions.reduce((accumulator, item) => {
|
|
const { key, remote } = item[1];
|
|
const res = retrieveRemoteInfo({
|
|
hostOptions,
|
|
remoteAlias: key,
|
|
remote
|
|
});
|
|
if (accumulator[key]) {
|
|
accumulator[key] = {
|
|
...accumulator[key],
|
|
url: res.url,
|
|
apiTypeUrl: accumulator[key].apiTypeUrl || res.apiTypeUrl
|
|
};
|
|
return accumulator;
|
|
}
|
|
accumulator[key] = res;
|
|
return accumulator;
|
|
}, remoteInfos);
|
|
};
|
|
const retrieveHostConfig = (options) => {
|
|
validateOptions(options);
|
|
const hostOptions = {
|
|
...defaultOptions$1,
|
|
...options
|
|
};
|
|
return {
|
|
hostOptions,
|
|
mapRemotesToDownload: resolveRemotes(hostOptions)
|
|
};
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/core/constant.ts
|
|
const REMOTE_ALIAS_IDENTIFIER = "REMOTE_ALIAS_IDENTIFIER";
|
|
const REMOTE_API_TYPES_FILE_NAME = "apis.d.ts";
|
|
const HOST_API_TYPES_FILE_NAME = "index.d.ts";
|
|
|
|
//#endregion
|
|
//#region src/core/lib/DTSManager.ts
|
|
var DTSManager = class {
|
|
constructor(options) {
|
|
this.options = cloneDeepOptions(options);
|
|
this.runtimePkgs = [
|
|
"@module-federation/runtime",
|
|
"@module-federation/enhanced/runtime",
|
|
"@module-federation/runtime-tools"
|
|
];
|
|
this.loadedRemoteAPIAlias = /* @__PURE__ */ new Set();
|
|
this.remoteAliasMap = {};
|
|
this.extraOptions = options?.extraOptions || {};
|
|
this.updatedRemoteInfos = {};
|
|
}
|
|
generateAPITypes(mapComponentsToExpose) {
|
|
const exposePaths = /* @__PURE__ */ new Set();
|
|
const packageType = Object.keys(mapComponentsToExpose).reduce((sum, exposeKey) => {
|
|
const exposePath = path.join(REMOTE_ALIAS_IDENTIFIER, exposeKey).split(path.sep).join("/");
|
|
exposePaths.add(`'${exposePath}'`);
|
|
sum = `T extends '${exposePath}' ? typeof import('${exposePath}') :` + sum;
|
|
return sum;
|
|
}, "any;");
|
|
return `
|
|
export type RemoteKeys = ${[...exposePaths].join(" | ")};
|
|
type PackageType<T> = ${packageType}`;
|
|
}
|
|
async extractRemoteTypes(options) {
|
|
const { remoteOptions, tsConfig } = options;
|
|
if (!remoteOptions.extractRemoteTypes) return;
|
|
let hasRemotes = false;
|
|
const remotes = remoteOptions.moduleFederationConfig.remotes;
|
|
if (remotes) {
|
|
if (Array.isArray(remotes)) hasRemotes = Boolean(remotes.length);
|
|
else if (typeof remotes === "object") hasRemotes = Boolean(Object.keys(remotes).length);
|
|
}
|
|
const mfTypesPath = retrieveMfTypesPath(tsConfig, remoteOptions);
|
|
if (hasRemotes && this.options.host) try {
|
|
const { hostOptions } = retrieveHostConfig(this.options.host);
|
|
const remoteTypesFolder = path.resolve(hostOptions.context, hostOptions.typesFolder);
|
|
const targetDir = path.join(mfTypesPath, "node_modules");
|
|
if (fs.existsSync(remoteTypesFolder)) {
|
|
const targetFolder = path.resolve(remoteOptions.context, targetDir);
|
|
await mkdir(targetFolder, { recursive: true });
|
|
await cp(remoteTypesFolder, targetFolder, {
|
|
recursive: true,
|
|
force: true
|
|
});
|
|
}
|
|
} catch (err) {
|
|
if (this.options.host?.abortOnError === false) fileLog(`Unable to copy remote types, ${err}`, "extractRemoteTypes", "error");
|
|
else throw err;
|
|
}
|
|
}
|
|
async generateTypes() {
|
|
try {
|
|
const { options } = this;
|
|
if (!options.remote) throw new Error("options.remote is required if you want to generateTypes");
|
|
const { remoteOptions, tsConfig, mapComponentsToExpose } = retrieveRemoteConfig(options.remote);
|
|
if (!Object.keys(mapComponentsToExpose).length) return;
|
|
if (!tsConfig.files?.length) {
|
|
logger$1.info("No type files to compile, skip");
|
|
return;
|
|
}
|
|
if (tsConfig.compilerOptions.tsBuildInfoFile) try {
|
|
const tsBuildInfoFile = path.resolve(remoteOptions.context, tsConfig.compilerOptions.tsBuildInfoFile);
|
|
const mfTypesPath = retrieveMfTypesPath(tsConfig, remoteOptions);
|
|
if (!fs.existsSync(mfTypesPath)) fs.rmSync(tsBuildInfoFile, { force: true });
|
|
} catch (e) {}
|
|
await this.extractRemoteTypes({
|
|
remoteOptions,
|
|
tsConfig,
|
|
mapComponentsToExpose
|
|
});
|
|
await compileTs(mapComponentsToExpose, tsConfig, remoteOptions);
|
|
await createTypesArchive(tsConfig, remoteOptions);
|
|
let apiTypesPath = "";
|
|
if (remoteOptions.generateAPITypes) {
|
|
const apiTypes = this.generateAPITypes(mapComponentsToExpose);
|
|
apiTypesPath = retrieveMfAPITypesPath(tsConfig, remoteOptions);
|
|
fs.writeFileSync(apiTypesPath, apiTypes);
|
|
}
|
|
try {
|
|
if (remoteOptions.deleteTypesFolder) await rm(retrieveMfTypesPath(tsConfig, remoteOptions), {
|
|
recursive: true,
|
|
force: true
|
|
});
|
|
} catch (err) {
|
|
if (isDebugMode()) console.error(err);
|
|
}
|
|
logger$1.success("Federated types created correctly");
|
|
} catch (error) {
|
|
if (this.options.remote?.abortOnError === false) {
|
|
if (this.options.displayErrorInTerminal) logger$1.error(error);
|
|
} else throw error;
|
|
}
|
|
}
|
|
async requestRemoteManifest(remoteInfo, hostOptions) {
|
|
try {
|
|
if (!remoteInfo.url.includes(MANIFEST_EXT)) return remoteInfo;
|
|
if (remoteInfo.zipUrl) return remoteInfo;
|
|
const url = remoteInfo.url;
|
|
const manifestJson = (await nativeFetch(url, {
|
|
timeout: hostOptions.timeout,
|
|
family: hostOptions.family
|
|
})).data;
|
|
if (!manifestJson.metaData.types.zip) throw new Error(`Can not get ${remoteInfo.name}'s types archive url!`);
|
|
const addProtocol = (u) => {
|
|
if (u.startsWith("//")) return `https:${u}`;
|
|
return u;
|
|
};
|
|
let publicPath;
|
|
if ("publicPath" in manifestJson.metaData) publicPath = manifestJson.metaData.publicPath;
|
|
else {
|
|
const getPublicPath = new Function(manifestJson.metaData.getPublicPath);
|
|
if (manifestJson.metaData.getPublicPath.startsWith("function")) publicPath = getPublicPath()();
|
|
else publicPath = getPublicPath();
|
|
}
|
|
if (publicPath === "auto") publicPath = inferAutoPublicPath(remoteInfo.url);
|
|
remoteInfo.zipUrl = new URL(path.join(addProtocol(publicPath), manifestJson.metaData.types.zip)).href;
|
|
if (!manifestJson.metaData.types.api) {
|
|
console.warn(`Can not get ${remoteInfo.name}'s api types url!`);
|
|
remoteInfo.apiTypeUrl = "";
|
|
return remoteInfo;
|
|
}
|
|
remoteInfo.apiTypeUrl = new URL(path.join(addProtocol(publicPath), manifestJson.metaData.types.api)).href;
|
|
return remoteInfo;
|
|
} catch (_err) {
|
|
fileLog(`fetch manifest failed, ${_err}, ${remoteInfo.name} will be ignored`, "requestRemoteManifest", "error");
|
|
return remoteInfo;
|
|
}
|
|
}
|
|
async consumeTargetRemotes(hostOptions, remoteInfo) {
|
|
if (!remoteInfo.zipUrl) throw new Error(`Can not get ${remoteInfo.name}'s types archive url!`);
|
|
return downloadTypesArchive(hostOptions)([remoteInfo.alias, remoteInfo.zipUrl]);
|
|
}
|
|
async downloadAPITypes(remoteInfo, destinationPath, hostOptions) {
|
|
const { apiTypeUrl } = remoteInfo;
|
|
if (!apiTypeUrl) return;
|
|
try {
|
|
let apiTypeFile = (await nativeFetch(apiTypeUrl, {
|
|
timeout: hostOptions.timeout,
|
|
family: hostOptions.family
|
|
})).data;
|
|
apiTypeFile = apiTypeFile.replaceAll(REMOTE_ALIAS_IDENTIFIER, remoteInfo.alias);
|
|
const filePath = path.join(destinationPath, REMOTE_API_TYPES_FILE_NAME);
|
|
fs.writeFileSync(filePath, apiTypeFile);
|
|
const existed = this.loadedRemoteAPIAlias.has(remoteInfo.alias);
|
|
this.loadedRemoteAPIAlias.add(remoteInfo.alias);
|
|
fileLog(`success`, "downloadAPITypes", "info");
|
|
return existed;
|
|
} catch (err) {
|
|
fileLog(`Unable to download "${remoteInfo.name}" api types, ${err}`, "downloadAPITypes", "error");
|
|
}
|
|
}
|
|
consumeAPITypes(hostOptions) {
|
|
const apiTypeFileName = path.join(hostOptions.context, hostOptions.typesFolder, HOST_API_TYPES_FILE_NAME);
|
|
try {
|
|
const existedFile = fs.readFileSync(apiTypeFileName, "utf-8");
|
|
new ThirdPartyExtractor({ destDir: "" }).collectTypeImports(existedFile).forEach((existedImport) => {
|
|
const alias = existedImport.split("./").slice(1).join("./").replace("/apis.d.ts", "");
|
|
this.loadedRemoteAPIAlias.add(alias);
|
|
});
|
|
} catch (err) {}
|
|
if (!this.loadedRemoteAPIAlias.size) return;
|
|
const packageTypes = [];
|
|
const remoteKeys = [];
|
|
const importTypeStr = [...this.loadedRemoteAPIAlias].sort().map((alias, index) => {
|
|
const remoteKey = `RemoteKeys_${index}`;
|
|
const packageType = `PackageType_${index}`;
|
|
packageTypes.push(`T extends ${remoteKey} ? ${packageType}<T>`);
|
|
remoteKeys.push(remoteKey);
|
|
return `import type { PackageType as ${packageType},RemoteKeys as ${remoteKey} } from './${alias}/apis.d.ts';`;
|
|
}).join("\n");
|
|
const remoteKeysStr = `type RemoteKeys = ${remoteKeys.join(" | ")};`;
|
|
const packageTypesStr = `type PackageType<T, Y=any> = ${[...packageTypes, "Y"].join(" :\n")} ;`;
|
|
const runtimePkgs = /* @__PURE__ */ new Set();
|
|
[...this.runtimePkgs, ...hostOptions.runtimePkgs].forEach((pkg) => {
|
|
runtimePkgs.add(pkg);
|
|
});
|
|
const fileStr = `${importTypeStr}
|
|
${[...runtimePkgs].map((pkg) => {
|
|
return `declare module "${pkg}" {
|
|
${remoteKeysStr}
|
|
${packageTypesStr}
|
|
export function loadRemote<T extends RemoteKeys,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
export function loadRemote<T extends string,Y>(packageName: T): Promise<PackageType<T, Y>>;
|
|
}`;
|
|
}).join("\n")}
|
|
`;
|
|
fs.writeFileSync(path.join(hostOptions.context, hostOptions.typesFolder, HOST_API_TYPES_FILE_NAME), fileStr);
|
|
}
|
|
async consumeArchiveTypes(options) {
|
|
const { hostOptions, mapRemotesToDownload } = retrieveHostConfig(options);
|
|
const downloadPromises = Object.entries(mapRemotesToDownload).map(async (item) => {
|
|
const remoteInfo = item[1];
|
|
if (!this.remoteAliasMap[remoteInfo.alias]) {
|
|
const requiredRemoteInfo = await this.requestRemoteManifest(remoteInfo, hostOptions);
|
|
this.remoteAliasMap[remoteInfo.alias] = requiredRemoteInfo;
|
|
}
|
|
return this.consumeTargetRemotes(hostOptions, this.remoteAliasMap[remoteInfo.alias]);
|
|
});
|
|
return {
|
|
hostOptions,
|
|
downloadPromisesResult: await Promise.allSettled(downloadPromises)
|
|
};
|
|
}
|
|
async consumeTypes() {
|
|
try {
|
|
const { options } = this;
|
|
if (!options.host) throw new Error("options.host is required if you want to consumeTypes");
|
|
const { mapRemotesToDownload } = retrieveHostConfig(options.host);
|
|
if (!Object.keys(mapRemotesToDownload).length) return;
|
|
const { downloadPromisesResult, hostOptions } = await this.consumeArchiveTypes(options.host);
|
|
if (hostOptions.consumeAPITypes) {
|
|
await Promise.all(downloadPromisesResult.map(async (item) => {
|
|
if (item.status === "rejected" || !item.value) return;
|
|
const [alias, destinationPath] = item.value;
|
|
const remoteInfo = this.remoteAliasMap[alias];
|
|
if (!remoteInfo) return;
|
|
await this.downloadAPITypes(remoteInfo, destinationPath, hostOptions);
|
|
}));
|
|
this.consumeAPITypes(hostOptions);
|
|
}
|
|
logger$1.success("Federated types extraction completed");
|
|
} catch (err) {
|
|
if (this.options.host?.abortOnError === false) fileLog(`Unable to consume federated types, ${err}`, "consumeTypes", "error");
|
|
else throw err;
|
|
}
|
|
}
|
|
async updateTypes(options) {
|
|
try {
|
|
const { remoteName, updateMode, remoteTarPath, remoteInfo: updatedRemoteInfo, once } = options;
|
|
const hostName = this.options?.host?.moduleFederationConfig?.name;
|
|
fileLog(`options: ${JSON.stringify(options, null, 2)};\nhostName: ${hostName}`, "updateTypes", "info");
|
|
if (updateMode === UpdateMode.POSITIVE && remoteName === hostName) {
|
|
if (!this.options.remote) return;
|
|
await this.generateTypes();
|
|
} else {
|
|
const { remoteAliasMap } = this;
|
|
if (!this.options.host) return;
|
|
const { hostOptions, mapRemotesToDownload } = retrieveHostConfig(this.options.host);
|
|
const loadedRemoteInfo = Object.values(remoteAliasMap).find((i) => i.name === remoteName);
|
|
const consumeTypes = async (requiredRemoteInfo) => {
|
|
fileLog(`consumeTypes start`, "updateTypes", "info");
|
|
if (!requiredRemoteInfo.zipUrl) throw new Error(`Can not get ${requiredRemoteInfo.name}'s types archive url!`);
|
|
const [_alias, destinationPath] = await this.consumeTargetRemotes(hostOptions, {
|
|
...requiredRemoteInfo,
|
|
zipUrl: remoteTarPath || requiredRemoteInfo.zipUrl
|
|
});
|
|
if (await this.downloadAPITypes(requiredRemoteInfo, destinationPath, hostOptions)) this.consumeAPITypes(hostOptions);
|
|
fileLog(`consumeTypes end`, "updateTypes", "info");
|
|
};
|
|
fileLog(`loadedRemoteInfo: ${JSON.stringify(loadedRemoteInfo, null, 2)}`, "updateTypes", "info");
|
|
if (!loadedRemoteInfo) {
|
|
const remoteInfo = Object.values(mapRemotesToDownload).find((item) => {
|
|
return item.name === remoteName;
|
|
});
|
|
fileLog(`remoteInfo: ${JSON.stringify(remoteInfo, null, 2)}`, "updateTypes", "info");
|
|
if (remoteInfo) {
|
|
if (!this.remoteAliasMap[remoteInfo.alias]) {
|
|
const requiredRemoteInfo = await this.requestRemoteManifest(remoteInfo, hostOptions);
|
|
this.remoteAliasMap[remoteInfo.alias] = requiredRemoteInfo;
|
|
}
|
|
await consumeTypes(this.remoteAliasMap[remoteInfo.alias]);
|
|
} else if (updatedRemoteInfo) {
|
|
const consumeDynamicRemoteTypes = async () => {
|
|
await consumeTypes(this.updatedRemoteInfos[updatedRemoteInfo.name]);
|
|
};
|
|
if (!this.updatedRemoteInfos[updatedRemoteInfo.name]) {
|
|
const parsedRemoteInfo = retrieveRemoteInfo({
|
|
hostOptions,
|
|
remoteAlias: updatedRemoteInfo.alias || updatedRemoteInfo.name,
|
|
remote: updatedRemoteInfo.url
|
|
});
|
|
fileLog(`start request manifest`, "consumeTypes", "info");
|
|
this.updatedRemoteInfos[updatedRemoteInfo.name] = await this.requestRemoteManifest(parsedRemoteInfo, hostOptions);
|
|
fileLog(`end request manifest, this.updatedRemoteInfos[updatedRemoteInfo.name]: ${JSON.stringify(this.updatedRemoteInfos[updatedRemoteInfo.name], null, 2)}`, "updateTypes", "info");
|
|
await consumeDynamicRemoteTypes();
|
|
}
|
|
if (!once && this.updatedRemoteInfos[updatedRemoteInfo.name]) await consumeDynamicRemoteTypes();
|
|
}
|
|
} else await consumeTypes(loadedRemoteInfo);
|
|
}
|
|
} catch (err) {
|
|
fileLog(`updateTypes fail, ${err}`, "updateTypes", "error");
|
|
}
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/core/lib/utils.ts
|
|
const dispatcherCache = /* @__PURE__ */ new Map();
|
|
function getDTSManagerConstructor(implementation) {
|
|
if (implementation) {
|
|
const NewConstructor = __require(implementation);
|
|
return NewConstructor.default ? NewConstructor.default : NewConstructor;
|
|
}
|
|
return DTSManager;
|
|
}
|
|
const validateOptions = (options) => {
|
|
if (!options.moduleFederationConfig) throw new Error("moduleFederationConfig is required");
|
|
};
|
|
function retrieveTypesAssetsInfo(options) {
|
|
let apiTypesPath = "";
|
|
let zipTypesPath = "";
|
|
try {
|
|
const { tsConfig, remoteOptions, mapComponentsToExpose } = retrieveRemoteConfig(options);
|
|
if (!Object.keys(mapComponentsToExpose).length || !tsConfig.files.length) return {
|
|
apiTypesPath,
|
|
zipTypesPath,
|
|
zipName: "",
|
|
apiFileName: ""
|
|
};
|
|
zipTypesPath = retrieveTypesZipPath(retrieveMfTypesPath(tsConfig, remoteOptions), remoteOptions);
|
|
if (remoteOptions.generateAPITypes) apiTypesPath = retrieveMfAPITypesPath(tsConfig, remoteOptions);
|
|
return {
|
|
apiTypesPath,
|
|
zipTypesPath,
|
|
zipName: path.basename(zipTypesPath),
|
|
apiFileName: path.basename(apiTypesPath)
|
|
};
|
|
} catch (err) {
|
|
console.error(ansiColors.red(`Unable to compile federated types, ${err}`));
|
|
return {
|
|
apiTypesPath: "",
|
|
zipTypesPath: "",
|
|
zipName: "",
|
|
apiFileName: ""
|
|
};
|
|
}
|
|
}
|
|
function isDebugMode() {
|
|
return Boolean(process.env["FEDERATION_DEBUG"]) || process.env["NODE_ENV"] === "test";
|
|
}
|
|
const isTSProject = (dtsOptions, context = process.cwd()) => {
|
|
if (dtsOptions === false) return false;
|
|
try {
|
|
let filepath = "";
|
|
if (typeof dtsOptions === "object" && dtsOptions.tsConfigPath) filepath = dtsOptions.tsConfigPath;
|
|
else filepath = path.resolve(context, "./tsconfig.json");
|
|
if (!path.isAbsolute(filepath)) filepath = path.resolve(context, filepath);
|
|
return fs.existsSync(filepath);
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
};
|
|
function cloneDeepOptions(options) {
|
|
const excludeKeys = new Set(["manifest", "async"]);
|
|
const cache = /* @__PURE__ */ new WeakMap();
|
|
function sanitize(val, key) {
|
|
if (key !== void 0 && excludeKeys.has(key) || typeof val === "function") return false;
|
|
if (key === "extractThirdParty" && Array.isArray(val)) return val.map(String);
|
|
if (Array.isArray(val)) {
|
|
if (cache.has(val)) return cache.get(val);
|
|
const out = [];
|
|
cache.set(val, out);
|
|
val.forEach((v, i) => out.push(sanitize(v, String(i))));
|
|
return out;
|
|
}
|
|
if (val !== null && typeof val === "object" && Object.getPrototypeOf(val) === Object.prototype) {
|
|
const obj = val;
|
|
if (cache.has(obj)) return cache.get(obj);
|
|
const out = {};
|
|
cache.set(obj, out);
|
|
for (const [k, v] of Object.entries(obj)) out[k] = sanitize(v, k);
|
|
return out;
|
|
}
|
|
return val;
|
|
}
|
|
return structuredClone(sanitize(options));
|
|
}
|
|
const getEnvHeaders = () => {
|
|
const headersStr = getProcessEnv()["MF_ENV_HEADERS"];
|
|
if (!headersStr || headersStr === "undefined") return {};
|
|
try {
|
|
return { ...JSON.parse(headersStr) };
|
|
} catch {
|
|
return {};
|
|
}
|
|
};
|
|
const createDispatcherFromFamily = (family) => {
|
|
if (!family) return void 0;
|
|
if (dispatcherCache.has(family)) return dispatcherCache.get(family);
|
|
try {
|
|
const dispatcher = new Agent({ connect: { family } });
|
|
dispatcherCache.set(family, dispatcher);
|
|
return dispatcher;
|
|
} catch {}
|
|
};
|
|
const toHeaderRecord = (headers) => {
|
|
const out = {};
|
|
headers.forEach((value, key) => {
|
|
out[key.toLowerCase()] = value;
|
|
});
|
|
return out;
|
|
};
|
|
async function nativeFetch(url, config) {
|
|
const controller = new AbortController();
|
|
const timeoutMs = config?.timeout ?? 6e4;
|
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
const headers = {
|
|
...getEnvHeaders(),
|
|
...config?.headers ?? {}
|
|
};
|
|
const dispatcher = config?.dispatcher ?? createDispatcherFromFamily(config?.family);
|
|
try {
|
|
const resp = await fetch(url, {
|
|
headers,
|
|
signal: controller.signal,
|
|
...dispatcher ? { dispatcher } : {},
|
|
...config?.agent ? { agent: config.agent } : {}
|
|
});
|
|
const headerRecord = toHeaderRecord(resp.headers);
|
|
if (!resp.ok) throw new Error(`Request failed with status ${resp.status}`);
|
|
if (config?.responseType === "arraybuffer") return {
|
|
data: await resp.arrayBuffer(),
|
|
headers: headerRecord,
|
|
status: resp.status
|
|
};
|
|
return {
|
|
data: (resp.headers.get("content-type") || "").includes("application/json") || url.endsWith(".json") ? await resp.json() : await resp.text(),
|
|
headers: headerRecord,
|
|
status: resp.status
|
|
};
|
|
} finally {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/core/configurations/remotePlugin.ts
|
|
const defaultOptions = {
|
|
tsConfigPath: "./tsconfig.json",
|
|
typesFolder: "@mf-types",
|
|
compiledTypesFolder: "compiled-types",
|
|
hostRemoteTypesFolder: "@mf-types",
|
|
deleteTypesFolder: true,
|
|
additionalFilesToCompile: [],
|
|
compilerInstance: "tsc",
|
|
compileInChildProcess: false,
|
|
implementation: "",
|
|
generateAPITypes: false,
|
|
context: process.cwd(),
|
|
abortOnError: true,
|
|
extractRemoteTypes: false,
|
|
extractThirdParty: false,
|
|
outputDir: "",
|
|
deleteTsConfig: true
|
|
};
|
|
function getEffectiveRootDir(parsedCommandLine) {
|
|
const compilerOptions = parsedCommandLine.options;
|
|
if (compilerOptions.rootDir) return compilerOptions.rootDir;
|
|
const files = parsedCommandLine.fileNames;
|
|
if (files.length > 0) return files.map((file) => dirname(file)).reduce((commonPath, fileDir) => {
|
|
while (!fileDir.startsWith(commonPath)) commonPath = dirname(commonPath);
|
|
return commonPath;
|
|
}, files[0]);
|
|
if (parsedCommandLine.projectReferences.length) {
|
|
const relativeReferences = parsedCommandLine.projectReferences.filter((reference) => !isAbsolute(reference.originalPath ?? reference.path));
|
|
const referencesForRoot = relativeReferences.length ? relativeReferences : parsedCommandLine.projectReferences;
|
|
return referencesForRoot.map((reference) => dirname(reference.path)).reduce((commonPath, filePath) => {
|
|
while (!filePath.startsWith(commonPath)) commonPath = dirname(commonPath);
|
|
return commonPath;
|
|
}, dirname(referencesForRoot[0].path));
|
|
}
|
|
throw new Error("Can not get effective rootDir, please set compilerOptions.rootDir !");
|
|
}
|
|
const getDependentFiles = (rootFiles, configContent, rootDir) => {
|
|
const dependentFiles = typescript.createProgram(rootFiles, configContent.options).getSourceFiles().map((file) => file.fileName).filter((file) => !file.endsWith(".d.ts") && file.startsWith(rootDir));
|
|
return dependentFiles.length ? dependentFiles : rootFiles;
|
|
};
|
|
const readTsConfig = ({ tsConfigPath, typesFolder, compiledTypesFolder, context, additionalFilesToCompile, outputDir }, mapComponentsToExpose) => {
|
|
const resolvedTsConfigPath = resolve(context, tsConfigPath);
|
|
const readResult = typescript.readConfigFile(resolvedTsConfigPath, typescript.sys.readFile);
|
|
if (readResult.error) throw new Error(readResult.error.messageText.toString());
|
|
const rawTsConfigJson = readResult.config;
|
|
const configContent = typescript.parseJsonConfigFileContent(rawTsConfigJson, typescript.sys, dirname(resolvedTsConfigPath));
|
|
const rootDir = getEffectiveRootDir(configContent);
|
|
const outDir = resolve(context, outputDir || configContent.options.outDir || "dist", typesFolder, compiledTypesFolder);
|
|
const defaultCompilerOptions = {
|
|
rootDir,
|
|
emitDeclarationOnly: true,
|
|
noEmit: false,
|
|
declaration: true,
|
|
outDir
|
|
};
|
|
rawTsConfigJson.compilerOptions = rawTsConfigJson.compilerOptions || {};
|
|
rawTsConfigJson.compilerOptions = {
|
|
incremental: true,
|
|
tsBuildInfoFile: resolve(context, "node_modules/.cache/mf-types/.tsbuildinfo"),
|
|
...rawTsConfigJson.compilerOptions,
|
|
...defaultCompilerOptions
|
|
};
|
|
const { paths, baseUrl, ...restCompilerOptions } = rawTsConfigJson.compilerOptions || {};
|
|
rawTsConfigJson.compilerOptions = restCompilerOptions;
|
|
const outDirWithoutTypesFolder = resolve(context, outputDir || configContent.options.outDir || "dist");
|
|
const excludeExtensions = [".mdx", ".md"];
|
|
const filesToCompile = [...getDependentFiles([...Object.values(mapComponentsToExpose), ...additionalFilesToCompile].filter((filename) => !excludeExtensions.some((ext) => filename.endsWith(ext))), configContent, rootDir), ...configContent.fileNames.filter((filename) => filename.endsWith(".d.ts") && !filename.startsWith(outDirWithoutTypesFolder))];
|
|
rawTsConfigJson.include = [];
|
|
rawTsConfigJson.files = [...new Set(filesToCompile)];
|
|
rawTsConfigJson.exclude = [];
|
|
"references" in rawTsConfigJson && delete rawTsConfigJson.references;
|
|
rawTsConfigJson.extends = resolvedTsConfigPath;
|
|
rawTsConfigJson.compilerOptions.declarationDir = outDir;
|
|
return rawTsConfigJson;
|
|
};
|
|
const TS_EXTENSIONS = [
|
|
"ts",
|
|
"tsx",
|
|
"vue",
|
|
"svelte"
|
|
];
|
|
const resolveWithExtension = (exposedPath, context) => {
|
|
if (extname(exposedPath)) return resolve(context, exposedPath);
|
|
for (const extension of TS_EXTENSIONS) {
|
|
const exposedPathWithExtension = resolve(context, `${exposedPath}.${extension}`);
|
|
if (existsSync(exposedPathWithExtension)) return exposedPathWithExtension;
|
|
}
|
|
};
|
|
const resolveExposes = (remoteOptions) => {
|
|
return utils.parseOptions(remoteOptions.moduleFederationConfig.exposes || {}, (item, key) => ({
|
|
exposePath: Array.isArray(item) ? item[0] : item,
|
|
key
|
|
}), (item, key) => ({
|
|
exposePath: Array.isArray(item.import) ? item.import[0] : item.import[0],
|
|
key
|
|
})).reduce((accumulator, item) => {
|
|
const { exposePath, key } = item[1];
|
|
accumulator[key] = resolveWithExtension(exposePath, remoteOptions.context) || resolveWithExtension(join(exposePath, "index"), remoteOptions.context) || exposePath;
|
|
return accumulator;
|
|
}, {});
|
|
};
|
|
const retrieveRemoteConfig = (options) => {
|
|
validateOptions(options);
|
|
const remoteOptions = {
|
|
...defaultOptions,
|
|
...options
|
|
};
|
|
const mapComponentsToExpose = resolveExposes(remoteOptions);
|
|
const tsConfig = readTsConfig(remoteOptions, mapComponentsToExpose);
|
|
if (tsConfig.compilerOptions.incremental && tsConfig.compilerOptions.tsBuildInfoFile && options.deleteTypesFolder !== true) remoteOptions.deleteTypesFolder = false;
|
|
return {
|
|
tsConfig,
|
|
mapComponentsToExpose,
|
|
remoteOptions
|
|
};
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/core/lib/generateTypes.ts
|
|
async function generateTypes(options) {
|
|
return new (getDTSManagerConstructor(options.remote?.implementation))(options).generateTypes();
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/core/rpc/types.ts
|
|
let RpcGMCallTypes = /* @__PURE__ */ function(RpcGMCallTypes) {
|
|
RpcGMCallTypes["CALL"] = "mf_call";
|
|
RpcGMCallTypes["RESOLVE"] = "mf_resolve";
|
|
RpcGMCallTypes["REJECT"] = "mf_reject";
|
|
RpcGMCallTypes["EXIT"] = "mf_exit";
|
|
return RpcGMCallTypes;
|
|
}({});
|
|
|
|
//#endregion
|
|
//#region src/core/rpc/expose-rpc.ts
|
|
function exposeRpc(fn) {
|
|
const sendMessage = (message) => new Promise((resolve, reject) => {
|
|
if (!process$1.send) reject(/* @__PURE__ */ new Error(`Process ${process$1.pid} doesn't have IPC channels`));
|
|
else if (!process$1.connected) reject(/* @__PURE__ */ new Error(`Process ${process$1.pid} doesn't have open IPC channels`));
|
|
else process$1.send(message, void 0, void 0, (error) => {
|
|
if (error) reject(error);
|
|
else resolve(void 0);
|
|
});
|
|
});
|
|
const handleMessage = async (message) => {
|
|
if (message.type === RpcGMCallTypes.CALL) {
|
|
if (!process$1.send) return;
|
|
let value, error;
|
|
try {
|
|
value = await fn(...message.args);
|
|
} catch (fnError) {
|
|
error = fnError;
|
|
}
|
|
try {
|
|
if (error) await sendMessage({
|
|
type: RpcGMCallTypes.REJECT,
|
|
id: message.id,
|
|
error
|
|
});
|
|
else await sendMessage({
|
|
type: RpcGMCallTypes.RESOLVE,
|
|
id: message.id,
|
|
value
|
|
});
|
|
} catch (sendError) {
|
|
if (error) {
|
|
if (error instanceof Error) console.error(error);
|
|
}
|
|
console.error(sendError);
|
|
}
|
|
}
|
|
};
|
|
process$1.on("message", handleMessage);
|
|
}
|
|
|
|
//#endregion
|
|
export { retrieveMfTypesPath as _, cloneDeepOptions as a, ModuleFederationDevServer as b, isTSProject as c, DTSManager as d, HOST_API_TYPES_FILE_NAME as f, retrieveTypesZipPath as g, retrieveHostConfig as h, retrieveRemoteConfig as i, retrieveTypesAssetsInfo as l, REMOTE_API_TYPES_FILE_NAME as m, RpcGMCallTypes as n, getDTSManagerConstructor as o, REMOTE_ALIAS_IDENTIFIER as p, generateTypes as r, isDebugMode as s, exposeRpc as t, validateOptions as u, retrieveOriginalOutDir as v, __exportAll as x, createHttpServer as y }; |