pob 38.13.0 → 39.0.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.
@@ -2,4 +2,20 @@
2
2
  [install]
3
3
  # Only install package versions published at least 3 days ago
4
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"]
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
+ ]
@@ -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 {
@@ -165,19 +166,7 @@ export default class MonorepoWorkspacesGenerator extends Generator {
165
166
 
166
167
  if (this.fs.exists(readmePath)) {
167
168
  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;
169
+ content = extractMonorepoReadmeContent(readmeFullContent);
181
170
  }
182
171
 
183
172
  return copyAndFormatTpl(
@@ -185,7 +174,7 @@ export default class MonorepoWorkspacesGenerator extends Generator {
185
174
  this.templatePath("README.md.ejs"),
186
175
  readmePath,
187
176
  {
188
- title: pkg.description,
177
+ title: pkg.description || pkg.name.replace(/-monorepo$/, ""),
189
178
  description: "",
190
179
  packages: this.packages,
191
180
  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.0.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.14.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"