pob 38.13.0 → 39.1.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.
@@ -62,8 +62,6 @@ jobs:
62
62
 
63
63
  - name: Install pnpm
64
64
  uses: pnpm/action-setup@v6
65
- with:
66
- version: 12
67
65
  <% } -%>
68
66
 
69
67
  - uses: actions/setup-node@v7
@@ -1,5 +1,21 @@
1
1
  # Generated by pob: do not edit, it is overwritten on each pob run.
2
2
  [install]
3
- # Only install package versions published at least 3 days ago
4
- minimumReleaseAge = 259200 # seconds - in #23162 it'll allow "3d" too
5
- minimumReleaseAgeExcludes = ["@pob/root", "@pob/esbuild", "@pob/rollup", "@pob/rollup-esbuild", "@pob/rollup-typescript", "@pob/sort-object", "@pob/sort-pkg", "@pob/version", "pob-dependencies", "pob-dependencies", "alouette", "alouette-icons", "nightingale", "nightingale-logger", "nightingale-levels", "nightingale-types"]
3
+ # Only install package versions published at least 2.5 days ago
4
+ minimumReleaseAge = 216000 # seconds
5
+ minimumReleaseAgeExcludes = [
6
+ "@pob/root",
7
+ "@pob/esbuild",
8
+ "@pob/rollup",
9
+ "@pob/rollup-esbuild",
10
+ "@pob/rollup-typescript",
11
+ "@pob/sort-object",
12
+ "@pob/sort-pkg",
13
+ "@pob/version",
14
+ "pob-dependencies",
15
+ "alouette",
16
+ "alouette-icons",
17
+ "nightingale",
18
+ "nightingale-logger",
19
+ "nightingale-levels",
20
+ "nightingale-types",
21
+ ]
@@ -24,8 +24,6 @@ jobs:
24
24
  <% } else if (packageManager === 'pnpm') { -%>
25
25
  - name: Install pnpm
26
26
  uses: pnpm/action-setup@v6
27
- with:
28
- version: 12
29
27
 
30
28
  <% } -%>
31
29
  - uses: actions/setup-node@v7
@@ -25,8 +25,6 @@ jobs:
25
25
  <% } else if (packageManager === 'pnpm') { -%>
26
26
  - name: Install pnpm
27
27
  uses: pnpm/action-setup@v6
28
- with:
29
- version: 12
30
28
 
31
29
  <% } -%>
32
30
  - uses: actions/setup-node@v7
@@ -70,8 +68,6 @@ jobs:
70
68
  <% } else if (packageManager === 'pnpm') { -%>
71
69
  - name: Install pnpm
72
70
  uses: pnpm/action-setup@v6
73
- with:
74
- version: 12
75
71
 
76
72
  <% } -%>
77
73
  - uses: actions/setup-node@v7
@@ -117,8 +113,6 @@ jobs:
117
113
  <% } else if (packageManager === 'pnpm') { -%>
118
114
  - name: Install pnpm
119
115
  uses: pnpm/action-setup@v6
120
- with:
121
- version: 12
122
116
 
123
117
  <% } -%>
124
118
  - uses: actions/setup-node@v7
@@ -183,8 +177,6 @@ jobs:
183
177
  <% } else if (packageManager === 'pnpm') { -%>
184
178
  - name: Install pnpm
185
179
  uses: pnpm/action-setup@v6
186
- with:
187
- version: 12
188
180
 
189
181
  <% } -%>
190
182
  - uses: actions/setup-node@v7
@@ -259,8 +251,6 @@ jobs:
259
251
  <% } else if (packageManager === 'pnpm') { -%>
260
252
  - name: Install pnpm
261
253
  uses: pnpm/action-setup@v6
262
- with:
263
- version: 12
264
254
 
265
255
  <% } -%>
266
256
  - uses: actions/setup-node@v7
@@ -28,8 +28,6 @@ jobs:
28
28
  <% } else if (packageManager === 'pnpm') { -%>
29
29
  - name: Install pnpm
30
30
  uses: pnpm/action-setup@v6
31
- with:
32
- version: 12
33
31
 
34
32
  <% } -%>
35
33
  - name: Use Node.js ${{ matrix.node-version }}
@@ -1,11 +1,11 @@
1
1
  import sortObject from "@pob/sort-object";
2
2
  import { FAILSAFE_SCHEMA, dump, loadAll } from "js-yaml";
3
- import { lt } from "semver";
4
3
  import Generator from "yeoman-generator";
5
4
  import { writeAndFormat } from "../../../utils/writeAndFormat.js";
6
5
 
7
6
  const minimumReleaseAgeExcludePackages = [
8
7
  "@pob/*",
8
+ "pob",
9
9
  "pob-dependencies",
10
10
  "check-package-dependencies",
11
11
  "alouette",
@@ -38,13 +38,18 @@ export default class CorePnpmGenerator extends Generator {
38
38
  const pkg = this.fs.readJSON(this.destinationPath("package.json"));
39
39
 
40
40
  if (this.options.enable) {
41
- if (
42
- pkg.packageManager &&
43
- (!pkg.packageManager.startsWith("pnpm@") ||
44
- lt(pkg.packageManager.slice("pnpm@".length), "12.0.0"))
45
- ) {
46
- delete pkg.packageManager;
47
- }
41
+ // devEngines.packageManager replaces packageManager: pnpm switches to
42
+ // this version on startup (including when pnpm install is run by pob
43
+ // with an older pnpm), and pnpm/action-setup reads it.
44
+ delete pkg.packageManager;
45
+ pkg.devEngines = {
46
+ ...pkg.devEngines,
47
+ packageManager: {
48
+ name: "pnpm",
49
+ version: "^12.0.0",
50
+ onFail: "download",
51
+ },
52
+ };
48
53
 
49
54
  const configString = this.fs.read(
50
55
  this.destinationPath("pnpm-workspace.yaml"),
@@ -81,7 +86,7 @@ export default class CorePnpmGenerator extends Generator {
81
86
  delete config.packages;
82
87
  }
83
88
  config.savePrefix = this.options.type === "app" ? "" : "^";
84
- config.minimumReleaseAge = 1440 * 3; // 3 days in minutes
89
+ config.minimumReleaseAge = 1440 * 2.5; // 2.5 days in minutes
85
90
  config.minimumReleaseAgeExclude = minimumReleaseAgeExcludePackages;
86
91
  config.dedupePeerDependents = true;
87
92
  config.nodeLinker = "hoisted";
@@ -95,6 +100,10 @@ export default class CorePnpmGenerator extends Generator {
95
100
  if (pkg.packageManager?.startsWith("pnpm@")) {
96
101
  delete pkg.packageManager;
97
102
  }
103
+ if (pkg.devEngines?.packageManager?.name === "pnpm") {
104
+ delete pkg.devEngines.packageManager;
105
+ if (Object.keys(pkg.devEngines).length === 0) delete pkg.devEngines;
106
+ }
98
107
  this.fs.delete("pnpm-lock.yaml");
99
108
  this.fs.delete("pnpm-workspace.yaml");
100
109
  }
@@ -183,7 +183,7 @@ export default class CoreYarnGenerator extends Generator {
183
183
  config.defaultSemverRangePrefix = this.options.type === "app" ? "" : "^";
184
184
  delete config.enableMessageNames; // was a config for yarn < 4
185
185
  config.nodeLinker = this.options.yarnNodeLinker;
186
- config.npmMinimalAgeGate = 1440 * 3; // 3 days
186
+ config.npmMinimalAgeGate = 1440 * 2.5; // 2.5 days
187
187
  config.npmPreapprovedPackages = [
188
188
  "@pob/*",
189
189
  "pob-dependencies",
@@ -1,9 +1,8 @@
1
- /* oxlint-disable regexp/match-any */
2
- /* oxlint-disable regexp/no-super-linear-backtracking */
3
1
  import camelCase from "lodash.camelcase";
4
2
  import Generator from "yeoman-generator";
5
3
  import inMonorepo from "../../../utils/inMonorepo.js";
6
4
  import * as packageUtils from "../../../utils/package.js";
5
+ import { extractLibReadmeContent } from "../../../utils/readmeContent.js";
7
6
  import { copyAndFormatTpl } from "../../../utils/writeAndFormat.js";
8
7
 
9
8
  export default class LibReadmeGenerator extends Generator {
@@ -49,39 +48,7 @@ export default class LibReadmeGenerator extends Generator {
49
48
 
50
49
  if (this.fs.exists(readmePath)) {
51
50
  const readmeFullContent = this.fs.read(readmePath);
52
- content = readmeFullContent.match(
53
- /^<h1 align="center"[^#*]+([^]+)\[npm-image\]:/,
54
- );
55
- if (!content) {
56
- content = readmeFullContent.match(
57
- /^<h1 align="center"[^#*]+([^]+)\[daviddm-image\]:/,
58
- );
59
- }
60
- if (!content) {
61
- content = readmeFullContent.match(
62
- /^<h3 align="center"[^#*]+([^]+)\[npm-image\]:/,
63
- );
64
- }
65
- if (!content) {
66
- content = readmeFullContent.match(
67
- /^<h3 align="center"[^#*]+([^]+)\[daviddm-image\]:/,
68
- );
69
- }
70
- if (!content) {
71
- content = readmeFullContent.match(/^<h1 align="center"[^#*]+([^]+)$/);
72
- }
73
- if (!content) {
74
- content = readmeFullContent.match(/^<h3 align="center"[^#*]+([^]+)$/);
75
- }
76
- if (!content) content = readmeFullContent.match(/^<h3[^#*]+([^]+)$/);
77
- if (!content) {
78
- content = readmeFullContent.match(/^#[^#*]+([^]+)\[npm-image\]:/);
79
- }
80
- if (!content) {
81
- content = readmeFullContent.match(/^#[^#*]+([^]+)\[daviddm-image\]:/);
82
- }
83
- if (!content) content = readmeFullContent.match(/^#[^#*]+([^]+)$/);
84
- content = content ? content[1].trim() : readmeFullContent;
51
+ content = extractLibReadmeContent(readmeFullContent);
85
52
  }
86
53
 
87
54
  const author = packageUtils.parsePkgAuthor(pkg);
@@ -6,6 +6,7 @@ import {
6
6
  workspacesRunExcluding,
7
7
  workspacesRunTopological,
8
8
  } from "../../../utils/packageManagerWorkspacesUtils.js";
9
+ import { extractMonorepoReadmeContent } from "../../../utils/readmeContent.js";
9
10
  import { copyAndFormatTpl } from "../../../utils/writeAndFormat.js";
10
11
 
11
12
  export default class MonorepoWorkspacesGenerator extends Generator {
@@ -84,11 +85,9 @@ export default class MonorepoWorkspacesGenerator extends Generator {
84
85
  if (!pkg.engines) pkg.engines = {};
85
86
  pkg.engines.yarn = "< 0.0.0";
86
87
  pkg.engines.npm = ">= 6.4.0";
87
- } else if (this.options.packageManager === "pnpm") {
88
- if (!pkg.engines) pkg.engines = {};
89
- pkg.engines.pnpm = ">= 12.0.0";
90
- delete pkg.engines.yarn;
91
88
  } else if (pkg.engines) {
89
+ // the pnpm version is set in devEngines by pob:core:pnpm
90
+ delete pkg.engines.pnpm;
92
91
  delete pkg.engines.yarn;
93
92
  }
94
93
 
@@ -165,19 +164,7 @@ export default class MonorepoWorkspacesGenerator extends Generator {
165
164
 
166
165
  if (this.fs.exists(readmePath)) {
167
166
  const readmeFullContent = this.fs.read(readmePath);
168
- // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
169
- content = readmeFullContent.match(/^<h1 align="center"[^#*]+([^]+)$/);
170
- if (!content) {
171
- // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
172
- content = readmeFullContent.match(/^<h3 align="center"[^#*]+([^]+)$/);
173
- }
174
- if (!content) {
175
- // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
176
- content = readmeFullContent.match(/^<h3[^#*]+([^]+)$/);
177
- }
178
- // oxlint-disable-next-line regexp/no-super-linear-backtracking, regexp/match-any
179
- if (!content) content = readmeFullContent.match(/^#[^#*]+([^]+)$/);
180
- content = content ? content[1].trim() : readmeFullContent;
167
+ content = extractMonorepoReadmeContent(readmeFullContent);
181
168
  }
182
169
 
183
170
  return copyAndFormatTpl(
@@ -185,7 +172,7 @@ export default class MonorepoWorkspacesGenerator extends Generator {
185
172
  this.templatePath("README.md.ejs"),
186
173
  readmePath,
187
174
  {
188
- title: pkg.description,
175
+ title: pkg.description || pkg.name.replace(/-monorepo$/, ""),
189
176
  description: "",
190
177
  packages: this.packages,
191
178
  ci: this.fs.exists(this.destinationPath(".github/workflows/push.yml")),
@@ -0,0 +1,52 @@
1
+ // Generators regenerate the header of a README and keep the hand-written body
2
+ // that follows it. The body is whatever comes after the generated header, so
3
+ // it is found by matching the header itself — not by looking for the first
4
+ // heading, which loses any prose placed right after the header, and stops early
5
+ // on a `#` or `*` inside the header (a description such as "C# client").
6
+
7
+ /* oxlint-disable regexp/no-super-linear-backtracking, regexp/match-any */
8
+
9
+ // <h1>, optional <p> description, then the packages table
10
+ const monorepoHeaderRegex =
11
+ /^<h1 align="center">[^]*?\n<h3>📦 Packages<\/h3>\n[^|]*(?:\|[^\n]*\n)+([^]*)$/;
12
+
13
+ // <h1>, then the <p> description and badges blocks
14
+ const libHeaderRegex =
15
+ /^<h1 align="center">[^]*?<\/h1>\n(?:\s*<p align="center">[^]*?<\/p>\n)+([^]*)$/;
16
+
17
+ const matchFirst = (content, regexes) => {
18
+ for (const regex of regexes) {
19
+ const match = content.match(regex);
20
+ if (match) return match[1].trim();
21
+ }
22
+ return content;
23
+ };
24
+
25
+ export function extractMonorepoReadmeContent(readmeFullContent) {
26
+ return matchFirst(readmeFullContent, [
27
+ monorepoHeaderRegex,
28
+ // legacy headers
29
+ /^<h1 align="center"[^#*]+([^]+)$/,
30
+ /^<h3 align="center"[^#*]+([^]+)$/,
31
+ /^<h3[^#*]+([^]+)$/,
32
+ /^#[^#*]+([^]+)$/,
33
+ ]);
34
+ }
35
+
36
+ export function extractLibReadmeContent(readmeFullContent) {
37
+ return matchFirst(readmeFullContent, [
38
+ // legacy headers with badges as reference links at the end
39
+ /^<h1 align="center"[^#*]+([^]+)\[npm-image\]:/,
40
+ /^<h1 align="center"[^#*]+([^]+)\[daviddm-image\]:/,
41
+ /^<h3 align="center"[^#*]+([^]+)\[npm-image\]:/,
42
+ /^<h3 align="center"[^#*]+([^]+)\[daviddm-image\]:/,
43
+ libHeaderRegex,
44
+ // legacy headers
45
+ /^<h1 align="center"[^#*]+([^]+)$/,
46
+ /^<h3 align="center"[^#*]+([^]+)$/,
47
+ /^<h3[^#*]+([^]+)$/,
48
+ /^#[^#*]+([^]+)\[npm-image\]:/,
49
+ /^#[^#*]+([^]+)\[daviddm-image\]:/,
50
+ /^#[^#*]+([^]+)$/,
51
+ ]);
52
+ }
@@ -0,0 +1,167 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { describe, expect, it } from "vitest";
3
+ import { create as createMemFs } from "mem-fs";
4
+ import { create as createEditor } from "mem-fs-editor";
5
+ import {
6
+ extractLibReadmeContent,
7
+ extractMonorepoReadmeContent,
8
+ } from "./readmeContent.js";
9
+ import { copyAndFormatTpl } from "./writeAndFormat.js";
10
+
11
+ const templatePath = (generator) =>
12
+ fileURLToPath(
13
+ new URL(
14
+ `../generators/${generator}/templates/README.md.ejs`,
15
+ import.meta.url,
16
+ ),
17
+ );
18
+
19
+ // renders the README exactly as the generator writes it: template, then oxfmt
20
+ async function render(generator, options) {
21
+ const fs = createEditor(createMemFs());
22
+ const destinationPath = "/virtual/README.md";
23
+ await copyAndFormatTpl(fs, templatePath(generator), destinationPath, options);
24
+ return fs.read(destinationPath);
25
+ }
26
+
27
+ const packages = [
28
+ { name: "pkg-a", description: "First package" },
29
+ { name: "pkg-b", description: "Second package" },
30
+ ];
31
+
32
+ const renderMonorepo = (content, { title = "a monorepo" } = {}) =>
33
+ render("monorepo/workspaces", {
34
+ title,
35
+ description: "",
36
+ packages,
37
+ ci: false,
38
+ content,
39
+ });
40
+
41
+ const renderLib = (content, options = {}) =>
42
+ render("lib/readme", {
43
+ title: "pkg-a",
44
+ privatePackage: false,
45
+ packageName: "pkg-a",
46
+ camelCaseProjectName: "pkgA",
47
+ description: "First package",
48
+ gitHost: "github",
49
+ gitAccount: "someone",
50
+ gitName: "pkg-a",
51
+ codecov: false,
52
+ documentation: false,
53
+ documentationUrl: undefined,
54
+ testing: false,
55
+ ci: false,
56
+ ...options,
57
+ content,
58
+ });
59
+
60
+ const proseBody = `Every tool here answers the same two questions:
61
+
62
+ - **why is this duplicated?** — which dependents force several versions.
63
+ - **dedupe it** — compute the fixes and apply them.
64
+
65
+ ## Usage
66
+
67
+ Run it.`;
68
+
69
+ const headingBody = `## About
70
+
71
+ Some text, with **bold**.`;
72
+
73
+ describe("extractMonorepoReadmeContent", () => {
74
+ it.each([
75
+ ["a body starting with a heading", headingBody],
76
+ ["a body starting with prose", proseBody],
77
+ ])("keeps %s through regeneration", async (_, body) => {
78
+ const readme = await renderMonorepo(body);
79
+ expect(extractMonorepoReadmeContent(readme)).toBe(body);
80
+ // a second run is stable
81
+ expect(await renderMonorepo(extractMonorepoReadmeContent(readme))).toBe(
82
+ readme,
83
+ );
84
+ });
85
+
86
+ it("keeps the body when the title contains # or *", async () => {
87
+ const readme = await renderMonorepo(proseBody, {
88
+ title: "C# tools, **bold**",
89
+ });
90
+ expect(extractMonorepoReadmeContent(readme)).toBe(proseBody);
91
+ });
92
+
93
+ it("extracts nothing from a README without a body", async () => {
94
+ expect(extractMonorepoReadmeContent(await renderMonorepo(""))).toBe("");
95
+ });
96
+
97
+ // pm-tools README: hand-written prose right after the table lost everything
98
+ // up to the first `*`, i.e. the paragraph and the first bullet's `- `
99
+ it("strips a hand-edited header and keeps the prose after the table", () => {
100
+ const readme = `<h1 align="center">
101
+ pm-tools
102
+ </h1>
103
+
104
+ <p align="center">
105
+ Find out why a dependency is duplicated in a lockfile, and dedupe it.
106
+ </p>
107
+
108
+ <h3>📦 Packages</h3>
109
+
110
+ This repository is a monorepo managed with [bun workspaces](https://bun.com/docs/install/workspaces).
111
+
112
+ | Package | Version | Description |
113
+ | --------------------- | ------- | ---------------- |
114
+ | [pkg-a](packages/pkg-a) | | First package |
115
+
116
+ ${proseBody}
117
+ `;
118
+ expect(extractMonorepoReadmeContent(readme)).toBe(proseBody);
119
+ });
120
+
121
+ it("falls back to the first heading for a README not generated by pob", () => {
122
+ expect(extractMonorepoReadmeContent("# title\n\nintro\n\n## Usage\n")).toBe(
123
+ "## Usage",
124
+ );
125
+ });
126
+ });
127
+
128
+ describe("extractLibReadmeContent", () => {
129
+ it.each([
130
+ ["a body starting with a heading", headingBody, {}],
131
+ ["a body starting with prose", proseBody, {}],
132
+ [
133
+ "a description containing # or *",
134
+ headingBody,
135
+ { description: "C# **x**" },
136
+ ],
137
+ ["no description", proseBody, { description: undefined }],
138
+ [
139
+ "an empty badges block",
140
+ proseBody,
141
+ { privatePackage: true, gitHost: undefined },
142
+ ],
143
+ ])(
144
+ "keeps the body with %s through regeneration",
145
+ async (_, body, options) => {
146
+ const readme = await renderLib(body, options);
147
+ expect(extractLibReadmeContent(readme)).toBe(body);
148
+ expect(await renderLib(extractLibReadmeContent(readme), options)).toBe(
149
+ readme,
150
+ );
151
+ },
152
+ );
153
+
154
+ it("strips legacy badges reference links", () => {
155
+ const readme = `<h1 align="center">
156
+ pkg-a
157
+ </h1>
158
+
159
+ ## Usage
160
+
161
+ Run it.
162
+
163
+ [npm-image]: https://img.shields.io/npm/v/pkg-a.svg
164
+ `;
165
+ expect(extractLibReadmeContent(readme)).toBe("## Usage\n\nRun it.");
166
+ });
167
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pob",
3
- "version": "38.13.0",
3
+ "version": "39.1.0",
4
4
  "description": "Pile of bones, library generator with git/babel/typescript/typedoc/readme/vitest",
5
5
  "keywords": [
6
6
  "skeleton"
@@ -31,8 +31,8 @@
31
31
  "./package.json": "./package.json"
32
32
  },
33
33
  "dependencies": {
34
- "@pob/eslint-config": "67.11.0",
35
- "@pob/eslint-config-typescript-react": "67.11.0",
34
+ "@pob/eslint-config": "67.12.0",
35
+ "@pob/eslint-config-typescript-react": "67.12.0",
36
36
  "@pob/sort-object": "11.2.0",
37
37
  "@pob/sort-pkg": "13.2.0",
38
38
  "@types/inquirer": "9.0.10",
@@ -46,25 +46,25 @@
46
46
  "lodash.camelcase": "^4.3.0",
47
47
  "lodash.kebabcase": "^4.1.1",
48
48
  "mem-fs": "4.1.5",
49
- "mem-fs-editor": "12.0.6",
49
+ "mem-fs-editor": "12.1.0",
50
50
  "minimist": "1.2.8",
51
- "oxfmt": "0.54.0",
51
+ "oxfmt": "0.72.0",
52
52
  "parse-author": "2.0.0",
53
- "pob-dependencies": "27.11.0",
53
+ "pob-dependencies": "27.12.0",
54
54
  "semver": "7.8.5",
55
55
  "typescript": "npm:@typescript/typescript6@6.0.2",
56
56
  "validate-npm-package-name": "^8.0.0",
57
57
  "yeoman-environment": "6.1.0",
58
- "yeoman-generator": "8.2.2"
58
+ "yeoman-generator": "10.0.1"
59
59
  },
60
60
  "devDependencies": {
61
- "@pob/root": "27.13.0",
61
+ "@pob/root": "27.15.0",
62
62
  "@types/node": "24.19.1",
63
63
  "@typescript/native": "npm:typescript@7.0.2",
64
64
  "@yeoman/types": "1.11.1"
65
65
  },
66
66
  "engines": {
67
- "node": ">=22.18.0"
67
+ "node": ">=24.14.1"
68
68
  },
69
69
  "pob": {
70
70
  "typescript": "check-only"