@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/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 { context, json, net, string } from "@dbx-tools/shared-core";
5
- import { statSync as stat } from "./file.ts";
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
- /** `fs.stat` of {@link path}, when it exists. */
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 = stat(output);
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 { cwd, output, path: pathStats ? output : undefined, pathStats, url };
53
+ return { output, path: pathStats ? output : undefined, pathStats, url };
50
54
  }
51
- return { cwd, output };
55
+ return { output };
52
56
  }
53
57
 
54
- /**
55
- * {@link projectContextCommandOutput} through the process-context cache.
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
- return context.cached(
67
- ["project", command, ...args],
68
- (resolved) => projectContextCommandOutput(command, args, resolved ?? resolve(cwd ?? ".")),
69
- cwd ? resolve(cwd) : undefined,
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 npmRoot(cwd?: string): string | undefined {
74
- const parsed = projectContextCommand("npm", ["prefix"], cwd);
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 parsed = projectContextCommand("git", ["rev-parse", "--show-toplevel"], cwd);
80
- return parsed.pathStats?.isDirectory() ? parsed.path : undefined;
84
+ const resolved = resolveWorkingDirectory(cwd);
85
+ return commandRoot("git", ["rev-parse", "--show-toplevel"], resolved);
81
86
  }
82
87
 
83
- export function root(cwd: string = process.cwd()): string | undefined {
84
- let current = resolve(cwd);
88
+ export function root(cwd?: string): string | undefined {
89
+ const resolved = resolveWorkingDirectory(cwd);
90
+ let current = resolved;
85
91
 
86
- if (!stat(current)?.isDirectory()) {
92
+ if (!fileStatSync(current)?.isDirectory()) {
87
93
  current = dirname(current);
88
94
  }
89
95
  const boundaries = new Set(
90
- [npmRoot(cwd), gitRoot(cwd)]
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 (stat(join(current, marker))?.isFile()) {
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: string = process.cwd()): Generator<string> {
156
- const base = resolve(cwd);
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 (stat(dir)?.isDirectory()) yield dir;
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: string = process.cwd()): string {
175
+ function workspaceRoot(cwd?: string): string {
170
176
  let last: string | undefined;
171
177
  for (const dir of resolveProjectRoots(cwd)) {
172
- if (stat(resolve(dir, "package.json"))?.isFile()) return dir;
178
+ if (fileStatSync(resolve(dir, "package.json"))?.isFile()) return dir;
173
179
  last = dir;
174
180
  }
175
- return last ?? resolve(cwd);
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: string = process.cwd()): string {
184
- const rootDir = workspaceRoot(cwd);
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`. Both underlying commands are cached per `cwd` via
256
- * {@link projectContextCommand}.
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
- cwd: string = process.cwd(),
265
- format: "https" | "npm" = "https",
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
- * Cached per `cwd` via {@link projectContextCommand}.
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 (!stat(pkgPath)?.isFile()) return undefined;
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(process.cwd(), "npm"));
339
+ console.log("repository url (npm):", repositoryUrl(undefined, "npm"));
325
340
  }