@vnejs/monorepo 0.2.1 → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vnejs/monorepo",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Monorepo tooling for @vnejs",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -97,7 +97,7 @@ program
97
97
 
98
98
  program
99
99
  .command("run-changed")
100
- .description("Run npm script in git-changed workspace packages (sequential by default)")
100
+ .description("Run npm script in git-changed workspace packages (dependency layers; sequential by default)")
101
101
  .option("--script <script>", "npm script name from package.json")
102
102
  .option("--ref <ref>", "git ref to compare against", "HEAD")
103
103
  .option("--dry-run", "list changed packages without running the script", false)
@@ -1,30 +1,39 @@
1
1
  const path = require("path");
2
2
 
3
3
  const { getChangedPackages } = require("../changed-packages");
4
- const resolveGraph = require("../workspace/resolve-graph");
4
+ const listWorkspacePackages = require("../workspace/list-packages");
5
+ const { buildDependencyGraph, restrictGraph } = require("../workspace/build-graph");
6
+ const { topoSortLayers } = require("../workspace/topo-sort");
5
7
  const { runAsync } = require("../publish/utils");
6
8
  const confirm = require("../utils/confirm");
7
9
  const { formatPackageLine } = require("../utils/format-package");
8
10
  const schedulePackages = require("./schedule");
9
11
 
10
- const sortByGraphOrder = (root, changedPackages, script) => {
11
- const changedNames = new Set(changedPackages.map((pkg) => pkg.name));
12
- const { order } = resolveGraph({ cwd: root, script });
12
+ const groupChangedByGraph = (root, changedPackages) => {
13
+ const { packages: all } = listWorkspacePackages({ cwd: root });
14
+ const { graph } = buildDependencyGraph(all);
15
+ const byChangedName = new Map(changedPackages.map((pkg) => [pkg.name, pkg]));
16
+ const changedNames = [...byChangedName.keys()];
17
+ const layers = topoSortLayers(restrictGraph(graph, changedNames), changedNames).map((layer) => layer.map((name) => byChangedName.get(name)));
13
18
 
14
- const sorted = order.filter((pkg) => changedNames.has(pkg.name));
19
+ return { packages: layers.flat(), layers };
20
+ };
15
21
 
16
- const sortedNames = new Set(sorted.map((pkg) => pkg.name));
17
- const rest = changedPackages.filter((pkg) => !sortedNames.has(pkg.name));
22
+ const printPackages = (layers) => {
23
+ const total = layers.reduce((sum, layer) => sum + layer.length, 0);
24
+ let index = 0;
18
25
 
19
- return [...sorted, ...rest];
20
- };
26
+ layers.forEach((layer, layerIndex) => {
27
+ console.log(`\n=== Layer ${layerIndex + 1}/${layers.length} (${layer.length} packages) ===`);
21
28
 
22
- const printPackages = (packages) => {
23
- packages.forEach((pkg, index) => {
24
- console.log(formatPackageLine(pkg, { tag: `[${index + 1}/${packages.length}]` }));
29
+ layer.forEach((pkg) => {
30
+ index += 1;
31
+ console.log(formatPackageLine(pkg, { indent: " ", tag: `[${index}/${total}]` }));
32
+ });
25
33
  });
34
+
26
35
  console.log("");
27
- console.log(`Total: ${packages.length}`);
36
+ console.log(`Total: ${total}, layers: ${layers.length}`);
28
37
  };
29
38
 
30
39
  const parseConcurrency = (value) => {
@@ -71,16 +80,16 @@ module.exports = async (options = {}) => {
71
80
  return;
72
81
  }
73
82
 
74
- const packages = sortByGraphOrder(root, changedPackages, script);
83
+ const { packages, layers } = groupChangedByGraph(root, changedPackages);
75
84
 
76
85
  if (options.dryRun) {
77
- printPackages(packages);
86
+ printPackages(layers);
78
87
  return;
79
88
  }
80
89
 
81
90
  if (options.confirm) {
82
91
  console.log(`\nThe following packages will run "${script}":\n`);
83
- printPackages(packages);
92
+ printPackages(layers);
84
93
 
85
94
  const confirmed = await confirm(`Continue? [y/N] `);
86
95
 
@@ -92,8 +101,11 @@ module.exports = async (options = {}) => {
92
101
  console.log("");
93
102
  }
94
103
 
104
+ console.log(`\n=== Run-changed: ${packages.length} packages, ${layers.length} layers ===`);
105
+
95
106
  await schedulePackages({
96
107
  packages,
108
+ layers,
97
109
  concurrency,
98
110
  parallelMs,
99
111
  firstSerial,
@@ -2,14 +2,26 @@ const pLimit = require("p-limit").default;
2
2
 
3
3
  const PARALLEL_WINDOW_ENDED = "Parallel window ended; concurrency set to 1";
4
4
 
5
- const schedulePackages = async ({
6
- packages,
7
- runFn,
8
- concurrency = 1,
9
- parallelMs,
10
- firstSerial = false,
11
- log = console.log,
12
- }) => {
5
+ const toGroups = (packages, layers) => {
6
+ const source = layers ?? (packages?.length ? [packages] : []);
7
+
8
+ return source.map((layer) => [...layer]).filter((layer) => layer.length);
9
+ };
10
+
11
+ const rejectFailed = (results, log) => {
12
+ const failed = results.filter((result) => result.status === "rejected");
13
+
14
+ if (!failed.length) return;
15
+
16
+ for (const result of failed) {
17
+ const reason = result.reason;
18
+ log(reason?.stack ?? String(reason));
19
+ }
20
+
21
+ throw new Error(`${failed.length} package script(s) failed.`);
22
+ };
23
+
24
+ const schedulePackages = async ({ packages = [], layers, runFn, concurrency = 1, parallelMs, firstSerial = false, log = console.log }) => {
13
25
  if (!Number.isInteger(concurrency) || concurrency < 1) {
14
26
  throw new Error("concurrency must be an integer >= 1");
15
27
  }
@@ -18,18 +30,20 @@ const schedulePackages = async ({
18
30
  throw new Error("parallelMs must be a number >= 0");
19
31
  }
20
32
 
21
- if (!packages.length) return;
33
+ const groups = toGroups(packages, layers);
34
+
35
+ if (!groups.length) return;
22
36
 
23
37
  let startIndex = 0;
24
38
 
25
39
  if (firstSerial) {
26
- await runFn(packages[0], 0);
40
+ await runFn(groups[0][0], 0);
27
41
  startIndex = 1;
28
- }
29
-
30
- const remaining = packages.slice(startIndex);
42
+ groups[0] = groups[0].slice(1);
31
43
 
32
- if (!remaining.length) return;
44
+ if (!groups[0].length) groups.shift();
45
+ if (!groups.length) return;
46
+ }
33
47
 
34
48
  const limit = pLimit(concurrency);
35
49
  let timer;
@@ -42,20 +56,13 @@ const schedulePackages = async ({
42
56
  }
43
57
 
44
58
  try {
45
- const results = await Promise.allSettled(
46
- remaining.map((pkg, offset) => limit(() => runFn(pkg, startIndex + offset))),
47
- );
48
-
49
- const failed = results.filter((result) => result.status === "rejected");
59
+ for (const layer of groups) {
60
+ const layerStart = startIndex;
61
+ const results = await Promise.allSettled(layer.map((pkg, offset) => limit(() => runFn(pkg, layerStart + offset))));
50
62
 
51
- if (!failed.length) return;
52
-
53
- for (const result of failed) {
54
- const reason = result.reason;
55
- log(reason?.stack ?? String(reason));
63
+ startIndex += layer.length;
64
+ rejectFailed(results, log);
56
65
  }
57
-
58
- throw new Error(`${failed.length} package script(s) failed.`);
59
66
  } finally {
60
67
  if (timer) clearTimeout(timer);
61
68
  }
@@ -38,6 +38,18 @@ const flush = async () => {
38
38
  await Promise.resolve();
39
39
  };
40
40
 
41
+ const waitUntilStarted = async (started, expected) => {
42
+ for (let i = 0; i < 20; i++) {
43
+ if (started.length >= expected.length && expected.every((name, index) => started[index] === name)) {
44
+ return;
45
+ }
46
+
47
+ await flush();
48
+ }
49
+
50
+ expect(started).toEqual(expected);
51
+ };
52
+
41
53
  describe("schedulePackages", () => {
42
54
  afterEach(() => {
43
55
  vi.useRealTimers();
@@ -141,6 +153,70 @@ describe("schedulePackages", () => {
141
153
  await done;
142
154
  });
143
155
 
156
+ it("does not start a later layer until the previous layer finishes", async () => {
157
+ const { started, runFn, resolve } = createTrackedRunner();
158
+ const done = schedulePackages({
159
+ layers: [packages("a", "b"), packages("c")],
160
+ runFn,
161
+ concurrency: 2,
162
+ });
163
+
164
+ await flush();
165
+ expect(started).toEqual(["a", "b"]);
166
+
167
+ resolve("a");
168
+ await flush();
169
+ expect(started).toEqual(["a", "b"]);
170
+
171
+ resolve("b");
172
+ await waitUntilStarted(started, ["a", "b", "c"]);
173
+
174
+ resolve("c");
175
+ await done;
176
+ });
177
+
178
+ it("does not start the next layer when the current layer fails", async () => {
179
+ const { started, runFn, resolve, reject } = createTrackedRunner();
180
+ const logs = [];
181
+ const done = schedulePackages({
182
+ layers: [packages("a", "b"), packages("c")],
183
+ runFn,
184
+ concurrency: 2,
185
+ log: (message) => logs.push(message),
186
+ });
187
+
188
+ await flush();
189
+ reject("a", new Error("a failed"));
190
+ resolve("b");
191
+
192
+ await expect(done).rejects.toThrow("1 package script(s) failed.");
193
+ expect(started).toEqual(["a", "b"]);
194
+ expect(logs.some((message) => String(message).includes("a failed"))).toBe(true);
195
+ });
196
+
197
+ it("runs firstSerial from the first layer, then the rest of that layer", async () => {
198
+ const { started, runFn, resolve } = createTrackedRunner();
199
+ const done = schedulePackages({
200
+ layers: [packages("a", "b"), packages("c")],
201
+ runFn,
202
+ concurrency: 2,
203
+ firstSerial: true,
204
+ });
205
+
206
+ await flush();
207
+ expect(started).toEqual(["a"]);
208
+
209
+ resolve("a");
210
+ await flush();
211
+ expect(started).toEqual(["a", "b"]);
212
+
213
+ resolve("b");
214
+ await waitUntilStarted(started, ["a", "b", "c"]);
215
+
216
+ resolve("c");
217
+ await done;
218
+ });
219
+
144
220
  it("waits for in-flight failures and then rejects", async () => {
145
221
  const { started, runFn, resolve, reject } = createTrackedRunner();
146
222
  const logs = [];