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.
- package/CHANGELOG.md +61 -0
- package/lib/generators/common/format-lint/CommonLintGenerator.js +2 -3
- package/lib/generators/common/format-lint/templates/prettierignore.ejs +1 -1
- package/lib/generators/common/husky/CommonHuskyGenerator.js +8 -11
- package/lib/generators/common/old-dependencies/CommonRemoveOldDependenciesGenerator.js +1 -0
- package/lib/generators/common/release/CommonReleaseGenerator.js +2 -7
- package/lib/generators/common/release/templates/workflow-release.yml.ejs +20 -13
- package/lib/generators/common/testing/CommonTestingGenerator.js +19 -74
- package/lib/generators/core/bun/CoreBunGenerator.js +6 -0
- package/lib/generators/core/bun/templates/bunfig.toml.ejs +3 -0
- package/lib/generators/core/ci/templates/github-action-documentation-workflow.yml.ejs +6 -3
- package/lib/generators/core/ci/templates/github-action-push-workflow-split.yml.ejs +9 -5
- package/lib/generators/core/ci/templates/github-action-push-workflow.yml.ejs +8 -4
- package/lib/generators/core/yarn/CoreYarnGenerator.js +11 -3
- package/lib/generators/monorepo/PobMonorepoGenerator.js +12 -25
- package/lib/utils/execUtils.js +3 -0
- package/lib/utils/packageDependencyDescriptorUtils.js +52 -0
- package/lib/utils/packageDependencyDescriptorUtils.ts.backup +79 -0
- package/lib/utils/workspaceUtils.js +131 -0
- package/lib/utils/workspaceUtils.test.js +134 -0
- package/lib/utils/workspaceUtils.ts.backup +167 -0
- package/package.json +7 -11
- 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 && " : ""
|
|
@@ -69,17 +69,14 @@ export default class CommonHuskyGenerator extends Generator {
|
|
|
69
69
|
// '@commitlint/config-lerna-scopes': '6.1.3',
|
|
70
70
|
// });
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
this.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 = {
|
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 "$
|
|
94
|
-
echo "Missing env variable
|
|
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: $
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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);
|
|
@@ -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.
|
|
145
|
+
lt(pkg.packageManager.slice("yarn@".length), "4.12.0")
|
|
146
146
|
) {
|
|
147
|
-
pkg.packageManager = "yarn@4.
|
|
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 =
|
|
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
|
|
5
|
-
import
|
|
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 "
|
|
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
|
|
117
|
+
const workspaces = await discoverWorkspaces(this.destinationPath());
|
|
133
118
|
const batches = buildTopologicalOrderBatches(
|
|
134
|
-
|
|
135
|
-
buildDependenciesMaps(
|
|
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
|
|
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 &&
|
|
272
|
-
|
|
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,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": "
|
|
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.
|
|
50
|
-
"@pob/sort-pkg": "12.1.
|
|
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.
|
|
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": "
|
|
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": "
|
|
76
|
+
"@pob/root": "20.0.2",
|
|
81
77
|
"@types/node": "22.19.1"
|
|
82
78
|
}
|
|
83
79
|
}
|