@databricks/appkit 0.39.0 → 0.40.0
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/appkit/package.js +1 -1
- package/dist/cli/commands/generate-types.js +114 -5
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/spawn-lock.js +116 -0
- package/dist/cli/commands/spawn-lock.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +2 -2
- package/dist/type-generator/index.js +115 -12
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/preflight.js +24 -0
- package/dist/type-generator/preflight.js.map +1 -0
- package/dist/type-generator/query-registry.js +322 -90
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/types.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +141 -7
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/type-generator/warehouse-status.js +116 -0
- package/dist/type-generator/warehouse-status.js.map +1 -0
- package/docs/development/type-generation.md +13 -0
- package/docs/plugins/analytics.md +156 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/dist/appkit/package.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
|
+
import { acquireSpawnLock, getSpawnLockPath, releaseSpawnLock } from "./spawn-lock.js";
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
|
-
import { Command } from "commander";
|
|
4
|
+
import { Command, Option } from "commander";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
4
6
|
|
|
5
7
|
//#region src/cli/commands/generate-types.ts
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* Resolve the typegen pre-flight mode for the CLI. Defaults to "non-blocking" —
|
|
10
|
+
* a one-shot CLI can't describe in the background, so by default it never
|
|
11
|
+
* describes at all: it skips the warehouse probe AND every DESCRIBE, emits
|
|
12
|
+
* best-available types (cache where the SQL hash matches, else `result: unknown`)
|
|
13
|
+
* and returns immediately, never blocking on — or failing because of — a
|
|
14
|
+
* warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)
|
|
15
|
+
* for a deliberate/CI invocation that should wait for a starting warehouse and
|
|
16
|
+
* fail fast on a stopped one.
|
|
17
|
+
*/
|
|
18
|
+
function resolveTypegenMode(options) {
|
|
19
|
+
return options?.wait ? "blocking" : "non-blocking";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Generate types command implementation. Runs the library generate (which, in
|
|
23
|
+
* non-blocking mode, writes degraded types and returns immediately). This is the
|
|
24
|
+
* SAME work the worker performs in blocking mode in the background.
|
|
8
25
|
*/
|
|
9
26
|
async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
|
|
10
27
|
try {
|
|
11
28
|
const resolvedRootDir = rootDir || process.cwd();
|
|
12
29
|
const noCache = options?.noCache || false;
|
|
30
|
+
const mode = resolveTypegenMode(options);
|
|
13
31
|
const typeGen = await import("@databricks/appkit/type-generator");
|
|
14
32
|
const resolvedWarehouseId = warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;
|
|
15
33
|
if (resolvedWarehouseId) {
|
|
@@ -20,7 +38,8 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
|
|
|
20
38
|
queryFolder,
|
|
21
39
|
outFile: resolvedOutFile,
|
|
22
40
|
warehouseId: resolvedWarehouseId,
|
|
23
|
-
noCache
|
|
41
|
+
noCache,
|
|
42
|
+
mode
|
|
24
43
|
});
|
|
25
44
|
console.log(`Generated query types: ${resolvedOutFile}`);
|
|
26
45
|
}
|
|
@@ -37,15 +56,105 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
|
|
|
37
56
|
console.error("Please install @databricks/appkit to use this command.");
|
|
38
57
|
process.exit(1);
|
|
39
58
|
}
|
|
59
|
+
if (error instanceof Error && (error.name === "TypegenSyntaxError" || error.name === "TypegenFatalError")) {
|
|
60
|
+
console.error(error.message);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
40
63
|
throw error;
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Spawn the detached blocking worker that refreshes real types in the background
|
|
68
|
+
* after the foreground non-blocking generate has already written degraded types.
|
|
69
|
+
*
|
|
70
|
+
* Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry
|
|
71
|
+
* that launched us) with `generate-types --wait --worker-lock <lockPath>` plus
|
|
72
|
+
* the same positional target options the foreground used, so the worker writes
|
|
73
|
+
* to the same out file / reads the same query folder. The worker is:
|
|
74
|
+
* - `detached: true` + `.unref()` so it outlives this process (install/dev-setup
|
|
75
|
+
* can finish and exit while the worker keeps warming the warehouse).
|
|
76
|
+
* - `stdio: "ignore"` so it never holds the parent's pipes open or interleaves
|
|
77
|
+
* output into the install/dev log.
|
|
78
|
+
*
|
|
79
|
+
* Spawning is wrapped so any failure is non-fatal: the caller still has degraded
|
|
80
|
+
* types and exits 0.
|
|
81
|
+
*
|
|
82
|
+
* @param lockPath - the acquired single-flight lock; passed to the worker so it
|
|
83
|
+
* releases the SAME lock when it finishes.
|
|
84
|
+
* @param targets - the foreground's positional args, forwarded verbatim.
|
|
85
|
+
* @returns true if the worker was spawned, false if spawning threw.
|
|
86
|
+
*/
|
|
87
|
+
function spawnTypegenWorker(lockPath, targets) {
|
|
88
|
+
const cliEntry = process.argv[1];
|
|
89
|
+
const positionals = [];
|
|
90
|
+
for (const value of [
|
|
91
|
+
targets.rootDir,
|
|
92
|
+
targets.outFile,
|
|
93
|
+
targets.warehouseId
|
|
94
|
+
]) {
|
|
95
|
+
if (value === void 0) break;
|
|
96
|
+
positionals.push(value);
|
|
97
|
+
}
|
|
98
|
+
const args = [
|
|
99
|
+
...process.execArgv,
|
|
100
|
+
cliEntry,
|
|
101
|
+
"generate-types",
|
|
102
|
+
"--wait",
|
|
103
|
+
"--worker-lock",
|
|
104
|
+
lockPath,
|
|
105
|
+
...positionals
|
|
106
|
+
];
|
|
107
|
+
try {
|
|
108
|
+
spawn(process.execPath, args, {
|
|
109
|
+
detached: true,
|
|
110
|
+
stdio: "ignore"
|
|
111
|
+
}).unref();
|
|
112
|
+
return true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(`Could not start background type refresh: ${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The command action. Orchestrates the non-blocking foreground contract:
|
|
120
|
+
* 1. Run the library generate (writes degraded types immediately in non-blocking
|
|
121
|
+
* mode; does the full blocking lifecycle when this is the worker).
|
|
122
|
+
* 2. If this is a non-blocking, non-worker invocation, try to spawn the detached
|
|
123
|
+
* blocking worker behind the single-flight lock. If the lock is already held
|
|
124
|
+
* by a live worker, skip (single-flight) with a one-line note. Either way the
|
|
125
|
+
* foreground returns normally (exit 0).
|
|
126
|
+
* 3. If this IS the worker (`--worker-lock` present), it ran blocking above and
|
|
127
|
+
* releases the lock here (and via a process-exit guard, so a hard failure /
|
|
128
|
+
* process.exit still frees it).
|
|
129
|
+
*/
|
|
130
|
+
async function generateTypesAction(rootDir, outFile, warehouseId, options) {
|
|
131
|
+
const isWorker = typeof options.workerLock === "string";
|
|
132
|
+
if (isWorker && options.workerLock) {
|
|
133
|
+
const lockPath = options.workerLock;
|
|
134
|
+
process.once("exit", () => releaseSpawnLock(lockPath));
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
await runGenerateTypes(rootDir, outFile, warehouseId, options);
|
|
138
|
+
} finally {
|
|
139
|
+
if (isWorker && options.workerLock) releaseSpawnLock(options.workerLock);
|
|
140
|
+
}
|
|
141
|
+
if (!isWorker && resolveTypegenMode(options) === "non-blocking") {
|
|
142
|
+
const lockPath = getSpawnLockPath(rootDir || process.cwd());
|
|
143
|
+
if (acquireSpawnLock(lockPath)) spawnTypegenWorker(lockPath, {
|
|
144
|
+
rootDir,
|
|
145
|
+
outFile,
|
|
146
|
+
warehouseId
|
|
147
|
+
});
|
|
148
|
+
else console.log("Type refresh already in progress, skipping.");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const generateTypesCommand = new Command("generate-types").description("Generate TypeScript types from SQL queries").argument("[rootDir]", "Root directory of the project", process.cwd()).argument("[outFile]", "Output file path", path.join(process.cwd(), "shared/appkit-types/analytics.d.ts")).argument("[warehouseId]", "Databricks warehouse ID").option("--no-cache", "Disable caching for type generation").option("--wait", "Wait for warehouse readiness instead of degrading (use for CI)").addOption(new Option("--worker-lock <path>", "Internal: detached worker lock path").hideHelp()).addHelpText("after", `
|
|
44
152
|
Examples:
|
|
45
153
|
$ appkit generate-types
|
|
46
154
|
$ appkit generate-types . shared/appkit-types/analytics.d.ts
|
|
47
155
|
$ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id
|
|
48
|
-
$ appkit generate-types --no-cache
|
|
156
|
+
$ appkit generate-types --no-cache
|
|
157
|
+
$ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`).action(generateTypesAction);
|
|
49
158
|
|
|
50
159
|
//#endregion
|
|
51
160
|
export { generateTypesCommand };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command } from \"commander\";\n\n/**\n * Generate types command implementation\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: { noCache?: boolean },\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (fs.existsSync(queryFolder)) {\n await typeGen.generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n throw error;\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache`,\n )\n .action(runGenerateTypes);\n"],"mappings":";;;;;;;;AAOA,eAAe,iBACb,SACA,SACA,aACA,SACA;AACA,KAAI;EACF,MAAM,kBAAkB,WAAW,QAAQ,KAAK;EAChD,MAAM,UAAU,SAAS,WAAW;EAEpC,MAAM,UAAU,MAAM,OAAO;EAG7B,MAAM,sBACJ,eAAe,QAAQ,IAAI;AAE7B,MAAI,qBAAqB;GACvB,MAAM,kBACJ,WACA,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC;GAEhE,MAAM,cAAc,KAAK,KAAK,iBAAiB,iBAAiB;AAChE,OAAI,GAAG,WAAW,YAAY,EAAE;AAC9B,UAAM,QAAQ,uBAAuB;KACnC;KACA,SAAS;KACT,aAAa;KACb;KACD,CAAC;AACF,YAAQ,IAAI,0BAA0B,kBAAkB;;QAG1D,SAAQ,MACN,oGACD;EAIH,MAAM,iBAAiB,KAAK,KAC1B,QAAQ,KAAK,EACb,mCACD;AACD,QAAM,QAAQ,qBAAqB;GACjC,SAAS;GACT;GACD,CAAC;AACF,UAAQ,IAAI,4BAA4B,iBAAiB;UAClD,OAAO;AACd,MACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,qBAAqB,EAC5C;AACA,WAAQ,MACN,+EACD;AACD,WAAQ,MAAM,yDAAyD;AACvE,WAAQ,KAAK,EAAE;;AAEjB,QAAM;;;AAIV,MAAa,uBAAuB,IAAI,QAAQ,iBAAiB,CAC9D,YAAY,6CAA6C,CACzD,SAAS,aAAa,iCAAiC,QAAQ,KAAK,CAAC,CACrE,SACC,aACA,oBACA,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC,CAC/D,CACA,SAAS,iBAAiB,0BAA0B,CACpD,OAAO,cAAc,sCAAsC,CAC3D,YACC,SACA;;;;;sCAMD,CACA,OAAO,iBAAiB"}
|
|
1
|
+
{"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (fs.existsSync(queryFolder)) {\n await typeGen.generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/cli/commands/spawn-lock.ts
|
|
5
|
+
/**
|
|
6
|
+
* How long a spawn lock is considered fresh. A held lock newer than this means a
|
|
7
|
+
* background worker is genuinely in flight, so the foreground skips spawning;
|
|
8
|
+
* older than this the lock is presumed orphaned (the worker crashed/was killed
|
|
9
|
+
* before it could release) and is stolen.
|
|
10
|
+
*
|
|
11
|
+
* Must comfortably exceed the worker's worst-case runtime: the blocking preflight
|
|
12
|
+
* wait cap (PREFLIGHT_WAIT_MAX_MS = 5 min in the type-generator) plus a DESCRIBE
|
|
13
|
+
* budget. Six minutes leaves ~1 min of headroom over a worker that waits the full
|
|
14
|
+
* preflight window and then describes.
|
|
15
|
+
*/
|
|
16
|
+
const SPAWN_LOCK_STALE_MS = 360 * 1e3;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the on-disk path of the single-flight spawn lock for a project.
|
|
19
|
+
*
|
|
20
|
+
* Lives alongside the type-generator cache (`node_modules/.databricks/appkit/`)
|
|
21
|
+
* so it shares the same already-creatable, gitignored, per-project location and
|
|
22
|
+
* doesn't introduce a new directory. The lock is keyed only by project root, so
|
|
23
|
+
* concurrent `generate-types` invocations for the same project (postinstall +
|
|
24
|
+
* predev, say) contend for one lock and only one wins the spawn.
|
|
25
|
+
*
|
|
26
|
+
* @param rootDir - project root (the resolved first CLI argument / cwd).
|
|
27
|
+
* @returns absolute path to the lock file.
|
|
28
|
+
*/
|
|
29
|
+
function getSpawnLockPath(rootDir) {
|
|
30
|
+
return path.join(rootDir, "node_modules", ".databricks", "appkit", ".appkit-typegen-worker.lock");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Try to acquire the single-flight spawn lock.
|
|
34
|
+
*
|
|
35
|
+
* Atomic create via `fs.writeFileSync(lockPath, ..., { flag: "wx" })` — `wx`
|
|
36
|
+
* fails (EEXIST) if the file already exists, so the create itself is the
|
|
37
|
+
* mutual-exclusion primitive (no check-then-create race between two foreground
|
|
38
|
+
* processes). The lock body records pid + timestamp purely for debugging.
|
|
39
|
+
*
|
|
40
|
+
* On EEXIST we stat the existing lock:
|
|
41
|
+
* - fresh (mtime within {@link staleMs}) → a worker is in flight, return false.
|
|
42
|
+
* - stale (mtime older than staleMs) → presumed orphaned; unlink and recreate.
|
|
43
|
+
* The recreate also uses `wx`, so if a competing process steals it first we
|
|
44
|
+
* lose the race cleanly and return false.
|
|
45
|
+
*
|
|
46
|
+
* Any unexpected error (permission, ENOENT on a missing parent dir we couldn't
|
|
47
|
+
* create, …) is swallowed and reported as "not acquired": failing to take the
|
|
48
|
+
* lock must never break the foreground — at worst we skip the background refresh.
|
|
49
|
+
*
|
|
50
|
+
* @param lockPath - path returned by {@link getSpawnLockPath}.
|
|
51
|
+
* @param staleMs - age beyond which a held lock is stolen. Defaults to
|
|
52
|
+
* {@link SPAWN_LOCK_STALE_MS}.
|
|
53
|
+
* @returns true if this caller now owns the lock (and must release it), false if
|
|
54
|
+
* another live worker holds it or the lock couldn't be taken.
|
|
55
|
+
*/
|
|
56
|
+
function acquireSpawnLock(lockPath, staleMs = SPAWN_LOCK_STALE_MS) {
|
|
57
|
+
const body = `${process.pid} ${Date.now()}\n`;
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
60
|
+
} catch {}
|
|
61
|
+
try {
|
|
62
|
+
fs.writeFileSync(lockPath, body, { flag: "wx" });
|
|
63
|
+
return true;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!isErrnoException(error) || error.code !== "EEXIST") return false;
|
|
66
|
+
}
|
|
67
|
+
let mtimeMs;
|
|
68
|
+
try {
|
|
69
|
+
mtimeMs = fs.statSync(lockPath).mtimeMs;
|
|
70
|
+
} catch {
|
|
71
|
+
return tryCreate(lockPath, body);
|
|
72
|
+
}
|
|
73
|
+
if (Date.now() - mtimeMs < staleMs) return false;
|
|
74
|
+
try {
|
|
75
|
+
fs.unlinkSync(lockPath);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (isErrnoException(error) && error.code !== "ENOENT") return false;
|
|
78
|
+
}
|
|
79
|
+
return tryCreate(lockPath, body);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Release the spawn lock. Unlink, ignoring ENOENT (already gone — e.g. it was
|
|
83
|
+
* stolen as stale by another process, or never existed). Any other error is
|
|
84
|
+
* swallowed: a failed release at worst leaves a stale lock that the next caller
|
|
85
|
+
* will steal after {@link SPAWN_LOCK_STALE_MS}.
|
|
86
|
+
*
|
|
87
|
+
* @param lockPath - path returned by {@link getSpawnLockPath}.
|
|
88
|
+
*/
|
|
89
|
+
function releaseSpawnLock(lockPath) {
|
|
90
|
+
try {
|
|
91
|
+
fs.unlinkSync(lockPath);
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Attempt an atomic `wx` create, returning whether it succeeded. EEXIST (a
|
|
96
|
+
* racing creator beat us) and any other error map to false.
|
|
97
|
+
*/
|
|
98
|
+
function tryCreate(lockPath, body) {
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(lockPath, body, { flag: "wx" });
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Narrow an unknown caught value to a Node errno exception so `.code` is safe to
|
|
108
|
+
* read.
|
|
109
|
+
*/
|
|
110
|
+
function isErrnoException(error) {
|
|
111
|
+
return error instanceof Error && "code" in error;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { acquireSpawnLock, getSpawnLockPath, releaseSpawnLock };
|
|
116
|
+
//# sourceMappingURL=spawn-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spawn-lock.js","names":[],"sources":["../../../src/cli/commands/spawn-lock.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * How long a spawn lock is considered fresh. A held lock newer than this means a\n * background worker is genuinely in flight, so the foreground skips spawning;\n * older than this the lock is presumed orphaned (the worker crashed/was killed\n * before it could release) and is stolen.\n *\n * Must comfortably exceed the worker's worst-case runtime: the blocking preflight\n * wait cap (PREFLIGHT_WAIT_MAX_MS = 5 min in the type-generator) plus a DESCRIBE\n * budget. Six minutes leaves ~1 min of headroom over a worker that waits the full\n * preflight window and then describes.\n */\nexport const SPAWN_LOCK_STALE_MS = 6 * 60 * 1000;\n\n/**\n * Resolve the on-disk path of the single-flight spawn lock for a project.\n *\n * Lives alongside the type-generator cache (`node_modules/.databricks/appkit/`)\n * so it shares the same already-creatable, gitignored, per-project location and\n * doesn't introduce a new directory. The lock is keyed only by project root, so\n * concurrent `generate-types` invocations for the same project (postinstall +\n * predev, say) contend for one lock and only one wins the spawn.\n *\n * @param rootDir - project root (the resolved first CLI argument / cwd).\n * @returns absolute path to the lock file.\n */\nexport function getSpawnLockPath(rootDir: string): string {\n return path.join(\n rootDir,\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n \".appkit-typegen-worker.lock\",\n );\n}\n\n/**\n * Try to acquire the single-flight spawn lock.\n *\n * Atomic create via `fs.writeFileSync(lockPath, ..., { flag: \"wx\" })` — `wx`\n * fails (EEXIST) if the file already exists, so the create itself is the\n * mutual-exclusion primitive (no check-then-create race between two foreground\n * processes). The lock body records pid + timestamp purely for debugging.\n *\n * On EEXIST we stat the existing lock:\n * - fresh (mtime within {@link staleMs}) → a worker is in flight, return false.\n * - stale (mtime older than staleMs) → presumed orphaned; unlink and recreate.\n * The recreate also uses `wx`, so if a competing process steals it first we\n * lose the race cleanly and return false.\n *\n * Any unexpected error (permission, ENOENT on a missing parent dir we couldn't\n * create, …) is swallowed and reported as \"not acquired\": failing to take the\n * lock must never break the foreground — at worst we skip the background refresh.\n *\n * @param lockPath - path returned by {@link getSpawnLockPath}.\n * @param staleMs - age beyond which a held lock is stolen. Defaults to\n * {@link SPAWN_LOCK_STALE_MS}.\n * @returns true if this caller now owns the lock (and must release it), false if\n * another live worker holds it or the lock couldn't be taken.\n */\nexport function acquireSpawnLock(\n lockPath: string,\n staleMs: number = SPAWN_LOCK_STALE_MS,\n): boolean {\n const body = `${process.pid} ${Date.now()}\\n`;\n\n try {\n fs.mkdirSync(path.dirname(lockPath), { recursive: true });\n } catch {\n // Parent dir creation is best-effort; the create below will surface any real\n // problem and we'll treat it as \"not acquired\".\n }\n\n try {\n fs.writeFileSync(lockPath, body, { flag: \"wx\" });\n return true;\n } catch (error) {\n if (!isErrnoException(error) || error.code !== \"EEXIST\") {\n // Unexpected failure — don't let lock IO break the foreground.\n return false;\n }\n }\n\n // Lock exists — decide fresh vs stale.\n let mtimeMs: number;\n try {\n mtimeMs = fs.statSync(lockPath).mtimeMs;\n } catch {\n // It vanished between the failed create and the stat (released by the\n // worker). Try once more to take it.\n return tryCreate(lockPath, body);\n }\n\n if (Date.now() - mtimeMs < staleMs) {\n // A worker is genuinely in flight.\n return false;\n }\n\n // Stale: steal it. Unlink (ignore ENOENT — someone else may have cleaned up)\n // then re-create with `wx` so we still lose cleanly to a racing stealer.\n try {\n fs.unlinkSync(lockPath);\n } catch (error) {\n if (isErrnoException(error) && error.code !== \"ENOENT\") {\n return false;\n }\n }\n return tryCreate(lockPath, body);\n}\n\n/**\n * Release the spawn lock. Unlink, ignoring ENOENT (already gone — e.g. it was\n * stolen as stale by another process, or never existed). Any other error is\n * swallowed: a failed release at worst leaves a stale lock that the next caller\n * will steal after {@link SPAWN_LOCK_STALE_MS}.\n *\n * @param lockPath - path returned by {@link getSpawnLockPath}.\n */\nexport function releaseSpawnLock(lockPath: string): void {\n try {\n fs.unlinkSync(lockPath);\n } catch {\n // ENOENT or any other error — releasing is best-effort.\n }\n}\n\n/**\n * Attempt an atomic `wx` create, returning whether it succeeded. EEXIST (a\n * racing creator beat us) and any other error map to false.\n */\nfunction tryCreate(lockPath: string, body: string): boolean {\n try {\n fs.writeFileSync(lockPath, body, { flag: \"wx\" });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Narrow an unknown caught value to a Node errno exception so `.code` is safe to\n * read.\n */\nfunction isErrnoException(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,MAAa,sBAAsB,MAAS;;;;;;;;;;;;;AAc5C,SAAgB,iBAAiB,SAAyB;AACxD,QAAO,KAAK,KACV,SACA,gBACA,eACA,UACA,8BACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BH,SAAgB,iBACd,UACA,UAAkB,qBACT;CACT,MAAM,OAAO,GAAG,QAAQ,IAAI,GAAG,KAAK,KAAK,CAAC;AAE1C,KAAI;AACF,KAAG,UAAU,KAAK,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;SACnD;AAKR,KAAI;AACF,KAAG,cAAc,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC;AAChD,SAAO;UACA,OAAO;AACd,MAAI,CAAC,iBAAiB,MAAM,IAAI,MAAM,SAAS,SAE7C,QAAO;;CAKX,IAAI;AACJ,KAAI;AACF,YAAU,GAAG,SAAS,SAAS,CAAC;SAC1B;AAGN,SAAO,UAAU,UAAU,KAAK;;AAGlC,KAAI,KAAK,KAAK,GAAG,UAAU,QAEzB,QAAO;AAKT,KAAI;AACF,KAAG,WAAW,SAAS;UAChB,OAAO;AACd,MAAI,iBAAiB,MAAM,IAAI,MAAM,SAAS,SAC5C,QAAO;;AAGX,QAAO,UAAU,UAAU,KAAK;;;;;;;;;;AAWlC,SAAgB,iBAAiB,UAAwB;AACvD,KAAI;AACF,KAAG,WAAW,SAAS;SACjB;;;;;;AASV,SAAS,UAAU,UAAkB,MAAuB;AAC1D,KAAI;AACF,KAAG,cAAc,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC;AAChD,SAAO;SACD;AACN,SAAO;;;;;;;AAQX,SAAS,iBAAiB,OAAgD;AACxE,QAAO,iBAAiB,SAAS,UAAU"}
|
package/dist/cli/index.js
CHANGED
package/dist/cli/index.js.map
CHANGED
|
@@ -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 { 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\
|
|
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\nawait cmd.parseAsync();\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,MAAM,IAAI,YAAY"}
|
|
@@ -162,9 +162,9 @@ declare const pluginManifestSchema: z.ZodObject<{
|
|
|
162
162
|
type: z.ZodLiteral<"sql_warehouse">;
|
|
163
163
|
}, z.core.$strict>, z.ZodObject<{
|
|
164
164
|
permission: z.ZodEnum<{
|
|
165
|
+
CAN_QUERY: "CAN_QUERY";
|
|
165
166
|
CAN_VIEW: "CAN_VIEW";
|
|
166
167
|
CAN_MANAGE: "CAN_MANAGE";
|
|
167
|
-
CAN_QUERY: "CAN_QUERY";
|
|
168
168
|
}>;
|
|
169
169
|
alias: z.ZodString;
|
|
170
170
|
resourceKey: z.ZodString;
|
|
@@ -683,9 +683,9 @@ declare const pluginManifestSchema: z.ZodObject<{
|
|
|
683
683
|
type: z.ZodLiteral<"sql_warehouse">;
|
|
684
684
|
}, z.core.$strict>, z.ZodObject<{
|
|
685
685
|
permission: z.ZodEnum<{
|
|
686
|
+
CAN_QUERY: "CAN_QUERY";
|
|
686
687
|
CAN_VIEW: "CAN_VIEW";
|
|
687
688
|
CAN_MANAGE: "CAN_MANAGE";
|
|
688
|
-
CAN_QUERY: "CAN_QUERY";
|
|
689
689
|
}>;
|
|
690
690
|
alias: z.ZodString;
|
|
691
691
|
resourceKey: z.ZodString;
|
|
@@ -2,6 +2,7 @@ import { createLogger } from "../logging/logger.js";
|
|
|
2
2
|
import { migrateProjectConfig, removeOldGeneratedTypes, resolveProjectRoot } from "./migration.js";
|
|
3
3
|
import { generateQueriesFromDescribe } from "./query-registry.js";
|
|
4
4
|
import { generateServingTypes as generateServingTypes$1 } from "./serving/generator.js";
|
|
5
|
+
import pc from "picocolors";
|
|
5
6
|
import fs from "node:fs/promises";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import dotenv from "dotenv";
|
|
@@ -9,6 +10,106 @@ import dotenv from "dotenv";
|
|
|
9
10
|
//#region src/type-generator/index.ts
|
|
10
11
|
dotenv.config();
|
|
11
12
|
const logger = createLogger("type-generator");
|
|
13
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
14
|
+
return count === 1 ? singular : pluralForm;
|
|
15
|
+
}
|
|
16
|
+
function formatFailureRows(label, queries, color) {
|
|
17
|
+
if (queries.length === 0) return [];
|
|
18
|
+
const byMessage = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const { name, message } of queries) {
|
|
20
|
+
const names = byMessage.get(message);
|
|
21
|
+
if (names) names.push(name);
|
|
22
|
+
else byMessage.set(message, [name]);
|
|
23
|
+
}
|
|
24
|
+
const maxNameLen = Math.max(...queries.map((query) => query.name.length));
|
|
25
|
+
const tag = color(label.padEnd(7));
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (const [message, names] of byMessage) {
|
|
28
|
+
if (names.length === 1) {
|
|
29
|
+
rows.push(` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
rows.push(` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, "query", "queries")})`)}`);
|
|
33
|
+
rows.push(` ${names.map((name) => pc.bold(name)).join(pc.dim(", "))}`);
|
|
34
|
+
}
|
|
35
|
+
return rows;
|
|
36
|
+
}
|
|
37
|
+
function formatTypegenFailureMessage(options) {
|
|
38
|
+
const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;
|
|
39
|
+
const total = syntaxErrors.length + fatalErrors.length;
|
|
40
|
+
const separator = pc.dim("─".repeat(60));
|
|
41
|
+
const warehouse = warehouseId ? ` against ${pc.dim(`warehouse ${warehouseId}`)}` : "";
|
|
42
|
+
return [
|
|
43
|
+
` ${pc.bold(pc.red("Type generation failed"))}`,
|
|
44
|
+
` ${separator}`,
|
|
45
|
+
` ${title}: ${total} ${plural(total, "query", "queries")} could not be described${warehouse}.`,
|
|
46
|
+
` AppKit wrote generated types with ${pc.bold("result: unknown")} for the failed ${plural(total, "query", "queries")}.`,
|
|
47
|
+
"",
|
|
48
|
+
...formatFailureRows("SQL ERR", syntaxErrors, pc.red),
|
|
49
|
+
...syntaxErrors.length > 0 && fatalErrors.length > 0 ? [""] : [],
|
|
50
|
+
...formatFailureRows("FATAL", fatalErrors, pc.red),
|
|
51
|
+
"",
|
|
52
|
+
` ${pc.bold("Common causes")}`,
|
|
53
|
+
...options.causes.map((cause) => ` - ${cause}`),
|
|
54
|
+
"",
|
|
55
|
+
` ${pc.bold("Next step")}`,
|
|
56
|
+
` ${options.nextStep}`
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*
|
|
61
|
+
* warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),
|
|
62
|
+
* as opposed to a connectivity failure (warehouse unreachable), which degrades
|
|
63
|
+
* silently. Whether this is fatal is the caller's decision: the Vite plugin and
|
|
64
|
+
* CLI fail the build in production and warn-only in development.
|
|
65
|
+
*/
|
|
66
|
+
var TypegenSyntaxError = class extends Error {
|
|
67
|
+
queries;
|
|
68
|
+
fatalQueries;
|
|
69
|
+
constructor(queries, warehouseId, fatalQueries = []) {
|
|
70
|
+
super(formatTypegenFailureMessage({
|
|
71
|
+
syntaxErrors: queries,
|
|
72
|
+
fatalErrors: fatalQueries,
|
|
73
|
+
warehouseId,
|
|
74
|
+
title: "DESCRIBE QUERY failed",
|
|
75
|
+
causes: [
|
|
76
|
+
"SQL syntax errors",
|
|
77
|
+
"missing tables or views",
|
|
78
|
+
"warehouse format incompatibilities"
|
|
79
|
+
],
|
|
80
|
+
nextStep: warehouseId ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.` : "Run each SQL ERR query directly in a Databricks SQL editor."
|
|
81
|
+
}));
|
|
82
|
+
this.name = "TypegenSyntaxError";
|
|
83
|
+
this.queries = queries;
|
|
84
|
+
this.fatalQueries = fatalQueries;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal
|
|
89
|
+
* setup/request problem, such as missing permissions, invalid warehouse IDs, or
|
|
90
|
+
* malformed SDK configuration. Like TypegenSyntaxError, this is thrown only
|
|
91
|
+
* after the declaration file has been written with `result: unknown` entries.
|
|
92
|
+
*/
|
|
93
|
+
var TypegenFatalError = class extends Error {
|
|
94
|
+
queries;
|
|
95
|
+
constructor(queries, warehouseId) {
|
|
96
|
+
super(formatTypegenFailureMessage({
|
|
97
|
+
syntaxErrors: [],
|
|
98
|
+
fatalErrors: queries,
|
|
99
|
+
warehouseId,
|
|
100
|
+
title: "DESCRIBE QUERY could not be requested",
|
|
101
|
+
causes: [
|
|
102
|
+
"missing warehouse permissions",
|
|
103
|
+
"invalid warehouse ID",
|
|
104
|
+
"authentication failure",
|
|
105
|
+
"SDK configuration errors"
|
|
106
|
+
],
|
|
107
|
+
nextStep: warehouseId ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.` : "Verify warehouse access and rerun type generation."
|
|
108
|
+
}));
|
|
109
|
+
this.name = "TypegenFatalError";
|
|
110
|
+
this.queries = queries;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
12
113
|
/**
|
|
13
114
|
* Generate type declarations for QueryRegistry
|
|
14
115
|
* Create the d.ts file from the plugin routes and query schemas
|
|
@@ -37,26 +138,28 @@ declare module "@databricks/appkit-ui/react" {
|
|
|
37
138
|
* @param options.querySchemaFile - optional path to query schema file (e.g. config/queries/schema.ts)
|
|
38
139
|
*/
|
|
39
140
|
async function generateFromEntryPoint(options) {
|
|
40
|
-
const { outFile, queryFolder, warehouseId, noCache } = options;
|
|
141
|
+
const { outFile, queryFolder, warehouseId, noCache, mode = "non-blocking" } = options;
|
|
41
142
|
const projectRoot = resolveProjectRoot(outFile);
|
|
42
143
|
logger.debug("Starting type generation...");
|
|
43
144
|
let queryRegistry = [];
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
]
|
|
145
|
+
let syntaxErrors = [];
|
|
146
|
+
let fatalErrors = [];
|
|
147
|
+
if (queryFolder) {
|
|
148
|
+
const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {
|
|
149
|
+
noCache,
|
|
150
|
+
mode
|
|
151
|
+
});
|
|
152
|
+
queryRegistry = result.schemas;
|
|
153
|
+
syntaxErrors = result.syntaxErrors ?? [];
|
|
154
|
+
fatalErrors = result.fatalErrors ?? [];
|
|
54
155
|
}
|
|
55
156
|
const typeDeclarations = generateTypeDeclarations(queryRegistry);
|
|
56
157
|
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
57
158
|
await fs.writeFile(outFile, typeDeclarations, "utf-8");
|
|
58
159
|
await removeOldGeneratedTypes(projectRoot, "appKitTypes.d.ts");
|
|
59
160
|
await migrateProjectConfig(projectRoot);
|
|
161
|
+
if (syntaxErrors.length > 0) throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);
|
|
162
|
+
if (fatalErrors.length > 0) throw new TypegenFatalError(fatalErrors, warehouseId);
|
|
60
163
|
logger.debug("Type generation complete!");
|
|
61
164
|
}
|
|
62
165
|
const generateServingTypes = generateServingTypes$1;
|
|
@@ -68,5 +171,5 @@ const ANALYTICS_TYPES_FILE = "analytics.d.ts";
|
|
|
68
171
|
const SERVING_TYPES_FILE = "serving.d.ts";
|
|
69
172
|
|
|
70
173
|
//#endregion
|
|
71
|
-
export { ANALYTICS_TYPES_FILE, SERVING_TYPES_FILE, TYPES_DIR, generateFromEntryPoint, generateServingTypes };
|
|
174
|
+
export { ANALYTICS_TYPES_FILE, SERVING_TYPES_FILE, TYPES_DIR, TypegenFatalError, TypegenSyntaxError, generateFromEntryPoint, generateServingTypes };
|
|
72
175
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QuerySchema } from \"./types\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.querySchemaFile - optional path to query schema file (e.g. config/queries/schema.ts)\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n}) {\n const { outFile, queryFolder, warehouseId, noCache } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n if (queryFolder)\n queryRegistry = await generateQueriesFromDescribe(\n queryFolder,\n warehouseId,\n {\n noCache,\n },\n );\n\n const failedQueries = queryRegistry.filter((q) =>\n q.type.includes(\"result: unknown\"),\n );\n if (failedQueries.length > 0) {\n const names = failedQueries.map((q) => q.name).join(\", \");\n throw new Error(\n [\n `Type generation failed: ${failedQueries.length} ${failedQueries.length === 1 ? \"query\" : \"queries\"} could not be described: ${names}.`,\n `DESCRIBE QUERY failed for these queries — see the error codes above for details.`,\n `Common causes: SQL syntax errors, missing tables/views, or warehouse format incompatibilities.`,\n `To debug: run the failing query directly in a SQL editor against warehouse ${warehouseId}.`,\n ].join(\"\\n\"),\n );\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n logger.debug(\"Type generation complete!\");\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n/** Directory name for generated AppKit type declaration files. */\nexport const TYPES_DIR = \"appkit-types\";\n/** Default filename for analytics query type declarations. */\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\n/** Default filename for serving endpoint type declarations. */\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\n"],"mappings":";;;;;;;;;AAaA,OAAO,QAAQ;AAEf,MAAM,SAAS,aAAa,iBAAiB;;;;;;;AAQ7C,SAAS,yBAAyB,eAA8B,EAAE,EAAU;CAC1E,MAAM,eAAe,aAClB,KAAK,EAAE,MAAM,WAAW;AAKvB,SAAO,OAAO,KAAK,IAJE,KAClB,MAAM,KAAK,CACX,KAAK,MAAM,MAAO,MAAM,IAAI,OAAO,OAAO,OAAQ,CAClD,KAAK,KAAK;GAEb,CACD,KAAK,MAAM;AAId,QAAO;;;;;;6BAFc,eAAe,KAAK,aAAa,SAAS,GAQvB;;;;;;;;;;;AAY1C,eAAsB,uBAAuB,SAK1C;CACD,MAAM,EAAE,SAAS,aAAa,aAAa,YAAY;CACvD,MAAM,cAAc,mBAAmB,QAAQ;AAE/C,QAAO,MAAM,8BAA8B;CAE3C,IAAI,gBAA+B,EAAE;AACrC,KAAI,YACF,iBAAgB,MAAM,4BACpB,aACA,aACA,EACE,SACD,CACF;CAEH,MAAM,gBAAgB,cAAc,QAAQ,MAC1C,EAAE,KAAK,SAAS,kBAAkB,CACnC;AACD,KAAI,cAAc,SAAS,GAAG;EAC5B,MAAM,QAAQ,cAAc,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;AACzD,QAAM,IAAI,MACR;GACE,2BAA2B,cAAc,OAAO,GAAG,cAAc,WAAW,IAAI,UAAU,UAAU,2BAA2B,MAAM;GACrI;GACA;GACA,8EAA8E,YAAY;GAC3F,CAAC,KAAK,KAAK,CACb;;CAGH,MAAM,mBAAmB,yBAAyB,cAAc;AAEhE,OAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,EAAE,EAAE,WAAW,MAAM,CAAC;AAC1D,OAAM,GAAG,UAAU,SAAS,kBAAkB,QAAQ;AAGtD,OAAM,wBAAwB,aAAa,mBAAmB;AAC9D,OAAM,qBAAqB,YAAY;AAEvC,QAAO,MAAM,4BAA4B;;AAM3C,MAAa,uBAAuBA;;AAGpC,MAAa,YAAY;;AAEzB,MAAa,uBAAuB;;AAEpC,MAAa,qBAAqB"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport type { PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n // Unique message → keep the compact one-line `tag name message` form.\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.querySchemaFile - optional path to query schema file (e.g. config/queries/schema.ts)\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any query\n // that could not be described. Connectivity failures pass silently so a\n // transient warehouse outage never blocks a build; genuine SQL errors and\n // non-connectivity fatal request failures surface after the file write.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n/** Directory name for generated AppKit type declaration files. */\nexport const TYPES_DIR = \"appkit-types\";\n/** Default filename for analytics query type declarations. */\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\n/** Default filename for serving endpoint type declarations. */\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/type-generator/preflight.ts
|
|
2
|
+
/**
|
|
3
|
+
* Pure policy mapping a warehouse state + mode to a preflight decision.
|
|
4
|
+
*
|
|
5
|
+
* Unknown/unexpected states fall through to `proceed`: the describe loop and
|
|
6
|
+
* its per-query backstop already degrade gracefully, so we don't want a new
|
|
7
|
+
* SDK state value to turn into a spurious `fatal`.
|
|
8
|
+
*/
|
|
9
|
+
function decidePreflight(state, mode) {
|
|
10
|
+
if (mode === "non-blocking") return "degradeAll";
|
|
11
|
+
switch (state) {
|
|
12
|
+
case "RUNNING": return "proceed";
|
|
13
|
+
case "STARTING": return "waitThenProceed";
|
|
14
|
+
case "STOPPED":
|
|
15
|
+
case "STOPPING": return "startWaitProceed";
|
|
16
|
+
case "DELETED":
|
|
17
|
+
case "DELETING": return "fatal";
|
|
18
|
+
default: return "proceed";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { decidePreflight };
|
|
24
|
+
//# sourceMappingURL=preflight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preflight.js","names":[],"sources":["../../src/type-generator/preflight.ts"],"sourcesContent":["import type { WarehouseState } from \"./warehouse-status\";\n\n/**\n * How aggressively typegen should react to a not-ready warehouse.\n * - `non-blocking`: never describe and never probe the warehouse — emit\n * best-available types (cache where the SQL hash matches, else `unknown`) and\n * return at once. The default for interactive/foreground runs that can't\n * afford to block on (or fail because of) a warehouse, even a RUNNING one.\n * - `blocking`: a startable warehouse is worth waiting for, and a stopped one\n * is worth starting — only a deleted/deleting warehouse is a hard failure.\n */\nexport type PreflightMode = \"non-blocking\" | \"blocking\";\n\n/**\n * What the caller should do given a warehouse state and mode.\n * - `proceed`: run DESCRIBE now.\n * - `degradeAll`: skip DESCRIBE; emit degraded (cached/`unknown`) types.\n * - `waitThenProceed`: wait for the warehouse to start, then run DESCRIBE.\n * - `startWaitProceed`: start the stopped warehouse, wait for RUNNING, then\n * run DESCRIBE.\n * - `fatal`: stop — the warehouse can't serve this run.\n */\nexport type PreflightDecision =\n | \"proceed\"\n | \"degradeAll\"\n | \"waitThenProceed\"\n | \"startWaitProceed\"\n | \"fatal\";\n\n/**\n * Pure policy mapping a warehouse state + mode to a preflight decision.\n *\n * Unknown/unexpected states fall through to `proceed`: the describe loop and\n * its per-query backstop already degrade gracefully, so we don't want a new\n * SDK state value to turn into a spurious `fatal`.\n */\nexport function decidePreflight(\n state: WarehouseState,\n mode: PreflightMode,\n): PreflightDecision {\n // `non-blocking` never describes regardless of state: emit cached/`unknown`\n // types and return. The caller short-circuits before probing, so this is only\n // a belt-and-suspenders mapping, but it keeps the policy total and\n // self-contained.\n if (mode === \"non-blocking\") return \"degradeAll\";\n\n // `blocking`: a starting warehouse is worth waiting for, a stopped one is\n // worth starting (then waiting), and only a deleted/deleting one is fatal.\n switch (state) {\n case \"RUNNING\":\n return \"proceed\";\n case \"STARTING\":\n return \"waitThenProceed\";\n case \"STOPPED\":\n case \"STOPPING\":\n return \"startWaitProceed\";\n case \"DELETED\":\n case \"DELETING\":\n return \"fatal\";\n default:\n return \"proceed\";\n }\n}\n"],"mappings":";;;;;;;;AAoCA,SAAgB,gBACd,OACA,MACmB;AAKnB,KAAI,SAAS,eAAgB,QAAO;AAIpC,SAAQ,OAAR;EACE,KAAK,UACH,QAAO;EACT,KAAK,WACH,QAAO;EACT,KAAK;EACL,KAAK,WACH,QAAO;EACT,KAAK;EACL,KAAK,WACH,QAAO;EACT,QACE,QAAO"}
|