@theholocron/pkg-utils 1.11.0 → 1.13.0
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 +17 -0
- package/dist/index.d.mts +106 -0
- package/dist/index.mjs +187 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,23 @@ pnpm, npm, yarn or bun, else the first lockfile found (`pnpm-lock.yaml`,
|
|
|
45
45
|
(default `"pnpm"`). `options.readFile` and `options.fileExists` replace the disk
|
|
46
46
|
access.
|
|
47
47
|
|
|
48
|
+
### `listWorkspacePackagesSync(repoRoot, options?)`
|
|
49
|
+
|
|
50
|
+
Every package directly under the workspace roots of `repoRoot` that has a
|
|
51
|
+
readable `package.json` holding a JSON object, as `{ name, dir, relativeDir,
|
|
52
|
+
manifest }` — roots in the order given, packages within a root sorted by
|
|
53
|
+
directory name. A missing root, and a directory without a usable manifest,
|
|
54
|
+
contribute nothing. Nothing is filtered by `name`, `version` or `private`: that
|
|
55
|
+
is the caller's call. `options.roots` (default `["packages", "plugins"]`) names
|
|
56
|
+
the directories to scan, each inside `repoRoot` (a `TypeError` otherwise); `options.readFile` and `options.listDirs` replace the
|
|
57
|
+
disk access. A listing error other than "not there" throws. `dir` is always absolute. A single-package repo has no roots — use `readPackageJson` on it.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
import { listWorkspacePackagesSync } from "@theholocron/pkg-utils";
|
|
61
|
+
|
|
62
|
+
const publishable = listWorkspacePackagesSync(repoRoot).filter((p) => !p.manifest.private && p.name);
|
|
63
|
+
```
|
|
64
|
+
|
|
48
65
|
### `PackageJson`
|
|
49
66
|
|
|
50
67
|
The fields these helpers read, typed; any other field passes through untyped.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
//#region src/package-json.type.d.ts
|
|
2
|
+
/** The `package.json` fields these utils and their callers read; any other field passes through untyped. */
|
|
3
|
+
interface PackageJson {
|
|
4
|
+
name?: string;
|
|
5
|
+
version?: string;
|
|
6
|
+
private?: boolean;
|
|
7
|
+
packageManager?: string;
|
|
8
|
+
scripts?: Record<string, string>;
|
|
9
|
+
dependencies?: Record<string, string>;
|
|
10
|
+
devDependencies?: Record<string, string>;
|
|
11
|
+
peerDependencies?: Record<string, string>;
|
|
12
|
+
optionalDependencies?: Record<string, string>;
|
|
13
|
+
[field: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/dependency-names.d.ts
|
|
17
|
+
export type DependencyField = "dependencies" | "devDependencies" | "peerDependencies" | "optionalDependencies";
|
|
18
|
+
/**
|
|
19
|
+
* Every package name declared in `pkg`'s dependency maps — `dependencies` and
|
|
20
|
+
* `devDependencies` unless `fields` says otherwise. An absent `pkg`, a missing
|
|
21
|
+
* map, an array or any other non-object contributes nothing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function dependencyNames(pkg: PackageJson | undefined, fields?: readonly DependencyField[]): ReadonlySet<string>;
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/detect-package-manager.d.ts
|
|
26
|
+
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
|
|
27
|
+
export interface DetectPackageManagerOptions {
|
|
28
|
+
/** Replaces the `package.json` read — for callers with an injected or in-memory filesystem. */
|
|
29
|
+
readFile?: (path: string) => string;
|
|
30
|
+
/** Replaces the lockfile existence check. */
|
|
31
|
+
fileExists?: (path: string) => boolean;
|
|
32
|
+
/** Returned when neither `package.json` nor a lockfile says. Defaults to `"pnpm"`. */
|
|
33
|
+
fallback?: PackageManager;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The package manager `dir` uses: the name in `package.json#packageManager`
|
|
37
|
+
* (`pnpm@10.0.0` → `pnpm`) when it's one of pnpm, npm, yarn or bun, else the
|
|
38
|
+
* first lockfile found, else `options.fallback`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function detectPackageManager(dir: string, options?: DetectPackageManagerOptions): PackageManager;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/workspace-package.type.d.ts
|
|
43
|
+
/** One workspace package found by {@link listWorkspacePackagesSync}. */
|
|
44
|
+
interface WorkspacePackageEntry {
|
|
45
|
+
/** The manifest's `name`, when it has one. */
|
|
46
|
+
name: string | undefined;
|
|
47
|
+
/** Absolute path to the package directory. */
|
|
48
|
+
dir: string;
|
|
49
|
+
/** The directory relative to the repo root, `/`-separated (`packages/cli`). */
|
|
50
|
+
relativeDir: string;
|
|
51
|
+
manifest: PackageJson;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/list-workspace-packages.d.ts
|
|
55
|
+
export interface ListWorkspacePackagesOptions {
|
|
56
|
+
/** Directories under the repo root whose children are packages; each must stay inside it. Defaults to `packages` and `plugins`. */
|
|
57
|
+
roots?: readonly string[];
|
|
58
|
+
/** Replaces the `package.json` read — for callers with an injected or in-memory filesystem. */
|
|
59
|
+
readFile?: (path: string) => string;
|
|
60
|
+
/** Replaces the directory listing: the names of the subdirectories of `dir`. Throws an `ENOENT` error when `dir` is absent. */
|
|
61
|
+
listDirs?: (dir: string) => string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Every package directly under the workspace `roots` of `repoRoot` that has a
|
|
65
|
+
* readable `package.json` holding a JSON object — roots in the order given,
|
|
66
|
+
* packages within a root sorted by directory name. A missing root, and a
|
|
67
|
+
* directory without a usable manifest, contribute nothing.
|
|
68
|
+
*
|
|
69
|
+
* Nothing is filtered by `name`, `version` or `private`: that is the caller's
|
|
70
|
+
* call (publishing wants public packages, version bumping wants all of them).
|
|
71
|
+
* `repoRoot` is resolved against the working directory, so `dir` is always
|
|
72
|
+
* absolute. A root that is absolute or climbs out of `repoRoot` throws a
|
|
73
|
+
* `TypeError`, and so does any listing error other than "not there"
|
|
74
|
+
* (`ENOENT`/`ENOTDIR`). A single-package repo has no roots; read its root manifest with
|
|
75
|
+
* `readPackageJson` instead.
|
|
76
|
+
*/
|
|
77
|
+
export declare function listWorkspacePackagesSync(repoRoot: string, options?: ListWorkspacePackagesOptions): WorkspacePackageEntry[];
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region src/parse-package-json.d.ts
|
|
80
|
+
/**
|
|
81
|
+
* Parses `package.json` text. Returns `undefined` for anything that isn't a
|
|
82
|
+
* JSON object — invalid JSON, or valid JSON of another type (an array, `null`,
|
|
83
|
+
* a string) — rather than throwing. A leading byte-order mark is ignored, as
|
|
84
|
+
* npm does.
|
|
85
|
+
*
|
|
86
|
+
* The fields {@link PackageJson} types are checked, so the result honours its
|
|
87
|
+
* type: a recognized field of the wrong type (a numeric `name`, an array
|
|
88
|
+
* `dependencies`) is left out, and the string-map fields keep only their
|
|
89
|
+
* string-valued entries. Every other field passes through untouched.
|
|
90
|
+
*/
|
|
91
|
+
export declare function parsePackageJson(text: string): PackageJson | undefined;
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/read-package-json.d.ts
|
|
94
|
+
export interface ReadPackageJsonOptions {
|
|
95
|
+
/** Replaces the file read — for callers with an injected or in-memory filesystem. */
|
|
96
|
+
readFile?: (path: string) => string;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The parsed `<dir>/package.json`, or `undefined` when it is missing,
|
|
100
|
+
* unreadable or not a JSON object.
|
|
101
|
+
*/
|
|
102
|
+
export declare function readPackageJson(dir: string): Promise<PackageJson | undefined>;
|
|
103
|
+
/** Synchronous twin of {@link readPackageJson}, with an injectable `readFile`. */
|
|
104
|
+
export declare function readPackageJsonSync(dir: string, options?: ReadPackageJsonOptions): PackageJson | undefined;
|
|
105
|
+
//#endregion
|
|
106
|
+
export type { PackageJson, WorkspacePackageEntry };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
//#region src/dependency-names.ts
|
|
5
|
+
/**
|
|
6
|
+
* Every package name declared in `pkg`'s dependency maps — `dependencies` and
|
|
7
|
+
* `devDependencies` unless `fields` says otherwise. An absent `pkg`, a missing
|
|
8
|
+
* map, an array or any other non-object contributes nothing.
|
|
9
|
+
*/
|
|
10
|
+
function dependencyNames(pkg, fields = ["dependencies", "devDependencies"]) {
|
|
11
|
+
const names = /* @__PURE__ */ new Set();
|
|
12
|
+
for (const field of fields) {
|
|
13
|
+
const map = pkg?.[field];
|
|
14
|
+
if (typeof map === "object" && map !== null && !Array.isArray(map)) for (const name of Object.keys(map)) names.add(name);
|
|
15
|
+
}
|
|
16
|
+
return names;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/parse-package-json.ts
|
|
20
|
+
const STRING_FIELDS = [
|
|
21
|
+
"name",
|
|
22
|
+
"version",
|
|
23
|
+
"packageManager"
|
|
24
|
+
];
|
|
25
|
+
const MAP_FIELDS = [
|
|
26
|
+
"scripts",
|
|
27
|
+
"dependencies",
|
|
28
|
+
"devDependencies",
|
|
29
|
+
"peerDependencies",
|
|
30
|
+
"optionalDependencies"
|
|
31
|
+
];
|
|
32
|
+
/** A plain object's string-valued entries; `undefined` for anything that isn't a plain object. */
|
|
33
|
+
function stringMap(value) {
|
|
34
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
35
|
+
return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parses `package.json` text. Returns `undefined` for anything that isn't a
|
|
39
|
+
* JSON object — invalid JSON, or valid JSON of another type (an array, `null`,
|
|
40
|
+
* a string) — rather than throwing. A leading byte-order mark is ignored, as
|
|
41
|
+
* npm does.
|
|
42
|
+
*
|
|
43
|
+
* The fields {@link PackageJson} types are checked, so the result honours its
|
|
44
|
+
* type: a recognized field of the wrong type (a numeric `name`, an array
|
|
45
|
+
* `dependencies`) is left out, and the string-map fields keep only their
|
|
46
|
+
* string-valued entries. Every other field passes through untouched.
|
|
47
|
+
*/
|
|
48
|
+
function parsePackageJson(text) {
|
|
49
|
+
let value;
|
|
50
|
+
try {
|
|
51
|
+
value = JSON.parse(text.charCodeAt(0) === 65279 ? text.slice(1) : text);
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
56
|
+
const raw = value;
|
|
57
|
+
const pkg = { ...raw };
|
|
58
|
+
for (const field of STRING_FIELDS) if (typeof raw[field] !== "string") delete pkg[field];
|
|
59
|
+
if (typeof raw["private"] !== "boolean") delete pkg["private"];
|
|
60
|
+
for (const field of MAP_FIELDS) {
|
|
61
|
+
const map = stringMap(raw[field]);
|
|
62
|
+
if (map) pkg[field] = map;
|
|
63
|
+
else delete pkg[field];
|
|
64
|
+
}
|
|
65
|
+
return pkg;
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/detect-package-manager.ts
|
|
69
|
+
const MANAGERS = [
|
|
70
|
+
"pnpm",
|
|
71
|
+
"npm",
|
|
72
|
+
"yarn",
|
|
73
|
+
"bun"
|
|
74
|
+
];
|
|
75
|
+
/** Checked in this order; the first lockfile present wins. */
|
|
76
|
+
const LOCKFILES = [
|
|
77
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
78
|
+
["bun.lockb", "bun"],
|
|
79
|
+
["bun.lock", "bun"],
|
|
80
|
+
["yarn.lock", "yarn"],
|
|
81
|
+
["npm-shrinkwrap.json", "npm"],
|
|
82
|
+
["package-lock.json", "npm"]
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* The package manager `dir` uses: the name in `package.json#packageManager`
|
|
86
|
+
* (`pnpm@10.0.0` → `pnpm`) when it's one of pnpm, npm, yarn or bun, else the
|
|
87
|
+
* first lockfile found, else `options.fallback`.
|
|
88
|
+
*/
|
|
89
|
+
function detectPackageManager(dir, options = {}) {
|
|
90
|
+
const read = options.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
91
|
+
const exists = options.fileExists ?? existsSync;
|
|
92
|
+
let declared;
|
|
93
|
+
try {
|
|
94
|
+
const field = parsePackageJson(read(join(dir, "package.json")))?.packageManager;
|
|
95
|
+
declared = typeof field === "string" ? field.split("@")[0] : void 0;
|
|
96
|
+
} catch {}
|
|
97
|
+
if (declared && MANAGERS.includes(declared)) return declared;
|
|
98
|
+
for (const [file, manager] of LOCKFILES) if (exists(join(dir, file))) return manager;
|
|
99
|
+
return options.fallback ?? "pnpm";
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/list-workspace-packages.ts
|
|
103
|
+
function isAbsent(error) {
|
|
104
|
+
const code = error?.code;
|
|
105
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
106
|
+
}
|
|
107
|
+
const DEFAULT_ROOTS = ["packages", "plugins"];
|
|
108
|
+
function defaultListDirs(dir) {
|
|
109
|
+
return readdirSync(dir).filter((name) => {
|
|
110
|
+
try {
|
|
111
|
+
return statSync(join(dir, name)).isDirectory();
|
|
112
|
+
} catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Every package directly under the workspace `roots` of `repoRoot` that has a
|
|
119
|
+
* readable `package.json` holding a JSON object — roots in the order given,
|
|
120
|
+
* packages within a root sorted by directory name. A missing root, and a
|
|
121
|
+
* directory without a usable manifest, contribute nothing.
|
|
122
|
+
*
|
|
123
|
+
* Nothing is filtered by `name`, `version` or `private`: that is the caller's
|
|
124
|
+
* call (publishing wants public packages, version bumping wants all of them).
|
|
125
|
+
* `repoRoot` is resolved against the working directory, so `dir` is always
|
|
126
|
+
* absolute. A root that is absolute or climbs out of `repoRoot` throws a
|
|
127
|
+
* `TypeError`, and so does any listing error other than "not there"
|
|
128
|
+
* (`ENOENT`/`ENOTDIR`). A single-package repo has no roots; read its root manifest with
|
|
129
|
+
* `readPackageJson` instead.
|
|
130
|
+
*/
|
|
131
|
+
function listWorkspacePackagesSync(repoRoot, options = {}) {
|
|
132
|
+
const { roots = DEFAULT_ROOTS, listDirs = defaultListDirs } = options;
|
|
133
|
+
const base = resolve(repoRoot);
|
|
134
|
+
const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
135
|
+
const entries = [];
|
|
136
|
+
for (const root of roots) {
|
|
137
|
+
const rel = relative(base, resolve(base, root));
|
|
138
|
+
if (isAbsolute(root) || rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) throw new TypeError(`workspace root must be a directory inside the repo, got ${JSON.stringify(root)}`);
|
|
139
|
+
let folders;
|
|
140
|
+
try {
|
|
141
|
+
folders = [...listDirs(join(base, root))].sort();
|
|
142
|
+
} catch (error) {
|
|
143
|
+
if (isAbsent(error)) continue;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
for (const folder of folders) {
|
|
147
|
+
const dir = join(base, root, folder);
|
|
148
|
+
let manifest;
|
|
149
|
+
try {
|
|
150
|
+
manifest = parsePackageJson(readFile(join(dir, "package.json")));
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (manifest) entries.push({
|
|
155
|
+
name: manifest.name,
|
|
156
|
+
dir,
|
|
157
|
+
relativeDir: `${root}/${folder}`,
|
|
158
|
+
manifest
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return entries;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/read-package-json.ts
|
|
166
|
+
/**
|
|
167
|
+
* The parsed `<dir>/package.json`, or `undefined` when it is missing,
|
|
168
|
+
* unreadable or not a JSON object.
|
|
169
|
+
*/
|
|
170
|
+
async function readPackageJson(dir) {
|
|
171
|
+
try {
|
|
172
|
+
return parsePackageJson(await readFile(join(dir, "package.json"), "utf8"));
|
|
173
|
+
} catch {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Synchronous twin of {@link readPackageJson}, with an injectable `readFile`. */
|
|
178
|
+
function readPackageJsonSync(dir, options = {}) {
|
|
179
|
+
const read = options.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
180
|
+
try {
|
|
181
|
+
return parsePackageJson(read(join(dir, "package.json")));
|
|
182
|
+
} catch {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
//#endregion
|
|
187
|
+
export { dependencyNames, detectPackageManager, listWorkspacePackagesSync, parsePackageJson, readPackageJson, readPackageJsonSync };
|