barebrowse 0.14.0 → 0.17.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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * consent-patterns.js — Shared cookie-consent detection data.
3
+ *
4
+ * The engine-agnostic half of consent handling: the multilingual regex sets
5
+ * and dialog roles used to recognise a consent dialog and its "accept" button.
6
+ * Both the CDP walker (consent.js) and the BiDi walker (consent-firefox.js)
7
+ * import these so the language coverage is single-sourced — a new language or
8
+ * pattern is added once and both engines get it.
9
+ */
10
+
11
+ // Button text patterns that mean "accept all" / "I agree" across common languages.
12
+ // Order matters: more specific patterns first to avoid false positives.
13
+ export const ACCEPT_PATTERNS = [
14
+ // English
15
+ /\baccept\s*all\b/i,
16
+ /\ballow\s*all\b/i,
17
+ /\bagree\s*to\s*all\b/i,
18
+ /\byes,?\s*i\s*agree\b/i,
19
+ /\bi\s*agree\b/i,
20
+ /\baccept\s*cookies?\b/i,
21
+ /\ballow\s*cookies?\b/i,
22
+ /\bgot\s*it\b/i,
23
+ // Dutch
24
+ /\balles\s*accepteren\b/i,
25
+ /\balles\s*toestaan\b/i,
26
+ /\baccepteren\b/i,
27
+ /\bakkoord\b/i,
28
+ // German
29
+ /\balle\s*akzeptieren\b/i,
30
+ /\ballem\s*zustimmen\b/i,
31
+ /\balle\s*cookies?\s*akzeptieren\b/i,
32
+ // French
33
+ /\btout\s*accepter\b/i,
34
+ /\baccepter\s*tout\b/i,
35
+ /\bj['']accepte\b/i,
36
+ // Spanish
37
+ /\baceptar\s*todo\b/i,
38
+ /\baceptar\s*todas?\b/i,
39
+ // Italian
40
+ /\baccetta\s*tutto\b/i,
41
+ /\baccetto\b/i,
42
+ // Portuguese
43
+ /\baceitar\s*tudo\b/i,
44
+ // Russian
45
+ /принять\s*все/i,
46
+ /принять/i,
47
+ /согласен/i,
48
+ // Ukrainian
49
+ /прийняти\s*все/i,
50
+ /прийняти/i,
51
+ // Polish
52
+ /zaakceptuj\s*wszystk/i,
53
+ /akceptuj\s*wszystk/i,
54
+ /zgadzam\s*się/i,
55
+ // Czech
56
+ /přijmout\s*vše/i,
57
+ /souhlasím/i,
58
+ // Turkish
59
+ /tümünü\s*kabul\s*et/i,
60
+ /kabul\s*et/i,
61
+ /kabul\s*ediyorum/i,
62
+ // Romanian
63
+ /acceptă\s*tot/i,
64
+ /accept\s*toate/i,
65
+ // Hungarian
66
+ /összes\s*elfogadás/i,
67
+ /elfogad/i,
68
+ // Greek
69
+ /αποδοχή\s*όλων/i,
70
+ /αποδέχομαι/i,
71
+ // Swedish
72
+ /acceptera\s*alla/i,
73
+ /godkänn\s*alla/i,
74
+ // Danish
75
+ /accepter\s*alle/i,
76
+ /acceptér\s*alle/i,
77
+ // Norwegian
78
+ /godta\s*alle/i,
79
+ /aksepter\s*alle/i,
80
+ // Finnish
81
+ /hyväksy\s*kaikki/i,
82
+ /hyväksyn/i,
83
+ // Arabic
84
+ /قبول\s*الكل/,
85
+ /قبول\s*الجميع/,
86
+ /موافق/,
87
+ /قبول/,
88
+ // Persian
89
+ /پذیرش\s*همه/,
90
+ /موافقم/,
91
+ /پذیرش/,
92
+ // Chinese (Simplified + Traditional)
93
+ /全部接受/,
94
+ /接受所有/,
95
+ /接受全部/,
96
+ /同意并继续/,
97
+ /全部接受/,
98
+ /接受/,
99
+ /同意/,
100
+ // Japanese
101
+ /すべて受け入れ/,
102
+ /すべて許可/,
103
+ /同意する/,
104
+ /同意します/,
105
+ // Korean
106
+ /모두\s*수락/,
107
+ /모두\s*동의/,
108
+ /동의합니다/,
109
+ /수락/,
110
+ // Vietnamese
111
+ /chấp\s*nhận\s*tất\s*cả/i,
112
+ /đồng\s*ý\s*tất\s*cả/i,
113
+ /đồng\s*ý/i,
114
+ // Thai
115
+ /ยอมรับทั้งหมด/,
116
+ /ยอมรับ/,
117
+ // Hindi
118
+ /सभी\s*स्वीकार/,
119
+ /स्वीकार\s*करें/,
120
+ /सहमत/,
121
+ // Indonesian / Malay
122
+ /terima\s*semua/i,
123
+ /setuju/i,
124
+ // Generic single-word fallbacks (only matched inside dialogs)
125
+ /^accept$/i,
126
+ /^agree$/i,
127
+ /^ok$/i,
128
+ ];
129
+
130
+ // Roles that indicate a consent dialog container.
131
+ export const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
132
+
133
+ // Text patterns in dialog names/headings that confirm it's about consent.
134
+ export const CONSENT_DIALOG_HINTS = [
135
+ /cookie/i,
136
+ /consent/i,
137
+ /privacy/i,
138
+ /before\s*you\s*continue/i,
139
+ /voordat\s*je\s*verdergaat/i, // Dutch
140
+ /bevor\s*du\s*fortf/i, // German
141
+ /avant\s*de\s*continuer/i, // French
142
+ /antes\s*de\s*continuar/i, // Spanish / Portuguese
143
+ /prima\s*di\s*continuare/i, // Italian
144
+ /zanim\s*przejdziesz/i, // Polish
145
+ /прежде\s*чем\s*продолжить/i, // Russian
146
+ /devam\s*etmeden\s*önce/i, // Turkish
147
+ /続行する前に/, // Japanese
148
+ /继续前/, // Chinese Simplified
149
+ /繼續前/, // Chinese Traditional
150
+ /계속하기\s*전에/, // Korean
151
+ /trước\s*khi\s*tiếp\s*tục/i, // Vietnamese
152
+ /ملفات\s*تعريف\s*الارتباط/, // Arabic: cookies
153
+ /คุกกี้/, // Thai: cookies
154
+ ];
package/src/consent.js CHANGED
@@ -4,152 +4,13 @@
4
4
  * Scans the page ARIA tree for consent dialogs and clicks the "accept" button.
5
5
  * Works across languages by matching common accept/agree patterns.
6
6
  * Runs once after page load — no polling, no mutation observers.
7
+ *
8
+ * The multilingual pattern sets live in consent-patterns.js (shared with the
9
+ * Firefox/BiDi walker in consent-firefox.js); this file owns the CDP-specific
10
+ * tree walk + click.
7
11
  */
8
12
 
9
- // Button text patterns that mean "accept all" / "I agree" across common languages.
10
- // Order matters: more specific patterns first to avoid false positives.
11
- const ACCEPT_PATTERNS = [
12
- // English
13
- /\baccept\s*all\b/i,
14
- /\ballow\s*all\b/i,
15
- /\bagree\s*to\s*all\b/i,
16
- /\byes,?\s*i\s*agree\b/i,
17
- /\bi\s*agree\b/i,
18
- /\baccept\s*cookies?\b/i,
19
- /\ballow\s*cookies?\b/i,
20
- /\bgot\s*it\b/i,
21
- // Dutch
22
- /\balles\s*accepteren\b/i,
23
- /\balles\s*toestaan\b/i,
24
- /\baccepteren\b/i,
25
- /\bakkoord\b/i,
26
- // German
27
- /\balle\s*akzeptieren\b/i,
28
- /\ballem\s*zustimmen\b/i,
29
- /\balle\s*cookies?\s*akzeptieren\b/i,
30
- // French
31
- /\btout\s*accepter\b/i,
32
- /\baccepter\s*tout\b/i,
33
- /\bj['']accepte\b/i,
34
- // Spanish
35
- /\baceptar\s*todo\b/i,
36
- /\baceptar\s*todas?\b/i,
37
- // Italian
38
- /\baccetta\s*tutto\b/i,
39
- /\baccetto\b/i,
40
- // Portuguese
41
- /\baceitar\s*tudo\b/i,
42
- // Russian
43
- /принять\s*все/i,
44
- /принять/i,
45
- /согласен/i,
46
- // Ukrainian
47
- /прийняти\s*все/i,
48
- /прийняти/i,
49
- // Polish
50
- /zaakceptuj\s*wszystk/i,
51
- /akceptuj\s*wszystk/i,
52
- /zgadzam\s*się/i,
53
- // Czech
54
- /přijmout\s*vše/i,
55
- /souhlasím/i,
56
- // Turkish
57
- /tümünü\s*kabul\s*et/i,
58
- /kabul\s*et/i,
59
- /kabul\s*ediyorum/i,
60
- // Romanian
61
- /acceptă\s*tot/i,
62
- /accept\s*toate/i,
63
- // Hungarian
64
- /összes\s*elfogadás/i,
65
- /elfogad/i,
66
- // Greek
67
- /αποδοχή\s*όλων/i,
68
- /αποδέχομαι/i,
69
- // Swedish
70
- /acceptera\s*alla/i,
71
- /godkänn\s*alla/i,
72
- // Danish
73
- /accepter\s*alle/i,
74
- /acceptér\s*alle/i,
75
- // Norwegian
76
- /godta\s*alle/i,
77
- /aksepter\s*alle/i,
78
- // Finnish
79
- /hyväksy\s*kaikki/i,
80
- /hyväksyn/i,
81
- // Arabic
82
- /قبول\s*الكل/,
83
- /قبول\s*الجميع/,
84
- /موافق/,
85
- /قبول/,
86
- // Persian
87
- /پذیرش\s*همه/,
88
- /موافقم/,
89
- /پذیرش/,
90
- // Chinese (Simplified + Traditional)
91
- /全部接受/,
92
- /接受所有/,
93
- /接受全部/,
94
- /同意并继续/,
95
- /全部接受/,
96
- /接受/,
97
- /同意/,
98
- // Japanese
99
- /すべて受け入れ/,
100
- /すべて許可/,
101
- /同意する/,
102
- /同意します/,
103
- // Korean
104
- /모두\s*수락/,
105
- /모두\s*동의/,
106
- /동의합니다/,
107
- /수락/,
108
- // Vietnamese
109
- /chấp\s*nhận\s*tất\s*cả/i,
110
- /đồng\s*ý\s*tất\s*cả/i,
111
- /đồng\s*ý/i,
112
- // Thai
113
- /ยอมรับทั้งหมด/,
114
- /ยอมรับ/,
115
- // Hindi
116
- /सभी\s*स्वीकार/,
117
- /स्वीकार\s*करें/,
118
- /सहमत/,
119
- // Indonesian / Malay
120
- /terima\s*semua/i,
121
- /setuju/i,
122
- // Generic single-word fallbacks (only matched inside dialogs)
123
- /^accept$/i,
124
- /^agree$/i,
125
- /^ok$/i,
126
- ];
127
-
128
- // Roles that indicate a consent dialog container.
129
- const DIALOG_ROLES = new Set(['dialog', 'alertdialog']);
130
-
131
- // Text patterns in dialog names/headings that confirm it's about consent.
132
- const CONSENT_DIALOG_HINTS = [
133
- /cookie/i,
134
- /consent/i,
135
- /privacy/i,
136
- /before\s*you\s*continue/i,
137
- /voordat\s*je\s*verdergaat/i, // Dutch
138
- /bevor\s*du\s*fortf/i, // German
139
- /avant\s*de\s*continuer/i, // French
140
- /antes\s*de\s*continuar/i, // Spanish / Portuguese
141
- /prima\s*di\s*continuare/i, // Italian
142
- /zanim\s*przejdziesz/i, // Polish
143
- /прежде\s*чем\s*продолжить/i, // Russian
144
- /devam\s*etmeden\s*önce/i, // Turkish
145
- /続行する前に/, // Japanese
146
- /继续前/, // Chinese Simplified
147
- /繼續前/, // Chinese Traditional
148
- /계속하기\s*전에/, // Korean
149
- /trước\s*khi\s*tiếp\s*tục/i, // Vietnamese
150
- /ملفات\s*تعريف\s*الارتباط/, // Arabic: cookies
151
- /คุกกี้/, // Thai: cookies
152
- ];
13
+ import { ACCEPT_PATTERNS, DIALOG_ROLES, CONSENT_DIALOG_HINTS } from './consent-patterns.js';
153
14
 
154
15
  /**
155
16
  * Click a node via JavaScript .click() instead of mouse events.
package/src/daemon.js CHANGED
@@ -31,24 +31,25 @@ function tokenMatches(expected, got) {
31
31
  const SESSION_FILE = 'session.json';
32
32
 
33
33
  /**
34
- * Spawn a detached child process that runs the daemon.
35
- * Parent polls for session.json, then exits.
34
+ * Build the argv for the detached daemon child. Pure + exported so the
35
+ * flag-forwarding contract is unit-testable: a forward that's silently
36
+ * dropped here (as `--incognito` was in v0.15.0, turning `open --incognito`
37
+ * into a fully-authenticated session) becomes a failing test, not a leak.
38
+ * @param {object} opts - the session options parsed in cli.js cmdOpen
39
+ * @param {string} absDir - resolved output directory
40
+ * @param {string|undefined} initialUrl - URL to navigate on start (may be undefined)
41
+ * @param {string} cliPath - path to cli.js for the child to run
42
+ * @returns {string[]}
36
43
  */
37
- export async function startDaemon(opts, outputDir, initialUrl) {
38
- const absDir = resolve(outputDir);
39
- mkdirSync(absDir, { recursive: true, mode: 0o700 });
40
-
41
- // Clean stale session
42
- const sessionPath = join(absDir, SESSION_FILE);
43
- if (existsSync(sessionPath)) unlinkSync(sessionPath);
44
-
45
- // Build child args
46
- const args = [join(import.meta.dirname, '..', 'cli.js'), '--daemon-internal'];
44
+ export function buildDaemonArgs(opts, absDir, initialUrl, cliPath) {
45
+ const args = [cliPath, '--daemon-internal'];
47
46
  args.push('--output-dir', absDir);
48
47
  if (initialUrl) args.push('--url', initialUrl);
48
+ if (opts.engine) args.push('--engine', opts.engine);
49
49
  if (opts.mode) args.push('--mode', opts.mode);
50
50
  if (opts.port) args.push('--port', String(opts.port));
51
51
  if (opts.cookies === false) args.push('--no-cookies');
52
+ if (opts.incognito) args.push('--incognito');
52
53
  if (opts.browser) args.push('--browser', opts.browser);
53
54
  if (opts.timeout) args.push('--timeout', String(opts.timeout));
54
55
  if (opts.pruneMode) args.push('--prune-mode', opts.pruneMode);
@@ -63,6 +64,91 @@ export async function startDaemon(opts, outputDir, initialUrl) {
63
64
  }
64
65
  if (opts.blockPrivateNetwork) args.push('--block-private-network');
65
66
  if (opts.uploadDir) args.push('--upload-dir', opts.uploadDir);
67
+ return args;
68
+ }
69
+
70
+ /**
71
+ * Wire Firefox/BiDi console + network capture onto the daemon's log arrays.
72
+ * The BiDi analogue of the CDP `Runtime.consoleAPICalled` / `Network.*`
73
+ * capture — measured event shapes: `log.entryAdded` carries {method, level,
74
+ * args:[{type,value}], text}; the network events key in-flight requests by
75
+ * `request.request` and expose `response.{status,statusText,mimeType}` /
76
+ * top-level `errorText`. Pure + exported so the shape mapping (warn→warning
77
+ * normalization, arg extraction, orphan-safe response/error pairing) is
78
+ * unit-testable against a fake bidi without launching Firefox.
79
+ *
80
+ * @param {object} bidi - BiDi client (async .subscribe(), .on(method, cb))
81
+ * @param {object} sinks
82
+ * @param {Array} sinks.consoleLogs - push-target for console entries
83
+ * @param {Array} sinks.networkLogs - push-target for completed/failed requests
84
+ * @param {Map} sinks.pendingRequests - in-flight request bookkeeping
85
+ * @returns {Promise<void>}
86
+ */
87
+ export async function attachBiDiCapture(bidi, { consoleLogs, networkLogs, pendingRequests }) {
88
+ await bidi.subscribe([
89
+ 'log.entryAdded',
90
+ 'network.beforeRequestSent',
91
+ 'network.responseCompleted',
92
+ 'network.fetchError',
93
+ ]);
94
+
95
+ bidi.on('log.entryAdded', (e) => {
96
+ consoleLogs.push({
97
+ // Map BiDi's console method (or level for uncaught JS errors) to CDP's
98
+ // `type` vocabulary so `console-logs --level warning` matches on both
99
+ // engines — BiDi says 'warn', CDP says 'warning'.
100
+ type: e.method === 'warn' ? 'warning' : (e.method || e.level),
101
+ timestamp: new Date().toISOString(),
102
+ args: Array.isArray(e.args) && e.args.length
103
+ ? e.args.map((a) => a.value ?? a.type)
104
+ : [e.text],
105
+ });
106
+ });
107
+
108
+ bidi.on('network.beforeRequestSent', (e) => {
109
+ pendingRequests.set(e.request.request, {
110
+ url: e.request.url,
111
+ method: e.request.method,
112
+ timestamp: new Date().toISOString(),
113
+ });
114
+ });
115
+
116
+ bidi.on('network.responseCompleted', (e) => {
117
+ const req = pendingRequests.get(e.request.request);
118
+ if (req) {
119
+ networkLogs.push({
120
+ ...req,
121
+ status: e.response.status,
122
+ statusText: e.response.statusText,
123
+ mimeType: e.response.mimeType,
124
+ });
125
+ pendingRequests.delete(e.request.request);
126
+ }
127
+ });
128
+
129
+ bidi.on('network.fetchError', (e) => {
130
+ const req = pendingRequests.get(e.request.request);
131
+ if (req) {
132
+ networkLogs.push({ ...req, status: 0, error: e.errorText });
133
+ pendingRequests.delete(e.request.request);
134
+ }
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Spawn a detached child process that runs the daemon.
140
+ * Parent polls for session.json, then exits.
141
+ */
142
+ export async function startDaemon(opts, outputDir, initialUrl) {
143
+ const absDir = resolve(outputDir);
144
+ mkdirSync(absDir, { recursive: true, mode: 0o700 });
145
+
146
+ // Clean stale session
147
+ const sessionPath = join(absDir, SESSION_FILE);
148
+ if (existsSync(sessionPath)) unlinkSync(sessionPath);
149
+
150
+ // Build child args (pure + testable — see buildDaemonArgs).
151
+ const args = buildDaemonArgs(opts, absDir, initialUrl, join(import.meta.dirname, '..', 'cli.js'));
66
152
 
67
153
  const child = spawn(process.execPath, args, {
68
154
  detached: true,
@@ -104,6 +190,7 @@ export async function runDaemon(opts, outputDir, initialUrl) {
104
190
 
105
191
  // Connect to browser
106
192
  const page = await connect({
193
+ engine: opts.engine,
107
194
  mode: opts.mode || 'headless',
108
195
  port: opts.port ? Number(opts.port) : undefined,
109
196
  consent: opts.consent,
@@ -115,55 +202,65 @@ export async function runDaemon(opts, outputDir, initialUrl) {
115
202
  blockUrls: opts.blockUrls,
116
203
  blockPrivateNetwork: opts.blockPrivateNetwork,
117
204
  uploadDir: opts.uploadDir,
205
+ // incognito neuters injectCookies() session-wide; cookies:false (below)
206
+ // is the legacy per-open equivalent. Either yields a logged-out session.
207
+ incognito: opts.incognito || opts.cookies === false,
118
208
  });
119
209
 
120
- // Console log capture
210
+ // Console + network log capture. CDP uses Runtime/Network events off
211
+ // page.cdp; the Firefox/BiDi engine (page.bidi) captures the same shape over
212
+ // log.entryAdded / network.* events (Phase 2 — attachBiDiCapture). Either
213
+ // way console-logs / network-log / wait-idle behave the same for both.
121
214
  const consoleLogs = [];
122
- await page.cdp.send('Runtime.enable');
123
- page.cdp.on('Runtime.consoleAPICalled', (params) => {
124
- consoleLogs.push({
125
- type: params.type,
126
- timestamp: new Date().toISOString(),
127
- args: params.args.map((a) => a.value ?? a.description ?? a.type),
128
- });
129
- });
130
-
131
- // Network log capture (Network.enable already called by connect)
132
215
  const networkLogs = [];
133
216
  const pendingRequests = new Map();
134
217
 
135
- page.cdp.on('Network.requestWillBeSent', (params) => {
136
- pendingRequests.set(params.requestId, {
137
- url: params.request.url,
138
- method: params.request.method,
139
- timestamp: new Date().toISOString(),
218
+ if (page.cdp) {
219
+ await page.cdp.send('Runtime.enable');
220
+ page.cdp.on('Runtime.consoleAPICalled', (params) => {
221
+ consoleLogs.push({
222
+ type: params.type,
223
+ timestamp: new Date().toISOString(),
224
+ args: params.args.map((a) => a.value ?? a.description ?? a.type),
225
+ });
140
226
  });
141
- });
142
227
 
143
- page.cdp.on('Network.responseReceived', (params) => {
144
- const req = pendingRequests.get(params.requestId);
145
- if (req) {
146
- networkLogs.push({
147
- ...req,
148
- status: params.response.status,
149
- statusText: params.response.statusText,
150
- mimeType: params.response.mimeType,
228
+ // Network log capture (Network.enable already called by connect)
229
+ page.cdp.on('Network.requestWillBeSent', (params) => {
230
+ pendingRequests.set(params.requestId, {
231
+ url: params.request.url,
232
+ method: params.request.method,
233
+ timestamp: new Date().toISOString(),
151
234
  });
152
- pendingRequests.delete(params.requestId);
153
- }
154
- });
235
+ });
155
236
 
156
- page.cdp.on('Network.loadingFailed', (params) => {
157
- const req = pendingRequests.get(params.requestId);
158
- if (req) {
159
- networkLogs.push({
160
- ...req,
161
- status: 0,
162
- error: params.errorText,
163
- });
164
- pendingRequests.delete(params.requestId);
165
- }
166
- });
237
+ page.cdp.on('Network.responseReceived', (params) => {
238
+ const req = pendingRequests.get(params.requestId);
239
+ if (req) {
240
+ networkLogs.push({
241
+ ...req,
242
+ status: params.response.status,
243
+ statusText: params.response.statusText,
244
+ mimeType: params.response.mimeType,
245
+ });
246
+ pendingRequests.delete(params.requestId);
247
+ }
248
+ });
249
+
250
+ page.cdp.on('Network.loadingFailed', (params) => {
251
+ const req = pendingRequests.get(params.requestId);
252
+ if (req) {
253
+ networkLogs.push({
254
+ ...req,
255
+ status: 0,
256
+ error: params.errorText,
257
+ });
258
+ pendingRequests.delete(params.requestId);
259
+ }
260
+ });
261
+ } else if (page.bidi) {
262
+ await attachBiDiCapture(page.bidi, { consoleLogs, networkLogs, pendingRequests });
263
+ }
167
264
 
168
265
  // Navigate to initial URL if provided
169
266
  if (initialUrl) {
@@ -316,6 +413,20 @@ export async function runDaemon(opts, outputDir, initialUrl) {
316
413
  },
317
414
 
318
415
  async eval({ expression }) {
416
+ // Firefox/BiDi has no page.cdp — evaluate over BiDi in the active context.
417
+ if (!page.cdp) {
418
+ try {
419
+ // BiDi's raw result.value serializes objects into a {type,value}
420
+ // entries structure, not a plain object (CDP's returnByValue does).
421
+ // Stringify + parse in-page so an object/array eval matches the CDP
422
+ // shape (same trick readable() uses). `undefined` → JSON `null`.
423
+ const wrapped = `(async () => { const v = await (${expression}); return JSON.stringify(v === undefined ? null : v); })()`;
424
+ const raw = await page.bidi.evaluate(page.context, wrapped, true);
425
+ return { ok: true, value: raw == null ? null : JSON.parse(raw) };
426
+ } catch (err) {
427
+ return { ok: false, error: err.message || 'eval error' };
428
+ }
429
+ }
319
430
  const result = await page.cdp.send('Runtime.evaluate', {
320
431
  expression,
321
432
  returnByValue: true,