every-plugin
Version:
196 lines (171 loc) • 6.94 kB
text/typescript
import { createInstance, getInstance } from "@module-federation/enhanced/runtime";
import { setGlobalFederationInstance } from "@module-federation/runtime-core";
import { Effect } from "effect";
import type { AnyPlugin } from "../../types";
import { ModuleFederationError } from "../errors";
import { type CoreSharedDepName, MF_CORE_SHARED_DEPS } from "../mf-config";
import { getNormalizedRemoteName } from "./normalize";
type RemoteModule = (new () => AnyPlugin) | { default: new () => AnyPlugin };
const coreModuleLoaders: Record<CoreSharedDepName, () => Promise<unknown>> = {
"every-plugin": () => import("every-plugin"),
effect: () => import("effect"),
zod: () => import("zod"),
"@orpc/contract": () => import("@orpc/contract"),
"@orpc/server": () => import("@orpc/server"),
};
function buildSharedConfig(): Record<
string,
{
version: string;
get: () => Promise<() => unknown>;
shareConfig: (typeof MF_CORE_SHARED_DEPS)[CoreSharedDepName]["shareConfig"];
}
> {
return Object.fromEntries(
(
Object.entries(MF_CORE_SHARED_DEPS) as [
CoreSharedDepName,
(typeof MF_CORE_SHARED_DEPS)[CoreSharedDepName],
][]
).map(([name, config]) => {
const load = coreModuleLoaders[name];
if (!load) {
throw new Error(`Missing core shared module loader for ${name}`);
}
return [
name,
{
version: config.version,
get: () => load().then((mod) => () => mod),
shareConfig: config.shareConfig,
},
];
}),
);
}
const createModuleFederationInstance = Effect.cached(
Effect.sync(() => {
try {
const shared = buildSharedConfig();
let instance = getInstance();
if (!instance) {
instance = createInstance({
name: "host",
remotes: [],
shared,
});
setGlobalFederationInstance(instance);
} else {
instance.registerShared(shared);
}
return instance;
} catch (error) {
throw new Error(`Failed to initialize Module Federation: ${error}`);
}
}),
);
export class ModuleFederationService extends Effect.Service<ModuleFederationService>()(
"ModuleFederationService",
{
effect: Effect.gen(function* () {
const mf = yield* Effect.flatten(createModuleFederationInstance);
return {
registerRemote: (pluginId: string, url: string) =>
Effect.gen(function* () {
console.log(`[MF] Registering ${pluginId}`);
const remoteName = getNormalizedRemoteName(pluginId);
const type = url.endsWith("/mf-manifest.json")
? ("manifest" as const)
: url.endsWith("/remoteEntry.js")
? ("script" as const)
: undefined;
yield* Effect.try({
try: () =>
mf.registerRemotes([
{
name: remoteName,
entry: url,
...(type ? { type } : {}),
},
]),
catch: (error): ModuleFederationError =>
new ModuleFederationError({
pluginId,
remoteUrl: url,
cause: error instanceof Error ? error : new Error(String(error)),
}),
});
console.log(`[MF] ✅ Registered ${pluginId}`);
}),
loadRemoteConstructor: (pluginId: string, url: string) =>
Effect.gen(function* () {
const remoteName = getNormalizedRemoteName(pluginId);
console.log(`[MF] Loading remote ${remoteName}`);
const modulePath = `${remoteName}/plugin`;
return yield* Effect.tryPromise({
try: async () => {
const container = await mf.loadRemote<RemoteModule>(modulePath);
if (!container) {
throw new Error(`No container returned for ${modulePath}`);
}
// Support multiple export patterns: direct function, default export, named exports
let Constructor: any;
if (typeof container === "function") {
// Direct function export
Constructor = container;
} else if (container.default) {
// Default export
Constructor = container.default;
} else {
// Named export fallback - prioritize exports with 'binding' property (plugin classes)
Constructor = Object.values(container).find(
(exp) => typeof exp === "function" && (exp as any).binding !== undefined,
);
// Fallback to any function export if no binding found
if (!Constructor) {
Constructor = Object.values(container).find(
(exp) => typeof exp === "function" && exp.prototype?.constructor === exp,
);
}
}
if (!Constructor || typeof Constructor !== "function") {
const containerInfo =
typeof container === "object"
? `Available exports: ${Object.keys(container).join(", ")}`
: `Container type: ${typeof container}`;
throw new Error(
`No valid plugin constructor found for '${pluginId}'.\n` +
`Supported patterns:\n` +
` - export const YourPlugin = createPlugin({...})\n` +
` - export default createPlugin({...})\n` +
`${containerInfo}`,
);
}
// Validate it looks like a plugin constructor (has binding property)
if (!(Constructor as any).binding) {
const containerInfo =
typeof container === "object"
? `Found exports: ${Object.keys(container).join(", ")}`
: `Container type: ${typeof container}`;
throw new Error(
`Invalid plugin constructor for '${pluginId}'. ` +
`The exported value must be created with createPlugin(). ` +
`Found a function but it's missing the required 'binding' property.\n` +
`${containerInfo}`,
);
}
console.log(`[MF] ✅ Loaded constructor for ${pluginId}`);
return Constructor;
},
catch: (error): ModuleFederationError =>
new ModuleFederationError({
pluginId,
remoteUrl: url,
cause: error instanceof Error ? error : new Error(String(error)),
}),
});
}),
};
}),
},
) {}