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.
Files changed (28) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/lib/generators/app/PobAppGenerator.js +10 -6
  3. package/lib/generators/app/ignorePaths.js +2 -1
  4. package/lib/generators/common/format-lint/CommonLintGenerator.js +18 -14
  5. package/lib/generators/common/format-lint/templates/prettierignore.ejs +1 -4
  6. package/lib/generators/common/husky/CommonHuskyGenerator.js +8 -11
  7. package/lib/generators/common/old-dependencies/CommonRemoveOldDependenciesGenerator.js +1 -0
  8. package/lib/generators/common/release/CommonReleaseGenerator.js +0 -7
  9. package/lib/generators/common/release/templates/workflow-release.yml.ejs +8 -12
  10. package/lib/generators/common/testing/CommonTestingGenerator.js +17 -85
  11. package/lib/generators/common/typescript/CommonTypescriptGenerator.js +1 -1
  12. package/lib/generators/core/ci/CoreCIGenerator.js +0 -36
  13. package/lib/generators/core/ci/templates/github-action-documentation-workflow.yml.ejs +6 -3
  14. package/lib/generators/core/ci/templates/github-action-push-workflow-split.yml.ejs +9 -5
  15. package/lib/generators/core/ci/templates/github-action-push-workflow.yml.ejs +8 -4
  16. package/lib/generators/core/yarn/CoreYarnGenerator.js +2 -2
  17. package/lib/generators/lib/PobLibGenerator.js +1 -4
  18. package/lib/generators/monorepo/PobMonorepoGenerator.js +41 -30
  19. package/lib/generators/monorepo/lerna/MonorepoLernaGenerator.js +12 -25
  20. package/lib/generators/monorepo/workspaces/MonorepoWorkspacesGenerator.js +6 -10
  21. package/lib/generators/pob/PobBaseGenerator.js +1 -1
  22. package/lib/utils/packageDependencyDescriptorUtils.js +52 -0
  23. package/lib/utils/packageDependencyDescriptorUtils.ts.backup +79 -0
  24. package/lib/utils/workspaceUtils.js +131 -0
  25. package/lib/utils/workspaceUtils.test.js +134 -0
  26. package/lib/utils/workspaceUtils.ts.backup +167 -0
  27. package/package.json +5 -9
  28. 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 { 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",
@@ -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 yarnProject = await createYarnProject(this.destinationPath());
117
+ const workspaces = await discoverWorkspaces(this.destinationPath());
122
118
  const batches = buildTopologicalOrderBatches(
123
- yarnProject,
124
- buildDependenciesMaps(yarnProject),
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 === yarnProject.topLevelWorkspace) {
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: gitignorePaths.join("\n"),
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 (isYarnVersionEnabled) {
134
- if (pkg.version === "0.0.0" && lernaConfig && lernaConfig.version) {
135
- if (lernaConfig.version === "independent") {
136
- delete pkg.version;
137
- } else {
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
- packageUtils.addOrRemoveScripts(
155
- pkg,
156
- this.options.packageManager === "yarn" && !isYarnVersionEnabled,
157
- {
158
- version:
159
- "YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn && git add yarn.lock",
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
- packageUtils.addOrRemoveScripts(
115
- pkg,
116
- this.options.packageManager === "yarn" && !isYarnVersionEnabled,
117
- {
118
- version:
119
- "YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn && git add yarn.lock",
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 lerna package: ");
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
+ });