@theholocron/pkg-utils 1.16.1-alpha.2 → 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 CHANGED
@@ -76,6 +76,18 @@ const names = discoverWorkspacePackages(repoRoot, { group: "clients" }).map((p)
76
76
 
77
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
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
+
79
91
  ### `PackageJson`
80
92
 
81
93
  The fields these helpers read, typed; any other field passes through untyped.
package/dist/index.d.mts CHANGED
@@ -135,6 +135,54 @@ export declare function resolveWorkspaceGlobs(repoRoot: string, options?: Pick<D
135
135
  * string-valued entries. Every other field passes through untouched.
136
136
  */
137
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
+ }>;
138
186
  export interface ReadPackageJsonOptions {
139
187
  /** Replaces the file read — for callers with an injected or in-memory filesystem. */
140
188
  readFile?: (path: string) => string;
package/dist/index.mjs CHANGED
@@ -158,6 +158,126 @@ 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
+ }
161
281
  // src/workspace-globs.ts
162
282
  /**
163
283
  * The `packages:` list of a `pnpm-workspace.yaml`. A line-based read of the
@@ -170,7 +290,7 @@ function listWorkspacePackagesSync(repoRoot, options = {}) {
170
290
  */
171
291
  function readWorkspaceGlobs(workspaceYaml) {
172
292
  const lines = workspaceYaml.split("\n");
173
- const start = lines.findIndex((line) => /^packages:\s*(#.*)?$/.test(line));
293
+ const start = lines.findIndex((line) => PACKAGES_KEY.test(line));
174
294
  const globs = {
175
295
  include: [],
176
296
  exclude: []
@@ -305,4 +425,4 @@ function readPackageJsonSync(dir, options = {}) {
305
425
  return;
306
426
  }
307
427
  }
308
- export { DEFAULT_WORKSPACE_ROOTS, dependencyNames, detectPackageManager, discoverWorkspacePackages, listWorkspacePackagesSync, parsePackageJson, readPackageJson, readPackageJsonSync, readWorkspaceGlobs, resolveWorkspaceGlobs };
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.2",
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",
@@ -41,9 +41,9 @@
41
41
  "tsdown": "^0.23.3",
42
42
  "typescript": "^5.9.3",
43
43
  "vitest": "^5.0.5",
44
- "@theholocron/eslint-config": "^8.9.0-alpha.3",
44
+ "@theholocron/tsdown-config": "^8.9.0-alpha.3",
45
45
  "@theholocron/vitest-config": "^8.9.0-alpha.3",
46
- "@theholocron/tsdown-config": "^8.9.0-alpha.3"
46
+ "@theholocron/eslint-config": "^8.9.0-alpha.3"
47
47
  },
48
48
  "engines": {
49
49
  "node": ">=22"