@guanzhu.me/pw-cli 0.0.14 → 0.0.16
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 +35 -39
- package/bin/pw-cli.js +37 -0
- package/package.json +1 -1
- package/src/cli.js +8 -4
- package/src/executor.js +88 -26
- package/src/fast-send.js +123 -0
- package/src/launch-daemon.js +1 -1
package/README.md
CHANGED
|
@@ -20,8 +20,8 @@ Raw Playwright is excellent for test suites and scripted automation, but ad hoc
|
|
|
20
20
|
- Headed mode by default
|
|
21
21
|
- Named profile support
|
|
22
22
|
- `run-code` for inline JavaScript or piped stdin
|
|
23
|
-
- `run-script` for executing local JavaScript files
|
|
24
|
-
- `run-script` supports CommonJS
|
|
23
|
+
- `run-script` for executing local JavaScript files with `main` function convention
|
|
24
|
+
- `run-script` supports standard CommonJS modules (`require`, `__filename`, `__dirname`) and also bare-code scripts
|
|
25
25
|
- Queue management for multi-step flows
|
|
26
26
|
- Automatic browser launch when needed
|
|
27
27
|
- XPath command conversion for common actions
|
|
@@ -74,46 +74,42 @@ Run a local script:
|
|
|
74
74
|
pw-cli run-script ./scrape.js --url https://example.com
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
`run-script` is intended for multi-step automation.
|
|
78
|
-
|
|
79
|
-
- Playwright globals: `page`, `context`, `browser`, `playwright`
|
|
80
|
-
- Script args: `args`
|
|
81
|
-
- CommonJS globals: `require`, `module`, `exports`, `__filename`, `__dirname`
|
|
82
|
-
|
|
83
|
-
More complete example:
|
|
77
|
+
`run-script` is intended for multi-step automation. Define an `async function main` that receives Playwright globals as a single object:
|
|
84
78
|
|
|
85
79
|
```javascript
|
|
86
80
|
// scripts/extract-links.js
|
|
87
81
|
const fs = require('fs');
|
|
88
82
|
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
nodes
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
83
|
+
async function main({ page, args }) {
|
|
84
|
+
const url = args[args.indexOf('--url') + 1] || 'https://example.com';
|
|
85
|
+
const output = args[args.indexOf('--output') + 1] || 'links.json';
|
|
86
|
+
|
|
87
|
+
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
88
|
+
|
|
89
|
+
const links = await page.locator('a').evaluateAll(nodes =>
|
|
90
|
+
nodes
|
|
91
|
+
.map(a => ({
|
|
92
|
+
text: a.textContent.trim(),
|
|
93
|
+
href: a.href,
|
|
94
|
+
}))
|
|
95
|
+
.filter(item => item.href)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
output,
|
|
100
|
+
JSON.stringify(
|
|
101
|
+
{
|
|
102
|
+
url,
|
|
103
|
+
count: links.length,
|
|
104
|
+
links,
|
|
105
|
+
},
|
|
106
|
+
null,
|
|
107
|
+
2
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
return `saved ${links.length} links to ${output}`;
|
|
112
|
+
}
|
|
117
113
|
```
|
|
118
114
|
|
|
119
115
|
```bash
|
|
@@ -154,7 +150,7 @@ pw-cli list
|
|
|
154
150
|
- `open` injects headed and persistent defaults
|
|
155
151
|
- Browser-backed commands can auto-open a browser session if needed
|
|
156
152
|
- `run-code` accepts stdin and plain inline statements
|
|
157
|
-
- `run-script` executes a local `.js` file
|
|
153
|
+
- `run-script` executes a local `.js` file — auto-detects `main` function, `module.exports`, or bare code
|
|
158
154
|
- Common element commands accept XPath refs
|
|
159
155
|
- `queue` lets you batch multiple commands and run them in order
|
|
160
156
|
|
|
@@ -281,7 +277,7 @@ console [min-level] list console messages
|
|
|
281
277
|
run-code <code> run playwright code snippet
|
|
282
278
|
pw-cli: reads code from stdin when <code> is omitted
|
|
283
279
|
pw-cli: wraps plain statements in an async function
|
|
284
|
-
run-script <file> [...] run a local JavaScript file
|
|
280
|
+
run-script <file> [...] run a local JavaScript file (main function or module.exports)
|
|
285
281
|
network list all network requests since loading the page
|
|
286
282
|
tracing-start start trace recording
|
|
287
283
|
tracing-stop stop trace recording
|
package/bin/pw-cli.js
CHANGED
|
@@ -771,6 +771,43 @@ async function main() {
|
|
|
771
771
|
return;
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
+
// ── Fast path: send command directly to playwright-cli daemon socket ────
|
|
775
|
+
// Skip heavy setup (npm root -g, require playwright, CDP probes) entirely.
|
|
776
|
+
// Only for commands that the daemon handles AND don't need local preprocessing.
|
|
777
|
+
if (command && !MGMT_COMMANDS.has(command)) {
|
|
778
|
+
// Build the args array the daemon expects: strip session flags, keep command + args
|
|
779
|
+
let fastArgs = [...rawArgv];
|
|
780
|
+
// Remove session flags (-s xxx / --session xxx / --session=xxx)
|
|
781
|
+
fastArgs = fastArgs.filter((a, i, arr) => {
|
|
782
|
+
if (a === '-s' || a === '--session') { arr[i + 1] = undefined; return false; }
|
|
783
|
+
if (a === undefined) return false;
|
|
784
|
+
if (a.startsWith('-s=') || a.startsWith('--session=')) return false;
|
|
785
|
+
return true;
|
|
786
|
+
}).filter(Boolean);
|
|
787
|
+
|
|
788
|
+
// Apply XPath conversion if needed
|
|
789
|
+
fastArgs = convertXPathCommand(fastArgs);
|
|
790
|
+
|
|
791
|
+
// Handle run-code wrapping for XPath-converted commands
|
|
792
|
+
if (fastArgs[0] === 'run-code' && fastArgs.length > 1) {
|
|
793
|
+
const code = fastArgs.slice(1).join(' ');
|
|
794
|
+
fastArgs = ['run-code', wrapCodeIfNeeded(code)];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const { sendCommand } = require('../src/fast-send');
|
|
798
|
+
const result = await sendCommand(fastArgs, session);
|
|
799
|
+
if (result !== null) {
|
|
800
|
+
// Daemon responded — use its result
|
|
801
|
+
if (result.isError) {
|
|
802
|
+
process.stderr.write(`${result.text}\n`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
if (result.text) process.stdout.write(result.text + '\n');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// result === null means daemon not running — fall through to full path
|
|
809
|
+
}
|
|
810
|
+
|
|
774
811
|
// ── From here on: delegate to playwright-cli (with enhancements) ─────────
|
|
775
812
|
const cliPath = findPlaywrightCli();
|
|
776
813
|
if (!cliPath) {
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -55,10 +55,14 @@ function getRunScriptHelp() {
|
|
|
55
55
|
return `Usage:
|
|
56
56
|
pw-cli run-script <file.js> [args...]
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
Script format (standard module):
|
|
59
|
+
module.exports = async function ({ page, context, browser, playwright, args }) {
|
|
60
|
+
// your code here
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
The exported function receives { page, context, browser, playwright, args }.
|
|
64
|
+
CommonJS globals (require, __filename, __dirname) are available as usual.
|
|
65
|
+
Legacy bare-code scripts (without module.exports) are still supported.
|
|
62
66
|
|
|
63
67
|
Example:
|
|
64
68
|
pw-cli run-script ./scripts/extract-links.js --url https://example.com --output links.json`;
|
package/src/executor.js
CHANGED
|
@@ -79,25 +79,7 @@ async function execCode(code, { browser, context, page, playwright }) {
|
|
|
79
79
|
return withTemporaryGlobals(globals, () => runCode(code, globals));
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
const absPath = path.resolve(scriptPath);
|
|
84
|
-
if (!fs.existsSync(absPath)) {
|
|
85
|
-
const err = new Error(`Script not found: ${absPath}`);
|
|
86
|
-
err.code = 'ENOENT';
|
|
87
|
-
throw err;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const code = fs.readFileSync(absPath, 'utf8');
|
|
91
|
-
const moduleDir = path.dirname(absPath);
|
|
92
|
-
const scriptModule = {
|
|
93
|
-
id: absPath,
|
|
94
|
-
filename: absPath,
|
|
95
|
-
path: moduleDir,
|
|
96
|
-
exports: {},
|
|
97
|
-
loaded: false,
|
|
98
|
-
children: [],
|
|
99
|
-
parent: require.main || module,
|
|
100
|
-
};
|
|
82
|
+
function buildScriptRequire(absPath) {
|
|
101
83
|
const localRequire = Module.createRequire(absPath);
|
|
102
84
|
const scriptRequire = function scriptRequire(id) {
|
|
103
85
|
try {
|
|
@@ -119,16 +101,92 @@ async function execScript(scriptPath, scriptArgs, { browser, context, page, play
|
|
|
119
101
|
scriptRequire.resolve = localRequire.resolve.bind(localRequire);
|
|
120
102
|
scriptRequire.cache = require.cache;
|
|
121
103
|
scriptRequire.extensions = require.extensions;
|
|
104
|
+
return scriptRequire;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isModuleExport(code) {
|
|
108
|
+
return /\bmodule\.exports\b/.test(code) || /\bexports\./.test(code);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasMainFunction(code) {
|
|
112
|
+
return /\b(async\s+)?function\s+main\s*\(/.test(code);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function execScript(scriptPath, scriptArgs, { browser, context, page, playwright }) {
|
|
116
|
+
const absPath = path.resolve(scriptPath);
|
|
117
|
+
if (!fs.existsSync(absPath)) {
|
|
118
|
+
const err = new Error(`Script not found: ${absPath}`);
|
|
119
|
+
err.code = 'ENOENT';
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const code = fs.readFileSync(absPath, 'utf8');
|
|
124
|
+
const pwGlobals = { page, context, browser, playwright, args: scriptArgs };
|
|
125
|
+
|
|
126
|
+
// Standard module pattern: script uses module.exports = function(...)
|
|
127
|
+
// The exported function receives Playwright globals as a single object argument.
|
|
128
|
+
if (isModuleExport(code)) {
|
|
129
|
+
return execModuleScript(absPath, code, pwGlobals);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Legacy bare-code pattern: script body is executed directly with globals as local variables.
|
|
133
|
+
return execBareScript(absPath, code, pwGlobals);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function execModuleScript(absPath, code, pwGlobals) {
|
|
137
|
+
const moduleDir = path.dirname(absPath);
|
|
138
|
+
const scriptRequire = buildScriptRequire(absPath);
|
|
139
|
+
const scriptModule = {
|
|
140
|
+
id: absPath,
|
|
141
|
+
filename: absPath,
|
|
142
|
+
path: moduleDir,
|
|
143
|
+
exports: {},
|
|
144
|
+
loaded: false,
|
|
145
|
+
children: [],
|
|
146
|
+
parent: require.main || module,
|
|
147
|
+
require: scriptRequire,
|
|
148
|
+
};
|
|
122
149
|
scriptRequire.main = scriptModule;
|
|
123
150
|
|
|
124
|
-
|
|
151
|
+
// Evaluate the module body to populate module.exports.
|
|
152
|
+
// Playwright globals are available during evaluation for backward compat.
|
|
153
|
+
const wrapGlobals = {
|
|
154
|
+
...pwGlobals,
|
|
155
|
+
require: scriptRequire,
|
|
156
|
+
module: scriptModule,
|
|
157
|
+
exports: scriptModule.exports,
|
|
158
|
+
console,
|
|
159
|
+
process,
|
|
160
|
+
__filename: absPath,
|
|
161
|
+
__dirname: moduleDir,
|
|
162
|
+
};
|
|
163
|
+
await withTemporaryGlobals(wrapGlobals, () => runProgram(code, wrapGlobals));
|
|
164
|
+
scriptModule.loaded = true;
|
|
165
|
+
|
|
166
|
+
const exported = scriptModule.exports;
|
|
167
|
+
if (typeof exported === 'function') {
|
|
168
|
+
return exported(pwGlobals);
|
|
169
|
+
}
|
|
170
|
+
return exported;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function execBareScript(absPath, code, pwGlobals) {
|
|
174
|
+
const moduleDir = path.dirname(absPath);
|
|
175
|
+
const scriptRequire = buildScriptRequire(absPath);
|
|
176
|
+
const scriptModule = {
|
|
177
|
+
id: absPath,
|
|
178
|
+
filename: absPath,
|
|
179
|
+
path: moduleDir,
|
|
180
|
+
exports: {},
|
|
181
|
+
loaded: false,
|
|
182
|
+
children: [],
|
|
183
|
+
parent: require.main || module,
|
|
184
|
+
require: scriptRequire,
|
|
185
|
+
};
|
|
186
|
+
scriptRequire.main = scriptModule;
|
|
125
187
|
|
|
126
188
|
const globals = {
|
|
127
|
-
|
|
128
|
-
context,
|
|
129
|
-
page,
|
|
130
|
-
playwright,
|
|
131
|
-
args: scriptArgs,
|
|
189
|
+
...pwGlobals,
|
|
132
190
|
require: scriptRequire,
|
|
133
191
|
module: scriptModule,
|
|
134
192
|
exports: scriptModule.exports,
|
|
@@ -137,7 +195,11 @@ async function execScript(scriptPath, scriptArgs, { browser, context, page, play
|
|
|
137
195
|
__filename: absPath,
|
|
138
196
|
__dirname: moduleDir,
|
|
139
197
|
};
|
|
140
|
-
|
|
198
|
+
// If the script defines a main function, append a call to it.
|
|
199
|
+
const finalCode = hasMainFunction(code)
|
|
200
|
+
? code + '\nreturn main({ page, context, browser, playwright, args });'
|
|
201
|
+
: code;
|
|
202
|
+
const result = await withTemporaryGlobals(globals, () => runProgram(finalCode, globals));
|
|
141
203
|
scriptModule.loaded = true;
|
|
142
204
|
return result === undefined ? scriptModule.exports : result;
|
|
143
205
|
}
|
package/src/fast-send.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
const HOME_DIR = os.homedir();
|
|
10
|
+
const PW_CLI_DIR = path.join(HOME_DIR, '.pw-cli');
|
|
11
|
+
const WORKSPACE_HASH = crypto.createHash('sha1')
|
|
12
|
+
.update(PW_CLI_DIR)
|
|
13
|
+
.digest('hex')
|
|
14
|
+
.substring(0, 16);
|
|
15
|
+
|
|
16
|
+
function getSocketPath(sessionName = 'default') {
|
|
17
|
+
const socketName = `${sessionName}.sock`;
|
|
18
|
+
if (os.platform() === 'win32')
|
|
19
|
+
return `\\\\.\\pipe\\${WORKSPACE_HASH}-${socketName}`;
|
|
20
|
+
const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli');
|
|
21
|
+
return path.join(socketsDir, WORKSPACE_HASH, socketName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getVersion() {
|
|
25
|
+
// Read version from session file (already written by playwright-cli daemon)
|
|
26
|
+
const sessionFile = path.join(
|
|
27
|
+
os.platform() === 'win32'
|
|
28
|
+
? path.join(process.env.LOCALAPPDATA || path.join(HOME_DIR, 'AppData', 'Local'), 'ms-playwright', 'daemon')
|
|
29
|
+
: os.platform() === 'darwin'
|
|
30
|
+
? path.join(HOME_DIR, 'Library', 'Caches', 'ms-playwright', 'daemon')
|
|
31
|
+
: path.join(process.env.XDG_CACHE_HOME || path.join(HOME_DIR, '.cache'), 'ms-playwright', 'daemon'),
|
|
32
|
+
WORKSPACE_HASH,
|
|
33
|
+
'default.session'
|
|
34
|
+
);
|
|
35
|
+
try {
|
|
36
|
+
const session = JSON.parse(fs.readFileSync(sessionFile, 'utf8'));
|
|
37
|
+
return session.version;
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert a string array to minimist-style object.
|
|
45
|
+
* The daemon expects { _: [cmd, arg1, ...], flagName: value, ... }.
|
|
46
|
+
*/
|
|
47
|
+
function toMinimistArgs(argv) {
|
|
48
|
+
const result = { _: [] };
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const arg = argv[i];
|
|
51
|
+
if (arg.startsWith('--')) {
|
|
52
|
+
const key = arg.slice(2);
|
|
53
|
+
const next = argv[i + 1];
|
|
54
|
+
if (next && !next.startsWith('-')) {
|
|
55
|
+
result[key] = next;
|
|
56
|
+
i++;
|
|
57
|
+
} else {
|
|
58
|
+
result[key] = true;
|
|
59
|
+
}
|
|
60
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
61
|
+
const key = arg.slice(1);
|
|
62
|
+
const next = argv[i + 1];
|
|
63
|
+
if (next && !next.startsWith('-')) {
|
|
64
|
+
result[key] = next;
|
|
65
|
+
i++;
|
|
66
|
+
} else {
|
|
67
|
+
result[key] = true;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
result._.push(arg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Send a command directly to the playwright-cli daemon socket.
|
|
78
|
+
* Returns { text, isError } or null (daemon not running).
|
|
79
|
+
*/
|
|
80
|
+
function sendCommand(args, sessionName = 'default') {
|
|
81
|
+
const socketPath = getSocketPath(sessionName);
|
|
82
|
+
const version = getVersion();
|
|
83
|
+
if (!version) return Promise.resolve(null);
|
|
84
|
+
|
|
85
|
+
const minimistArgs = toMinimistArgs(args);
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const socket = net.createConnection(socketPath, () => {
|
|
89
|
+
const message = JSON.stringify({
|
|
90
|
+
id: 1,
|
|
91
|
+
method: 'run',
|
|
92
|
+
params: { args: minimistArgs, cwd: process.cwd() },
|
|
93
|
+
version,
|
|
94
|
+
}) + '\n';
|
|
95
|
+
socket.write(message);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let buf = '';
|
|
99
|
+
socket.on('data', chunk => {
|
|
100
|
+
buf += chunk.toString();
|
|
101
|
+
const nlIdx = buf.indexOf('\n');
|
|
102
|
+
if (nlIdx === -1) return;
|
|
103
|
+
const line = buf.slice(0, nlIdx);
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
socket.destroy();
|
|
106
|
+
try {
|
|
107
|
+
const resp = JSON.parse(line);
|
|
108
|
+
if (resp.error) {
|
|
109
|
+
reject(new Error(resp.error));
|
|
110
|
+
} else {
|
|
111
|
+
resolve(resp.result);
|
|
112
|
+
}
|
|
113
|
+
} catch (e) {
|
|
114
|
+
reject(new Error('Invalid daemon response'));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
socket.on('error', () => { clearTimeout(timer); resolve(null); }); // connection failed = daemon not running
|
|
119
|
+
const timer = setTimeout(() => { socket.destroy(); resolve(null); }, 3000);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { sendCommand, getSocketPath, getVersion };
|
package/src/launch-daemon.js
CHANGED
|
@@ -62,7 +62,7 @@ if (!profileDir) {
|
|
|
62
62
|
channel: 'chrome',
|
|
63
63
|
headless,
|
|
64
64
|
args: [`--remote-debugging-port=${port}`],
|
|
65
|
-
ignoreDefaultArgs: ['--enable-automation'],
|
|
65
|
+
ignoreDefaultArgs: ['--enable-automation', '--no-sandbox'],
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
// Wait briefly for CDP to be ready, then signal
|