pob 29.9.0 → 31.0.1

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 (23) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/lib/generators/common/format-lint/CommonLintGenerator.js +2 -3
  3. package/lib/generators/common/format-lint/templates/prettierignore.ejs +1 -1
  4. package/lib/generators/common/husky/CommonHuskyGenerator.js +8 -11
  5. package/lib/generators/common/old-dependencies/CommonRemoveOldDependenciesGenerator.js +1 -0
  6. package/lib/generators/common/release/CommonReleaseGenerator.js +2 -7
  7. package/lib/generators/common/release/templates/workflow-release.yml.ejs +20 -13
  8. package/lib/generators/common/testing/CommonTestingGenerator.js +19 -74
  9. package/lib/generators/core/bun/CoreBunGenerator.js +6 -0
  10. package/lib/generators/core/bun/templates/bunfig.toml.ejs +3 -0
  11. package/lib/generators/core/ci/templates/github-action-documentation-workflow.yml.ejs +6 -3
  12. package/lib/generators/core/ci/templates/github-action-push-workflow-split.yml.ejs +9 -5
  13. package/lib/generators/core/ci/templates/github-action-push-workflow.yml.ejs +8 -4
  14. package/lib/generators/core/yarn/CoreYarnGenerator.js +11 -3
  15. package/lib/generators/monorepo/PobMonorepoGenerator.js +12 -25
  16. package/lib/utils/execUtils.js +3 -0
  17. package/lib/utils/packageDependencyDescriptorUtils.js +52 -0
  18. package/lib/utils/packageDependencyDescriptorUtils.ts.backup +79 -0
  19. package/lib/utils/workspaceUtils.js +131 -0
  20. package/lib/utils/workspaceUtils.test.js +134 -0
  21. package/lib/utils/workspaceUtils.ts.backup +167 -0
  22. package/package.json +7 -11
  23. package/lib/generators/common/husky/templates/lint-staged.config.cjs.txt +0 -5
package/CHANGELOG.md CHANGED
@@ -3,6 +3,67 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [31.0.1](https://github.com/christophehurpeau/pob/compare/pob@31.0.0...pob@31.0.1) (2025-12-06)
7
+
8
+ ### Bug Fixes
9
+
10
+ * update all workflows
11
+ * update workflow-release ejs
12
+
13
+ Version bump for dependency: @pob/root
14
+
15
+
16
+ ## [31.0.0](https://github.com/christophehurpeau/pob/compare/pob@30.0.0...pob@31.0.0) (2025-12-06)
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * migrate pob-version to @pob/version and add initial bun support
21
+
22
+ ### Features
23
+
24
+ * add bunfig.toml with minimumReleaseAge
25
+ * add npmMinimalAgeGate in yarn config
26
+ * add npmMinimumReleaseAgeExclude option to CoreYarnGenerator
27
+ * add quoteArg utility function and update script paths in generators
28
+ * migrate pob-version to @pob/version and add initial bun support
29
+ * try to implement publish with bun
30
+ * update npmPreapprovedPackages in CoreYarnGenerator
31
+
32
+ ### Bug Fixes
33
+
34
+ * simplify script path handling in CommonLintGenerator and CommonTestingGenerator
35
+
36
+ Version bump for dependency: @pob/sort-object
37
+ Version bump for dependency: @pob/sort-pkg
38
+ Version bump for dependency: @pob/root
39
+
40
+
41
+ ## [30.0.0](https://github.com/christophehurpeau/pob/compare/pob@29.9.0...pob@30.0.0) (2025-11-24)
42
+
43
+ ### ⚠ BREAKING CHANGES
44
+
45
+ * migrate createLintStagedConfig to ES module and remove CommonJS version
46
+ * drop swc with jest support
47
+
48
+ ### Features
49
+
50
+ * add ci permissions
51
+ * drop swc with jest support
52
+ * implement package dependency descriptor utilities and workspace management to replace yarn packages and better support bun
53
+ * migrate createLintStagedConfig to ES module and remove CommonJS version
54
+ * update templates for bun v2
55
+ * update yarn to 4.12.0
56
+
57
+ ### Bug Fixes
58
+
59
+ * add rollup-plugin-copy to the list of removed dependencies
60
+ * **deps:** update js-yaml to version 4.1.1
61
+ * move /.yarnrc.yml to yarn section
62
+ * update E2E testing paths to handle boolean and string values correctly
63
+
64
+ Version bump for dependency: @pob/root
65
+
66
+
6
67
  ## [29.9.0](https://github.com/christophehurpeau/pob/compare/pob@29.8.0...pob@29.9.0) (2025-11-18)
7
68
 
8
69
  ### Features
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import Generator from "yeoman-generator";
3
+ import { quoteArg } from "../../../utils/execUtils.js";
3
4
  import inMonorepo from "../../../utils/inMonorepo.js";
4
5
  import * as packageUtils from "../../../utils/package.js";
5
6
  import { copyAndFormatTpl } from "../../../utils/writeAndFormat.js";
@@ -522,9 +523,7 @@ export default class CommonFormatLintGenerator extends Generator {
522
523
 
523
524
  packageUtils.addScripts(pkg, {
524
525
  "lint:eslint": globalEslint
525
- ? `yarn ../.. run eslint ${args} ${path
526
- .relative("../..", ".")
527
- .replace("\\", "/")}`
526
+ ? `yarn ../.. run eslint ${args} ${quoteArg(path.relative("../..", "."))}`
528
527
  : `eslint ${args} .`,
529
528
  lint: `${
530
529
  useTypescript && !composite ? "tsc && " : ""
@@ -1,9 +1,9 @@
1
1
  CHANGELOG.md
2
- /.yarnrc.yml
3
2
  <% if (documentation) { -%>
4
3
  /docs
5
4
  <% } -%>
6
5
  <% if (packageManager === 'yarn' && inRoot) { -%>
6
+ /.yarnrc.yml
7
7
  /.yarn/**/*
8
8
  <% if (yarnNodeLinker === 'pnp') { -%>
9
9
  /.pnp.*
@@ -69,17 +69,14 @@ export default class CommonHuskyGenerator extends Generator {
69
69
  // '@commitlint/config-lerna-scopes': '6.1.3',
70
70
  // });
71
71
 
72
- if (pkg.type !== "module") {
73
- this.fs.copy(
74
- this.templatePath("lint-staged.config.cjs.txt"),
75
- this.destinationPath("lint-staged.config.js"),
76
- );
77
- } else {
78
- this.fs.copy(
79
- this.templatePath("lint-staged.config.js.txt"),
80
- this.destinationPath("lint-staged.config.js"),
81
- );
82
- }
72
+ this.fs.copy(
73
+ this.templatePath("lint-staged.config.js.txt"),
74
+ this.destinationPath(
75
+ pkg.type !== "module"
76
+ ? "lint-staged.config.mjs"
77
+ : "lint-staged.config.js",
78
+ ),
79
+ );
83
80
  }
84
81
 
85
82
  pkg.commitlint = {
@@ -48,6 +48,7 @@ export default class CommonRemoveOldDependenciesGenerator extends Generator {
48
48
  "pob-release",
49
49
  "eslint-plugin-typescript",
50
50
  "xunit-file",
51
+ "rollup-plugin-copy",
51
52
  ]);
52
53
 
53
54
  this.fs.writeJSON(this.destinationPath("package.json"), pkg);
@@ -69,16 +69,11 @@ export default class CommonReleaseGenerator extends Generator {
69
69
  const pkg = this.fs.readJSON(this.destinationPath("package.json"));
70
70
 
71
71
  if (this.options.enable && this.options.ci) {
72
- const useLegacyName = this.fs.exists(
73
- this.destinationPath(".github/workflows/publish.yml"),
74
- );
75
-
76
- const name = useLegacyName ? "publish.yml" : "release.yml";
72
+ this.fs.delete(this.destinationPath(".github/workflows/publish.yml"));
77
73
 
78
- // TODO rename release (release = version + publish)
79
74
  this.fs.copyTpl(
80
75
  this.templatePath("workflow-release.yml.ejs"),
81
- this.destinationPath(`.github/workflows/${name}`),
76
+ this.destinationPath(".github/workflows/release.yml"),
82
77
  {
83
78
  packageManager: this.options.packageManager,
84
79
  enablePublish: this.options.enablePublish,
@@ -24,19 +24,29 @@ on:
24
24
  default: "major"
25
25
  <% } -%>
26
26
 
27
+ permissions:
28
+ id-token: write # Required for OIDC
29
+ contents: write
30
+
27
31
  jobs:
28
32
  release:
29
33
  runs-on: ubuntu-latest
30
34
  steps:
31
35
  - uses: actions/checkout@v5
32
36
  with:
33
- token: ${{ secrets.GH_TOKEN }}
37
+ ssh-key: ${{ secrets.PUSH_DEPLOY_KEY }}
34
38
  fetch-depth: 0
35
39
  fetch-tags: true
36
40
  <% if (packageManager === 'yarn') { -%>
37
41
 
38
42
  - name: Enable Corepack
39
43
  run: corepack enable
44
+ <% } else if (packageManager === 'bun') { -%>
45
+
46
+ - name: Install bun
47
+ uses: oven-sh/setup-bun@v2
48
+ with:
49
+ bun-version: latest
40
50
  <% } -%>
41
51
 
42
52
  - uses: actions/setup-node@v6
@@ -61,16 +71,13 @@ jobs:
61
71
  run: yarn install --immutable --immutable-cache
62
72
  <% } -%>
63
73
  <% } else if (packageManager === 'bun') { -%>
64
- - name: Install bun
65
- uses: oven-sh/setup-bun@v1
66
-
67
74
  - name: Install Dependencies
68
75
  run: bun install --frozen-lockfile
69
76
  <% } -%>
70
77
 
71
78
  - name: New version (dry run)
72
79
  if: github.ref == 'refs/heads/main' && inputs.dry-run
73
- run: yarn yarn-version version --dry-run<% if (isMonorepo && isMonorepoIndependent) { %> --bump-dependents-highest-as=${{ inputs.bump-dependents-highest-as }}<% } %>
80
+ run: <%= packageManager %> run pob-version --dry-run<% if (isMonorepo && isMonorepoIndependent) { %> --bump-dependents-highest-as=${{ inputs.bump-dependents-highest-as }}<% } %>
74
81
 
75
82
  - name: Configure Git user
76
83
  if: github.ref == 'refs/heads/main' && !inputs.dry-run
@@ -81,27 +88,27 @@ jobs:
81
88
  - name: New version
82
89
  if: github.ref == 'refs/heads/main' && !inputs.dry-run
83
90
  run: |
84
- yarn yarn-version version --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' : '' %>'
91
+ <%= packageManager %> run 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' : '' %>'
85
92
  env:
86
93
  HUSKY: 0
87
- GH_TOKEN: ${{ secrets.GH_TOKEN }}
88
94
  YARN_ENABLE_IMMUTABLE_INSTALLS: false
89
- <% if (enablePublish) { -%>
95
+ GH_TOKEN: {{ secrets.GITHUB_TOKEN }}
96
+ <% if (enablePublish && packageManager === "yarn") { -%>
90
97
 
91
98
  - name: Publish to npm
99
+ if: github.ref == 'refs/heads/main' && !inputs.dry-run
92
100
  run: |
93
- if [ -z "$NODE_AUTH_TOKEN" ]; then
94
- echo "Missing env variable NODE_AUTH_TOKEN"
101
+ if [ -z "$NPM_AUTH_TOKEN" ]; then
102
+ echo "Missing env variable NPM_AUTH_TOKEN"
95
103
  exit 1
96
104
  fi
97
105
  echo >> ./.yarnrc.yml
98
- echo "npmAuthToken: $NODE_AUTH_TOKEN" >> ./.yarnrc.yml
106
+ echo "npmAuthToken: $NPM_AUTH_TOKEN" >> ./.yarnrc.yml
99
107
  <% if (isMonorepo) { -%>
100
108
  yarn workspaces foreach --all --parallel --no-private npm publish --tolerate-republish
101
109
  <% } else { -%>
102
110
  yarn npm publish
103
111
  <% } -%>
104
- if: github.ref == 'refs/heads/main' && !inputs.dry-run
105
112
  env:
106
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
113
+ NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
107
114
  <% } -%>
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import Generator from "yeoman-generator";
4
+ import { quoteArg } from "../../../utils/execUtils.js";
4
5
  import inMonorepo from "../../../utils/inMonorepo.js";
5
6
  import * as packageUtils from "../../../utils/package.js";
6
7
  import {
@@ -103,13 +104,6 @@ export default class CommonTestingGenerator extends Generator {
103
104
  description:
104
105
  "Disable git cache. See https://yarnpkg.com/features/caching#offline-mirror.",
105
106
  });
106
-
107
- this.option("swc", {
108
- type: Boolean,
109
- required: false,
110
- default: false,
111
- description: "Use swc to transpile code.",
112
- });
113
107
  }
114
108
 
115
109
  default() {
@@ -179,17 +173,13 @@ export default class CommonTestingGenerator extends Generator {
179
173
  const tsTestUtil = (() => {
180
174
  if (testRunner === "vitest") return undefined;
181
175
  if (!withTypescript) return undefined;
182
- if (this.options.swc || isJestRunner) return "swc";
176
+ if (isJestRunner && !transpileWithBabel && withTypescript) {
177
+ throw new Error("SWC is no longer supported. Migrate to vitest.");
178
+ }
183
179
  return "node";
184
180
  })();
185
181
 
186
- const dependenciesForTestUtil = {
187
- swc: {
188
- devDependenciesShared: ["@swc/core"],
189
- devDependenciesWithJest: ["@swc/jest"],
190
- devDependenciesWithNode: ["@swc-node/register"],
191
- },
192
- };
182
+ const dependenciesForTestUtil = {};
193
183
 
194
184
  Object.entries(dependenciesForTestUtil).forEach(
195
185
  ([
@@ -257,9 +247,6 @@ export default class CommonTestingGenerator extends Generator {
257
247
  switch (tsTestUtil) {
258
248
  case "node":
259
249
  return "";
260
- case "swc":
261
- return "--import=@swc-node/register/esm";
262
-
263
250
  // no default
264
251
  }
265
252
  })();
@@ -447,23 +434,6 @@ export default class CommonTestingGenerator extends Generator {
447
434
  delete jestConfig.extensionsToTreatAsEsm;
448
435
  }
449
436
 
450
- if (tsTestUtil === "swc" && !transpileWithBabel && withTypescript) {
451
- jestConfig.transform = {
452
- [hasReact ? "^.+\\.tsx?$" : "^.+\\.ts$"]: ["@swc/jest"],
453
- };
454
- } else if (jestConfig.transform) {
455
- jestConfig.transform = Object.fromEntries(
456
- Object.entries(jestConfig.transform).filter(
457
- ([key, value]) =>
458
- value !== "@swc/jest" &&
459
- !(Array.isArray(value) && value[0] === "@swc/jest"),
460
- ),
461
- );
462
- if (Object.keys(jestConfig.transform).length === 0) {
463
- delete jestConfig.transform;
464
- }
465
- }
466
-
467
437
  writeAndFormatJson(this.fs, jestConfigPath, jestConfig);
468
438
  }
469
439
  } else {
@@ -477,9 +447,7 @@ export default class CommonTestingGenerator extends Generator {
477
447
  delete pkg.scripts["test:coverage"];
478
448
  }
479
449
  packageUtils.addScripts(pkg, {
480
- test: `yarn ../../ run test -- ${path
481
- .relative("../..", ".")
482
- .replace("\\", "/")}`,
450
+ test: `yarn ../../ run test -- ${quoteArg(path.relative("../..", "."))}`,
483
451
  });
484
452
  } else {
485
453
  const withTypescript =
@@ -554,42 +522,19 @@ export default class CommonTestingGenerator extends Generator {
554
522
  };
555
523
  } else if (!transpileWithBabel && !withTypescript) {
556
524
  delete jestConfig.transform;
557
- } else {
558
- if (
559
- tsTestUtil === "swc" &&
560
- !transpileWithBabel &&
561
- withTypescript
562
- ) {
563
- jestConfig.transform = {
564
- [hasReact ? "^.+\\.tsx?$" : "^.+\\.ts$"]: ["@swc/jest"],
565
- };
566
- } else if (jestConfig.transform) {
567
- jestConfig.transform = Object.fromEntries(
568
- Object.entries(jestConfig.transform).filter(
569
- ([key, value]) =>
570
- value !== "@swc/jest" &&
571
- !(Array.isArray(value) && value[0] === "@swc/jest"),
572
- ),
573
- );
574
- if (Object.keys(jestConfig.transform).length === 0) {
575
- delete jestConfig.transform;
576
- }
577
- }
578
-
579
- if (jestConfig.transform) {
580
- jestConfig.transform = Object.fromEntries(
581
- Object.entries(jestConfig.transform).filter(
582
- ([key, value]) =>
583
- !(
584
- value &&
585
- Array.isArray(value) &&
586
- value[0] === "jest-esbuild"
587
- ),
588
- ),
589
- );
590
- if (Object.keys(jestConfig.transform).length === 0) {
591
- delete jestConfig.transform;
592
- }
525
+ } else if (jestConfig.transform) {
526
+ jestConfig.transform = Object.fromEntries(
527
+ Object.entries(jestConfig.transform).filter(
528
+ ([key, value]) =>
529
+ !(
530
+ value &&
531
+ Array.isArray(value) &&
532
+ value[0] === "jest-esbuild"
533
+ ),
534
+ ),
535
+ );
536
+ if (Object.keys(jestConfig.transform).length === 0) {
537
+ delete jestConfig.transform;
593
538
  }
594
539
  }
595
540
 
@@ -27,8 +27,14 @@ export default class CoreBunGenerator extends Generator {
27
27
  const pkg = this.fs.readJSON(this.destinationPath("package.json"));
28
28
 
29
29
  if (this.options.enable) {
30
+ this.fs.copyTpl(
31
+ this.templatePath("bunfig.toml.ejs"),
32
+ this.destinationPath("bunfig.toml"),
33
+ {},
34
+ );
30
35
  } else {
31
36
  this.fs.delete("bun.lock");
37
+ this.fs.delete("bunfig.toml");
32
38
  }
33
39
 
34
40
  this.fs.writeJSON(this.destinationPath("package.json"), pkg);
@@ -0,0 +1,3 @@
1
+ [install]
2
+ # Only install package versions published at least 3 days ago
3
+ minimumReleaseAge = 259200 # seconds - in #23162 it'll allow "3d" too
@@ -14,6 +14,12 @@ jobs:
14
14
  - name: Enable Corepack
15
15
  run: corepack enable
16
16
 
17
+ <% } else if (packageManager === 'bun') { -%>
18
+ - name: Install bun
19
+ uses: oven-sh/setup-bun@v2
20
+ with:
21
+ bun-version: latest
22
+
17
23
  <% } -%>
18
24
  - uses: actions/setup-node@v6
19
25
  with:
@@ -28,9 +34,6 @@ jobs:
28
34
  run: yarn install --immutable --immutable-cache
29
35
  <% } -%>
30
36
  <% } else if (packageManager === 'bun') { -%>
31
- - name: Install bun
32
- uses: oven-sh/setup-bun@v1
33
-
34
37
  - name: Install Dependencies
35
38
  run: bun install --frozen-lockfile
36
39
  <% } -%>
@@ -1,5 +1,6 @@
1
1
  name: Push CI
2
-
2
+ permissions:
3
+ contents: read
3
4
  on: [push]
4
5
 
5
6
  jobs:
@@ -113,6 +114,12 @@ jobs:
113
114
  - name: Enable Corepack
114
115
  run: corepack enable
115
116
 
117
+ <% } else if (packageManager === 'bun') { -%>
118
+ - name: Install bun
119
+ uses: oven-sh/setup-bun@v2
120
+ with:
121
+ bun-version: latest
122
+
116
123
  <% } -%>
117
124
  - uses: actions/setup-node@v6
118
125
  with:
@@ -127,9 +134,6 @@ jobs:
127
134
  run: yarn install --immutable --immutable-cache
128
135
  <% } -%>
129
136
  <% } else if (packageManager === 'bun') { -%>
130
- - name: Install bun
131
- uses: oven-sh/setup-bun@v1
132
-
133
137
  - name: Install Dependencies
134
138
  run: bun install --frozen-lockfile
135
139
  <% } -%>
@@ -185,7 +189,7 @@ jobs:
185
189
  <% } -%>
186
190
 
187
191
  - name: E2E testing
188
- run: <%= packageManager %> <%= e2eTesting === '.' ? '.' : `./${e2eTesting}` %> run test:e2e
192
+ run: <%= packageManager %> <%= e2eTesting === '.' || e2eTesting === true ? '.' : `./${e2eTesting}` %> run test:e2e
189
193
  <% } -%>
190
194
  <% if (isReleasePleaseEnabled) { -%>
191
195
  release:
@@ -1,5 +1,6 @@
1
1
  name: Push CI
2
-
2
+ permissions:
3
+ contents: read
3
4
  on: [push]
4
5
 
5
6
  jobs:
@@ -17,6 +18,12 @@ jobs:
17
18
  - name: Enable Corepack
18
19
  run: corepack enable
19
20
 
21
+ <% } else if (packageManager === 'bun') { -%>
22
+ - name: Install bun
23
+ uses: oven-sh/setup-bun@v2
24
+ with:
25
+ bun-version: latest
26
+
20
27
  <% } -%>
21
28
  - name: Use Node.js ${{ matrix.node-version }}
22
29
  uses: actions/setup-node@v6
@@ -36,9 +43,6 @@ jobs:
36
43
  - name: Install Dependencies
37
44
  run: npm ci
38
45
  <% } else if (packageManager === 'bun') { -%>
39
- - name: Install bun
40
- uses: oven-sh/setup-bun@v1
41
-
42
46
  - name: Install Dependencies
43
47
  run: bun install --frozen-lockfile
44
48
  <% } -%>
@@ -142,9 +142,9 @@ export default class CoreYarnGenerator extends Generator {
142
142
  if (
143
143
  !pkg.packageManager ||
144
144
  !pkg.packageManager.startsWith("yarn@") ||
145
- lt(pkg.packageManager.slice("yarn@".length), "4.10.3")
145
+ lt(pkg.packageManager.slice("yarn@".length), "4.12.0")
146
146
  ) {
147
- pkg.packageManager = "yarn@4.10.3";
147
+ pkg.packageManager = "yarn@4.12.0";
148
148
  }
149
149
 
150
150
  // must be done after plugins installed
@@ -159,7 +159,7 @@ export default class CoreYarnGenerator extends Generator {
159
159
  // leave default compressionLevel instead of this next line
160
160
  delete config.compressionLevel;
161
161
  // config.compressionLevel = "mixed"; // optimized for size
162
- config.enableGlobalCache = "true";
162
+ config.enableGlobalCache = true;
163
163
  delete config.supportedArchitectures;
164
164
  } else {
165
165
  config.compressionLevel = 0; // optimized for github config
@@ -175,6 +175,14 @@ export default class CoreYarnGenerator extends Generator {
175
175
  config.defaultSemverRangePrefix = this.options.type === "app" ? "" : "^";
176
176
  delete config.enableMessageNames; // was a config for yarn < 4
177
177
  config.nodeLinker = this.options.yarnNodeLinker;
178
+ config.npmMinimalAgeGate = 1440 * 3; // 3 days
179
+ config.npmPreapprovedPackages = [
180
+ "@pob/*",
181
+ "alouette",
182
+ "alouette-icons",
183
+ "nightingale",
184
+ "nightingale-logger",
185
+ ];
178
186
 
179
187
  if (config.yarnPath) {
180
188
  this.fs.delete(config.yarnPath);
@@ -1,31 +1,16 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import { platform } from "node:process";
4
- import { getPluginConfiguration } from "@yarnpkg/cli";
5
- import { Configuration, Project } from "@yarnpkg/core";
6
- import { ppath } from "@yarnpkg/fslib";
4
+ import Generator from "yeoman-generator";
5
+ import * as packageUtils from "../../utils/package.js";
7
6
  import {
8
7
  buildDependenciesMaps,
9
8
  buildTopologicalOrderBatches,
9
+ discoverWorkspaces,
10
10
  getWorkspaceName,
11
- } from "yarn-workspace-utils";
12
- import Generator from "yeoman-generator";
13
- import * as packageUtils from "../../utils/package.js";
11
+ } from "../../utils/workspaceUtils.js";
14
12
  import { copyAndFormatTpl } from "../../utils/writeAndFormat.js";
15
13
 
16
- export const createYarnProject = async () => {
17
- const portablePath = ppath.cwd();
18
-
19
- const configuration = await Configuration.find(
20
- portablePath,
21
- // eslint-disable-next-line unicorn/no-array-method-this-argument -- not an array
22
- getPluginConfiguration(),
23
- );
24
- // eslint-disable-next-line unicorn/no-array-method-this-argument -- not an array
25
- const { project } = await Project.find(configuration, portablePath);
26
- return project;
27
- };
28
-
29
14
  const getAppTypes = (configs) => {
30
15
  const appConfigs = configs.filter(
31
16
  (config) => config && config.project && config.project.type === "app",
@@ -129,10 +114,10 @@ export default class PobMonorepoGenerator extends Generator {
129
114
  }
130
115
 
131
116
  async initializing() {
132
- const yarnProject = await createYarnProject(this.destinationPath());
117
+ const workspaces = await discoverWorkspaces(this.destinationPath());
133
118
  const batches = buildTopologicalOrderBatches(
134
- yarnProject,
135
- buildDependenciesMaps(yarnProject),
119
+ workspaces,
120
+ buildDependenciesMaps(workspaces),
136
121
  );
137
122
 
138
123
  this.packages = [];
@@ -145,7 +130,7 @@ export default class PobMonorepoGenerator extends Generator {
145
130
  );
146
131
 
147
132
  batch.forEach((workspace) => {
148
- if (workspace === yarnProject.topLevelWorkspace) {
133
+ if (workspace.isRoot) {
149
134
  return;
150
135
  }
151
136
  this.packages.push(workspace.manifest.raw);
@@ -268,8 +253,10 @@ export default class PobMonorepoGenerator extends Generator {
268
253
  });
269
254
 
270
255
  const rootIgnorePaths = [
271
- this.pobLernaConfig.e2eTesting && "/playwright-report/",
272
- this.pobLernaConfig.e2eTesting && "/test-results/",
256
+ this.pobLernaConfig.e2eTesting &&
257
+ `${this.pobLernaConfig.e2eTesting === "." || this.pobLernaConfig.e2eTesting === true ? "" : `/${this.pobLernaConfig.e2eTesting}`}/playwright-report/`,
258
+ this.pobLernaConfig.e2eTesting &&
259
+ `${this.pobLernaConfig.e2eTesting === "." || this.pobLernaConfig.e2eTesting === true ? "" : `/${this.pobLernaConfig.e2eTesting}`}/test-results/`,
273
260
  ].filter(Boolean);
274
261
 
275
262
  const gitignorePaths = [
@@ -0,0 +1,3 @@
1
+ export const quoteArg = (s) => {
2
+ return `'${s.replace(/'/g, "'\"'")}'`;
3
+ };
@@ -0,0 +1,52 @@
1
+ // TODO use pm-utils when available
2
+
3
+ export const PackageDescriptorNameUtils = {
4
+ parse: (value) => {
5
+ if (value.startsWith("@")) {
6
+ const [scope, name] = value.slice(1).split("/", 2);
7
+ return { scope, name };
8
+ }
9
+ return { name: value };
10
+ },
11
+ stringify: (descriptor) => {
12
+ return descriptor.scope === undefined
13
+ ? descriptor.name
14
+ : `@${descriptor.scope}/${descriptor.name}`;
15
+ },
16
+ };
17
+
18
+ export const PackageDependencyDescriptorUtils = {
19
+ make: (descriptor, selector) => {
20
+ return {
21
+ key: descriptor.key,
22
+ npmName: descriptor.npmName,
23
+ nameDescriptor: descriptor.nameDescriptor,
24
+ selector,
25
+ };
26
+ },
27
+ parse: (dependencyKey, dependencyValue) => {
28
+ const parseFromNpm = (v) => {
29
+ if (!v.startsWith("@")) return v.split("@", 2);
30
+ const [packageNameWithoutFirstChar, selector] = v.slice(1).split("@", 2);
31
+ return [`@${packageNameWithoutFirstChar}`, selector];
32
+ };
33
+ const [name, selector] = dependencyValue.startsWith("npm:")
34
+ ? parseFromNpm(dependencyValue.slice("npm:".length))
35
+ : [dependencyKey, dependencyValue];
36
+
37
+ return {
38
+ key: dependencyKey,
39
+ npmName: name,
40
+ nameDescriptor: PackageDescriptorNameUtils.parse(name),
41
+ selector,
42
+ };
43
+ },
44
+ stringify: (descriptor) => {
45
+ return [
46
+ descriptor.key,
47
+ descriptor.npmName !== descriptor.key
48
+ ? `npm:${descriptor.npmName}@${descriptor.selector}`
49
+ : descriptor.selector,
50
+ ];
51
+ },
52
+ };
@@ -0,0 +1,79 @@
1
+ export interface PackageDescriptorName {
2
+ scope?: string;
3
+ name: string;
4
+ }
5
+
6
+ interface DescriptorUtils<Descriptor> {
7
+ parse: (value: string) => Descriptor;
8
+ stringify: (descriptor: Descriptor) => string;
9
+ }
10
+
11
+ export const PackageDescriptorNameUtils: DescriptorUtils<PackageDescriptorName> =
12
+ {
13
+ parse: (value) => {
14
+ if (value.startsWith("@")) {
15
+ const [scope, name] = value.slice(1).split("/", 2);
16
+ return { scope, name };
17
+ }
18
+ return { name: value };
19
+ },
20
+ stringify: (descriptor) => {
21
+ return descriptor.scope === undefined
22
+ ? descriptor.name
23
+ : `@${descriptor.scope}/${descriptor.name}`;
24
+ },
25
+ };
26
+
27
+ export interface PackageDependencyDescriptor {
28
+ key: string;
29
+ npmName: string;
30
+ nameDescriptor: PackageDescriptorName;
31
+ selector: string; // can be npm tag or version or version range or git url or local folder path
32
+ }
33
+
34
+ interface PackageDependencyDescriptorUtils<
35
+ Descriptor = PackageDependencyDescriptor,
36
+ > {
37
+ make: (descriptor: Descriptor, selector: string) => Descriptor;
38
+ parse: (dependencyKey: string, dependencyValue: string) => Descriptor;
39
+ stringify: (descriptor: Descriptor) => [key: string, value: string];
40
+ }
41
+
42
+ export const PackageDependencyDescriptorUtils: PackageDependencyDescriptorUtils =
43
+ {
44
+ make: (descriptor, selector) => {
45
+ return {
46
+ key: descriptor.key,
47
+ npmName: descriptor.npmName,
48
+ nameDescriptor: descriptor.nameDescriptor,
49
+ selector,
50
+ };
51
+ },
52
+ parse: (dependencyKey, dependencyValue) => {
53
+ const [name, selector] = dependencyValue.startsWith("npm:")
54
+ ? (() => {
55
+ const v = dependencyValue.slice("npm:".length);
56
+ if (!v.startsWith("@")) return v.split("@", 2);
57
+ const [packageNameWithoutFirstChar, selector] = v
58
+ .slice(1)
59
+ .split("@", 2);
60
+ return [`@${packageNameWithoutFirstChar}`, selector];
61
+ })()
62
+ : [dependencyKey, dependencyValue];
63
+
64
+ return {
65
+ key: dependencyKey,
66
+ npmName: name,
67
+ nameDescriptor: PackageDescriptorNameUtils.parse(name),
68
+ selector,
69
+ };
70
+ },
71
+ stringify: (descriptor) => {
72
+ return [
73
+ descriptor.key,
74
+ descriptor.npmName !== descriptor.key
75
+ ? `npm:${descriptor.npmName}@${descriptor.selector}`
76
+ : descriptor.selector,
77
+ ];
78
+ },
79
+ };
@@ -0,0 +1,131 @@
1
+ import fs from "node:fs";
2
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
3
+ import { glob } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { PackageDependencyDescriptorUtils } from "./packageDependencyDescriptorUtils.js";
6
+
7
+ export const getWorkspaceName = (workspace) => {
8
+ if (workspace?.manifest?.raw?.name) return workspace.manifest.raw.name;
9
+ return path.basename(workspace.location) || "unnamed-workspace";
10
+ };
11
+
12
+ export const discoverWorkspaces = async (rootPath) => {
13
+ const rootPackageJSONPath = path.join(rootPath, "package.json");
14
+ const rootPkg = JSON.parse(fs.readFileSync(rootPackageJSONPath));
15
+
16
+ let workspaceGlobs = [];
17
+ if (Array.isArray(rootPkg.workspaces)) {
18
+ workspaceGlobs = rootPkg.workspaces;
19
+ } else if (typeof rootPkg.workspaces === "object") {
20
+ workspaceGlobs = (rootPkg.workspaces && rootPkg.workspaces.packages) || [];
21
+ }
22
+
23
+ const workspaces = [
24
+ {
25
+ name: rootPkg.name,
26
+ location: ".",
27
+ manifest: { raw: rootPkg },
28
+ relativeCwd: { toString: () => "." },
29
+ isRoot: true,
30
+ },
31
+ ];
32
+
33
+ const patternPackageJsons = workspaceGlobs.map((g) =>
34
+ path.join(g, "package.json"),
35
+ );
36
+ const found = new Set();
37
+ for (const pattern of patternPackageJsons) {
38
+ for await (const match of glob(pattern, { cwd: rootPath, nodir: true })) {
39
+ if (found.has(match)) continue;
40
+ found.add(match);
41
+ const filePath = path.join(rootPath, match);
42
+ const content = JSON.parse(fs.readFileSync(filePath));
43
+ const dir = path.dirname(match);
44
+ workspaces.push({
45
+ name: content.name,
46
+ location: dir || ".",
47
+ manifest: { raw: content },
48
+ relativeCwd: { toString: () => dir || "." },
49
+ });
50
+ }
51
+ }
52
+
53
+ return workspaces;
54
+ };
55
+
56
+ export const buildDependenciesMaps = (workspaces) => {
57
+ const dependenciesMap = new Map();
58
+
59
+ const workspacesByName = new Map(
60
+ workspaces.filter((w) => !!w.name).map((w) => [w.name, w]),
61
+ );
62
+ const dependencyTypes = [
63
+ "dependencies",
64
+ "devDependencies",
65
+ "peerDependencies",
66
+ ];
67
+
68
+ for (const dependent of workspaces) {
69
+ for (const set of dependencyTypes) {
70
+ const deps =
71
+ (dependent.manifest.raw && dependent.manifest.raw[set]) || {};
72
+ for (const [dependencyKey, dependencyValue] of Object.entries(deps)) {
73
+ if (!dependencyValue) continue;
74
+ const descriptor = PackageDependencyDescriptorUtils.parse(
75
+ dependencyKey,
76
+ String(dependencyValue),
77
+ );
78
+ const workspace = workspacesByName.get(descriptor.npmName);
79
+ if (!workspace) continue;
80
+
81
+ const entries = dependenciesMap.get(dependent) || [];
82
+ entries.push([workspace, set, descriptor]);
83
+ dependenciesMap.set(dependent, entries);
84
+ }
85
+ }
86
+ }
87
+
88
+ return dependenciesMap;
89
+ };
90
+
91
+ export const buildTopologicalOrderBatches = (workspaces, dependenciesMap) => {
92
+ const batches = [];
93
+ const added = new Set();
94
+ const toAdd = new Set(workspaces);
95
+
96
+ while (toAdd.size > 0) {
97
+ const batch = new Set();
98
+ for (const workspace of toAdd) {
99
+ if (workspace.isRoot && toAdd.size > 1) continue;
100
+ const dependencies = dependenciesMap.get(workspace);
101
+ if (!dependencies || dependencies.every((w) => added.has(w[0]))) {
102
+ batch.add(workspace);
103
+ }
104
+ }
105
+
106
+ for (const workspace of batch) {
107
+ added.add(workspace);
108
+ toAdd.delete(workspace);
109
+ }
110
+
111
+ if (batch.size === 0) {
112
+ throw new Error("Circular dependency detected");
113
+ }
114
+ batches.push([...batch]);
115
+ }
116
+
117
+ return batches;
118
+ };
119
+
120
+ export const buildDependentsMaps = (workspaces) => {
121
+ const dependentsMap = new Map();
122
+ const dependenciesMap = buildDependenciesMaps(workspaces);
123
+ for (const [dependent, relations] of dependenciesMap) {
124
+ for (const [workspace, set, descriptor] of relations) {
125
+ const cmd = dependentsMap.get(workspace) || [];
126
+ cmd.push([dependent, set, descriptor]);
127
+ dependentsMap.set(workspace, cmd);
128
+ }
129
+ }
130
+ return dependentsMap;
131
+ };
@@ -0,0 +1,134 @@
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 {
6
+ buildDependenciesMaps,
7
+ buildTopologicalOrderBatches,
8
+ discoverWorkspaces,
9
+ } from "./workspaceUtils.js";
10
+
11
+ async function prepareMonorepo(tmpDir) {
12
+ await fs.mkdir(path.join(tmpDir, "packages"), { recursive: true });
13
+ const rootPkg = {
14
+ name: "root",
15
+ workspaces: ["packages/*"],
16
+ };
17
+ await fs.writeFile(
18
+ path.join(tmpDir, "package.json"),
19
+ JSON.stringify(rootPkg, null, 2),
20
+ );
21
+
22
+ // package c
23
+ const cDir = path.join(tmpDir, "packages/c");
24
+ await fs.mkdir(cDir, { recursive: true });
25
+ await fs.writeFile(
26
+ path.join(cDir, "package.json"),
27
+ JSON.stringify({ name: "@ex/c" }, null, 2),
28
+ );
29
+
30
+ // package b depends on c
31
+ const bDir = path.join(tmpDir, "packages/b");
32
+ await fs.mkdir(bDir, { recursive: true });
33
+ await fs.writeFile(
34
+ path.join(bDir, "package.json"),
35
+ JSON.stringify(
36
+ { name: "@ex/b", dependencies: { "@ex/c": "1.0.0" } },
37
+ null,
38
+ 2,
39
+ ),
40
+ );
41
+
42
+ // package a depends on b
43
+ const aDir = path.join(tmpDir, "packages/a");
44
+ await fs.mkdir(aDir, { recursive: true });
45
+ await fs.writeFile(
46
+ path.join(aDir, "package.json"),
47
+ JSON.stringify(
48
+ { name: "@ex/a", dependencies: { "@ex/b": "1.0.0" } },
49
+ null,
50
+ 2,
51
+ ),
52
+ );
53
+
54
+ return tmpDir;
55
+ }
56
+
57
+ describe("workspaceUtils", () => {
58
+ it("should discover workspaces", async () => {
59
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "pob-"));
60
+ await prepareMonorepo(tmp);
61
+ const workspaces = await discoverWorkspaces(tmp);
62
+ // root + a + b + c
63
+ expect(workspaces.length).toBeGreaterThanOrEqual(4);
64
+ const names = workspaces.map((w) => w.name).toSorted();
65
+ expect(names).toEqual(["@ex/a", "@ex/b", "@ex/c", "root"].toSorted());
66
+ });
67
+
68
+ it("should build dependencies maps and topological batches", async () => {
69
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "pob-"));
70
+ await prepareMonorepo(tmp);
71
+ const workspaces = await discoverWorkspaces(tmp);
72
+ const dependenciesMap = buildDependenciesMaps(workspaces);
73
+ // find workspace by name
74
+ const byName = new Map(workspaces.map((w) => [w.name, w]));
75
+ const a = byName.get("@ex/a");
76
+ const b = byName.get("@ex/b");
77
+ // const c = byName.get("@ex/c");
78
+ expect(dependenciesMap.get(a)).toBeDefined();
79
+ expect(dependenciesMap.get(b)).toBeDefined();
80
+ // topological batches
81
+ const batches = buildTopologicalOrderBatches(workspaces, dependenciesMap);
82
+ // flatten names excluding root
83
+ const nonRoot = batches
84
+ .flat()
85
+ .filter((w) => !w.isRoot)
86
+ .map((w) => w.name);
87
+ // c should come before b which comes before a
88
+ expect(nonRoot.indexOf("@ex/c")).toBeLessThan(nonRoot.indexOf("@ex/b"));
89
+ expect(nonRoot.indexOf("@ex/b")).toBeLessThan(nonRoot.indexOf("@ex/a"));
90
+ });
91
+
92
+ it("throws on circular dependency", async () => {
93
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "pob-"));
94
+ await fs.mkdir(path.join(tmp, "packages"), { recursive: true });
95
+ const rootPkg = {
96
+ name: "root",
97
+ workspaces: ["packages/*"],
98
+ };
99
+ await fs.writeFile(
100
+ path.join(tmp, "package.json"),
101
+ JSON.stringify(rootPkg, null, 2),
102
+ );
103
+
104
+ // package x depends on y
105
+ const xDir = path.join(tmp, "packages/x");
106
+ await fs.mkdir(xDir, { recursive: true });
107
+ await fs.writeFile(
108
+ path.join(xDir, "package.json"),
109
+ JSON.stringify(
110
+ { name: "@ex/x", dependencies: { "@ex/y": "1.0.0" } },
111
+ null,
112
+ 2,
113
+ ),
114
+ );
115
+
116
+ // package y depends on x
117
+ const yDir = path.join(tmp, "packages/y");
118
+ await fs.mkdir(yDir, { recursive: true });
119
+ await fs.writeFile(
120
+ path.join(yDir, "package.json"),
121
+ JSON.stringify(
122
+ { name: "@ex/y", dependencies: { "@ex/x": "1.0.0" } },
123
+ null,
124
+ 2,
125
+ ),
126
+ );
127
+
128
+ const workspaces = await discoverWorkspaces(tmp);
129
+ const dependenciesMap = buildDependenciesMaps(workspaces);
130
+ expect(() =>
131
+ buildTopologicalOrderBatches(workspaces, dependenciesMap),
132
+ ).toThrow();
133
+ });
134
+ });
@@ -0,0 +1,167 @@
1
+ import fs from "node:fs";
2
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
3
+ import { glob } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import type { PackageJson } from "type-fest";
6
+ import {
7
+ PackageDependencyDescriptorUtils,
8
+ PackageDescriptorNameUtils,
9
+ } from "./packageDependencyDescriptorUtils";
10
+ import type { PackageDependencyDescriptor } from "./packageDependencyDescriptorUtils.ts";
11
+
12
+ export interface Workspace {
13
+ name?: string;
14
+ location: string;
15
+ manifest: { raw: PackageJson };
16
+ relativeCwd: { toString: () => string };
17
+ isRoot?: boolean;
18
+ }
19
+
20
+ export const getWorkspaceName = (workspace: Workspace): string => {
21
+ if (workspace?.manifest?.raw?.name) return workspace.manifest.raw.name;
22
+ return path.basename(workspace.location) || "unnamed-workspace";
23
+ };
24
+
25
+ export type DependencyType =
26
+ | "dependencies"
27
+ | "devDependencies"
28
+ | "peerDependencies";
29
+
30
+ type WorkspacesDependenciesMap = Map<
31
+ Workspace,
32
+ [Workspace, DependencyType, PackageDependencyDescriptor][]
33
+ >;
34
+
35
+ export const discoverWorkspaces = async (
36
+ rootPath: string,
37
+ ): Promise<Workspace[]> => {
38
+ const rootPackageJSONPath = path.join(rootPath, "package.json");
39
+ const rootPkg: PackageJson = JSON.parse(fs.readFileSync(rootPackageJSONPath));
40
+
41
+ let workspaceGlobs: string[] = [];
42
+ if (Array.isArray((rootPkg as any).workspaces)) {
43
+ workspaceGlobs = (rootPkg as any).workspaces;
44
+ } else if (typeof (rootPkg as any).workspaces === "object") {
45
+ workspaceGlobs = (rootPkg as any).workspaces.packages || [];
46
+ }
47
+
48
+ // include root as workspace
49
+ const workspaces: Workspace[] = [
50
+ {
51
+ name: rootPkg.name,
52
+ location: ".",
53
+ manifest: { raw: rootPkg },
54
+ relativeCwd: { toString: () => "." },
55
+ isRoot: true,
56
+ } as Workspace,
57
+ ];
58
+
59
+ const patternPackageJsons = workspaceGlobs.map((globPattern) =>
60
+ path.join(globPattern, "package.json"),
61
+ );
62
+ const found = new Set<string>();
63
+ for (const pattern of patternPackageJsons) {
64
+ for await (const match of glob(pattern, { cwd: rootPath, nodir: true })) {
65
+ if (found.has(match)) continue;
66
+ found.add(match);
67
+ const filePath = path.join(rootPath, match);
68
+ const content = JSON.parse(fs.readFileSync(filePath));
69
+ const dir = path.dirname(match);
70
+ workspaces.push({
71
+ name: content.name,
72
+ location: dir || ".",
73
+ manifest: { raw: content },
74
+ relativeCwd: { toString: () => dir || "." },
75
+ });
76
+ }
77
+ }
78
+
79
+ return workspaces;
80
+ };
81
+
82
+ export const buildDependenciesMaps = (
83
+ workspaces: Workspace[],
84
+ ): WorkspacesDependenciesMap => {
85
+ const dependenciesMap: WorkspacesDependenciesMap = new Map();
86
+
87
+ const workspacesByName = new Map<string, Workspace>(
88
+ workspaces.filter((w) => !!w.name).map((w) => [w.name, w]),
89
+ );
90
+
91
+ const dependencyTypes: DependencyType[] = [
92
+ "dependencies",
93
+ "devDependencies",
94
+ "peerDependencies",
95
+ ];
96
+
97
+ for (const dependent of workspaces) {
98
+ for (const set of dependencyTypes) {
99
+ const deps = (dependent.manifest.raw as any)[set] || {};
100
+ for (const [dependencyKey, dependencyValue] of Object.entries(deps)) {
101
+ if (!dependencyValue) continue;
102
+ const descriptor = PackageDependencyDescriptorUtils.parse(
103
+ dependencyKey,
104
+ String(dependencyValue),
105
+ );
106
+ const workspace = workspacesByName.get(descriptor.npmName);
107
+ if (!workspace) continue;
108
+
109
+ const entries = dependenciesMap.get(dependent) || [];
110
+ entries.push([workspace, set, descriptor]);
111
+ dependenciesMap.set(dependent, entries);
112
+ }
113
+ }
114
+ }
115
+
116
+ return dependenciesMap;
117
+ };
118
+
119
+ export const buildTopologicalOrderBatches = (
120
+ workspaces: Workspace[],
121
+ dependenciesMap: WorkspacesDependenciesMap,
122
+ ): Workspace[][] => {
123
+ const batches: Workspace[][] = [];
124
+
125
+ const added = new Set<Workspace>();
126
+ const toAdd = new Set<Workspace>(workspaces);
127
+
128
+ while (toAdd.size > 0) {
129
+ const batch = new Set<Workspace>();
130
+ for (const workspace of toAdd) {
131
+ // skip root workspace until the end when there are others
132
+ if (workspace.isRoot && toAdd.size > 1) continue;
133
+
134
+ const dependencies = dependenciesMap.get(workspace);
135
+ if (!dependencies || dependencies.every((w) => added.has(w[0]))) {
136
+ batch.add(workspace);
137
+ }
138
+ }
139
+
140
+ for (const workspace of batch) {
141
+ added.add(workspace);
142
+ toAdd.delete(workspace);
143
+ }
144
+
145
+ if (batch.size === 0) {
146
+ throw new Error("Circular dependency detected");
147
+ }
148
+ batches.push([...batch]);
149
+ }
150
+
151
+ return batches;
152
+ };
153
+
154
+ export const buildDependentsMaps = (
155
+ workspaces: Workspace[],
156
+ ): WorkspacesDependenciesMap => {
157
+ const dependentsMap: WorkspacesDependenciesMap = new Map();
158
+ const dependenciesMap = buildDependenciesMaps(workspaces);
159
+ for (const [dependent, relations] of dependenciesMap) {
160
+ for (const [workspace, set, descriptor] of relations) {
161
+ const cmd = dependentsMap.get(workspace) || [];
162
+ cmd.push([dependent, set, descriptor]);
163
+ dependentsMap.set(workspace, cmd);
164
+ }
165
+ }
166
+ return dependentsMap;
167
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pob",
3
- "version": "29.9.0",
3
+ "version": "31.0.1",
4
4
  "description": "Pile of bones, library generator with git/babel/typescript/typedoc/readme/jest",
5
5
  "keywords": [
6
6
  "skeleton"
@@ -36,7 +36,7 @@
36
36
  "build:definitions": "tsc -p tsconfig.json",
37
37
  "format": "prettier --write",
38
38
  "lint": "yarn run lint:eslint",
39
- "lint:eslint": "yarn ../.. run eslint --quiet packages/pob"
39
+ "lint:eslint": "yarn ../.. run eslint --quiet 'packages/pob'"
40
40
  },
41
41
  "pob": {
42
42
  "typescript": "check-only"
@@ -46,20 +46,17 @@
46
46
  "@pob/eslint-config": "62.0.0",
47
47
  "@pob/eslint-config-typescript": "62.0.0",
48
48
  "@pob/eslint-config-typescript-react": "62.0.0",
49
- "@pob/sort-object": "10.1.0",
50
- "@pob/sort-pkg": "12.1.0",
49
+ "@pob/sort-object": "10.1.1",
50
+ "@pob/sort-pkg": "12.1.1",
51
51
  "@prettier/sync": "0.6.1",
52
52
  "@types/inquirer": "9.0.9",
53
- "@yarnpkg/cli": "4.11.0",
54
- "@yarnpkg/core": "4.5.0",
55
- "@yarnpkg/fslib": "3.1.4",
56
53
  "@yeoman/adapter": "3.1.0",
57
54
  "@yeoman/types": "1.8.0",
58
55
  "eslint": "9.39.1",
59
56
  "findup-sync": "^5.0.0",
60
57
  "git-remote-url": "^1.0.1",
61
58
  "github-username": "^9.0.0",
62
- "js-yaml": "^4.1.0",
59
+ "js-yaml": "^4.1.1",
63
60
  "json5": "^2.2.3",
64
61
  "lodash.camelcase": "^4.3.0",
65
62
  "lodash.kebabcase": "^4.1.1",
@@ -67,17 +64,16 @@
67
64
  "mem-fs-editor": "11.1.4",
68
65
  "minimist": "1.2.8",
69
66
  "parse-author": "2.0.0",
70
- "pob-dependencies": "20.6.0",
67
+ "pob-dependencies": "21.0.1",
71
68
  "prettier": "3.6.2",
72
69
  "semver": "7.7.3",
73
70
  "typescript": "5.9.3",
74
71
  "validate-npm-package-name": "^6.0.2",
75
- "yarn-workspace-utils": "9.5.0",
76
72
  "yeoman-environment": "5.0.0",
77
73
  "yeoman-generator": "7.5.1"
78
74
  },
79
75
  "devDependencies": {
80
- "@pob/root": "19.8.0",
76
+ "@pob/root": "20.0.2",
81
77
  "@types/node": "22.19.1"
82
78
  }
83
79
  }
@@ -1,5 +0,0 @@
1
- "use strict";
2
-
3
- const createLintStagedConfig = require("@pob/root/createLintStagedConfig.cjs");
4
-
5
- module.exports = createLintStagedConfig();