@szymonrybczak/playwright-mcp 0.0.2 → 0.0.4
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/index.d.ts +21 -8
- package/lib/cjs/_virtual/_commonjsHelpers.js +9 -0
- package/lib/cjs/_virtual/browser.js +11 -0
- package/lib/cjs/_virtual/browser2.js +7 -0
- package/lib/cjs/cloudflare/package.json.js +10 -0
- package/lib/cjs/index.js +28 -0
- package/lib/cjs/node_modules/debug/src/browser.js +290 -0
- package/lib/cjs/node_modules/debug/src/common.js +307 -0
- package/lib/cjs/node_modules/ms/index.js +176 -0
- package/lib/cjs/package.js +9 -0
- package/lib/cjs/src/browserContextFactory.js +240 -0
- package/lib/cjs/src/config.js +77 -0
- package/lib/cjs/src/connection.js +74 -0
- package/lib/cjs/src/context.js +281 -0
- package/lib/cjs/src/fileUtils.js +25 -0
- package/lib/cjs/src/index.js +15 -0
- package/lib/cjs/src/javascript.js +41 -0
- package/lib/cjs/src/manualPromise.js +39 -0
- package/lib/cjs/src/pageSnapshot.js +35 -0
- package/lib/cjs/src/tab.js +91 -0
- package/lib/cjs/src/tools/common.js +60 -0
- package/lib/cjs/src/tools/console.js +36 -0
- package/lib/cjs/src/tools/dialogs.js +44 -0
- package/lib/cjs/src/tools/files.js +43 -0
- package/lib/cjs/src/tools/install.js +49 -0
- package/lib/cjs/src/tools/keyboard.js +38 -0
- package/lib/cjs/src/tools/navigate.js +85 -0
- package/lib/cjs/src/tools/network.js +43 -0
- package/lib/cjs/src/tools/pdf.js +42 -0
- package/lib/cjs/src/tools/screenshot.js +69 -0
- package/lib/cjs/src/tools/snapshot.js +195 -0
- package/lib/cjs/src/tools/tabs.js +110 -0
- package/lib/cjs/src/tools/testing.js +52 -0
- package/lib/cjs/src/tools/tool.js +9 -0
- package/lib/cjs/src/tools/utils.js +75 -0
- package/lib/cjs/src/tools/vision.js +181 -0
- package/lib/cjs/src/tools/wait.js +51 -0
- package/lib/cjs/src/tools.js +54 -0
- package/lib/esm/_virtual/_commonjsHelpers.js +5 -0
- package/lib/esm/_virtual/browser.js +7 -0
- package/lib/esm/_virtual/browser2.js +3 -0
- package/lib/esm/cloudflare/package.json.js +5 -0
- package/lib/esm/index.js +24 -0
- package/lib/esm/node_modules/debug/src/browser.js +286 -0
- package/lib/esm/node_modules/debug/src/common.js +303 -0
- package/lib/esm/node_modules/ms/index.js +172 -0
- package/lib/esm/package.js +5 -0
- package/lib/esm/src/browserContextFactory.js +216 -0
- package/lib/esm/src/config.js +72 -0
- package/lib/esm/src/connection.js +69 -0
- package/lib/esm/src/context.js +277 -0
- package/lib/esm/src/fileUtils.js +20 -0
- package/lib/esm/src/index.js +11 -0
- package/lib/esm/src/javascript.js +35 -0
- package/lib/esm/src/manualPromise.js +35 -0
- package/lib/esm/src/pageSnapshot.js +31 -0
- package/lib/esm/src/tab.js +87 -0
- package/lib/esm/src/tools/common.js +56 -0
- package/lib/esm/src/tools/console.js +32 -0
- package/lib/esm/src/tools/dialogs.js +40 -0
- package/lib/esm/src/tools/files.js +39 -0
- package/lib/esm/src/tools/install.js +45 -0
- package/lib/esm/src/tools/keyboard.js +34 -0
- package/lib/esm/src/tools/navigate.js +81 -0
- package/lib/esm/src/tools/network.js +39 -0
- package/lib/esm/src/tools/pdf.js +38 -0
- package/lib/esm/src/tools/screenshot.js +65 -0
- package/lib/esm/src/tools/snapshot.js +191 -0
- package/lib/esm/src/tools/tabs.js +106 -0
- package/lib/esm/src/tools/testing.js +48 -0
- package/lib/esm/src/tools/tool.js +5 -0
- package/lib/esm/src/tools/utils.js +68 -0
- package/lib/esm/src/tools/vision.js +177 -0
- package/lib/esm/src/tools/wait.js +47 -0
- package/lib/esm/src/tools.js +49 -0
- package/package.json +15 -44
- package/LICENSE +0 -202
- package/README.md +0 -508
- package/cli.js +0 -18
- package/config.d.ts +0 -128
- package/index.js +0 -19
- package/lib/browserContextFactory.js +0 -227
- package/lib/browserServer.js +0 -151
- package/lib/config.js +0 -189
- package/lib/connection.js +0 -82
- package/lib/context.js +0 -291
- package/lib/fileUtils.js +0 -32
- package/lib/httpServer.js +0 -201
- package/lib/index.js +0 -36
- package/lib/javascript.js +0 -49
- package/lib/manualPromise.js +0 -111
- package/lib/package.js +0 -20
- package/lib/pageSnapshot.js +0 -43
- package/lib/program.js +0 -72
- package/lib/server.js +0 -48
- package/lib/tab.js +0 -101
- package/lib/tools/common.js +0 -68
- package/lib/tools/console.js +0 -44
- package/lib/tools/dialogs.js +0 -52
- package/lib/tools/files.js +0 -51
- package/lib/tools/install.js +0 -57
- package/lib/tools/keyboard.js +0 -46
- package/lib/tools/navigate.js +0 -93
- package/lib/tools/network.js +0 -51
- package/lib/tools/pdf.js +0 -49
- package/lib/tools/screenshot.js +0 -77
- package/lib/tools/snapshot.js +0 -204
- package/lib/tools/tabs.js +0 -118
- package/lib/tools/testing.js +0 -60
- package/lib/tools/tool.js +0 -18
- package/lib/tools/utils.js +0 -80
- package/lib/tools/vision.js +0 -189
- package/lib/tools/wait.js +0 -59
- package/lib/tools.js +0 -61
- package/lib/transport.js +0 -133
package/lib/context.js
DELETED
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import debug from 'debug';
|
|
17
|
-
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
|
|
18
|
-
import { ManualPromise } from './manualPromise.js';
|
|
19
|
-
import { Tab } from './tab.js';
|
|
20
|
-
import { outputFile } from './config.js';
|
|
21
|
-
const testDebug = debug('pw:mcp:test');
|
|
22
|
-
export class Context {
|
|
23
|
-
tools;
|
|
24
|
-
config;
|
|
25
|
-
_browserContextPromise;
|
|
26
|
-
_browserContextFactory;
|
|
27
|
-
_tabs = [];
|
|
28
|
-
_currentTab;
|
|
29
|
-
_modalStates = [];
|
|
30
|
-
_pendingAction;
|
|
31
|
-
_downloads = [];
|
|
32
|
-
clientVersion;
|
|
33
|
-
constructor(tools, config, browserContextFactory) {
|
|
34
|
-
this.tools = tools;
|
|
35
|
-
this.config = config;
|
|
36
|
-
this._browserContextFactory = browserContextFactory;
|
|
37
|
-
testDebug('create context');
|
|
38
|
-
}
|
|
39
|
-
clientSupportsImages() {
|
|
40
|
-
if (this.config.imageResponses === 'allow')
|
|
41
|
-
return true;
|
|
42
|
-
if (this.config.imageResponses === 'omit')
|
|
43
|
-
return false;
|
|
44
|
-
return !this.clientVersion?.name.includes('cursor');
|
|
45
|
-
}
|
|
46
|
-
modalStates() {
|
|
47
|
-
return this._modalStates;
|
|
48
|
-
}
|
|
49
|
-
setModalState(modalState, inTab) {
|
|
50
|
-
this._modalStates.push({ ...modalState, tab: inTab });
|
|
51
|
-
}
|
|
52
|
-
clearModalState(modalState) {
|
|
53
|
-
this._modalStates = this._modalStates.filter(state => state !== modalState);
|
|
54
|
-
}
|
|
55
|
-
modalStatesMarkdown() {
|
|
56
|
-
const result = ['### Modal state'];
|
|
57
|
-
if (this._modalStates.length === 0)
|
|
58
|
-
result.push('- There is no modal state present');
|
|
59
|
-
for (const state of this._modalStates) {
|
|
60
|
-
const tool = this.tools.find(tool => tool.clearsModalState === state.type);
|
|
61
|
-
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
62
|
-
}
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
tabs() {
|
|
66
|
-
return this._tabs;
|
|
67
|
-
}
|
|
68
|
-
currentTabOrDie() {
|
|
69
|
-
if (!this._currentTab)
|
|
70
|
-
throw new Error('No current snapshot available. Capture a snapshot or navigate to a new location first.');
|
|
71
|
-
return this._currentTab;
|
|
72
|
-
}
|
|
73
|
-
async newTab() {
|
|
74
|
-
const { browserContext } = await this._ensureBrowserContext();
|
|
75
|
-
const page = await browserContext.newPage();
|
|
76
|
-
this._currentTab = this._tabs.find(t => t.page === page);
|
|
77
|
-
return this._currentTab;
|
|
78
|
-
}
|
|
79
|
-
async selectTab(index) {
|
|
80
|
-
this._currentTab = this._tabs[index - 1];
|
|
81
|
-
await this._currentTab.page.bringToFront();
|
|
82
|
-
}
|
|
83
|
-
async ensureTab() {
|
|
84
|
-
const { browserContext } = await this._ensureBrowserContext();
|
|
85
|
-
if (!this._currentTab)
|
|
86
|
-
await browserContext.newPage();
|
|
87
|
-
return this._currentTab;
|
|
88
|
-
}
|
|
89
|
-
async listTabsMarkdown() {
|
|
90
|
-
if (!this._tabs.length)
|
|
91
|
-
return '### No tabs open';
|
|
92
|
-
const lines = ['### Open tabs'];
|
|
93
|
-
for (let i = 0; i < this._tabs.length; i++) {
|
|
94
|
-
const tab = this._tabs[i];
|
|
95
|
-
const title = await tab.title();
|
|
96
|
-
const url = tab.page.url();
|
|
97
|
-
const current = tab === this._currentTab ? ' (current)' : '';
|
|
98
|
-
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
|
99
|
-
}
|
|
100
|
-
return lines.join('\n');
|
|
101
|
-
}
|
|
102
|
-
async closeTab(index) {
|
|
103
|
-
const tab = index === undefined ? this._currentTab : this._tabs[index - 1];
|
|
104
|
-
await tab?.page.close();
|
|
105
|
-
return await this.listTabsMarkdown();
|
|
106
|
-
}
|
|
107
|
-
async run(tool, params) {
|
|
108
|
-
// Tab management is done outside of the action() call.
|
|
109
|
-
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
110
|
-
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
111
|
-
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : undefined;
|
|
112
|
-
if (resultOverride)
|
|
113
|
-
return resultOverride;
|
|
114
|
-
if (!this._currentTab) {
|
|
115
|
-
return {
|
|
116
|
-
content: [{
|
|
117
|
-
type: 'text',
|
|
118
|
-
text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.',
|
|
119
|
-
}],
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
const tab = this.currentTabOrDie();
|
|
123
|
-
// TODO: race against modal dialogs to resolve clicks.
|
|
124
|
-
let actionResult;
|
|
125
|
-
try {
|
|
126
|
-
if (waitForNetwork)
|
|
127
|
-
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? undefined;
|
|
128
|
-
else
|
|
129
|
-
actionResult = await racingAction?.() ?? undefined;
|
|
130
|
-
}
|
|
131
|
-
finally {
|
|
132
|
-
if (captureSnapshot && !this._javaScriptBlocked())
|
|
133
|
-
await tab.captureSnapshot();
|
|
134
|
-
}
|
|
135
|
-
const result = [];
|
|
136
|
-
result.push(`- Ran Playwright code:
|
|
137
|
-
\`\`\`js
|
|
138
|
-
${code.join('\n')}
|
|
139
|
-
\`\`\`
|
|
140
|
-
`);
|
|
141
|
-
if (this.modalStates().length) {
|
|
142
|
-
result.push(...this.modalStatesMarkdown());
|
|
143
|
-
return {
|
|
144
|
-
content: [{
|
|
145
|
-
type: 'text',
|
|
146
|
-
text: result.join('\n'),
|
|
147
|
-
}],
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
if (this._downloads.length) {
|
|
151
|
-
result.push('', '### Downloads');
|
|
152
|
-
for (const entry of this._downloads) {
|
|
153
|
-
if (entry.finished)
|
|
154
|
-
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
|
155
|
-
else
|
|
156
|
-
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
|
157
|
-
}
|
|
158
|
-
result.push('');
|
|
159
|
-
}
|
|
160
|
-
if (this.tabs().length > 1)
|
|
161
|
-
result.push(await this.listTabsMarkdown(), '');
|
|
162
|
-
if (this.tabs().length > 1)
|
|
163
|
-
result.push('### Current tab');
|
|
164
|
-
result.push(`- Page URL: ${tab.page.url()}`, `- Page Title: ${await tab.title()}`);
|
|
165
|
-
if (captureSnapshot && tab.hasSnapshot())
|
|
166
|
-
result.push(tab.snapshotOrDie().text());
|
|
167
|
-
const content = actionResult?.content ?? [];
|
|
168
|
-
return {
|
|
169
|
-
content: [
|
|
170
|
-
...content,
|
|
171
|
-
{
|
|
172
|
-
type: 'text',
|
|
173
|
-
text: result.join('\n'),
|
|
174
|
-
}
|
|
175
|
-
],
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
async waitForTimeout(time) {
|
|
179
|
-
if (!this._currentTab || this._javaScriptBlocked()) {
|
|
180
|
-
await new Promise(f => setTimeout(f, time));
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
await callOnPageNoTrace(this._currentTab.page, page => {
|
|
184
|
-
return page.evaluate(() => new Promise(f => setTimeout(f, 1000)));
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
async _raceAgainstModalDialogs(action) {
|
|
188
|
-
this._pendingAction = {
|
|
189
|
-
dialogShown: new ManualPromise(),
|
|
190
|
-
};
|
|
191
|
-
let result;
|
|
192
|
-
try {
|
|
193
|
-
await Promise.race([
|
|
194
|
-
action().then(r => result = r),
|
|
195
|
-
this._pendingAction.dialogShown,
|
|
196
|
-
]);
|
|
197
|
-
}
|
|
198
|
-
finally {
|
|
199
|
-
this._pendingAction = undefined;
|
|
200
|
-
}
|
|
201
|
-
return result;
|
|
202
|
-
}
|
|
203
|
-
_javaScriptBlocked() {
|
|
204
|
-
return this._modalStates.some(state => state.type === 'dialog');
|
|
205
|
-
}
|
|
206
|
-
dialogShown(tab, dialog) {
|
|
207
|
-
this.setModalState({
|
|
208
|
-
type: 'dialog',
|
|
209
|
-
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
|
210
|
-
dialog,
|
|
211
|
-
}, tab);
|
|
212
|
-
this._pendingAction?.dialogShown.resolve();
|
|
213
|
-
}
|
|
214
|
-
async downloadStarted(tab, download) {
|
|
215
|
-
const entry = {
|
|
216
|
-
download,
|
|
217
|
-
finished: false,
|
|
218
|
-
outputFile: await outputFile(this.config, download.suggestedFilename())
|
|
219
|
-
};
|
|
220
|
-
this._downloads.push(entry);
|
|
221
|
-
await download.saveAs(entry.outputFile);
|
|
222
|
-
entry.finished = true;
|
|
223
|
-
}
|
|
224
|
-
_onPageCreated(page) {
|
|
225
|
-
const tab = new Tab(this, page, tab => this._onPageClosed(tab));
|
|
226
|
-
this._tabs.push(tab);
|
|
227
|
-
if (!this._currentTab)
|
|
228
|
-
this._currentTab = tab;
|
|
229
|
-
}
|
|
230
|
-
_onPageClosed(tab) {
|
|
231
|
-
this._modalStates = this._modalStates.filter(state => state.tab !== tab);
|
|
232
|
-
const index = this._tabs.indexOf(tab);
|
|
233
|
-
if (index === -1)
|
|
234
|
-
return;
|
|
235
|
-
this._tabs.splice(index, 1);
|
|
236
|
-
if (this._currentTab === tab)
|
|
237
|
-
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
238
|
-
if (!this._tabs.length)
|
|
239
|
-
void this.close();
|
|
240
|
-
}
|
|
241
|
-
async close() {
|
|
242
|
-
if (!this._browserContextPromise)
|
|
243
|
-
return;
|
|
244
|
-
testDebug('close context');
|
|
245
|
-
const promise = this._browserContextPromise;
|
|
246
|
-
this._browserContextPromise = undefined;
|
|
247
|
-
await promise.then(async ({ browserContext, close }) => {
|
|
248
|
-
if (this.config.saveTrace)
|
|
249
|
-
await browserContext.tracing.stop();
|
|
250
|
-
await close();
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
async _setupRequestInterception(context) {
|
|
254
|
-
if (this.config.network?.allowedOrigins?.length) {
|
|
255
|
-
await context.route('**', route => route.abort('blockedbyclient'));
|
|
256
|
-
for (const origin of this.config.network.allowedOrigins)
|
|
257
|
-
await context.route(`*://${origin}/**`, route => route.continue());
|
|
258
|
-
}
|
|
259
|
-
if (this.config.network?.blockedOrigins?.length) {
|
|
260
|
-
for (const origin of this.config.network.blockedOrigins)
|
|
261
|
-
await context.route(`*://${origin}/**`, route => route.abort('blockedbyclient'));
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
_ensureBrowserContext() {
|
|
265
|
-
if (!this._browserContextPromise) {
|
|
266
|
-
this._browserContextPromise = this._setupBrowserContext();
|
|
267
|
-
this._browserContextPromise.catch(() => {
|
|
268
|
-
this._browserContextPromise = undefined;
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
return this._browserContextPromise;
|
|
272
|
-
}
|
|
273
|
-
async _setupBrowserContext() {
|
|
274
|
-
// TODO: move to the browser context factory to make it based on isolation mode.
|
|
275
|
-
const result = await this._browserContextFactory.createContext();
|
|
276
|
-
const { browserContext } = result;
|
|
277
|
-
await this._setupRequestInterception(browserContext);
|
|
278
|
-
for (const page of browserContext.pages())
|
|
279
|
-
this._onPageCreated(page);
|
|
280
|
-
browserContext.on('page', page => this._onPageCreated(page));
|
|
281
|
-
if (this.config.saveTrace) {
|
|
282
|
-
await browserContext.tracing.start({
|
|
283
|
-
name: 'trace',
|
|
284
|
-
screenshots: false,
|
|
285
|
-
snapshots: true,
|
|
286
|
-
sources: false,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
}
|
package/lib/fileUtils.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import os from 'node:os';
|
|
17
|
-
import path from 'node:path';
|
|
18
|
-
export function cacheDir() {
|
|
19
|
-
let cacheDirectory;
|
|
20
|
-
if (process.platform === 'linux')
|
|
21
|
-
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
22
|
-
else if (process.platform === 'darwin')
|
|
23
|
-
cacheDirectory = path.join(os.homedir(), 'Library', 'Caches');
|
|
24
|
-
else if (process.platform === 'win32')
|
|
25
|
-
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
26
|
-
else
|
|
27
|
-
throw new Error('Unsupported platform: ' + process.platform);
|
|
28
|
-
return path.join(cacheDirectory, 'ms-playwright');
|
|
29
|
-
}
|
|
30
|
-
export async function userDataDir(browserConfig) {
|
|
31
|
-
return path.join(cacheDir(), 'ms-playwright', `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
32
|
-
}
|
package/lib/httpServer.js
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import fs from 'fs';
|
|
17
|
-
import path from 'path';
|
|
18
|
-
import http from 'http';
|
|
19
|
-
import mime from 'mime';
|
|
20
|
-
import { ManualPromise } from './manualPromise.js';
|
|
21
|
-
export class HttpServer {
|
|
22
|
-
_server;
|
|
23
|
-
_urlPrefixPrecise = '';
|
|
24
|
-
_urlPrefixHumanReadable = '';
|
|
25
|
-
_port = 0;
|
|
26
|
-
_routes = [];
|
|
27
|
-
constructor() {
|
|
28
|
-
this._server = http.createServer(this._onRequest.bind(this));
|
|
29
|
-
decorateServer(this._server);
|
|
30
|
-
}
|
|
31
|
-
server() {
|
|
32
|
-
return this._server;
|
|
33
|
-
}
|
|
34
|
-
routePrefix(prefix, handler) {
|
|
35
|
-
this._routes.push({ prefix, handler });
|
|
36
|
-
}
|
|
37
|
-
routePath(path, handler) {
|
|
38
|
-
this._routes.push({ exact: path, handler });
|
|
39
|
-
}
|
|
40
|
-
port() {
|
|
41
|
-
return this._port;
|
|
42
|
-
}
|
|
43
|
-
async _tryStart(port, host) {
|
|
44
|
-
const errorPromise = new ManualPromise();
|
|
45
|
-
const errorListener = (error) => errorPromise.reject(error);
|
|
46
|
-
this._server.on('error', errorListener);
|
|
47
|
-
try {
|
|
48
|
-
this._server.listen(port, host);
|
|
49
|
-
await Promise.race([
|
|
50
|
-
new Promise(cb => this._server.once('listening', cb)),
|
|
51
|
-
errorPromise,
|
|
52
|
-
]);
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
this._server.removeListener('error', errorListener);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async start(options = {}) {
|
|
59
|
-
const host = options.host || 'localhost';
|
|
60
|
-
if (options.preferredPort) {
|
|
61
|
-
try {
|
|
62
|
-
await this._tryStart(options.preferredPort, host);
|
|
63
|
-
}
|
|
64
|
-
catch (e) {
|
|
65
|
-
if (!e || !e.message || !e.message.includes('EADDRINUSE'))
|
|
66
|
-
throw e;
|
|
67
|
-
await this._tryStart(undefined, host);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
else {
|
|
71
|
-
await this._tryStart(options.port, host);
|
|
72
|
-
}
|
|
73
|
-
const address = this._server.address();
|
|
74
|
-
if (typeof address === 'string') {
|
|
75
|
-
this._urlPrefixPrecise = address;
|
|
76
|
-
this._urlPrefixHumanReadable = address;
|
|
77
|
-
}
|
|
78
|
-
else {
|
|
79
|
-
this._port = address.port;
|
|
80
|
-
const resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
|
|
81
|
-
this._urlPrefixPrecise = `http://${resolvedHost}:${address.port}`;
|
|
82
|
-
this._urlPrefixHumanReadable = `http://${host}:${address.port}`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
async stop() {
|
|
86
|
-
await new Promise(cb => this._server.close(cb));
|
|
87
|
-
}
|
|
88
|
-
urlPrefix(purpose) {
|
|
89
|
-
return purpose === 'human-readable' ? this._urlPrefixHumanReadable : this._urlPrefixPrecise;
|
|
90
|
-
}
|
|
91
|
-
serveFile(request, response, absoluteFilePath, headers) {
|
|
92
|
-
try {
|
|
93
|
-
for (const [name, value] of Object.entries(headers || {}))
|
|
94
|
-
response.setHeader(name, value);
|
|
95
|
-
if (request.headers.range)
|
|
96
|
-
this._serveRangeFile(request, response, absoluteFilePath);
|
|
97
|
-
else
|
|
98
|
-
this._serveFile(response, absoluteFilePath);
|
|
99
|
-
return true;
|
|
100
|
-
}
|
|
101
|
-
catch (e) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
_serveFile(response, absoluteFilePath) {
|
|
106
|
-
const content = fs.readFileSync(absoluteFilePath);
|
|
107
|
-
response.statusCode = 200;
|
|
108
|
-
const contentType = mime.getType(path.extname(absoluteFilePath)) || 'application/octet-stream';
|
|
109
|
-
response.setHeader('Content-Type', contentType);
|
|
110
|
-
response.setHeader('Content-Length', content.byteLength);
|
|
111
|
-
response.end(content);
|
|
112
|
-
}
|
|
113
|
-
_serveRangeFile(request, response, absoluteFilePath) {
|
|
114
|
-
const range = request.headers.range;
|
|
115
|
-
if (!range || !range.startsWith('bytes=') || range.includes(', ') || [...range].filter(char => char === '-').length !== 1) {
|
|
116
|
-
response.statusCode = 400;
|
|
117
|
-
return response.end('Bad request');
|
|
118
|
-
}
|
|
119
|
-
// Parse the range header: https://datatracker.ietf.org/doc/html/rfc7233#section-2.1
|
|
120
|
-
const [startStr, endStr] = range.replace(/bytes=/, '').split('-');
|
|
121
|
-
// Both start and end (when passing to fs.createReadStream) and the range header are inclusive and start counting at 0.
|
|
122
|
-
let start;
|
|
123
|
-
let end;
|
|
124
|
-
const size = fs.statSync(absoluteFilePath).size;
|
|
125
|
-
if (startStr !== '' && endStr === '') {
|
|
126
|
-
// No end specified: use the whole file
|
|
127
|
-
start = +startStr;
|
|
128
|
-
end = size - 1;
|
|
129
|
-
}
|
|
130
|
-
else if (startStr === '' && endStr !== '') {
|
|
131
|
-
// No start specified: calculate start manually
|
|
132
|
-
start = size - +endStr;
|
|
133
|
-
end = size - 1;
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
start = +startStr;
|
|
137
|
-
end = +endStr;
|
|
138
|
-
}
|
|
139
|
-
// Handle unavailable range request
|
|
140
|
-
if (Number.isNaN(start) || Number.isNaN(end) || start >= size || end >= size || start > end) {
|
|
141
|
-
// Return the 416 Range Not Satisfiable: https://datatracker.ietf.org/doc/html/rfc7233#section-4.4
|
|
142
|
-
response.writeHead(416, {
|
|
143
|
-
'Content-Range': `bytes */${size}`
|
|
144
|
-
});
|
|
145
|
-
return response.end();
|
|
146
|
-
}
|
|
147
|
-
// Sending Partial Content: https://datatracker.ietf.org/doc/html/rfc7233#section-4.1
|
|
148
|
-
response.writeHead(206, {
|
|
149
|
-
'Content-Range': `bytes ${start}-${end}/${size}`,
|
|
150
|
-
'Accept-Ranges': 'bytes',
|
|
151
|
-
'Content-Length': end - start + 1,
|
|
152
|
-
'Content-Type': mime.getType(path.extname(absoluteFilePath)),
|
|
153
|
-
});
|
|
154
|
-
const readable = fs.createReadStream(absoluteFilePath, { start, end });
|
|
155
|
-
readable.pipe(response);
|
|
156
|
-
}
|
|
157
|
-
_onRequest(request, response) {
|
|
158
|
-
if (request.method === 'OPTIONS') {
|
|
159
|
-
response.writeHead(200);
|
|
160
|
-
response.end();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
request.on('error', () => response.end());
|
|
164
|
-
try {
|
|
165
|
-
if (!request.url) {
|
|
166
|
-
response.end();
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
const url = new URL('http://localhost' + request.url);
|
|
170
|
-
for (const route of this._routes) {
|
|
171
|
-
if (route.exact && url.pathname === route.exact) {
|
|
172
|
-
route.handler(request, response);
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (route.prefix && url.pathname.startsWith(route.prefix)) {
|
|
176
|
-
route.handler(request, response);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
response.statusCode = 404;
|
|
181
|
-
response.end();
|
|
182
|
-
}
|
|
183
|
-
catch (e) {
|
|
184
|
-
response.end();
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
function decorateServer(server) {
|
|
189
|
-
const sockets = new Set();
|
|
190
|
-
server.on('connection', socket => {
|
|
191
|
-
sockets.add(socket);
|
|
192
|
-
socket.once('close', () => sockets.delete(socket));
|
|
193
|
-
});
|
|
194
|
-
const close = server.close;
|
|
195
|
-
server.close = (callback) => {
|
|
196
|
-
for (const socket of sockets)
|
|
197
|
-
socket.destroy();
|
|
198
|
-
sockets.clear();
|
|
199
|
-
return close.call(server, callback);
|
|
200
|
-
};
|
|
201
|
-
}
|
package/lib/index.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
import { createConnection as createConnectionImpl } from './connection.js';
|
|
17
|
-
import { resolveConfig } from './config.js';
|
|
18
|
-
import { contextFactory } from './browserContextFactory.js';
|
|
19
|
-
export async function createConnection(userConfig = {}, contextGetter) {
|
|
20
|
-
const config = await resolveConfig(userConfig);
|
|
21
|
-
const factory = contextGetter ? new SimpleBrowserContextFactory(contextGetter) : contextFactory(config.browser);
|
|
22
|
-
return createConnectionImpl(config, factory);
|
|
23
|
-
}
|
|
24
|
-
class SimpleBrowserContextFactory {
|
|
25
|
-
_contextGetter;
|
|
26
|
-
constructor(contextGetter) {
|
|
27
|
-
this._contextGetter = contextGetter;
|
|
28
|
-
}
|
|
29
|
-
async createContext() {
|
|
30
|
-
const browserContext = await this._contextGetter();
|
|
31
|
-
return {
|
|
32
|
-
browserContext,
|
|
33
|
-
close: () => browserContext.close()
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
}
|
package/lib/javascript.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright (c) Microsoft Corporation.
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
// adapted from:
|
|
17
|
-
// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/utils/isomorphic/stringUtils.ts
|
|
18
|
-
// - https://github.com/microsoft/playwright/blob/76ee48dc9d4034536e3ec5b2c7ce8be3b79418a8/packages/playwright-core/src/server/codegen/javascript.ts
|
|
19
|
-
// NOTE: this function should not be used to escape any selectors.
|
|
20
|
-
export function escapeWithQuotes(text, char = '\'') {
|
|
21
|
-
const stringified = JSON.stringify(text);
|
|
22
|
-
const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
|
|
23
|
-
if (char === '\'')
|
|
24
|
-
return char + escapedText.replace(/[']/g, '\\\'') + char;
|
|
25
|
-
if (char === '"')
|
|
26
|
-
return char + escapedText.replace(/["]/g, '\\"') + char;
|
|
27
|
-
if (char === '`')
|
|
28
|
-
return char + escapedText.replace(/[`]/g, '`') + char;
|
|
29
|
-
throw new Error('Invalid escape char');
|
|
30
|
-
}
|
|
31
|
-
export function quote(text) {
|
|
32
|
-
return escapeWithQuotes(text, '\'');
|
|
33
|
-
}
|
|
34
|
-
export function formatObject(value, indent = ' ') {
|
|
35
|
-
if (typeof value === 'string')
|
|
36
|
-
return quote(value);
|
|
37
|
-
if (Array.isArray(value))
|
|
38
|
-
return `[${value.map(o => formatObject(o)).join(', ')}]`;
|
|
39
|
-
if (typeof value === 'object') {
|
|
40
|
-
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
|
|
41
|
-
if (!keys.length)
|
|
42
|
-
return '{}';
|
|
43
|
-
const tokens = [];
|
|
44
|
-
for (const key of keys)
|
|
45
|
-
tokens.push(`${key}: ${formatObject(value[key])}`);
|
|
46
|
-
return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
|
|
47
|
-
}
|
|
48
|
-
return String(value);
|
|
49
|
-
}
|