@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 +12 -0
- package/dist/index.d.mts +48 -0
- package/dist/index.mjs +122 -2
- package/package.json +3 -3
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) =>
|
|
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.
|
|
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/
|
|
44
|
+
"@theholocron/tsdown-config": "^8.9.0-alpha.3",
|
|
45
45
|
"@theholocron/vitest-config": "^8.9.0-alpha.3",
|
|
46
|
-
"@theholocron/
|
|
46
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.3"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
49
|
"node": ">=22"
|