pob 38.4.0 → 38.6.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.
Files changed (39) hide show
  1. package/lib/generators/app/PobAppGenerator.js +21 -3
  2. package/lib/generators/app/ignorePaths.js +4 -1
  3. package/lib/generators/common/e2e/CommonE2eGenerator.js +1 -1
  4. package/lib/generators/common/format-lint/CommonLintGenerator.js +43 -9
  5. package/lib/generators/common/format-lint/templates/oxfmtrc.jsonc.ejs +1 -1
  6. package/lib/generators/common/release/CommonReleaseGenerator.js +8 -13
  7. package/lib/generators/common/release/templates/workflow-release.yml.ejs +5 -0
  8. package/lib/generators/common/testing/CommonTestingGenerator.js +2 -2
  9. package/lib/generators/common/transpiler/CommonTranspilerGenerator.js +1 -81
  10. package/lib/generators/common/typescript/templates/tsconfig.tools.json.ejs +1 -0
  11. package/lib/generators/core/ci/CoreCIGenerator.js +1 -7
  12. package/lib/generators/core/ci/templates/github-action-documentation-workflow.yml.ejs +7 -2
  13. package/lib/generators/core/ci/templates/github-action-push-workflow-split.yml.ejs +18 -0
  14. package/lib/generators/core/ci/templates/github-action-push-workflow.yml.ejs +5 -2
  15. package/lib/generators/core/git/generators/github/CoreGitGithubGenerator.js +22 -2
  16. package/lib/generators/core/gitignore/CoreGitignoreGenerator.js +0 -8
  17. package/lib/generators/core/gitignore/templates/gitignore.ejs +6 -4
  18. package/lib/generators/core/node/CoreNodeGenerator.js +24 -0
  19. package/lib/generators/core/package/CorePackageGenerator.js +1 -17
  20. package/lib/generators/core/vscode/templates/extensions.json.ejs +1 -0
  21. package/lib/generators/core/vscode/templates/settings.json.ejs +1 -0
  22. package/lib/generators/generatedDocsOutputPath.test.js +39 -0
  23. package/lib/generators/lib/PobLibGenerator.js +31 -3
  24. package/lib/generators/lib/doc/LibDocGenerator.js +5 -2
  25. package/lib/generators/lib/doc/templates/tsconfig.doc.json.ejs +1 -1
  26. package/lib/generators/lib/doc/templates/tsconfig.doc.json.lerna.ejs +1 -1
  27. package/lib/generators/lib/readme/templates/README.md.ejs +1 -1
  28. package/lib/generators/monorepo/PobMonorepoGenerator.js +20 -6
  29. package/lib/generators/monorepo/typescript/templates/tsconfig.json.ejs +4 -4
  30. package/lib/generators/monorepo/typescript/templates/tsconfig.tools.json.ejs +5 -1
  31. package/lib/generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js +2 -1
  32. package/lib/pob.js +6 -0
  33. package/lib/utils/git.js +27 -0
  34. package/lib/utils/legacyGeneratedDocs.js +26 -0
  35. package/lib/utils/legacyGeneratedDocs.test.js +58 -0
  36. package/lib/utils/nodeEngines.js +124 -0
  37. package/lib/utils/nodeEngines.test.js +168 -0
  38. package/lib/utils/packageManagerUtils.js +17 -0
  39. package/package.json +11 -10
@@ -33,6 +33,7 @@
33
33
  "editor.formatOnSave": true,
34
34
  "eslint.codeActionsOnSave.mode": "all",
35
35
  "editor.codeActionsOnSave": {
36
+ "source.fixAll.oxc": "always",
36
37
  "source.fixAll.eslint": "always",
37
38
  "source.organizeImports": "never"
38
39
  },
@@ -0,0 +1,39 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const generatorsDir = import.meta.dirname;
6
+
7
+ /**
8
+ * The "docs" directory is reserved for hand written documentation in generated
9
+ * projects. Generated output (typedoc, coverage lcov, gh-pages publish dir)
10
+ * lives in "generated-docs". This matches a bare "docs" path segment, while
11
+ * allowing "generated-docs", "generate:docs" and urls like
12
+ * "docs.renovatebot.com".
13
+ */
14
+ const legacyDocsPath = /(?<![\w.:-])docs(?![\w.-])/;
15
+
16
+ describe("generated documentation output path", () => {
17
+ it("never targets the reserved directory", async () => {
18
+ const entries = await fs.readdir(generatorsDir, {
19
+ recursive: true,
20
+ withFileTypes: true,
21
+ });
22
+
23
+ const offenders = [];
24
+ for (const entry of entries) {
25
+ if (!entry.isFile() || entry.name.endsWith(".test.js")) continue;
26
+ const filePath = path.join(entry.parentPath, entry.name);
27
+ const content = await fs.readFile(filePath, "utf8");
28
+ content.split("\n").forEach((line, index) => {
29
+ if (legacyDocsPath.test(line)) {
30
+ offenders.push(
31
+ `${path.relative(generatorsDir, filePath)}:${index + 1}: ${line.trim()}`,
32
+ );
33
+ }
34
+ });
35
+ }
36
+
37
+ expect(offenders).toEqual([]);
38
+ });
39
+ });
@@ -1,6 +1,8 @@
1
1
  import { rmSync } from "node:fs";
2
2
  import Generator from "yeoman-generator";
3
+ import { removeFromGitIndex } from "../../utils/git.js";
3
4
  import inMonorepo from "../../utils/inMonorepo.js";
5
+ import removeLegacyGeneratedDocs from "../../utils/legacyGeneratedDocs.js";
4
6
  import { latestLTS } from "../../utils/nodeVersions.js";
5
7
  import * as packageUtils from "../../utils/package.js";
6
8
 
@@ -254,6 +256,17 @@ export default class PobLibGenerator extends Generator {
254
256
  this.pobjson.e2e = e2e;
255
257
  }
256
258
 
259
+ // build in git
260
+ if (!this.updateOnly) {
261
+ const { buildInGit } = await this.prompt({
262
+ type: "confirm",
263
+ name: "buildInGit",
264
+ message: "Would you like the build output committed in git ?",
265
+ default: this.pobjson.buildInGit === true,
266
+ });
267
+ this.pobjson.buildInGit = buildInGit;
268
+ }
269
+
257
270
  this.fs.writeJSON(this.destinationPath("package.json"), pkg);
258
271
 
259
272
  this.composeWith("pob:common:babel", {
@@ -378,8 +391,7 @@ export default class PobLibGenerator extends Generator {
378
391
  enable: !inMonorepo && this.pobjson.testing,
379
392
  packageManager,
380
393
  enablePublish: true,
381
- withBabel,
382
- withTypescript,
394
+ build: withBabel || withTypescript,
383
395
  isMonorepo: false,
384
396
  ci: this.options.ci,
385
397
  disableYarnGitCache: this.options.disableYarnGitCache,
@@ -397,6 +409,9 @@ export default class PobLibGenerator extends Generator {
397
409
  });
398
410
 
399
411
  const buildDirectory = withBabel || withTypescript ? "dist" : "";
412
+ const buildInGit = this.pobjson.buildInGit === true;
413
+ this.buildDirectory = buildDirectory;
414
+ this.buildInGit = buildInGit;
400
415
 
401
416
  // must be after doc, testing
402
417
  this.composeWith("pob:core:gitignore", {
@@ -405,6 +420,7 @@ export default class PobLibGenerator extends Generator {
405
420
  typescript: withTypescript,
406
421
  documentation: this.pobjson.documentation,
407
422
  testing: !!this.pobjson.testing,
423
+ paths: !buildInGit && buildDirectory ? `/${buildDirectory}` : "",
408
424
  buildDirectory,
409
425
  playwright: this.pobjson.e2e,
410
426
  });
@@ -464,7 +480,7 @@ export default class PobLibGenerator extends Generator {
464
480
  "lib-node14",
465
481
  "lib-node16",
466
482
  "coverage",
467
- this.pobjson.documentation && "docs",
483
+ this.pobjson.documentation && "generated-docs",
468
484
  !(withBabel || withTypescript) && "dist",
469
485
  ]
470
486
  .filter(Boolean)
@@ -472,11 +488,23 @@ export default class PobLibGenerator extends Generator {
472
488
  rmSync(path, { recursive: true, force: true });
473
489
  });
474
490
 
491
+ removeLegacyGeneratedDocs((path) => this.destinationPath(path));
492
+
475
493
  const { pobjson } = this;
476
494
 
477
495
  this.config.set("lib", pobjson);
478
496
  this.config.save();
479
497
 
498
+ this.composeWith("pob:core:node", {
499
+ onlyLatestLTS: this.onlyLatestLTS,
500
+ });
480
501
  this.composeWith("pob:core:sort-package");
481
502
  }
503
+
504
+ end() {
505
+ // the build output is no longer committed: remove it from git, keeping it on disk.
506
+ if (!this.buildInGit && this.buildDirectory) {
507
+ removeFromGitIndex(this.destinationPath(), this.buildDirectory);
508
+ }
509
+ }
482
510
  }
@@ -1,5 +1,6 @@
1
1
  import Generator from "yeoman-generator";
2
2
  import inMonorepo from "../../../utils/inMonorepo.js";
3
+ import removeLegacyGeneratedDocs from "../../../utils/legacyGeneratedDocs.js";
3
4
  import * as packageUtils from "../../../utils/package.js";
4
5
  import { copyAndFormatTpl } from "../../../utils/writeAndFormat.js";
5
6
 
@@ -35,6 +36,8 @@ export default class LibDocGenerator extends Generator {
35
36
  }
36
37
 
37
38
  async writing() {
39
+ removeLegacyGeneratedDocs((path) => this.destinationPath(path));
40
+
38
41
  if (this.fs.exists(this.destinationPath("jsdoc.conf.json"))) {
39
42
  this.fs.delete(this.destinationPath("jsdoc.conf.json"));
40
43
  }
@@ -112,8 +115,8 @@ export default class LibDocGenerator extends Generator {
112
115
  }
113
116
  } else {
114
117
  // this.fs.delete(this.destinationPath('jsdoc.conf.js'));
115
- if (this.fs.exists(this.destinationPath("docs"))) {
116
- this.fs.delete(this.destinationPath("docs"));
118
+ if (this.fs.exists(this.destinationPath("generated-docs"))) {
119
+ this.fs.delete(this.destinationPath("generated-docs"));
117
120
  }
118
121
 
119
122
  if (this.fs.exists(this.destinationPath("tsconfig.doc.json"))) {
@@ -10,7 +10,7 @@
10
10
  "src/**/__tests__/**"
11
11
  ],
12
12
  "readme": "<%= readme %>",
13
- "out": "docs",
13
+ "out": "generated-docs",
14
14
  "excludePrivate": true,
15
15
  "gitRevision": "main"
16
16
  },
@@ -50,7 +50,7 @@
50
50
  ],
51
51
  */ -%>
52
52
  "readme": "<%= readme %>",
53
- "out": "docs",
53
+ "out": "generated-docs",
54
54
  "excludePrivate": true,
55
55
  "gitRevision": "master"
56
56
  }
@@ -36,7 +36,7 @@ npm install --save <%= packageName %>
36
36
  <% if (documentation && ci && gitHost === 'github') { -%>
37
37
  ## API
38
38
 
39
- [https://<%= gitAccount %>.github.io/<%= packageName %>/docs](http://<%= gitAccount %>.github.io/<%= packageName %>/docs)
39
+ [https://<%= gitAccount %>.github.io/<%= packageName %>/](https://<%= gitAccount %>.github.io/<%= packageName %>/)
40
40
 
41
41
  <% } -%>
42
42
  ## Usage
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import { platform } from "node:process";
4
4
  import Generator from "yeoman-generator";
5
+ import removeLegacyGeneratedDocs from "../../utils/legacyGeneratedDocs.js";
5
6
  import * as packageUtils from "../../utils/package.js";
6
7
  import { packageManagerRun } from "../../utils/packageManagerUtils.js";
7
8
  import {
@@ -160,7 +161,7 @@ export default class PobMonorepoGenerator extends Generator {
160
161
  return;
161
162
  }
162
163
 
163
- if (this.pobMonorepoConfig.ciPushWorkflow) {
164
+ if (this.pobMonorepoConfig?.ciPushWorkflow) {
164
165
  throw new Error(
165
166
  "ciPushWorkflow is deprecated, use disablePushWorkflow instead",
166
167
  );
@@ -181,7 +182,7 @@ export default class PobMonorepoGenerator extends Generator {
181
182
  name: "disablePushWorkflow",
182
183
  message: "Would you like to DISABLE push workflow ?",
183
184
  when: (answers) => answers.ci,
184
- default: config.disablePushWorkflow,
185
+ default: config?.disablePushWorkflow,
185
186
  },
186
187
  {
187
188
  type: "confirm",
@@ -256,6 +257,14 @@ export default class PobMonorepoGenerator extends Generator {
256
257
 
257
258
  const splitCIJobs = this.packageNames.length > 8;
258
259
 
260
+ // the root has a build script when there are typescript definitions or rollup
261
+ // configs to build. The build output is not necessarily committed, so it must be
262
+ // built in ci and before publishing.
263
+ const hasRootBuild =
264
+ this.pobMonorepoConfig.typescript === true ||
265
+ (!this.options.isAppProject &&
266
+ this.packages.some((pkg) => pkg.pob?.bundler));
267
+
259
268
  this.composeWith("pob:common:testing", {
260
269
  monorepo: true,
261
270
  enable: this.pobMonorepoConfig.testing,
@@ -263,7 +272,7 @@ export default class PobMonorepoGenerator extends Generator {
263
272
  disableYarnGitCache: this.options.disableYarnGitCache,
264
273
  testing: this.pobMonorepoConfig.testing,
265
274
  e2eTesting: this.pobMonorepoConfig.e2eTesting,
266
- build: this.pobMonorepoConfig.typescript === true,
275
+ build: hasRootBuild,
267
276
  typescript: this.pobMonorepoConfig.typescript,
268
277
  documentation: !!this.pobMonorepoConfig.documentation,
269
278
  codecov: this.pobMonorepoConfig.testing && this.pobMonorepoConfig.codecov,
@@ -295,7 +304,7 @@ export default class PobMonorepoGenerator extends Generator {
295
304
  documentation: this.pobMonorepoConfig.documentation,
296
305
  storybook: pkg?.devDependencies?.storybook,
297
306
  typescript: this.pobMonorepoConfig.typescript,
298
- build: this.pobMonorepoConfig.typescript === true,
307
+ build: hasRootBuild,
299
308
  testing: this.pobMonorepoConfig.testing,
300
309
  testRunner: this.pobMonorepoConfig.testRunner,
301
310
  packageManager: this.options.packageManager,
@@ -354,7 +363,7 @@ export default class PobMonorepoGenerator extends Generator {
354
363
  enable: true,
355
364
  packageManager: this.options.packageManager,
356
365
  enablePublish: !this.options.isAppProject,
357
- withBabel: this.pobMonorepoConfig.typescript,
366
+ build: hasRootBuild,
358
367
  isMonorepo: true,
359
368
  ci: this.pobMonorepoConfig.ci,
360
369
  disableYarnGitCache: this.options.disableYarnGitCache,
@@ -376,9 +385,11 @@ export default class PobMonorepoGenerator extends Generator {
376
385
 
377
386
  if (platform !== "win32") {
378
387
  execSync(
379
- `rm -Rf ${["lib-*", "coverage", "docs"].filter(Boolean).join(" ")}`,
388
+ `rm -Rf ${["lib-*", "coverage", "generated-docs"].filter(Boolean).join(" ")}`,
380
389
  );
381
390
  }
391
+
392
+ removeLegacyGeneratedDocs((path) => this.destinationPath(path));
382
393
  }
383
394
 
384
395
  async writing() {
@@ -443,6 +454,9 @@ export default class PobMonorepoGenerator extends Generator {
443
454
  this.fs.writeJSON(this.destinationPath("package.json"), pkg);
444
455
  }
445
456
 
457
+ this.composeWith("pob:core:node", {
458
+ onlyLatestLTS: this.options.onlyLatestLTS,
459
+ });
446
460
  this.composeWith("pob:core:sort-package");
447
461
  }
448
462
 
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "files": [],
3
3
  "references": [
4
- <% if (hasTools) { -%>
5
- { "path": "tsconfig.tools.json" }<%= packagePaths.length > 0 ? ',' : '' %>
6
- <% } -%>
7
4
  <% packagePaths.forEach((packagePath, index) => { -%>
8
- { "path": "<%= packagePath %>/tsconfig<%= tsConfigSuffix ? `.${tsConfigSuffix}` : '' %>.json" }<%= index === packagePaths.length -1 ? '' : ',' %>
5
+ { "path": "<%= packagePath %>/tsconfig<%= tsConfigSuffix ? `.${tsConfigSuffix}` : '' %>.json" }<%= index === packagePaths.length -1 && !hasTools ? '' : ',' %>
9
6
  <% }) -%>
7
+ <% if (hasTools) { -%>
8
+ { "path": "tsconfig.tools.json" }
9
+ <% } -%>
10
10
  ]
11
11
  }
@@ -4,6 +4,7 @@
4
4
  "@pob/root/tsconfigs/targets/node-<%= nodeVersion %>.json"
5
5
  ],
6
6
  "include": [
7
+ "*.config.js",
7
8
  "*.config.ts",
8
9
  "*.config.mts",
9
10
  "*.config.cts",
@@ -13,7 +14,10 @@
13
14
  <% workspaceGlobs.forEach((workspaceGlob, index) => { -%>
14
15
  "<%= workspaceGlob %>/scripts/**/*.ts",
15
16
  "<%= workspaceGlob %>/scripts/**/*.mts",
16
- "<%= workspaceGlob %>/scripts/**/*.cts"<%= index === workspaceGlobs.length - 1 ? '' : ',' %>
17
+ "<%= workspaceGlob %>/scripts/**/*.cts",
18
+ "<%= workspaceGlob %>/e2e/**/*.ts",
19
+ "<%= workspaceGlob %>/e2e/**/*.mts",
20
+ "<%= workspaceGlob %>/e2e/**/*.cts"<%= index === workspaceGlobs.length - 1 ? '' : ',' %>
17
21
  <% }) -%>
18
22
  ],
19
23
  "compilerOptions": {
@@ -96,9 +96,10 @@ export default class MonorepoWorkspacesGenerator extends Generator {
96
96
  const packageManager = this.options.packageManager;
97
97
 
98
98
  packageUtils.addScripts(pkg, {
99
- lint: `${packageManager} run format && ${packageManager} run lint:eslint`,
99
+ lint: `${packageManager} run format && ${packageManager} run lint:oxlint && ${packageManager} run lint:eslint`,
100
100
  format: "oxfmt",
101
101
  "format:check": "oxfmt --check .",
102
+ "lint:oxlint": monorepoConfig && monorepoConfig.eslint && "oxlint",
102
103
  "lint:eslint":
103
104
  monorepoConfig &&
104
105
  monorepoConfig.eslint &&
package/lib/pob.js CHANGED
@@ -31,6 +31,7 @@ import CoreEditorConfigGenerator from "./generators/core/editorconfig/CoreEditor
31
31
  import CoreGitGenerator from "./generators/core/git/CoreGitGenerator.js";
32
32
  import CoreGitGithubGenerator from "./generators/core/git/generators/github/CoreGitGithubGenerator.js";
33
33
  import CoreGitignoreGenerator from "./generators/core/gitignore/CoreGitignoreGenerator.js";
34
+ import CoreNodeGenerator from "./generators/core/node/CoreNodeGenerator.js";
34
35
  import CoreNpmGenerator from "./generators/core/npm/CoreNpmGenerator.js";
35
36
  import CorePackageGenerator from "./generators/core/package/CorePackageGenerator.js";
36
37
  import CorePnpmGenerator from "./generators/core/pnpm/CorePnpmGenerator.js";
@@ -183,6 +184,11 @@ env.registerStub(
183
184
  "pob:core:gitignore",
184
185
  `${__dirname}/generators/core/gitignore/CoreGitignoreGenerator.js`,
185
186
  );
187
+ env.registerStub(
188
+ CoreNodeGenerator,
189
+ "pob:core:node",
190
+ `${__dirname}/generators/core/node/CoreNodeGenerator.js`,
191
+ );
186
192
  env.registerStub(
187
193
  CoreNpmGenerator,
188
194
  "pob:core:npm",
@@ -0,0 +1,27 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ export const isTrackedInGit = (cwd, path) => {
4
+ try {
5
+ return (
6
+ execFileSync("git", ["ls-files", "--", path], {
7
+ cwd,
8
+ encoding: "utf8",
9
+ }).trim() !== ""
10
+ );
11
+ } catch {
12
+ return false;
13
+ }
14
+ };
15
+
16
+ /**
17
+ * Removes a path from the git index, keeping it on disk. Used to migrate
18
+ * repositories where the build output was committed.
19
+ */
20
+ export const removeFromGitIndex = (cwd, path) => {
21
+ if (!isTrackedInGit(cwd, path)) return false;
22
+
23
+ execFileSync("git", ["rm", "-r", "--cached", "--quiet", "--", path], { cwd });
24
+ console.log(`removed ${path} from git, it is no longer committed`);
25
+
26
+ return true;
27
+ };
@@ -0,0 +1,26 @@
1
+ import { existsSync, rmSync } from "node:fs";
2
+
3
+ const GENERATED_MARKERS = [
4
+ ".nojekyll",
5
+ "assets",
6
+ "index.html",
7
+ "coverage.lcov",
8
+ ];
9
+
10
+ /**
11
+ * Remove the legacy generated "docs" directory, which is now "generated-docs".
12
+ * Only removes it when it still looks generated, so that a hand-written
13
+ * markdown "docs" directory is left untouched.
14
+ *
15
+ * @param {(path: string) => string} destinationPath
16
+ */
17
+ export default function removeLegacyGeneratedDocs(destinationPath) {
18
+ const docsPath = destinationPath("docs");
19
+ if (!existsSync(docsPath)) return;
20
+ if (
21
+ !GENERATED_MARKERS.some((marker) => existsSync(`${docsPath}/${marker}`))
22
+ ) {
23
+ return;
24
+ }
25
+ rmSync(docsPath, { recursive: true, force: true });
26
+ }
@@ -0,0 +1,58 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import removeLegacyGeneratedDocs from "./legacyGeneratedDocs.js";
6
+
7
+ async function prepareDocs(files) {
8
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "pob-"));
9
+ if (files) {
10
+ await fs.mkdir(path.join(tmpDir, "docs"), { recursive: true });
11
+ for (const file of files) {
12
+ await fs.writeFile(path.join(tmpDir, "docs", file), "");
13
+ }
14
+ }
15
+ return tmpDir;
16
+ }
17
+
18
+ const exists = (docsPath) =>
19
+ fs
20
+ .stat(docsPath)
21
+ .then(() => true)
22
+ .catch(() => false);
23
+
24
+ const run = (tmpDir) =>
25
+ removeLegacyGeneratedDocs((destination) => path.join(tmpDir, destination));
26
+
27
+ describe("removeLegacyGeneratedDocs", () => {
28
+ it.each([".nojekyll", "index.html", "coverage.lcov"])(
29
+ "removes docs generated by typedoc or coverage, detected by %s",
30
+ async (marker) => {
31
+ const tmp = await prepareDocs([marker]);
32
+ run(tmp);
33
+ expect(await exists(path.join(tmp, "docs"))).toBe(false);
34
+ },
35
+ );
36
+
37
+ it("removes docs detected by the typedoc assets directory", async () => {
38
+ const tmp = await prepareDocs([]);
39
+ await fs.mkdir(path.join(tmp, "docs/assets"), { recursive: true });
40
+ run(tmp);
41
+ expect(await exists(path.join(tmp, "docs"))).toBe(false);
42
+ });
43
+
44
+ it("keeps hand written documentation", async () => {
45
+ const tmp = await prepareDocs(["guide.md", "README.md"]);
46
+ run(tmp);
47
+ expect(await fs.readdir(path.join(tmp, "docs"))).toEqual([
48
+ "README.md",
49
+ "guide.md",
50
+ ]);
51
+ });
52
+
53
+ it("does nothing when there is no docs directory", async () => {
54
+ const tmp = await prepareDocs(undefined);
55
+ expect(() => run(tmp)).not.toThrow();
56
+ expect(await exists(path.join(tmp, "docs"))).toBe(false);
57
+ });
58
+ });
@@ -0,0 +1,124 @@
1
+ import semver from "semver";
2
+ import { latestLTS, maintenanceLTS } from "./nodeVersions.js";
3
+ import * as packageUtils from "./package.js";
4
+
5
+ // 22.18.0 is the version with strip typescript out of experimental status
6
+ const minVersionForMajor = new Map([
7
+ ["22", "22.18.0"],
8
+ ["24", "24.14.1"],
9
+ ["26", "26.0.0"],
10
+ ]);
11
+
12
+ const legacyMajors = ["4", "6", "8", "10", "12", "14", "16", "18", "20"];
13
+
14
+ /**
15
+ * Min node major supported, from pob envs when defined, from the LTS otherwise.
16
+ */
17
+ export const getEnvsMinNodeMajor = (envs, onlyLatestLTS) => {
18
+ if (!envs) return onlyLatestLTS ? latestLTS : maintenanceLTS;
19
+ return String(
20
+ Math.min(
21
+ ...envs
22
+ .filter((env) => env.target === "node")
23
+ .map((env) => Number(env.version)),
24
+ ),
25
+ );
26
+ };
27
+
28
+ /**
29
+ * Min node major of a range, undefined when missing or not parseable
30
+ * (workspace:*, catalog:, npm: alias...).
31
+ */
32
+ export const getMinMajor = (range) => {
33
+ if (!range) return undefined;
34
+ try {
35
+ const minVersion = semver.minVersion(range);
36
+ return minVersion ? semver.major(minVersion) : undefined;
37
+ } catch {
38
+ return undefined;
39
+ }
40
+ };
41
+
42
+ /**
43
+ * engines.node value for a min node major. When envs are not defined, an
44
+ * already higher engines.node is kept.
45
+ */
46
+ export const resolveEnginesNode = (
47
+ currentEnginesNode,
48
+ minNodeMajor,
49
+ hasEnvs,
50
+ ) => {
51
+ const major = legacyMajors.includes(String(minNodeMajor))
52
+ ? maintenanceLTS
53
+ : String(minNodeMajor);
54
+ const minVersion = minVersionForMajor.get(major);
55
+ if (!minVersion) {
56
+ throw new Error(`Invalid min node version: ${minNodeMajor}`);
57
+ }
58
+
59
+ if (!hasEnvs) {
60
+ const currentMinMajor = getMinMajor(currentEnginesNode);
61
+ if (currentMinMajor !== undefined && currentMinMajor > Number(major)) {
62
+ return currentEnginesNode;
63
+ }
64
+ }
65
+
66
+ return `>=${minVersion}`;
67
+ };
68
+
69
+ /**
70
+ * Updates "@types/node" when it is lower than engines.node min major.
71
+ * Never adds it, never lowers devDependencies.
72
+ */
73
+ export const updateTypesNodeFromEngines = (pkg) => {
74
+ const minMajor = getMinMajor(pkg.engines?.node);
75
+ if (minMajor === undefined) return;
76
+ const range = `>=${minMajor}.0.0`;
77
+
78
+ if (pkg.dependencies?.["@types/node"]) {
79
+ pkg.dependencies["@types/node"] = range;
80
+ }
81
+
82
+ const currentDevVersion = pkg.devDependencies?.["@types/node"];
83
+ if (currentDevVersion) {
84
+ const currentMinMajor = getMinMajor(currentDevVersion);
85
+ if (currentMinMajor !== undefined && currentMinMajor < minMajor) {
86
+ pkg.devDependencies["@types/node"] = range;
87
+ }
88
+ }
89
+ };
90
+
91
+ /**
92
+ * Sets engines.node from pob envs, then keeps "@types/node" in sync with it.
93
+ */
94
+ export const updateNodeEngines = (pkg, { onlyLatestLTS } = {}) => {
95
+ const envs = pkg.pob?.envs || pkg.pob?.babelEnvs;
96
+ const hasTargetNode = envs?.some((env) => env.target === "node");
97
+
98
+ if (!pkg.engines) pkg.engines = {};
99
+
100
+ if (hasTargetNode || !envs) {
101
+ pkg.engines.node = resolveEnginesNode(
102
+ pkg.engines.node,
103
+ getEnvsMinNodeMajor(envs, onlyLatestLTS),
104
+ !!envs,
105
+ );
106
+
107
+ updateTypesNodeFromEngines(pkg);
108
+ } else {
109
+ packageUtils.removeDependencies(pkg, ["@types/node"]);
110
+ packageUtils.removeDevDependencies(pkg, ["@types/node"]);
111
+
112
+ // Supports oldest current or active LTS version of node
113
+ const minVersion = minVersionForMajor.get(
114
+ onlyLatestLTS ? latestLTS : maintenanceLTS,
115
+ );
116
+
117
+ if (
118
+ !pkg.engines.node ||
119
+ semver.lt(semver.minVersion(pkg.engines.node), minVersion)
120
+ ) {
121
+ pkg.engines.node = `>=${minVersion}`;
122
+ }
123
+ }
124
+ };