pob 38.12.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.
@@ -267,6 +267,7 @@ export default class PobAppGenerator extends Generator {
267
267
  updateOnly: this.options.updateOnly,
268
268
  resolveJsonModule: true,
269
269
  onlyLatestLTS: true,
270
+ packageManager: this.options.packageManager,
270
271
  enableHashSlash,
271
272
  plugins: (() => {
272
273
  if (this.appConfig.type === "next.js") {
@@ -15,15 +15,25 @@ on:
15
15
  default: false
16
16
  <% if (isMonorepo && isMonorepoIndependent) { -%>
17
17
  bump-dependents-highest-as:
18
- description: "Bump dependents highest as"
19
- required: false
18
+ description: "Bump dependents highest version as"
19
+ required: true
20
20
  type: choice
21
21
  options:
22
22
  - major
23
23
  - minor
24
24
  - patch
25
- default: "major"
25
+ default: major
26
26
  <% } -%>
27
+ force:
28
+ description: "Force bump all packages, even without changes"
29
+ required: true
30
+ type: choice
31
+ options:
32
+ - none
33
+ - major
34
+ - minor
35
+ - patch
36
+ default: none
27
37
 
28
38
  permissions:
29
39
  id-token: write # Required for OIDC
@@ -92,7 +102,7 @@ jobs:
92
102
 
93
103
  - name: New version (dry run)
94
104
  if: github.ref == 'refs/heads/main' && inputs.dry-run
95
- run: <%= packageManagerExec(packageManager, 'pob-version') %> --dry-run<% if (isMonorepo && isMonorepoIndependent) { %> --bump-dependents-highest-as=${{ inputs.bump-dependents-highest-as }}<% } %>
105
+ run: <%= packageManagerExec(packageManager, 'pob-version') %> --dry-run<% if (isMonorepo && isMonorepoIndependent) { %> --bump-dependents-highest-as=${{ inputs.bump-dependents-highest-as }}<% } %> ${{ inputs.force != 'none' && format('--force={0}', inputs.force) || '' }}
96
106
 
97
107
  - name: Configure Git user
98
108
  if: github.ref == 'refs/heads/main' && !inputs.dry-run
@@ -103,7 +113,7 @@ jobs:
103
113
  - name: New version
104
114
  if: github.ref == 'refs/heads/main' && !inputs.dry-run
105
115
  run: |
106
- <%= packageManagerExec(packageManager, 'pob-version') %> <% if (enablePublish && packageManager === "bun") { %>--publish <% } %>--create-release=github <% if (isMonorepo && isMonorepoIndependent) { %> --bump-dependents-highest-as=${{ inputs.bump-dependents-highest-as }}<% } %> -m 'chore: release <%- isMonorepoIndependent ? '' : '%v ' %>[skip ci]<%- isMonorepoIndependent ? '\\n\\n%t' : '' %>'
116
+ <%= packageManagerExec(packageManager, 'pob-version') %> <% if (enablePublish && packageManager === "bun") { %>--publish --publish-provenance <% } %>--create-release=github<% if (isMonorepo && isMonorepoIndependent) { %> --bump-dependents-highest-as=${{ inputs.bump-dependents-highest-as }}<% } %> ${{ inputs.force != 'none' && format('--force={0}', inputs.force) || '' }} -m 'chore: release <%- isMonorepoIndependent ? '' : '%v ' %>[skip ci]<%- isMonorepoIndependent ? '\\n\\n%t' : '' %>'
107
117
  env:
108
118
  POB_GIT_HOOKS: 0
109
119
  YARN_ENABLE_IMMUTABLE_INSTALLS: false
@@ -115,15 +125,15 @@ jobs:
115
125
  run: |
116
126
  <% if (packageManager === 'yarn') { -%>
117
127
  <% if (isMonorepo) { -%>
118
- yarn workspaces foreach --all --parallel --no-private npm publish --tolerate-republish
128
+ yarn workspaces foreach --all --parallel --no-private npm publish --tolerate-republish --provenance
119
129
  <% } else { -%>
120
- yarn npm publish
130
+ yarn npm publish --provenance
121
131
  <% } -%>
122
132
  <% } else if (packageManager === 'pnpm') { -%>
123
133
  <% if (isMonorepo) { -%>
124
- pnpm -r publish --no-git-checks
134
+ pnpm -r publish --no-git-checks --provenance
125
135
  <% } else { -%>
126
- pnpm publish --no-git-checks
136
+ pnpm publish --no-git-checks --provenance
127
137
  <% } -%>
128
138
  <% } -%>
129
139
  <% } -%>
@@ -102,6 +102,12 @@ export default class CommonTypescriptGenerator extends Generator {
102
102
  description: "typescript additional includes",
103
103
  });
104
104
 
105
+ this.option("packageManager", {
106
+ type: String,
107
+ default: "yarn",
108
+ description: "yarn, npm, bun, or pnpm",
109
+ });
110
+
105
111
  this.option("onlyLatestLTS", {
106
112
  type: Boolean,
107
113
  required: false,
@@ -146,6 +152,7 @@ export default class CommonTypescriptGenerator extends Generator {
146
152
  },
147
153
  ];
148
154
  const withNode = envs.some((env) => env.target === "node");
155
+ const withBun = this.options.packageManager === "bun";
149
156
 
150
157
  const presets = (() => {
151
158
  const babelEnvs =
@@ -172,7 +179,9 @@ export default class CommonTypescriptGenerator extends Generator {
172
179
  pkg.pob.bundler === false ||
173
180
  pkg.pob.bundler === "tsc"
174
181
  ) {
175
- return [`@pob/root/tsconfigs/targets/node-${nodeVersion}.json`];
182
+ return withBun
183
+ ? ["@pob/root/tsconfigs/targets/bun.json"]
184
+ : [`@pob/root/tsconfigs/targets/node-${nodeVersion}.json`];
176
185
  }
177
186
  if (envs && envs.every((env) => env.target === "node")) {
178
187
  return [
@@ -199,6 +208,9 @@ export default class CommonTypescriptGenerator extends Generator {
199
208
  }
200
209
  return ["@pob/root/tsconfigs/targets/webpack.json"];
201
210
  }
211
+ if (withBun) {
212
+ return ["@pob/root/tsconfigs/targets/bun.json"];
213
+ }
202
214
  return [];
203
215
  })();
204
216
 
@@ -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
+ ]
@@ -323,6 +323,7 @@ export default class PobLibGenerator extends Generator {
323
323
  updateOnly: this.options.updateOnly,
324
324
  builddefs: true,
325
325
  onlyLatestLTS: this.onlyLatestLTS,
326
+ packageManager,
326
327
  srcDirectory: withTypescript ? "src" : "lib",
327
328
  });
328
329
 
@@ -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.12.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.12.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"