@theholocron/pkg-utils 1.12.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 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 CHANGED
@@ -39,6 +39,43 @@ export interface DetectPackageManagerOptions {
39
39
  */
40
40
  export declare function detectPackageManager(dir: string, options?: DetectPackageManagerOptions): PackageManager;
41
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
42
79
  //#region src/parse-package-json.d.ts
43
80
  /**
44
81
  * Parses `package.json` text. Returns `undefined` for anything that isn't a
@@ -66,4 +103,4 @@ export declare function readPackageJson(dir: string): Promise<PackageJson | unde
66
103
  /** Synchronous twin of {@link readPackageJson}, with an injectable `readFile`. */
67
104
  export declare function readPackageJsonSync(dir: string, options?: ReadPackageJsonOptions): PackageJson | undefined;
68
105
  //#endregion
69
- export type { PackageJson };
106
+ export type { PackageJson, WorkspacePackageEntry };
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { readFile } from "node:fs/promises";
4
4
  //#region src/dependency-names.ts
5
5
  /**
@@ -99,6 +99,69 @@ function detectPackageManager(dir, options = {}) {
99
99
  return options.fallback ?? "pnpm";
100
100
  }
101
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
102
165
  //#region src/read-package-json.ts
103
166
  /**
104
167
  * The parsed `<dir>/package.json`, or `undefined` when it is missing,
@@ -121,4 +184,4 @@ function readPackageJsonSync(dir, options = {}) {
121
184
  }
122
185
  }
123
186
  //#endregion
124
- export { dependencyNames, detectPackageManager, parsePackageJson, readPackageJson, readPackageJsonSync };
187
+ export { dependencyNames, detectPackageManager, listWorkspacePackagesSync, parsePackageJson, readPackageJson, readPackageJsonSync };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/pkg-utils",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "Read package.json, list its dependencies and detect the package manager.",
5
5
  "keywords": [
6
6
  "package.json",