@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/file.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Stats, statSync as nodeStatSync } from "node:fs";
|
|
7
7
|
|
|
8
|
+
const recordCache = new Map<string, Record<string, unknown> | undefined>();
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Best-effort `fs.stat` (sync). Returns `undefined` for a blank path or when the
|
|
10
12
|
* path can't be stat'd (missing, permission denied, ...), so callers can treat
|
|
@@ -18,3 +20,20 @@ export function statSync(path: string): Stats | undefined {
|
|
|
18
20
|
}
|
|
19
21
|
return undefined;
|
|
20
22
|
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load and cache a parsed record by caller-defined source identity.
|
|
26
|
+
*
|
|
27
|
+
* Every result is cached, including empty records and `undefined`. Include every
|
|
28
|
+
* input that affects parsing in `key`, such as a file path plus a Databricks
|
|
29
|
+
* profile.
|
|
30
|
+
*/
|
|
31
|
+
export function cachedRecord<T extends Record<string, unknown>>(
|
|
32
|
+
key: string,
|
|
33
|
+
loader: () => T | undefined,
|
|
34
|
+
): T | undefined {
|
|
35
|
+
if (recordCache.has(key)) return recordCache.get(key) as T | undefined;
|
|
36
|
+
const value = loader();
|
|
37
|
+
recordCache.set(key, value);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
package/src/project.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { Stats, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
import { statSync as
|
|
4
|
+
import { json, net, string } from "@dbx-tools/shared-core";
|
|
5
|
+
import { statSync as fileStatSync } from "./file.ts";
|
|
6
6
|
|
|
7
7
|
const ROOT_MARKERS = [
|
|
8
8
|
".projenrc.ts",
|
|
@@ -12,13 +12,17 @@ const ROOT_MARKERS = [
|
|
|
12
12
|
"package.json",
|
|
13
13
|
] as const;
|
|
14
14
|
|
|
15
|
+
/** Resolve a blank, null, omitted, relative, or absolute cwd to an absolute path. */
|
|
16
|
+
export function resolveWorkingDirectory(cwd?: string | null): string {
|
|
17
|
+
return resolve(string.trimToNull(cwd) ?? process.cwd());
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/** A command's stdout, classified as a filesystem path and/or a URL. */
|
|
16
21
|
export interface ProjectContext {
|
|
17
|
-
readonly cwd: string;
|
|
18
22
|
readonly output: string;
|
|
19
23
|
/** `output` when it names something on disk. */
|
|
20
24
|
readonly path?: string;
|
|
21
|
-
/** `
|
|
25
|
+
/** `file.statSync` of {@link path}, when it exists. */
|
|
22
26
|
readonly pathStats?: Stats;
|
|
23
27
|
/**
|
|
24
28
|
* `output` parsed into a chainable {@link net.UrlBuilder}, when it is a real
|
|
@@ -40,54 +44,56 @@ function projectContextCommandOutput(command: string, args: string[], cwd: strin
|
|
|
40
44
|
const result = spawnSync(command, args, { cwd, stdio: ["ignore", "pipe", "ignore"] });
|
|
41
45
|
const output = result?.stdout?.toString()?.trim();
|
|
42
46
|
if (result.status === 0 && output) {
|
|
43
|
-
const pathStats =
|
|
47
|
+
const pathStats = fileStatSync(output);
|
|
44
48
|
// Only an EXPLICIT `scheme://...` counts as a URL. `urlBuilder` otherwise
|
|
45
49
|
// synthesizes one (a bare `example.com` -> `https://…`, an absolute path ->
|
|
46
50
|
// `http://localhost/…`), which would mislabel directory outputs and bare
|
|
47
51
|
// tokens - so gate on the raw output already carrying a scheme + authority.
|
|
48
52
|
const url = /^[a-z][a-z0-9+.-]*:\/\//i.test(output) ? net.urlBuilder(output) : undefined;
|
|
49
|
-
return {
|
|
53
|
+
return { output, path: pathStats ? output : undefined, pathStats, url };
|
|
50
54
|
}
|
|
51
|
-
return {
|
|
55
|
+
return { output };
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
*
|
|
57
|
-
* These commands (`npm prefix`, `git rev-parse`, `gh repo view`) are pure
|
|
58
|
-
* functions of the directory they run in, so their output is worth keeping - but
|
|
59
|
-
* only while the process is still IN that directory. {@link context.cached}
|
|
60
|
-
* owns that rule: the slot remembers the `cwd` it was loaded under and misses
|
|
61
|
-
* once the process moves, and a lookup for some OTHER directory runs the command
|
|
62
|
-
* without caching, so it can't evict the hot entry. The slot name carries the
|
|
63
|
-
* command and its arguments (order-sensitive), so two commands never share one.
|
|
64
|
-
*/
|
|
58
|
+
const projectCommandCache = new Map<string, ProjectContext>();
|
|
59
|
+
|
|
65
60
|
function projectContextCommand(command: string, args: string[], cwd?: string): ProjectContext {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
(
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
62
|
+
const key =
|
|
63
|
+
resolved === resolveWorkingDirectory() ? JSON.stringify([command, ...args]) : undefined;
|
|
64
|
+
if (key !== undefined) {
|
|
65
|
+
const cached = projectCommandCache.get(key);
|
|
66
|
+
if (cached !== undefined) return cached;
|
|
67
|
+
}
|
|
68
|
+
const value = projectContextCommandOutput(command, args, resolved);
|
|
69
|
+
if (key !== undefined) projectCommandCache.set(key, value);
|
|
70
|
+
return value;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function
|
|
74
|
-
const parsed = projectContextCommand(
|
|
73
|
+
function commandRoot(command: string, args: string[], cwd: string): string | undefined {
|
|
74
|
+
const parsed = projectContextCommand(command, args, cwd);
|
|
75
75
|
return parsed.pathStats?.isDirectory() ? parsed.path : undefined;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function npmRoot(cwd?: string): string | undefined {
|
|
79
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
80
|
+
return commandRoot("npm", ["prefix"], resolved);
|
|
81
|
+
}
|
|
82
|
+
|
|
78
83
|
function gitRoot(cwd?: string): string | undefined {
|
|
79
|
-
const
|
|
80
|
-
return
|
|
84
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
85
|
+
return commandRoot("git", ["rev-parse", "--show-toplevel"], resolved);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
|
-
export function root(cwd
|
|
84
|
-
|
|
88
|
+
export function root(cwd?: string): string | undefined {
|
|
89
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
90
|
+
let current = resolved;
|
|
85
91
|
|
|
86
|
-
if (!
|
|
92
|
+
if (!fileStatSync(current)?.isDirectory()) {
|
|
87
93
|
current = dirname(current);
|
|
88
94
|
}
|
|
89
95
|
const boundaries = new Set(
|
|
90
|
-
[npmRoot(
|
|
96
|
+
[npmRoot(resolved), gitRoot(resolved)]
|
|
91
97
|
.filter((path): path is string => path !== undefined)
|
|
92
98
|
.map((path) => resolve(path)),
|
|
93
99
|
);
|
|
@@ -95,7 +101,7 @@ export function root(cwd: string = process.cwd()): string | undefined {
|
|
|
95
101
|
let best: { dir: string; priority: number } | undefined;
|
|
96
102
|
while (true) {
|
|
97
103
|
for (const [priority, marker] of ROOT_MARKERS.entries()) {
|
|
98
|
-
if (
|
|
104
|
+
if (fileStatSync(join(current, marker))?.isFile()) {
|
|
99
105
|
if (
|
|
100
106
|
best === undefined ||
|
|
101
107
|
priority < best.priority ||
|
|
@@ -152,27 +158,27 @@ function lastPathSegment(path: string): string {
|
|
|
152
158
|
* `npm prefix`, the git top-level, then `cwd` itself. Duplicates are skipped;
|
|
153
159
|
* only existing directories are yielded (except the final `cwd` fallback).
|
|
154
160
|
*/
|
|
155
|
-
export function* resolveProjectRoots(cwd
|
|
156
|
-
const base =
|
|
161
|
+
export function* resolveProjectRoots(cwd?: string): Generator<string> {
|
|
162
|
+
const base = resolveWorkingDirectory(cwd);
|
|
157
163
|
const seen = new Set<string>();
|
|
158
164
|
for (const candidate of [npmRoot(base), gitRoot(base)]) {
|
|
159
165
|
if (!candidate) continue;
|
|
160
166
|
const dir = resolve(candidate);
|
|
161
167
|
if (seen.has(dir)) continue;
|
|
162
168
|
seen.add(dir);
|
|
163
|
-
if (
|
|
169
|
+
if (fileStatSync(dir)?.isDirectory()) yield dir;
|
|
164
170
|
}
|
|
165
171
|
if (!seen.has(base)) yield base;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
/** The nearest ancestor of `cwd` (from {@link resolveProjectRoots}) with a `package.json`. */
|
|
169
|
-
function workspaceRoot(cwd
|
|
175
|
+
function workspaceRoot(cwd?: string): string {
|
|
170
176
|
let last: string | undefined;
|
|
171
177
|
for (const dir of resolveProjectRoots(cwd)) {
|
|
172
|
-
if (
|
|
178
|
+
if (fileStatSync(resolve(dir, "package.json"))?.isFile()) return dir;
|
|
173
179
|
last = dir;
|
|
174
180
|
}
|
|
175
|
-
return last ??
|
|
181
|
+
return last ?? resolveWorkingDirectory(cwd);
|
|
176
182
|
}
|
|
177
183
|
|
|
178
184
|
/**
|
|
@@ -180,8 +186,9 @@ function workspaceRoot(cwd: string = process.cwd()): string {
|
|
|
180
186
|
* `package.json` `name`, then the git remote's repo name, then the root
|
|
181
187
|
* directory's basename.
|
|
182
188
|
*/
|
|
183
|
-
export function name(cwd
|
|
184
|
-
const
|
|
189
|
+
export function name(cwd?: string): string {
|
|
190
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
191
|
+
const rootDir = workspaceRoot(resolved);
|
|
185
192
|
|
|
186
193
|
const fromPackage = readPackageName(resolve(rootDir, "package.json"));
|
|
187
194
|
if (fromPackage) return fromPackage;
|
|
@@ -252,19 +259,17 @@ function repositoryUrlFromGit(cwd?: string): string | undefined {
|
|
|
252
259
|
/**
|
|
253
260
|
* The repo's canonical remote URL, or `undefined` when there is no git remote.
|
|
254
261
|
* Tries `gh repo view` first (host-accurate, no parsing), then normalizes
|
|
255
|
-
* `git remote get-url origin`.
|
|
256
|
-
*
|
|
262
|
+
* `git remote get-url origin`. A blank, omitted, or explicitly current `cwd`
|
|
263
|
+
* reuses cached command probes; another resolved directory executes directly.
|
|
257
264
|
*
|
|
258
265
|
* @param cwd - directory to resolve from (defaults to `process.cwd()`).
|
|
259
266
|
* @param format - `"https"` (default) yields `https://host/owner/repo`;
|
|
260
267
|
* `"npm"` yields npm's `git+https://host/owner/repo.git` form (for a
|
|
261
268
|
* `package.json` `repository.url` that passes npm provenance).
|
|
262
269
|
*/
|
|
263
|
-
export function repositoryUrl(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
): string | undefined {
|
|
267
|
-
const https = repositoryUrlFromGh(cwd) ?? repositoryUrlFromGit(cwd);
|
|
270
|
+
export function repositoryUrl(cwd?: string, format: "https" | "npm" = "https"): string | undefined {
|
|
271
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
272
|
+
const https = repositoryUrlFromGh(resolved) ?? repositoryUrlFromGit(resolved);
|
|
268
273
|
if (!https) return undefined;
|
|
269
274
|
return format === "npm" ? `git+${https.replace(/\.git$/, "")}.git` : https;
|
|
270
275
|
}
|
|
@@ -272,11 +277,21 @@ export function repositoryUrl(
|
|
|
272
277
|
/**
|
|
273
278
|
* The active npm registry (`npm config get registry`) as a chainable
|
|
274
279
|
* {@link net.UrlBuilder}, or `undefined` when npm is absent or prints no URL.
|
|
275
|
-
*
|
|
280
|
+
* A blank, null, omitted, or explicitly current `cwd` reuses the cached npm
|
|
281
|
+
* command; another resolved directory executes it directly. Environment options
|
|
282
|
+
* are evaluated on every call.
|
|
276
283
|
*/
|
|
277
284
|
export function npmRegistry(
|
|
278
285
|
cwd?: string | null,
|
|
279
286
|
options?: { overrideOnly?: boolean; envVars?: boolean },
|
|
287
|
+
): net.UrlBuilder | undefined {
|
|
288
|
+
const resolved = resolveWorkingDirectory(cwd);
|
|
289
|
+
return resolveNpmRegistry(resolved, options);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function resolveNpmRegistry(
|
|
293
|
+
cwd: string | null | undefined,
|
|
294
|
+
options: { overrideOnly?: boolean; envVars?: boolean } | undefined,
|
|
280
295
|
): net.UrlBuilder | undefined {
|
|
281
296
|
const candidates = (function* () {
|
|
282
297
|
yield projectContextCommand(
|
|
@@ -311,7 +326,7 @@ export function npmRegistry(
|
|
|
311
326
|
}
|
|
312
327
|
|
|
313
328
|
function readPackageName(pkgPath: string): string | undefined {
|
|
314
|
-
if (!
|
|
329
|
+
if (!fileStatSync(pkgPath)?.isFile()) return undefined;
|
|
315
330
|
return string.trimToNull(json.parseRecord(readFileSync(pkgPath, "utf8"))?.name) ?? undefined;
|
|
316
331
|
}
|
|
317
332
|
|
|
@@ -321,5 +336,5 @@ if (import.meta.main) {
|
|
|
321
336
|
console.log("package root:", root());
|
|
322
337
|
console.log("project name:", name());
|
|
323
338
|
console.log("repository url:", repositoryUrl());
|
|
324
|
-
console.log("repository url (npm):", repositoryUrl(
|
|
339
|
+
console.log("repository url (npm):", repositoryUrl(undefined, "npm"));
|
|
325
340
|
}
|