pob 29.8.0 → 30.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/lib/generators/app/PobAppGenerator.js +10 -6
- package/lib/generators/app/ignorePaths.js +2 -1
- package/lib/generators/common/format-lint/CommonLintGenerator.js +18 -14
- package/lib/generators/common/format-lint/templates/prettierignore.ejs +1 -4
- 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 +0 -7
- package/lib/generators/common/release/templates/workflow-release.yml.ejs +8 -12
- package/lib/generators/common/testing/CommonTestingGenerator.js +17 -85
- package/lib/generators/common/typescript/CommonTypescriptGenerator.js +1 -1
- package/lib/generators/core/ci/CoreCIGenerator.js +0 -36
- 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 +2 -2
- package/lib/generators/lib/PobLibGenerator.js +1 -4
- package/lib/generators/monorepo/PobMonorepoGenerator.js +41 -30
- package/lib/generators/monorepo/lerna/MonorepoLernaGenerator.js +12 -25
- package/lib/generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js +6 -10
- package/lib/generators/pob/PobBaseGenerator.js +1 -1
- 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 +5 -9
- package/lib/generators/common/husky/templates/lint-staged.config.cjs.txt +0 -5
|
@@ -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",
|
|
@@ -60,6 +45,17 @@ const hasBuild = (packages, configs) =>
|
|
|
60
45
|
),
|
|
61
46
|
);
|
|
62
47
|
|
|
48
|
+
const hasData = (packages, configs) =>
|
|
49
|
+
configs.some(
|
|
50
|
+
(config, index) =>
|
|
51
|
+
!!(
|
|
52
|
+
config &&
|
|
53
|
+
config.project &&
|
|
54
|
+
config.project.type === "app" &&
|
|
55
|
+
config.app.type === "alp-node"
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
63
59
|
const hasTamagui = (packages, configs) =>
|
|
64
60
|
packages.some(
|
|
65
61
|
(pkg) =>
|
|
@@ -118,10 +114,10 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
118
114
|
}
|
|
119
115
|
|
|
120
116
|
async initializing() {
|
|
121
|
-
const
|
|
117
|
+
const workspaces = await discoverWorkspaces(this.destinationPath());
|
|
122
118
|
const batches = buildTopologicalOrderBatches(
|
|
123
|
-
|
|
124
|
-
buildDependenciesMaps(
|
|
119
|
+
workspaces,
|
|
120
|
+
buildDependenciesMaps(workspaces),
|
|
125
121
|
);
|
|
126
122
|
|
|
127
123
|
this.packages = [];
|
|
@@ -134,7 +130,7 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
134
130
|
);
|
|
135
131
|
|
|
136
132
|
batch.forEach((workspace) => {
|
|
137
|
-
if (workspace
|
|
133
|
+
if (workspace.isRoot) {
|
|
138
134
|
return;
|
|
139
135
|
}
|
|
140
136
|
this.packages.push(workspace.manifest.raw);
|
|
@@ -180,6 +176,13 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
180
176
|
when: (answers) => answers.ci,
|
|
181
177
|
default: config ? config.testing : true,
|
|
182
178
|
},
|
|
179
|
+
{
|
|
180
|
+
type: "confirm",
|
|
181
|
+
name: "e2eTesting",
|
|
182
|
+
message: "Would you like e2e testing ?",
|
|
183
|
+
when: (answers) => answers.ci,
|
|
184
|
+
default: config ? config.e2eTesting : true,
|
|
185
|
+
},
|
|
183
186
|
{
|
|
184
187
|
type: "confirm",
|
|
185
188
|
name: "codecov",
|
|
@@ -229,8 +232,6 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
229
232
|
|
|
230
233
|
this.composeWith("pob:common:husky", {});
|
|
231
234
|
|
|
232
|
-
const isYarnVersionEnabled = this.pobLernaConfig.ci;
|
|
233
|
-
|
|
234
235
|
const splitCIJobs = this.packageNames.length > 8;
|
|
235
236
|
|
|
236
237
|
this.composeWith("pob:common:testing", {
|
|
@@ -238,8 +239,6 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
238
239
|
enable: this.pobLernaConfig.testing,
|
|
239
240
|
runner: this.pobLernaConfig.testRunner || "jest",
|
|
240
241
|
disableYarnGitCache: this.options.disableYarnGitCache,
|
|
241
|
-
enableReleasePlease: false,
|
|
242
|
-
enableYarnVersion: isYarnVersionEnabled,
|
|
243
242
|
testing: this.pobLernaConfig.testing,
|
|
244
243
|
e2eTesting: this.pobLernaConfig.e2eTesting,
|
|
245
244
|
build: this.pobLernaConfig.typescript === true,
|
|
@@ -253,6 +252,13 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
253
252
|
splitCIJobs,
|
|
254
253
|
});
|
|
255
254
|
|
|
255
|
+
const rootIgnorePaths = [
|
|
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/`,
|
|
260
|
+
].filter(Boolean);
|
|
261
|
+
|
|
256
262
|
const gitignorePaths = [
|
|
257
263
|
hasTamagui(this.packages, this.packageConfigs) && ".tamagui",
|
|
258
264
|
].filter(Boolean);
|
|
@@ -260,6 +266,7 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
260
266
|
this.composeWith("pob:common:format-lint", {
|
|
261
267
|
monorepo: true,
|
|
262
268
|
documentation: this.pobLernaConfig.documentation,
|
|
269
|
+
storybook: pkg?.devDependencies?.storybook,
|
|
263
270
|
typescript: this.pobLernaConfig.typescript,
|
|
264
271
|
build: this.pobLernaConfig.typescript === true,
|
|
265
272
|
testing: this.pobLernaConfig.testing,
|
|
@@ -271,10 +278,11 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
271
278
|
...gitignorePaths.map((path) => `/${path}`),
|
|
272
279
|
hasDist(this.packages, this.packageConfigs) && "/dist",
|
|
273
280
|
hasBuild(this.packages, this.packageConfigs) && "/build",
|
|
281
|
+
hasData(this.packages, this.packageConfigs) && "/data",
|
|
274
282
|
]
|
|
275
283
|
.filter(Boolean)
|
|
276
284
|
.join("\n"),
|
|
277
|
-
rootIgnorePaths:
|
|
285
|
+
rootIgnorePaths: rootIgnorePaths.join("\n"),
|
|
278
286
|
});
|
|
279
287
|
|
|
280
288
|
this.composeWith("pob:lib:doc", {
|
|
@@ -304,7 +312,11 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
304
312
|
documentation: this.pobLernaConfig.documentation,
|
|
305
313
|
testing: this.pobLernaConfig.testing,
|
|
306
314
|
// TODO add workspaces paths like we do in format-lint
|
|
307
|
-
paths:
|
|
315
|
+
paths: [
|
|
316
|
+
// TODO remove gitignorePaths
|
|
317
|
+
...gitignorePaths,
|
|
318
|
+
...rootIgnorePaths,
|
|
319
|
+
].join("\n"),
|
|
308
320
|
// todo: fix this using workspaces
|
|
309
321
|
// buildDirectory: this.pobLernaConfig.typescript ? `/*/build` : "",
|
|
310
322
|
});
|
|
@@ -317,7 +329,6 @@ export default class PobMonorepoGenerator extends Generator {
|
|
|
317
329
|
enablePublish: !this.options.isAppProject,
|
|
318
330
|
withBabel: this.pobLernaConfig.typescript,
|
|
319
331
|
isMonorepo: true,
|
|
320
|
-
enableYarnVersion: isYarnVersionEnabled,
|
|
321
332
|
ci: this.pobLernaConfig.ci,
|
|
322
333
|
disableYarnGitCache: this.options.disableYarnGitCache,
|
|
323
334
|
updateOnly: this.options.updateOnly,
|
|
@@ -100,9 +100,6 @@ export default class MonorepoLernaGenerator extends Generator {
|
|
|
100
100
|
packageUtils.removeDependencies(pkg, ["lerna"]);
|
|
101
101
|
packageUtils.removeDevDependencies(pkg, ["lerna"]);
|
|
102
102
|
|
|
103
|
-
// TODO remove lerna completely
|
|
104
|
-
const isYarnVersionEnabled = true;
|
|
105
|
-
|
|
106
103
|
const getPackagePobConfig = (config) => ({
|
|
107
104
|
babelEnvs: [],
|
|
108
105
|
...(config && config.pob),
|
|
@@ -130,35 +127,25 @@ export default class MonorepoLernaGenerator extends Generator {
|
|
|
130
127
|
lernaConfig.command.publish.ignoreChanges.push("**/tsconfig.json");
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
if (
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
pkg.version = lernaConfig.version;
|
|
139
|
-
}
|
|
130
|
+
if (pkg.version === "0.0.0" && lernaConfig && lernaConfig.version) {
|
|
131
|
+
if (lernaConfig.version === "independent") {
|
|
132
|
+
delete pkg.version;
|
|
133
|
+
} else {
|
|
134
|
+
pkg.version = lernaConfig.version;
|
|
140
135
|
}
|
|
141
|
-
this.fs.delete(this.destinationPath("lerna.json"));
|
|
142
|
-
} else {
|
|
143
|
-
writeAndFormatJson(
|
|
144
|
-
this.fs,
|
|
145
|
-
this.destinationPath("lerna.json"),
|
|
146
|
-
lernaConfig,
|
|
147
|
-
);
|
|
148
136
|
}
|
|
137
|
+
this.fs.delete(this.destinationPath("lerna.json"));
|
|
149
138
|
|
|
150
139
|
if (this.fs.exists(this.destinationPath("lerna-debug.log"))) {
|
|
151
140
|
this.fs.delete(this.destinationPath("lerna-debug.log"));
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
|
|
155
|
-
pkg
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
},
|
|
161
|
-
);
|
|
143
|
+
if (
|
|
144
|
+
pkg.scripts?.version ===
|
|
145
|
+
"YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn && git add yarn.lock"
|
|
146
|
+
) {
|
|
147
|
+
delete pkg.scripts.version;
|
|
148
|
+
}
|
|
162
149
|
|
|
163
150
|
this.fs.writeJSON(this.destinationPath("package.json"), pkg);
|
|
164
151
|
}
|
|
@@ -81,8 +81,6 @@ export default class MonorepoWorkspacesGenerator extends Generator {
|
|
|
81
81
|
delete pkg.engines.yarn;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const isYarnVersionEnabled = true;
|
|
85
|
-
|
|
86
84
|
if (pkg.name !== "pob-monorepo") {
|
|
87
85
|
packageUtils.addDevDependencies(pkg, ["repository-check-dirty"]);
|
|
88
86
|
}
|
|
@@ -111,14 +109,12 @@ export default class MonorepoWorkspacesGenerator extends Generator {
|
|
|
111
109
|
: "npm run lint:eslint --workspaces",
|
|
112
110
|
});
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
pkg
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
},
|
|
121
|
-
);
|
|
112
|
+
if (
|
|
113
|
+
pkg.scripts?.version ===
|
|
114
|
+
"YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn && git add yarn.lock"
|
|
115
|
+
) {
|
|
116
|
+
delete pkg.scripts.version;
|
|
117
|
+
}
|
|
122
118
|
|
|
123
119
|
if (this.options.isAppProject) {
|
|
124
120
|
packageUtils.addOrRemoveScripts(pkg, withBundler, {
|
|
@@ -291,7 +291,7 @@ export default class PobBaseGenerator extends Generator {
|
|
|
291
291
|
|
|
292
292
|
end() {
|
|
293
293
|
if (this.isMonorepo && !this.options.updateOnly) {
|
|
294
|
-
console.log("To create a new
|
|
294
|
+
console.log("To create a new monorepo package: ");
|
|
295
295
|
console.log(" pob add <packageName>");
|
|
296
296
|
}
|
|
297
297
|
}
|
|
@@ -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
|
+
});
|