@dbx-tools/core 0.9.12 → 0.9.18
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 +54 -10
- package/index.ts +5 -3
- package/lib/index.d.ts +5 -3
- package/lib/index.js +3 -2
- package/lib/src/bin.d.ts +12 -1
- package/lib/src/bin.js +36 -8
- package/lib/src/dependency-resolver.d.ts +83 -0
- package/lib/src/dependency-resolver.js +515 -0
- package/lib/src/project-utils.d.ts +18 -9
- package/lib/src/project-utils.js +217 -32
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -4
- package/src/bin.ts +52 -8
- package/src/dependency-resolver.ts +654 -0
- package/src/project-utils.ts +233 -38
package/src/project-utils.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { Stats, readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
-
import { json, net, stringUtils } from "@dbx-tools/shared-core";
|
|
5
|
-
import {
|
|
5
|
+
import { json, net, object, stringUtils } from "@dbx-tools/shared-core";
|
|
6
|
+
import { parse as parseYaml } from "yaml";
|
|
7
|
+
import { cachedRecord, statSync as fileStatSync } from "./file.ts";
|
|
6
8
|
|
|
7
9
|
const ROOT_MARKERS = [
|
|
8
10
|
".projenrc.ts",
|
|
@@ -75,11 +77,33 @@ function commandRoot(command: string, args: string[], cwd: string): string | und
|
|
|
75
77
|
return parsed.pathStats?.isDirectory() ? parsed.path : undefined;
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Closest directory with a `package.json` or `node_modules`, matching `npm prefix`.
|
|
82
|
+
*
|
|
83
|
+
* Lifecycle scripts from npm and Bun set `npm_config_local_prefix` to that
|
|
84
|
+
* directory. Read it first when resolving this process's cwd so a missing npm
|
|
85
|
+
* CLI is not required. `npm_config_prefix` / `PREFIX` are the global install
|
|
86
|
+
* prefix and must not be used here.
|
|
87
|
+
*
|
|
88
|
+
* Bun has no prefix command (`bun pm bin` is the `.bin` directory). pnpm's
|
|
89
|
+
* `prefix` command is unimplemented; `pnpm root` prints `node_modules`.
|
|
90
|
+
*/
|
|
78
91
|
function npmRoot(cwd?: string): string | undefined {
|
|
79
92
|
const resolved = resolveWorkingDirectory(cwd);
|
|
93
|
+
if (resolved === resolveWorkingDirectory()) {
|
|
94
|
+
const fromEnv = envDirectory(process.env.npm_config_local_prefix);
|
|
95
|
+
if (fromEnv) return fromEnv;
|
|
96
|
+
}
|
|
80
97
|
return commandRoot("npm", ["prefix"], resolved);
|
|
81
98
|
}
|
|
82
99
|
|
|
100
|
+
function envDirectory(value: string | undefined): string | undefined {
|
|
101
|
+
const path = stringUtils.trimToNull(value);
|
|
102
|
+
if (!path) return undefined;
|
|
103
|
+
const resolved = resolve(path);
|
|
104
|
+
return fileStatSync(resolved)?.isDirectory() ? resolved : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
83
107
|
function gitRoot(cwd?: string): string | undefined {
|
|
84
108
|
const resolved = resolveWorkingDirectory(cwd);
|
|
85
109
|
return commandRoot("git", ["rev-parse", "--show-toplevel"], resolved);
|
|
@@ -275,57 +299,228 @@ export function repositoryUrl(cwd?: string, format: "https" | "npm" = "https"):
|
|
|
275
299
|
return format === "npm" ? `git+${https.replace(/\.git$/, "")}.git` : https;
|
|
276
300
|
}
|
|
277
301
|
|
|
302
|
+
/** Public npm, used only after env, npmrc, bunfig, and pnpm yaml miss. */
|
|
303
|
+
const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
304
|
+
|
|
305
|
+
/** Inputs that control active npm registry discovery. */
|
|
306
|
+
export type NpmRegistryOptions = {
|
|
307
|
+
/** Skip public npmjs.org so callers can detect a configured override. */
|
|
308
|
+
overrideOnly?: boolean;
|
|
309
|
+
/**
|
|
310
|
+
* Read `npm_config_registry`, `NPM_CONFIG_REGISTRY`, and `BUN_CONFIG_REGISTRY`
|
|
311
|
+
* first. Defaults to true so the same cascade a package manager uses is the
|
|
312
|
+
* default.
|
|
313
|
+
*/
|
|
314
|
+
envVars?: boolean;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const npmRegistryCache = new Map<string, net.UrlBuilder | undefined>();
|
|
318
|
+
|
|
278
319
|
/**
|
|
279
|
-
* The active npm registry
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
320
|
+
* The active npm registry as a chainable {@link net.UrlBuilder}.
|
|
321
|
+
*
|
|
322
|
+
* Resolution is manager-agnostic and memoized per cwd plus options: environment
|
|
323
|
+
* variables, then `.npmrc` (project ancestors, user, global), then Bun
|
|
324
|
+
* `bunfig.toml` `install.registry`, then pnpm workspace/global YAML, then
|
|
325
|
+
* `https://registry.npmjs.org/`. `overrideOnly` skips that public default.
|
|
284
326
|
*/
|
|
285
327
|
export function npmRegistry(
|
|
286
328
|
cwd?: string | null,
|
|
287
|
-
options?:
|
|
329
|
+
options?: NpmRegistryOptions,
|
|
288
330
|
): net.UrlBuilder | undefined {
|
|
289
331
|
const resolved = resolveWorkingDirectory(cwd);
|
|
290
|
-
|
|
332
|
+
const envVars = options?.envVars !== false;
|
|
333
|
+
const overrideOnly = Boolean(options?.overrideOnly);
|
|
334
|
+
const key = JSON.stringify([
|
|
335
|
+
resolved,
|
|
336
|
+
overrideOnly,
|
|
337
|
+
envVars,
|
|
338
|
+
homedir(),
|
|
339
|
+
process.env.NPM_CONFIG_USERCONFIG ?? "",
|
|
340
|
+
process.env.NPM_CONFIG_GLOBALCONFIG ?? "",
|
|
341
|
+
process.env.PREFIX ?? "",
|
|
342
|
+
envVars ? (process.env.npm_config_registry ?? "") : "",
|
|
343
|
+
envVars ? (process.env.NPM_CONFIG_REGISTRY ?? "") : "",
|
|
344
|
+
envVars ? (process.env.BUN_CONFIG_REGISTRY ?? "") : "",
|
|
345
|
+
]);
|
|
346
|
+
if (npmRegistryCache.has(key)) return npmRegistryCache.get(key);
|
|
347
|
+
const value = resolveNpmRegistry(resolved, { overrideOnly, envVars });
|
|
348
|
+
npmRegistryCache.set(key, value);
|
|
349
|
+
return value;
|
|
291
350
|
}
|
|
292
351
|
|
|
293
352
|
function resolveNpmRegistry(
|
|
294
|
-
cwd: string
|
|
295
|
-
options: { overrideOnly
|
|
353
|
+
cwd: string,
|
|
354
|
+
options: { overrideOnly: boolean; envVars: boolean },
|
|
296
355
|
): net.UrlBuilder | undefined {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
356
|
+
for (const candidate of npmRegistryCandidates(cwd, options.envVars)) {
|
|
357
|
+
const url = toRegistryUrl(candidate, options.overrideOnly);
|
|
358
|
+
if (url) return url;
|
|
359
|
+
}
|
|
360
|
+
return toRegistryUrl(DEFAULT_NPM_REGISTRY, options.overrideOnly);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function* npmRegistryCandidates(cwd: string, envVars: boolean): Generator<string | undefined> {
|
|
364
|
+
if (envVars) {
|
|
365
|
+
yield process.env.npm_config_registry;
|
|
366
|
+
yield process.env.NPM_CONFIG_REGISTRY;
|
|
367
|
+
yield process.env.BUN_CONFIG_REGISTRY;
|
|
368
|
+
}
|
|
369
|
+
for (const dir of ancestorDirs(cwd)) {
|
|
370
|
+
yield readNpmrcRegistry(join(dir, ".npmrc"));
|
|
371
|
+
}
|
|
372
|
+
yield readNpmrcRegistry(process.env.NPM_CONFIG_USERCONFIG ?? join(homedir(), ".npmrc"));
|
|
373
|
+
yield readNpmrcRegistry(process.env.NPM_CONFIG_GLOBALCONFIG ?? defaultGlobalNpmrc());
|
|
374
|
+
for (const dir of ancestorDirs(cwd)) {
|
|
375
|
+
yield readBunfigRegistry(join(dir, "bunfig.toml"));
|
|
376
|
+
}
|
|
377
|
+
yield readBunfigRegistry(join(homedir(), "bunfig.toml"));
|
|
378
|
+
for (const dir of ancestorDirs(cwd)) {
|
|
379
|
+
yield readYamlRegistry(join(dir, "pnpm-workspace.yaml"));
|
|
380
|
+
}
|
|
381
|
+
for (const path of pnpmGlobalConfigPaths()) {
|
|
382
|
+
yield readYamlRegistry(path);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function toRegistryUrl(
|
|
387
|
+
candidate: string | undefined,
|
|
388
|
+
overrideOnly: boolean,
|
|
389
|
+
): net.UrlBuilder | undefined {
|
|
390
|
+
const trimmed = stringUtils.trimToNull(candidate);
|
|
391
|
+
if (!trimmed) return undefined;
|
|
392
|
+
const url = net.urlBuilder(trimmed);
|
|
393
|
+
if (!url) return undefined;
|
|
394
|
+
if (overrideOnly && isPublicNpmRegistry(url)) return undefined;
|
|
395
|
+
return url;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isPublicNpmRegistry(url: net.UrlBuilder): boolean {
|
|
399
|
+
return url.hostname === "registry.npmjs.org";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function* ancestorDirs(cwd: string): Generator<string> {
|
|
403
|
+
let current = cwd;
|
|
404
|
+
while (true) {
|
|
405
|
+
yield current;
|
|
406
|
+
const parent = dirname(current);
|
|
407
|
+
if (parent === current) return;
|
|
408
|
+
current = parent;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function defaultGlobalNpmrc(): string {
|
|
413
|
+
if (process.platform === "win32") {
|
|
414
|
+
return join(process.env.PROGRAMDATA ?? "C:\\ProgramData", "npm", "etc", "npmrc");
|
|
415
|
+
}
|
|
416
|
+
return join(process.env.PREFIX ?? "/usr/local", "etc", "npmrc");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function pnpmGlobalConfigPaths(): string[] {
|
|
420
|
+
const home = homedir();
|
|
421
|
+
if (process.platform === "darwin") {
|
|
422
|
+
return [join(home, "Library/Preferences/pnpm/config.yaml")];
|
|
423
|
+
}
|
|
424
|
+
if (process.platform === "win32") {
|
|
425
|
+
return [join(process.env.LOCALAPPDATA ?? join(home, "AppData/Local"), "pnpm/config.yaml")];
|
|
426
|
+
}
|
|
427
|
+
return [join(home, ".config/pnpm/config.yaml")];
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function readNpmrcRegistry(path: string): string | undefined {
|
|
431
|
+
return cachedRecord(`npmrc-registry:${path}`, () => {
|
|
432
|
+
const text = readTextFile(path);
|
|
433
|
+
const registry = text ? parseIniRegistry(text) : undefined;
|
|
434
|
+
return registry ? { registry } : undefined;
|
|
435
|
+
})?.registry;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readBunfigRegistry(path: string): string | undefined {
|
|
439
|
+
return cachedRecord(`bunfig-registry:${path}`, () => {
|
|
440
|
+
const text = readTextFile(path);
|
|
441
|
+
const registry = text ? parseBunfigRegistry(text) : undefined;
|
|
442
|
+
return registry ? { registry } : undefined;
|
|
443
|
+
})?.registry;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function readYamlRegistry(path: string): string | undefined {
|
|
447
|
+
return cachedRecord(`yaml-registry:${path}`, () => {
|
|
448
|
+
const text = readTextFile(path);
|
|
449
|
+
if (!text) return undefined;
|
|
450
|
+
const parsed = parseYaml(text);
|
|
451
|
+
if (!object.isRecord(parsed)) return undefined;
|
|
452
|
+
const registry = yamlRegistry(parsed);
|
|
453
|
+
return registry ? { registry } : undefined;
|
|
454
|
+
})?.registry;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function readTextFile(path: string): string | undefined {
|
|
458
|
+
if (!fileStatSync(path)?.isFile()) return undefined;
|
|
459
|
+
return readFileSync(path, "utf8");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Parse a `registry=` assignment from npmrc / ini text. */
|
|
463
|
+
function parseIniRegistry(text: string): string | undefined {
|
|
464
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
465
|
+
const line = raw.trim();
|
|
466
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
467
|
+
const eq = line.indexOf("=");
|
|
468
|
+
if (eq === -1) continue;
|
|
469
|
+
if (line.slice(0, eq).trim() !== "registry") continue;
|
|
470
|
+
return unquoteIniValue(line.slice(eq + 1));
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Read Bun `install.registry` from a bunfig.toml. Table form
|
|
477
|
+
* `[install.registry]` with `url = "..."` is accepted; scoped maps are ignored.
|
|
478
|
+
*/
|
|
479
|
+
function parseBunfigRegistry(text: string): string | undefined {
|
|
480
|
+
let section = "";
|
|
481
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
482
|
+
const line = stripTomlComment(raw).trim();
|
|
483
|
+
if (!line) continue;
|
|
484
|
+
const header = /^\[([^\]]+)\]$/.exec(line);
|
|
485
|
+
if (header) {
|
|
486
|
+
section = header[1]!.trim();
|
|
487
|
+
continue;
|
|
324
488
|
}
|
|
489
|
+
const eq = line.indexOf("=");
|
|
490
|
+
if (eq === -1) continue;
|
|
491
|
+
const key = line.slice(0, eq).trim();
|
|
492
|
+
const value = unquoteIniValue(line.slice(eq + 1));
|
|
493
|
+
if (section === "install" && key === "registry" && !value.startsWith("{")) return value;
|
|
494
|
+
if (section === "install.registry" && key === "url") return value;
|
|
495
|
+
if (!section && key === "install.registry") return value;
|
|
325
496
|
}
|
|
326
497
|
return undefined;
|
|
327
498
|
}
|
|
328
499
|
|
|
500
|
+
function yamlRegistry(data: Record<string, unknown>): string | undefined {
|
|
501
|
+
if (typeof data.registry === "string") return data.registry;
|
|
502
|
+
const registries = data.registries;
|
|
503
|
+
if (object.isRecord(registries) && typeof registries.default === "string") {
|
|
504
|
+
return registries.default;
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function unquoteIniValue(value: string): string {
|
|
510
|
+
const trimmed = value.trim();
|
|
511
|
+
const comment = trimmed.search(/\s+#/);
|
|
512
|
+
const raw = comment === -1 ? trimmed : trimmed.slice(0, comment).trim();
|
|
513
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
514
|
+
return raw.slice(1, -1);
|
|
515
|
+
}
|
|
516
|
+
return raw;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function stripTomlComment(line: string): string {
|
|
520
|
+
const hash = line.indexOf("#");
|
|
521
|
+
return hash === -1 ? line : line.slice(0, hash);
|
|
522
|
+
}
|
|
523
|
+
|
|
329
524
|
function readPackageName(pkgPath: string): string | undefined {
|
|
330
525
|
if (!fileStatSync(pkgPath)?.isFile()) return undefined;
|
|
331
526
|
return stringUtils.trimToNull(json.parseRecord(readFileSync(pkgPath, "utf8"))?.name) ?? undefined;
|