every-plugin 0.8.9 → 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.
- package/dist/_virtual/{rolldown_runtime.cjs → _rolldown/runtime.cjs} +15 -1
- package/dist/_virtual/{rolldown_runtime.mjs → _rolldown/runtime.mjs} +15 -2
- package/dist/build/rspack/dev-server-middleware.cjs +15 -4
- package/dist/build/rspack/dev-server-middleware.cjs.map +1 -1
- package/dist/build/rspack/dev-server-middleware.mjs +15 -4
- package/dist/build/rspack/dev-server-middleware.mjs.map +1 -1
- package/dist/build/rspack/fix-mf-data-uri-plugin.cjs +44 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.cjs.map +1 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.d.cts +11 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.d.cts.map +1 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.d.mts +11 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.d.mts.map +1 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.mjs +43 -0
- package/dist/build/rspack/fix-mf-data-uri-plugin.mjs.map +1 -0
- package/dist/build/rspack/index.cjs +8 -6
- package/dist/build/rspack/index.d.cts +4 -3
- package/dist/build/rspack/index.d.mts +4 -3
- package/dist/build/rspack/index.mjs +4 -3
- package/dist/build/rspack/module-federation.cjs +3 -3
- package/dist/build/rspack/module-federation.cjs.map +1 -1
- package/dist/build/rspack/module-federation.mjs +1 -1
- package/dist/build/rspack/module-federation.mjs.map +1 -1
- package/dist/build/rspack/plugin.cjs +83 -5
- package/dist/build/rspack/plugin.cjs.map +1 -1
- package/dist/build/rspack/plugin.d.cts +12 -1
- package/dist/build/rspack/plugin.d.cts.map +1 -1
- package/dist/build/rspack/plugin.d.mts +12 -1
- package/dist/build/rspack/plugin.d.mts.map +1 -1
- package/dist/build/rspack/plugin.mjs +80 -5
- package/dist/build/rspack/plugin.mjs.map +1 -1
- package/dist/build/rspack/utils.cjs +5 -4
- package/dist/build/rspack/utils.cjs.map +1 -1
- package/dist/build/rspack/utils.mjs +3 -2
- package/dist/build/rspack/utils.mjs.map +1 -1
- package/dist/build/shared-deps.cjs +50 -65
- package/dist/build/shared-deps.cjs.map +1 -1
- package/dist/build/shared-deps.d.cts +27 -4
- package/dist/build/shared-deps.d.cts.map +1 -1
- package/dist/build/shared-deps.d.mts +27 -4
- package/dist/build/shared-deps.d.mts.map +1 -1
- package/dist/build/shared-deps.mjs +48 -64
- package/dist/build/shared-deps.mjs.map +1 -1
- package/dist/errors.cjs +1 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts.map +1 -1
- package/dist/errors.d.mts.map +1 -1
- package/dist/errors.mjs.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/orpc-client.cjs +17 -0
- package/dist/orpc-client.d.cts +2 -0
- package/dist/orpc-client.d.mts +2 -0
- package/dist/orpc-client.mjs +5 -0
- package/dist/orpc-openapi.cjs +17 -0
- package/dist/orpc-openapi.d.cts +2 -0
- package/dist/orpc-openapi.d.mts +2 -0
- package/dist/orpc-openapi.mjs +5 -0
- package/dist/orpc.cjs +8 -7
- package/dist/plugin.cjs +10 -9
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +28 -29
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +28 -29
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +10 -10
- package/dist/plugin.mjs.map +1 -1
- package/dist/runtime/errors.cjs +2 -2
- package/dist/runtime/errors.cjs.map +1 -1
- package/dist/runtime/errors.d.cts +4 -4
- package/dist/runtime/errors.d.cts.map +1 -1
- package/dist/runtime/errors.d.mts +4 -4
- package/dist/runtime/errors.d.mts.map +1 -1
- package/dist/runtime/errors.mjs +1 -1
- package/dist/runtime/errors.mjs.map +1 -1
- package/dist/runtime/index.cjs +24 -20
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +3 -3
- package/dist/runtime/index.d.cts.map +1 -1
- package/dist/runtime/index.d.mts +3 -3
- package/dist/runtime/index.d.mts.map +1 -1
- package/dist/runtime/index.mjs +12 -9
- package/dist/runtime/index.mjs.map +1 -1
- package/dist/runtime/mf-config.cjs +33 -0
- package/dist/runtime/mf-config.cjs.map +1 -0
- package/dist/runtime/mf-config.d.cts +41 -0
- package/dist/runtime/mf-config.d.cts.map +1 -0
- package/dist/runtime/mf-config.d.mts +41 -0
- package/dist/runtime/mf-config.d.mts.map +1 -0
- package/dist/runtime/mf-config.mjs +29 -0
- package/dist/runtime/mf-config.mjs.map +1 -0
- package/dist/runtime/services/module-federation.service.cjs +19 -56
- package/dist/runtime/services/module-federation.service.cjs.map +1 -1
- package/dist/runtime/services/module-federation.service.d.cts.map +1 -1
- package/dist/runtime/services/module-federation.service.d.mts.map +1 -1
- package/dist/runtime/services/module-federation.service.mjs +18 -55
- package/dist/runtime/services/module-federation.service.mjs.map +1 -1
- package/dist/runtime/services/normalize.cjs +3 -2
- package/dist/runtime/services/normalize.cjs.map +1 -1
- package/dist/runtime/services/normalize.d.cts.map +1 -1
- package/dist/runtime/services/normalize.d.mts.map +1 -1
- package/dist/runtime/services/normalize.mjs +2 -2
- package/dist/runtime/services/normalize.mjs.map +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.cjs +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.d.cts.map +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.d.mts.map +1 -1
- package/dist/runtime/services/plugin-lifecycle.service.mjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.cjs +3 -3
- package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.d.cts +1 -1
- package/dist/runtime/services/plugin-loader.service.d.cts.map +1 -1
- package/dist/runtime/services/plugin-loader.service.d.mts +1 -1
- package/dist/runtime/services/plugin-loader.service.d.mts.map +1 -1
- package/dist/runtime/services/plugin-loader.service.mjs +2 -2
- package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
- package/dist/runtime/services/plugin.service.cjs +1 -1
- package/dist/runtime/services/plugin.service.cjs.map +1 -1
- package/dist/runtime/services/plugin.service.d.cts +6 -7
- package/dist/runtime/services/plugin.service.d.cts.map +1 -1
- package/dist/runtime/services/plugin.service.d.mts +6 -7
- package/dist/runtime/services/plugin.service.d.mts.map +1 -1
- package/dist/runtime/services/plugin.service.mjs.map +1 -1
- package/dist/runtime/services/secrets.service.cjs +4 -4
- package/dist/runtime/services/secrets.service.cjs.map +1 -1
- package/dist/runtime/services/secrets.service.d.cts.map +1 -1
- package/dist/runtime/services/secrets.service.d.mts.map +1 -1
- package/dist/runtime/services/secrets.service.mjs +3 -3
- package/dist/runtime/services/secrets.service.mjs.map +1 -1
- package/dist/runtime/validation.cjs +1 -1
- package/dist/runtime/validation.cjs.map +1 -1
- package/dist/runtime/validation.mjs.map +1 -1
- package/dist/testing/index.cjs +2 -1
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +0 -2
- package/dist/testing/index.d.cts.map +1 -1
- package/dist/testing/index.d.mts +0 -2
- package/dist/testing/index.d.mts.map +1 -1
- package/dist/testing/index.mjs +1 -1
- package/dist/testing/index.mjs.map +1 -1
- package/dist/types.d.cts +22 -22
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +22 -22
- package/dist/types.d.mts.map +1 -1
- package/dist/zod.cjs +4 -3
- package/dist/zod.mjs +2 -2
- package/package.json +204 -194
- package/src/build/rspack/dev-server-middleware.ts +233 -0
- package/src/build/rspack/fix-mf-data-uri-plugin.ts +49 -0
- package/src/build/rspack/index.ts +14 -0
- package/src/build/rspack/module-federation.ts +6 -0
- package/src/build/rspack/plugin.ts +252 -0
- package/src/build/rspack/utils.ts +60 -0
- package/src/build/shared-deps.ts +79 -0
- package/src/effect.ts +1 -0
- package/src/errors.ts +140 -0
- package/src/index.ts +26 -0
- package/src/orpc-client.ts +2 -0
- package/src/orpc-openapi.ts +2 -0
- package/src/orpc.ts +9 -0
- package/src/plugin.ts +220 -0
- package/src/runtime/errors.ts +332 -0
- package/src/runtime/index.ts +258 -0
- package/src/runtime/mf-config.ts +29 -0
- package/src/runtime/services/module-federation.service.ts +181 -0
- package/src/runtime/services/normalize.ts +18 -0
- package/src/runtime/services/plugin-lifecycle.service.ts +86 -0
- package/src/runtime/services/plugin-loader.service.ts +254 -0
- package/src/runtime/services/plugin.service.ts +66 -0
- package/src/runtime/services/secrets.service.ts +74 -0
- package/src/runtime/services.ts +5 -0
- package/src/runtime/validation.ts +23 -0
- package/src/testing/index.ts +87 -0
- package/src/testing/mocks/module-federation.service.ts +41 -0
- package/src/types.ts +309 -0
- package/src/zod-core.ts +4 -0
- package/src/zod.ts +1 -0
- package/dist/package.cjs +0 -47
- package/dist/package.cjs.map +0 -1
- package/dist/package.mjs +0 -29
- package/dist/package.mjs.map +0 -1
|
@@ -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
|
+
) {}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { InferSchemaInput, InferSchemaOutput } from "@orpc/contract";
|
|
2
|
+
import { Context, Effect, Scope } from "effect";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
import type {
|
|
5
|
+
AnyPlugin,
|
|
6
|
+
AnyPluginConstructor,
|
|
7
|
+
InitializedPlugin,
|
|
8
|
+
LoadedPlugin,
|
|
9
|
+
PluginInstance,
|
|
10
|
+
PluginMetadata,
|
|
11
|
+
PluginRegistry,
|
|
12
|
+
} from "../../types";
|
|
13
|
+
import { PluginRuntimeError, toPluginRuntimeError } from "../errors";
|
|
14
|
+
import { validate } from "../validation";
|
|
15
|
+
import { ModuleFederationService } from "./module-federation.service";
|
|
16
|
+
import { SecretsService } from "./secrets.service";
|
|
17
|
+
|
|
18
|
+
export class PluginRegistryTag extends Context.Tag("PluginRegistry")<
|
|
19
|
+
PluginRegistryTag,
|
|
20
|
+
PluginRegistry
|
|
21
|
+
>() {}
|
|
22
|
+
|
|
23
|
+
export class PluginMapTag extends Context.Tag("PluginMap")<
|
|
24
|
+
PluginMapTag,
|
|
25
|
+
Record<string, AnyPluginConstructor>
|
|
26
|
+
>() {}
|
|
27
|
+
|
|
28
|
+
export class RegistryService extends Effect.Service<RegistryService>()("RegistryService", {
|
|
29
|
+
effect: Effect.gen(function* () {
|
|
30
|
+
const registry = yield* PluginRegistryTag;
|
|
31
|
+
const pluginMap = yield* PluginMapTag;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
get: (pluginId: string) =>
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const entry = registry[pluginId];
|
|
37
|
+
|
|
38
|
+
if (!entry) {
|
|
39
|
+
return yield* Effect.fail(
|
|
40
|
+
new PluginRuntimeError({
|
|
41
|
+
pluginId,
|
|
42
|
+
operation: "validate-plugin-id",
|
|
43
|
+
cause: new Error(`Plugin ${pluginId} not found in registry`),
|
|
44
|
+
retryable: false,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if ("module" in entry) {
|
|
50
|
+
return {
|
|
51
|
+
constructor: entry.module,
|
|
52
|
+
metadata: {
|
|
53
|
+
remoteUrl: entry.remote || "",
|
|
54
|
+
version: entry.version,
|
|
55
|
+
description: entry.description,
|
|
56
|
+
} as PluginMetadata,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
constructor: null,
|
|
62
|
+
metadata: {
|
|
63
|
+
remoteUrl: entry.remote,
|
|
64
|
+
version: entry.version,
|
|
65
|
+
description: entry.description,
|
|
66
|
+
} as PluginMetadata,
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
|
|
70
|
+
getModule: (pluginId: string) => Effect.succeed(pluginMap[pluginId] || null),
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
}) {}
|
|
74
|
+
|
|
75
|
+
export class PluginLoaderService extends Effect.Service<PluginLoaderService>()(
|
|
76
|
+
"PluginLoaderService",
|
|
77
|
+
{
|
|
78
|
+
effect: Effect.gen(function* () {
|
|
79
|
+
const moduleFederationService = yield* ModuleFederationService;
|
|
80
|
+
const secretsService = yield* SecretsService;
|
|
81
|
+
const registryService = yield* RegistryService;
|
|
82
|
+
|
|
83
|
+
const resolveUrl = (baseUrl: string, version?: string): string =>
|
|
84
|
+
version && version !== "latest" ? baseUrl.replace("@latest", `@${version}`) : baseUrl;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
loadPlugin: (pluginId: string) =>
|
|
88
|
+
Effect.gen(function* () {
|
|
89
|
+
const entry = yield* registryService.get(pluginId);
|
|
90
|
+
|
|
91
|
+
if (entry.constructor) {
|
|
92
|
+
yield* Effect.logDebug("Loading plugin from direct module", { pluginId });
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
ctor: entry.constructor,
|
|
96
|
+
metadata: entry.metadata,
|
|
97
|
+
} satisfies LoadedPlugin;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const url = entry.metadata.remoteUrl;
|
|
101
|
+
if (!url) {
|
|
102
|
+
return yield* Effect.fail(
|
|
103
|
+
new PluginRuntimeError({
|
|
104
|
+
pluginId,
|
|
105
|
+
operation: "load-plugin",
|
|
106
|
+
cause: new Error(`Plugin ${pluginId} has no module or remote URL configured`),
|
|
107
|
+
retryable: false,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const resolvedUrl = resolveUrl(url);
|
|
113
|
+
|
|
114
|
+
yield* moduleFederationService
|
|
115
|
+
.registerRemote(pluginId, resolvedUrl)
|
|
116
|
+
.pipe(
|
|
117
|
+
Effect.mapError((error) =>
|
|
118
|
+
toPluginRuntimeError(error, pluginId, undefined, "register-remote", true),
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
yield* Effect.logDebug("Loading plugin from remote", { pluginId, url: resolvedUrl });
|
|
123
|
+
|
|
124
|
+
const ctor = yield* moduleFederationService
|
|
125
|
+
.loadRemoteConstructor(pluginId, resolvedUrl)
|
|
126
|
+
.pipe(
|
|
127
|
+
Effect.mapError((error) =>
|
|
128
|
+
toPluginRuntimeError(error, pluginId, undefined, "load-remote", false),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
ctor,
|
|
134
|
+
metadata: entry.metadata,
|
|
135
|
+
} satisfies LoadedPlugin;
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
instantiatePlugin: <T extends AnyPlugin>(pluginId: string, loadedPlugin: LoadedPlugin<T>) =>
|
|
139
|
+
Effect.gen(function* () {
|
|
140
|
+
const instance = yield* Effect.try(() => new loadedPlugin.ctor()).pipe(
|
|
141
|
+
Effect.mapError((error) =>
|
|
142
|
+
toPluginRuntimeError(error, pluginId, undefined, "instantiate-plugin", false),
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
(instance.id as string) = pluginId;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
plugin: instance,
|
|
150
|
+
metadata: loadedPlugin.metadata,
|
|
151
|
+
} satisfies PluginInstance<T>;
|
|
152
|
+
}),
|
|
153
|
+
|
|
154
|
+
initializePlugin: <T extends AnyPlugin>(
|
|
155
|
+
pluginInstance: PluginInstance<T>,
|
|
156
|
+
config: {
|
|
157
|
+
variables: InferSchemaInput<T["configSchema"]["variables"]>;
|
|
158
|
+
secrets: InferSchemaInput<T["configSchema"]["secrets"]>;
|
|
159
|
+
},
|
|
160
|
+
plugins?: Record<string, unknown>,
|
|
161
|
+
) =>
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
const { plugin } = pluginInstance;
|
|
164
|
+
|
|
165
|
+
// Validate and hydrate config
|
|
166
|
+
const validatedVariables = yield* validate(
|
|
167
|
+
plugin.configSchema.variables as z.ZodSchema<
|
|
168
|
+
InferSchemaOutput<T["configSchema"]["variables"]>
|
|
169
|
+
>,
|
|
170
|
+
config.variables,
|
|
171
|
+
plugin.id,
|
|
172
|
+
"config",
|
|
173
|
+
).pipe(
|
|
174
|
+
Effect.mapError(
|
|
175
|
+
(validationError) =>
|
|
176
|
+
new PluginRuntimeError({
|
|
177
|
+
pluginId: plugin.id,
|
|
178
|
+
operation: "validate-config",
|
|
179
|
+
cause: validationError.zodError,
|
|
180
|
+
retryable: false,
|
|
181
|
+
}),
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// Validate secrets
|
|
186
|
+
const validatedSecrets = yield* validate(
|
|
187
|
+
plugin.configSchema.secrets as z.ZodSchema<
|
|
188
|
+
InferSchemaOutput<T["configSchema"]["secrets"]>
|
|
189
|
+
>,
|
|
190
|
+
config.secrets,
|
|
191
|
+
plugin.id,
|
|
192
|
+
"config",
|
|
193
|
+
).pipe(
|
|
194
|
+
Effect.mapError(
|
|
195
|
+
(validationError) =>
|
|
196
|
+
new PluginRuntimeError({
|
|
197
|
+
pluginId: plugin.id,
|
|
198
|
+
operation: "validate-secrets",
|
|
199
|
+
cause: validationError.zodError,
|
|
200
|
+
retryable: false,
|
|
201
|
+
}),
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Hydrate secrets in variables
|
|
206
|
+
const hydratedConfig = yield* secretsService.hydrateSecrets({
|
|
207
|
+
variables: validatedVariables,
|
|
208
|
+
secrets: validatedSecrets,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const _variables = yield* validate(
|
|
212
|
+
plugin.configSchema.variables as z.ZodSchema<
|
|
213
|
+
InferSchemaOutput<T["configSchema"]["variables"]>
|
|
214
|
+
>,
|
|
215
|
+
hydratedConfig.variables,
|
|
216
|
+
plugin.id,
|
|
217
|
+
"config",
|
|
218
|
+
).pipe(
|
|
219
|
+
Effect.mapError(
|
|
220
|
+
(validationError) =>
|
|
221
|
+
new PluginRuntimeError({
|
|
222
|
+
pluginId: plugin.id,
|
|
223
|
+
operation: "validate-hydrated-config",
|
|
224
|
+
cause: validationError.zodError,
|
|
225
|
+
retryable: false,
|
|
226
|
+
}),
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Create a long-lived scope for this plugin instance
|
|
231
|
+
const scope = yield* Scope.make();
|
|
232
|
+
|
|
233
|
+
// Initialize plugin within the scope
|
|
234
|
+
const context = yield* plugin
|
|
235
|
+
.initialize({ variables: _variables, secrets: hydratedConfig.secrets }, plugins ?? {})
|
|
236
|
+
.pipe(
|
|
237
|
+
Effect.provideService(Scope.Scope, scope),
|
|
238
|
+
Effect.mapError((error) =>
|
|
239
|
+
toPluginRuntimeError(error, plugin.id, undefined, "initialize-plugin", false),
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
plugin,
|
|
245
|
+
metadata: pluginInstance.metadata,
|
|
246
|
+
config: { variables: _variables, secrets: hydratedConfig.secrets },
|
|
247
|
+
context,
|
|
248
|
+
scope,
|
|
249
|
+
} satisfies InitializedPlugin<T>;
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
254
|
+
) {}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Effect, Exit, Layer, Scope } from "effect";
|
|
2
|
+
import type {
|
|
3
|
+
AnyPlugin,
|
|
4
|
+
AnyPluginConstructor,
|
|
5
|
+
InitializedPlugin,
|
|
6
|
+
PluginRegistry,
|
|
7
|
+
SecretsConfig,
|
|
8
|
+
} from "../../types";
|
|
9
|
+
import { ModuleFederationService } from "./module-federation.service";
|
|
10
|
+
import { PluginLifecycleService } from "./plugin-lifecycle.service";
|
|
11
|
+
import {
|
|
12
|
+
PluginLoaderService,
|
|
13
|
+
PluginMapTag,
|
|
14
|
+
PluginRegistryTag,
|
|
15
|
+
RegistryService,
|
|
16
|
+
} from "./plugin-loader.service";
|
|
17
|
+
import { SecretsConfigTag, SecretsService } from "./secrets.service";
|
|
18
|
+
|
|
19
|
+
export class PluginService extends Effect.Service<PluginService>()("PluginService", {
|
|
20
|
+
scoped: Effect.gen(function* () {
|
|
21
|
+
const loader = yield* PluginLoaderService;
|
|
22
|
+
const lifecycle = yield* PluginLifecycleService;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
loadPlugin: loader.loadPlugin,
|
|
26
|
+
instantiatePlugin: loader.instantiatePlugin,
|
|
27
|
+
initializePlugin: loader.initializePlugin,
|
|
28
|
+
shutdownPlugin: (plugin: InitializedPlugin<AnyPlugin>) =>
|
|
29
|
+
Effect.gen(function* () {
|
|
30
|
+
// Shutdown the plugin first (graceful cleanup)
|
|
31
|
+
yield* plugin.plugin.shutdown().pipe(Effect.catchAll(() => Effect.void));
|
|
32
|
+
|
|
33
|
+
// Close the plugin scope to interrupt fibers and release resources
|
|
34
|
+
yield* Scope.close(plugin.scope, Exit.succeed(undefined));
|
|
35
|
+
|
|
36
|
+
// Unregister from lifecycle tracking
|
|
37
|
+
yield* lifecycle.unregister(plugin);
|
|
38
|
+
}),
|
|
39
|
+
cleanup: lifecycle.cleanup,
|
|
40
|
+
};
|
|
41
|
+
}),
|
|
42
|
+
}) {
|
|
43
|
+
static Live = (
|
|
44
|
+
registry: PluginRegistry,
|
|
45
|
+
secrets: SecretsConfig,
|
|
46
|
+
pluginMap: Record<string, AnyPluginConstructor> = {},
|
|
47
|
+
) => {
|
|
48
|
+
const contextLayer = Layer.mergeAll(
|
|
49
|
+
Layer.succeed(PluginRegistryTag, registry),
|
|
50
|
+
Layer.succeed(SecretsConfigTag, secrets),
|
|
51
|
+
Layer.succeed(PluginMapTag, pluginMap),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const servicesLayer = Layer.mergeAll(
|
|
55
|
+
ModuleFederationService.Default,
|
|
56
|
+
SecretsService.Default,
|
|
57
|
+
RegistryService.Default,
|
|
58
|
+
PluginLifecycleService.Default,
|
|
59
|
+
).pipe(Layer.provide(contextLayer));
|
|
60
|
+
|
|
61
|
+
return PluginService.Default.pipe(
|
|
62
|
+
Layer.provide(PluginLoaderService.Default),
|
|
63
|
+
Layer.provide(servicesLayer),
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { SecretsConfig } from "../../types";
|
|
4
|
+
import { PluginRuntimeError } from "../errors";
|
|
5
|
+
|
|
6
|
+
const configSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
secrets: z.record(z.string(), z.unknown()),
|
|
9
|
+
})
|
|
10
|
+
.loose();
|
|
11
|
+
|
|
12
|
+
export class SecretsConfigTag extends Context.Tag("SecretsConfig")<
|
|
13
|
+
SecretsConfigTag,
|
|
14
|
+
SecretsConfig
|
|
15
|
+
>() {}
|
|
16
|
+
|
|
17
|
+
export class SecretsService extends Effect.Service<SecretsService>()("SecretsService", {
|
|
18
|
+
effect: Effect.gen(function* () {
|
|
19
|
+
const secrets = yield* SecretsConfigTag;
|
|
20
|
+
|
|
21
|
+
const hydrateValue = (value: unknown): unknown => {
|
|
22
|
+
if (typeof value === "string") {
|
|
23
|
+
let result = value;
|
|
24
|
+
for (const [key, secretValue] of Object.entries(secrets)) {
|
|
25
|
+
const pattern = new RegExp(`{{${key}}}`, "g");
|
|
26
|
+
result = result.replace(pattern, String(secretValue));
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (Array.isArray(value)) {
|
|
32
|
+
return value.map(hydrateValue);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (value && typeof value === "object") {
|
|
36
|
+
const isPlainObject = value.constructor === Object || value.constructor === undefined;
|
|
37
|
+
|
|
38
|
+
if (isPlainObject) {
|
|
39
|
+
const hydrated: Record<string, unknown> = {};
|
|
40
|
+
for (const [key, val] of Object.entries(value)) {
|
|
41
|
+
hydrated[key] = hydrateValue(val);
|
|
42
|
+
}
|
|
43
|
+
return hydrated;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hydrated = Object.create(Object.getPrototypeOf(value));
|
|
47
|
+
for (const [key, val] of Object.entries(value)) {
|
|
48
|
+
hydrated[key] = hydrateValue(val);
|
|
49
|
+
}
|
|
50
|
+
return hydrated;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
hydrateSecrets: <T>(config: T) =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const parseResult = configSchema.parse(config);
|
|
60
|
+
try {
|
|
61
|
+
return hydrateValue(parseResult) as T;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return yield* Effect.fail(
|
|
64
|
+
new PluginRuntimeError({
|
|
65
|
+
operation: "hydrate-secrets",
|
|
66
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
67
|
+
retryable: false,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
}) {}
|