plan/react-rewrite #1
@@ -0,0 +1,73 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock the runtime package so tests don't need a real remote.
|
||||
vi.mock("@module-federation/enhanced/runtime", () => {
|
||||
return {
|
||||
init: vi.fn(),
|
||||
loadRemote: vi.fn(),
|
||||
registerRemotes: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { init, loadRemote, registerRemotes } from "@module-federation/enhanced/runtime";
|
||||
|
||||
describe("remote-loader", () => {
|
||||
beforeEach(async () => {
|
||||
const mod = await import("./remote-loader.js");
|
||||
mod.__resetRemoteLoaderForTests();
|
||||
vi.mocked(init).mockClear();
|
||||
vi.mocked(loadRemote).mockClear();
|
||||
vi.mocked(registerRemotes).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("registerRemote calls init once for the first remote, then registerRemotes for subsequent", async () => {
|
||||
const { registerRemote } = await import("./remote-loader.js");
|
||||
registerRemote({ name: "customer-ui", entry: "https://host.example/mf-manifest.json" });
|
||||
expect(init).toHaveBeenCalledTimes(1);
|
||||
expect(init).toHaveBeenCalledWith({
|
||||
name: "aeroflot_flights_host",
|
||||
remotes: [{ name: "customer-ui", entry: "https://host.example/mf-manifest.json" }],
|
||||
});
|
||||
|
||||
registerRemote({ name: "customer-analytics", entry: "https://host.example/analytics/mf-manifest.json" });
|
||||
expect(init).toHaveBeenCalledTimes(1); // still only once
|
||||
expect(registerRemotes).toHaveBeenCalledTimes(1);
|
||||
expect(registerRemotes).toHaveBeenCalledWith([
|
||||
{ name: "customer-analytics", entry: "https://host.example/analytics/mf-manifest.json" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("loadRemoteModule delegates to loadRemote with the concatenated key", async () => {
|
||||
const fakeModule = { default: "hello" };
|
||||
vi.mocked(loadRemote).mockResolvedValueOnce(fakeModule);
|
||||
|
||||
const { loadRemoteModule } = await import("./remote-loader.js");
|
||||
const result = await loadRemoteModule<{ default: string }>({
|
||||
name: "customer-ui",
|
||||
module: "./Header",
|
||||
});
|
||||
|
||||
expect(loadRemote).toHaveBeenCalledWith("customer-ui/Header");
|
||||
expect(result).toBe(fakeModule);
|
||||
});
|
||||
|
||||
it("loadRemoteModule rejects with a typed error if the module returns null/undefined", async () => {
|
||||
vi.mocked(loadRemote).mockResolvedValueOnce(null);
|
||||
const { loadRemoteModule } = await import("./remote-loader.js");
|
||||
await expect(
|
||||
loadRemoteModule({ name: "customer-ui", module: "./NonExistent" }),
|
||||
).rejects.toThrow(/customer-ui\/NonExistent/);
|
||||
});
|
||||
|
||||
it("loadRemoteModule propagates errors from loadRemote", async () => {
|
||||
vi.mocked(loadRemote).mockRejectedValueOnce(new Error("boom"));
|
||||
const { loadRemoteModule } = await import("./remote-loader.js");
|
||||
await expect(
|
||||
loadRemoteModule({ name: "customer-ui", module: "./Broken" }),
|
||||
).rejects.toThrow("boom");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
init,
|
||||
loadRemote,
|
||||
registerRemotes,
|
||||
} from "@module-federation/enhanced/runtime";
|
||||
|
||||
export interface RemoteModuleRef<T = unknown> {
|
||||
name: string;
|
||||
module: string;
|
||||
/** Phantom marker to preserve the generic parameter through the type system. */
|
||||
readonly __type?: T;
|
||||
}
|
||||
|
||||
export interface RemoteEntry {
|
||||
name: string;
|
||||
entry: string;
|
||||
}
|
||||
|
||||
let initialized = false;
|
||||
const HOST_NAME = "aeroflot_flights_host" as const;
|
||||
|
||||
/**
|
||||
* Registers a remote with the MF runtime. On the first call, this also calls
|
||||
* `init()` to bootstrap the MF runtime with the host identity. Subsequent
|
||||
* calls use `registerRemotes()` to add remotes without re-initializing.
|
||||
*/
|
||||
export function registerRemote(entry: RemoteEntry): void {
|
||||
if (!initialized) {
|
||||
init({
|
||||
name: HOST_NAME,
|
||||
remotes: [entry],
|
||||
});
|
||||
initialized = true;
|
||||
return;
|
||||
}
|
||||
registerRemotes([entry]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an exposed module from a previously registered remote. The `name` +
|
||||
* `module` fields are concatenated into MF's `remote/module` lookup key.
|
||||
*
|
||||
* Throws if the runtime returns a null/undefined module, which usually means
|
||||
* the remote manifest does not actually expose the requested path.
|
||||
*/
|
||||
export async function loadRemoteModule<T>(ref: RemoteModuleRef<T>): Promise<T> {
|
||||
const key = `${ref.name}/${ref.module.replace(/^\.\//, "")}`;
|
||||
const result = await loadRemote<T>(key);
|
||||
if (result === null || result === undefined) {
|
||||
throw new Error(`Remote module ${key} loaded as null/undefined`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Test-only: resets the one-shot init flag. */
|
||||
export function __resetRemoteLoaderForTests(): void {
|
||||
initialized = false;
|
||||
}
|
||||
Reference in New Issue
Block a user