every-plugin 0.9.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/_virtual/{rolldown_runtime.cjs → _rolldown/runtime.cjs} +15 -1
  2. package/dist/_virtual/{rolldown_runtime.mjs → _rolldown/runtime.mjs} +15 -2
  3. package/dist/build/rspack/dev-server-middleware.cjs +15 -4
  4. package/dist/build/rspack/dev-server-middleware.cjs.map +1 -1
  5. package/dist/build/rspack/dev-server-middleware.mjs +15 -4
  6. package/dist/build/rspack/dev-server-middleware.mjs.map +1 -1
  7. package/dist/build/rspack/fix-mf-data-uri-plugin.cjs +44 -0
  8. package/dist/build/rspack/fix-mf-data-uri-plugin.cjs.map +1 -0
  9. package/dist/build/rspack/fix-mf-data-uri-plugin.d.cts +11 -0
  10. package/dist/build/rspack/fix-mf-data-uri-plugin.d.cts.map +1 -0
  11. package/dist/build/rspack/fix-mf-data-uri-plugin.d.mts +11 -0
  12. package/dist/build/rspack/fix-mf-data-uri-plugin.d.mts.map +1 -0
  13. package/dist/build/rspack/fix-mf-data-uri-plugin.mjs +43 -0
  14. package/dist/build/rspack/fix-mf-data-uri-plugin.mjs.map +1 -0
  15. package/dist/build/rspack/index.cjs +8 -4
  16. package/dist/build/rspack/index.d.cts +3 -2
  17. package/dist/build/rspack/index.d.mts +3 -2
  18. package/dist/build/rspack/index.mjs +3 -2
  19. package/dist/build/rspack/module-federation.cjs +3 -3
  20. package/dist/build/rspack/module-federation.cjs.map +1 -1
  21. package/dist/build/rspack/module-federation.mjs +1 -1
  22. package/dist/build/rspack/module-federation.mjs.map +1 -1
  23. package/dist/build/rspack/plugin.cjs +83 -5
  24. package/dist/build/rspack/plugin.cjs.map +1 -1
  25. package/dist/build/rspack/plugin.d.cts +12 -1
  26. package/dist/build/rspack/plugin.d.cts.map +1 -1
  27. package/dist/build/rspack/plugin.d.mts +12 -1
  28. package/dist/build/rspack/plugin.d.mts.map +1 -1
  29. package/dist/build/rspack/plugin.mjs +80 -5
  30. package/dist/build/rspack/plugin.mjs.map +1 -1
  31. package/dist/build/rspack/utils.cjs +5 -4
  32. package/dist/build/rspack/utils.cjs.map +1 -1
  33. package/dist/build/rspack/utils.mjs +3 -2
  34. package/dist/build/rspack/utils.mjs.map +1 -1
  35. package/dist/build/shared-deps.cjs +50 -42
  36. package/dist/build/shared-deps.cjs.map +1 -1
  37. package/dist/build/shared-deps.d.cts +27 -2
  38. package/dist/build/shared-deps.d.cts.map +1 -1
  39. package/dist/build/shared-deps.d.mts +27 -2
  40. package/dist/build/shared-deps.d.mts.map +1 -1
  41. package/dist/build/shared-deps.mjs +48 -43
  42. package/dist/build/shared-deps.mjs.map +1 -1
  43. package/dist/errors.cjs +1 -0
  44. package/dist/errors.cjs.map +1 -1
  45. package/dist/errors.d.cts.map +1 -1
  46. package/dist/errors.d.mts.map +1 -1
  47. package/dist/errors.mjs.map +1 -1
  48. package/dist/index.cjs +2 -0
  49. package/dist/index.d.cts +2 -2
  50. package/dist/index.d.mts +2 -2
  51. package/dist/index.mjs +2 -2
  52. package/dist/orpc-client.cjs +17 -0
  53. package/dist/orpc-client.d.cts +2 -0
  54. package/dist/orpc-client.d.mts +2 -0
  55. package/dist/orpc-client.mjs +5 -0
  56. package/dist/orpc-openapi.cjs +17 -0
  57. package/dist/orpc-openapi.d.cts +2 -0
  58. package/dist/orpc-openapi.d.mts +2 -0
  59. package/dist/orpc-openapi.mjs +5 -0
  60. package/dist/orpc.cjs +8 -7
  61. package/dist/plugin.cjs +9 -7
  62. package/dist/plugin.cjs.map +1 -1
  63. package/dist/plugin.d.cts +23 -24
  64. package/dist/plugin.d.cts.map +1 -1
  65. package/dist/plugin.d.mts +23 -24
  66. package/dist/plugin.d.mts.map +1 -1
  67. package/dist/plugin.mjs +8 -7
  68. package/dist/plugin.mjs.map +1 -1
  69. package/dist/runtime/errors.cjs +2 -2
  70. package/dist/runtime/errors.cjs.map +1 -1
  71. package/dist/runtime/errors.d.cts +4 -4
  72. package/dist/runtime/errors.d.cts.map +1 -1
  73. package/dist/runtime/errors.d.mts +4 -4
  74. package/dist/runtime/errors.d.mts.map +1 -1
  75. package/dist/runtime/errors.mjs +1 -1
  76. package/dist/runtime/errors.mjs.map +1 -1
  77. package/dist/runtime/index.cjs +41 -37
  78. package/dist/runtime/index.cjs.map +1 -1
  79. package/dist/runtime/index.d.cts +3 -3
  80. package/dist/runtime/index.d.cts.map +1 -1
  81. package/dist/runtime/index.d.mts +3 -3
  82. package/dist/runtime/index.d.mts.map +1 -1
  83. package/dist/runtime/index.mjs +12 -9
  84. package/dist/runtime/index.mjs.map +1 -1
  85. package/dist/runtime/mf-config.cjs +33 -0
  86. package/dist/runtime/mf-config.cjs.map +1 -0
  87. package/dist/runtime/mf-config.d.cts +41 -0
  88. package/dist/runtime/mf-config.d.cts.map +1 -0
  89. package/dist/runtime/mf-config.d.mts +41 -0
  90. package/dist/runtime/mf-config.d.mts.map +1 -0
  91. package/dist/runtime/mf-config.mjs +29 -0
  92. package/dist/runtime/mf-config.mjs.map +1 -0
  93. package/dist/runtime/services/module-federation.service.cjs +19 -56
  94. package/dist/runtime/services/module-federation.service.cjs.map +1 -1
  95. package/dist/runtime/services/module-federation.service.d.cts.map +1 -1
  96. package/dist/runtime/services/module-federation.service.d.mts.map +1 -1
  97. package/dist/runtime/services/module-federation.service.mjs +18 -55
  98. package/dist/runtime/services/module-federation.service.mjs.map +1 -1
  99. package/dist/runtime/services/normalize.cjs +3 -2
  100. package/dist/runtime/services/normalize.cjs.map +1 -1
  101. package/dist/runtime/services/normalize.mjs +2 -2
  102. package/dist/runtime/services/normalize.mjs.map +1 -1
  103. package/dist/runtime/services/plugin-lifecycle.service.cjs +1 -1
  104. package/dist/runtime/services/plugin-lifecycle.service.cjs.map +1 -1
  105. package/dist/runtime/services/plugin-lifecycle.service.d.cts.map +1 -1
  106. package/dist/runtime/services/plugin-lifecycle.service.d.mts.map +1 -1
  107. package/dist/runtime/services/plugin-lifecycle.service.mjs.map +1 -1
  108. package/dist/runtime/services/plugin-loader.service.cjs +3 -3
  109. package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
  110. package/dist/runtime/services/plugin-loader.service.d.cts +1 -1
  111. package/dist/runtime/services/plugin-loader.service.d.cts.map +1 -1
  112. package/dist/runtime/services/plugin-loader.service.d.mts +1 -1
  113. package/dist/runtime/services/plugin-loader.service.d.mts.map +1 -1
  114. package/dist/runtime/services/plugin-loader.service.mjs +2 -2
  115. package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
  116. package/dist/runtime/services/plugin.service.cjs +1 -1
  117. package/dist/runtime/services/plugin.service.cjs.map +1 -1
  118. package/dist/runtime/services/plugin.service.d.cts +6 -7
  119. package/dist/runtime/services/plugin.service.d.cts.map +1 -1
  120. package/dist/runtime/services/plugin.service.d.mts +6 -7
  121. package/dist/runtime/services/plugin.service.d.mts.map +1 -1
  122. package/dist/runtime/services/plugin.service.mjs.map +1 -1
  123. package/dist/runtime/services/secrets.service.cjs +1 -1
  124. package/dist/runtime/services/secrets.service.cjs.map +1 -1
  125. package/dist/runtime/services/secrets.service.d.cts.map +1 -1
  126. package/dist/runtime/services/secrets.service.d.mts.map +1 -1
  127. package/dist/runtime/services/secrets.service.mjs.map +1 -1
  128. package/dist/runtime/validation.cjs +1 -1
  129. package/dist/runtime/validation.cjs.map +1 -1
  130. package/dist/runtime/validation.mjs.map +1 -1
  131. package/dist/testing/index.cjs +2 -1
  132. package/dist/testing/index.cjs.map +1 -1
  133. package/dist/testing/index.d.cts +0 -2
  134. package/dist/testing/index.d.cts.map +1 -1
  135. package/dist/testing/index.d.mts +0 -2
  136. package/dist/testing/index.d.mts.map +1 -1
  137. package/dist/testing/index.mjs +1 -1
  138. package/dist/testing/index.mjs.map +1 -1
  139. package/dist/types.d.cts +6 -6
  140. package/dist/types.d.cts.map +1 -1
  141. package/dist/types.d.mts +6 -6
  142. package/dist/types.d.mts.map +1 -1
  143. package/dist/zod.cjs +4 -3
  144. package/dist/zod.mjs +2 -2
  145. package/package.json +204 -182
  146. package/src/build/rspack/dev-server-middleware.ts +233 -0
  147. package/src/build/rspack/fix-mf-data-uri-plugin.ts +49 -0
  148. package/src/build/rspack/index.ts +14 -0
  149. package/src/build/rspack/module-federation.ts +6 -0
  150. package/src/build/rspack/plugin.ts +252 -0
  151. package/src/build/rspack/utils.ts +60 -0
  152. package/src/build/shared-deps.ts +79 -0
  153. package/src/effect.ts +1 -0
  154. package/src/errors.ts +140 -0
  155. package/src/index.ts +26 -0
  156. package/src/orpc-client.ts +2 -0
  157. package/src/orpc-openapi.ts +2 -0
  158. package/src/orpc.ts +9 -0
  159. package/src/plugin.ts +220 -0
  160. package/src/runtime/errors.ts +332 -0
  161. package/src/runtime/index.ts +258 -0
  162. package/src/runtime/mf-config.ts +29 -0
  163. package/src/runtime/services/module-federation.service.ts +181 -0
  164. package/src/runtime/services/normalize.ts +18 -0
  165. package/src/runtime/services/plugin-lifecycle.service.ts +86 -0
  166. package/src/runtime/services/plugin-loader.service.ts +254 -0
  167. package/src/runtime/services/plugin.service.ts +66 -0
  168. package/src/runtime/services/secrets.service.ts +74 -0
  169. package/src/runtime/services.ts +5 -0
  170. package/src/runtime/validation.ts +23 -0
  171. package/src/testing/index.ts +87 -0
  172. package/src/testing/mocks/module-federation.service.ts +41 -0
  173. package/src/types.ts +309 -0
  174. package/src/zod-core.ts +4 -0
  175. package/src/zod.ts +1 -0
  176. package/dist/package.cjs +0 -45
  177. package/dist/package.cjs.map +0 -1
  178. package/dist/package.mjs +0 -27
  179. package/dist/package.mjs.map +0 -1
@@ -0,0 +1,258 @@
1
+ import { createRouterClient } from "@orpc/server";
2
+ import { Cause, Effect, Exit, Hash, ManagedRuntime, Option } from "effect";
3
+ import type {
4
+ AnyPlugin,
5
+ AnyPluginConstructor,
6
+ InferRegistryFromEntries,
7
+ InitializedPlugin,
8
+ LoadedPlugin,
9
+ PluginConfigInput,
10
+ PluginInstance,
11
+ PluginRegistry,
12
+ PluginRegistryEntry,
13
+ PluginRouterType,
14
+ PluginRuntimeConfig,
15
+ RegisteredPlugin,
16
+ RegisteredPlugins,
17
+ UsePluginResult,
18
+ } from "../types";
19
+ import { PluginRuntimeError } from "./errors";
20
+ import { PluginService } from "./services/plugin.service";
21
+
22
+ export class PluginRuntime<R = RegisteredPlugins> {
23
+ readonly __registryType?: R;
24
+
25
+ private pluginCache = new Map<
26
+ string,
27
+ Effect.Effect<InitializedPlugin<AnyPlugin>, PluginRuntimeError>
28
+ >();
29
+
30
+ constructor(
31
+ private runtime: ManagedRuntime.ManagedRuntime<PluginService, never>,
32
+ private registry: PluginRegistry,
33
+ ) {}
34
+
35
+ private generateCacheKey(pluginId: string, config: unknown): string {
36
+ const configHash = Hash.structure(config as object).toString();
37
+ return `${pluginId}:${configHash}`;
38
+ }
39
+
40
+ private validatePluginId(pluginId: string): Effect.Effect<string, PluginRuntimeError> {
41
+ if (!(pluginId in this.registry)) {
42
+ return Effect.fail(
43
+ new PluginRuntimeError({
44
+ pluginId: String(pluginId),
45
+ operation: "validate-plugin-id",
46
+ cause: new Error(`Plugin ID '${String(pluginId)}' not found in registry.`),
47
+ retryable: false,
48
+ }),
49
+ );
50
+ }
51
+ return Effect.succeed(String(pluginId));
52
+ }
53
+
54
+ private async runPromise<A, E>(effect: Effect.Effect<A, E, PluginService>): Promise<A> {
55
+ const exit = await this.runtime.runPromiseExit(effect);
56
+
57
+ if (Exit.isFailure(exit)) {
58
+ const error = Cause.failureOption(exit.cause);
59
+ if (Option.isSome(error)) {
60
+ throw error.value;
61
+ }
62
+ throw Cause.squash(exit.cause);
63
+ }
64
+
65
+ return exit.value;
66
+ }
67
+
68
+ async usePlugin<K extends keyof R & string>(
69
+ pluginId: K,
70
+ config: PluginConfigInput<R[K]>,
71
+ plugins?: Record<string, unknown>,
72
+ ): Promise<UsePluginResult<K, R>> {
73
+ const cacheKey = this.generateCacheKey(pluginId, { ...config, __plugins: plugins ?? {} });
74
+
75
+ let cachedPlugin = this.pluginCache.get(cacheKey);
76
+ if (!cachedPlugin) {
77
+ const operation = Effect.gen(this, function* () {
78
+ const pluginService = yield* PluginService;
79
+ const validatedId = yield* this.validatePluginId(pluginId);
80
+
81
+ // Load → Instantiate → Initialize
82
+ const ctor = yield* pluginService.loadPlugin(validatedId);
83
+ const instance = yield* pluginService.instantiatePlugin(pluginId, ctor);
84
+ const initialized = yield* pluginService.initializePlugin(instance, config, plugins);
85
+
86
+ return initialized;
87
+ }).pipe(Effect.provide(this.runtime));
88
+
89
+ cachedPlugin = Effect.cached(operation).pipe(Effect.flatten);
90
+ this.pluginCache.set(cacheKey, cachedPlugin);
91
+ }
92
+
93
+ const initialized = await this.runPromise(cachedPlugin);
94
+
95
+ // Create client factory that accepts request context
96
+ const createClient = (context?: any) => {
97
+ const router = initialized.plugin.createRouter(initialized.context);
98
+ return createRouterClient(router, { context: context ?? {} });
99
+ };
100
+
101
+ return {
102
+ createClient: createClient as any,
103
+ router: initialized.plugin.createRouter(initialized.context) as PluginRouterType<R[K]>,
104
+ metadata: initialized.metadata,
105
+ initialized: initialized as InitializedPlugin<RegisteredPlugin<K, R>>,
106
+ } as UsePluginResult<K, R>;
107
+ }
108
+
109
+ async loadPlugin<K extends keyof R & string>(
110
+ pluginId: K,
111
+ ): Promise<LoadedPlugin<RegisteredPlugin<K, R>>> {
112
+ const effect = Effect.gen(function* () {
113
+ const pluginService = yield* PluginService;
114
+ return yield* pluginService.loadPlugin(pluginId);
115
+ });
116
+ return this.runPromise(effect) as Promise<LoadedPlugin<RegisteredPlugin<K, R>>>;
117
+ }
118
+
119
+ async instantiatePlugin<K extends keyof R & string>(
120
+ pluginId: K,
121
+ loadedPlugin: LoadedPlugin<RegisteredPlugin<K, R>>,
122
+ ): Promise<PluginInstance<RegisteredPlugin<K, R>>> {
123
+ const effect = Effect.gen(function* () {
124
+ const pluginService = yield* PluginService;
125
+ return yield* pluginService.instantiatePlugin(pluginId, loadedPlugin);
126
+ });
127
+ return this.runPromise(effect) as Promise<PluginInstance<RegisteredPlugin<K, R>>>;
128
+ }
129
+
130
+ async initializePlugin<T extends AnyPlugin>(
131
+ instance: PluginInstance<T>,
132
+ config: any,
133
+ plugins?: Record<string, unknown>,
134
+ ): Promise<InitializedPlugin<T>> {
135
+ const effect = Effect.gen(function* () {
136
+ const pluginService = yield* PluginService;
137
+ return yield* pluginService.initializePlugin(instance, config, plugins);
138
+ });
139
+ return this.runPromise(effect);
140
+ }
141
+
142
+ async shutdown(): Promise<void> {
143
+ const effect = Effect.gen(function* () {
144
+ const pluginService = yield* PluginService;
145
+ yield* pluginService.cleanup();
146
+ });
147
+ return this.runPromise(effect);
148
+ }
149
+
150
+ async evictPlugin<K extends keyof R & string>(
151
+ pluginId: K,
152
+ config: PluginConfigInput<R[K]>,
153
+ ): Promise<void> {
154
+ const cacheKey = this.generateCacheKey(pluginId, config);
155
+
156
+ const effect = Effect.gen(this, function* () {
157
+ const pluginService = yield* PluginService;
158
+ const cachedPlugin = this.pluginCache.get(cacheKey);
159
+
160
+ if (cachedPlugin) {
161
+ this.pluginCache.delete(cacheKey);
162
+
163
+ const pluginResult = yield* cachedPlugin.pipe(Effect.catchAll(() => Effect.succeed(null)));
164
+
165
+ if (pluginResult) {
166
+ yield* pluginService
167
+ .shutdownPlugin(pluginResult)
168
+ .pipe(Effect.catchAll(() => Effect.void));
169
+ }
170
+ }
171
+ }).pipe(Effect.catchAll(() => Effect.void));
172
+
173
+ return this.runPromise(effect);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Normalizes a remote URL to ensure it points to remoteEntry.js
179
+ * If the URL doesn't end with a file extension, appends /remoteEntry.js
180
+ */
181
+ function normalizeRemoteUrl(url: string): string {
182
+ if (!url) return url;
183
+ if (url.endsWith(".js") || url.endsWith(".json")) return url;
184
+ return `${url.endsWith("/") ? url.slice(0, -1) : url}/remoteEntry.js`;
185
+ }
186
+
187
+ /**
188
+ * Extract plugin map (module constructors) from registry entries
189
+ */
190
+ function extractPluginMap(
191
+ registry: Record<string, PluginRegistryEntry>,
192
+ ): Record<string, AnyPluginConstructor> {
193
+ const pluginMap: Record<string, AnyPluginConstructor> = {};
194
+
195
+ for (const [pluginId, entry] of Object.entries(registry)) {
196
+ if ("module" in entry && entry.module) {
197
+ pluginMap[pluginId] = entry.module;
198
+ }
199
+ }
200
+
201
+ return pluginMap;
202
+ }
203
+
204
+ /**
205
+ * Normalize registry entries - ensure remote URLs are properly formatted
206
+ */
207
+ function normalizeRegistry(registry: Record<string, PluginRegistryEntry>): PluginRegistry {
208
+ const normalized: Record<string, PluginRegistryEntry> = {};
209
+
210
+ for (const [pluginId, entry] of Object.entries(registry)) {
211
+ if ("module" in entry) {
212
+ normalized[pluginId] = {
213
+ ...entry,
214
+ remote: entry.remote ? normalizeRemoteUrl(entry.remote) : undefined,
215
+ };
216
+ } else {
217
+ normalized[pluginId] = {
218
+ ...entry,
219
+ remote: normalizeRemoteUrl(entry.remote),
220
+ };
221
+ }
222
+ }
223
+
224
+ return normalized as PluginRegistry;
225
+ }
226
+
227
+ /**
228
+ * Creates a plugin runtime with support for both module and remote plugin entries.
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * // With module entries (types inferred automatically)
233
+ * const runtime = createPluginRuntime({
234
+ * registry: {
235
+ * telegram: { module: TelegramPlugin },
236
+ * gopher: { remote: "https://cdn.example.com/gopher/remoteEntry.js" }
237
+ * },
238
+ * secrets: { API_KEY: "..." }
239
+ * });
240
+ *
241
+ * // Types are automatically inferred from module entries!
242
+ * const { router } = await runtime.usePlugin("telegram", config);
243
+ * ```
244
+ */
245
+ export function createPluginRuntime<TRegistry extends Record<string, PluginRegistryEntry>>(
246
+ config: PluginRuntimeConfig<TRegistry>,
247
+ ): PluginRuntime<InferRegistryFromEntries<TRegistry>> {
248
+ const secrets = config.secrets || {};
249
+ const normalizedRegistry = normalizeRegistry(config.registry);
250
+ const pluginMap = extractPluginMap(config.registry);
251
+
252
+ const layer = PluginService.Live(normalizedRegistry, secrets, pluginMap);
253
+ const runtime = ManagedRuntime.make(layer);
254
+
255
+ return new PluginRuntime(runtime, normalizedRegistry) as PluginRuntime<
256
+ InferRegistryFromEntries<TRegistry>
257
+ >;
258
+ }
@@ -0,0 +1,29 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ const require = createRequire(import.meta.url);
4
+ const pkg = require("../../package.json") as { version: string };
5
+ export const PLUGIN_VERSION = pkg.version;
6
+
7
+ export const SHARE_CONFIG = {
8
+ singleton: true,
9
+ requiredVersion: false,
10
+ strictVersion: false,
11
+ eager: false,
12
+ } as const;
13
+
14
+ export const MF_SHARED_DEPS = {
15
+ "every-plugin": {
16
+ version: PLUGIN_VERSION,
17
+ shareConfig: SHARE_CONFIG,
18
+ },
19
+ effect: {
20
+ version: "^3.21.0",
21
+ shareConfig: SHARE_CONFIG,
22
+ },
23
+ zod: {
24
+ version: "^4.3.6",
25
+ shareConfig: SHARE_CONFIG,
26
+ },
27
+ } as const;
28
+
29
+ export type SharedDepName = keyof typeof MF_SHARED_DEPS;
@@ -0,0 +1,181 @@
1
+ import { createInstance, getInstance } from "@module-federation/enhanced/runtime";
2
+ import { setGlobalFederationInstance } from "@module-federation/runtime-core";
3
+ import { Effect } from "effect";
4
+ import type { AnyPlugin } from "../../types";
5
+ import { ModuleFederationError } from "../errors";
6
+ import { MF_SHARED_DEPS, type SharedDepName } from "../mf-config";
7
+ import { getNormalizedRemoteName } from "./normalize";
8
+
9
+ type RemoteModule = (new () => AnyPlugin) | { default: new () => AnyPlugin };
10
+
11
+ const sharedModuleLoaders = {
12
+ "every-plugin": () => import("every-plugin"),
13
+ effect: () => import("effect"),
14
+ zod: () => import("zod"),
15
+ } satisfies Record<SharedDepName, () => Promise<unknown>>;
16
+
17
+ const createModuleFederationInstance = Effect.cached(
18
+ Effect.sync(() => {
19
+ try {
20
+ let instance = getInstance();
21
+
22
+ if (!instance) {
23
+ const shared = Object.fromEntries(
24
+ (
25
+ Object.entries(MF_SHARED_DEPS) as [
26
+ SharedDepName,
27
+ (typeof MF_SHARED_DEPS)[SharedDepName],
28
+ ][]
29
+ ).map(([name, config]) => {
30
+ const load = sharedModuleLoaders[name];
31
+
32
+ if (!load) {
33
+ throw new Error(`Missing shared module loader for ${name}`);
34
+ }
35
+
36
+ return [
37
+ name,
38
+ {
39
+ version: config.version,
40
+ get: () => load().then((mod) => () => mod),
41
+ shareConfig: config.shareConfig,
42
+ },
43
+ ];
44
+ }),
45
+ );
46
+
47
+ instance = createInstance({
48
+ name: "host",
49
+ remotes: [],
50
+ shared,
51
+ });
52
+
53
+ setGlobalFederationInstance(instance);
54
+ }
55
+
56
+ return instance;
57
+ } catch (error) {
58
+ throw new Error(`Failed to initialize Module Federation: ${error}`);
59
+ }
60
+ }),
61
+ );
62
+
63
+ export class ModuleFederationService extends Effect.Service<ModuleFederationService>()(
64
+ "ModuleFederationService",
65
+ {
66
+ effect: Effect.gen(function* () {
67
+ const mf = yield* Effect.flatten(createModuleFederationInstance);
68
+
69
+ return {
70
+ registerRemote: (pluginId: string, url: string) =>
71
+ Effect.gen(function* () {
72
+ console.log(`[MF] Registering ${pluginId}`);
73
+
74
+ const remoteName = getNormalizedRemoteName(pluginId);
75
+ const type = url.endsWith("/mf-manifest.json")
76
+ ? ("manifest" as const)
77
+ : url.endsWith("/remoteEntry.js")
78
+ ? ("script" as const)
79
+ : undefined;
80
+
81
+ yield* Effect.try({
82
+ try: () =>
83
+ mf.registerRemotes([
84
+ {
85
+ name: remoteName,
86
+ entry: url,
87
+ ...(type ? { type } : {}),
88
+ },
89
+ ]),
90
+ catch: (error): ModuleFederationError =>
91
+ new ModuleFederationError({
92
+ pluginId,
93
+ remoteUrl: url,
94
+ cause: error instanceof Error ? error : new Error(String(error)),
95
+ }),
96
+ });
97
+
98
+ console.log(`[MF] ✅ Registered ${pluginId}`);
99
+ }),
100
+
101
+ loadRemoteConstructor: (pluginId: string, url: string) =>
102
+ Effect.gen(function* () {
103
+ const remoteName = getNormalizedRemoteName(pluginId);
104
+ console.log(`[MF] Loading remote ${remoteName}`);
105
+ const modulePath = `${remoteName}/plugin`;
106
+
107
+ return yield* Effect.tryPromise({
108
+ try: async () => {
109
+ const container = await mf.loadRemote<RemoteModule>(modulePath);
110
+ if (!container) {
111
+ throw new Error(`No container returned for ${modulePath}`);
112
+ }
113
+
114
+ // Support multiple export patterns: direct function, default export, named exports
115
+ let Constructor: any;
116
+
117
+ if (typeof container === "function") {
118
+ // Direct function export
119
+ Constructor = container;
120
+ } else if (container.default) {
121
+ // Default export
122
+ Constructor = container.default;
123
+ } else {
124
+ // Named export fallback - prioritize exports with 'binding' property (plugin classes)
125
+ Constructor = Object.values(container).find(
126
+ (exp) => typeof exp === "function" && (exp as any).binding !== undefined,
127
+ );
128
+
129
+ // Fallback to any function export if no binding found
130
+ if (!Constructor) {
131
+ Constructor = Object.values(container).find(
132
+ (exp) => typeof exp === "function" && exp.prototype?.constructor === exp,
133
+ );
134
+ }
135
+ }
136
+
137
+ if (!Constructor || typeof Constructor !== "function") {
138
+ const containerInfo =
139
+ typeof container === "object"
140
+ ? `Available exports: ${Object.keys(container).join(", ")}`
141
+ : `Container type: ${typeof container}`;
142
+
143
+ throw new Error(
144
+ `No valid plugin constructor found for '${pluginId}'.\n` +
145
+ `Supported patterns:\n` +
146
+ ` - export const YourPlugin = createPlugin({...})\n` +
147
+ ` - export default createPlugin({...})\n` +
148
+ `${containerInfo}`,
149
+ );
150
+ }
151
+
152
+ // Validate it looks like a plugin constructor (has binding property)
153
+ if (!(Constructor as any).binding) {
154
+ const containerInfo =
155
+ typeof container === "object"
156
+ ? `Found exports: ${Object.keys(container).join(", ")}`
157
+ : `Container type: ${typeof container}`;
158
+
159
+ throw new Error(
160
+ `Invalid plugin constructor for '${pluginId}'. ` +
161
+ `The exported value must be created with createPlugin(). ` +
162
+ `Found a function but it's missing the required 'binding' property.\n` +
163
+ `${containerInfo}`,
164
+ );
165
+ }
166
+
167
+ console.log(`[MF] ✅ Loaded constructor for ${pluginId}`);
168
+ return Constructor;
169
+ },
170
+ catch: (error): ModuleFederationError =>
171
+ new ModuleFederationError({
172
+ pluginId,
173
+ remoteUrl: url,
174
+ cause: error instanceof Error ? error : new Error(String(error)),
175
+ }),
176
+ });
177
+ }),
178
+ };
179
+ }),
180
+ },
181
+ ) {}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Normalizes a package name into the module federation remote name.
3
+ * This function must produce identical results in both runtime and build contexts.
4
+ *
5
+ * Normalization rules:
6
+ * - Convert to lowercase
7
+ * - Strip a single leading '@' symbol
8
+ * - Replace '/' with '_'
9
+ * - Preserve hyphens and other characters
10
+ *
11
+ * Examples:
12
+ * - "@scope/my-plugin" → "scope_my-plugin"
13
+ * - "@SCOPE/Foo/Bar" → "scope_foo_bar"
14
+ * - "foo/bar" → "foo_bar"
15
+ * - "simple-plugin" → "simple-plugin"
16
+ */
17
+ export const getNormalizedRemoteName = (name: string): string =>
18
+ name.toLowerCase().replace(/^@/, "").replace(/\//g, "_");
@@ -0,0 +1,86 @@
1
+ import { Effect, Ref } from "effect";
2
+ import type { AnyPlugin, InitializedPlugin } from "../../types";
3
+ import { toPluginRuntimeError } from "../errors";
4
+
5
+ export class PluginLifecycleService extends Effect.Service<PluginLifecycleService>()(
6
+ "PluginLifecycleService",
7
+ {
8
+ effect: Effect.gen(function* () {
9
+ const activePlugins = yield* Ref.make(new Set<InitializedPlugin<AnyPlugin>>());
10
+
11
+ return {
12
+ register: <T extends AnyPlugin>(plugin: InitializedPlugin<T>) =>
13
+ Ref.update(activePlugins, (plugins) =>
14
+ new Set(plugins).add(plugin as InitializedPlugin<AnyPlugin>),
15
+ ),
16
+
17
+ unregister: (plugin: InitializedPlugin<AnyPlugin>) =>
18
+ Ref.update(activePlugins, (plugins) => {
19
+ const newSet = new Set(plugins);
20
+ newSet.delete(plugin);
21
+ return newSet;
22
+ }),
23
+
24
+ shutdown: (plugin: InitializedPlugin<AnyPlugin>) =>
25
+ Effect.gen(function* () {
26
+ // Remove from active plugins
27
+ yield* Ref.update(activePlugins, (plugins) => {
28
+ const newSet = new Set(plugins);
29
+ newSet.delete(plugin);
30
+ return newSet;
31
+ });
32
+
33
+ // Shutdown the plugin
34
+ yield* plugin.plugin
35
+ .shutdown()
36
+ .pipe(
37
+ Effect.mapError((error) =>
38
+ toPluginRuntimeError(
39
+ error,
40
+ plugin.plugin.id,
41
+ undefined,
42
+ "shutdown-plugin",
43
+ false,
44
+ ),
45
+ ),
46
+ );
47
+ }),
48
+
49
+ cleanup: () =>
50
+ Effect.gen(function* () {
51
+ const plugins = yield* Ref.get(activePlugins);
52
+
53
+ // Shutdown all active plugins concurrently
54
+ yield* Effect.forEach(
55
+ plugins,
56
+ (plugin) =>
57
+ plugin.plugin.shutdown().pipe(
58
+ Effect.mapError((error) =>
59
+ toPluginRuntimeError(
60
+ error,
61
+ plugin.plugin.id,
62
+ undefined,
63
+ "cleanup-shutdown",
64
+ false,
65
+ ),
66
+ ),
67
+ Effect.catchAll((error) =>
68
+ Effect.logError(
69
+ `Failed to shutdown plugin ${plugin.plugin.id} during cleanup`,
70
+ error,
71
+ { pluginId: plugin.plugin.id },
72
+ ),
73
+ ),
74
+ ),
75
+ { concurrency: "unbounded" },
76
+ );
77
+
78
+ // Clear the active plugins set
79
+ yield* Ref.set(activePlugins, new Set());
80
+ }).pipe(
81
+ Effect.catchAll(() => Effect.void), // Convert errors to void for cleanup
82
+ ),
83
+ };
84
+ }),
85
+ },
86
+ ) {}