pwvm 0.1.0 → 1.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/README.md +25 -5
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/uninstall.js +25 -0
- package/dist/core/uninstall.js +27 -0
- package/dist/index.js +84 -3
- package/dist/utils/registry.js +2 -0
- package/package.json +6 -2
- package/wiki/pwvm_icon_.png +0 -0
- package/wiki/pwvm_icon_white.png +0 -0
- package/wiki/switching_playwright_vesions.gif +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pwvm
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
### **Playwright Version Manager**
|
|
4
6
|
|
|
5
7
|
A simple *Playwright Version Manager* that solves a common pain point:
|
|
@@ -8,6 +10,8 @@ A simple *Playwright Version Manager* that solves a common pain point:
|
|
|
8
10
|
|
|
9
11
|
`pwvm` lets you install, manage, and switch Playwright versions reliably — **one command, predictable behavior**, just like `nvm` for Node.js.
|
|
10
12
|
|
|
13
|
+

|
|
14
|
+
|
|
11
15
|
---
|
|
12
16
|
|
|
13
17
|
## Why pwvm
|
|
@@ -43,25 +47,41 @@ Follow the printed instructions to add pwvm shims to your `PATH`.
|
|
|
43
47
|
## Common usage
|
|
44
48
|
|
|
45
49
|
```sh
|
|
46
|
-
pwvm install 1.
|
|
47
|
-
pwvm use 1.
|
|
50
|
+
pwvm install 1.57.0
|
|
51
|
+
pwvm use 1.57.0
|
|
52
|
+
playwright --version
|
|
48
53
|
playwright test
|
|
49
54
|
```
|
|
50
55
|
|
|
56
|
+

|
|
57
|
+
|
|
51
58
|
Pin versions per project with `.pwvmrc`:
|
|
52
59
|
|
|
53
60
|
```text
|
|
54
|
-
1.
|
|
61
|
+
1.57.0
|
|
55
62
|
```
|
|
56
63
|
|
|
64
|
+
### Deterministic guarantees pwvm already provides
|
|
65
|
+
• Playwright installed by exact version (`playwright@x.y.z`)
|
|
66
|
+
• Versions stored in isolated directories (`~/.pwvm/versions/<version>`)
|
|
67
|
+
• Browser binaries scoped per Playwright version
|
|
68
|
+
• `.pwvmrc` pins version per repo
|
|
69
|
+
• No implicit upgrades (no latest unless explicitly requested)
|
|
70
|
+
|
|
71
|
+
> [!NOTE] pwvm performs no background network activity and only installs software when explicitly requested.
|
|
72
|
+
|
|
57
73
|
---
|
|
58
74
|
|
|
59
|
-
|
|
75
|
+
### CI-friendly
|
|
60
76
|
|
|
61
77
|
`pwvm` works in GitHub Actions, Azure Pipelines, Bitbucket, and any CI where you control `PATH`.
|
|
62
78
|
|
|
63
79
|
Install → setup → select version → run Playwright.
|
|
64
80
|
|
|
81
|
+
### Dockerized
|
|
82
|
+
|
|
83
|
+
`pwvm` works seamlessly in Docker containers or local development.
|
|
84
|
+
|
|
65
85
|
---
|
|
66
86
|
|
|
67
87
|
## Support pwvm
|
|
@@ -72,7 +92,7 @@ If this tool saves you time, CI hours, or debugging frustration:
|
|
|
72
92
|
|
|
73
93
|
* ⭐ [Star the project](https://github.com/eaccmk/pwvm)
|
|
74
94
|
* ❤️ [Sponsor via GitHub](https://github.com/sponsors/eaccmk)
|
|
75
|
-
* 🔁 Share it with your team
|
|
95
|
+
* 🔁 Share it with your team and tag #QualityWithMillan
|
|
76
96
|
|
|
77
97
|
Your support helps keep pwvm maintained and improving.
|
|
78
98
|
|
package/dist/commands/doctor.js
CHANGED
|
@@ -24,7 +24,7 @@ export const runDoctorCommand = async (deps = {}) => {
|
|
|
24
24
|
const fsImpl = deps.fs ?? fs;
|
|
25
25
|
const pwvmDir = getPwvmDir();
|
|
26
26
|
const shimsDir = deps.shimsDir ?? getShimsDir();
|
|
27
|
-
const envPath = deps.envPath ?? process.env.PATH ?? "";
|
|
27
|
+
const envPath = deps.envPath ?? process.env.PATH ?? process.env.DOCKER_PATH ?? "";
|
|
28
28
|
const shimPath = path.join(shimsDir, "playwright");
|
|
29
29
|
const useColor = process.stdout.isTTY === true;
|
|
30
30
|
const withScope = (message) => message.startsWith(" ") ? `pwvm-doctor${message}` : `pwvm-doctor ${message}`;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { uninstallPlaywrightVersion, } from "../core/uninstall.js";
|
|
2
|
+
import { createLogger, formatError } from "../utils/logger.js";
|
|
3
|
+
export const runUninstallCommand = async (version, deps = {}) => {
|
|
4
|
+
const logger = deps.logger ?? createLogger();
|
|
5
|
+
const uninstall = deps.uninstall ?? uninstallPlaywrightVersion;
|
|
6
|
+
try {
|
|
7
|
+
await uninstall(version, deps.uninstallOptions);
|
|
8
|
+
logger.info(`Uninstalled Playwright ${version}`);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
let hint;
|
|
13
|
+
if (message.includes("is currently active")) {
|
|
14
|
+
hint = "Run `pwvm use <version>` to switch first.";
|
|
15
|
+
}
|
|
16
|
+
else if (message.includes("is not installed")) {
|
|
17
|
+
hint = "Run `pwvm list` to see installed versions.";
|
|
18
|
+
}
|
|
19
|
+
else if (message.includes("Invalid Playwright version")) {
|
|
20
|
+
hint = "Use a valid semver like 1.2.3.";
|
|
21
|
+
}
|
|
22
|
+
logger.error(formatError(message, hint));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import semver from "semver";
|
|
4
|
+
import { getVersionsDir } from "./paths.js";
|
|
5
|
+
import { getGlobalActiveVersion } from "./versions.js";
|
|
6
|
+
export const uninstallPlaywrightVersion = async (version, options = {}) => {
|
|
7
|
+
if (!semver.valid(version)) {
|
|
8
|
+
throw new Error(`Invalid Playwright version "${version}".`);
|
|
9
|
+
}
|
|
10
|
+
const fsImpl = options.fs ?? fs;
|
|
11
|
+
const activeVersion = await getGlobalActiveVersion({
|
|
12
|
+
homeDir: options.homeDir,
|
|
13
|
+
activeVersionFile: options.activeVersionFile,
|
|
14
|
+
fs: fsImpl,
|
|
15
|
+
});
|
|
16
|
+
if (activeVersion === version) {
|
|
17
|
+
throw new Error(`Playwright version "${version}" is currently active.`);
|
|
18
|
+
}
|
|
19
|
+
const resolveVersionsDir = options.getVersionsDir ?? getVersionsDir;
|
|
20
|
+
const versionsDir = options.versionsDir ?? resolveVersionsDir(options.homeDir);
|
|
21
|
+
const targetDir = path.join(versionsDir, version);
|
|
22
|
+
const exists = await fsImpl.pathExists(targetDir);
|
|
23
|
+
if (!exists) {
|
|
24
|
+
throw new Error(`Playwright version "${version}" is not installed.`);
|
|
25
|
+
}
|
|
26
|
+
await fsImpl.remove(targetDir);
|
|
27
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import fs from "fs-extra";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { runCurrentCommand } from "./commands/current.js";
|
|
6
7
|
import { runDoctorCommand } from "./commands/doctor.js";
|
|
@@ -9,10 +10,13 @@ import { runListCommand } from "./commands/list.js";
|
|
|
9
10
|
import { runListRemoteCommand } from "./commands/listRemote.js";
|
|
10
11
|
import { runPruneCommand } from "./commands/prune.js";
|
|
11
12
|
import { runSetupCommand } from "./commands/setup.js";
|
|
13
|
+
import { runUninstallCommand } from "./commands/uninstall.js";
|
|
12
14
|
import { runUseCommand } from "./commands/use.js";
|
|
13
15
|
import { getPwvmDir, getShimsDir } from "./core/paths.js";
|
|
14
16
|
import { runPlaywrightShim } from "./core/shim.js";
|
|
15
17
|
import { createLogger } from "./utils/logger.js";
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const { version: pwvmVersion } = require("../package.json");
|
|
16
20
|
const program = new Command();
|
|
17
21
|
program.enablePositionalOptions();
|
|
18
22
|
const logger = createLogger();
|
|
@@ -34,8 +38,78 @@ const getShimArgs = () => {
|
|
|
34
38
|
};
|
|
35
39
|
program
|
|
36
40
|
.name("pwvm")
|
|
37
|
-
.description(
|
|
38
|
-
.version("
|
|
41
|
+
.description(`Playwright Version Manager (v${pwvmVersion})`)
|
|
42
|
+
.version(pwvmVersion, "-v, --version");
|
|
43
|
+
program.configureOutput({
|
|
44
|
+
outputError: () => { },
|
|
45
|
+
});
|
|
46
|
+
program.exitOverride((err) => {
|
|
47
|
+
// Commander control-flow exits (help / version)
|
|
48
|
+
if (err.code === "commander.helpDisplayed" ||
|
|
49
|
+
err.code === "commander.versionDisplayed" ||
|
|
50
|
+
err.code === "commander.help" ||
|
|
51
|
+
err.exitCode === 0) {
|
|
52
|
+
process.exitCode = err.exitCode || 0;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let message = err.message;
|
|
56
|
+
if (message.startsWith("error: ")) {
|
|
57
|
+
message = message.slice("error: ".length);
|
|
58
|
+
}
|
|
59
|
+
message = message.replace(/'([^']+)'/g, "\"$1\"");
|
|
60
|
+
if (message.length > 0) {
|
|
61
|
+
message = `${message[0].toUpperCase()}${message.slice(1)}`;
|
|
62
|
+
}
|
|
63
|
+
if (!message.endsWith(".")) {
|
|
64
|
+
message = `${message}.`;
|
|
65
|
+
}
|
|
66
|
+
logger.error(message);
|
|
67
|
+
process.exitCode = err.exitCode || 1;
|
|
68
|
+
});
|
|
69
|
+
program.configureHelp({
|
|
70
|
+
formatHelp: (cmd, helper) => {
|
|
71
|
+
const help = Object.getPrototypeOf(helper).formatHelp.call(helper, cmd, helper);
|
|
72
|
+
if (cmd !== program) {
|
|
73
|
+
return help;
|
|
74
|
+
}
|
|
75
|
+
const sections = help.split("\n\n");
|
|
76
|
+
if (sections.length < 2 || !sections[0].startsWith("Usage:")) {
|
|
77
|
+
return help;
|
|
78
|
+
}
|
|
79
|
+
let reordered = [sections[1], sections[0], ...sections.slice(2)].join("\n\n");
|
|
80
|
+
const installLine = " install Install a Playwright version";
|
|
81
|
+
const uninstallLine = " uninstall <version> Uninstall a specific Playwright version";
|
|
82
|
+
if (!reordered.includes(uninstallLine) && reordered.includes(installLine)) {
|
|
83
|
+
reordered = reordered.replace(`${installLine}\n`, `${installLine}\n${uninstallLine}\n`);
|
|
84
|
+
}
|
|
85
|
+
const lines = reordered.split("\n");
|
|
86
|
+
let inCommands = false;
|
|
87
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
88
|
+
const line = lines[i];
|
|
89
|
+
if (line === "Commands:") {
|
|
90
|
+
inCommands = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (inCommands) {
|
|
94
|
+
if (line.trim().length === 0) {
|
|
95
|
+
inCommands = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const match = /^ (.+?)( {2,})(\S.*)$/.exec(line);
|
|
99
|
+
if (match) {
|
|
100
|
+
const prefix = "pwvm ";
|
|
101
|
+
const term = match[1];
|
|
102
|
+
const spacing = match[2];
|
|
103
|
+
const desc = match[3];
|
|
104
|
+
const adjusted = Math.max(2, spacing.length - prefix.length);
|
|
105
|
+
lines[i] = ` ${prefix}${term}${" ".repeat(adjusted)}${desc}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
reordered = lines.join("\n");
|
|
110
|
+
return `\n${reordered}`;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
39
113
|
const shouldShowSetupNotice = async () => {
|
|
40
114
|
if (setupNoticeShown) {
|
|
41
115
|
return false;
|
|
@@ -99,6 +173,11 @@ program
|
|
|
99
173
|
.option("--with-browsers", "Install Playwright browsers (default)")
|
|
100
174
|
.option("--no-browsers", "Skip Playwright browser install")
|
|
101
175
|
.action((version, options) => invoke(runInstallCommand, version, options));
|
|
176
|
+
program
|
|
177
|
+
.command("uninstall")
|
|
178
|
+
.description("Uninstall a specific Playwright version")
|
|
179
|
+
.argument("<version>")
|
|
180
|
+
.action((version) => invoke(runUninstallCommand, version));
|
|
102
181
|
program
|
|
103
182
|
.command("list")
|
|
104
183
|
.description("List installed Playwright versions")
|
|
@@ -143,7 +222,9 @@ program
|
|
|
143
222
|
process.exitCode = 1;
|
|
144
223
|
}
|
|
145
224
|
});
|
|
146
|
-
|
|
225
|
+
program.parseAsync().catch(() => {
|
|
226
|
+
// All commander control-flow exits are handled by exitOverride.
|
|
227
|
+
});
|
|
147
228
|
function getShimErrorDetails(message) {
|
|
148
229
|
const guidanceMarker = ". Run ";
|
|
149
230
|
const guidanceIndex = message.indexOf(guidanceMarker);
|
package/dist/utils/registry.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import https from "node:https";
|
|
2
2
|
const REGISTRY_URL = "https://registry.npmjs.org";
|
|
3
|
+
// NOTE: This function MUST only be called from user-invoked commands
|
|
4
|
+
// such as `list-remote` or `install latest`. No background usage allowed.
|
|
3
5
|
export const fetchPackageMetadata = async (packageName) => new Promise((resolve, reject) => {
|
|
4
6
|
const url = `${REGISTRY_URL}/${encodeURIComponent(packageName)}`;
|
|
5
7
|
const request = https.get(url, {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "pwvm",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"description": "Playwright Version Manager",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
14
|
"README.md",
|
|
15
|
+
"wiki",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
@@ -25,8 +26,11 @@
|
|
|
25
26
|
"playwright-version-manager",
|
|
26
27
|
"playwright-cli",
|
|
27
28
|
"version-manager",
|
|
29
|
+
"version",
|
|
30
|
+
"manager",
|
|
28
31
|
"testing",
|
|
29
|
-
"
|
|
32
|
+
"ci",
|
|
33
|
+
"cli"
|
|
30
34
|
],
|
|
31
35
|
"engines": {
|
|
32
36
|
"node": ">=18"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|