@dbx-tools/core 0.6.88 → 0.6.90
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/README.md +52 -27
- package/index.ts +2 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -2
- package/lib/src/config.d.ts +70 -72
- package/lib/src/config.js +362 -342
- package/lib/src/file.d.ts +8 -0
- package/lib/src/file.js +16 -1
- package/lib/src/project.d.ts +8 -5
- package/lib/src/project.js +59 -42
- package/package.json +2 -11
- package/src/config.ts +399 -355
- package/src/file.ts +19 -0
- package/src/project.ts +65 -50
package/src/config.ts
CHANGED
|
@@ -1,47 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Layered configuration lookup: environment, `.env`,
|
|
2
|
+
* Layered configuration lookup: environment, `.env`, bundle, and `app.yaml`.
|
|
3
3
|
*
|
|
4
4
|
* Every package resolves settings the same way: take the caller's value, else
|
|
5
5
|
* an environment variable, else a default. Local development adds two fallback
|
|
6
|
-
* locations: `.env` files
|
|
7
|
-
* `
|
|
6
|
+
* locations: `.env` files, one App's `resources.apps.<app>.config.env` in
|
|
7
|
+
* `databricks.yml`, and literal env values in `app.yaml` / `app.yml`.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* {@link values} is LAZY (an `object.Sequence`), so `databricks bundle validate`
|
|
10
|
+
* is only spawned when the environment and `.env` both missed; app YAML is only
|
|
11
|
+
* read after the bundle source also missed.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* validate` is only spawned when the environment and `.env` both missed.
|
|
13
|
-
* - the parsed `.env` and bundle are cached through {@link context.cached}, so
|
|
14
|
-
* the spawn happens once per working directory and a `cwd` change misses
|
|
15
|
-
* rather than returning another project's config.
|
|
16
|
-
*
|
|
17
|
-
* Inside a deployed Databricks App both file sources are skipped by default
|
|
13
|
+
* Inside a deployed Databricks App all three file sources are skipped by default
|
|
18
14
|
* ({@link isDatabricksAppEnv}): the platform has already turned them into real
|
|
19
15
|
* environment variables, there is no bundle to validate, and the `databricks`
|
|
20
16
|
* CLI is not on the image. Boolean environment overrides can force either file
|
|
21
17
|
* source on or off when a tool needs different behavior.
|
|
22
18
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* 1. `@databricks/appkit` is resolved WITHOUT evaluating it. It is an optional
|
|
29
|
-
* peer, so a consumer that never installed it is not an AppKit project and
|
|
30
|
-
* the bundle is never loaded. The probe is cached, so this costs one
|
|
31
|
-
* resolution for the life of the process.
|
|
32
|
-
* 2. The bundle is read (once per context) and must describe EXACTLY ONE app
|
|
33
|
-
* with `config.env`. Nothing here says which of several apps this process
|
|
34
|
-
* is, so an ambiguous bundle contributes nothing rather than a guess.
|
|
35
|
-
* 3. AppKit's execution context confirms this process really is that app. The
|
|
36
|
-
* context does not exist until AppKit boots and `getExecutionContext()`
|
|
37
|
-
* THROWS until then, so the probe is caught and only the affirmative is
|
|
38
|
-
* remembered - a lookup before boot still resolves, and re-confirms later
|
|
39
|
-
* once the context is available.
|
|
40
|
-
*
|
|
41
|
-
* Only the single app's `config.env` is consulted. Root bundle `variables` are
|
|
42
|
-
* not: they are authoring inputs for the bundle itself (interpolated into
|
|
43
|
-
* targets, resources, and paths), so treating one as a process setting resolves
|
|
44
|
-
* names the deployed app never sees.
|
|
19
|
+
* Only the single bundle app's `config.env` and literal app YAML `env[].value`
|
|
20
|
+
* entries are consulted. Root bundle `variables` are not: they are authoring
|
|
21
|
+
* inputs for the bundle itself (interpolated into targets, resources, and paths),
|
|
22
|
+
* so treating one as a process setting resolves names the deployed app never sees.
|
|
45
23
|
*
|
|
46
24
|
* Node-only (`child_process`, `fs`, `process`).
|
|
47
25
|
*
|
|
@@ -50,27 +28,24 @@
|
|
|
50
28
|
|
|
51
29
|
import { spawnSync } from "node:child_process";
|
|
52
30
|
import { readFileSync } from "node:fs";
|
|
53
|
-
import { createRequire } from "node:module";
|
|
54
31
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
55
32
|
import { parseEnv } from "node:util";
|
|
56
|
-
import {
|
|
57
|
-
|
|
58
|
-
functionModule,
|
|
59
|
-
json,
|
|
60
|
-
log,
|
|
61
|
-
object,
|
|
62
|
-
string as stringModule,
|
|
63
|
-
} from "@dbx-tools/shared-core";
|
|
33
|
+
import { json, log, object, string as sharedString } from "@dbx-tools/shared-core";
|
|
34
|
+
import { parse as parseYaml } from "yaml";
|
|
64
35
|
import { z } from "zod";
|
|
65
|
-
import { statSync } from "./file.ts";
|
|
66
|
-
import { root as resolveProjectRoot } from "./project.ts";
|
|
36
|
+
import { cachedRecord, statSync } from "./file.ts";
|
|
37
|
+
import { resolveWorkingDirectory, root as resolveProjectRoot } from "./project.ts";
|
|
67
38
|
|
|
68
39
|
const logger = log.logger("config");
|
|
40
|
+
const configFileCache = new Map<string, string | undefined>();
|
|
69
41
|
|
|
70
|
-
|
|
42
|
+
type ConfigKey = string | readonly string[];
|
|
43
|
+
|
|
44
|
+
export type ConfigMapValue = string | readonly string[] | null | undefined;
|
|
45
|
+
export type ConfigData = Readonly<Record<string, ConfigMapValue>>;
|
|
71
46
|
|
|
72
47
|
/** Where a value may come from, consulted in the order given. */
|
|
73
|
-
export type ConfigSource = "env" | "dotenv" | "bundle";
|
|
48
|
+
export type ConfigSource = "config" | "env" | "dotenv" | "bundle" | "app";
|
|
74
49
|
|
|
75
50
|
export interface ConfigOptions {
|
|
76
51
|
/**
|
|
@@ -79,10 +54,16 @@ export interface ConfigOptions {
|
|
|
79
54
|
scope?: string | readonly string[];
|
|
80
55
|
/** Capability namespaces inserted after the scope and before each key. */
|
|
81
56
|
prefix?: string | readonly string[];
|
|
57
|
+
/** Constant config maps read by the `config` source. */
|
|
58
|
+
data?: ConfigData | readonly ConfigData[];
|
|
59
|
+
/** Parsed bundle data used instead of loading `databricks.yml`. */
|
|
60
|
+
bundleData?: ConfigFile | Record<string, unknown>;
|
|
61
|
+
/** Parsed app YAML data used instead of loading `app.yaml`. */
|
|
62
|
+
appData?: ConfigFile | Record<string, unknown>;
|
|
63
|
+
/** Sources in precedence order. Default: `config`, `env`, `dotenv`, `bundle`, `app`. */
|
|
64
|
+
sources?: ConfigSource | readonly ConfigSource[];
|
|
82
65
|
/** Directory to resolve `.env` and the bundle from. Default: `process.cwd()`. */
|
|
83
66
|
cwd?: string;
|
|
84
|
-
/** Sources in precedence order. Default: `env`, `dotenv`, `bundle`. */
|
|
85
|
-
sources?: ConfigSource | readonly ConfigSource[];
|
|
86
67
|
}
|
|
87
68
|
|
|
88
69
|
/** One resolved value, tagged with the key and source it came from. */
|
|
@@ -99,8 +80,9 @@ export interface ConfigFile {
|
|
|
99
80
|
}
|
|
100
81
|
|
|
101
82
|
const DEFAULT_SCOPE = "DBX_TOOLS";
|
|
102
|
-
const DEFAULT_SOURCES: readonly ConfigSource[] = ["env", "dotenv", "bundle"];
|
|
83
|
+
const DEFAULT_SOURCES: readonly ConfigSource[] = ["config", "env", "dotenv", "bundle", "app"];
|
|
103
84
|
const BUNDLE_FILE_NAMES = ["databricks.yml", "databricks.yaml"] as const;
|
|
85
|
+
const APP_FILE_NAMES = ["app.yaml", "app.yml"] as const;
|
|
104
86
|
const DOTENV_FILE_NAME = ".env";
|
|
105
87
|
const NODE_ENV_ALTERNATIVES = {
|
|
106
88
|
production: ["prod"],
|
|
@@ -111,77 +93,171 @@ const NODE_ENV_ALTERNATIVES = {
|
|
|
111
93
|
export const MAX_TCP_PORT = 65_535;
|
|
112
94
|
|
|
113
95
|
/** Boolean environment override for {@link isDatabricksAppEnv}. */
|
|
114
|
-
|
|
96
|
+
const DATABRICKS_APP_ENV_KEY = "DBX_TOOLS_DATABRICKS_APP_ENV";
|
|
115
97
|
|
|
116
98
|
/** Boolean environment override for project `.env` reads. */
|
|
117
|
-
|
|
99
|
+
const CONFIG_DOTENV_KEY = "DBX_TOOLS_CONFIG_DOTENV";
|
|
118
100
|
|
|
119
101
|
/** Boolean environment override for Databricks bundle reads. */
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* The OPTIONAL AppKit peer whose presence marks this as a Databricks App
|
|
124
|
-
* project. Held as a constant so the resolve probe and the lazy import name it
|
|
125
|
-
* once, and so neither site carries a literal a bundler would try to follow.
|
|
126
|
-
*/
|
|
127
|
-
const APPKIT_MODULE = "@databricks/appkit";
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* The one thing this module needs from AppKit: whether a real execution context
|
|
131
|
-
* is active. Typed structurally so an OPTIONAL peer never becomes a type
|
|
132
|
-
* dependency, and `client` is `unknown` because its presence is all that is read.
|
|
133
|
-
*/
|
|
134
|
-
type AppKitModule = {
|
|
135
|
-
getExecutionContext(): { client?: unknown } | undefined;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Whether AppKit's execution context has EVER been observed. Latching is the
|
|
140
|
-
* point: a context confirms this process is the bundle's app, and that fact does
|
|
141
|
-
* not stop being true when a later lookup happens outside a request scope.
|
|
142
|
-
*/
|
|
143
|
-
let appKitContext = false;
|
|
102
|
+
const CONFIG_BUNDLE_KEY = "DBX_TOOLS_CONFIG_BUNDLE";
|
|
144
103
|
|
|
145
|
-
/**
|
|
146
|
-
const
|
|
104
|
+
/** Boolean environment override for Databricks App YAML reads. */
|
|
105
|
+
const CONFIG_APP_KEY = "DBX_TOOLS_CONFIG_APP";
|
|
147
106
|
|
|
148
107
|
/** Exact process-environment lookup for callers that do not read local config files. */
|
|
149
108
|
export const ENV_ONLY = { scope: [] as const, sources: "env" as const };
|
|
150
109
|
|
|
151
|
-
export const
|
|
110
|
+
export const valueSchema = z
|
|
152
111
|
.string()
|
|
153
|
-
.
|
|
154
|
-
.
|
|
112
|
+
.trim()
|
|
113
|
+
.min(1)
|
|
155
114
|
.refine((value: string) => !/\$\{[^}]+\}/.test(value), {
|
|
156
115
|
message: "Interpolated values are not allowed",
|
|
157
116
|
});
|
|
158
117
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
118
|
+
const portValueSchema = z.preprocess(
|
|
119
|
+
(input) => object.toNumber(input),
|
|
120
|
+
z.number().int().min(1).max(MAX_TCP_PORT),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const positiveNumberValue = z.preprocess((input) => object.toNumber(input), z.number().positive());
|
|
124
|
+
const positiveIntValue = positiveNumberValue.transform(Math.floor);
|
|
125
|
+
|
|
126
|
+
const workspaceHostSchema = valueSchema.refine(
|
|
127
|
+
(host) => {
|
|
128
|
+
try {
|
|
129
|
+
return ["http:", "https:"].includes(new URL(host).protocol);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{ message: "Must be an HTTP(S) URL" },
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const databricksAppEnvSchema = z.object({
|
|
138
|
+
DATABRICKS_APP_NAME: valueSchema,
|
|
139
|
+
DATABRICKS_HOST: workspaceHostSchema,
|
|
140
|
+
DATABRICKS_APP_PORT: portValueSchema,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
/** A named bundle or App resource; concrete resource fields pass through. */
|
|
144
|
+
export const bundleResourceSchema = z.object({ name: valueSchema.optional() }).passthrough();
|
|
167
145
|
|
|
168
146
|
export const bundleEnvEntrySchema = z.object({
|
|
169
|
-
name:
|
|
147
|
+
name: valueSchema.optional(),
|
|
170
148
|
value: z.string().optional(),
|
|
171
|
-
value_from:
|
|
149
|
+
value_from: valueSchema.optional(),
|
|
172
150
|
});
|
|
173
151
|
|
|
174
152
|
export const bundleAppSchema = z.object({
|
|
175
|
-
name:
|
|
176
|
-
source_code_path: z.string().optional(),
|
|
153
|
+
name: valueSchema.optional(),
|
|
177
154
|
config: z.object({ env: z.array(bundleEnvEntrySchema).optional() }).optional(),
|
|
178
155
|
resources: z.array(bundleResourceSchema).optional(),
|
|
179
156
|
});
|
|
180
157
|
|
|
181
|
-
const
|
|
158
|
+
export const appEnvEntrySchema = z.object({
|
|
159
|
+
name: valueSchema,
|
|
160
|
+
value: z.string().optional(),
|
|
161
|
+
valueFrom: valueSchema.optional(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export const appSchema = z.object({
|
|
165
|
+
env: z.array(appEnvEntrySchema).optional(),
|
|
166
|
+
resources: z.array(bundleResourceSchema).optional(),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const bundleConfigSchema = z.object({
|
|
182
170
|
resources: z.object({ apps: z.record(z.string(), bundleAppSchema).optional() }).optional(),
|
|
183
171
|
});
|
|
184
172
|
|
|
173
|
+
/** Flatten the single bundle App's `config.env`, resolving resource references. */
|
|
174
|
+
export function flattenBundleEnv(data: unknown): Record<string, string> {
|
|
175
|
+
const parsed = bundleConfigSchema.safeParse(data);
|
|
176
|
+
if (!parsed.success) return {};
|
|
177
|
+
const apps = Object.values(parsed.data.resources?.apps ?? {});
|
|
178
|
+
if (apps.length !== 1) return {};
|
|
179
|
+
const app = apps[0];
|
|
180
|
+
const resources = new Map(
|
|
181
|
+
(app?.resources ?? [])
|
|
182
|
+
.filter((resource) => resource.name)
|
|
183
|
+
.map((resource) => [resource.name!, resource] as const),
|
|
184
|
+
);
|
|
185
|
+
const result: Record<string, string> = {};
|
|
186
|
+
for (const entry of app?.config?.env ?? []) {
|
|
187
|
+
if (!entry.name) continue;
|
|
188
|
+
const literal = valueSchema.safeParse(entry.value);
|
|
189
|
+
if (literal.success) {
|
|
190
|
+
result[entry.name] = literal.data;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const referenced = entry.value_from
|
|
194
|
+
? resourceValue(resources.get(entry.value_from))
|
|
195
|
+
: undefined;
|
|
196
|
+
if (referenced) result[entry.name] = referenced;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Flatten `app.yaml` env entries, resolving `valueFrom` resource references. */
|
|
202
|
+
export function flattenAppEnv(data: unknown): Record<string, string> {
|
|
203
|
+
const parsed = appSchema.safeParse(data);
|
|
204
|
+
if (!parsed.success) return {};
|
|
205
|
+
const resources = new Map(
|
|
206
|
+
(parsed.data.resources ?? [])
|
|
207
|
+
.filter((resource) => resource.name)
|
|
208
|
+
.map((resource) => [resource.name!, resource] as const),
|
|
209
|
+
);
|
|
210
|
+
const result: Record<string, string> = {};
|
|
211
|
+
for (const entry of parsed.data.env ?? []) {
|
|
212
|
+
const literal = valueSchema.safeParse(entry.value);
|
|
213
|
+
if (literal.success) {
|
|
214
|
+
result[entry.name] = literal.data;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const referenced = entry.valueFrom ? resourceValue(resources.get(entry.valueFrom)) : undefined;
|
|
218
|
+
if (referenced) result[entry.name] = referenced;
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resourceValue(resource: unknown): string | undefined {
|
|
224
|
+
for (const path of [
|
|
225
|
+
["sql_warehouse", "id"],
|
|
226
|
+
["genie_space", "space_id"],
|
|
227
|
+
["postgres", "endpoint"],
|
|
228
|
+
["postgres", "database"],
|
|
229
|
+
["postgres", "branch"],
|
|
230
|
+
] as const) {
|
|
231
|
+
let value = resource;
|
|
232
|
+
for (const part of path) value = object.isRecord(value) ? value[part] : undefined;
|
|
233
|
+
const parsed = valueSchema.safeParse(value);
|
|
234
|
+
if (parsed.success) return parsed.data;
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Walk a dot-separated path through parsed bundle data. */
|
|
240
|
+
export function getBundlePath(data: Record<string, unknown>, path: string): string | undefined {
|
|
241
|
+
const parts = path.split(".").filter(Boolean);
|
|
242
|
+
if (parts.length === 0) return undefined;
|
|
243
|
+
let current: unknown = data;
|
|
244
|
+
for (let index = 0; index < parts.length; index++) {
|
|
245
|
+
if (!object.isRecord(current)) return undefined;
|
|
246
|
+
const next = current[parts[index]!];
|
|
247
|
+
if (index === parts.length - 1) {
|
|
248
|
+
const direct = valueSchema.safeParse(next);
|
|
249
|
+
if (direct.success) return direct.data;
|
|
250
|
+
if (object.isRecord(next)) {
|
|
251
|
+
const nested = valueSchema.safeParse(next.value);
|
|
252
|
+
return nested.success ? nested.data : undefined;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
current = next;
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
185
261
|
/**
|
|
186
262
|
* Detect a Databricks App runtime from its required name, host, and port.
|
|
187
263
|
*
|
|
@@ -195,60 +271,78 @@ export function isDatabricksAppEnv(
|
|
|
195
271
|
): boolean {
|
|
196
272
|
const override = object.toBoolean(source[DATABRICKS_APP_ENV_KEY]);
|
|
197
273
|
if (override !== undefined) return override;
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
274
|
+
const parsed = databricksAppEnvSchema.safeParse(source);
|
|
275
|
+
return parsed.success;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Exact, uppercase, and tokenized-uppercase names for a human-friendly key. */
|
|
279
|
+
export function environmentKeys(name: string): readonly string[] {
|
|
280
|
+
const trimmed = name.trim();
|
|
281
|
+
if (!trimmed) return [];
|
|
282
|
+
return object
|
|
283
|
+
.sequence(
|
|
284
|
+
trimmed,
|
|
285
|
+
trimmed.toUpperCase(),
|
|
286
|
+
Array.from(sharedString.tokenize(trimmed)).join("_").toUpperCase(),
|
|
287
|
+
)
|
|
288
|
+
.filter(Boolean)
|
|
289
|
+
.distinct()
|
|
290
|
+
.toArray();
|
|
214
291
|
}
|
|
215
292
|
|
|
216
293
|
/** Candidate names in scope, prefix, and input precedence order. */
|
|
217
|
-
function keys(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
294
|
+
function keys(
|
|
295
|
+
input: ConfigKey,
|
|
296
|
+
options: Pick<ConfigOptions, "scope" | "prefix"> = {},
|
|
297
|
+
): readonly string[] {
|
|
298
|
+
const scopes = object
|
|
299
|
+
.sequence(options.scope ?? DEFAULT_SCOPE)
|
|
300
|
+
.map(sharedString.trimToEmpty)
|
|
301
|
+
.filter(Boolean)
|
|
302
|
+
.distinct()
|
|
303
|
+
.toArray();
|
|
304
|
+
const prefixes =
|
|
305
|
+
options.prefix === undefined
|
|
306
|
+
? []
|
|
307
|
+
: object
|
|
308
|
+
.sequence(options.prefix)
|
|
309
|
+
.map(sharedString.trimToEmpty)
|
|
310
|
+
.filter(Boolean)
|
|
311
|
+
.distinct()
|
|
312
|
+
.toArray();
|
|
313
|
+
return object
|
|
314
|
+
.sequence(input)
|
|
315
|
+
.map(sharedString.trimToEmpty)
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.distinct()
|
|
318
|
+
.flatMap((key) => {
|
|
319
|
+
const prefixed = prefixes.length > 0 ? prefixes.map((prefix) => `${prefix}_${key}`) : [key];
|
|
320
|
+
return [
|
|
321
|
+
...scopes.flatMap((scope) => prefixed.map((name) => `${scope}_${name}`)),
|
|
322
|
+
...prefixed,
|
|
323
|
+
key,
|
|
324
|
+
];
|
|
325
|
+
})
|
|
326
|
+
.distinct()
|
|
327
|
+
.toArray();
|
|
234
328
|
}
|
|
235
329
|
|
|
236
330
|
/**
|
|
237
331
|
* Every value that resolves for `input`, in source-then-key precedence order,
|
|
238
332
|
* as a LAZY sequence: nothing past the first consumed element is read, so
|
|
239
|
-
* `values(key).
|
|
333
|
+
* `values(key).at(0)` never spawns the Databricks CLI when the environment
|
|
240
334
|
* already answered.
|
|
241
335
|
*
|
|
242
336
|
*/
|
|
243
337
|
function values(input: ConfigKey, options: ConfigOptions = {}): object.Sequence<ConfigValue> {
|
|
244
338
|
const candidates = keys(input, options);
|
|
245
|
-
const sources =
|
|
339
|
+
const sources = configSources(options);
|
|
246
340
|
return object.sequence({
|
|
247
341
|
*[Symbol.iterator](): Generator<ConfigValue> {
|
|
248
342
|
for (const source of sources) {
|
|
249
|
-
for (const map of read(source, options
|
|
343
|
+
for (const map of read(source, options)) {
|
|
250
344
|
for (const key of candidates) {
|
|
251
|
-
const value =
|
|
345
|
+
const value = configMapValue(map[key]);
|
|
252
346
|
if (value !== null) yield { key, source, value };
|
|
253
347
|
}
|
|
254
348
|
}
|
|
@@ -257,6 +351,27 @@ function values(input: ConfigKey, options: ConfigOptions = {}): object.Sequence<
|
|
|
257
351
|
});
|
|
258
352
|
}
|
|
259
353
|
|
|
354
|
+
function configSources(options: ConfigOptions): readonly ConfigSource[] {
|
|
355
|
+
const sources: ConfigSource[] = [
|
|
356
|
+
...object.sequence<ConfigSource>(options.sources ?? DEFAULT_SOURCES).distinct(),
|
|
357
|
+
];
|
|
358
|
+
if (options.data !== undefined && options.sources !== undefined && !sources.includes("config")) {
|
|
359
|
+
sources.push("config");
|
|
360
|
+
}
|
|
361
|
+
return sources;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function configMapValue(value: ConfigMapValue | unknown): string | null {
|
|
365
|
+
if (Array.isArray(value)) {
|
|
366
|
+
for (const entry of value) {
|
|
367
|
+
const resolved = sharedString.trimToNull(entry);
|
|
368
|
+
if (resolved !== null) return resolved;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
return sharedString.trimToNull(value);
|
|
373
|
+
}
|
|
374
|
+
|
|
260
375
|
/**
|
|
261
376
|
* The first value that resolves for `input`, or `undefined`.
|
|
262
377
|
*
|
|
@@ -267,6 +382,11 @@ export function text(input: ConfigKey, options: ConfigOptions = {}): string | un
|
|
|
267
382
|
return values(input, options).at(0)?.value;
|
|
268
383
|
}
|
|
269
384
|
|
|
385
|
+
/** Resolve a human-friendly name through {@link environmentKeys} and {@link text}. */
|
|
386
|
+
export function resolveValue(name: string, options: ConfigOptions = {}): string | undefined {
|
|
387
|
+
return text(environmentKeys(name), options);
|
|
388
|
+
}
|
|
389
|
+
|
|
270
390
|
/**
|
|
271
391
|
* The PRIMARY (fully-scoped) name for `input` - what to print in a log line or an
|
|
272
392
|
* error, so the message names the variable a reader should set. Do not index
|
|
@@ -287,7 +407,7 @@ export function name(
|
|
|
287
407
|
* Known long and short names are interchangeable; unknown names pass through.
|
|
288
408
|
*/
|
|
289
409
|
function nodeEnvNames(nodeEnv: unknown): string[] {
|
|
290
|
-
const name =
|
|
410
|
+
const name = sharedString.trimToNull(nodeEnv)?.toLowerCase();
|
|
291
411
|
if (!name || !/^[a-z0-9_-]+$/.test(name)) return [];
|
|
292
412
|
for (const [canonical, alternatives] of Object.entries(NODE_ENV_ALTERNATIVES)) {
|
|
293
413
|
const names: readonly string[] = alternatives;
|
|
@@ -298,13 +418,6 @@ function nodeEnvNames(nodeEnv: unknown): string[] {
|
|
|
298
418
|
return [name];
|
|
299
419
|
}
|
|
300
420
|
|
|
301
|
-
function join(...parts: string[]): string {
|
|
302
|
-
return parts
|
|
303
|
-
.map((part) => part.trim())
|
|
304
|
-
.filter(Boolean)
|
|
305
|
-
.join("_");
|
|
306
|
-
}
|
|
307
|
-
|
|
308
421
|
/**
|
|
309
422
|
* Resolve a string: `configured` when non-empty, else {@link text}, else `undefined`.
|
|
310
423
|
*
|
|
@@ -319,7 +432,7 @@ export function string(
|
|
|
319
432
|
input: ConfigKey,
|
|
320
433
|
options?: ConfigOptions,
|
|
321
434
|
): string | undefined {
|
|
322
|
-
return
|
|
435
|
+
return sharedString.trimToNull(configured) ?? text(input, options);
|
|
323
436
|
}
|
|
324
437
|
|
|
325
438
|
/**
|
|
@@ -344,7 +457,10 @@ export function positiveNumber(
|
|
|
344
457
|
fallback: number,
|
|
345
458
|
options?: ConfigOptions,
|
|
346
459
|
): number {
|
|
347
|
-
|
|
460
|
+
const fromConfig = positiveNumberValue.safeParse(configured);
|
|
461
|
+
if (fromConfig.success) return fromConfig.data;
|
|
462
|
+
const fromSources = positiveNumberValue.safeParse(text(input, options));
|
|
463
|
+
return fromSources.success ? fromSources.data : fallback;
|
|
348
464
|
}
|
|
349
465
|
|
|
350
466
|
/**
|
|
@@ -358,8 +474,27 @@ export function positiveInt(
|
|
|
358
474
|
fallback: number,
|
|
359
475
|
options?: ConfigOptions,
|
|
360
476
|
): number {
|
|
361
|
-
const
|
|
362
|
-
return
|
|
477
|
+
const fromConfig = positiveIntValue.safeParse(configured);
|
|
478
|
+
if (fromConfig.success) return fromConfig.data;
|
|
479
|
+
const fromSources = positiveIntValue.safeParse(text(input, options));
|
|
480
|
+
return fromSources.success ? fromSources.data : Math.floor(fallback);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Resolve a TCP port between 1 and {@link MAX_TCP_PORT}. Invalid configured or
|
|
485
|
+
* sourced values fall back to the caller's default, which may be a sentinel
|
|
486
|
+
* such as `0` when the caller uses one.
|
|
487
|
+
*/
|
|
488
|
+
export function port(
|
|
489
|
+
configured: unknown,
|
|
490
|
+
input: ConfigKey,
|
|
491
|
+
fallback: number,
|
|
492
|
+
options?: ConfigOptions,
|
|
493
|
+
): number {
|
|
494
|
+
const fromConfig = portValueSchema.safeParse(configured);
|
|
495
|
+
if (fromConfig.success) return fromConfig.data;
|
|
496
|
+
const fromSources = portValueSchema.safeParse(text(input, options));
|
|
497
|
+
return fromSources.success ? fromSources.data : fallback;
|
|
363
498
|
}
|
|
364
499
|
|
|
365
500
|
/**
|
|
@@ -372,221 +507,107 @@ export function list(
|
|
|
372
507
|
transform?: (entry: string) => string,
|
|
373
508
|
options?: ConfigOptions,
|
|
374
509
|
): string[] {
|
|
375
|
-
const fromConfig =
|
|
510
|
+
const fromConfig = sharedString.parseList(configured, transform);
|
|
376
511
|
return fromConfig.length > 0
|
|
377
512
|
? fromConfig
|
|
378
|
-
:
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
function toPositiveNumber(value: unknown): number | undefined {
|
|
382
|
-
const parsed = object.toNumber(value);
|
|
383
|
-
return parsed !== undefined && parsed > 0 ? parsed : undefined;
|
|
513
|
+
: sharedString.parseList(text(input, options), transform);
|
|
384
514
|
}
|
|
385
515
|
|
|
386
516
|
/**
|
|
387
517
|
* The Databricks bundle output for `cwd` - `databricks bundle validate --output
|
|
388
518
|
* json` run from the directory holding `databricks.yml`, with the config file's
|
|
389
519
|
* path. A non-zero validation may still return partial JSON with usable App
|
|
390
|
-
* config. `undefined` when bundle reads are disabled,
|
|
391
|
-
*
|
|
392
|
-
*
|
|
520
|
+
* config. `undefined` when bundle reads are disabled, the process is production
|
|
521
|
+
* or a deployed App without an explicit override, there is no bundle, or the
|
|
522
|
+
* CLI produces no JSON.
|
|
393
523
|
*
|
|
394
|
-
*
|
|
395
|
-
* `@databricks/appkit` must be RESOLVABLE (no AppKit, no bundle - and the probe
|
|
396
|
-
* never evaluates the module); the bundle must describe exactly ONE app carrying
|
|
397
|
-
* `config.env`; and AppKit's execution context must confirm this process is that
|
|
398
|
-
* app. Only the confirmation is remembered, so a lookup during boot - before any
|
|
399
|
-
* context exists - still resolves from the bundle and re-confirms later.
|
|
524
|
+
* Parsed validation output is cached by bundle path and Databricks profile.
|
|
400
525
|
*
|
|
401
|
-
* Cached once per resolved working-directory context and
|
|
402
|
-
* `DATABRICKS_CONFIG_PROFILE` through {@link context.cached}, so repeated
|
|
403
|
-
* lookups do not rerun validation and changing either cannot return another
|
|
404
|
-
* context's bundle.
|
|
405
526
|
*/
|
|
406
527
|
export function bundleFile(cwd?: string | null): ConfigFile | undefined {
|
|
407
|
-
const production = process.env.NODE_ENV?.trim().toLowerCase() === "production";
|
|
408
528
|
const override = object.toBoolean(process.env[CONFIG_BUNDLE_KEY]);
|
|
409
529
|
if (override === false) return undefined;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
if (!appKitInstalled()) return undefined;
|
|
530
|
+
if (
|
|
531
|
+
override === undefined &&
|
|
532
|
+
(process.env.NODE_ENV?.trim().toLowerCase() === "production" || isDatabricksAppEnv())
|
|
533
|
+
) {
|
|
534
|
+
return undefined;
|
|
416
535
|
}
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
loadBundleFile(resolved, profile),
|
|
420
|
-
);
|
|
421
|
-
if (override !== undefined || file === undefined) return file;
|
|
422
|
-
// Already confirmed: this process is the app, and that does not stop being
|
|
423
|
-
// true, so skip re-deriving the gate on every lookup.
|
|
424
|
-
if (appKitContext) return file;
|
|
425
|
-
// The app must be unambiguous. `bundleApp` is what decides "exactly one app
|
|
426
|
-
// with `config.env`", so an empty map IS the ambiguous or app-less bundle and
|
|
427
|
-
// needs no separate check.
|
|
428
|
-
if (Object.keys(bundleApp(file.data)).length === 0) return undefined;
|
|
429
|
-
confirmAppKitContext();
|
|
430
|
-
return file;
|
|
536
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
537
|
+
return loadBundleFile(resolved, sharedString.trimToNull(process.env.DATABRICKS_CONFIG_PROFILE));
|
|
431
538
|
}
|
|
432
539
|
|
|
433
|
-
/**
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
* deliberately does not have - node-appkit resolves those against its extended
|
|
439
|
-
* {@link bundleResourceSchema}. An `apps` block with more than one app is
|
|
440
|
-
* ambiguous (nothing here says which app this process is), so it yields nothing
|
|
441
|
-
* rather than a guess.
|
|
442
|
-
*
|
|
443
|
-
* Memoized against the parsed bundle it came from, because both the gate in
|
|
444
|
-
* {@link bundleFile} and the lookup in {@link read} ask the same question of the
|
|
445
|
-
* same cached object - validating that payload twice per lookup would be pure
|
|
446
|
-
* waste. A `WeakMap` keeps the entry alive exactly as long as the bundle is.
|
|
447
|
-
*/
|
|
448
|
-
function bundleApp(input: unknown): Record<string, string> {
|
|
449
|
-
if (!object.isRecord(input)) return bundleAppEntries(input);
|
|
450
|
-
const cached = BUNDLE_APP_CACHE.get(input);
|
|
451
|
-
if (cached) return cached;
|
|
452
|
-
const result = bundleAppEntries(input);
|
|
453
|
-
BUNDLE_APP_CACHE.set(input, result);
|
|
454
|
-
return result;
|
|
540
|
+
/** The parsed `app.yaml` / `app.yml` for `cwd`, when local App config reads are enabled. */
|
|
541
|
+
export function appFile(cwd?: string | null): ConfigFile | undefined {
|
|
542
|
+
const enabled = object.toBoolean(process.env[CONFIG_APP_KEY]) ?? !isDatabricksAppEnv();
|
|
543
|
+
if (!enabled) return undefined;
|
|
544
|
+
return loadAppFile(resolveWorkingDirectory(cwd));
|
|
455
545
|
}
|
|
456
546
|
|
|
457
|
-
/**
|
|
458
|
-
function
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
const apps = parsed.data.resources?.apps;
|
|
462
|
-
if (!apps) return {};
|
|
463
|
-
const names = Object.keys(apps);
|
|
464
|
-
if (names.length !== 1) return {};
|
|
465
|
-
const entries = apps[names[0]!]?.config?.env ?? [];
|
|
466
|
-
const result: Record<string, string> = {};
|
|
467
|
-
for (const entry of entries) {
|
|
468
|
-
if (!entry.name) continue;
|
|
469
|
-
const value = resolvedString(entry.value);
|
|
470
|
-
if (value !== null) result[entry.name] = value;
|
|
471
|
-
}
|
|
472
|
-
return result;
|
|
547
|
+
/** The single bundle App's resolved environment, when available. */
|
|
548
|
+
function bundleEnvironment(options: ConfigOptions): Record<string, string> | undefined {
|
|
549
|
+
const data = sourceData(options.bundleData) ?? bundleFile(options.cwd)?.data;
|
|
550
|
+
return data ? flattenBundleEnv(data) : undefined;
|
|
473
551
|
}
|
|
474
552
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
*
|
|
479
|
-
* Resolution, not import: this answers "is this an AppKit project" without
|
|
480
|
-
* evaluating the module, so the cheap negative (a plain library or CLI consumer
|
|
481
|
-
* that never installed the optional peer) costs one path lookup and never boots
|
|
482
|
-
* AppKit as a side effect of reading config. A runtime with no `createRequire`
|
|
483
|
-
* (a browser bundle that reached this Node-only module anyway) reports absent.
|
|
484
|
-
*/
|
|
485
|
-
const appKitInstalled = functionModule.memoize((): boolean => {
|
|
486
|
-
try {
|
|
487
|
-
// Indirect specifier: a bundler must not try to follow an OPTIONAL peer that
|
|
488
|
-
// a browser build has no way to resolve.
|
|
489
|
-
const specifier = APPKIT_MODULE;
|
|
490
|
-
createRequire(import.meta.url).resolve(specifier);
|
|
491
|
-
return true;
|
|
492
|
-
} catch {
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* AppKit's execution context once it exists, or `undefined`.
|
|
499
|
-
*
|
|
500
|
-
* Lazily imported (and `@vite-ignore`d) because `@databricks/appkit` is an
|
|
501
|
-
* OPTIONAL peer this module must never make required. The import is only
|
|
502
|
-
* attempted once {@link appKitInstalled} says there is something to import, and a
|
|
503
|
-
* failure resolves to `undefined` rather than rejecting - a missing peer is the
|
|
504
|
-
* expected path, not an error.
|
|
505
|
-
*
|
|
506
|
-
* `getExecutionContext()` THROWS until AppKit has booted, so the call is caught
|
|
507
|
-
* and only a real context is reported. The module handle is memoized; the
|
|
508
|
-
* context lookup is not, so a call before boot answers "not yet" and a later one
|
|
509
|
-
* still sees the context.
|
|
510
|
-
*/
|
|
511
|
-
const appKitModule = functionModule.memoize(async (): Promise<AppKitModule | undefined> => {
|
|
512
|
-
if (!appKitInstalled()) return undefined;
|
|
513
|
-
const specifier = APPKIT_MODULE;
|
|
514
|
-
return (await import(/* @vite-ignore */ specifier).catch(() => undefined)) as
|
|
515
|
-
AppKitModule | undefined;
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Latch {@link appKitContext} once AppKit's execution context exists.
|
|
520
|
-
*
|
|
521
|
-
* Fire-and-forget, because the caller is a SYNCHRONOUS lookup and the check
|
|
522
|
-
* needs a lazy import. The affirmative is all that is stored: AppKit's context
|
|
523
|
-
* does not exist until it boots, and `getExecutionContext()` throws until then,
|
|
524
|
-
* so remembering a negative would permanently disable the bundle for a process
|
|
525
|
-
* that is seconds away from having one. Every call before the latch closes
|
|
526
|
-
* re-attempts, which is what makes "not available yet" retry rather than stick.
|
|
527
|
-
*/
|
|
528
|
-
function confirmAppKitContext(): void {
|
|
529
|
-
void appKitModule()
|
|
530
|
-
.then((appkit) => {
|
|
531
|
-
if (!appkit) return;
|
|
532
|
-
try {
|
|
533
|
-
if (appkit.getExecutionContext()?.client) appKitContext = true;
|
|
534
|
-
} catch {
|
|
535
|
-
// AppKit has not booted yet; a later call re-checks.
|
|
536
|
-
}
|
|
537
|
-
})
|
|
538
|
-
.catch(() => undefined);
|
|
553
|
+
function appEnvironment(options: ConfigOptions): Record<string, string> | undefined {
|
|
554
|
+
const data = sourceData(options.appData) ?? appFile(options.cwd)?.data;
|
|
555
|
+
return data ? flattenAppEnv(data) : undefined;
|
|
539
556
|
}
|
|
540
557
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
558
|
+
function sourceData(
|
|
559
|
+
source: ConfigFile | Record<string, unknown> | undefined,
|
|
560
|
+
): Record<string, unknown> | undefined {
|
|
561
|
+
if (!source) return undefined;
|
|
562
|
+
if ("path" in source && typeof source.path === "string" && object.isRecord(source.data)) {
|
|
563
|
+
return source.data;
|
|
564
|
+
}
|
|
565
|
+
return source as Record<string, unknown>;
|
|
545
566
|
}
|
|
546
567
|
|
|
547
|
-
/** Parsed `.env` for `cwd`, or `{}
|
|
568
|
+
/** Parsed `.env` for `cwd`, or `{}`; parsed file data is cached by path. */
|
|
548
569
|
function dotenv(cwd?: string | null): Record<string, string | undefined> {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
);
|
|
570
|
+
const enabled = object.toBoolean(process.env[CONFIG_DOTENV_KEY]) ?? !isDatabricksAppEnv();
|
|
571
|
+
if (!enabled) return {};
|
|
572
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
573
|
+
return loadDotenv(resolved, nodeEnvNames(process.env.NODE_ENV));
|
|
554
574
|
}
|
|
555
575
|
|
|
556
|
-
/**
|
|
557
|
-
function cachedConfig<T>(
|
|
558
|
-
name: readonly string[],
|
|
559
|
-
cwd: string | null | undefined,
|
|
560
|
-
loader: (resolved: string) => T,
|
|
561
|
-
): T {
|
|
562
|
-
const active = context.getContext() ?? "";
|
|
563
|
-
const resolved = resolve(cwd ?? ".");
|
|
564
|
-
return context.cached(["config", active, resolved, ...name], () => loader(resolved));
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
/**
|
|
568
|
-
* Maps for one source in precedence order. Bundle App config and root variables
|
|
569
|
-
* stay separate so a first-match lookup does not parse variables after an App
|
|
570
|
-
* value resolves.
|
|
571
|
-
*/
|
|
576
|
+
/** Maps for one source in precedence order. */
|
|
572
577
|
function read(
|
|
573
578
|
source: ConfigSource,
|
|
574
|
-
|
|
575
|
-
): object.Sequence<Record<string,
|
|
579
|
+
options: ConfigOptions,
|
|
580
|
+
): object.Sequence<Readonly<Record<string, unknown>>> {
|
|
576
581
|
return object.sequence({
|
|
577
|
-
*[Symbol.iterator](): Generator<Record<string,
|
|
582
|
+
*[Symbol.iterator](): Generator<Readonly<Record<string, unknown>>> {
|
|
578
583
|
switch (source) {
|
|
584
|
+
case "config": {
|
|
585
|
+
const data = options.data;
|
|
586
|
+
if (Array.isArray(data)) {
|
|
587
|
+
yield* data;
|
|
588
|
+
} else if (data !== undefined) {
|
|
589
|
+
yield data as ConfigData;
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
579
593
|
case "env":
|
|
580
594
|
yield process.env;
|
|
581
595
|
break;
|
|
582
596
|
case "dotenv":
|
|
583
|
-
yield dotenv(cwd);
|
|
597
|
+
yield dotenv(options.cwd);
|
|
584
598
|
break;
|
|
585
599
|
case "bundle": {
|
|
586
|
-
const
|
|
587
|
-
if (
|
|
600
|
+
const environment = bundleEnvironment(options);
|
|
601
|
+
if (environment) yield environment;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
case "app": {
|
|
605
|
+
const environment = appEnvironment(options);
|
|
606
|
+
if (environment) yield environment;
|
|
588
607
|
break;
|
|
589
608
|
}
|
|
609
|
+
default:
|
|
610
|
+
throw new TypeError(`Unknown config source: ${source}`);
|
|
590
611
|
}
|
|
591
612
|
},
|
|
592
613
|
});
|
|
@@ -598,6 +619,8 @@ function read(
|
|
|
598
619
|
*/
|
|
599
620
|
function findConfigFile(cwd: string, names: readonly string[]): string | undefined {
|
|
600
621
|
const start = resolve(cwd);
|
|
622
|
+
const key = JSON.stringify([start, ...names]);
|
|
623
|
+
if (configFileCache.has(key)) return configFileCache.get(key);
|
|
601
624
|
const root = resolveProjectRoot(start);
|
|
602
625
|
const pathFromRoot = root ? relative(root, start) : undefined;
|
|
603
626
|
const boundary =
|
|
@@ -611,18 +634,17 @@ function findConfigFile(cwd: string, names: readonly string[]): string | undefin
|
|
|
611
634
|
for (let dir = start; ; dir = dirname(dir)) {
|
|
612
635
|
for (const name of names) {
|
|
613
636
|
const path = resolve(dir, name);
|
|
614
|
-
if (statSync(path)?.isFile())
|
|
637
|
+
if (statSync(path)?.isFile()) {
|
|
638
|
+
configFileCache.set(key, path);
|
|
639
|
+
return path;
|
|
640
|
+
}
|
|
615
641
|
}
|
|
616
642
|
if (boundary === undefined || dir === boundary) break;
|
|
617
643
|
}
|
|
644
|
+
configFileCache.set(key, undefined);
|
|
618
645
|
return undefined;
|
|
619
646
|
}
|
|
620
647
|
|
|
621
|
-
/** A recognized source override, else the caller's automatic default. */
|
|
622
|
-
function fileSourceEnabled(key: string, fallback: boolean = !isDatabricksAppEnv()): boolean {
|
|
623
|
-
return object.toBoolean(process.env[key]) ?? fallback;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
648
|
function loadDotenv(
|
|
627
649
|
cwd: string,
|
|
628
650
|
environments: readonly string[],
|
|
@@ -630,42 +652,64 @@ function loadDotenv(
|
|
|
630
652
|
const names = [...environments.map((name) => `${DOTENV_FILE_NAME}.${name}`), DOTENV_FILE_NAME];
|
|
631
653
|
const path = findConfigFile(cwd, names);
|
|
632
654
|
if (!path) return {};
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
655
|
+
return (
|
|
656
|
+
cachedRecord(JSON.stringify(["dotenv", path]), () => {
|
|
657
|
+
try {
|
|
658
|
+
return parseEnv(readFileSync(path, "utf8"));
|
|
659
|
+
} catch {
|
|
660
|
+
logger.warn("failed to read dotenv file", { path });
|
|
661
|
+
return {};
|
|
662
|
+
}
|
|
663
|
+
}) ?? {}
|
|
664
|
+
);
|
|
639
665
|
}
|
|
640
666
|
|
|
641
667
|
function loadBundleFile(cwd: string, profile: string | null): ConfigFile | undefined {
|
|
642
668
|
const path = findConfigFile(cwd, BUNDLE_FILE_NAMES);
|
|
643
669
|
if (!path) return undefined;
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
670
|
+
const data = cachedRecord(JSON.stringify(["bundle", path, profile]), () => {
|
|
671
|
+
const args = [
|
|
672
|
+
"bundle",
|
|
673
|
+
"validate",
|
|
674
|
+
"--output",
|
|
675
|
+
"json",
|
|
676
|
+
...(profile === null ? [] : ["--profile", profile]),
|
|
677
|
+
];
|
|
678
|
+
const result = spawnSync("databricks", args, {
|
|
679
|
+
cwd: resolve(path, ".."),
|
|
680
|
+
encoding: "utf8",
|
|
681
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
682
|
+
});
|
|
683
|
+
const output = sharedString.trimToNull(result.stdout);
|
|
684
|
+
const error = sharedString.trimToNull(result.stderr);
|
|
685
|
+
if (output === null) {
|
|
686
|
+
logger.debug("bundle validate produced no JSON", { path, status: result.status, error });
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
const parsed = json.parseRecord(output);
|
|
690
|
+
if (!parsed) {
|
|
691
|
+
logger.warn("failed to parse bundle validate output", { path, status: result.status });
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
if (result.status !== 0) {
|
|
695
|
+
logger.debug("using partial bundle output", { path, status: result.status, error });
|
|
696
|
+
}
|
|
697
|
+
return parsed;
|
|
655
698
|
});
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const data =
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
699
|
+
return data === undefined ? undefined : { path, data };
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function loadAppFile(cwd: string): ConfigFile | undefined {
|
|
703
|
+
const path = findConfigFile(cwd, APP_FILE_NAMES);
|
|
704
|
+
if (!path) return undefined;
|
|
705
|
+
const data = cachedRecord(JSON.stringify(["app", path]), () => {
|
|
706
|
+
try {
|
|
707
|
+
const parsed = parseYaml(readFileSync(path, "utf8"));
|
|
708
|
+
return object.isRecord(parsed) ? parsed : undefined;
|
|
709
|
+
} catch {
|
|
710
|
+
logger.warn("failed to parse app yaml", { path });
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
return data === undefined ? undefined : { path, data };
|
|
671
715
|
}
|