every-plugin 3.0.0-rc.0 → 3.0.0-rc.11
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/build/rspack/chunk-completeness-plugin.cjs +19 -7
- package/dist/build/rspack/chunk-completeness-plugin.cjs.map +1 -1
- package/dist/build/rspack/chunk-completeness-plugin.mjs +19 -7
- package/dist/build/rspack/chunk-completeness-plugin.mjs.map +1 -1
- package/dist/build/ui/generated-config.cjs +4 -0
- package/dist/build/ui/generated-config.cjs.map +1 -1
- package/dist/build/ui/generated-config.d.cts.map +1 -1
- package/dist/build/ui/generated-config.d.mts.map +1 -1
- package/dist/build/ui/generated-config.mjs +4 -1
- package/dist/build/ui/generated-config.mjs.map +1 -1
- package/dist/build/ui/rsbuild-config.cjs +1 -0
- package/dist/build/ui/rsbuild-config.cjs.map +1 -1
- package/dist/build/ui/rsbuild-config.mjs +1 -0
- package/dist/build/ui/rsbuild-config.mjs.map +1 -1
- package/dist/client.cjs +69 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +15 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +15 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +68 -0
- package/dist/client.mjs.map +1 -0
- package/dist/dev/host.cjs +149 -0
- package/dist/dev/host.cjs.map +1 -0
- package/dist/dev/host.d.cts +32 -0
- package/dist/dev/host.d.cts.map +1 -0
- package/dist/dev/host.d.mts +32 -0
- package/dist/dev/host.d.mts.map +1 -0
- package/dist/dev/host.mjs +148 -0
- package/dist/dev/host.mjs.map +1 -0
- package/dist/dev/serve.cjs +16 -111
- package/dist/dev/serve.cjs.map +1 -1
- package/dist/dev/serve.d.cts.map +1 -1
- package/dist/dev/serve.d.mts.map +1 -1
- package/dist/dev/serve.mjs +17 -112
- package/dist/dev/serve.mjs.map +1 -1
- package/dist/errors.cjs +1 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.cts +2 -2
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +2 -2
- package/dist/errors.mjs.map +1 -1
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +5 -4
- package/dist/index.d.mts +5 -4
- package/dist/index.mjs +3 -2
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.cjs.map +1 -1
- package/dist/plugin.d.cts +23 -3
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.mts +22 -2
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/remote-entry.cjs +0 -1
- package/dist/remote-entry.cjs.map +1 -1
- package/dist/remote-entry.d.cts +1 -2
- package/dist/remote-entry.d.cts.map +1 -1
- package/dist/remote-entry.d.mts +1 -2
- package/dist/remote-entry.d.mts.map +1 -1
- package/dist/remote-entry.mjs +1 -1
- package/dist/remote-entry.mjs.map +1 -1
- package/dist/runtime/errors.cjs +20 -0
- package/dist/runtime/errors.cjs.map +1 -1
- package/dist/runtime/errors.d.cts +23 -1
- package/dist/runtime/errors.d.cts.map +1 -1
- package/dist/runtime/errors.d.mts +23 -1
- package/dist/runtime/errors.d.mts.map +1 -1
- package/dist/runtime/errors.mjs +20 -1
- package/dist/runtime/errors.mjs.map +1 -1
- package/dist/runtime/mf-config.cjs +1 -1
- package/dist/runtime/mf-config.mjs +1 -1
- package/dist/runtime/services/module-federation.service.cjs +1 -7
- package/dist/runtime/services/module-federation.service.cjs.map +1 -1
- package/dist/runtime/services/module-federation.service.mjs +1 -7
- package/dist/runtime/services/module-federation.service.mjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.cjs +29 -1
- package/dist/runtime/services/plugin-loader.service.cjs.map +1 -1
- package/dist/runtime/services/plugin-loader.service.d.cts +2 -2
- package/dist/runtime/services/plugin-loader.service.mjs +29 -1
- package/dist/runtime/services/plugin-loader.service.mjs.map +1 -1
- package/dist/runtime/services/shared-identity.cjs +1 -1
- package/dist/runtime/services/shared-identity.cjs.map +1 -1
- package/dist/runtime/services/shared-identity.mjs +1 -1
- package/dist/runtime/services/shared-identity.mjs.map +1 -1
- package/dist/types.d.cts +27 -2
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +26 -1
- package/dist/types.d.mts.map +1 -1
- package/dist/ui/manifest/construct.cjs +69 -18
- package/dist/ui/manifest/construct.cjs.map +1 -1
- package/dist/ui/manifest/construct.d.cts.map +1 -1
- package/dist/ui/manifest/construct.d.mts.map +1 -1
- package/dist/ui/manifest/construct.mjs +69 -18
- package/dist/ui/manifest/construct.mjs.map +1 -1
- package/dist/ui/manifest/generator.cjs +1 -1
- package/dist/ui/manifest/generator.mjs +1 -1
- package/dist/ui/manifest/index.cjs +1 -1
- package/dist/ui/manifest/index.mjs +1 -1
- package/dist/version-manifest.cjs +14 -2
- package/dist/version-manifest.cjs.map +1 -1
- package/dist/version-manifest.d.cts +11 -1
- package/dist/version-manifest.d.cts.map +1 -1
- package/dist/version-manifest.d.mts +11 -1
- package/dist/version-manifest.d.mts.map +1 -1
- package/dist/version-manifest.mjs +14 -2
- package/dist/version-manifest.mjs.map +1 -1
- package/package.json +20 -3
- package/skills/plugin-client/SKILL.md +10 -4
- package/skills/plugin-testing/SKILL.md +2 -1
- package/src/build/rspack/chunk-completeness-plugin.ts +15 -6
- package/src/build/ui/generated-config.ts +4 -0
- package/src/build/ui/rsbuild-config.ts +1 -0
- package/src/client.ts +111 -0
- package/src/dev/host.ts +235 -0
- package/src/dev/serve.ts +22 -140
- package/src/errors.ts +2 -1
- package/src/index.ts +9 -0
- package/src/plugin.ts +26 -1
- package/src/remote-entry.ts +1 -2
- package/src/runtime/errors.ts +44 -0
- package/src/runtime/services/module-federation.service.ts +4 -7
- package/src/runtime/services/plugin-loader.service.ts +46 -2
- package/src/runtime/services/shared-identity.ts +1 -1
- package/src/types.ts +40 -2
- package/src/ui/manifest/construct.ts +68 -15
- package/src/version-manifest.ts +19 -1
package/src/plugin.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ContractedRouter, Implementer } from "@orpc/server";
|
|
|
5
5
|
import { implement } from "@orpc/server";
|
|
6
6
|
import type { Scope } from "effect";
|
|
7
7
|
import { Context, Effect, Layer } from "effect";
|
|
8
|
+
import type { PluginWarningInfo } from "./runtime/errors";
|
|
8
9
|
import { type PluginRuntimeError, toPluginRuntimeError } from "./runtime/errors";
|
|
9
10
|
|
|
10
11
|
type ContextOutput<T> = T extends AnySchema ? InferSchemaOutput<T> : Record<string, never>;
|
|
@@ -19,11 +20,26 @@ export type PluginConfigFor<
|
|
|
19
20
|
context: TRequestContext;
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
23
|
+
export type PluginInitializeInput<V extends AnySchema, S extends AnySchema> = {
|
|
24
|
+
/**
|
|
25
|
+
* The framework-assigned plugin id — the same key the host uses for secret
|
|
26
|
+
* injection and per-plugin database namespacing. Arrives as a value so
|
|
27
|
+
* plugin code never needs the ambient `PluginIdTag`.
|
|
28
|
+
*/
|
|
29
|
+
pluginId: string;
|
|
23
30
|
variables: InferSchemaOutput<V>;
|
|
24
31
|
secrets: InferSchemaOutput<S>;
|
|
25
32
|
};
|
|
26
33
|
|
|
34
|
+
/**
|
|
35
|
+
* A plugin's own config-derived warnings, evaluated once with the validated
|
|
36
|
+
* config after initialization. Declaring `warnings` opts the plugin out of
|
|
37
|
+
* the loader's default missing-secret warning — these messages replace it.
|
|
38
|
+
*/
|
|
39
|
+
export type PluginWarningsFn<V extends AnySchema, S extends AnySchema> = (
|
|
40
|
+
config: PluginInitializeInput<V, S>,
|
|
41
|
+
) => PluginWarningInfo[] | string[];
|
|
42
|
+
|
|
27
43
|
export class PluginIdTag extends Context.Service<PluginIdTag, string>()("PluginId") {}
|
|
28
44
|
|
|
29
45
|
/**
|
|
@@ -50,6 +66,13 @@ type PluginDefinition<
|
|
|
50
66
|
* module statically.
|
|
51
67
|
*/
|
|
52
68
|
servicesTag?: Context.Key<any, any>;
|
|
69
|
+
/**
|
|
70
|
+
* Optional warnings function evaluated once with the validated config after
|
|
71
|
+
* initialization. Returns structured non-fatal warnings (e.g. an optional
|
|
72
|
+
* secret was not set, so the feature depending on it is disabled).
|
|
73
|
+
* Declaring it opts out of the loader's default missing-secret warning.
|
|
74
|
+
*/
|
|
75
|
+
warnings?: PluginWarningsFn<V, S>;
|
|
53
76
|
/**
|
|
54
77
|
* Initialize the plugin by returning an Effect `Layer`.
|
|
55
78
|
*
|
|
@@ -107,6 +130,7 @@ export interface Plugin<
|
|
|
107
130
|
readonly contract: TContract;
|
|
108
131
|
readonly configSchema: PluginConfigFor<TVariables, TSecrets, TRequestContext>;
|
|
109
132
|
readonly servicesTag?: Context.Key<any, any>;
|
|
133
|
+
readonly warnings?: PluginWarningsFn<TVariables, TSecrets>;
|
|
110
134
|
|
|
111
135
|
initialize(
|
|
112
136
|
config: PluginInitializeInput<TVariables, TSecrets>,
|
|
@@ -156,6 +180,7 @@ export const createPlugin: CreatePluginFn = function createPlugin<
|
|
|
156
180
|
readonly contract = config.contract;
|
|
157
181
|
readonly configSchema = configSchema;
|
|
158
182
|
readonly servicesTag = config.servicesTag;
|
|
183
|
+
readonly warnings = config.warnings;
|
|
159
184
|
|
|
160
185
|
initialize(
|
|
161
186
|
pluginConfig: PluginInitializeInput<V, S>,
|
package/src/remote-entry.ts
CHANGED
|
@@ -23,8 +23,7 @@ const DEFAULT_TIMEOUT_MS = 120_000;
|
|
|
23
23
|
const RETRY_BASE_DELAY_MS = 500;
|
|
24
24
|
const RETRY_MAX_DELAY_MS = 5_000;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
new Promise((resolve) => setTimeout(resolve, ms));
|
|
26
|
+
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
27
|
|
|
29
28
|
/**
|
|
30
29
|
* Deletes the failed remote's keys from the process-global entry-load cache.
|
package/src/runtime/errors.ts
CHANGED
|
@@ -282,6 +282,50 @@ export interface PluginLoadFailureInfo {
|
|
|
282
282
|
dbUrlMasked?: string;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Structured, serializable warning surfaced alongside plugin failures in
|
|
287
|
+
* health endpoints and dashboards. Warnings are non-fatal: the plugin loaded
|
|
288
|
+
* and initialized successfully, but runs with reduced functionality (e.g. an
|
|
289
|
+
* optional secret was not set, so the feature depending on it is disabled).
|
|
290
|
+
*/
|
|
291
|
+
export interface PluginWarningInfo {
|
|
292
|
+
/** loader-side key (host entry key, dev plugin id) */
|
|
293
|
+
pluginKey: string;
|
|
294
|
+
message: string;
|
|
295
|
+
hint?: string;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* The loader's default warning policy for declared-but-unset secrets: a
|
|
300
|
+
* successfully initialized plugin whose declared secret names resolved to
|
|
301
|
+
* nothing (missing or empty in the environment) gets one consolidated
|
|
302
|
+
* warning. `*_DATABASE_URL` keys are excluded — they have the shared-API-DB
|
|
303
|
+
* fallback, and a genuinely-missing DB URL fails initialization (a `failure`,
|
|
304
|
+
* not a warning). A plugin that declares its own `warnings` function opts out
|
|
305
|
+
* of this default.
|
|
306
|
+
*/
|
|
307
|
+
export const defaultMissingSecretWarnings = (
|
|
308
|
+
pluginKey: string,
|
|
309
|
+
declaredKeys: readonly string[],
|
|
310
|
+
resolvedKeys: readonly string[],
|
|
311
|
+
): PluginWarningInfo[] => {
|
|
312
|
+
const resolved = new Set(resolvedKeys);
|
|
313
|
+
const missing = declaredKeys.filter(
|
|
314
|
+
(key) => !resolved.has(key) && !key.endsWith("_DATABASE_URL"),
|
|
315
|
+
);
|
|
316
|
+
if (missing.length === 0) return [];
|
|
317
|
+
return [
|
|
318
|
+
{
|
|
319
|
+
pluginKey,
|
|
320
|
+
message:
|
|
321
|
+
missing.length === 1
|
|
322
|
+
? `Declared secret ${missing[0]} is not set — features depending on it are disabled`
|
|
323
|
+
: `${missing.length} declared secrets are not set: ${missing.join(", ")} — features depending on them are disabled`,
|
|
324
|
+
hint: "Set the secret(s) in your environment (.env in development); see the plugin's config schema for what each enables",
|
|
325
|
+
},
|
|
326
|
+
];
|
|
327
|
+
};
|
|
328
|
+
|
|
285
329
|
const NETWORK_RETRYABLE_PATTERNS = ["ETIMEDOUT", "ECONNRESET", "timeout", "503", "429"] as const;
|
|
286
330
|
const NETWORK_DEAD_PATTERNS = ["ECONNREFUSED", "ENOTFOUND", "EHOSTUNREACH"] as const;
|
|
287
331
|
const RETRYABLE_ORPC_CODES = new Set([
|
|
@@ -111,13 +111,10 @@ const resolveRemoteSelfName = (pluginId: string, url: string) =>
|
|
|
111
111
|
const memo = remoteSelfNames.get(memoKey);
|
|
112
112
|
if (memo) return memo;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} catch {
|
|
119
|
-
selfName = null;
|
|
120
|
-
}
|
|
114
|
+
const manifest = yield* Effect.tryPromise(() => fetchRemoteIdentityManifest(url)).pipe(
|
|
115
|
+
Effect.orElseSucceed(() => null),
|
|
116
|
+
);
|
|
117
|
+
const selfName = manifest?.metaData?.name?.trim() || null;
|
|
121
118
|
|
|
122
119
|
const resolved = selfName ?? getNormalizedRemoteName(pluginId);
|
|
123
120
|
remoteSelfNames.set(memoKey, resolved);
|
|
@@ -11,11 +11,39 @@ import type {
|
|
|
11
11
|
PluginMetadata,
|
|
12
12
|
PluginRegistry,
|
|
13
13
|
} from "../../types";
|
|
14
|
+
import type { PluginWarningInfo } from "../errors";
|
|
14
15
|
import { PluginRuntimeError, toPluginRuntimeError } from "../errors";
|
|
15
16
|
import { validate } from "../validation";
|
|
16
17
|
import { ModuleFederationService } from "./module-federation.service";
|
|
17
18
|
import { SecretsService } from "./secrets.service";
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate a plugin's `warnings` function against the validated config.
|
|
22
|
+
* Fail-safe: a throwing warnings function becomes a warning itself rather
|
|
23
|
+
* than failing initialization. Strings are normalized to `PluginWarningInfo`
|
|
24
|
+
* with the plugin's registry key.
|
|
25
|
+
*/
|
|
26
|
+
const collectPluginWarnings = (
|
|
27
|
+
plugin: AnyPlugin,
|
|
28
|
+
config: { pluginId: string; variables: unknown; secrets: unknown },
|
|
29
|
+
): PluginWarningInfo[] => {
|
|
30
|
+
const warningsFn = plugin.warnings;
|
|
31
|
+
if (!warningsFn) return [];
|
|
32
|
+
try {
|
|
33
|
+
const raw = warningsFn(config as never);
|
|
34
|
+
return raw.map((warning) =>
|
|
35
|
+
typeof warning === "string" ? { pluginKey: plugin.id, message: warning } : warning,
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return [
|
|
39
|
+
{
|
|
40
|
+
pluginKey: plugin.id,
|
|
41
|
+
message: `Plugin warnings check failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
19
47
|
export class PluginRegistryTag extends Context.Service<PluginRegistryTag, PluginRegistry>()(
|
|
20
48
|
"PluginRegistry",
|
|
21
49
|
) {}
|
|
@@ -271,7 +299,14 @@ export const PluginLoaderServiceDefault = Layer.effect(
|
|
|
271
299
|
// initialization leaks until the process exits.
|
|
272
300
|
const effectContext = yield* Effect.gen(function* () {
|
|
273
301
|
const layer = yield* plugin
|
|
274
|
-
.initialize(
|
|
302
|
+
.initialize(
|
|
303
|
+
{
|
|
304
|
+
pluginId: plugin.id,
|
|
305
|
+
variables: _variables,
|
|
306
|
+
secrets: hydratedConfig.secrets,
|
|
307
|
+
},
|
|
308
|
+
plugins ?? {},
|
|
309
|
+
)
|
|
275
310
|
.pipe(
|
|
276
311
|
Effect.provideService(PluginIdTag, plugin.id),
|
|
277
312
|
Effect.provideService(Scope.Scope, scope),
|
|
@@ -310,9 +345,18 @@ export const PluginLoaderServiceDefault = Layer.effect(
|
|
|
310
345
|
return {
|
|
311
346
|
plugin,
|
|
312
347
|
metadata: pluginInstance.metadata,
|
|
313
|
-
config: {
|
|
348
|
+
config: {
|
|
349
|
+
pluginId: plugin.id,
|
|
350
|
+
variables: _variables,
|
|
351
|
+
secrets: hydratedConfig.secrets,
|
|
352
|
+
},
|
|
314
353
|
effectContext,
|
|
315
354
|
scope,
|
|
355
|
+
warnings: collectPluginWarnings(plugin, {
|
|
356
|
+
pluginId: plugin.id,
|
|
357
|
+
variables: _variables,
|
|
358
|
+
secrets: hydratedConfig.secrets,
|
|
359
|
+
}),
|
|
316
360
|
} satisfies InitializedPlugin<T>;
|
|
317
361
|
}),
|
|
318
362
|
};
|
|
@@ -19,7 +19,7 @@ function manifestUrl(remoteUrl: string): string {
|
|
|
19
19
|
|
|
20
20
|
function normalizeBaseUrl(remoteUrl: string): string {
|
|
21
21
|
const withoutHash = remoteUrl.split("#")[0] ?? remoteUrl;
|
|
22
|
-
return withoutHash.replace(/\/(mf-manifest\.json|remoteEntry
|
|
22
|
+
return withoutHash.replace(/\/(mf-manifest\.json|remoteEntry(?:\.[^/]+)?\.js)$/i, "");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function compareSharedIdentity(
|
package/src/types.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import type { ContractedRouter, RouterClient } from "@orpc/server";
|
|
8
8
|
import type { Context, Scope } from "effect";
|
|
9
9
|
import type { Plugin } from "./plugin";
|
|
10
|
+
import type { PluginWarningInfo } from "./runtime/errors";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Registry bindings interface - populated via module augmentation
|
|
@@ -23,6 +24,24 @@ import type { Plugin } from "./plugin";
|
|
|
23
24
|
// biome-ignore lint/suspicious/noEmptyInterface: required for module augmentation pattern
|
|
24
25
|
export interface RegisteredPlugins {}
|
|
25
26
|
|
|
27
|
+
export type ModuleTransport = "inproc" | "rpc" | "mf";
|
|
28
|
+
|
|
29
|
+
export type ModuleSurface = "api" | "ui" | "jobs";
|
|
30
|
+
|
|
31
|
+
export interface ModuleEndpoint {
|
|
32
|
+
readonly transport: ModuleTransport;
|
|
33
|
+
readonly surface?: ModuleSurface;
|
|
34
|
+
readonly url: string;
|
|
35
|
+
readonly path?: `/${string}`;
|
|
36
|
+
readonly manifest?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type PluginTransport = ModuleTransport;
|
|
40
|
+
|
|
41
|
+
export type PluginSurface = ModuleSurface;
|
|
42
|
+
|
|
43
|
+
export type PluginEndpoint = ModuleEndpoint;
|
|
44
|
+
|
|
26
45
|
/**
|
|
27
46
|
* Base type for any plugin instance.
|
|
28
47
|
*/
|
|
@@ -45,8 +64,14 @@ export type AnyPluginConstructor = {
|
|
|
45
64
|
* Registry entry that supports both direct module imports and remote URLs
|
|
46
65
|
*/
|
|
47
66
|
export type PluginRegistryEntry =
|
|
48
|
-
| {
|
|
49
|
-
|
|
67
|
+
| {
|
|
68
|
+
module: AnyPluginConstructor;
|
|
69
|
+
remote?: string;
|
|
70
|
+
endpoint?: PluginEndpoint;
|
|
71
|
+
version?: string;
|
|
72
|
+
description?: string;
|
|
73
|
+
}
|
|
74
|
+
| { remote: string; endpoint?: PluginEndpoint; version?: string; description?: string };
|
|
50
75
|
|
|
51
76
|
/**
|
|
52
77
|
* Extract plugin constructor type from registry entry
|
|
@@ -115,8 +140,18 @@ export type PluginClientType<T> = RouterClient<PluginRouterType<T>>;
|
|
|
115
140
|
export type PluginServicesEntry<T = any> = {
|
|
116
141
|
client: (context?: PluginContextInput<T>) => PluginClientType<T>;
|
|
117
142
|
router: PluginRouterType<T>;
|
|
143
|
+
endpoint?: PluginEndpoint;
|
|
118
144
|
};
|
|
119
145
|
|
|
146
|
+
export type ModuleClientEntry<C extends AnyContractRouter = AnyContractRouter> = {
|
|
147
|
+
client: (context?: Record<string, unknown>) => RouterClient<ContractedRouter<C, any>>;
|
|
148
|
+
router?: ContractedRouter<C, any>;
|
|
149
|
+
endpoint?: PluginEndpoint;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export type PluginClientEntry<C extends AnyContractRouter = AnyContractRouter> =
|
|
153
|
+
ModuleClientEntry<C>;
|
|
154
|
+
|
|
120
155
|
/**
|
|
121
156
|
* Extract plugin type from registered plugins by key
|
|
122
157
|
* @param K - The plugin key
|
|
@@ -194,6 +229,7 @@ export interface InitializedPlugin<T extends AnyPlugin = AnyPlugin> {
|
|
|
194
229
|
readonly plugin: T;
|
|
195
230
|
readonly metadata: PluginMetadata;
|
|
196
231
|
readonly config: {
|
|
232
|
+
pluginId: string;
|
|
197
233
|
variables: InferSchemaOutput<T["configSchema"]["variables"]>;
|
|
198
234
|
secrets: InferSchemaOutput<T["configSchema"]["secrets"]>;
|
|
199
235
|
};
|
|
@@ -204,6 +240,8 @@ export interface InitializedPlugin<T extends AnyPlugin = AnyPlugin> {
|
|
|
204
240
|
*/
|
|
205
241
|
readonly effectContext: Context.Context<any>;
|
|
206
242
|
readonly scope: Scope.Closeable;
|
|
243
|
+
/** Non-fatal config warnings collected during initialization (empty when none). */
|
|
244
|
+
readonly warnings: PluginWarningInfo[];
|
|
207
245
|
}
|
|
208
246
|
|
|
209
247
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AnyRoute } from "@tanstack/react-router";
|
|
2
2
|
import { createRootRoute, createRoute, Outlet, redirect } from "@tanstack/react-router";
|
|
3
|
+
import { CORE_UI_PLUGIN_KEY } from "./contract";
|
|
3
4
|
import { digestOf } from "./descriptor";
|
|
4
5
|
import type { PluginManifest, RouteRecord } from "./manifest-schema";
|
|
5
6
|
import { MOUNT_REGISTRY, type MountDef, type MountId, resolveMountSegment } from "./mount-registry";
|
|
@@ -177,23 +178,53 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
177
178
|
resolved.push(await input.resolve(ref));
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
const ordered = [
|
|
182
|
+
...resolved.filter((plugin) => plugin.key === CORE_UI_PLUGIN_KEY),
|
|
183
|
+
...resolved.filter((plugin) => plugin.key !== CORE_UI_PLUGIN_KEY),
|
|
184
|
+
];
|
|
185
|
+
|
|
180
186
|
const optionsByPlugin = new Map<string, Map<string, RouteOptionsBundle>>();
|
|
187
|
+
const skippedByPlugin = new Map<string, Set<string>>();
|
|
181
188
|
for (const plugin of resolved) {
|
|
189
|
+
// A plugin with no manifest at all contributes no routes — skip it rather
|
|
190
|
+
// than crashing SSR composition (same contract as the missing-loader skip
|
|
191
|
+
// below).
|
|
192
|
+
if (!plugin.manifest?.routes) continue;
|
|
182
193
|
const optionsById = new Map<string, RouteOptionsBundle>();
|
|
194
|
+
const skipped = new Set<string>();
|
|
183
195
|
await Promise.all(
|
|
184
196
|
plugin.manifest.routes.map(async (record) => {
|
|
185
197
|
const loader = plugin.routeConfig.routeConfigLoaders[record.id];
|
|
186
198
|
if (!loader) {
|
|
187
|
-
|
|
199
|
+
console.log(
|
|
200
|
+
`[Compose] skipping route "${record.id}" in "${plugin.key}" — no routeConfig loader`,
|
|
201
|
+
);
|
|
202
|
+
skipped.add(record.id);
|
|
203
|
+
return;
|
|
188
204
|
}
|
|
189
205
|
optionsById.set(record.id, await loader());
|
|
190
206
|
}),
|
|
191
207
|
);
|
|
208
|
+
// cascade: any route whose parent is skipped is also skipped
|
|
209
|
+
let changed = true;
|
|
210
|
+
while (changed) {
|
|
211
|
+
changed = false;
|
|
212
|
+
for (const record of plugin.manifest.routes) {
|
|
213
|
+
if (skipped.has(record.id)) continue;
|
|
214
|
+
if (record.parentId && skipped.has(record.parentId)) {
|
|
215
|
+
skipped.add(record.id);
|
|
216
|
+
changed = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
192
220
|
optionsByPlugin.set(plugin.key, optionsById);
|
|
221
|
+
skippedByPlugin.set(plugin.key, skipped);
|
|
193
222
|
}
|
|
194
223
|
|
|
195
224
|
const declarations = new Map<MountId, MountDeclaration>();
|
|
196
|
-
|
|
225
|
+
const mountByLayoutId = new Map<string, MountId>();
|
|
226
|
+
for (const plugin of ordered) {
|
|
227
|
+
if (!plugin.manifest?.routes) continue;
|
|
197
228
|
for (const record of plugin.manifest.routes) {
|
|
198
229
|
if (!record.mount) continue;
|
|
199
230
|
const mountId = record.mount as MountId;
|
|
@@ -205,15 +236,24 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
205
236
|
}
|
|
206
237
|
const existing = declarations.get(mountId);
|
|
207
238
|
if (existing) {
|
|
239
|
+
if (existing.key === CORE_UI_PLUGIN_KEY && plugin.key !== CORE_UI_PLUGIN_KEY) {
|
|
240
|
+
console.log(
|
|
241
|
+
`[Compose] mount declaration "${existing.key}:${existing.record.id}" (${mountId}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed declaration skipped`,
|
|
242
|
+
);
|
|
243
|
+
mountByLayoutId.set(record.id, mountId);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
208
246
|
throw new Error(
|
|
209
247
|
`mount "${mountId}" declared by multiple sources: "${existing.key}" (${existing.record.file}) and "${plugin.key}" (${record.file})`,
|
|
210
248
|
);
|
|
211
249
|
}
|
|
250
|
+
if (skippedByPlugin.get(plugin.key)!.has(record.id)) continue;
|
|
212
251
|
declarations.set(mountId, {
|
|
213
252
|
key: plugin.key,
|
|
214
253
|
record,
|
|
215
254
|
options: optionsByPlugin.get(plugin.key)!.get(record.id)!,
|
|
216
255
|
});
|
|
256
|
+
mountByLayoutId.set(record.id, mountId);
|
|
217
257
|
}
|
|
218
258
|
}
|
|
219
259
|
|
|
@@ -244,11 +284,6 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
244
284
|
mountRoutes.set(mountId, toAnyRoute(route));
|
|
245
285
|
}
|
|
246
286
|
|
|
247
|
-
const mountByLayoutId = new Map<string, MountId>();
|
|
248
|
-
for (const [mountId, declaration] of declarations) {
|
|
249
|
-
mountByLayoutId.set(declaration.record.id, mountId);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
287
|
const nav: NavItem[] = [];
|
|
253
288
|
const mountCounts: Record<string, number> = {};
|
|
254
289
|
const childrenByParent = new Map<AnyRoute, AnyRoute[]>();
|
|
@@ -260,7 +295,8 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
260
295
|
childrenByParent.set(parent, list);
|
|
261
296
|
};
|
|
262
297
|
|
|
263
|
-
for (const plugin of
|
|
298
|
+
for (const plugin of ordered) {
|
|
299
|
+
if (!plugin.manifest?.routes) continue;
|
|
264
300
|
const optionsById = optionsByPlugin.get(plugin.key)!;
|
|
265
301
|
const fullPathById = new Map<string, string>();
|
|
266
302
|
const mountById = new Map<string, MountId>();
|
|
@@ -301,18 +337,25 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
301
337
|
throw new Error(`route "${record.id}" in "${plugin.key}" must live under a mount`);
|
|
302
338
|
};
|
|
303
339
|
|
|
304
|
-
const pending = plugin.manifest.routes.filter(
|
|
340
|
+
const pending = plugin.manifest.routes.filter(
|
|
341
|
+
(record) => !record.mount && !skippedByPlugin.get(plugin.key)!.has(record.id),
|
|
342
|
+
);
|
|
343
|
+
const shadowedIds = new Set<string>();
|
|
305
344
|
let progressed = true;
|
|
306
345
|
while (pending.length > 0 && progressed) {
|
|
307
346
|
progressed = false;
|
|
308
347
|
for (let i = 0; i < pending.length; i += 1) {
|
|
309
348
|
const record = pending[i]!;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
349
|
+
const parentId = record.parentId;
|
|
350
|
+
if (parentId) {
|
|
351
|
+
if (shadowedIds.has(parentId)) {
|
|
352
|
+
shadowedIds.add(record.id);
|
|
353
|
+
pending.splice(i, 1);
|
|
354
|
+
i -= 1;
|
|
355
|
+
progressed = true;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
if (!byId.has(parentId) && !mountByLayoutId.has(parentId)) continue;
|
|
316
359
|
}
|
|
317
360
|
pending.splice(i, 1);
|
|
318
361
|
progressed = true;
|
|
@@ -325,6 +368,16 @@ export async function constructTree(input: ConstructInput): Promise<ConstructedT
|
|
|
325
368
|
const siblings = claimedPathsByParent.get(parent.route) ?? new Map<string, string>();
|
|
326
369
|
const claimant = siblings.get(routePath);
|
|
327
370
|
if (claimant) {
|
|
371
|
+
if (
|
|
372
|
+
plugin.key !== CORE_UI_PLUGIN_KEY &&
|
|
373
|
+
claimant.startsWith(`${CORE_UI_PLUGIN_KEY}:`)
|
|
374
|
+
) {
|
|
375
|
+
console.log(
|
|
376
|
+
`[Compose] route claim "${claimant}" (${routePath}) shadows "${plugin.key}:${record.id}" — core ui wins; shadowed route skipped`,
|
|
377
|
+
);
|
|
378
|
+
shadowedIds.add(record.id);
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
328
381
|
throw new Error(
|
|
329
382
|
`path collision: "${plugin.key}" route "${record.id}" declares "${routePath}" which is already claimed by "${claimant}" under the same parent`,
|
|
330
383
|
);
|
package/src/version-manifest.ts
CHANGED
|
@@ -41,9 +41,26 @@ export const WorkspaceVersionManifestSchema = z.object({
|
|
|
41
41
|
* ignore it (zod strips unknown keys). Participates in the version hash.
|
|
42
42
|
*/
|
|
43
43
|
files: z.record(z.string(), SRI).optional(),
|
|
44
|
+
/**
|
|
45
|
+
* Source archive of the workspace subtree (object path → served-object SRI)
|
|
46
|
+
* published beside the dists when the runtime opts in via
|
|
47
|
+
* `publish.sourcePublish` — lets `bos import` dereference a published
|
|
48
|
+
* config slot to the exact source bytes that build came from, with no
|
|
49
|
+
* GitHub dependency. Readers that predate the field ignore it (zod strips
|
|
50
|
+
* unknown keys). Participates in the version hash.
|
|
51
|
+
*/
|
|
52
|
+
source: z
|
|
53
|
+
.object({
|
|
54
|
+
file: z.string().min(1),
|
|
55
|
+
integrity: SRI,
|
|
56
|
+
})
|
|
57
|
+
.optional(),
|
|
44
58
|
});
|
|
45
59
|
export type WorkspaceVersionManifest = z.infer<typeof WorkspaceVersionManifestSchema>;
|
|
46
60
|
|
|
61
|
+
/** A published-artifact coordinate: object path within the bundle namespace + its served SRI. */
|
|
62
|
+
export type SourceRef = NonNullable<WorkspaceVersionManifest["source"]>;
|
|
63
|
+
|
|
47
64
|
export interface VersionManifestInput {
|
|
48
65
|
/** Omit: a per-build timestamp makes the same version id map to different
|
|
49
66
|
* manifest bytes per machine, breaking the pin's SRI against the image's
|
|
@@ -57,6 +74,7 @@ export interface VersionManifestInput {
|
|
|
57
74
|
assets?: Record<string, string>;
|
|
58
75
|
shared?: Record<string, string>;
|
|
59
76
|
files?: Record<string, string>;
|
|
77
|
+
source?: { file: string; integrity: string };
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/**
|
|
@@ -67,7 +85,7 @@ export interface VersionManifestInput {
|
|
|
67
85
|
* coordinate → a new version (and a new file).
|
|
68
86
|
*/
|
|
69
87
|
export function composeVersionManifest(input: VersionManifestInput): WorkspaceVersionManifest {
|
|
70
|
-
const { builtAt, ...hashedInputs } = input;
|
|
88
|
+
const { builtAt: _builtAt, ...hashedInputs } = input;
|
|
71
89
|
const version = createHash("sha256")
|
|
72
90
|
.update(stableStringify(hashedInputs))
|
|
73
91
|
.digest("hex")
|