@theholocron/pkg-utils 1.16.1-alpha.1 → 1.16.1-alpha.3
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 +26 -0
- package/dist/index.d.mts +105 -0
- package/dist/index.mjs +247 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -62,6 +62,32 @@ 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
|
+
|
|
79
|
+
### `parseCatalogs(text)`
|
|
80
|
+
|
|
81
|
+
The `catalog:` and `catalogs:` blocks of a `pnpm-workspace.yaml` as `{ default: {...}, <name>: {...} }`, package name to range. A line-based read of the flat format pnpm writes; entries outside a catalog are ignored.
|
|
82
|
+
|
|
83
|
+
### `addWorkspaceMember(workspaceYaml, member)`
|
|
84
|
+
|
|
85
|
+
Adds `member` (for example `"."`) as the first quoted entry of the `packages:` list and returns `{ content, changed }`. A line-based edit that only adds a line, so the `catalog:` and `overrides:` blocks after it keep their formatting. Nothing changes when the member is already listed, or when there is no plain block-list `packages:`.
|
|
86
|
+
|
|
87
|
+
### `bumpCatalogPins(content, fetchLatest, options?)`
|
|
88
|
+
|
|
89
|
+
Rewrites the pins of every catalog entry to the latest version `fetchLatest(name)` returns, keeping the range prefix (`^`, `~`) and the quoting. It never downgrades, leaves a package with no answer alone, and asks once per package. `options.include` limits which packages are looked up. Returns `{ content, bumps }`. `gt(a, b)` is the comparison it uses for `x.y.z` release versions.
|
|
90
|
+
|
|
65
91
|
### `PackageJson`
|
|
66
92
|
|
|
67
93
|
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`,
|
|
@@ -78,6 +135,54 @@ export declare function listWorkspacePackagesSync(repoRoot: string, options?: Li
|
|
|
78
135
|
* string-valued entries. Every other field passes through untouched.
|
|
79
136
|
*/
|
|
80
137
|
export declare function parsePackageJson(text: string): PackageJson | undefined;
|
|
138
|
+
/** The `catalog:` and `catalogs:` of a `pnpm-workspace.yaml`: the default one under `default`, the named ones under their names. */
|
|
139
|
+
export type Catalogs = Record<string, Record<string, string>>;
|
|
140
|
+
/** The `packages:` key of a `pnpm-workspace.yaml`, as a line. */
|
|
141
|
+
export declare const PACKAGES_KEY: RegExp;
|
|
142
|
+
/** What an edit of a `pnpm-workspace.yaml` gives back: the new text, and whether it differs from what was passed in. */
|
|
143
|
+
export interface WorkspaceYamlEdit {
|
|
144
|
+
/** The file's text after the edit; the text passed in when nothing changed. */
|
|
145
|
+
content: string;
|
|
146
|
+
/** Whether `content` differs from the text passed in. */
|
|
147
|
+
changed: boolean;
|
|
148
|
+
}
|
|
149
|
+
/** A catalog pin rewritten by {@link bumpCatalogPins}. */
|
|
150
|
+
export interface CatalogBump {
|
|
151
|
+
/** The package whose pin was rewritten. */
|
|
152
|
+
pkg: string;
|
|
153
|
+
/** The version it was pinned to. */
|
|
154
|
+
from: string;
|
|
155
|
+
/** The version it is pinned to now. */
|
|
156
|
+
to: string;
|
|
157
|
+
}
|
|
158
|
+
/** Resolves a package's latest published version, or `null` when there is no answer. */
|
|
159
|
+
export type FetchLatest = (pkg: string) => Promise<string | null>;
|
|
160
|
+
/** What {@link bumpCatalogPins} may be told. */
|
|
161
|
+
export interface BumpCatalogPinsOptions {
|
|
162
|
+
/** Which pinned packages to look up. Default: all of them. */
|
|
163
|
+
include?: (pkg: string) => boolean;
|
|
164
|
+
}
|
|
165
|
+
/** Parse the `catalog:` and `catalogs:` blocks of `pnpm-workspace.yaml` (the generated, flat format). */
|
|
166
|
+
export declare function parseCatalogs(text: string): Catalogs;
|
|
167
|
+
/**
|
|
168
|
+
* Add `member` as the first entry of the `packages:` list, unless the list already has it. A targeted
|
|
169
|
+
* line-based edit, not a YAML round-trip: the file routinely carries a `catalog:`/`catalogs:`/`overrides:`
|
|
170
|
+
* block after `packages:`, and a parser would reformat content nobody asked to touch. It only ever adds a
|
|
171
|
+
* line, quoted like `"docs"`, and changes nothing when there is no `packages:` key or it is not the plain
|
|
172
|
+
* block-list form.
|
|
173
|
+
*/
|
|
174
|
+
export declare function addWorkspaceMember(workspaceYaml: string, member: string): WorkspaceYamlEdit;
|
|
175
|
+
/** `a > b` for `x.y.z` release versions (prerelease tags are ignored). */
|
|
176
|
+
export declare function gt(a: string, b: string): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Rewrite the pins of the `catalog:` and every named `catalogs.*` entry to the latest published version,
|
|
179
|
+
* keeping the range prefix (`^`, `~`). Never downgrades; a package with no answer from `fetchLatest` is left
|
|
180
|
+
* alone. `fetchLatest` is called once per package.
|
|
181
|
+
*/
|
|
182
|
+
export declare function bumpCatalogPins(content: string, fetchLatest: FetchLatest, options?: BumpCatalogPinsOptions): Promise<{
|
|
183
|
+
content: string;
|
|
184
|
+
bumps: CatalogBump[];
|
|
185
|
+
}>;
|
|
81
186
|
export interface ReadPackageJsonOptions {
|
|
82
187
|
/** Replaces the file read — for callers with an injected or in-memory filesystem. */
|
|
83
188
|
readFile?: (path: string) => string;
|
package/dist/index.mjs
CHANGED
|
@@ -158,6 +158,252 @@ function listWorkspacePackagesSync(repoRoot, options = {}) {
|
|
|
158
158
|
}
|
|
159
159
|
return entries;
|
|
160
160
|
}
|
|
161
|
+
// src/pnpm-workspace.ts
|
|
162
|
+
/** The `packages:` key of a `pnpm-workspace.yaml`, as a line. */
|
|
163
|
+
const PACKAGES_KEY = /^packages:\s*(#.*)?$/;
|
|
164
|
+
/** Parse the `catalog:` and `catalogs:` blocks of `pnpm-workspace.yaml` (the generated, flat format). */
|
|
165
|
+
function parseCatalogs(text) {
|
|
166
|
+
const catalogs = { default: {} };
|
|
167
|
+
let current = null;
|
|
168
|
+
let inCatalogs = false;
|
|
169
|
+
for (const raw of text.split("\n")) {
|
|
170
|
+
if (raw.trim() === "" || raw.trimStart().startsWith("#")) continue;
|
|
171
|
+
if (/^catalog:\s*$/.test(raw)) {
|
|
172
|
+
current = "default";
|
|
173
|
+
inCatalogs = false;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (/^catalogs:\s*$/.test(raw)) {
|
|
177
|
+
current = null;
|
|
178
|
+
inCatalogs = true;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (/^\S/.test(raw)) {
|
|
182
|
+
current = null;
|
|
183
|
+
inCatalogs = false;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const named = inCatalogs ? raw.match(/^ {2}([A-Za-z0-9_-]+):\s*$/) : null;
|
|
187
|
+
if (named?.[1]) {
|
|
188
|
+
current = named[1];
|
|
189
|
+
catalogs[current] ??= {};
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const entry = raw.match(/^\s+("[^"]+"|[^\s:][^:]*?):\s*(.+?)\s*$/);
|
|
193
|
+
if (entry?.[1] && entry[2] && current) (catalogs[current] ??= {})[entry[1].replace(/^"|"$/g, "")] = entry[2].replace(/^"|"$/g, "");
|
|
194
|
+
}
|
|
195
|
+
return catalogs;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Add `member` as the first entry of the `packages:` list, unless the list already has it. A targeted
|
|
199
|
+
* line-based edit, not a YAML round-trip: the file routinely carries a `catalog:`/`catalogs:`/`overrides:`
|
|
200
|
+
* block after `packages:`, and a parser would reformat content nobody asked to touch. It only ever adds a
|
|
201
|
+
* line, quoted like `"docs"`, and changes nothing when there is no `packages:` key or it is not the plain
|
|
202
|
+
* block-list form.
|
|
203
|
+
*/
|
|
204
|
+
function addWorkspaceMember(workspaceYaml, member) {
|
|
205
|
+
const lines = workspaceYaml.split("\n");
|
|
206
|
+
const packagesLineIdx = lines.findIndex((l) => PACKAGES_KEY.test(l));
|
|
207
|
+
if (packagesLineIdx === -1) return {
|
|
208
|
+
content: workspaceYaml,
|
|
209
|
+
changed: false
|
|
210
|
+
};
|
|
211
|
+
const items = [];
|
|
212
|
+
let cursor = packagesLineIdx + 1;
|
|
213
|
+
while (cursor < lines.length && /^\s*-\s*/.test(lines[cursor])) {
|
|
214
|
+
items.push(lines[cursor]);
|
|
215
|
+
cursor++;
|
|
216
|
+
}
|
|
217
|
+
if (items.some((item) => {
|
|
218
|
+
const value = item.replace(/^\s*-\s*/, "").replace(/^["']|["']$/g, "").trim();
|
|
219
|
+
return value === member || member === "." && value === "";
|
|
220
|
+
})) return {
|
|
221
|
+
content: workspaceYaml,
|
|
222
|
+
changed: false
|
|
223
|
+
};
|
|
224
|
+
const indent = items[0]?.match(/^(\s*-\s*)/)?.[1] ?? " - ";
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
...lines.slice(0, packagesLineIdx + 1),
|
|
228
|
+
`${indent}"${member}"`,
|
|
229
|
+
...lines.slice(packagesLineIdx + 1)
|
|
230
|
+
].join("\n"),
|
|
231
|
+
changed: true
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** `a > b` for `x.y.z` release versions (prerelease tags are ignored). */
|
|
235
|
+
function gt(a, b) {
|
|
236
|
+
const pa = a.split(".").map((n) => parseInt(n, 10));
|
|
237
|
+
const pb = b.split(".").map((n) => parseInt(n, 10));
|
|
238
|
+
for (let i = 0; i < 3; i++) {
|
|
239
|
+
const x = pa[i] ?? 0;
|
|
240
|
+
const y = pb[i] ?? 0;
|
|
241
|
+
if (x !== y) return x > y;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const CATALOG_LINE = /^(\s*['"]?)((?:@[a-z0-9._-]+\/)?[a-z0-9._-]+)(['"]?\s*:\s*['"]?)([~^]?)(\d+\.\d+\.\d+[\w.-]*)(['"]?\s*)$/;
|
|
246
|
+
/**
|
|
247
|
+
* Rewrite the pins of the `catalog:` and every named `catalogs.*` entry to the latest published version,
|
|
248
|
+
* keeping the range prefix (`^`, `~`). Never downgrades; a package with no answer from `fetchLatest` is left
|
|
249
|
+
* alone. `fetchLatest` is called once per package.
|
|
250
|
+
*/
|
|
251
|
+
async function bumpCatalogPins(content, fetchLatest, options = {}) {
|
|
252
|
+
const include = options.include ?? (() => true);
|
|
253
|
+
const lines = content.split("\n");
|
|
254
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
255
|
+
for (const line of lines) {
|
|
256
|
+
const m = CATALOG_LINE.exec(line);
|
|
257
|
+
if (m && include(m[2])) wanted.add(m[2]);
|
|
258
|
+
}
|
|
259
|
+
const latest = /* @__PURE__ */ new Map();
|
|
260
|
+
await Promise.all([...wanted].map(async (pkg) => {
|
|
261
|
+
latest.set(pkg, await fetchLatest(pkg));
|
|
262
|
+
}));
|
|
263
|
+
const bumps = [];
|
|
264
|
+
return {
|
|
265
|
+
content: lines.map((line) => {
|
|
266
|
+
const m = CATALOG_LINE.exec(line);
|
|
267
|
+
if (!m) return line;
|
|
268
|
+
const [, pre, pkg, mid, prefix, current, post] = m;
|
|
269
|
+
const next = latest.get(pkg);
|
|
270
|
+
if (!next || next === current || !gt(next, current)) return line;
|
|
271
|
+
bumps.push({
|
|
272
|
+
pkg,
|
|
273
|
+
from: current,
|
|
274
|
+
to: next
|
|
275
|
+
});
|
|
276
|
+
return `${pre}${pkg}${mid}${prefix}${next}${post}`;
|
|
277
|
+
}).join("\n"),
|
|
278
|
+
bumps
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// src/workspace-globs.ts
|
|
282
|
+
/**
|
|
283
|
+
* The `packages:` list of a `pnpm-workspace.yaml`. A line-based read of the
|
|
284
|
+
* plain block-list form, not a YAML parse -- the same approach (and for the
|
|
285
|
+
* same reason) as `ensureRootWorkspaceMember()`: the file routinely carries a
|
|
286
|
+
* `catalog:`/`catalogs:`/`overrides:` block after `packages:` that nothing
|
|
287
|
+
* here should have to round-trip. Quotes and trailing comments are stripped;
|
|
288
|
+
* a missing `packages:` key, or the inline `packages: [a, b]` form, reads as no
|
|
289
|
+
* globs (callers fall back to the default roots).
|
|
290
|
+
*/
|
|
291
|
+
function readWorkspaceGlobs(workspaceYaml) {
|
|
292
|
+
const lines = workspaceYaml.split("\n");
|
|
293
|
+
const start = lines.findIndex((line) => PACKAGES_KEY.test(line));
|
|
294
|
+
const globs = {
|
|
295
|
+
include: [],
|
|
296
|
+
exclude: []
|
|
297
|
+
};
|
|
298
|
+
if (start === -1) return globs;
|
|
299
|
+
for (const line of lines.slice(start + 1)) {
|
|
300
|
+
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
301
|
+
const item = /^\s*-\s*(.*)$/.exec(line);
|
|
302
|
+
if (!item) break;
|
|
303
|
+
const value = item[1].replace(/(^|\s)#.*$/, "").trim().replace(/^["']|["']$/g, "").trim();
|
|
304
|
+
if (value === "") continue;
|
|
305
|
+
if (value.startsWith("!")) globs.exclude.push(normalizeGlob(value.slice(1)));
|
|
306
|
+
else globs.include.push(normalizeGlob(value));
|
|
307
|
+
}
|
|
308
|
+
return globs;
|
|
309
|
+
}
|
|
310
|
+
/** `./a/b/` -> `a/b`: the form {@link discoverWorkspacePackages} matches against `relativeDir`. */
|
|
311
|
+
function normalizeGlob(glob) {
|
|
312
|
+
return glob.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
313
|
+
}
|
|
314
|
+
// src/discover-workspace-packages.ts
|
|
315
|
+
/** Where packages live when there is no `pnpm-workspace.yaml` (or it lists none): the layout before globs were read. */
|
|
316
|
+
const DEFAULT_WORKSPACE_ROOTS = [
|
|
317
|
+
"packages",
|
|
318
|
+
"apps",
|
|
319
|
+
"plugins"
|
|
320
|
+
];
|
|
321
|
+
/** Characters pnpm globs treat specially. A literal directory has none of them. */
|
|
322
|
+
const GLOB_CHARS = /[*?[\]{}!]/;
|
|
323
|
+
/**
|
|
324
|
+
* Every package the repo's `pnpm-workspace.yaml` declares, as
|
|
325
|
+
* `listWorkspacePackagesSync()` entries -- in glob order, packages within a
|
|
326
|
+
* `<dir>/*` glob sorted by name. Two forms of glob are understood:
|
|
327
|
+
*
|
|
328
|
+
* - `<dir>/*`: each child of `<dir>` that has a `package.json` (`clients/*`).
|
|
329
|
+
* - a literal directory: that directory is the package (`observability`,
|
|
330
|
+
* `docs/theme`).
|
|
331
|
+
*
|
|
332
|
+
* `!pattern` entries drop matching packages. The repo root (`.`) is not a
|
|
333
|
+
* workspace child and is skipped; callers that treat a root-only repo as a
|
|
334
|
+
* single package read the root manifest themselves. Any other glob throws
|
|
335
|
+
* rather than being silently skipped. Without a `pnpm-workspace.yaml`, or one
|
|
336
|
+
* with no `packages:`, the default roots are scanned.
|
|
337
|
+
*
|
|
338
|
+
* Nothing is filtered by `name`, `version` or `private`; that is the caller's
|
|
339
|
+
* call, as for `listWorkspacePackagesSync()`.
|
|
340
|
+
*/
|
|
341
|
+
function discoverWorkspacePackages(repoRoot, options = {}) {
|
|
342
|
+
const base = resolve(repoRoot);
|
|
343
|
+
const readFile = options.readFile ?? ((path) => readFileSync(path, "utf8"));
|
|
344
|
+
const { include, exclude } = resolveWorkspaceGlobs(base, options);
|
|
345
|
+
const entries = /* @__PURE__ */ new Map();
|
|
346
|
+
for (const glob of include) {
|
|
347
|
+
if (glob === "." || glob === "") continue;
|
|
348
|
+
for (const entry of expandGlob(base, glob, {
|
|
349
|
+
readFile,
|
|
350
|
+
listDirs: options.listDirs
|
|
351
|
+
})) if (!exclude.some((pattern) => matches(entry.relativeDir, pattern))) entries.set(entry.relativeDir, entry);
|
|
352
|
+
}
|
|
353
|
+
const { group, excludeGroups = [] } = options;
|
|
354
|
+
return [...entries.values()].filter(({ relativeDir }) => (group === void 0 || inGroup(relativeDir, group)) && !excludeGroups.some((excluded) => inGroup(relativeDir, excluded)));
|
|
355
|
+
}
|
|
356
|
+
/** `relativeDir` is the group's folder, or is under it. `clients` holds `clients/http`, not `clients-old/x`. */
|
|
357
|
+
function inGroup(relativeDir, group) {
|
|
358
|
+
const folder = group.replace(/^\.\//, "").replace(/\/+$/, "");
|
|
359
|
+
return relativeDir === folder || relativeDir.startsWith(`${folder}/`);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The globs {@link discoverWorkspacePackages} scans: the repo's
|
|
363
|
+
* `pnpm-workspace.yaml` `packages:` list, or the {@link DEFAULT_WORKSPACE_ROOTS}
|
|
364
|
+
* as `<root>/*` when there is no such file or it lists none.
|
|
365
|
+
*/
|
|
366
|
+
function resolveWorkspaceGlobs(repoRoot, options = {}) {
|
|
367
|
+
const readText = options.readText ?? ((path) => readFileSync(path, "utf8"));
|
|
368
|
+
let workspaceYaml = "";
|
|
369
|
+
try {
|
|
370
|
+
workspaceYaml = readText(join(resolve(repoRoot), "pnpm-workspace.yaml"));
|
|
371
|
+
} catch {}
|
|
372
|
+
const { include, exclude } = readWorkspaceGlobs(workspaceYaml);
|
|
373
|
+
return {
|
|
374
|
+
include: include.length > 0 ? include : DEFAULT_WORKSPACE_ROOTS.map((root) => `${root}/*`),
|
|
375
|
+
exclude
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function expandGlob(base, glob, options) {
|
|
379
|
+
if (glob.endsWith("/*") && !GLOB_CHARS.test(glob.slice(0, -2))) return listWorkspacePackagesSync(base, {
|
|
380
|
+
...options,
|
|
381
|
+
roots: [glob.slice(0, -2)]
|
|
382
|
+
});
|
|
383
|
+
if (GLOB_CHARS.test(glob)) throw new TypeError(`unsupported workspace pattern ${JSON.stringify(glob)}: only "<dir>/*" and literal directories are supported`);
|
|
384
|
+
const rel = relative(base, resolve(base, glob));
|
|
385
|
+
if (isAbsolute(glob) || rel === ".." || rel.startsWith(`..${sep}`)) throw new TypeError(`workspace directory must be inside the repo, got ${JSON.stringify(glob)}`);
|
|
386
|
+
const dir = join(base, glob);
|
|
387
|
+
try {
|
|
388
|
+
const manifest = parsePackageJson(options.readFile(join(dir, "package.json")));
|
|
389
|
+
return manifest ? [{
|
|
390
|
+
name: manifest.name,
|
|
391
|
+
dir,
|
|
392
|
+
relativeDir: glob,
|
|
393
|
+
manifest
|
|
394
|
+
}] : [];
|
|
395
|
+
} catch {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** `relativeDir` against an exclusion: the same directory, or a child of `<dir>/*`. */
|
|
400
|
+
function matches(relativeDir, pattern) {
|
|
401
|
+
if (pattern.endsWith("/*")) {
|
|
402
|
+
const parent = pattern.slice(0, -2);
|
|
403
|
+
return relativeDir.startsWith(`${parent}/`) && !relativeDir.slice(parent.length + 1).includes("/");
|
|
404
|
+
}
|
|
405
|
+
return relativeDir === pattern;
|
|
406
|
+
}
|
|
161
407
|
// src/read-package-json.ts
|
|
162
408
|
/**
|
|
163
409
|
* The parsed `<dir>/package.json`, or `undefined` when it is missing,
|
|
@@ -179,4 +425,4 @@ function readPackageJsonSync(dir, options = {}) {
|
|
|
179
425
|
return;
|
|
180
426
|
}
|
|
181
427
|
}
|
|
182
|
-
export { dependencyNames, detectPackageManager, listWorkspacePackagesSync, parsePackageJson, readPackageJson, readPackageJsonSync };
|
|
428
|
+
export { DEFAULT_WORKSPACE_ROOTS, PACKAGES_KEY, addWorkspaceMember, bumpCatalogPins, dependencyNames, detectPackageManager, discoverWorkspacePackages, gt, listWorkspacePackagesSync, parseCatalogs, 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.
|
|
3
|
+
"version": "1.16.1-alpha.3",
|
|
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/tsdown-config": "^8.9.0-alpha.3",
|
|
45
|
+
"@theholocron/vitest-config": "^8.9.0-alpha.3",
|
|
46
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.3"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=22"
|