Skip to content

Commit a06b6e3

Browse files
authored
feat: add onPluginsReady callback to createApp, remove autoStart (#280)
* feat: add customize callback to createApp, remove autoStart Replace the post-await extend/start ceremony with a declarative `customize` callback on createApp config. The callback runs after plugin setup but before the server starts, giving access to the full appkit handle for registering custom routes or async setup. - Add `customize` option to createApp config - Server start is now orchestrated by createApp (lookup by name) - Remove `autoStart` from public API, ServerConfig, and manifest - Remove `start()` from server plugin exports - Remove autoStart guards from extend() and getServer() - Remove ServerError.autoStartConflict() - Migrate dev-playground, template, and all tests Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> * feat: rename customize to onPluginsReady, add codemod CLI and runtime detection Rename the lifecycle hook from `customize` to `onPluginsReady` to clearly communicate when it fires (after plugins are ready, before server starts). Add `appkit codemod customize-callback` CLI command that auto-migrates old autoStart/extend/start patterns to the new onPluginsReady callback. Supports both .then() chain (Pattern A) and await + imperative (Pattern B, with bail-out for complex cases). Add runtime detection that throws helpful errors when users pass autoStart to server() or call server.start() after upgrading, directing them to run the codemod. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> * fix: exclude codemod fixture files from typecheck The test fixture .ts files import @databricks/appkit which doesn't exist in the shared package, causing tsc to fail in CI. Exclude the fixtures directory from the shared tsconfig. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> * refactor: split codemod into separate PR Remove the codemod CLI from this PR to keep the review focused on the core lifecycle change. The codemod will land as a follow-up with bug fixes from review. Runtime detection (constructor autoStart throw + exports().start() trap) stays since it's part of the migration story. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> * fix: add debug logging for onPluginsReady lifecycle hook Log when the onPluginsReady hook starts and completes to aid debugging in development mode. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> * fix: rename codemod reference to on-plugins-ready Update runtime detection error messages to point users to `npx appkit codemod on-plugins-ready` to match the hook name. Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> --------- Signed-off-by: MarioCadenas <MarioCadenas@users.noreply.github.com> Co-authored-by: MarioCadenas <MarioCadenas@users.noreply.github.com>
1 parent 5334308 commit a06b6e3

16 files changed

Lines changed: 236 additions & 198 deletions

File tree

apps/dev-playground/server/index.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ const adminOnly: FilePolicy = (action, _resource, user) => {
5050

5151
createApp({
5252
plugins: [
53-
server({ autoStart: false }),
53+
server(),
5454
reconnect(),
5555
telemetryExamples(),
5656
analytics({}),
@@ -95,9 +95,8 @@ createApp({
9595
// }),
9696
],
9797
...(process.env.APPKIT_E2E_TEST && { client: createMockClient() }),
98-
}).then((appkit) => {
99-
appkit.server
100-
.extend((app) => {
98+
onPluginsReady(appkit) {
99+
appkit.server.extend((app) => {
101100
app.get("/sp", (_req, res) => {
102101
appkit.analytics
103102
.query("SELECT * FROM samples.nyctaxi.trips;")
@@ -195,9 +194,9 @@ createApp({
195194
results,
196195
});
197196
});
198-
})
199-
.start();
200-
});
197+
});
198+
},
199+
}).catch(console.error);
201200

202201
type ProbeResult = {
203202
volume: string;

apps/dev-playground/shared/appkit-types/analytics.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ declare module "@databricks/appkit-ui/react" {
119119
result: Array<{
120120
/** @sqlType STRING */
121121
string_value: string;
122-
/** @sqlType STRING */
123-
number_value: string;
124-
/** @sqlType STRING */
125-
boolean_value: string;
122+
/** @sqlType INT */
123+
number_value: number;
124+
/** @sqlType BOOLEAN */
125+
boolean_value: boolean;
126126
/** @sqlType STRING */
127127
date_value: string;
128128
/** @sqlType STRING */

docs/docs/api/appkit/Class.ServerError.md

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ Use for server start/stop issues, configuration conflicts, etc.
66
## Example
77

88
```typescript
9-
throw new ServerError("Cannot get server when autoStart is true");
109
throw new ServerError("Server not started");
1110
```
1211

@@ -151,26 +150,6 @@ Create a human-readable string representation
151150

152151
***
153152

154-
### autoStartConflict()
155-
156-
```ts
157-
static autoStartConflict(operation: string): ServerError;
158-
```
159-
160-
Create a server error for autoStart conflict
161-
162-
#### Parameters
163-
164-
| Parameter | Type |
165-
| ------ | ------ |
166-
| `operation` | `string` |
167-
168-
#### Returns
169-
170-
`ServerError`
171-
172-
***
173-
174153
### clientDirectoryNotFound()
175154

176155
```ts

docs/docs/api/appkit/Function.createApp.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
function createApp<T>(config: {
55
cache?: CacheConfig;
66
client?: WorkspaceClient;
7+
onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
78
plugins?: T;
89
telemetry?: TelemetryConfig;
910
}): Promise<PluginMap<T>>;
@@ -13,6 +14,9 @@ Bootstraps AppKit with the provided configuration.
1314

1415
Initializes telemetry, cache, and service context, then registers plugins
1516
in phase order (core, normal, deferred) and awaits their setup.
17+
If a `onPluginsReady` callback is provided it runs after plugin setup but
18+
before the server starts, giving you access to the full appkit handle
19+
for registering custom routes or performing async setup.
1620
The returned object maps each plugin name to its `exports()` API,
1721
with an `asUser(req)` method for user-scoped execution.
1822

@@ -26,9 +30,10 @@ with an `asUser(req)` method for user-scoped execution.
2630

2731
| Parameter | Type |
2832
| ------ | ------ |
29-
| `config` | \{ `cache?`: [`CacheConfig`](Interface.CacheConfig.md); `client?`: `WorkspaceClient`; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](Interface.TelemetryConfig.md); \} |
33+
| `config` | \{ `cache?`: [`CacheConfig`](Interface.CacheConfig.md); `client?`: `WorkspaceClient`; `onPluginsReady?`: (`appkit`: `PluginMap`\<`T`\>) => `void` \| `Promise`\<`void`\>; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](Interface.TelemetryConfig.md); \} |
3034
| `config.cache?` | [`CacheConfig`](Interface.CacheConfig.md) |
3135
| `config.client?` | `WorkspaceClient` |
36+
| `config.onPluginsReady?` | (`appkit`: `PluginMap`\<`T`\>) => `void` \| `Promise`\<`void`\> |
3237
| `config.plugins?` | `T` |
3338
| `config.telemetry?` | [`TelemetryConfig`](Interface.TelemetryConfig.md) |
3439

@@ -51,12 +56,12 @@ await createApp({
5156
```ts
5257
import { createApp, server, analytics } from "@databricks/appkit";
5358

54-
const appkit = await createApp({
55-
plugins: [server({ autoStart: false }), analytics({})],
56-
});
57-
58-
appkit.server.extend((app) => {
59-
app.get("/custom", (_req, res) => res.json({ ok: true }));
59+
await createApp({
60+
plugins: [server(), analytics({})],
61+
onPluginsReady(appkit) {
62+
appkit.server.extend((app) => {
63+
app.get("/custom", (_req, res) => res.json({ ok: true }));
64+
});
65+
},
6066
});
61-
await appkit.server.start();
6267
```

docs/docs/plugins/server.md

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,38 @@ await createApp({
3636
});
3737
```
3838

39-
## Manual server start example
39+
## Custom routes example
4040

41-
When you need to extend Express with custom routes:
41+
Use the `onPluginsReady` callback to extend Express with custom routes before the server starts:
4242

4343
```ts
4444
import { createApp, server } from "@databricks/appkit";
4545

46-
const appkit = await createApp({
47-
plugins: [server({ autoStart: false })],
46+
await createApp({
47+
plugins: [server()],
48+
onPluginsReady(appkit) {
49+
appkit.server.extend((app) => {
50+
app.get("/custom", (_req, res) => res.json({ ok: true }));
51+
});
52+
},
4853
});
54+
```
4955

50-
appkit.server.extend((app) => {
51-
app.get("/custom", (_req, res) => res.json({ ok: true }));
52-
});
56+
The `onPluginsReady` callback also supports async operations:
5357

54-
await appkit.server.start();
58+
```ts
59+
await createApp({
60+
plugins: [server()],
61+
async onPluginsReady(appkit) {
62+
const pool = await initializeDatabase();
63+
appkit.server.extend((app) => {
64+
app.get("/data", async (_req, res) => {
65+
const result = await pool.query("SELECT 1");
66+
res.json(result);
67+
});
68+
});
69+
},
70+
});
5571
```
5672

5773
## Configuration options
@@ -64,7 +80,6 @@ await createApp({
6480
server({
6581
port: 8000, // default: Number(process.env.DATABRICKS_APP_PORT) || 8000
6682
host: "0.0.0.0", // default: process.env.FLASK_RUN_HOST || "0.0.0.0"
67-
autoStart: true, // default: true
6883
staticPath: "dist", // optional: force a specific static directory
6984
}),
7085
],

packages/appkit/src/core/appkit.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import type {
1010
} from "shared";
1111
import { CacheManager } from "../cache";
1212
import { ServiceContext } from "../context";
13+
import { createLogger } from "../logging/logger";
1314
import { ResourceRegistry, ResourceType } from "../registry";
1415
import type { TelemetryConfig } from "../telemetry";
1516
import { TelemetryManager } from "../telemetry";
1617

18+
const logger = createLogger("appkit");
19+
1720
export class AppKit<TPlugins extends InputPluginMap> {
1821
#pluginInstances: Record<string, BasePlugin> = {};
1922
#setupPromises: Promise<void>[] = [];
@@ -167,6 +170,7 @@ export class AppKit<TPlugins extends InputPluginMap> {
167170
telemetry?: TelemetryConfig;
168171
cache?: CacheConfig;
169172
client?: WorkspaceClient;
173+
onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
170174
} = {},
171175
): Promise<PluginMap<T>> {
172176
// Initialize core services
@@ -200,7 +204,20 @@ export class AppKit<TPlugins extends InputPluginMap> {
200204

201205
await Promise.all(instance.#setupPromises);
202206

203-
return instance as unknown as PluginMap<T>;
207+
const handle = instance as unknown as PluginMap<T>;
208+
209+
if (config.onPluginsReady) {
210+
logger.debug("Running onPluginsReady hook");
211+
await config.onPluginsReady(handle);
212+
logger.debug("onPluginsReady hook completed");
213+
}
214+
215+
const serverPlugin = instance.#pluginInstances.server;
216+
if (serverPlugin && typeof (serverPlugin as any).start === "function") {
217+
await (serverPlugin as any).start();
218+
}
219+
220+
return handle;
204221
}
205222

206223
private static preparePlugins(
@@ -222,6 +239,9 @@ export class AppKit<TPlugins extends InputPluginMap> {
222239
*
223240
* Initializes telemetry, cache, and service context, then registers plugins
224241
* in phase order (core, normal, deferred) and awaits their setup.
242+
* If a `onPluginsReady` callback is provided it runs after plugin setup but
243+
* before the server starts, giving you access to the full appkit handle
244+
* for registering custom routes or performing async setup.
225245
* The returned object maps each plugin name to its `exports()` API,
226246
* with an `asUser(req)` method for user-scoped execution.
227247
*
@@ -236,18 +256,18 @@ export class AppKit<TPlugins extends InputPluginMap> {
236256
* });
237257
* ```
238258
*
239-
* @example Extended Server with analytics and custom endpoint
259+
* @example Server with custom routes via onPluginsReady
240260
* ```ts
241261
* import { createApp, server, analytics } from "@databricks/appkit";
242262
*
243-
* const appkit = await createApp({
244-
* plugins: [server({ autoStart: false }), analytics({})],
245-
* });
246-
*
247-
* appkit.server.extend((app) => {
248-
* app.get("/custom", (_req, res) => res.json({ ok: true }));
263+
* await createApp({
264+
* plugins: [server(), analytics({})],
265+
* onPluginsReady(appkit) {
266+
* appkit.server.extend((app) => {
267+
* app.get("/custom", (_req, res) => res.json({ ok: true }));
268+
* });
269+
* },
249270
* });
250-
* await appkit.server.start();
251271
* ```
252272
*/
253273
export async function createApp<
@@ -258,6 +278,7 @@ export async function createApp<
258278
telemetry?: TelemetryConfig;
259279
cache?: CacheConfig;
260280
client?: WorkspaceClient;
281+
onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
261282
} = {},
262283
): Promise<PluginMap<T>> {
263284
return AppKit._createApp(config);

packages/appkit/src/errors/server.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { AppKitError } from "./base";
66
*
77
* @example
88
* ```typescript
9-
* throw new ServerError("Cannot get server when autoStart is true");
109
* throw new ServerError("Server not started");
1110
* ```
1211
*/
@@ -15,15 +14,6 @@ export class ServerError extends AppKitError {
1514
readonly statusCode = 500;
1615
readonly isRetryable = false;
1716

18-
/**
19-
* Create a server error for autoStart conflict
20-
*/
21-
static autoStartConflict(operation: string): ServerError {
22-
return new ServerError(`Cannot ${operation} when autoStart is true`, {
23-
context: { operation },
24-
});
25-
}
26-
2717
/**
2818
* Create a server error for server not started
2919
*/

packages/appkit/src/errors/tests/errors.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -348,12 +348,6 @@ describe("ServerError", () => {
348348
expect(error.isRetryable).toBe(false);
349349
});
350350

351-
test("autoStartConflict should create proper error", () => {
352-
const error = ServerError.autoStartConflict("get server");
353-
expect(error.message).toBe("Cannot get server when autoStart is true");
354-
expect(error.context?.operation).toBe("get server");
355-
});
356-
357351
test("notStarted should create proper error", () => {
358352
const error = ServerError.notStarted();
359353
expect(error.message).toContain("Server not started");

packages/appkit/src/plugins/analytics/tests/analytics.integration.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,11 @@ describe("Analytics Plugin Integration", () => {
4646
serverPlugin({
4747
port: TEST_PORT,
4848
host: "127.0.0.1",
49-
autoStart: false,
5049
}),
5150
analytics({}),
5251
],
5352
});
5453

55-
await app.server.start();
5654
server = app.server.getServer();
5755
baseUrl = `http://127.0.0.1:${TEST_PORT}`;
5856
});

packages/appkit/src/plugins/files/tests/plugin.integration.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,11 @@ describe("Files Plugin Integration", () => {
8787
serverPlugin({
8888
port: TEST_PORT,
8989
host: "127.0.0.1",
90-
autoStart: false,
9190
}),
9291
files(),
9392
],
9493
});
9594

96-
await appkit.server.start();
9795
server = appkit.server.getServer();
9896
baseUrl = `http://127.0.0.1:${TEST_PORT}`;
9997
});

0 commit comments

Comments
 (0)