Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/appkit/src/core/appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ export class AppKit<TPlugins extends InputPluginMap> {
};
const pluginInstance = new Plugin(baseConfig);

if (typeof pluginInstance.attachContext === "function") {
pluginInstance.attachContext({
context: this.#context,
telemetryConfig: baseConfig.telemetry,
});
}

this.#pluginInstances[name] = pluginInstance;

this.#context.registerPlugin(name, pluginInstance);
Expand Down
51 changes: 47 additions & 4 deletions packages/appkit/src/plugin/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,11 @@ export abstract class Plugin<
> implements BasePlugin
{
protected isReady = false;
protected cache: CacheManager;
protected cache!: CacheManager;
protected app: AppManager;
protected devFileReader: DevFileReader;
protected streamManager: StreamManager;
protected telemetry: ITelemetry;
protected telemetry!: ITelemetry;
protected context?: PluginContext;

/** Registered endpoints for this plugin */
Expand All @@ -195,15 +195,58 @@ export abstract class Plugin<
config.name ??
(this.constructor as { manifest?: { name: string } }).manifest?.name ??
"plugin";
this.telemetry = TelemetryManager.getProvider(this.name, config.telemetry);
this.streamManager = new StreamManager();
this.cache = CacheManager.getInstanceSync();
this.app = new AppManager();
this.devFileReader = DevFileReader.getInstance();
this.context = (config as Record<string, unknown>).context as
| PluginContext
| undefined;

// Eagerly bind telemetry + cache if the core services have already been
// initialized (normal createApp path, or tests that mock CacheManager).
// If they haven't, we leave these undefined and rely on `attachContext`
// being called later — this lets factories eagerly construct plugin
// instances at module top-level before `createApp` has run.
this.tryAttachContext();
}

private tryAttachContext(): void {
try {
this.cache = CacheManager.getInstanceSync();
} catch {
return;
}
this.telemetry = TelemetryManager.getProvider(
this.name,
this.config.telemetry,
);
this.isReady = true;
}

/**
* Binds runtime dependencies (telemetry provider, cache, plugin context) to
* this plugin. Called by `AppKit._createApp` after construction and before
* `setup()`. Idempotent: safe to call if the constructor already bound them
* eagerly. Kept separate so factories can eagerly construct plugin instances
* without running this before `TelemetryManager.initialize()` /
* `CacheManager.getInstance()` have run.
*/
attachContext(
deps: {
context?: unknown;
telemetryConfig?: BasePluginConfig["telemetry"];
} = {},
): void {
if (!this.cache) {
this.cache = CacheManager.getInstanceSync();
}
this.telemetry = TelemetryManager.getProvider(
this.name,
deps.telemetryConfig ?? this.config.telemetry,
);
if (deps.context !== undefined) {
this.context = deps.context as PluginContext;
}
this.isReady = true;
}

Expand Down
20 changes: 18 additions & 2 deletions packages/appkit/src/plugins/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export class ServerPlugin extends Plugin {
this.serverApplication = express();
this.server = null;
this.serverExtensions = [];
}

attachContext(deps: Parameters<Plugin["attachContext"]>[0] = {}): void {
super.attachContext(deps);
this.telemetry.registerInstrumentations([
instrumentations.http,
instrumentations.express,
Expand All @@ -68,9 +72,21 @@ export class ServerPlugin extends Plugin {

/** Setup the server plugin. */
async setup() {
if (this.shouldAutoStart()) {
await this.start();
if (!this.shouldAutoStart()) return;
if (this.context) {
// Defer the actual listen+extendRoutes to the `setup:complete` lifecycle
// hook. That way every plugin (including other deferred-phase plugins
// like `agents`) is already registered in PluginContext by the time
// extendRoutes() iterates. Otherwise plugins declared after server()
// in the plugin array would be silently dropped from /api/* mounts.
this.context.onLifecycle("setup:complete", async () => {
await this.start();
});
return;
}
// No plugin context (e.g. tests constructing ServerPlugin directly) —
// start immediately.
await this.start();
}

/** Get the server configuration. */
Expand Down
9 changes: 9 additions & 0 deletions packages/shared/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export interface BasePlugin {
exports?(): unknown;

clientConfig?(): Record<string, unknown>;

/**
* Binds runtime dependencies (telemetry, cache, plugin context) after the
* plugin has been constructed. Called by the AppKit core before `setup()`.
*/
attachContext?(deps: {
context?: unknown;
telemetryConfig?: TelemetryOptions;
}): void;
}

/** Base configuration interface for AppKit plugins */
Expand Down