@guanzhu.me/pw-cli 0.0.13 → 0.0.15

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 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-style scripts that use `require`, `module`, `exports`, `__filename`, and `__dirname`
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. Your script can use:
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
- const url = args[args.indexOf('--url') + 1] || 'https://example.com';
90
- const output = args[args.indexOf('--output') + 1] || 'links.json';
91
-
92
- await page.goto(url, { waitUntil: 'networkidle' });
93
-
94
- const links = await page.locator('a').evaluateAll(nodes =>
95
- nodes
96
- .map(a => ({
97
- text: a.textContent.trim(),
98
- href: a.href,
99
- }))
100
- .filter(item => item.href)
101
- );
102
-
103
- fs.writeFileSync(
104
- output,
105
- JSON.stringify(
106
- {
107
- url,
108
- count: links.length,
109
- links,
110
- },
111
- null,
112
- 2
113
- )
114
- );
115
-
116
- return `saved ${links.length} links to ${output}`;
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 with Playwright globals, CommonJS globals, and `args`
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 with Playwright globals and script args
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanzhu.me/pw-cli",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Persistent Playwright browser CLI with headed defaults, profile support, queueing, and script execution",
5
5
  "bin": {
6
6
  "pw-cli": "./bin/pw-cli.js"
@@ -7,7 +7,7 @@ const fs = require('fs');
7
7
  const crypto = require('crypto');
8
8
  const { execSync } = require('child_process');
9
9
  const { readState, writeState, clearState, getProfileDir } = require('./state');
10
- const { probeCDP, findFreePort, sleep } = require('./utils');
10
+ const { probeCDP, findFreePort, sleep, fetchActivePageUrl } = require('./utils');
11
11
 
12
12
  const DAEMON_SCRIPT = path.join(__dirname, 'launch-daemon.js');
13
13
 
@@ -61,6 +61,32 @@ function loadPlaywright() {
61
61
  throw new Error('playwright is not installed. Run: npm install -g playwright');
62
62
  }
63
63
 
64
+ function pickPage(pages, activeUrl) {
65
+ if (!pages || pages.length === 0) return null;
66
+ if (activeUrl) {
67
+ const matchingPages = pages.filter(page => {
68
+ try {
69
+ return page.url() === activeUrl;
70
+ } catch {
71
+ return false;
72
+ }
73
+ });
74
+ if (matchingPages.length > 0) {
75
+ return matchingPages[matchingPages.length - 1];
76
+ }
77
+ }
78
+ return pages[pages.length - 1];
79
+ }
80
+
81
+ async function resolveContextAndPage(browser, cdpPort) {
82
+ const contexts = browser.contexts();
83
+ const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
84
+ const pages = context.pages();
85
+ const activeUrl = cdpPort ? await fetchActivePageUrl(cdpPort) : null;
86
+ const page = pickPage(pages, activeUrl) || await context.newPage();
87
+ return { context, page };
88
+ }
89
+
64
90
  // ---------------------------------------------------------------------------
65
91
  // Our own CDP-based browser launcher (fallback when playwright-cli not running)
66
92
  // ---------------------------------------------------------------------------
@@ -131,10 +157,7 @@ async function getConnection({ headless = false, profile = 'default', port: pref
131
157
  if (alive) {
132
158
  try {
133
159
  const browser = await playwright.chromium.connectOverCDP(`http://127.0.0.1:${cliCdpPort}`);
134
- const contexts = browser.contexts();
135
- const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
136
- const pages = context.pages();
137
- const page = pages.length > 0 ? pages[0] : await context.newPage();
160
+ const { context, page } = await resolveContextAndPage(browser, cliCdpPort);
138
161
  return { browser, context, page, playwright };
139
162
  } catch {
140
163
  // fall through to own browser
@@ -163,10 +186,7 @@ async function getConnection({ headless = false, profile = 'default', port: pref
163
186
  }
164
187
 
165
188
  const browser = await playwright.chromium.connectOverCDP(cdpUrl);
166
- const contexts = browser.contexts();
167
- const context = contexts.length > 0 ? contexts[0] : await browser.newContext();
168
- const pages = context.pages();
169
- const page = pages.length > 0 ? pages[0] : await context.newPage();
189
+ const { context, page } = await resolveContextAndPage(browser, state ? state.port : null);
170
190
 
171
191
  return { browser, context, page, playwright };
172
192
  }
@@ -188,4 +208,4 @@ async function killBrowser() {
188
208
  return true;
189
209
  }
190
210
 
191
- module.exports = { getConnection, killBrowser, getPlaywrightCliCdpPort };
211
+ module.exports = { getConnection, killBrowser, getPlaywrightCliCdpPort, pickPage };
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
- What the script receives:
59
- - Playwright globals: page, context, browser, playwright
60
- - Script args array: args
61
- - CommonJS globals: require, module, exports, __filename, __dirname
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
- async function execScript(scriptPath, scriptArgs, { browser, context, page, playwright }) {
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
- scriptModule.require = scriptRequire;
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
- browser,
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
- const result = await withTemporaryGlobals(globals, () => runProgram(code, globals));
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
  }
@@ -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