Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions src/module_federation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
jest.mock("../webpack/module_federation--dynamic-federation", () => ({
__esModule: true,
default: jest.fn(async (remote) => globalThis[remote]),
}));

describe("webpack/module_federation", () => {
const FAKE = "__patternslib_mf__fake";
const BROKEN = "__patternslib_mf__broken";
let events;
const on_loaded = () => events.push("loaded");

beforeEach(() => {
jest.resetModules();
events = [];
document.addEventListener("patternslib__mf--loaded", on_loaded);
});

afterEach(() => {
document.removeEventListener("patternslib__mf--loaded", on_loaded);
delete window[FAKE];
delete window[BROKEN];
delete window.__patternslib_mf_initialized;
delete window.__patternslib_container_map;
jest.restoreAllMocks();
});

it("resolves the initialization promise only after all remotes are initialized", async () => {
let release;
const factory = jest.fn(() => ({}));
window[FAKE] = {
get: jest.fn(
() => new Promise((resolve) => (release = () => resolve(factory))),
),
};

require("../webpack/module_federation");

const promise = window.__patternslib_mf_initialized;
expect(promise).toBeInstanceOf(Promise);

let settled = false;
promise.then(() => (settled = true));

// document ready has fired, the remote is being loaded but not done yet.
await new Promise((resolve) => setTimeout(resolve, 10));
expect(window[FAKE].get).toHaveBeenCalledWith("./main");
expect(settled).toBe(false);
expect(events).toEqual([]);

release();
const bundles = await promise;

expect(bundles).toEqual([FAKE]);
expect(factory).toHaveBeenCalled();
expect(window.__patternslib_container_map[`${FAKE}-./main`]).toBe(true);
// The event is dispatched after the promise resolved.
expect(events).toEqual(["loaded"]);
});

it("resolves even when a remote fails to initialize", async () => {
const error_spy = jest.spyOn(console, "error").mockImplementation(() => {});
window[BROKEN] = {
get: jest.fn(async () => {
throw new Error("boom");
}),
};
window[FAKE] = { get: jest.fn(async () => () => ({})) };

require("../webpack/module_federation");
const bundles = await window.__patternslib_mf_initialized;

expect(bundles.sort()).toEqual([BROKEN, FAKE].sort());
expect(error_spy).toHaveBeenCalledWith(
expect.stringContaining(BROKEN),
expect.any(Error),
);
expect(window.__patternslib_container_map[`${FAKE}-./main`]).toBe(true);
expect(window.__patternslib_container_map[`${BROKEN}-./main`]).toBeUndefined();
expect(events).toEqual(["loaded"]);
});

it("waits for the promise a remote's main module exports as default", async () => {
let release;
const bundle_code = jest.fn();
// The main module only starts the dynamic import of the bundle code and
// exports that promise.
const main_module = {
default: new Promise((resolve) => (release = () => resolve(bundle_code()))),
};
window[FAKE] = { get: jest.fn(async () => () => main_module) };

require("../webpack/module_federation");

let settled = false;
window.__patternslib_mf_initialized.then(() => (settled = true));
await new Promise((resolve) => setTimeout(resolve, 10));

// main has run, but the bundle code has not — still waiting.
expect(window[FAKE].get).toHaveBeenCalled();
expect(settled).toBe(false);
expect(events).toEqual([]);

release();
await window.__patternslib_mf_initialized;

expect(bundle_code).toHaveBeenCalled();
expect(events).toEqual(["loaded"]);
});

it("resolves immediately when no remotes are registered", async () => {
require("../webpack/module_federation");
const bundles = await window.__patternslib_mf_initialized;
expect(bundles).toEqual([]);
expect(events).toEqual(["loaded"]);
});
});
49 changes: 43 additions & 6 deletions webpack/module_federation.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,23 @@ export async function initialize_remote({ remote_name, exposed_module = "./main"
const factory = await container.get(exposed_module);
const module = factory();

// A remote's main module usually only does a dynamic import of the
// actual bundle code (``import("./bundle")``) — the async boundary
// webpack needs to consume shared modules. The patterns and components
// are only registered once that chunk has run. If the main module
// exports that import promise as its default export
// (``export default import("./bundle")``), wait for it, so that
// ``__patternslib_mf_initialized`` really resolves after the remote's
// registrations are done. Remotes without such an export keep working
// as before.
if (typeof module?.default?.then === "function") {
await module.default;
}

container_map[`${remote_name}-${exposed_module}`] = true;

console.debug(
`Patternslib Module Federation: Loaded and initialized bundle "${remote_name}".`
`Patternslib Module Federation: Loaded and initialized bundle "${remote_name}".`,
);

return module;
Expand All @@ -41,17 +54,41 @@ function document_ready(fn) {
}
}

document_ready(function () {
// A promise which resolves once all Module Federation enabled bundles have
// been loaded and initialized (or failed to do so). It is created right at
// module load time, so consumers like the Patternslib registry can wait for
// it no matter whether they run before or after document ready.
// The Patternslib registry uses this to defer the initial DOM scan until all
// remote bundles had the chance to register their patterns and components.
let resolve_initialized;
window.__patternslib_mf_initialized = new Promise((resolve) => {
resolve_initialized = resolve;
});

document_ready(async function () {
// Automatically initialize all Module Federation enabled Patternslib based
// bundles by filtering for the prefix ``__patternslib_mf__``.
// Do this on document ready, as this is the time where all MF bundles have
// been registered in the global namespace.
const bundles = Object.keys(window).filter((it) => it.indexOf(MF_NAME_PREFIX) === 0);
for (const bundle_name of bundles) {
// Now load + initialize each bundle.
initialize_remote({ remote_name: bundle_name });

// Load + initialize all bundles in parallel and wait for all of them to
// settle. A failing bundle must not block the others, nor the
// initialization of the page.
const results = await Promise.allSettled(
bundles.map((bundle_name) => initialize_remote({ remote_name: bundle_name })),
);
for (const [index, result] of results.entries()) {
if (result.status === "rejected") {
console.error(
`Patternslib Module Federation: Failed to initialize bundle "${bundles[index]}".`,
result.reason,
);
}
}

resolve_initialized(bundles);
document.dispatchEvent(
new Event("patternslib__mf--loaded", { bubbles: true, cancelable: false })
new Event("patternslib__mf--loaded", { bubbles: true, cancelable: false }),
);
});
Loading