every-plugin 0.1.3 → 0.1.5
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/client/index.d.ts +18 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +33 -0
- package/dist/effect.d.ts +2 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/orpc.d.ts +3 -0
- package/dist/orpc.d.ts.map +1 -0
- package/dist/orpc.js +2 -0
- package/dist/plugin.d.ts +56 -85
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +33 -103
- package/dist/runtime/errors.d.ts +0 -14
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/errors.js +0 -2
- package/dist/runtime/index.d.ts +40 -18
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +127 -24
- package/dist/runtime/services/module-federation.service.d.ts +2 -1
- package/dist/runtime/services/module-federation.service.d.ts.map +1 -1
- package/dist/runtime/services/module-federation.service.js +21 -23
- package/dist/runtime/services/normalize.d.ts +18 -0
- package/dist/runtime/services/normalize.d.ts.map +1 -0
- package/dist/runtime/services/normalize.js +17 -0
- package/dist/runtime/services/plugin-lifecycle.service.d.ts +17 -0
- package/dist/runtime/services/plugin-lifecycle.service.d.ts.map +1 -0
- package/dist/runtime/services/plugin-lifecycle.service.js +35 -0
- package/dist/runtime/services/plugin-loader.service.d.ts +21 -0
- package/dist/runtime/services/plugin-loader.service.d.ts.map +1 -0
- package/dist/runtime/services/plugin-loader.service.js +83 -0
- package/dist/runtime/services/plugin.service.d.ts +12 -20
- package/dist/runtime/services/plugin.service.d.ts.map +1 -1
- package/dist/runtime/services/plugin.service.js +18 -268
- package/dist/runtime/services/secrets.service.d.ts +2 -3
- package/dist/runtime/services/secrets.service.d.ts.map +1 -1
- package/dist/runtime/services/secrets.service.js +7 -5
- package/dist/runtime/services.d.ts +1 -1
- package/dist/runtime/services.d.ts.map +1 -1
- package/dist/runtime/services.js +1 -1
- package/dist/testing/index.d.ts +11 -16
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +25 -27
- package/dist/testing/mocks/module-federation.service.d.ts +1 -1
- package/dist/testing/mocks/module-federation.service.d.ts.map +1 -1
- package/dist/types.d.ts +177 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/zod.d.ts +2 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +1 -0
- package/package.json +35 -10
- package/dist/runtime/streaming/index.d.ts +0 -4
- package/dist/runtime/streaming/index.d.ts.map +0 -1
- package/dist/runtime/streaming/index.js +0 -2
- package/dist/runtime/streaming/state-effects.d.ts +0 -28
- package/dist/runtime/streaming/state-effects.d.ts.map +0 -1
- package/dist/runtime/streaming/state-effects.js +0 -30
- package/dist/runtime/streaming/streaming.d.ts +0 -8
- package/dist/runtime/streaming/streaming.d.ts.map +0 -1
- package/dist/runtime/streaming/streaming.js +0 -113
- package/dist/runtime/streaming/types.d.ts +0 -19
- package/dist/runtime/streaming/types.d.ts.map +0 -1
- package/dist/runtime/test-utils.d.ts +0 -3
- package/dist/runtime/test-utils.d.ts.map +0 -1
- package/dist/runtime/test-utils.js +0 -7
- package/dist/runtime/types.d.ts +0 -40
- package/dist/runtime/types.d.ts.map +0 -1
- package/dist/runtime/types.js +0 -1
- /package/dist/{runtime/streaming/types.js → types.js} +0 -0
package/dist/runtime/index.js
CHANGED
|
@@ -1,29 +1,132 @@
|
|
|
1
|
-
import { Effect, Layer, ManagedRuntime } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { Context, Effect, Hash, Layer, ManagedRuntime } from "effect";
|
|
2
|
+
import { createPluginClient, getPluginRouter } from "../client/index";
|
|
3
|
+
import { PluginRuntimeError } from "./errors";
|
|
4
|
+
import { SecretsService } from "./services";
|
|
5
|
+
import { ModuleFederationService } from "./services/module-federation.service";
|
|
6
|
+
import { PluginService } from "./services/plugin.service";
|
|
7
|
+
export class PluginRuntimeImpl {
|
|
8
|
+
pluginService;
|
|
9
|
+
registry;
|
|
10
|
+
pluginCache = new Map();
|
|
11
|
+
constructor(pluginService, registry) {
|
|
12
|
+
this.pluginService = pluginService;
|
|
13
|
+
this.registry = registry;
|
|
14
|
+
}
|
|
15
|
+
generateCacheKey(pluginId, config) {
|
|
16
|
+
const configHash = Hash.structure(config).toString();
|
|
17
|
+
return `${pluginId}:${configHash}`;
|
|
18
|
+
}
|
|
19
|
+
validatePluginId(pluginId) {
|
|
20
|
+
const pluginIdStr = pluginId;
|
|
21
|
+
// Runtime validation against registry
|
|
22
|
+
if (!(pluginIdStr in this.registry)) {
|
|
23
|
+
return Effect.fail(new PluginRuntimeError({
|
|
24
|
+
pluginId: pluginIdStr,
|
|
25
|
+
operation: "validate-plugin-id",
|
|
26
|
+
cause: new Error(`Plugin ID '${pluginIdStr}' not found in registry`),
|
|
27
|
+
retryable: false,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
return Effect.succeed(pluginIdStr);
|
|
31
|
+
}
|
|
32
|
+
loadPlugin(pluginId) {
|
|
33
|
+
const self = this;
|
|
34
|
+
return Effect.gen(function* () {
|
|
35
|
+
const validatedId = yield* self.validatePluginId(pluginId);
|
|
36
|
+
const result = yield* self.pluginService.loadPlugin(validatedId);
|
|
37
|
+
return result;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
instantiatePlugin(ctor) {
|
|
41
|
+
const self = this;
|
|
42
|
+
return Effect.gen(function* () {
|
|
43
|
+
const result = yield* self.pluginService.instantiatePlugin(ctor);
|
|
44
|
+
return result;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
initializePlugin(instance, config) {
|
|
48
|
+
const self = this;
|
|
49
|
+
return Effect.gen(function* () {
|
|
50
|
+
const result = yield* self.pluginService.initializePlugin(instance, config);
|
|
51
|
+
return result;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
usePlugin(pluginId, config) {
|
|
55
|
+
const cacheKey = this.generateCacheKey(String(pluginId), config);
|
|
56
|
+
let cachedPlugin = this.pluginCache.get(cacheKey);
|
|
57
|
+
if (!cachedPlugin) {
|
|
58
|
+
const self = this;
|
|
59
|
+
const operation = Effect.gen(function* () {
|
|
60
|
+
const validatedId = yield* self.validatePluginId(pluginId);
|
|
61
|
+
// Load → Instantiate → Initialize
|
|
62
|
+
const ctor = yield* self.pluginService.loadPlugin(validatedId);
|
|
63
|
+
const instance = yield* self.pluginService.instantiatePlugin(ctor);
|
|
64
|
+
const initialized = yield* self.pluginService.initializePlugin(instance, config);
|
|
65
|
+
const client = createPluginClient(initialized);
|
|
66
|
+
const router = getPluginRouter(initialized);
|
|
67
|
+
return {
|
|
68
|
+
client,
|
|
69
|
+
router,
|
|
70
|
+
metadata: initialized.metadata,
|
|
71
|
+
initialized
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
cachedPlugin = Effect.cached(operation).pipe(Effect.flatten);
|
|
75
|
+
this.pluginCache.set(cacheKey, cachedPlugin);
|
|
76
|
+
}
|
|
77
|
+
return cachedPlugin;
|
|
78
|
+
}
|
|
79
|
+
shutdown() {
|
|
80
|
+
return this.pluginService.cleanup();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Evict a plugin from cache and shutdown its instance
|
|
84
|
+
*/
|
|
85
|
+
evictPlugin(pluginId, config) {
|
|
86
|
+
const cacheKey = this.generateCacheKey(String(pluginId), config);
|
|
87
|
+
const self = this;
|
|
88
|
+
return Effect.gen(function* () {
|
|
89
|
+
// Get cached plugin if it exists
|
|
90
|
+
const cachedPlugin = self.pluginCache.get(cacheKey);
|
|
91
|
+
if (cachedPlugin) {
|
|
92
|
+
// Remove from cache first
|
|
93
|
+
self.pluginCache.delete(cacheKey);
|
|
94
|
+
// Try to shutdown the plugin gracefully
|
|
95
|
+
const pluginResult = yield* cachedPlugin.pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
96
|
+
if (pluginResult?.initialized) {
|
|
97
|
+
yield* self.pluginService.shutdownPlugin(pluginResult.initialized).pipe(Effect.catchAll(() => Effect.void) // Ignore shutdown errors during eviction
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}).pipe(Effect.catchAll(() => Effect.void) // Never fail
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export class PluginRuntimeService extends Context.Tag("PluginRuntimeService")() {
|
|
12
106
|
static Live = (config) => {
|
|
13
107
|
const secrets = config.secrets || {};
|
|
14
|
-
|
|
15
|
-
return Layer.effect(PluginRuntime, Effect.gen(function* () {
|
|
108
|
+
return Layer.scoped(PluginRuntimeService, Effect.gen(function* () {
|
|
16
109
|
const pluginService = yield* PluginService;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
110
|
+
// Register cleanup finalizer for automatic resource management
|
|
111
|
+
yield* Effect.addFinalizer(() => pluginService.cleanup().pipe(Effect.catchAll(() => Effect.void) // Ensure cleanup never fails
|
|
112
|
+
));
|
|
113
|
+
return pluginService;
|
|
114
|
+
})).pipe(Layer.provide(PluginService.Live(config.registry, secrets).pipe(Layer.provide(Layer.mergeAll(ModuleFederationService.Live, SecretsService.Live(secrets))))));
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Creates a typed plugin runtime with compile-time registry key validation.
|
|
119
|
+
*/
|
|
120
|
+
export function createPluginRuntime(config) {
|
|
121
|
+
const runtimeConfig = {
|
|
122
|
+
registry: config.registry,
|
|
123
|
+
secrets: config.secrets,
|
|
124
|
+
options: config.options
|
|
27
125
|
};
|
|
126
|
+
const runtime = ManagedRuntime.make(PluginRuntimeService.Live(runtimeConfig));
|
|
127
|
+
const createTypedRuntime = Effect.gen(function* () {
|
|
128
|
+
const pluginService = yield* PluginRuntimeService;
|
|
129
|
+
return new PluginRuntimeImpl(pluginService, runtimeConfig.registry);
|
|
130
|
+
}).pipe(Effect.provide(runtime));
|
|
131
|
+
return { runtime, PluginRuntime: createTypedRuntime };
|
|
28
132
|
}
|
|
29
|
-
export const createPluginRuntime = (config) => ManagedRuntime.make(PluginRuntime.Live(config));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Effect, Layer } from "effect";
|
|
2
|
+
import type { AnyPlugin } from "../../types";
|
|
2
3
|
import { ModuleFederationError } from "../errors";
|
|
3
4
|
export interface IModuleFederationService {
|
|
4
5
|
readonly registerRemote: (pluginId: string, url: string) => Effect.Effect<void, ModuleFederationError>;
|
|
5
|
-
readonly loadRemoteConstructor: (pluginId: string, url: string) => Effect.Effect<
|
|
6
|
+
readonly loadRemoteConstructor: (pluginId: string, url: string) => Effect.Effect<new () => AnyPlugin, ModuleFederationError>;
|
|
6
7
|
}
|
|
7
8
|
declare const ModuleFederationService_base: import("effect/Context").TagClass<ModuleFederationService, "ModuleFederationService", IModuleFederationService> & Effect.Tag.Proxy<ModuleFederationService, IModuleFederationService> & {
|
|
8
9
|
use: <X>(body: (_: IModuleFederationService) => X) => [X] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, ModuleFederationService | R> : [X] extends [PromiseLike<infer A_1>] ? Effect.Effect<A_1, import("effect/Cause").UnknownException, ModuleFederationService> : Effect.Effect<X, never, ModuleFederationService>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module-federation.service.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/module-federation.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AASlD,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,cAAc,EAAE,CACxB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,KACP,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;IAChD,QAAQ,CAAC,qBAAqB,EAAE,CAC/B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,KACP,MAAM,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"module-federation.service.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/module-federation.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AASlD,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,cAAc,EAAE,CACxB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,KACP,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,qBAAqB,CAAC,CAAC;IAChD,QAAQ,CAAC,qBAAqB,EAAE,CAC/B,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,KACP,MAAM,CAAC,MAAM,CAAC,UAAU,SAAS,EAAE,qBAAqB,CAAC,CAAC;CAC/D;;;;AAsED,qBAAa,uBAAwB,SAAQ,4BAES;IACrD,MAAM,CAAC,IAAI,qDAyFT;CACF;AAED,eAAO,MAAM,0BAA0B,gCAA0B,CAAC;AAClE,eAAO,MAAM,2BAA2B,oDAA+B,CAAC"}
|
|
@@ -3,12 +3,8 @@ import { createInstance, getInstance, } from "@module-federation/enhanced/runtim
|
|
|
3
3
|
import { setGlobalFederationInstance } from "@module-federation/runtime-core";
|
|
4
4
|
import { Effect, Layer } from "effect";
|
|
5
5
|
import { ModuleFederationError } from "../errors";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
.toLowerCase()
|
|
9
|
-
.replace(/^@/, "")
|
|
10
|
-
.replace(/\//g, "_");
|
|
11
|
-
};
|
|
6
|
+
import { getNormalizedRemoteName } from "./normalize";
|
|
7
|
+
const pkg = require("../../../package.json");
|
|
12
8
|
const createModuleFederationInstance = Effect.cached(Effect.sync(() => {
|
|
13
9
|
try {
|
|
14
10
|
let instance = getInstance();
|
|
@@ -18,43 +14,48 @@ const createModuleFederationInstance = Effect.cached(Effect.sync(() => {
|
|
|
18
14
|
remotes: [],
|
|
19
15
|
shared: {
|
|
20
16
|
"every-plugin": {
|
|
17
|
+
version: pkg.version,
|
|
21
18
|
shareConfig: {
|
|
22
19
|
singleton: true,
|
|
23
|
-
requiredVersion:
|
|
20
|
+
requiredVersion: `^${pkg.version}`,
|
|
24
21
|
eager: true,
|
|
25
|
-
strictVersion:
|
|
22
|
+
strictVersion: true,
|
|
26
23
|
},
|
|
27
24
|
},
|
|
28
25
|
effect: {
|
|
26
|
+
version: pkg.dependencies.effect,
|
|
29
27
|
shareConfig: {
|
|
30
28
|
singleton: true,
|
|
31
|
-
requiredVersion:
|
|
29
|
+
requiredVersion: "^3.17.0", // Allow any 3.17.x patch version
|
|
32
30
|
eager: true,
|
|
33
|
-
strictVersion:
|
|
31
|
+
strictVersion: true, // Enforce major/minor compatibility
|
|
34
32
|
},
|
|
35
33
|
},
|
|
36
34
|
zod: {
|
|
35
|
+
version: pkg.dependencies.zod,
|
|
37
36
|
shareConfig: {
|
|
38
37
|
singleton: true,
|
|
39
|
-
requiredVersion:
|
|
38
|
+
requiredVersion: "^4.1.0", // Allow any 4.1.x patch version
|
|
40
39
|
eager: true,
|
|
41
|
-
strictVersion:
|
|
40
|
+
strictVersion: true,
|
|
42
41
|
},
|
|
43
42
|
},
|
|
44
43
|
"@orpc/contract": {
|
|
44
|
+
version: pkg.dependencies["@orpc/contract"],
|
|
45
45
|
shareConfig: {
|
|
46
46
|
singleton: true,
|
|
47
|
-
requiredVersion:
|
|
47
|
+
requiredVersion: "^1.8.0", // Allow any 1.8.x patch version
|
|
48
48
|
eager: true,
|
|
49
|
-
strictVersion:
|
|
49
|
+
strictVersion: true,
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
"@orpc/server": {
|
|
53
|
+
version: pkg.dependencies["@orpc/server"],
|
|
53
54
|
shareConfig: {
|
|
54
55
|
singleton: true,
|
|
55
|
-
requiredVersion:
|
|
56
|
+
requiredVersion: "^1.8.0", // Allow any 1.8.x patch version
|
|
56
57
|
eager: true,
|
|
57
|
-
strictVersion:
|
|
58
|
+
strictVersion: true,
|
|
58
59
|
},
|
|
59
60
|
}
|
|
60
61
|
},
|
|
@@ -90,11 +91,8 @@ export class ModuleFederationService extends Effect.Tag("ModuleFederationService
|
|
|
90
91
|
cause: new Error(`Remote not available (${url}): ${JSON.stringify(error)}`),
|
|
91
92
|
}));
|
|
92
93
|
}
|
|
93
|
-
// Register remote
|
|
94
|
-
const remoteName = pluginId
|
|
95
|
-
.toLowerCase()
|
|
96
|
-
.replace(/^@/, "")
|
|
97
|
-
.replace(/\//g, "_");
|
|
94
|
+
// Register remote using normalized name
|
|
95
|
+
const remoteName = getNormalizedRemoteName(pluginId);
|
|
98
96
|
yield* Effect.try({
|
|
99
97
|
try: () => mf.registerRemotes([{ name: remoteName, entry: url }]),
|
|
100
98
|
catch: (error) => new ModuleFederationError({
|
|
@@ -106,7 +104,7 @@ export class ModuleFederationService extends Effect.Tag("ModuleFederationService
|
|
|
106
104
|
console.log(`[MF] ✅ Registered ${pluginId}`);
|
|
107
105
|
}),
|
|
108
106
|
loadRemoteConstructor: (pluginId, url) => Effect.gen(function* () {
|
|
109
|
-
const remoteName =
|
|
107
|
+
const remoteName = getNormalizedRemoteName(pluginId);
|
|
110
108
|
console.log(`[MF] Loading remote ${remoteName}`);
|
|
111
109
|
const modulePath = `${remoteName}/plugin`;
|
|
112
110
|
return yield* Effect.tryPromise({
|
|
@@ -117,7 +115,7 @@ export class ModuleFederationService extends Effect.Tag("ModuleFederationService
|
|
|
117
115
|
}
|
|
118
116
|
const Constructor = typeof container === "function"
|
|
119
117
|
? container
|
|
120
|
-
: container
|
|
118
|
+
: container.default;
|
|
121
119
|
if (!Constructor || typeof Constructor !== "function") {
|
|
122
120
|
throw new Error(`No valid constructor found. Container type: ${typeof container}`);
|
|
123
121
|
}
|
|
@@ -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 declare const getNormalizedRemoteName: (name: string) => string;
|
|
18
|
+
//# sourceMappingURL=normalize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/normalize.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,MACG,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
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) => name.toLowerCase().replace(/^@/, "").replace(/\//g, "_");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import type { AnyPlugin, InitializedPlugin } from "../../types";
|
|
3
|
+
import { type PluginRuntimeError } from "../errors";
|
|
4
|
+
export interface IPluginLifecycleService {
|
|
5
|
+
readonly register: <T extends AnyPlugin>(plugin: InitializedPlugin<T>) => Effect.Effect<void, never>;
|
|
6
|
+
readonly unregister: (plugin: InitializedPlugin<AnyPlugin>) => Effect.Effect<void, never>;
|
|
7
|
+
readonly shutdown: (plugin: InitializedPlugin<AnyPlugin>) => Effect.Effect<void, PluginRuntimeError>;
|
|
8
|
+
readonly cleanup: () => Effect.Effect<void, never>;
|
|
9
|
+
}
|
|
10
|
+
declare const PluginLifecycleService_base: Context.TagClass<PluginLifecycleService, "PluginLifecycleService", IPluginLifecycleService>;
|
|
11
|
+
export declare class PluginLifecycleService extends PluginLifecycleService_base {
|
|
12
|
+
static Live: Layer.Layer<PluginLifecycleService, never, never>;
|
|
13
|
+
}
|
|
14
|
+
export declare const PluginLifecycleServiceTag: typeof PluginLifecycleService;
|
|
15
|
+
export declare const PluginLifecycleServiceLive: Layer.Layer<PluginLifecycleService, never, never>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=plugin-lifecycle.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-lifecycle.service.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/plugin-lifecycle.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAO,MAAM,QAAQ,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,KAAK,kBAAkB,EAAwB,MAAM,WAAW,CAAC;AAE1E,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,SAAS,SAAS,EACrC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,CACnB,MAAM,EAAE,iBAAiB,CAAC,SAAS,CAAC,KACjC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CACjB,MAAM,EAAE,iBAAiB,CAAC,SAAS,CAAC,KACjC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;CACpD;;AAED,qBAAa,sBAAuB,SAAQ,2BAGzC;IACD,MAAM,CAAC,IAAI,oDAiET;CACH;AAED,eAAO,MAAM,yBAAyB,+BAAyB,CAAC;AAChE,eAAO,MAAM,0BAA0B,mDAA8B,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Ref } from "effect";
|
|
2
|
+
import { toPluginRuntimeError } from "../errors";
|
|
3
|
+
export class PluginLifecycleService extends Context.Tag("PluginLifecycleService")() {
|
|
4
|
+
static Live = Layer.effect(PluginLifecycleService, Effect.gen(function* () {
|
|
5
|
+
const activePlugins = yield* Ref.make(new Set());
|
|
6
|
+
return {
|
|
7
|
+
register: (plugin) => Ref.update(activePlugins, plugins => new Set(plugins).add(plugin)),
|
|
8
|
+
unregister: (plugin) => Ref.update(activePlugins, plugins => {
|
|
9
|
+
const newSet = new Set(plugins);
|
|
10
|
+
newSet.delete(plugin);
|
|
11
|
+
return newSet;
|
|
12
|
+
}),
|
|
13
|
+
shutdown: (plugin) => Effect.gen(function* () {
|
|
14
|
+
// Remove from active plugins
|
|
15
|
+
yield* Ref.update(activePlugins, plugins => {
|
|
16
|
+
const newSet = new Set(plugins);
|
|
17
|
+
newSet.delete(plugin);
|
|
18
|
+
return newSet;
|
|
19
|
+
});
|
|
20
|
+
// Shutdown the plugin
|
|
21
|
+
yield* plugin.plugin.shutdown().pipe(Effect.mapError(error => toPluginRuntimeError(error, plugin.plugin.id, undefined, "shutdown-plugin", false)));
|
|
22
|
+
}),
|
|
23
|
+
cleanup: () => Effect.gen(function* () {
|
|
24
|
+
const plugins = yield* Ref.get(activePlugins);
|
|
25
|
+
// Shutdown all active plugins concurrently
|
|
26
|
+
yield* Effect.forEach(plugins, plugin => plugin.plugin.shutdown().pipe(Effect.mapError(error => toPluginRuntimeError(error, plugin.plugin.id, undefined, "cleanup-shutdown", false)), Effect.catchAll(error => Effect.logError(`Failed to shutdown plugin ${plugin.plugin.id} during cleanup`, error, { pluginId: plugin.plugin.id }))), { concurrency: "unbounded" });
|
|
27
|
+
// Clear the active plugins set
|
|
28
|
+
yield* Ref.set(activePlugins, new Set());
|
|
29
|
+
}).pipe(Effect.catchAll(() => Effect.void) // Convert errors to void for cleanup
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
export const PluginLifecycleServiceTag = PluginLifecycleService;
|
|
35
|
+
export const PluginLifecycleServiceLive = PluginLifecycleService.Live;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import type { AnyPlugin, InitializedPlugin, PluginConstructor, PluginInstance, PluginRegistry, SecretsConfig } from "../../types";
|
|
3
|
+
import { PluginRuntimeError } from "../errors";
|
|
4
|
+
import { ModuleFederationService } from "./module-federation.service";
|
|
5
|
+
import { SecretsService } from "./secrets.service";
|
|
6
|
+
export interface IPluginLoaderService {
|
|
7
|
+
readonly loadPlugin: (pluginId: string) => Effect.Effect<PluginConstructor<AnyPlugin>, PluginRuntimeError>;
|
|
8
|
+
readonly instantiatePlugin: <T extends AnyPlugin>(pluginConstructor: PluginConstructor<T>) => Effect.Effect<PluginInstance<T>, PluginRuntimeError>;
|
|
9
|
+
readonly initializePlugin: <T extends AnyPlugin>(pluginInstance: PluginInstance<T>, config: {
|
|
10
|
+
variables: unknown;
|
|
11
|
+
secrets: unknown;
|
|
12
|
+
}) => Effect.Effect<InitializedPlugin<T>, PluginRuntimeError>;
|
|
13
|
+
}
|
|
14
|
+
declare const PluginLoaderService_base: Context.TagClass<PluginLoaderService, "PluginLoaderService", IPluginLoaderService>;
|
|
15
|
+
export declare class PluginLoaderService extends PluginLoaderService_base {
|
|
16
|
+
static Live: (registry: PluginRegistry, secrets: SecretsConfig) => Layer.Layer<PluginLoaderService, never, ModuleFederationService | SecretsService>;
|
|
17
|
+
}
|
|
18
|
+
export declare const PluginLoaderServiceTag: typeof PluginLoaderService;
|
|
19
|
+
export declare const PluginLoaderServiceLive: (registry: PluginRegistry, secrets: SecretsConfig) => Layer.Layer<PluginLoaderService, never, ModuleFederationService | SecretsService>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=plugin-loader.service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-loader.service.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/plugin-loader.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAS,MAAM,QAAQ,CAAC;AAEvD,OAAO,KAAK,EACX,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,aAAa,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAwB,MAAM,WAAW,CAAC;AAErE,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,UAAU,EAAE,CACpB,QAAQ,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACrE,QAAQ,CAAC,iBAAiB,EAAE,CAAC,CAAC,SAAS,SAAS,EAC/C,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC,KACnC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC1D,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC,SAAS,SAAS,EAC9C,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,EACjC,MAAM,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAC5C,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;CAC7D;;AAED,qBAAa,mBAAoB,SAAQ,wBAGtC;IACF,MAAM,CAAC,IAAI,GAAI,UAAU,cAAc,EAAE,SAAS,aAAa,uFA4J5D;CACH;AAED,eAAO,MAAM,sBAAsB,4BAAsB,CAAC;AAC1D,eAAO,MAAM,uBAAuB,aAhKV,cAAc,WAAW,aAAa,sFAgKD,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Scope } from "effect";
|
|
2
|
+
import { PluginRuntimeError, toPluginRuntimeError } from "../errors";
|
|
3
|
+
import { validate } from "../validation";
|
|
4
|
+
import { ModuleFederationService } from "./module-federation.service";
|
|
5
|
+
import { SecretsService } from "./secrets.service";
|
|
6
|
+
export class PluginLoaderService extends Context.Tag("PluginLoaderService")() {
|
|
7
|
+
static Live = (registry, secrets) => Layer.effect(PluginLoaderService, Effect.gen(function* () {
|
|
8
|
+
const moduleFederationService = yield* ModuleFederationService;
|
|
9
|
+
const secretsService = yield* SecretsService;
|
|
10
|
+
const resolveUrl = (baseUrl, version) => version && version !== "latest"
|
|
11
|
+
? baseUrl.replace("@latest", `@${version}`)
|
|
12
|
+
: baseUrl;
|
|
13
|
+
const getMetadata = (pluginId) => Effect.fromNullable(registry[pluginId]).pipe(Effect.mapError(() => new PluginRuntimeError({
|
|
14
|
+
pluginId,
|
|
15
|
+
operation: "load-plugin",
|
|
16
|
+
cause: new Error(`Plugin ${pluginId} not found in registry`),
|
|
17
|
+
retryable: false,
|
|
18
|
+
})));
|
|
19
|
+
return {
|
|
20
|
+
loadPlugin: (pluginId) => Effect.gen(function* () {
|
|
21
|
+
const metadata = yield* getMetadata(pluginId);
|
|
22
|
+
const url = resolveUrl(metadata.remoteUrl);
|
|
23
|
+
yield* moduleFederationService.registerRemote(pluginId, url).pipe(Effect.mapError((error) => toPluginRuntimeError(error.cause, pluginId, undefined, "register-remote", true)));
|
|
24
|
+
yield* Effect.logDebug("Loading plugin", { pluginId, url });
|
|
25
|
+
const ctor = yield* moduleFederationService.loadRemoteConstructor(pluginId, url).pipe(Effect.mapError((error) => toPluginRuntimeError(error.cause, pluginId, undefined, "load-remote", false)));
|
|
26
|
+
return {
|
|
27
|
+
ctor,
|
|
28
|
+
metadata: {
|
|
29
|
+
pluginId,
|
|
30
|
+
version: metadata.version,
|
|
31
|
+
type: metadata.type,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
35
|
+
instantiatePlugin: (pluginConstructor) => Effect.gen(function* () {
|
|
36
|
+
const instance = yield* Effect.try(() => new pluginConstructor.ctor()).pipe(Effect.mapError((error) => toPluginRuntimeError(error, pluginConstructor.metadata.pluginId, undefined, "instantiate-plugin", false)));
|
|
37
|
+
// Validate plugin ID matches
|
|
38
|
+
if (instance.id !== pluginConstructor.metadata.pluginId) {
|
|
39
|
+
return yield* Effect.fail(new PluginRuntimeError({
|
|
40
|
+
pluginId: pluginConstructor.metadata.pluginId,
|
|
41
|
+
operation: "validate-plugin-id",
|
|
42
|
+
cause: new Error(`Plugin ID mismatch: expected ${pluginConstructor.metadata.pluginId}, got ${instance.id}`),
|
|
43
|
+
retryable: false,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
plugin: instance,
|
|
48
|
+
metadata: pluginConstructor.metadata,
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
initializePlugin: (pluginInstance, config) => Effect.gen(function* () {
|
|
52
|
+
const { plugin } = pluginInstance;
|
|
53
|
+
// Validate and hydrate config
|
|
54
|
+
const validatedConfig = yield* validate(plugin.configSchema, config, plugin.id, "config").pipe(Effect.mapError((validationError) => new PluginRuntimeError({
|
|
55
|
+
pluginId: plugin.id,
|
|
56
|
+
operation: "validate-config",
|
|
57
|
+
cause: validationError.zodError,
|
|
58
|
+
retryable: false,
|
|
59
|
+
})));
|
|
60
|
+
const hydratedConfig = yield* secretsService.hydrateSecrets(validatedConfig, secrets);
|
|
61
|
+
const finalConfig = yield* validate(plugin.configSchema, hydratedConfig, plugin.id, "config").pipe(Effect.mapError((validationError) => new PluginRuntimeError({
|
|
62
|
+
pluginId: plugin.id,
|
|
63
|
+
operation: "validate-hydrated-config",
|
|
64
|
+
cause: validationError.zodError,
|
|
65
|
+
retryable: false,
|
|
66
|
+
})));
|
|
67
|
+
// Create a long-lived scope for this plugin instance
|
|
68
|
+
const scope = yield* Scope.make();
|
|
69
|
+
// Initialize plugin within the scope
|
|
70
|
+
const context = yield* plugin.initialize(finalConfig).pipe(Effect.provideService(Scope.Scope, scope), Effect.mapError((error) => toPluginRuntimeError(error, plugin.id, undefined, "initialize-plugin", false)));
|
|
71
|
+
return {
|
|
72
|
+
plugin,
|
|
73
|
+
metadata: pluginInstance.metadata,
|
|
74
|
+
config: finalConfig,
|
|
75
|
+
context,
|
|
76
|
+
scope,
|
|
77
|
+
};
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
export const PluginLoaderServiceTag = PluginLoaderService;
|
|
83
|
+
export const PluginLoaderServiceLive = PluginLoaderService.Live;
|
|
@@ -1,29 +1,21 @@
|
|
|
1
|
-
import { Effect, Layer
|
|
2
|
-
import type {
|
|
3
|
-
import
|
|
4
|
-
import { PluginRuntimeError } from "../errors";
|
|
5
|
-
import { type StreamingOptions } from "../streaming";
|
|
6
|
-
import type { AnyPlugin, InitializedPlugin, PluginConstructor, PluginInstance, SecretsConfig } from "../types";
|
|
7
|
-
import { ModuleFederationService } from "./module-federation.service";
|
|
8
|
-
import { SecretsService } from "./secrets.service";
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import type { AnyPlugin, InitializedPlugin, PluginConstructor, PluginInstance, PluginRegistry, SecretsConfig } from "../../types";
|
|
3
|
+
import type { PluginRuntimeError } from "../errors";
|
|
9
4
|
export interface IPluginService {
|
|
10
|
-
readonly loadPlugin: (pluginId: string) => Effect.Effect<PluginConstructor
|
|
11
|
-
readonly instantiatePlugin: <T extends AnyPlugin>(pluginConstructor: PluginConstructor) => Effect.Effect<PluginInstance<T>, PluginRuntimeError>;
|
|
12
|
-
readonly initializePlugin: <T extends AnyPlugin>(pluginInstance: PluginInstance<T>, config:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
readonly loadPlugin: (pluginId: string) => Effect.Effect<PluginConstructor<AnyPlugin>, PluginRuntimeError>;
|
|
6
|
+
readonly instantiatePlugin: <T extends AnyPlugin>(pluginConstructor: PluginConstructor<T>) => Effect.Effect<PluginInstance<T>, PluginRuntimeError>;
|
|
7
|
+
readonly initializePlugin: <T extends AnyPlugin>(pluginInstance: PluginInstance<T>, config: {
|
|
8
|
+
variables: unknown;
|
|
9
|
+
secrets: unknown;
|
|
10
|
+
}) => Effect.Effect<InitializedPlugin<T>, PluginRuntimeError>;
|
|
16
11
|
readonly shutdownPlugin: (plugin: InitializedPlugin<AnyPlugin>) => Effect.Effect<void, PluginRuntimeError>;
|
|
17
|
-
readonly clearCache: () => Effect.Effect<void, never>;
|
|
18
12
|
readonly cleanup: () => Effect.Effect<void, never>;
|
|
19
13
|
}
|
|
20
|
-
declare const PluginService_base:
|
|
21
|
-
use: <X>(body: (_: IPluginService) => X) => [X] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, R | PluginService> : [X] extends [PromiseLike<infer A_1>] ? Effect.Effect<A_1, import("effect/Cause").UnknownException, PluginService> : Effect.Effect<X, never, PluginService>;
|
|
22
|
-
};
|
|
14
|
+
declare const PluginService_base: Context.TagClass<PluginService, "PluginService", IPluginService>;
|
|
23
15
|
export declare class PluginService extends PluginService_base {
|
|
24
|
-
static Live: (registry: PluginRegistry, secrets: SecretsConfig) => Layer.Layer<PluginService, never,
|
|
16
|
+
static Live: (registry: PluginRegistry, secrets: SecretsConfig) => Layer.Layer<PluginService, never, import("./module-federation.service").ModuleFederationService | import("./secrets.service").SecretsService>;
|
|
25
17
|
}
|
|
26
18
|
export declare const PluginServiceTag: typeof PluginService;
|
|
27
|
-
export declare const PluginServiceLive: (registry: PluginRegistry, secrets: SecretsConfig) => Layer.Layer<PluginService, never,
|
|
19
|
+
export declare const PluginServiceLive: (registry: PluginRegistry, secrets: SecretsConfig) => Layer.Layer<PluginService, never, import("./module-federation.service").ModuleFederationService | import("./secrets.service").SecretsService>;
|
|
28
20
|
export {};
|
|
29
21
|
//# sourceMappingURL=plugin.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.service.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/plugin.service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugin.service.d.ts","sourceRoot":"","sources":["../../../src/runtime/services/plugin.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAQ,KAAK,EAAS,MAAM,QAAQ,CAAC;AAC7D,OAAO,KAAK,EACX,SAAS,EACT,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,aAAa,EACb,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAIpD,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,UAAU,EAAE,CACpB,QAAQ,EAAE,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,CAAC;IACrE,QAAQ,CAAC,iBAAiB,EAAE,CAAC,CAAC,SAAS,SAAS,EAC/C,iBAAiB,EAAE,iBAAiB,CAAC,CAAC,CAAC,KACnC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC1D,QAAQ,CAAC,gBAAgB,EAAE,CAAC,CAAC,SAAS,SAAS,EAC9C,cAAc,EAAE,cAAc,CAAC,CAAC,CAAC,EACjC,MAAM,EAAE;QAAE,SAAS,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,KAC5C,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;IAC7D,QAAQ,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,iBAAiB,CAAC,SAAS,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;IAC3G,QAAQ,CAAC,OAAO,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;CACnD;;AAED,qBAAa,aAAc,SAAQ,kBAGhC;IACF,MAAM,CAAC,IAAI,GAAI,UAAU,cAAc,EAAE,SAAS,aAAa,mJAkC5D;CACH;AAED,eAAO,MAAM,gBAAgB,sBAAgB,CAAC;AAC9C,eAAO,MAAM,iBAAiB,aAtCJ,cAAc,WAAW,aAAa,kJAsCb,CAAC"}
|