@theholocron/pkg-utils 1.16.1-alpha.1 → 1.16.1-alpha.2

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
@@ -62,6 +62,20 @@ import { listWorkspacePackagesSync } from "@theholocron/pkg-utils";
62
62
  const publishable = listWorkspacePackagesSync(repoRoot).filter((p) => !p.manifest.private && p.name);
63
63
  ```
64
64
 
65
+ ### `discoverWorkspacePackages(repoRoot, options?)`
66
+
67
+ Every package the repo's `pnpm-workspace.yaml` declares, as `listWorkspacePackagesSync()` entries — in glob order, packages within a `<dir>/*` glob sorted by name. Two glob forms are understood: `<dir>/*` (each child of `<dir>` with a `package.json`) and a literal directory (that directory is the package). `!pattern` entries drop matching packages, the repo root (`.`) is skipped, and any other glob throws. Without a `pnpm-workspace.yaml`, or one with no `packages:`, `DEFAULT_WORKSPACE_ROOTS` (`packages`, `apps`, `plugins`) are scanned. `options.group` keeps one version group's folder, `options.excludeGroups` leaves some out, and `options.readText`, `options.readFile` and `options.listDirs` replace the disk access.
68
+
69
+ ```typescript
70
+ import { discoverWorkspacePackages } from "@theholocron/pkg-utils";
71
+
72
+ const names = discoverWorkspacePackages(repoRoot, { group: "clients" }).map((p) => p.name);
73
+ ```
74
+
75
+ ### `resolveWorkspaceGlobs(repoRoot, options?)` and `readWorkspaceGlobs(yaml)`
76
+
77
+ `readWorkspaceGlobs` reads the `packages:` list of a `pnpm-workspace.yaml` text into `{ include, exclude }` (a line-based read, not a YAML parse; quotes and trailing comments are stripped). `resolveWorkspaceGlobs` reads the file in `repoRoot` and falls back to the default roots as `<root>/*` when it lists none.
78
+
65
79
  ### `PackageJson`
66
80
 
67
81
  The fields these helpers read, typed; any other field passes through untyped.
package/dist/index.d.mts CHANGED
@@ -66,6 +66,63 @@ export interface ListWorkspacePackagesOptions {
66
66
  * `readPackageJson` instead.
67
67
  */
68
68
  export declare function listWorkspacePackagesSync(repoRoot: string, options?: ListWorkspacePackagesOptions): WorkspacePackageEntry[];
69
+ /** The `packages:` entries of a `pnpm-workspace.yaml`, split by whether they include or exclude (`!pattern`) packages. */
70
+ export interface WorkspaceGlobs {
71
+ include: string[];
72
+ exclude: string[];
73
+ }
74
+ /**
75
+ * The `packages:` list of a `pnpm-workspace.yaml`. A line-based read of the
76
+ * plain block-list form, not a YAML parse -- the same approach (and for the
77
+ * same reason) as `ensureRootWorkspaceMember()`: the file routinely carries a
78
+ * `catalog:`/`catalogs:`/`overrides:` block after `packages:` that nothing
79
+ * here should have to round-trip. Quotes and trailing comments are stripped;
80
+ * a missing `packages:` key, or the inline `packages: [a, b]` form, reads as no
81
+ * globs (callers fall back to the default roots).
82
+ */
83
+ export declare function readWorkspaceGlobs(workspaceYaml: string): WorkspaceGlobs;
84
+ /** Where packages live when there is no `pnpm-workspace.yaml` (or it lists none): the layout before globs were read. */
85
+ export declare const DEFAULT_WORKSPACE_ROOTS: readonly string[];
86
+ export interface DiscoverWorkspacePackagesOptions extends Pick<ListWorkspacePackagesOptions, "listDirs" | "readFile"> {
87
+ /** Replaces the read of `pnpm-workspace.yaml`. Throw (`ENOENT`) when it is absent -- or leave it out to read the real file. */
88
+ readText?: (path: string) => string;
89
+ /**
90
+ * Only the packages of one version group: a folder of the repo (`clients`,
91
+ * `observability`). A package belongs when it is that folder or sits under it.
92
+ */
93
+ group?: string;
94
+ /**
95
+ * Leave out the packages of these version groups. A repo that releases some folders on
96
+ * their own line releases everything else together, and that release must not sweep the
97
+ * independent groups in.
98
+ */
99
+ excludeGroups?: readonly string[];
100
+ }
101
+ /**
102
+ * Every package the repo's `pnpm-workspace.yaml` declares, as
103
+ * `listWorkspacePackagesSync()` entries -- in glob order, packages within a
104
+ * `<dir>/*` glob sorted by name. Two forms of glob are understood:
105
+ *
106
+ * - `<dir>/*`: each child of `<dir>` that has a `package.json` (`clients/*`).
107
+ * - a literal directory: that directory is the package (`observability`,
108
+ * `docs/theme`).
109
+ *
110
+ * `!pattern` entries drop matching packages. The repo root (`.`) is not a
111
+ * workspace child and is skipped; callers that treat a root-only repo as a
112
+ * single package read the root manifest themselves. Any other glob throws
113
+ * rather than being silently skipped. Without a `pnpm-workspace.yaml`, or one
114
+ * with no `packages:`, the default roots are scanned.
115
+ *
116
+ * Nothing is filtered by `name`, `version` or `private`; that is the caller's
117
+ * call, as for `listWorkspacePackagesSync()`.
118
+ */
119
+ export declare function discoverWorkspacePackages(repoRoot: string, options?: DiscoverWorkspacePackagesOptions): WorkspacePackageEntry[];
120
+ /**
121
+ * The globs {@link discoverWorkspacePackages} scans: the repo's
122
+ * `pnpm-workspace.yaml` `packages:` list, or the {@link DEFAULT_WORKSPACE_ROOTS}
123
+ * as `<root>/*` when there is no such file or it lists none.
124
+ */
125
+ export declare function resolveWorkspaceGlobs(repoRoot: string, options?: Pick<DiscoverWorkspacePackagesOptions, "readText">): WorkspaceGlobs;
69
126
  /**
70
127
  * Parses `package.json` text. Returns `undefined` for anything that isn't a
71
128
  * JSON object — invalid JSON, or valid JSON of another type (an array, `null`,
package/dist/index.mjs CHANGED
@@ -158,6 +158,132 @@ function listWorkspacePackagesSync(repoRoot, options = {}) {
158
158
  }
159
159
  return entries;
160
160
  }
161
+ // src/workspace-globs.ts
162
+ /**
163
+ * The `packages:` list of a `pnpm-workspace.yaml`. A line-based read of the
164
+ * plain block-list form, not a YAML parse -- the same approach (and for the
165
+ * same reason) as `ensureRootWorkspaceMember()`: the file routinely carries a
166
+ * `catalog:`/`catalogs:`/`overrides:` block after `packages:` that nothing
167
+ * here should have to round-trip. Quotes and trailing comments are stripped;
168
+ * a missing `packages:` key, or the inline `packages: [a, b]` form, reads as no
169
+ * globs (callers fall back to the default roots).
170
+ */
171
+ function readWorkspaceGlobs(workspaceYaml) {
172
+ const lines = workspaceYaml.split("\n");
173
+ const start = lines.findIndex((line) => /^packages:\s*(#.*)?$/.test(line));
174
+ const globs = {
175
+ include: [],
176
+ exclude: []
177
+ };
178
+ if (start === -1) return globs;
179
+ for (const line of lines.slice(start + 1)) {
180
+ if (line.trim() === "" || /^\s*#/.test(line)) continue;
181
+ const item = /^\s*-\s*(.*)$/.exec(line);
182
+ if (!item) break;
183
+ const value = item[1].replace(/(^|\s)#.*$/, "").trim().replace(/^["']|["']$/g, "").trim();
184
+ if (value === "") continue;
185
+ if (value.startsWith("!")) globs.exclude.push(normalizeGlob(value.slice(1)));
186
+ else globs.include.push(normalizeGlob(value));
187
+ }
188
+ return globs;
189
+ }
190
+ /** `./a/b/` -> `a/b`: the form {@link discoverWorkspacePackages} matches against `relativeDir`. */
191
+ function normalizeGlob(glob) {
192
+ return glob.replace(/^\.\//, "").replace(/\/+$/, "");
193
+ }
194
+ // src/discover-workspace-packages.ts
195
+ /** Where packages live when there is no `pnpm-workspace.yaml` (or it lists none): the layout before globs were read. */
196
+ const DEFAULT_WORKSPACE_ROOTS = [
197
+ "packages",
198
+ "apps",
199
+ "plugins"
200
+ ];
201
+ /** Characters pnpm globs treat specially. A literal directory has none of them. */
202
+ const GLOB_CHARS = /[*?[\]{}!]/;
203
+ /**
204
+ * Every package the repo's `pnpm-workspace.yaml` declares, as
205
+ * `listWorkspacePackagesSync()` entries -- in glob order, packages within a
206
+ * `<dir>/*` glob sorted by name. Two forms of glob are understood:
207
+ *
208
+ * - `<dir>/*`: each child of `<dir>` that has a `package.json` (`clients/*`).
209
+ * - a literal directory: that directory is the package (`observability`,
210
+ * `docs/theme`).
211
+ *
212
+ * `!pattern` entries drop matching packages. The repo root (`.`) is not a
213
+ * workspace child and is skipped; callers that treat a root-only repo as a
214
+ * single package read the root manifest themselves. Any other glob throws
215
+ * rather than being silently skipped. Without a `pnpm-workspace.yaml`, or one
216
+ * with no `packages:`, the default roots are scanned.
217
+ *
218
+ * Nothing is filtered by `name`, `version` or `private`; that is the caller's
219
+ * call, as for `listWorkspacePackagesSync()`.
220
+ */
221
+ function discoverWorkspacePackages(repoRoot, options = {}) {
222
+ const base = resolve(repoRoot);
223
+ const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));
224
+ const { include, exclude } = resolveWorkspaceGlobs(base, options);
225
+ const entries = /* @__PURE__ */ new Map();
226
+ for (const glob of include) {
227
+ if (glob === "." || glob === "") continue;
228
+ for (const entry of expandGlob(base, glob, {
229
+ readFile,
230
+ listDirs: options.listDirs
231
+ })) if (!exclude.some((pattern) => matches(entry.relativeDir, pattern))) entries.set(entry.relativeDir, entry);
232
+ }
233
+ const { group, excludeGroups = [] } = options;
234
+ return [...entries.values()].filter(({ relativeDir }) => (group === void 0 || inGroup(relativeDir, group)) && !excludeGroups.some((excluded) => inGroup(relativeDir, excluded)));
235
+ }
236
+ /** `relativeDir` is the group's folder, or is under it. `clients` holds `clients/http`, not `clients-old/x`. */
237
+ function inGroup(relativeDir, group) {
238
+ const folder = group.replace(/^\.\//, "").replace(/\/+$/, "");
239
+ return relativeDir === folder || relativeDir.startsWith(`${folder}/`);
240
+ }
241
+ /**
242
+ * The globs {@link discoverWorkspacePackages} scans: the repo's
243
+ * `pnpm-workspace.yaml` `packages:` list, or the {@link DEFAULT_WORKSPACE_ROOTS}
244
+ * as `<root>/*` when there is no such file or it lists none.
245
+ */
246
+ function resolveWorkspaceGlobs(repoRoot, options = {}) {
247
+ const readText = options.readText ?? ((path) => readFileSync(path, "utf8"));
248
+ let workspaceYaml = "";
249
+ try {
250
+ workspaceYaml = readText(join(resolve(repoRoot), "pnpm-workspace.yaml"));
251
+ } catch {}
252
+ const { include, exclude } = readWorkspaceGlobs(workspaceYaml);
253
+ return {
254
+ include: include.length > 0 ? include : DEFAULT_WORKSPACE_ROOTS.map((root) => `${root}/*`),
255
+ exclude
256
+ };
257
+ }
258
+ function expandGlob(base, glob, options) {
259
+ if (glob.endsWith("/*") && !GLOB_CHARS.test(glob.slice(0, -2))) return listWorkspacePackagesSync(base, {
260
+ ...options,
261
+ roots: [glob.slice(0, -2)]
262
+ });
263
+ if (GLOB_CHARS.test(glob)) throw new TypeError(`unsupported workspace pattern ${JSON.stringify(glob)}: only "<dir>/*" and literal directories are supported`);
264
+ const rel = relative(base, resolve(base, glob));
265
+ if (isAbsolute(glob) || rel === ".." || rel.startsWith(`..${sep}`)) throw new TypeError(`workspace directory must be inside the repo, got ${JSON.stringify(glob)}`);
266
+ const dir = join(base, glob);
267
+ try {
268
+ const manifest = parsePackageJson(options.readFile(join(dir, "package.json")));
269
+ return manifest ? [{
270
+ name: manifest.name,
271
+ dir,
272
+ relativeDir: glob,
273
+ manifest
274
+ }] : [];
275
+ } catch {
276
+ return [];
277
+ }
278
+ }
279
+ /** `relativeDir` against an exclusion: the same directory, or a child of `<dir>/*`. */
280
+ function matches(relativeDir, pattern) {
281
+ if (pattern.endsWith("/*")) {
282
+ const parent = pattern.slice(0, -2);
283
+ return relativeDir.startsWith(`${parent}/`) && !relativeDir.slice(parent.length + 1).includes("/");
284
+ }
285
+ return relativeDir === pattern;
286
+ }
161
287
  // src/read-package-json.ts
162
288
  /**
163
289
  * The parsed `<dir>/package.json`, or `undefined` when it is missing,
@@ -179,4 +305,4 @@ function readPackageJsonSync(dir, options = {}) {
179
305
  return;
180
306
  }
181
307
  }
182
- export { dependencyNames, detectPackageManager, listWorkspacePackagesSync, parsePackageJson, readPackageJson, readPackageJsonSync };
308
+ export { DEFAULT_WORKSPACE_ROOTS, dependencyNames, detectPackageManager, discoverWorkspacePackages, listWorkspacePackagesSync, parsePackageJson, readPackageJson, readPackageJsonSync, readWorkspaceGlobs, resolveWorkspaceGlobs };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/pkg-utils",
3
- "version": "1.16.1-alpha.1",
3
+ "version": "1.16.1-alpha.2",
4
4
  "description": "Read package.json, list its dependencies and detect the package manager.",
5
5
  "keywords": [
6
6
  "package.json",
@@ -31,9 +31,6 @@
31
31
  "dist"
32
32
  ],
33
33
  "devDependencies": {
34
- "@theholocron/eslint-config": "^8.8.2",
35
- "@theholocron/tsdown-config": "^8.8.2",
36
- "@theholocron/vitest-config": "^8.8.2",
37
34
  "@tsconfig/node-lts": "^24.0.1",
38
35
  "@types/node": "^26",
39
36
  "@vitest/coverage-v8": "^5.0.5",
@@ -43,7 +40,10 @@
43
40
  "globals": "^17.11.0",
44
41
  "tsdown": "^0.23.3",
45
42
  "typescript": "^5.9.3",
46
- "vitest": "^5.0.5"
43
+ "vitest": "^5.0.5",
44
+ "@theholocron/eslint-config": "^8.9.0-alpha.3",
45
+ "@theholocron/vitest-config": "^8.9.0-alpha.3",
46
+ "@theholocron/tsdown-config": "^8.9.0-alpha.3"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22"