@vnejs/monorepo 0.2.0 → 0.2.2

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,13 +1,13 @@
1
1
  {
2
2
  "name": "@vnejs/monorepo",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Monorepo tooling for @vnejs",
5
5
  "main": "src/index.js",
6
6
  "bin": {
7
7
  "@vnejs/monorepo": "./src/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1",
10
+ "test": "npx @vnejs/monorepo test",
11
11
  "publish:major": "npm version major && npm publish --access public",
12
12
  "publish:minor": "npm version minor && npm publish --access public",
13
13
  "publish:patch": "npm version patch && npm publish --access public"
@@ -15,8 +15,10 @@
15
15
  "author": "",
16
16
  "license": "ISC",
17
17
  "dependencies": {
18
+ "@vnejs/configs.vitest": "~0.2.0",
18
19
  "commander": "~15.0.0",
19
20
  "happy-dom": "~20.10.6",
21
+ "p-limit": "~7.3.0",
20
22
  "typescript": "7.0.2",
21
23
  "vitest": "~4.1.9"
22
24
  }
package/src/bump/index.js CHANGED
@@ -153,6 +153,9 @@ module.exports = async (version, options = {}) => {
153
153
  script: "publish:patch",
154
154
  ref: options.ref ?? "HEAD",
155
155
  dryRun,
156
+ concurrency: 8,
157
+ parallelMs: 270_000,
158
+ firstSerial: true,
156
159
  });
157
160
  }
158
161
 
@@ -8,17 +8,17 @@ module.exports = (options = {}) => {
8
8
 
9
9
  console.log("==> Removing node_modules...");
10
10
  run("rm -rf node_modules", root);
11
- run('find . -name node_modules -type d -prune -exec rm -rf {} +', root);
11
+ run("find . -name node_modules -type d -prune -exec rm -rf {} +", root);
12
12
 
13
13
  console.log("==> Removing Turbo cache...");
14
14
  run("rm -rf .turbo", root);
15
- run('find . -name .turbo -type d -prune -exec rm -rf {} +', root);
15
+ run("find . -name .turbo -type d -prune -exec rm -rf {} +", root);
16
16
 
17
17
  console.log("==> Removing dist folders...");
18
- run('find . -name dist -type d -prune -exec rm -rf {} +', root);
18
+ run("find . -name dist -type d -prune -exec rm -rf {} +", root);
19
19
 
20
20
  console.log("==> Removing package-lock files...");
21
- run('find . -name package-lock.json -type f -delete', root);
21
+ run("find . -name package-lock.json -type f -delete", root);
22
22
 
23
23
  console.log("\n==> Clear complete.");
24
24
  };
package/src/cli.js CHANGED
@@ -97,16 +97,23 @@ program
97
97
 
98
98
  program
99
99
  .command("run-changed")
100
- .description("Run npm script sequentially in git-changed workspace packages")
100
+ .description("Run npm script in git-changed workspace packages (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)
104
104
  .option("--confirm", "ask for confirmation before running the script", false)
105
- .action(runChangedAction);
105
+ .option("--concurrency <n>", "max parallel package scripts (p-limit)", "1")
106
+ .option("--parallel-ms <ms>", "after this window, drop concurrency to 1")
107
+ .option("--first-serial", "run the first package to completion before the parallel pool", false)
108
+ .action((options) =>
109
+ runChangedAction({
110
+ ...options,
111
+ concurrency: Number(options.concurrency),
112
+ parallelMs: options.parallelMs == null ? undefined : Number(options.parallelMs),
113
+ firstSerial: Boolean(options.firstSerial),
114
+ }),
115
+ );
106
116
 
107
- program
108
- .command("clear")
109
- .description("Clean node_modules, caches, dist, and lockfiles")
110
- .action(clearAction);
117
+ program.command("clear").description("Clean node_modules, caches, dist, and lockfiles").action(clearAction);
111
118
 
112
119
  program.parse();
@@ -69,9 +69,7 @@ module.exports = (options = {}) => {
69
69
  if (options.flat) {
70
70
  order.forEach((pkg, index) => {
71
71
  const meta = layerMetaByName.get(pkg.name);
72
- console.log(
73
- `${index + 1}\t${meta.layer}/${meta.layerTotal}\t${formatPackageRef(pkg)}\t${pkg.dir}`,
74
- );
72
+ console.log(`${index + 1}\t${meta.layer}/${meta.layerTotal}\t${formatPackageRef(pkg)}\t${pkg.dir}`);
75
73
  });
76
74
  } else {
77
75
  for (let layerIndex = 0; layerIndex < layers.length; layerIndex++) {
@@ -19,8 +19,7 @@ module.exports = (options = {}) => {
19
19
  // TS 7 no longer exports `typescript/bin/tsc`; resolve via package.json `bin`.
20
20
  const typescriptPkgPath = require.resolve("typescript/package.json");
21
21
  const typescriptPkg = require(typescriptPkgPath);
22
- const tscRel =
23
- typeof typescriptPkg.bin === "string" ? typescriptPkg.bin : typescriptPkg.bin.tsc;
22
+ const tscRel = typeof typescriptPkg.bin === "string" ? typescriptPkg.bin : typescriptPkg.bin.tsc;
24
23
  const tsc = path.resolve(path.dirname(typescriptPkgPath), tscRel);
25
24
  execSync(`node "${tsc}" -p "${config}"`, { cwd: pkgDir, stdio: "inherit" });
26
25
  };
@@ -1,4 +1,4 @@
1
- const { execSync } = require("child_process");
1
+ const { execSync, spawn } = require("child_process");
2
2
  const { existsSync, readFileSync, writeFileSync } = require("fs");
3
3
  const path = require("path");
4
4
 
@@ -28,6 +28,21 @@ const run = (command, pkgDir) => {
28
28
  execSync(command, { cwd: pkgDir, stdio: "inherit" });
29
29
  };
30
30
 
31
+ const runAsync = (command, pkgDir) =>
32
+ new Promise((resolve, reject) => {
33
+ const child = spawn(command, { cwd: pkgDir, stdio: "inherit", shell: true });
34
+
35
+ child.on("error", reject);
36
+ child.on("close", (code) => {
37
+ if (code === 0) {
38
+ resolve();
39
+ return;
40
+ }
41
+
42
+ reject(new Error(`${command} exited with ${code}`));
43
+ });
44
+ });
45
+
31
46
  const prepareForPublish = (pkgDir) => {
32
47
  const pkg = readPackageJson(pkgDir);
33
48
  if (!pkg.scripts?.build) return;
@@ -35,4 +50,4 @@ const prepareForPublish = (pkgDir) => {
35
50
  run("npx @vnejs/monorepo package --clean", pkgDir);
36
51
  };
37
52
 
38
- module.exports = { readPackageName, readVersion, writeVersion, run, prepareForPublish };
53
+ module.exports = { readPackageName, readVersion, writeVersion, run, runAsync, prepareForPublish };
@@ -2,9 +2,10 @@ const path = require("path");
2
2
 
3
3
  const { getChangedPackages } = require("../changed-packages");
4
4
  const resolveGraph = require("../workspace/resolve-graph");
5
- const { run } = require("../publish/utils");
5
+ const { runAsync } = require("../publish/utils");
6
6
  const confirm = require("../utils/confirm");
7
7
  const { formatPackageLine } = require("../utils/format-package");
8
+ const schedulePackages = require("./schedule");
8
9
 
9
10
  const sortByGraphOrder = (root, changedPackages, script) => {
10
11
  const changedNames = new Set(changedPackages.map((pkg) => pkg.name));
@@ -26,8 +27,37 @@ const printPackages = (packages) => {
26
27
  console.log(`Total: ${packages.length}`);
27
28
  };
28
29
 
30
+ const parseConcurrency = (value) => {
31
+ if (value == null) return 1;
32
+
33
+ const concurrency = Number(value);
34
+
35
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
36
+ console.error('Invalid "--concurrency". Must be an integer >= 1.');
37
+ process.exit(1);
38
+ }
39
+
40
+ return concurrency;
41
+ };
42
+
43
+ const parseParallelMs = (value) => {
44
+ if (value == null) return undefined;
45
+
46
+ const parallelMs = Number(value);
47
+
48
+ if (!Number.isFinite(parallelMs) || parallelMs < 0) {
49
+ console.error('Invalid "--parallel-ms". Must be a number >= 0.');
50
+ process.exit(1);
51
+ }
52
+
53
+ return parallelMs;
54
+ };
55
+
29
56
  module.exports = async (options = {}) => {
30
57
  const script = options.script;
58
+ const concurrency = parseConcurrency(options.concurrency);
59
+ const parallelMs = parseParallelMs(options.parallelMs);
60
+ const firstSerial = Boolean(options.firstSerial);
31
61
 
32
62
  if (!options.dryRun && !script) {
33
63
  console.error('Missing required option "--script".');
@@ -62,9 +92,15 @@ module.exports = async (options = {}) => {
62
92
  console.log("");
63
93
  }
64
94
 
65
- for (const [index, pkg] of packages.entries()) {
66
- const pkgDir = path.join(root, pkg.dir);
67
- console.log(formatPackageLine(pkg, { leadingNewline: true, tag: `[${index + 1}/${packages.length}]` }));
68
- run(`npm run ${script}`, pkgDir);
69
- }
95
+ await schedulePackages({
96
+ packages,
97
+ concurrency,
98
+ parallelMs,
99
+ firstSerial,
100
+ runFn: async (pkg, index) => {
101
+ const pkgDir = path.join(root, pkg.dir);
102
+ console.log(formatPackageLine(pkg, { leadingNewline: true, tag: `[${index + 1}/${packages.length}]` }));
103
+ await runAsync(`npm run ${script}`, pkgDir);
104
+ },
105
+ });
70
106
  };
@@ -0,0 +1,56 @@
1
+ const pLimit = require("p-limit").default;
2
+
3
+ const PARALLEL_WINDOW_ENDED = "Parallel window ended; concurrency set to 1";
4
+
5
+ const schedulePackages = async ({ packages, runFn, concurrency = 1, parallelMs, firstSerial = false, log = console.log }) => {
6
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
7
+ throw new Error("concurrency must be an integer >= 1");
8
+ }
9
+
10
+ if (parallelMs != null && (!Number.isFinite(parallelMs) || parallelMs < 0)) {
11
+ throw new Error("parallelMs must be a number >= 0");
12
+ }
13
+
14
+ if (!packages.length) return;
15
+
16
+ let startIndex = 0;
17
+
18
+ if (firstSerial) {
19
+ await runFn(packages[0], 0);
20
+ startIndex = 1;
21
+ }
22
+
23
+ const remaining = packages.slice(startIndex);
24
+
25
+ if (!remaining.length) return;
26
+
27
+ const limit = pLimit(concurrency);
28
+ let timer;
29
+
30
+ if (parallelMs != null && concurrency > 1) {
31
+ timer = setTimeout(() => {
32
+ limit.concurrency = 1;
33
+ log(PARALLEL_WINDOW_ENDED);
34
+ }, parallelMs);
35
+ }
36
+
37
+ try {
38
+ const results = await Promise.allSettled(remaining.map((pkg, offset) => limit(() => runFn(pkg, startIndex + offset))));
39
+
40
+ const failed = results.filter((result) => result.status === "rejected");
41
+
42
+ if (!failed.length) return;
43
+
44
+ for (const result of failed) {
45
+ const reason = result.reason;
46
+ log(reason?.stack ?? String(reason));
47
+ }
48
+
49
+ throw new Error(`${failed.length} package script(s) failed.`);
50
+ } finally {
51
+ if (timer) clearTimeout(timer);
52
+ }
53
+ };
54
+
55
+ module.exports = schedulePackages;
56
+ module.exports.PARALLEL_WINDOW_ENDED = PARALLEL_WINDOW_ENDED;
@@ -0,0 +1,163 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import schedulePackages, { PARALLEL_WINDOW_ENDED } from "./schedule.js";
4
+
5
+ const packages = (...names) => names.map((name) => ({ name }));
6
+
7
+ const createDeferred = () => {
8
+ let resolve;
9
+ let reject;
10
+ const promise = new Promise((res, rej) => {
11
+ resolve = res;
12
+ reject = rej;
13
+ });
14
+
15
+ return { promise, resolve, reject };
16
+ };
17
+
18
+ const createTrackedRunner = () => {
19
+ const started = [];
20
+ const deferreds = new Map();
21
+
22
+ const runFn = (pkg) => {
23
+ started.push(pkg.name);
24
+ const deferred = createDeferred();
25
+ deferreds.set(pkg.name, deferred);
26
+ return deferred.promise;
27
+ };
28
+
29
+ const resolve = (name) => deferreds.get(name).resolve();
30
+ const reject = (name, error) => deferreds.get(name).reject(error);
31
+
32
+ return { started, deferreds, runFn, resolve, reject };
33
+ };
34
+
35
+ const flush = async () => {
36
+ await Promise.resolve();
37
+ await Promise.resolve();
38
+ await Promise.resolve();
39
+ };
40
+
41
+ describe("schedulePackages", () => {
42
+ afterEach(() => {
43
+ vi.useRealTimers();
44
+ });
45
+
46
+ it("runs the first package outside the pool when firstSerial is set", async () => {
47
+ const { started, runFn, resolve } = createTrackedRunner();
48
+ const done = schedulePackages({
49
+ packages: packages("a", "b", "c"),
50
+ runFn,
51
+ concurrency: 2,
52
+ firstSerial: true,
53
+ });
54
+
55
+ await flush();
56
+ expect(started).toEqual(["a"]);
57
+
58
+ resolve("a");
59
+ await flush();
60
+ expect(started).toEqual(["a", "b", "c"]);
61
+
62
+ resolve("b");
63
+ resolve("c");
64
+ await done;
65
+ });
66
+
67
+ it("does not start the pool when the first-serial package fails", async () => {
68
+ const { started, runFn, reject } = createTrackedRunner();
69
+ const done = schedulePackages({
70
+ packages: packages("a", "b"),
71
+ runFn,
72
+ concurrency: 2,
73
+ firstSerial: true,
74
+ });
75
+
76
+ await flush();
77
+ reject("a", new Error("first failed"));
78
+
79
+ await expect(done).rejects.toThrow("first failed");
80
+ expect(started).toEqual(["a"]);
81
+ });
82
+
83
+ it("drops concurrency to 1 after parallelMs; in-flight tasks keep running", async () => {
84
+ vi.useFakeTimers();
85
+
86
+ const { started, runFn, resolve } = createTrackedRunner();
87
+ const logs = [];
88
+ const done = schedulePackages({
89
+ packages: packages("a", "b", "c", "d", "e"),
90
+ runFn,
91
+ concurrency: 3,
92
+ parallelMs: 100,
93
+ log: (message) => logs.push(message),
94
+ });
95
+
96
+ await flush();
97
+ expect(started).toEqual(["a", "b", "c"]);
98
+
99
+ await vi.advanceTimersByTimeAsync(100);
100
+ expect(logs).toContain(PARALLEL_WINDOW_ENDED);
101
+ expect(started).toEqual(["a", "b", "c"]);
102
+
103
+ resolve("a");
104
+ await flush();
105
+ expect(started).toEqual(["a", "b", "c"]);
106
+
107
+ resolve("b");
108
+ await flush();
109
+ expect(started).toEqual(["a", "b", "c"]);
110
+
111
+ resolve("c");
112
+ await flush();
113
+ expect(started).toEqual(["a", "b", "c", "d"]);
114
+
115
+ resolve("d");
116
+ await flush();
117
+ expect(started).toEqual(["a", "b", "c", "d", "e"]);
118
+
119
+ resolve("e");
120
+ await done;
121
+ });
122
+
123
+ it("keeps concurrency for the whole run when parallelMs is omitted", async () => {
124
+ const { started, runFn, resolve } = createTrackedRunner();
125
+ const done = schedulePackages({
126
+ packages: packages("a", "b", "c", "d"),
127
+ runFn,
128
+ concurrency: 3,
129
+ });
130
+
131
+ await flush();
132
+ expect(started).toEqual(["a", "b", "c"]);
133
+
134
+ resolve("a");
135
+ await flush();
136
+ expect(started).toEqual(["a", "b", "c", "d"]);
137
+
138
+ resolve("b");
139
+ resolve("c");
140
+ resolve("d");
141
+ await done;
142
+ });
143
+
144
+ it("waits for in-flight failures and then rejects", async () => {
145
+ const { started, runFn, resolve, reject } = createTrackedRunner();
146
+ const logs = [];
147
+ const done = schedulePackages({
148
+ packages: packages("a", "b"),
149
+ runFn,
150
+ concurrency: 2,
151
+ log: (message) => logs.push(message),
152
+ });
153
+
154
+ await flush();
155
+ expect(started).toEqual(["a", "b"]);
156
+
157
+ reject("a", new Error("a failed"));
158
+ resolve("b");
159
+
160
+ await expect(done).rejects.toThrow("1 package script(s) failed.");
161
+ expect(logs.some((message) => String(message).includes("a failed"))).toBe(true);
162
+ });
163
+ });
@@ -32,9 +32,7 @@ const syncPackageDeps = (options = {}) => {
32
32
  return null;
33
33
  }
34
34
 
35
- const workspaceVersions = options.workspace
36
- ? new Map(listWorkspacePackages({ cwd: pkgDir }).packages.map((pkg) => [pkg.name, pkg.version]))
37
- : new Map();
35
+ const workspaceVersions = options.workspace ? new Map(listWorkspacePackages({ cwd: pkgDir }).packages.map((pkg) => [pkg.name, pkg.version])) : new Map();
38
36
 
39
37
  const pkg = readPackageJson(pkgDir);
40
38
  const changes = [];
@@ -38,9 +38,7 @@ module.exports = (name, options = {}) => {
38
38
  }
39
39
 
40
40
  if (attempt === maxAttempts) {
41
- console.warn(
42
- `${name}: expected ${expectVersion}.x on npm, last seen: ${version ?? "not found"} — giving up`,
43
- );
41
+ console.warn(`${name}: expected ${expectVersion}.x on npm, last seen: ${version ?? "not found"} — giving up`);
44
42
  return version;
45
43
  }
46
44
 
@@ -4,8 +4,7 @@ const isStubTestScript = require("./is-stub-test-script");
4
4
 
5
5
  const DEFAULT_EXCLUDED_DIRS = [];
6
6
 
7
- const isExcludedDir = (dir, excludedDirs) =>
8
- excludedDirs.some((excluded) => dir === excluded || dir.startsWith(`${excluded}/`));
7
+ const isExcludedDir = (dir, excludedDirs) => excludedDirs.some((excluded) => dir === excluded || dir.startsWith(`${excluded}/`));
9
8
 
10
9
  module.exports = (packages, options = {}) => {
11
10
  let result = packages;
@@ -0,0 +1,10 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { createVitestConfig } from "@vnejs/configs.vitest/create-config.js";
5
+
6
+ export default createVitestConfig({
7
+ dirname: path.dirname(fileURLToPath(import.meta.url)),
8
+ environment: "node",
9
+ include: ["src/**/*.test.js"],
10
+ });