@databricks/appkit-ui 0.25.1 → 0.26.1

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.
@@ -0,0 +1,16 @@
1
+ import { onPluginsReadyCommand } from "./on-plugins-ready.js";
2
+ import { Command } from "commander";
3
+
4
+ //#region src/cli/commands/codemod/index.ts
5
+ /**
6
+ * Parent command for codemod operations.
7
+ * Subcommands:
8
+ * - on-plugins-ready: Migrate from autoStart/extend/start to onPluginsReady callback
9
+ */
10
+ const codemodCommand = new Command("codemod").description("Run codemods to migrate to newer AppKit APIs").addCommand(onPluginsReadyCommand).addHelpText("after", `
11
+ Examples:
12
+ $ appkit codemod on-plugins-ready --write`);
13
+
14
+ //#endregion
15
+ export { codemodCommand };
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/cli/commands/codemod/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { onPluginsReadyCommand } from \"./on-plugins-ready\";\n\n/**\n * Parent command for codemod operations.\n * Subcommands:\n * - on-plugins-ready: Migrate from autoStart/extend/start to onPluginsReady callback\n */\nexport const codemodCommand = new Command(\"codemod\")\n .description(\"Run codemods to migrate to newer AppKit APIs\")\n .addCommand(onPluginsReadyCommand)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready --write`,\n );\n"],"mappings":";;;;;;;;;AAQA,MAAa,iBAAiB,IAAI,QAAQ,UAAU,CACjD,YAAY,+CAA+C,CAC3D,WAAW,sBAAsB,CACjC,YACC,SACA;;6CAGD"}
@@ -0,0 +1,364 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { Command } from "commander";
4
+ import { Lang, parse } from "@ast-grep/napi";
5
+
6
+ //#region src/cli/commands/codemod/on-plugins-ready.ts
7
+ const SEARCH_DIRS = [
8
+ "server",
9
+ "src",
10
+ "."
11
+ ];
12
+ const CANDIDATE_NAMES = ["server.ts", "index.ts"];
13
+ const SKIP_DIRS = new Set([
14
+ "node_modules",
15
+ "dist",
16
+ "build",
17
+ ".git"
18
+ ]);
19
+ function findServerEntryFiles(rootDir) {
20
+ const results = [];
21
+ for (const dir of SEARCH_DIRS) {
22
+ const absDir = path.resolve(rootDir, dir);
23
+ if (!fs.existsSync(absDir)) continue;
24
+ const files = dir === "." ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync) : findTsFiles(absDir);
25
+ for (const file of files) {
26
+ const content = fs.readFileSync(file, "utf-8");
27
+ if (content.includes("createApp") && content.includes("@databricks/appkit")) results.push(file);
28
+ }
29
+ }
30
+ return [...new Set(results)];
31
+ }
32
+ function findTsFiles(dir, files = []) {
33
+ let entries;
34
+ try {
35
+ entries = fs.readdirSync(dir, { withFileTypes: true });
36
+ } catch {
37
+ return files;
38
+ }
39
+ for (const entry of entries) {
40
+ const fullPath = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ if (SKIP_DIRS.has(entry.name)) continue;
43
+ findTsFiles(fullPath, files);
44
+ } else if (entry.isFile() && entry.name.endsWith(".ts")) files.push(fullPath);
45
+ }
46
+ return files;
47
+ }
48
+ function isAlreadyMigrated(content) {
49
+ return parse(Lang.TypeScript, content).root().findAll("createApp({ $$$PROPS })").some((match) => {
50
+ const text = match.text();
51
+ return /\bonPluginsReady\s*[(:]/.test(text);
52
+ });
53
+ }
54
+ /**
55
+ * Find the index of the matching closing delimiter for an opening one.
56
+ * Supports (), {}, and [].
57
+ */
58
+ function findMatchingClose(content, openIdx) {
59
+ const open = content[openIdx];
60
+ const close = {
61
+ "(": ")",
62
+ "{": "}",
63
+ "[": "]"
64
+ }[open];
65
+ if (!close) return -1;
66
+ let depth = 1;
67
+ let i = openIdx + 1;
68
+ while (i < content.length && depth > 0) {
69
+ const ch = content[i];
70
+ if (ch === open) depth++;
71
+ else if (ch === close) depth--;
72
+ if (ch === "\"" || ch === "'" || ch === "`") {
73
+ i = skipString(content, i);
74
+ continue;
75
+ }
76
+ i++;
77
+ }
78
+ return depth === 0 ? i - 1 : -1;
79
+ }
80
+ function skipString(content, startIdx) {
81
+ const quote = content[startIdx];
82
+ let i = startIdx + 1;
83
+ while (i < content.length) {
84
+ if (content[i] === "\\") {
85
+ i += 2;
86
+ continue;
87
+ }
88
+ if (content[i] === quote) return i + 1;
89
+ i++;
90
+ }
91
+ return i;
92
+ }
93
+ function stripAutoStartFromServerCalls(content) {
94
+ return content.replace(/server\(\{([^}]*)\}\)/g, (_fullMatch, propsStr) => {
95
+ const cleaned = propsStr.replace(/autoStart\s*:\s*(true|false)\s*,?\s*/g, "").replace(/,\s*$/, "").trim();
96
+ if (!cleaned) return "server()";
97
+ return `server({ ${cleaned} })`;
98
+ });
99
+ }
100
+ function migratePatternA(content) {
101
+ const warnings = [];
102
+ const createAppIdx = content.indexOf("createApp(");
103
+ if (createAppIdx === -1) return {
104
+ migrated: false,
105
+ content,
106
+ warnings
107
+ };
108
+ const configOpenParen = content.indexOf("(", createAppIdx);
109
+ const configCloseParen = findMatchingClose(content, configOpenParen);
110
+ if (configCloseParen === -1) return {
111
+ migrated: false,
112
+ content,
113
+ warnings
114
+ };
115
+ const afterCreateApp = content.slice(configCloseParen + 1);
116
+ if (!afterCreateApp.match(/^\s*\.then\s*\(/)) return {
117
+ migrated: false,
118
+ content,
119
+ warnings
120
+ };
121
+ const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(".then");
122
+ const thenOpenParen = content.indexOf("(", thenStart + 4);
123
+ const thenCloseParen = findMatchingClose(content, thenOpenParen);
124
+ if (thenCloseParen === -1) return {
125
+ migrated: false,
126
+ content,
127
+ warnings
128
+ };
129
+ const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);
130
+ const thenInner = thenRaw.trim();
131
+ const callbackMatch = thenInner.match(/^(?:async\s+)?\(\s*(\w+)\s*\)\s*=>\s*\{/);
132
+ if (!callbackMatch) return {
133
+ migrated: false,
134
+ content,
135
+ warnings
136
+ };
137
+ const paramName = callbackMatch[1];
138
+ const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf("{");
139
+ const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);
140
+ if (bodyCloseBrace === -1) return {
141
+ migrated: false,
142
+ content,
143
+ warnings
144
+ };
145
+ let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();
146
+ callbackBody = callbackBody.replace(/^\s*(?:await\s+)?\w+\.server\s*\.\s*start\(\s*\)\s*;?\s*$/gm, "").replace(/\n\s*\.start\(\s*\)\s*;?/g, ";").replace(/\.start\(\s*\)/g, "").replace(/\n\s*\n\s*\n/g, "\n\n").trim();
147
+ if (callbackBody.endsWith(";")) {} else if (!callbackBody.endsWith("}")) callbackBody += ";";
148
+ const isAsync = /^async\s/.test(thenInner.trim());
149
+ const catchPatternMatch = content.slice(thenCloseParen + 1).match(/^\s*(?:\)\s*)?\.catch\s*\(/);
150
+ let catchSuffix;
151
+ let consumeAfterThen;
152
+ if (catchPatternMatch) {
153
+ const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;
154
+ const catchCloseParen = findMatchingClose(content, catchOpenParen);
155
+ if (catchCloseParen !== -1) {
156
+ catchSuffix = `.catch(${content.slice(catchOpenParen + 1, catchCloseParen).trim()})`;
157
+ consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);
158
+ } else {
159
+ catchSuffix = ".catch(console.error)";
160
+ consumeAfterThen = 0;
161
+ }
162
+ } else {
163
+ catchSuffix = ".catch(console.error)";
164
+ consumeAfterThen = 0;
165
+ }
166
+ const configStr = content.slice(configOpenParen + 1, configCloseParen);
167
+ const lastBraceIdx = configStr.lastIndexOf("}");
168
+ if (lastBraceIdx === -1) return {
169
+ migrated: false,
170
+ content,
171
+ warnings
172
+ };
173
+ const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();
174
+ const needsComma = beforeLastBrace.endsWith(",") ? "" : ",";
175
+ const indentedBody = callbackBody.split("\n").map((line) => ` ${line.trimStart()}`).join("\n");
176
+ const newConfig = `${beforeLastBrace}${`${needsComma}\n ${isAsync ? "async " : ""}onPluginsReady(${paramName}) {\n${indentedBody}\n },`}\n}`;
177
+ let finalEnd = thenCloseParen + 1 + consumeAfterThen;
178
+ const trailing = content.slice(finalEnd).match(/^\s*\)?\s*;?\s*/);
179
+ if (trailing) finalEnd += trailing[0].length;
180
+ return {
181
+ migrated: true,
182
+ content: content.slice(0, createAppIdx) + `createApp(${newConfig})${catchSuffix};` + content.slice(finalEnd),
183
+ warnings
184
+ };
185
+ }
186
+ function migratePatternB(content) {
187
+ const warnings = [];
188
+ const match = content.match(/(?:const|let)\s+(\w+)\s*=\s*await\s+createApp\s*\(/);
189
+ if (!match) return {
190
+ migrated: false,
191
+ content,
192
+ warnings
193
+ };
194
+ const varName = match[1];
195
+ const matchIdx = content.indexOf(match[0]);
196
+ const configOpenParen = matchIdx + match[0].length - 1;
197
+ const configCloseParen = findMatchingClose(content, configOpenParen);
198
+ if (configCloseParen === -1) return {
199
+ migrated: false,
200
+ content,
201
+ warnings
202
+ };
203
+ const semiMatch = content.slice(configCloseParen + 1).match(/^\s*;/);
204
+ const createAppEnd = configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);
205
+ const afterCreateApp = content.slice(createAppEnd);
206
+ const varUsagePattern = new RegExp(`\\b${varName}\\.(\\w+)`, "g");
207
+ const usages = [];
208
+ for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) usages.push({
209
+ plugin: usageMatch[1],
210
+ index: usageMatch.index
211
+ });
212
+ if (usages.filter((u) => u.plugin !== "server").length > 0) {
213
+ warnings.push(`Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`);
214
+ return {
215
+ migrated: false,
216
+ content,
217
+ warnings
218
+ };
219
+ }
220
+ const extendPattern = new RegExp(`\\b${varName}\\.server\\.extend\\s*\\(`, "g");
221
+ const startPattern = new RegExp(`(?:await\\s+)?${varName}\\.server\\.start\\s*\\(\\s*\\)\\s*;`);
222
+ const extendMatches = [...afterCreateApp.matchAll(extendPattern)];
223
+ if (extendMatches.length > 1) {
224
+ warnings.push(`Found ${extendMatches.length} server.extend() calls. Please migrate manually.`);
225
+ return {
226
+ migrated: false,
227
+ content,
228
+ warnings
229
+ };
230
+ }
231
+ const extendExec = extendMatches[0] ?? null;
232
+ const startExec = startPattern.exec(afterCreateApp);
233
+ if (!startExec) return {
234
+ migrated: false,
235
+ content,
236
+ warnings
237
+ };
238
+ let extendArg = "";
239
+ let extendFullStatement = "";
240
+ if (extendExec) {
241
+ const extendOpenParen = createAppEnd + extendExec.index + extendExec[0].length - 1;
242
+ const extendCloseParen = findMatchingClose(content, extendOpenParen);
243
+ if (extendCloseParen !== -1) {
244
+ extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();
245
+ const stmtStart = createAppEnd + extendExec.index;
246
+ let stmtEnd = extendCloseParen + 1;
247
+ const trailingSemi = content.slice(stmtEnd).match(/^\s*;/);
248
+ if (trailingSemi) stmtEnd += trailingSemi[0].length;
249
+ extendFullStatement = content.slice(stmtStart, stmtEnd);
250
+ }
251
+ }
252
+ const startFullStatement = startExec[0];
253
+ const configStr = content.slice(configOpenParen + 1, configCloseParen);
254
+ const lastBraceIdx = configStr.lastIndexOf("}");
255
+ if (lastBraceIdx === -1) return {
256
+ migrated: false,
257
+ content,
258
+ warnings
259
+ };
260
+ const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();
261
+ const needsComma = beforeLastBrace.endsWith(",") ? "" : ",";
262
+ let onPluginsReadyProp;
263
+ if (extendArg) onPluginsReadyProp = `${needsComma}\n onPluginsReady(${varName}) {\n ${varName}.server.extend(${extendArg});\n },`;
264
+ else onPluginsReadyProp = "";
265
+ const newCreateApp = `await createApp(${`${beforeLastBrace}${onPluginsReadyProp}\n}`});`;
266
+ let result = content.slice(0, matchIdx) + newCreateApp;
267
+ let remaining = afterCreateApp;
268
+ if (extendFullStatement) remaining = remaining.replace(extendFullStatement, "");
269
+ remaining = remaining.replace(startFullStatement, "");
270
+ remaining = remaining.replace(/\n\s*\n\s*\n/g, "\n\n");
271
+ result += remaining;
272
+ return {
273
+ migrated: true,
274
+ content: result,
275
+ warnings
276
+ };
277
+ }
278
+ function migrateFile(filePath) {
279
+ const original = fs.readFileSync(filePath, "utf-8");
280
+ if (isAlreadyMigrated(original)) return {
281
+ migrated: false,
282
+ content: original,
283
+ warnings: ["Already migrated -- no changes needed."]
284
+ };
285
+ const content = stripAutoStartFromServerCalls(original);
286
+ const allWarnings = [];
287
+ const patternA = migratePatternA(content);
288
+ if (patternA.migrated) {
289
+ allWarnings.push(...patternA.warnings);
290
+ return {
291
+ migrated: true,
292
+ content: patternA.content,
293
+ warnings: allWarnings
294
+ };
295
+ }
296
+ allWarnings.push(...patternA.warnings);
297
+ const patternB = migratePatternB(content);
298
+ if (patternB.migrated) {
299
+ allWarnings.push(...patternB.warnings);
300
+ return {
301
+ migrated: true,
302
+ content: patternB.content,
303
+ warnings: allWarnings
304
+ };
305
+ }
306
+ allWarnings.push(...patternB.warnings);
307
+ if (content !== original) return {
308
+ migrated: true,
309
+ content,
310
+ warnings: allWarnings
311
+ };
312
+ return {
313
+ migrated: false,
314
+ content: original,
315
+ warnings: allWarnings
316
+ };
317
+ }
318
+ function runCodemod(options) {
319
+ const rootDir = process.cwd();
320
+ const write = options.write ?? false;
321
+ let files;
322
+ if (options.path) {
323
+ const absPath = path.resolve(rootDir, options.path);
324
+ if (!fs.existsSync(absPath)) {
325
+ console.error(`File not found: ${absPath}`);
326
+ process.exit(1);
327
+ }
328
+ files = [absPath];
329
+ } else files = findServerEntryFiles(rootDir);
330
+ if (files.length === 0) {
331
+ console.log("No files found importing createApp from @databricks/appkit.");
332
+ console.log("Use --path to specify a file explicitly.");
333
+ process.exit(0);
334
+ }
335
+ let hasChanges = false;
336
+ for (const file of files) {
337
+ const relPath = path.relative(rootDir, file);
338
+ const result = migrateFile(file);
339
+ for (const warning of result.warnings) console.log(` ${relPath}: ${warning}`);
340
+ if (!result.migrated) {
341
+ if (result.warnings.length === 0) console.log(` ${relPath}: No migration needed.`);
342
+ continue;
343
+ }
344
+ hasChanges = true;
345
+ if (write) {
346
+ fs.writeFileSync(file, result.content, "utf-8");
347
+ console.log(` ${relPath}: Migrated successfully.`);
348
+ } else {
349
+ console.log(`\n--- ${relPath} (dry run) ---`);
350
+ console.log(result.content);
351
+ console.log("---");
352
+ }
353
+ }
354
+ if (hasChanges && !write) console.log("\nDry run complete. Run with --write to apply changes.");
355
+ }
356
+ const onPluginsReadyCommand = new Command("on-plugins-ready").description("Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback").option("--path <file>", "Path to the server entry file to migrate").option("--write", "Apply changes (default: dry-run)", false).addHelpText("after", `
357
+ Examples:
358
+ $ appkit codemod on-plugins-ready # dry-run, auto-detect files
359
+ $ appkit codemod on-plugins-ready --write # apply changes
360
+ $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`).action(runCodemod);
361
+
362
+ //#endregion
363
+ export { onPluginsReadyCommand };
364
+ //# sourceMappingURL=on-plugins-ready.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"on-plugins-ready.js","names":[],"sources":["../../../../src/cli/commands/codemod/on-plugins-ready.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Lang, parse } from \"@ast-grep/napi\";\nimport { Command } from \"commander\";\n\nconst SEARCH_DIRS = [\"server\", \"src\", \".\"];\nconst CANDIDATE_NAMES = [\"server.ts\", \"index.ts\"];\nconst SKIP_DIRS = new Set([\"node_modules\", \"dist\", \"build\", \".git\"]);\n\nfunction findServerEntryFiles(rootDir: string): string[] {\n const results: string[] = [];\n\n for (const dir of SEARCH_DIRS) {\n const absDir = path.resolve(rootDir, dir);\n if (!fs.existsSync(absDir)) continue;\n\n const files =\n dir === \".\"\n ? CANDIDATE_NAMES.map((n) => path.join(absDir, n)).filter(fs.existsSync)\n : findTsFiles(absDir);\n\n for (const file of files) {\n const content = fs.readFileSync(file, \"utf-8\");\n if (\n content.includes(\"createApp\") &&\n content.includes(\"@databricks/appkit\")\n ) {\n results.push(file);\n }\n }\n }\n\n return [...new Set(results)];\n}\n\nfunction findTsFiles(dir: string, files: string[] = []): string[] {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(dir, { withFileTypes: true });\n } catch {\n return files;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n findTsFiles(fullPath, files);\n } else if (entry.isFile() && entry.name.endsWith(\".ts\")) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\nfunction isAlreadyMigrated(content: string): boolean {\n const ast = parse(Lang.TypeScript, content);\n const root = ast.root();\n return root.findAll(\"createApp({ $$$PROPS })\").some((match) => {\n const text = match.text();\n return /\\bonPluginsReady\\s*[(:]/.test(text);\n });\n}\n\n/**\n * Find the index of the matching closing delimiter for an opening one.\n * Supports (), {}, and [].\n */\nfunction findMatchingClose(content: string, openIdx: number): number {\n const open = content[openIdx];\n const closeMap: Record<string, string> = {\n \"(\": \")\",\n \"{\": \"}\",\n \"[\": \"]\",\n };\n const close = closeMap[open];\n if (!close) return -1;\n\n let depth = 1;\n let i = openIdx + 1;\n while (i < content.length && depth > 0) {\n const ch = content[i];\n if (ch === open) depth++;\n else if (ch === close) depth--;\n\n // skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(content, i);\n continue;\n }\n i++;\n }\n return depth === 0 ? i - 1 : -1;\n}\n\nfunction skipString(content: string, startIdx: number): number {\n const quote = content[startIdx];\n let i = startIdx + 1;\n while (i < content.length) {\n if (content[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (content[i] === quote) return i + 1;\n i++;\n }\n return i;\n}\n\nfunction stripAutoStartFromServerCalls(content: string): string {\n return content.replace(\n /server\\(\\{([^}]*)\\}\\)/g,\n (_fullMatch, propsStr: string) => {\n const cleaned = propsStr\n .replace(/autoStart\\s*:\\s*(true|false)\\s*,?\\s*/g, \"\")\n .replace(/,\\s*$/, \"\")\n .trim();\n if (!cleaned) return \"server()\";\n return `server({ ${cleaned} })`;\n },\n );\n}\n\ninterface MigrationResult {\n migrated: boolean;\n content: string;\n warnings: string[];\n}\n\nfunction migratePatternA(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Find createApp(...).then(\n const createAppIdx = content.indexOf(\"createApp(\");\n if (createAppIdx === -1) return { migrated: false, content, warnings };\n\n // Find the opening paren of createApp(\n const configOpenParen = content.indexOf(\"(\", createAppIdx);\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Check for .then( after the closing paren\n const afterCreateApp = content.slice(configCloseParen + 1);\n const thenMatch = afterCreateApp.match(/^\\s*\\.then\\s*\\(/);\n if (!thenMatch) return { migrated: false, content, warnings };\n\n const thenStart = configCloseParen + 1 + afterCreateApp.indexOf(\".then\");\n const thenOpenParen = content.indexOf(\"(\", thenStart + 4);\n const thenCloseParen = findMatchingClose(content, thenOpenParen);\n if (thenCloseParen === -1) return { migrated: false, content, warnings };\n\n // Extract the callback inside .then(...)\n const thenRaw = content.slice(thenOpenParen + 1, thenCloseParen);\n const thenInner = thenRaw.trim();\n\n // Parse callback: (param) => { body } or async (param) => { body }\n const callbackMatch = thenInner.match(\n /^(?:async\\s+)?\\(\\s*(\\w+)\\s*\\)\\s*=>\\s*\\{/,\n );\n if (!callbackMatch) return { migrated: false, content, warnings };\n\n const paramName = callbackMatch[1];\n const bodyOpenBrace = thenOpenParen + 1 + thenRaw.indexOf(\"{\");\n const bodyCloseBrace = findMatchingClose(content, bodyOpenBrace);\n if (bodyCloseBrace === -1) return { migrated: false, content, warnings };\n\n let callbackBody = content.slice(bodyOpenBrace + 1, bodyCloseBrace).trim();\n\n // Remove entire statements that are just .start() calls (e.g. `await appkit.server.start();`)\n callbackBody = callbackBody\n .replace(/^\\s*(?:await\\s+)?\\w+\\.server\\s*\\.\\s*start\\(\\s*\\)\\s*;?\\s*$/gm, \"\")\n .replace(/\\n\\s*\\.start\\(\\s*\\)\\s*;?/g, \";\")\n .replace(/\\.start\\(\\s*\\)/g, \"\")\n .replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\")\n .trim();\n\n // Clean up trailing semicolons\n if (callbackBody.endsWith(\";\")) {\n // fine\n } else if (!callbackBody.endsWith(\"}\")) {\n callbackBody += \";\";\n }\n\n // Detect if the callback was async\n const isAsync = /^async\\s/.test(thenInner.trim());\n\n // Check for .catch() after .then(...) using brace-aware parsing\n const afterThenClose = content.slice(thenCloseParen + 1);\n const catchPatternMatch = afterThenClose.match(/^\\s*(?:\\)\\s*)?\\.catch\\s*\\(/);\n\n let catchSuffix: string;\n let consumeAfterThen: number;\n\n if (catchPatternMatch) {\n const catchOpenParen = thenCloseParen + 1 + catchPatternMatch[0].length - 1;\n const catchCloseParen = findMatchingClose(content, catchOpenParen);\n if (catchCloseParen !== -1) {\n const catchArg = content\n .slice(catchOpenParen + 1, catchCloseParen)\n .trim();\n catchSuffix = `.catch(${catchArg})`;\n consumeAfterThen = catchCloseParen + 1 - (thenCloseParen + 1);\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n } else {\n catchSuffix = \".catch(console.error)\";\n consumeAfterThen = 0;\n }\n\n // Build the onPluginsReady property\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n // Indent the body properly\n const bodyLines = callbackBody.split(\"\\n\");\n const indentedBody = bodyLines\n .map((line) => ` ${line.trimStart()}`)\n .join(\"\\n\");\n\n const asyncPrefix = isAsync ? \"async \" : \"\";\n const onPluginsReadyProp = `${needsComma}\\n ${asyncPrefix}onPluginsReady(${paramName}) {\\n${indentedBody}\\n },`;\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n\n // Build the replacement\n const endIdx = thenCloseParen + 1 + consumeAfterThen;\n // Consume trailing ) ; and whitespace\n let finalEnd = endIdx;\n const trailing = content.slice(finalEnd).match(/^\\s*\\)?\\s*;?\\s*/);\n if (trailing) finalEnd += trailing[0].length;\n\n const newContent =\n content.slice(0, createAppIdx) +\n `createApp(${newConfig})${catchSuffix};` +\n content.slice(finalEnd);\n\n return { migrated: true, content: newContent, warnings };\n}\n\nfunction migratePatternB(content: string): MigrationResult {\n const warnings: string[] = [];\n\n // Match: const/let varName = await createApp({...});\n const awaitPattern = /(?:const|let)\\s+(\\w+)\\s*=\\s*await\\s+createApp\\s*\\(/;\n\n const match = content.match(awaitPattern);\n if (!match) return { migrated: false, content, warnings };\n\n const varName = match[1];\n const matchIdx = content.indexOf(match[0]);\n\n // Find the createApp(...) closing paren\n const configOpenParen = matchIdx + match[0].length - 1;\n const configCloseParen = findMatchingClose(content, configOpenParen);\n if (configCloseParen === -1) return { migrated: false, content, warnings };\n\n // Find the semicolon after the createApp call\n const afterCall = content.slice(configCloseParen + 1);\n const semiMatch = afterCall.match(/^\\s*;/);\n const createAppEnd =\n configCloseParen + 1 + (semiMatch ? semiMatch[0].length : 0);\n\n // Find all uses of varName after the createApp call\n const afterCreateApp = content.slice(createAppEnd);\n const varUsagePattern = new RegExp(`\\\\b${varName}\\\\.(\\\\w+)`, \"g\");\n\n const usages: { plugin: string; index: number }[] = [];\n for (const usageMatch of afterCreateApp.matchAll(varUsagePattern)) {\n usages.push({ plugin: usageMatch[1], index: usageMatch.index });\n }\n\n // Check for non-server usage\n const nonServerUsage = usages.filter((u) => u.plugin !== \"server\");\n if (nonServerUsage.length > 0) {\n warnings.push(\n `Found additional usage of '${varName}' handle outside server.extend/start. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n // Find the extend call(s) and start call in the after-createApp region\n const extendPattern = new RegExp(\n `\\\\b${varName}\\\\.server\\\\.extend\\\\s*\\\\(`,\n \"g\",\n );\n const startPattern = new RegExp(\n `(?:await\\\\s+)?${varName}\\\\.server\\\\.start\\\\s*\\\\(\\\\s*\\\\)\\\\s*;`,\n );\n\n const extendMatches = [...afterCreateApp.matchAll(extendPattern)];\n if (extendMatches.length > 1) {\n warnings.push(\n `Found ${extendMatches.length} server.extend() calls. Please migrate manually.`,\n );\n return { migrated: false, content, warnings };\n }\n\n const extendExec = extendMatches[0] ?? null;\n const startExec = startPattern.exec(afterCreateApp);\n\n if (!startExec) return { migrated: false, content, warnings };\n\n // Extract the extend call's argument\n let extendArg = \"\";\n let extendFullStatement = \"\";\n if (extendExec) {\n const extendOpenParen =\n createAppEnd + extendExec.index + extendExec[0].length - 1;\n const extendCloseParen = findMatchingClose(content, extendOpenParen);\n if (extendCloseParen !== -1) {\n extendArg = content.slice(extendOpenParen + 1, extendCloseParen).trim();\n // Find the full statement including trailing semicolon\n const stmtStart = createAppEnd + extendExec.index;\n let stmtEnd = extendCloseParen + 1;\n const afterExtend = content.slice(stmtEnd);\n const trailingSemi = afterExtend.match(/^\\s*;/);\n if (trailingSemi) stmtEnd += trailingSemi[0].length;\n extendFullStatement = content.slice(stmtStart, stmtEnd);\n }\n }\n\n const startFullStatement = startExec[0];\n\n // Build the onPluginsReady callback\n const configStr = content.slice(configOpenParen + 1, configCloseParen);\n const lastBraceIdx = configStr.lastIndexOf(\"}\");\n if (lastBraceIdx === -1) return { migrated: false, content, warnings };\n\n const beforeLastBrace = configStr.slice(0, lastBraceIdx).trimEnd();\n const needsComma = beforeLastBrace.endsWith(\",\") ? \"\" : \",\";\n\n let onPluginsReadyProp: string;\n if (extendArg) {\n onPluginsReadyProp =\n `${needsComma}\\n onPluginsReady(${varName}) {\\n` +\n ` ${varName}.server.extend(${extendArg});\\n` +\n \" },\";\n } else {\n onPluginsReadyProp = \"\";\n }\n\n const newConfig = `${beforeLastBrace}${onPluginsReadyProp}\\n}`;\n const newCreateApp = `await createApp(${newConfig});`;\n\n // Replace: remove const declaration, replace with plain await, remove extend + start\n let result = content.slice(0, matchIdx) + newCreateApp;\n let remaining = afterCreateApp;\n\n if (extendFullStatement) {\n remaining = remaining.replace(extendFullStatement, \"\");\n }\n remaining = remaining.replace(startFullStatement, \"\");\n\n // Clean up consecutive blank lines\n remaining = remaining.replace(/\\n\\s*\\n\\s*\\n/g, \"\\n\\n\");\n\n result += remaining;\n\n return { migrated: true, content: result, warnings };\n}\n\nexport function migrateFile(filePath: string): MigrationResult {\n const original = fs.readFileSync(filePath, \"utf-8\");\n\n if (isAlreadyMigrated(original)) {\n return {\n migrated: false,\n content: original,\n warnings: [\"Already migrated -- no changes needed.\"],\n };\n }\n\n const content = stripAutoStartFromServerCalls(original);\n const allWarnings: string[] = [];\n\n // Try Pattern A first\n const patternA = migratePatternA(content);\n if (patternA.migrated) {\n allWarnings.push(...patternA.warnings);\n return {\n migrated: true,\n content: patternA.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternA.warnings);\n\n // Try Pattern B\n const patternB = migratePatternB(content);\n if (patternB.migrated) {\n allWarnings.push(...patternB.warnings);\n return {\n migrated: true,\n content: patternB.content,\n warnings: allWarnings,\n };\n }\n allWarnings.push(...patternB.warnings);\n\n // Check if autoStart was stripped (content changed but no pattern matched)\n if (content !== original) {\n return { migrated: true, content, warnings: allWarnings };\n }\n\n return { migrated: false, content: original, warnings: allWarnings };\n}\n\nfunction runCodemod(options: { path?: string; write?: boolean }) {\n const rootDir = process.cwd();\n const write = options.write ?? false;\n\n let files: string[];\n if (options.path) {\n const absPath = path.resolve(rootDir, options.path);\n if (!fs.existsSync(absPath)) {\n console.error(`File not found: ${absPath}`);\n process.exit(1);\n }\n files = [absPath];\n } else {\n files = findServerEntryFiles(rootDir);\n }\n\n if (files.length === 0) {\n console.log(\"No files found importing createApp from @databricks/appkit.\");\n console.log(\"Use --path to specify a file explicitly.\");\n process.exit(0);\n }\n\n let hasChanges = false;\n\n for (const file of files) {\n const relPath = path.relative(rootDir, file);\n const result = migrateFile(file);\n\n for (const warning of result.warnings) {\n console.log(` ${relPath}: ${warning}`);\n }\n\n if (!result.migrated) {\n if (result.warnings.length === 0) {\n console.log(` ${relPath}: No migration needed.`);\n }\n continue;\n }\n\n hasChanges = true;\n\n if (write) {\n fs.writeFileSync(file, result.content, \"utf-8\");\n console.log(` ${relPath}: Migrated successfully.`);\n } else {\n console.log(`\\n--- ${relPath} (dry run) ---`);\n console.log(result.content);\n console.log(\"---\");\n }\n }\n\n if (hasChanges && !write) {\n console.log(\"\\nDry run complete. Run with --write to apply changes.\");\n }\n}\n\nexport const onPluginsReadyCommand = new Command(\"on-plugins-ready\")\n .description(\n \"Migrate createApp usage from autoStart/extend/start pattern to onPluginsReady callback\",\n )\n .option(\"--path <file>\", \"Path to the server entry file to migrate\")\n .option(\"--write\", \"Apply changes (default: dry-run)\", false)\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit codemod on-plugins-ready # dry-run, auto-detect files\n $ appkit codemod on-plugins-ready --write # apply changes\n $ appkit codemod on-plugins-ready --path server.ts # migrate a specific file`,\n )\n .action(runCodemod);\n"],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { codemodCommand } from "./commands/codemod/index.js";
2
3
  import { docsCommand } from "./commands/docs.js";
3
4
  import { generateTypesCommand } from "./commands/generate-types.js";
4
5
  import { lintCommand } from "./commands/lint.js";
@@ -20,6 +21,7 @@ cmd.addCommand(generateTypesCommand);
20
21
  cmd.addCommand(lintCommand);
21
22
  cmd.addCommand(docsCommand);
22
23
  cmd.addCommand(pluginCommand);
24
+ cmd.addCommand(codemodCommand);
23
25
  cmd.parse();
24
26
 
25
27
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\n\ncmd.parse();\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,UAAU,KADE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACzB,qBAAqB;AACrD,MAAM,MAAM,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEtD,MAAM,MAAM,IAAI,SAAS;AAEzB,IACG,KAAK,SAAS,CACd,YAAY,kCAAkC,CAC9C,QAAQ,IAAI,QAAQ;AAEvB,IAAI,WAAW,aAAa;AAC5B,IAAI,WAAW,qBAAqB;AACpC,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,cAAc;AAE7B,IAAI,OAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport \"dotenv/config\";\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { Command } from \"commander\";\nimport { codemodCommand } from \"./commands/codemod/index.js\";\nimport { docsCommand } from \"./commands/docs.js\";\nimport { generateTypesCommand } from \"./commands/generate-types.js\";\nimport { lintCommand } from \"./commands/lint.js\";\nimport { pluginCommand } from \"./commands/plugin/index.js\";\nimport { setupCommand } from \"./commands/setup.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = join(__dirname, \"../../package.json\");\nconst pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n\nconst cmd = new Command();\n\ncmd\n .name(\"appkit\")\n .description(\"CLI tools for Databricks AppKit\")\n .version(pkg.version);\n\ncmd.addCommand(setupCommand);\ncmd.addCommand(generateTypesCommand);\ncmd.addCommand(lintCommand);\ncmd.addCommand(docsCommand);\ncmd.addCommand(pluginCommand);\ncmd.addCommand(codemodCommand);\n\ncmd.parse();\n"],"mappings":";;;;;;;;;;;;;;AAcA,MAAM,UAAU,KADE,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EACzB,qBAAqB;AACrD,MAAM,MAAM,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC;AAEtD,MAAM,MAAM,IAAI,SAAS;AAEzB,IACG,KAAK,SAAS,CACd,YAAY,kCAAkC,CAC9C,QAAQ,IAAI,QAAQ;AAEvB,IAAI,WAAW,aAAa;AAC5B,IAAI,WAAW,qBAAqB;AACpC,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,YAAY;AAC3B,IAAI,WAAW,cAAc;AAC7B,IAAI,WAAW,eAAe;AAE9B,IAAI,OAAO"}
@@ -5,7 +5,6 @@ Error thrown when server lifecycle operations fail. Use for server start/stop is
5
5
  ## Example[​](#example "Direct link to Example")
6
6
 
7
7
  ```typescript
8
- throw new ServerError("Cannot get server when autoStart is true");
9
8
  throw new ServerError("Server not started");
10
9
 
11
10
  ```
@@ -158,27 +157,6 @@ Create a human-readable string representation
158
157
 
159
158
  ***
160
159
 
161
- ### autoStartConflict()[​](#autostartconflict "Direct link to autoStartConflict()")
162
-
163
- ```ts
164
- static autoStartConflict(operation: string): ServerError;
165
-
166
- ```
167
-
168
- Create a server error for autoStart conflict
169
-
170
- #### Parameters[​](#parameters-1 "Direct link to Parameters")
171
-
172
- | Parameter | Type |
173
- | ----------- | -------- |
174
- | `operation` | `string` |
175
-
176
- #### Returns[​](#returns-3 "Direct link to Returns")
177
-
178
- `ServerError`
179
-
180
- ***
181
-
182
160
  ### clientDirectoryNotFound()[​](#clientdirectorynotfound "Direct link to clientDirectoryNotFound()")
183
161
 
184
162
  ```ts
@@ -188,13 +166,13 @@ static clientDirectoryNotFound(searchedPaths: string[]): ServerError;
188
166
 
189
167
  Create a server error for missing client directory
190
168
 
191
- #### Parameters[​](#parameters-2 "Direct link to Parameters")
169
+ #### Parameters[​](#parameters-1 "Direct link to Parameters")
192
170
 
193
171
  | Parameter | Type |
194
172
  | --------------- | ----------- |
195
173
  | `searchedPaths` | `string`\[] |
196
174
 
197
- #### Returns[​](#returns-4 "Direct link to Returns")
175
+ #### Returns[​](#returns-3 "Direct link to Returns")
198
176
 
199
177
  `ServerError`
200
178
 
@@ -209,7 +187,7 @@ static notStarted(): ServerError;
209
187
 
210
188
  Create a server error for server not started
211
189
 
212
- #### Returns[​](#returns-5 "Direct link to Returns")
190
+ #### Returns[​](#returns-4 "Direct link to Returns")
213
191
 
214
192
  `ServerError`
215
193
 
@@ -224,6 +202,6 @@ static viteNotInitialized(): ServerError;
224
202
 
225
203
  Create a server error for Vite dev server not initialized
226
204
 
227
- #### Returns[​](#returns-6 "Direct link to Returns")
205
+ #### Returns[​](#returns-5 "Direct link to Returns")
228
206
 
229
207
  `ServerError`
@@ -4,6 +4,7 @@
4
4
  function createApp<T>(config: {
5
5
  cache?: CacheConfig;
6
6
  client?: WorkspaceClient;
7
+ onPluginsReady?: (appkit: PluginMap<T>) => void | Promise<void>;
7
8
  plugins?: T;
8
9
  telemetry?: TelemetryConfig;
9
10
  }): Promise<PluginMap<T>>;
@@ -12,7 +13,7 @@ function createApp<T>(config: {
12
13
 
13
14
  Bootstraps AppKit with the provided configuration.
14
15
 
15
- Initializes telemetry, cache, and service context, then registers plugins in phase order (core, normal, deferred) and awaits their setup. The returned object maps each plugin name to its `exports()` API, with an `asUser(req)` method for user-scoped execution.
16
+ Initializes telemetry, cache, and service context, then registers plugins in phase order (core, normal, deferred) and awaits their setup. If a `onPluginsReady` callback is provided it runs after plugin setup but before the server starts, giving you access to the full appkit handle for registering custom routes or performing async setup. The returned object maps each plugin name to its `exports()` API, with an `asUser(req)` method for user-scoped execution.
16
17
 
17
18
  ## Type Parameters[​](#type-parameters "Direct link to Type Parameters")
18
19
 
@@ -22,13 +23,14 @@ Initializes telemetry, cache, and service context, then registers plugins in pha
22
23
 
23
24
  ## Parameters[​](#parameters "Direct link to Parameters")
24
25
 
25
- | Parameter | Type |
26
- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
27
- | `config` | { `cache?`: [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md); `client?`: `WorkspaceClient`; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md); } |
28
- | `config.cache?` | [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md) |
29
- | `config.client?` | `WorkspaceClient` |
30
- | `config.plugins?` | `T` |
31
- | `config.telemetry?` | [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md) |
26
+ | Parameter | Type |
27
+ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
28
+ | `config` | { `cache?`: [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md); `client?`: `WorkspaceClient`; `onPluginsReady?`: (`appkit`: `PluginMap`<`T`>) => `void` \| `Promise`<`void`>; `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md); } |
29
+ | `config.cache?` | [`CacheConfig`](./docs/api/appkit/Interface.CacheConfig.md) |
30
+ | `config.client?` | `WorkspaceClient` |
31
+ | `config.onPluginsReady?` | (`appkit`: `PluginMap`<`T`>) => `void` \| `Promise`<`void`> |
32
+ | `config.plugins?` | `T` |
33
+ | `config.telemetry?` | [`TelemetryConfig`](./docs/api/appkit/Interface.TelemetryConfig.md) |
32
34
 
33
35
  ## Returns[​](#returns "Direct link to Returns")
34
36
 
@@ -50,13 +52,13 @@ await createApp({
50
52
  ```ts
51
53
  import { createApp, server, analytics } from "@databricks/appkit";
52
54
 
53
- const appkit = await createApp({
54
- plugins: [server({ autoStart: false }), analytics({})],
55
- });
56
-
57
- appkit.server.extend((app) => {
58
- app.get("/custom", (_req, res) => res.json({ ok: true }));
55
+ await createApp({
56
+ plugins: [server(), analytics({})],
57
+ onPluginsReady(appkit) {
58
+ appkit.server.extend((app) => {
59
+ app.get("/custom", (_req, res) => res.json({ ok: true }));
60
+ });
61
+ },
59
62
  });
60
- await appkit.server.start();
61
63
 
62
64
  ```
@@ -40,22 +40,39 @@ await createApp({
40
40
 
41
41
  ```
42
42
 
43
- ## Manual server start example[​](#manual-server-start-example "Direct link to Manual server start example")
43
+ ## Custom routes example[​](#custom-routes-example "Direct link to Custom routes example")
44
44
 
45
- When you need to extend Express with custom routes:
45
+ Use the `onPluginsReady` callback to extend Express with custom routes before the server starts:
46
46
 
47
47
  ```ts
48
48
  import { createApp, server } from "@databricks/appkit";
49
49
 
50
- const appkit = await createApp({
51
- plugins: [server({ autoStart: false })],
50
+ await createApp({
51
+ plugins: [server()],
52
+ onPluginsReady(appkit) {
53
+ appkit.server.extend((app) => {
54
+ app.get("/custom", (_req, res) => res.json({ ok: true }));
55
+ });
56
+ },
52
57
  });
53
58
 
54
- appkit.server.extend((app) => {
55
- app.get("/custom", (_req, res) => res.json({ ok: true }));
56
- });
59
+ ```
60
+
61
+ The `onPluginsReady` callback also supports async operations:
57
62
 
58
- await appkit.server.start();
63
+ ```ts
64
+ await createApp({
65
+ plugins: [server()],
66
+ async onPluginsReady(appkit) {
67
+ const pool = await initializeDatabase();
68
+ appkit.server.extend((app) => {
69
+ app.get("/data", async (_req, res) => {
70
+ const result = await pool.query("SELECT 1");
71
+ res.json(result);
72
+ });
73
+ });
74
+ },
75
+ });
59
76
 
60
77
  ```
61
78
 
@@ -69,7 +86,6 @@ await createApp({
69
86
  server({
70
87
  port: 8000, // default: Number(process.env.DATABRICKS_APP_PORT) || 8000
71
88
  host: "0.0.0.0", // default: process.env.FLASK_RUN_HOST || "0.0.0.0"
72
- autoStart: true, // default: true
73
89
  staticPath: "dist", // optional: force a specific static directory
74
90
  }),
75
91
  ],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@databricks/appkit-ui",
3
3
  "type": "module",
4
- "version": "0.25.1",
4
+ "version": "0.26.1",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",