@dbx-tools/core 0.9.154 → 0.9.157
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/index.ts +12 -6
- package/lib/index.d.ts +12 -6
- package/lib/index.js +13 -6
- package/lib/src/bin.js +3 -4
- package/lib/src/brand-files.d.ts +1 -101
- package/lib/src/brand-files.js +4 -14
- package/lib/src/bundle.js +10 -10
- package/lib/src/config-utils.d.ts +0 -5
- package/lib/src/config-utils.js +34 -49
- package/lib/src/dependency-resolver.js +12 -12
- package/lib/src/exec.js +4 -4
- package/lib/src/file-lock.js +12 -18
- package/lib/src/process-lock.js +8 -9
- package/lib/src/project-utils.d.ts +3 -3
- package/lib/src/project-utils.js +14 -14
- package/package.json +3 -91
- package/src/bin.ts +2 -3
- package/src/brand-files.ts +3 -18
- package/src/bundle.ts +9 -12
- package/src/config-utils.ts +50 -62
- package/src/dependency-resolver.ts +11 -11
- package/src/exec.ts +3 -3
- package/src/file-lock.ts +20 -17
- package/src/process-lock.ts +7 -8
- package/src/project-utils.ts +25 -19
package/src/bundle.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { opendir, realpath, stat } from "node:fs/promises";
|
|
13
13
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import { parseRecord, isRecord, optional, trimToNull } from "@dbx-tools/shared-core";
|
|
15
15
|
import { spawn } from "./exec.ts";
|
|
16
16
|
import { resolveWorkingDirectory, root } from "./project-utils.ts";
|
|
17
17
|
|
|
@@ -68,17 +68,14 @@ export async function* appResources(
|
|
|
68
68
|
for await (const bundlePath of bundlePaths(boundary, startDirectory)) {
|
|
69
69
|
const validated = await validateBundle(bundlePath, profile);
|
|
70
70
|
for (const [key, config] of Object.entries(bundleApps(validated.data))) {
|
|
71
|
-
if (!
|
|
71
|
+
if (!isRecord(config)) continue;
|
|
72
72
|
yield {
|
|
73
73
|
bundlePath,
|
|
74
74
|
key,
|
|
75
75
|
config,
|
|
76
76
|
data: validated.data,
|
|
77
|
-
...
|
|
78
|
-
|
|
79
|
-
resolveSourceCodePath(bundlePath, config.source_code_path),
|
|
80
|
-
),
|
|
81
|
-
...object.optional("bundleFailure", validated.bundleFailure),
|
|
77
|
+
...optional("sourceCodePath", resolveSourceCodePath(bundlePath, config.source_code_path)),
|
|
78
|
+
...optional("bundleFailure", validated.bundleFailure),
|
|
82
79
|
};
|
|
83
80
|
}
|
|
84
81
|
}
|
|
@@ -174,7 +171,7 @@ async function validateBundle(
|
|
|
174
171
|
stderr: "capture",
|
|
175
172
|
},
|
|
176
173
|
);
|
|
177
|
-
const detail =
|
|
174
|
+
const detail = trimToNull(result.stderr) ?? trimToNull(result.stdout);
|
|
178
175
|
const bundleFailure =
|
|
179
176
|
result.exitCode === 0
|
|
180
177
|
? undefined
|
|
@@ -184,17 +181,17 @@ async function validateBundle(
|
|
|
184
181
|
}`,
|
|
185
182
|
);
|
|
186
183
|
|
|
187
|
-
const parsed =
|
|
184
|
+
const parsed = parseRecord(result.stdout);
|
|
188
185
|
if (!parsed) {
|
|
189
186
|
if (bundleFailure) return { data: {}, bundleFailure };
|
|
190
187
|
throw new Error(`Databricks CLI returned invalid bundle JSON for ${bundlePath}`);
|
|
191
188
|
}
|
|
192
|
-
return { data: parsed, ...
|
|
189
|
+
return { data: parsed, ...optional("bundleFailure", bundleFailure) };
|
|
193
190
|
}
|
|
194
191
|
|
|
195
192
|
function bundleApps(bundle: Record<string, unknown>): Record<string, unknown> {
|
|
196
|
-
const resources =
|
|
197
|
-
return
|
|
193
|
+
const resources = isRecord(bundle.resources) ? bundle.resources : undefined;
|
|
194
|
+
return isRecord(resources?.apps) ? resources.apps : {};
|
|
198
195
|
}
|
|
199
196
|
|
|
200
197
|
async function isFile(path: string): Promise<boolean> {
|
package/src/config-utils.ts
CHANGED
|
@@ -36,12 +36,19 @@ import { readFileSync } from "node:fs";
|
|
|
36
36
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
37
37
|
import { parseEnv } from "node:util";
|
|
38
38
|
import {
|
|
39
|
-
environmentUtils,
|
|
40
|
-
json,
|
|
41
39
|
log,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
isDatabricksAppEnv,
|
|
41
|
+
parseRecord,
|
|
42
|
+
isRecord,
|
|
43
|
+
sequence,
|
|
44
|
+
type Sequence,
|
|
45
|
+
toBoolean,
|
|
46
|
+
tcpPortSchema,
|
|
47
|
+
tokenize,
|
|
48
|
+
trimToEmpty,
|
|
49
|
+
trimToNull,
|
|
50
|
+
parseList,
|
|
51
|
+
toNumber,
|
|
45
52
|
} from "@dbx-tools/shared-core";
|
|
46
53
|
import { parse as parseYaml } from "yaml";
|
|
47
54
|
import { z } from "zod";
|
|
@@ -106,9 +113,6 @@ const NODE_ENV_ALTERNATIVES = {
|
|
|
106
113
|
development: ["dev"],
|
|
107
114
|
} as const satisfies Record<string, readonly string[]>;
|
|
108
115
|
|
|
109
|
-
/** Highest valid TCP port number. */
|
|
110
|
-
export const MAX_TCP_PORT = sharedOptions.MAX_TCP_PORT;
|
|
111
|
-
|
|
112
116
|
/** Boolean environment override for project `.env` reads. */
|
|
113
117
|
const CONFIG_DOTENV_KEY = "DBX_TOOLS_CONFIG_DOTENV";
|
|
114
118
|
|
|
@@ -130,9 +134,9 @@ export const valueSchema = z
|
|
|
130
134
|
message: "Interpolated values are not allowed",
|
|
131
135
|
});
|
|
132
136
|
|
|
133
|
-
const portValueSchema =
|
|
137
|
+
const portValueSchema = tcpPortSchema;
|
|
134
138
|
|
|
135
|
-
const positiveNumberValue = z.preprocess((input) =>
|
|
139
|
+
const positiveNumberValue = z.preprocess((input) => toNumber(input), z.number().positive());
|
|
136
140
|
const positiveIntValue = positiveNumberValue.transform(Math.floor);
|
|
137
141
|
|
|
138
142
|
/** A named bundle or App resource; concrete resource fields pass through. */
|
|
@@ -228,7 +232,7 @@ function resourceValue(resource: unknown): string | undefined {
|
|
|
228
232
|
["postgres", "branch"],
|
|
229
233
|
] as const) {
|
|
230
234
|
let value = resource;
|
|
231
|
-
for (const part of path) value =
|
|
235
|
+
for (const part of path) value = isRecord(value) ? value[part] : undefined;
|
|
232
236
|
const parsed = valueSchema.safeParse(value);
|
|
233
237
|
if (parsed.success) return parsed.data;
|
|
234
238
|
}
|
|
@@ -241,12 +245,12 @@ export function getBundlePath(data: Record<string, unknown>, path: string): stri
|
|
|
241
245
|
if (parts.length === 0) return undefined;
|
|
242
246
|
let current: unknown = data;
|
|
243
247
|
for (let index = 0; index < parts.length; index++) {
|
|
244
|
-
if (!
|
|
248
|
+
if (!isRecord(current)) return undefined;
|
|
245
249
|
const next = current[parts[index]!];
|
|
246
250
|
if (index === parts.length - 1) {
|
|
247
251
|
const direct = valueSchema.safeParse(next);
|
|
248
252
|
if (direct.success) return direct.data;
|
|
249
|
-
if (
|
|
253
|
+
if (isRecord(next)) {
|
|
250
254
|
const nested = valueSchema.safeParse(next.value);
|
|
251
255
|
return nested.success ? nested.data : undefined;
|
|
252
256
|
}
|
|
@@ -261,12 +265,11 @@ export function getBundlePath(data: Record<string, unknown>, path: string): stri
|
|
|
261
265
|
export function environmentKeys(name: string): readonly string[] {
|
|
262
266
|
const trimmed = name.trim();
|
|
263
267
|
if (!trimmed) return [];
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
268
|
+
return sequence(
|
|
269
|
+
trimmed,
|
|
270
|
+
trimmed.toUpperCase(),
|
|
271
|
+
Array.from(tokenize(trimmed)).join("_").toUpperCase(),
|
|
272
|
+
)
|
|
270
273
|
.filter(Boolean)
|
|
271
274
|
.distinct()
|
|
272
275
|
.toArray();
|
|
@@ -277,24 +280,17 @@ function keys(
|
|
|
277
280
|
input: ConfigKey,
|
|
278
281
|
options: Pick<ConfigOptions, "scope" | "prefix"> = {},
|
|
279
282
|
): readonly string[] {
|
|
280
|
-
const scopes =
|
|
281
|
-
.
|
|
282
|
-
.map(stringUtils.trimToEmpty)
|
|
283
|
+
const scopes = sequence(options.scope ?? DEFAULT_SCOPE)
|
|
284
|
+
.map(trimToEmpty)
|
|
283
285
|
.filter(Boolean)
|
|
284
286
|
.distinct()
|
|
285
287
|
.toArray();
|
|
286
288
|
const prefixes =
|
|
287
289
|
options.prefix === undefined
|
|
288
290
|
? []
|
|
289
|
-
:
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
.filter(Boolean)
|
|
293
|
-
.distinct()
|
|
294
|
-
.toArray();
|
|
295
|
-
return object
|
|
296
|
-
.sequence(input)
|
|
297
|
-
.map(stringUtils.trimToEmpty)
|
|
291
|
+
: sequence(options.prefix).map(trimToEmpty).filter(Boolean).distinct().toArray();
|
|
292
|
+
return sequence(input)
|
|
293
|
+
.map(trimToEmpty)
|
|
298
294
|
.filter(Boolean)
|
|
299
295
|
.distinct()
|
|
300
296
|
.flatMap((key) => {
|
|
@@ -316,10 +312,10 @@ function keys(
|
|
|
316
312
|
* already answered.
|
|
317
313
|
*
|
|
318
314
|
*/
|
|
319
|
-
function values(input: ConfigKey, options: ConfigOptions = {}):
|
|
315
|
+
function values(input: ConfigKey, options: ConfigOptions = {}): Sequence<ConfigValue> {
|
|
320
316
|
const candidates = keys(input, options);
|
|
321
317
|
const sources = configSources(options);
|
|
322
|
-
return
|
|
318
|
+
return sequence({
|
|
323
319
|
*[Symbol.iterator](): Generator<ConfigValue> {
|
|
324
320
|
for (const source of sources) {
|
|
325
321
|
for (const map of read(source, options)) {
|
|
@@ -335,7 +331,7 @@ function values(input: ConfigKey, options: ConfigOptions = {}): object.Sequence<
|
|
|
335
331
|
|
|
336
332
|
function configSources(options: ConfigOptions): readonly ConfigSource[] {
|
|
337
333
|
const sources: ConfigSource[] = [
|
|
338
|
-
...
|
|
334
|
+
...sequence<ConfigSource>(options.sources ?? DEFAULT_SOURCES).distinct(),
|
|
339
335
|
];
|
|
340
336
|
if (options.data !== undefined && options.sources !== undefined && !sources.includes("config")) {
|
|
341
337
|
sources.push("config");
|
|
@@ -346,12 +342,12 @@ function configSources(options: ConfigOptions): readonly ConfigSource[] {
|
|
|
346
342
|
function configMapValue(value: ConfigMapValue | unknown): string | null {
|
|
347
343
|
if (Array.isArray(value)) {
|
|
348
344
|
for (const entry of value) {
|
|
349
|
-
const resolved =
|
|
345
|
+
const resolved = trimToNull(entry);
|
|
350
346
|
if (resolved !== null) return resolved;
|
|
351
347
|
}
|
|
352
348
|
return null;
|
|
353
349
|
}
|
|
354
|
-
return
|
|
350
|
+
return trimToNull(value);
|
|
355
351
|
}
|
|
356
352
|
|
|
357
353
|
/**
|
|
@@ -389,12 +385,12 @@ export function name(
|
|
|
389
385
|
* Known long and short names are interchangeable; unknown names pass through.
|
|
390
386
|
*/
|
|
391
387
|
function nodeEnvNames(nodeEnv: unknown): string[] {
|
|
392
|
-
const name =
|
|
388
|
+
const name = trimToNull(nodeEnv)?.toLowerCase();
|
|
393
389
|
if (!name || !/^[a-z0-9_-]+$/.test(name)) return [];
|
|
394
390
|
for (const [canonical, alternatives] of Object.entries(NODE_ENV_ALTERNATIVES)) {
|
|
395
391
|
const names: readonly string[] = alternatives;
|
|
396
392
|
if (name === canonical || names.includes(name)) {
|
|
397
|
-
return [...
|
|
393
|
+
return [...sequence(name, canonical, alternatives).distinct()];
|
|
398
394
|
}
|
|
399
395
|
}
|
|
400
396
|
return [name];
|
|
@@ -414,7 +410,7 @@ export function string(
|
|
|
414
410
|
input: ConfigKey,
|
|
415
411
|
options?: ConfigOptions,
|
|
416
412
|
): string | undefined {
|
|
417
|
-
return
|
|
413
|
+
return trimToNull(configured) ?? text(input, options);
|
|
418
414
|
}
|
|
419
415
|
|
|
420
416
|
/**
|
|
@@ -426,12 +422,9 @@ export function boolean(
|
|
|
426
422
|
input: ConfigKey,
|
|
427
423
|
options?: ConfigOptions,
|
|
428
424
|
): boolean | undefined {
|
|
429
|
-
return
|
|
425
|
+
return toBoolean(configured) ?? toBoolean(text(input, options));
|
|
430
426
|
}
|
|
431
427
|
|
|
432
|
-
/** Parse a finite number with the shared scalar coercion rules. */
|
|
433
|
-
export const toNumber = object.toNumber;
|
|
434
|
-
|
|
435
428
|
/**
|
|
436
429
|
* Resolve a positive number that may be fractional (a score threshold, a ratio).
|
|
437
430
|
* Use {@link positiveInt} for a count, port, or timeout.
|
|
@@ -492,10 +485,8 @@ export function list(
|
|
|
492
485
|
transform?: (entry: string) => string,
|
|
493
486
|
options?: ConfigOptions,
|
|
494
487
|
): string[] {
|
|
495
|
-
const fromConfig =
|
|
496
|
-
return fromConfig.length > 0
|
|
497
|
-
? fromConfig
|
|
498
|
-
: stringUtils.parseList(text(input, options), transform);
|
|
488
|
+
const fromConfig = parseList(configured, transform);
|
|
489
|
+
return fromConfig.length > 0 ? fromConfig : parseList(text(input, options), transform);
|
|
499
490
|
}
|
|
500
491
|
|
|
501
492
|
/**
|
|
@@ -510,23 +501,21 @@ export function list(
|
|
|
510
501
|
*
|
|
511
502
|
*/
|
|
512
503
|
export function bundleFile(cwd?: string | null, profile?: string | null): ConfigFile | undefined {
|
|
513
|
-
const override =
|
|
504
|
+
const override = toBoolean(process.env[CONFIG_BUNDLE_KEY]);
|
|
514
505
|
if (override === false) return undefined;
|
|
515
506
|
if (
|
|
516
507
|
override === undefined &&
|
|
517
|
-
(process.env.NODE_ENV?.trim().toLowerCase() === "production" ||
|
|
518
|
-
environmentUtils.isDatabricksAppEnv())
|
|
508
|
+
(process.env.NODE_ENV?.trim().toLowerCase() === "production" || isDatabricksAppEnv())
|
|
519
509
|
) {
|
|
520
510
|
return undefined;
|
|
521
511
|
}
|
|
522
512
|
const resolved = resolveWorkingDirectory(cwd);
|
|
523
|
-
return loadBundleFile(resolved,
|
|
513
|
+
return loadBundleFile(resolved, trimToNull(profile));
|
|
524
514
|
}
|
|
525
515
|
|
|
526
516
|
/** The parsed `app.yaml` / `app.yml` for `cwd`, when local App config reads are enabled. */
|
|
527
517
|
export function appFile(cwd?: string | null): ConfigFile | undefined {
|
|
528
|
-
const enabled =
|
|
529
|
-
object.toBoolean(process.env[CONFIG_APP_KEY]) ?? !environmentUtils.isDatabricksAppEnv();
|
|
518
|
+
const enabled = toBoolean(process.env[CONFIG_APP_KEY]) ?? !isDatabricksAppEnv();
|
|
530
519
|
if (!enabled) return undefined;
|
|
531
520
|
return loadAppFile(resolveWorkingDirectory(cwd));
|
|
532
521
|
}
|
|
@@ -547,7 +536,7 @@ function sourceData(
|
|
|
547
536
|
source: ConfigFile | Record<string, unknown> | undefined,
|
|
548
537
|
): Record<string, unknown> | undefined {
|
|
549
538
|
if (!source) return undefined;
|
|
550
|
-
if ("path" in source && typeof source.path === "string" &&
|
|
539
|
+
if ("path" in source && typeof source.path === "string" && isRecord(source.data)) {
|
|
551
540
|
return source.data;
|
|
552
541
|
}
|
|
553
542
|
return source as Record<string, unknown>;
|
|
@@ -555,8 +544,7 @@ function sourceData(
|
|
|
555
544
|
|
|
556
545
|
/** Parsed `.env` for `cwd`, or `{}`; parsed file data is cached by path. */
|
|
557
546
|
function dotenv(cwd?: string | null): Record<string, string | undefined> {
|
|
558
|
-
const enabled =
|
|
559
|
-
object.toBoolean(process.env[CONFIG_DOTENV_KEY]) ?? !environmentUtils.isDatabricksAppEnv();
|
|
547
|
+
const enabled = toBoolean(process.env[CONFIG_DOTENV_KEY]) ?? !isDatabricksAppEnv();
|
|
560
548
|
if (!enabled) return {};
|
|
561
549
|
const resolved = resolveWorkingDirectory(cwd);
|
|
562
550
|
return loadDotenv(resolved, nodeEnvNames(process.env.NODE_ENV));
|
|
@@ -566,8 +554,8 @@ function dotenv(cwd?: string | null): Record<string, string | undefined> {
|
|
|
566
554
|
function read(
|
|
567
555
|
source: ConfigSource,
|
|
568
556
|
options: ConfigOptions,
|
|
569
|
-
):
|
|
570
|
-
return
|
|
557
|
+
): Sequence<Readonly<Record<string, unknown>>> {
|
|
558
|
+
return sequence({
|
|
571
559
|
*[Symbol.iterator](): Generator<Readonly<Record<string, unknown>>> {
|
|
572
560
|
switch (source) {
|
|
573
561
|
case "config": {
|
|
@@ -672,13 +660,13 @@ function loadBundleFile(cwd: string, profile: string | null): ConfigFile | undef
|
|
|
672
660
|
encoding: "utf8",
|
|
673
661
|
stdio: ["ignore", "pipe", "pipe"],
|
|
674
662
|
});
|
|
675
|
-
const output =
|
|
676
|
-
const error =
|
|
663
|
+
const output = trimToNull(result.stdout);
|
|
664
|
+
const error = trimToNull(result.stderr);
|
|
677
665
|
if (output === null) {
|
|
678
666
|
logger.debug("bundle validate produced no JSON", { path, status: result.status, error });
|
|
679
667
|
return undefined;
|
|
680
668
|
}
|
|
681
|
-
const parsed =
|
|
669
|
+
const parsed = parseRecord(output);
|
|
682
670
|
if (!parsed) {
|
|
683
671
|
logger.warn("failed to parse bundle validate output", { path, status: result.status });
|
|
684
672
|
return undefined;
|
|
@@ -697,7 +685,7 @@ function loadAppFile(cwd: string): ConfigFile | undefined {
|
|
|
697
685
|
const data = cachedRecord(JSON.stringify(["app", path]), () => {
|
|
698
686
|
try {
|
|
699
687
|
const parsed = parseYaml(readFileSync(path, "utf8"));
|
|
700
|
-
return
|
|
688
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
701
689
|
} catch {
|
|
702
690
|
logger.warn("failed to parse app yaml", { path });
|
|
703
691
|
return undefined;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @module
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { parse, isRecord } from "@dbx-tools/shared-core";
|
|
15
15
|
import { maxSatisfying, satisfies, valid, validRange } from "semver";
|
|
16
16
|
|
|
17
17
|
import { npmRegistry } from "./project-utils.ts";
|
|
@@ -330,7 +330,7 @@ function normalizeDependency(request: DependencyInfo): DependencyInfo {
|
|
|
330
330
|
|
|
331
331
|
function parseSpecifierInput(name: string, value: unknown): DependencyInfo | undefined {
|
|
332
332
|
if (typeof value === "string") return specifierToDependency(name, value);
|
|
333
|
-
if (!
|
|
333
|
+
if (!isRecord(value)) return undefined;
|
|
334
334
|
const version = typeof value.version === "string" ? value.version : undefined;
|
|
335
335
|
const semverValue = typeof value.semver === "string" ? value.semver : undefined;
|
|
336
336
|
const range = typeof value.range === "string" ? value.range : undefined;
|
|
@@ -347,7 +347,7 @@ function dependencyMap(
|
|
|
347
347
|
}
|
|
348
348
|
|
|
349
349
|
function parseDependencyField(value: unknown): Record<string, DependencyInfo> {
|
|
350
|
-
if (!
|
|
350
|
+
if (!isRecord(value)) return {};
|
|
351
351
|
const out: Record<string, DependencyInfo> = {};
|
|
352
352
|
for (const [name, spec] of Object.entries(value)) {
|
|
353
353
|
const parsed = parseSpecifierInput(name, spec);
|
|
@@ -563,7 +563,7 @@ async function fetchRegistryJson(url: string): Promise<unknown> {
|
|
|
563
563
|
}).catch(() => undefined);
|
|
564
564
|
if (!response?.ok) return undefined;
|
|
565
565
|
const text = await response.text();
|
|
566
|
-
return
|
|
566
|
+
return parse(text);
|
|
567
567
|
}
|
|
568
568
|
|
|
569
569
|
function createPackageInfoLookup(
|
|
@@ -587,7 +587,7 @@ async function loadPackageInfo(
|
|
|
587
587
|
registryUrl?: string | URL,
|
|
588
588
|
): Promise<PackageInfo | undefined> {
|
|
589
589
|
const packument = await fetchRegistryJson(packumentUrl(name, registryUrl));
|
|
590
|
-
if (!
|
|
590
|
+
if (!isRecord(packument)) return undefined;
|
|
591
591
|
const versions = publishedVersions(packument);
|
|
592
592
|
if (versions.length === 0) return undefined;
|
|
593
593
|
return {
|
|
@@ -606,7 +606,7 @@ async function loadVersionDependencies(
|
|
|
606
606
|
const doc = await fetchRegistryJson(
|
|
607
607
|
`${packumentUrl(name, registryUrl)}/${encodeURIComponent(version)}`,
|
|
608
608
|
);
|
|
609
|
-
if (!
|
|
609
|
+
if (!isRecord(doc)) return {};
|
|
610
610
|
return stringMap(doc.dependencies);
|
|
611
611
|
}
|
|
612
612
|
|
|
@@ -618,13 +618,13 @@ function publishedVersions(packument: Record<string, unknown>): string[] {
|
|
|
618
618
|
if (Array.isArray(versions)) {
|
|
619
619
|
return versions.filter((value): value is string => typeof value === "string");
|
|
620
620
|
}
|
|
621
|
-
if (
|
|
621
|
+
if (isRecord(versions)) return Object.keys(versions);
|
|
622
622
|
return [];
|
|
623
623
|
}
|
|
624
624
|
|
|
625
625
|
function latestTag(packument: Record<string, unknown>): string | undefined {
|
|
626
626
|
const tags = packument["dist-tags"];
|
|
627
|
-
if (!
|
|
627
|
+
if (!isRecord(tags) || typeof tags.latest !== "string") return undefined;
|
|
628
628
|
return tags.latest;
|
|
629
629
|
}
|
|
630
630
|
|
|
@@ -632,20 +632,20 @@ function versionDependencies(
|
|
|
632
632
|
packument: Record<string, unknown>,
|
|
633
633
|
): Record<string, Record<string, string>> {
|
|
634
634
|
const versions = packument.versions;
|
|
635
|
-
if (!
|
|
635
|
+
if (!isRecord(versions)) {
|
|
636
636
|
return typeof packument.version === "string"
|
|
637
637
|
? { [packument.version]: stringMap(packument.dependencies) }
|
|
638
638
|
: {};
|
|
639
639
|
}
|
|
640
640
|
const out: Record<string, Record<string, string>> = {};
|
|
641
641
|
for (const [version, value] of Object.entries(versions)) {
|
|
642
|
-
if (
|
|
642
|
+
if (isRecord(value)) out[version] = stringMap(value.dependencies);
|
|
643
643
|
}
|
|
644
644
|
return out;
|
|
645
645
|
}
|
|
646
646
|
|
|
647
647
|
function stringMap(value: unknown): Record<string, string> {
|
|
648
|
-
if (!
|
|
648
|
+
if (!isRecord(value)) return {};
|
|
649
649
|
const out: Record<string, string> = {};
|
|
650
650
|
for (const [name, spec] of Object.entries(value)) {
|
|
651
651
|
if (typeof spec === "string") out[name] = spec;
|
package/src/exec.ts
CHANGED
|
@@ -54,7 +54,7 @@ import * as readline from "node:readline";
|
|
|
54
54
|
import { Readable } from "node:stream";
|
|
55
55
|
import { text } from "node:stream/consumers";
|
|
56
56
|
import { finished } from "node:stream/promises";
|
|
57
|
-
import {
|
|
57
|
+
import { log, toError, memoize } from "@dbx-tools/shared-core";
|
|
58
58
|
|
|
59
59
|
/** Shell-compatible exit code returned when an executable cannot be found. */
|
|
60
60
|
export const COMMAND_NOT_FOUND_EXIT_CODE = 127;
|
|
@@ -252,7 +252,7 @@ interface ProcessWithSignalExitEmitter extends NodeJS.Process {
|
|
|
252
252
|
|
|
253
253
|
type ParentSignalShutdown = (signal: NodeJS.Signals) => Promise<void>;
|
|
254
254
|
|
|
255
|
-
const parentSignalForwarderState =
|
|
255
|
+
const parentSignalForwarderState = memoize((): ParentSignalForwarderState => ({
|
|
256
256
|
children: new Map(),
|
|
257
257
|
handlers: new Map(),
|
|
258
258
|
forwardingSignals: new Set(),
|
|
@@ -607,7 +607,7 @@ async function tryProcessSnapshot(): Promise<ProcessSnapshotEntry[] | undefined>
|
|
|
607
607
|
try {
|
|
608
608
|
return await processSnapshot();
|
|
609
609
|
} catch (error) {
|
|
610
|
-
logger.debug("process snapshot failed", { error:
|
|
610
|
+
logger.debug("process snapshot failed", { error: toError(error) });
|
|
611
611
|
return undefined;
|
|
612
612
|
}
|
|
613
613
|
}
|
package/src/file-lock.ts
CHANGED
|
@@ -26,12 +26,16 @@ import { mkdir, open } from "node:fs/promises";
|
|
|
26
26
|
import { tmpdir } from "node:os";
|
|
27
27
|
import { dirname, join } from "node:path";
|
|
28
28
|
|
|
29
|
-
import
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
import {
|
|
30
|
+
sleep,
|
|
31
|
+
toError,
|
|
32
|
+
memoize,
|
|
33
|
+
fnvHash,
|
|
34
|
+
log,
|
|
35
|
+
toNumber,
|
|
36
|
+
toOneOrMany,
|
|
37
|
+
toStableKey,
|
|
38
|
+
} from "@dbx-tools/shared-core";
|
|
35
39
|
import lockfile from "proper-lockfile";
|
|
36
40
|
|
|
37
41
|
const logger = log.logger("core:file-lock");
|
|
@@ -49,10 +53,10 @@ const POLL_MS = 50;
|
|
|
49
53
|
* Matches `proper-lockfile`'s default. Not caller-configurable: every process
|
|
50
54
|
* must share the same threshold or a live holder can be mistaken for stale.
|
|
51
55
|
*/
|
|
52
|
-
const STALE_MS =
|
|
56
|
+
const STALE_MS = toNumber(process.env.FILE_LOCK_STALE_MS) ?? 10_000;
|
|
53
57
|
|
|
54
58
|
/** Heartbeat interval for refreshing lock-directory mtime (`STALE_MS / 2`). */
|
|
55
|
-
const UPDATE_MS =
|
|
59
|
+
const UPDATE_MS = toNumber(process.env.FILE_LOCK_UPDATE_MS) ?? STALE_MS / 2;
|
|
56
60
|
|
|
57
61
|
/** `flock(2)` operation bits (Linux / macOS / BSD). */
|
|
58
62
|
const LOCK_EX = 2;
|
|
@@ -208,13 +212,12 @@ export async function acquireFileLock(
|
|
|
208
212
|
|
|
209
213
|
/** Canonical filesystem-safe id for a lock key. */
|
|
210
214
|
function lockId(key: unknown): string {
|
|
211
|
-
const stable =
|
|
212
|
-
.
|
|
213
|
-
.map((part) => object.toStableKey(part))
|
|
215
|
+
const stable = toOneOrMany(key)
|
|
216
|
+
.map((part) => toStableKey(part))
|
|
214
217
|
.join("\u0000");
|
|
215
218
|
// Short digest so paths stay well under OS limits; collisions are fine — they
|
|
216
219
|
// only merge critical sections that already shared a key string.
|
|
217
|
-
return
|
|
220
|
+
return fnvHash(stable);
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
/**
|
|
@@ -225,7 +228,7 @@ function lockId(key: unknown): string {
|
|
|
225
228
|
* callers fall through to the next backend. A miss is cached too, so a process
|
|
226
229
|
* that cannot flock never retries the load.
|
|
227
230
|
*/
|
|
228
|
-
const resolveFlock =
|
|
231
|
+
const resolveFlock = memoize(async (): Promise<FlockFn | undefined> => {
|
|
229
232
|
if (process.platform === "win32") return undefined;
|
|
230
233
|
if (!process.versions.bun) return undefined;
|
|
231
234
|
|
|
@@ -240,7 +243,7 @@ const resolveFlock = functionUtils.memoize(async (): Promise<FlockFn | undefined
|
|
|
240
243
|
});
|
|
241
244
|
return lib.symbols.flock;
|
|
242
245
|
} catch (cause) {
|
|
243
|
-
logger.debug("flock FFI unavailable", { error:
|
|
246
|
+
logger.debug("flock FFI unavailable", { error: toError(cause) });
|
|
244
247
|
return undefined;
|
|
245
248
|
}
|
|
246
249
|
});
|
|
@@ -282,7 +285,7 @@ async function waitForFlock(
|
|
|
282
285
|
onWait();
|
|
283
286
|
}
|
|
284
287
|
assertBeforeDeadline(lockPath, deadline);
|
|
285
|
-
await
|
|
288
|
+
await sleep(POLL_MS);
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
|
|
@@ -333,13 +336,13 @@ async function acquireLockDirectory(
|
|
|
333
336
|
});
|
|
334
337
|
} catch (cause) {
|
|
335
338
|
const err = cause as NodeJS.ErrnoException;
|
|
336
|
-
if (err.code !== "ELOCKED") throw
|
|
339
|
+
if (err.code !== "ELOCKED") throw toError(cause);
|
|
337
340
|
if (!waiting) {
|
|
338
341
|
waiting = true;
|
|
339
342
|
onWait();
|
|
340
343
|
}
|
|
341
344
|
assertBeforeDeadline(lockPath, deadline);
|
|
342
|
-
await
|
|
345
|
+
await sleep(POLL_MS);
|
|
343
346
|
}
|
|
344
347
|
}
|
|
345
348
|
}
|
package/src/process-lock.ts
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
type Worker,
|
|
39
39
|
type WorkerOptions,
|
|
40
40
|
} from "node:worker_threads";
|
|
41
|
-
import {
|
|
41
|
+
import { toError, id, toOneOrMany, toStableKey, isRecord } from "@dbx-tools/shared-core";
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* `workerData` slot carrying the coordinator port into a worker, and the
|
|
@@ -242,7 +242,7 @@ class LockClient {
|
|
|
242
242
|
if (this.closed) {
|
|
243
243
|
throw new Error("Process lock port is closed");
|
|
244
244
|
}
|
|
245
|
-
const requestId =
|
|
245
|
+
const requestId = id();
|
|
246
246
|
this.retain();
|
|
247
247
|
try {
|
|
248
248
|
await new Promise<void>((resolve, reject) => {
|
|
@@ -251,7 +251,7 @@ class LockClient {
|
|
|
251
251
|
this.port.postMessage({ type: "acquire", requestId, key } satisfies LockRequest);
|
|
252
252
|
} catch (cause) {
|
|
253
253
|
this.pending.delete(requestId);
|
|
254
|
-
reject(
|
|
254
|
+
reject(toError(cause));
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
} catch (cause) {
|
|
@@ -330,9 +330,8 @@ export function withProcessLock<T>(key: unknown, fn: () => T | Promise<T>): Prom
|
|
|
330
330
|
|
|
331
331
|
/** Canonical string identity for a lock key (see `object.toStableKey`). */
|
|
332
332
|
function lockKey(key: unknown): string {
|
|
333
|
-
return
|
|
334
|
-
.
|
|
335
|
-
.map((part) => object.toStableKey(part))
|
|
333
|
+
return toOneOrMany(key)
|
|
334
|
+
.map((part) => toStableKey(part))
|
|
336
335
|
.join("\u0000");
|
|
337
336
|
}
|
|
338
337
|
|
|
@@ -348,7 +347,7 @@ function lockKey(key: unknown): string {
|
|
|
348
347
|
*/
|
|
349
348
|
export function processLockWorkerOptions(options: WorkerOptions = {}): WorkerOptions {
|
|
350
349
|
const port = createCoordinatorPort("processLockWorkerOptions");
|
|
351
|
-
const existing =
|
|
350
|
+
const existing = isRecord(options.workerData) ? options.workerData : {};
|
|
352
351
|
return {
|
|
353
352
|
...options,
|
|
354
353
|
workerData: { ...existing, [LOCK_PORT_KEY]: port },
|
|
@@ -404,7 +403,7 @@ export function processLockAttached(): Promise<void> {
|
|
|
404
403
|
}
|
|
405
404
|
return new Promise<void>((resolve) => {
|
|
406
405
|
const onMessage = (message: unknown): void => {
|
|
407
|
-
if (!
|
|
406
|
+
if (!isRecord(message) || message.type !== ATTACH_MESSAGE_TYPE) return;
|
|
408
407
|
port.off("message", onMessage);
|
|
409
408
|
client = new LockClient((message as unknown as AttachMessage).port);
|
|
410
409
|
resolve();
|