chore: allow client operation w/o local utils (#34790)

This commit is contained in:
Pavel Feldman 2025-02-13 16:15:11 -08:00 committed by GitHub
parent 90ec838318
commit 163aacf4b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 646 additions and 226 deletions

View File

@ -1,5 +1,4 @@
[*]
../common/
../protocol/
../utils/**
../utilsBundle.ts
../utils/isomorphic

View File

@ -25,6 +25,7 @@ import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { connectOverWebSocket } from './webSocket';
import type { Page } from './page';
import type * as types from './types';
@ -69,9 +70,8 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
return await this._wrapApiCall(async () => {
const deadline = options.timeout ? monotonicTime() + options.timeout : 0;
const headers = { 'x-playwright-browser': 'android', ...options.headers };
const localUtils = this._connection.localUtils();
const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: options.slowMo, timeout: options.timeout };
const connection = await localUtils.connect(connectParams);
const connection = await connectOverWebSocket(this._connection, connectParams);
let device: AndroidDevice;
connection.on('close', () => {

View File

@ -16,7 +16,7 @@
import { ChannelOwner } from './channelOwner';
import { Stream } from './stream';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import type * as channels from '@protocol/channels';
import type { Readable } from 'stream';

View File

@ -20,7 +20,7 @@ import { CDPSession } from './cdpSession';
import { ChannelOwner } from './channelOwner';
import { isTargetClosedError } from './errors';
import { Events } from './events';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import type { BrowserType } from './browserType';
import type { Page } from './page';

View File

@ -35,7 +35,7 @@ import { Waiter } from './waiter';
import { WebError } from './webError';
import { Worker } from './worker';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { urlMatchesEqual } from '../utils/isomorphic/urlMatch';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
@ -361,11 +361,14 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
}
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full' } = {}): Promise<void> {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Route from har is not supported in thin clients');
if (options.update) {
await this._recordIntoHAR(har, null, options);
return;
}
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });
this._harRouters.push(harRouter);
await harRouter.addContextRoute(this);
}
@ -484,8 +487,11 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
const isCompressed = harParams.content === 'attach' || harParams.path.endsWith('.zip');
const needCompressed = harParams.path.endsWith('.zip');
if (isCompressed && !needCompressed) {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Uncompressed har is not supported in thin clients');
await artifact.saveAs(harParams.path + '.tmp');
await this._connection.localUtils().harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
await localUtils.harUnzip({ zipFile: harParams.path + '.tmp', harFile: harParams.path });
} else {
await artifact.saveAs(harParams.path);
}

View File

@ -25,6 +25,7 @@ import { assert } from '../utils/isomorphic/debug';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { monotonicTime } from '../utils/isomorphic/time';
import { raceAgainstDeadline } from '../utils/isomorphic/timeoutRunner';
import { connectOverWebSocket } from './webSocket';
import type { Playwright } from './playwright';
import type { ConnectOptions, LaunchOptions, LaunchPersistentContextOptions, LaunchServerOptions, Logger } from './types';
@ -124,7 +125,6 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
return await this._wrapApiCall(async () => {
const deadline = params.timeout ? monotonicTime() + params.timeout : 0;
const headers = { 'x-playwright-browser': this.name(), ...params.headers };
const localUtils = this._connection.localUtils();
const connectParams: channels.LocalUtilsConnectParams = {
wsEndpoint: params.wsEndpoint,
headers,
@ -134,7 +134,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
};
if ((params as any).__testHookRedirectPortForwarding)
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
const connection = await localUtils.connect(connectParams);
const connection = await connectOverWebSocket(this._connection, connectParams);
let browser: Browser;
connection.on('close', () => {
// Emulate all pages, contexts and the browser closing upon disconnect.

View File

@ -108,8 +108,8 @@ export class Connection extends EventEmitter {
return this._rawBuffers;
}
localUtils(): LocalUtils {
return this._localUtils!;
localUtils(): LocalUtils | undefined {
return this._localUtils;
}
async initializePlaywright(): Promise<Playwright> {

View File

@ -20,10 +20,10 @@ import { promisify } from 'util';
import { Frame } from './frame';
import { JSHandle, parseResult, serializeArgument } from './jsHandle';
import { assert } from '../utils/isomorphic/debug';
import { fileUploadSizeLimit, mkdirIfNeeded } from '../common/fileUtils';
import { fileUploadSizeLimit, mkdirIfNeeded } from './fileUtils';
import { isString } from '../utils/isomorphic/rtti';
import { mime } from '../utilsBundle';
import { WritableStream } from './writableStream';
import { getMimeTypeForPath } from '../utils/isomorphic/mimeType';
import type { BrowserContext } from './browserContext';
import type { ChannelOwner } from './channelOwner';
@ -327,7 +327,7 @@ export async function convertInputFiles(platform: Platform, files: string | File
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
if (options.path) {
const mimeType = mime.getType(options.path);
const mimeType = getMimeTypeForPath(options.path);
if (mimeType === 'image/png')
return 'png';
else if (mimeType === 'image/jpeg')

View File

@ -20,7 +20,7 @@ import { TargetClosedError, isTargetClosedError } from './errors';
import { RawHeaders } from './network';
import { Tracing } from './tracing';
import { assert } from '../utils/isomorphic/debug';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { isString } from '../utils/isomorphic/rtti';

View File

@ -14,17 +14,12 @@
* limitations under the License.
*/
import type { Platform } from './platform';
import type { Platform } from '../common/platform';
// Keep in sync with the server.
export const fileUploadSizeLimit = 50 * 1024 * 1024;
export async function mkdirIfNeeded(platform: Platform, filePath: string) {
// This will harmlessly throw on windows if the dirname is the root directory.
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {});
}
export async function removeFolders(platform: Platform, dirs: string[]): Promise<Error[]> {
return await Promise.all(dirs.map((dir: string) =>
platform.fs().promises.rm(dir, { recursive: true, force: true, maxRetries: 10 }).catch(e => e)
));
}

View File

@ -15,12 +15,8 @@
*/
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import * as localUtils from '../common/localUtils';
import type { HeadersArray, Size } from './types';
import type { HarBackend } from '../common/harBackend';
import type { Platform } from '../common/platform';
import type { Size } from './types';
import type * as channels from '@protocol/channels';
type DeviceDescriptor = {
@ -35,8 +31,6 @@ type Devices = { [name: string]: DeviceDescriptor };
export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
readonly devices: Devices;
private _harBackends = new Map<string, HarBackend>();
private _stackSessions = new Map<string, localUtils.StackSession>();
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.LocalUtilsInitializer) {
super(parent, type, guid, initializer);
@ -47,132 +41,34 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
}
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
return await localUtils.zip(this._platform, this._stackSessions, params);
return await this._channel.zip(params);
}
async harOpen(params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
return await localUtils.harOpen(this._platform, this._harBackends, params);
return await this._channel.harOpen(params);
}
async harLookup(params: channels.LocalUtilsHarLookupParams): Promise<channels.LocalUtilsHarLookupResult> {
return await localUtils.harLookup(this._harBackends, params);
return await this._channel.harLookup(params);
}
async harClose(params: channels.LocalUtilsHarCloseParams): Promise<void> {
return await localUtils.harClose(this._harBackends, params);
return await this._channel.harClose(params);
}
async harUnzip(params: channels.LocalUtilsHarUnzipParams): Promise<void> {
return await localUtils.harUnzip(params);
return await this._channel.harUnzip(params);
}
async tracingStarted(params: channels.LocalUtilsTracingStartedParams): Promise<channels.LocalUtilsTracingStartedResult> {
return await localUtils.tracingStarted(this._stackSessions, params);
return await this._channel.tracingStarted(params);
}
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
return await localUtils.traceDiscarded(this._platform, this._stackSessions, params);
return await this._channel.traceDiscarded(params);
}
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {
return await localUtils.addStackToTracingNoReply(this._stackSessions, params);
}
async connect(params: channels.LocalUtilsConnectParams): Promise<Connection> {
const transport = this._platform.ws ? new WebSocketTransport(this._platform) : new JsonPipeTransport(this);
const connectHeaders = await transport.connect(params);
const connection = new Connection(this, this._platform, this._instrumentation, connectHeaders);
connection.markAsRemote();
connection.on('close', () => transport.close());
let closeError: string | undefined;
const onTransportClosed = (reason?: string) => {
connection.close(reason || closeError);
};
transport.onClose(reason => onTransportClosed(reason));
connection.onmessage = message => transport.send(message).catch(() => onTransportClosed());
transport.onMessage(message => {
try {
connection!.dispatch(message);
} catch (e) {
closeError = String(e);
transport.close();
}
});
return connection;
}
}
interface Transport {
connect(params: channels.LocalUtilsConnectParams): Promise<HeadersArray>;
send(message: any): Promise<void>;
onMessage(callback: (message: object) => void): void;
onClose(callback: (reason?: string) => void): void;
close(): Promise<void>;
}
class JsonPipeTransport implements Transport {
private _pipe: channels.JsonPipeChannel | undefined;
private _owner: ChannelOwner<channels.LocalUtilsChannel>;
constructor(owner: ChannelOwner<channels.LocalUtilsChannel>) {
this._owner = owner;
}
async connect(params: channels.LocalUtilsConnectParams) {
const { pipe, headers: connectHeaders } = await this._owner._wrapApiCall(async () => {
return await this._owner._channel.connect(params);
}, /* isInternal */ true);
this._pipe = pipe;
return connectHeaders;
}
async send(message: object) {
this._owner._wrapApiCall(async () => {
await this._pipe!.send({ message });
}, /* isInternal */ true);
}
onMessage(callback: (message: object) => void) {
this._pipe!.on('message', ({ message }) => callback(message));
}
onClose(callback: (reason?: string) => void) {
this._pipe!.on('closed', ({ reason }) => callback(reason));
}
async close() {
await this._owner._wrapApiCall(async () => {
await this._pipe!.close().catch(() => {});
}, /* isInternal */ true);
}
}
class WebSocketTransport implements Transport {
private _platform: Platform;
private _ws: WebSocket | undefined;
constructor(platform: Platform) {
this._platform = platform;
}
async connect(params: channels.LocalUtilsConnectParams) {
this._ws = this._platform.ws!(params.wsEndpoint);
return [];
}
async send(message: object) {
this._ws!.send(JSON.stringify(message));
}
onMessage(callback: (message: object) => void) {
this._ws!.addEventListener('message', event => callback(JSON.parse(event.data)));
}
onClose(callback: (reason?: string) => void) {
this._ws!.addEventListener('close', () => callback());
}
async close() {
this._ws!.close();
return await this._channel.addStackToTracingNoReply(params);
}
}

View File

@ -30,7 +30,7 @@ import { LongStandingScope, ManualPromise } from '../utils/isomorphic/manualProm
import { MultiMap } from '../utils/isomorphic/multimap';
import { isRegExp, isString } from '../utils/isomorphic/rtti';
import { rewriteErrorMessage } from '../utils/isomorphic/stackTrace';
import { mime } from '../utilsBundle';
import { getMimeTypeForPath } from '../utils/isomorphic/mimeType';
import type { BrowserContext } from './browserContext';
import type { Page } from './page';
@ -413,7 +413,7 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
else if (options.json)
headers['content-type'] = 'application/json';
else if (options.path)
headers['content-type'] = mime.getType(options.path) || 'application/octet-stream';
headers['content-type'] = getMimeTypeForPath(options.path) || 'application/octet-stream';
if (length && !('content-length' in headers))
headers['content-length'] = String(length);

View File

@ -35,7 +35,7 @@ import { Waiter } from './waiter';
import { Worker } from './worker';
import { TimeoutSettings } from '../utils/isomorphic/timeoutSettings';
import { assert } from '../utils/isomorphic/debug';
import { mkdirIfNeeded } from '../common/fileUtils';
import { mkdirIfNeeded } from './fileUtils';
import { headersObjectToArray } from '../utils/isomorphic/headers';
import { trimStringWithEllipsis } from '../utils/isomorphic/stringUtils';
import { urlMatches, urlMatchesEqual } from '../utils/isomorphic/urlMatch';
@ -525,11 +525,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
}
async routeFromHAR(har: string, options: { url?: string | RegExp, notFound?: 'abort' | 'fallback', update?: boolean, updateContent?: 'attach' | 'embed', updateMode?: 'minimal' | 'full'} = {}): Promise<void> {
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Route from har is not supported in thin clients');
if (options.update) {
await this._browserContext._recordIntoHAR(har, this, options);
return;
}
const harRouter = await HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', { urlMatch: options.url });
const harRouter = await HarRouter.create(localUtils, har, options.notFound || 'abort', { urlMatch: options.url });
this._harRouters.push(harRouter);
await harRouter.addPageRoute(this);
}

View File

@ -69,8 +69,8 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
this._isTracing = true;
this._connection.setIsTracing(true);
}
const result = await this._connection.localUtils().tracingStarted({ tracesDir: this._tracesDir, traceName });
this._stacksId = result.stacksId;
const result = await this._connection.localUtils()?.tracingStarted({ tracesDir: this._tracesDir, traceName });
this._stacksId = result?.stacksId;
}
async stopChunk(options: { path?: string } = {}) {
@ -89,15 +89,19 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
// Not interested in artifacts.
await this._channel.tracingStopChunk({ mode: 'discard' });
if (this._stacksId)
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
await this._connection.localUtils()!.traceDiscarded({ stacksId: this._stacksId });
return;
}
const localUtils = this._connection.localUtils();
if (!localUtils)
throw new Error('Cannot save trace in thin clients');
const isLocal = !this._connection.isRemote();
if (isLocal) {
const result = await this._channel.tracingStopChunk({ mode: 'entries' });
await this._connection.localUtils().zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
await localUtils.zip({ zipFile: filePath, entries: result.entries!, mode: 'write', stacksId: this._stacksId, includeSources: this._includeSources });
return;
}
@ -106,7 +110,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
// The artifact may be missing if the browser closed while stopping tracing.
if (!result.artifact) {
if (this._stacksId)
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
await localUtils.traceDiscarded({ stacksId: this._stacksId });
return;
}
@ -115,7 +119,7 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
await artifact.saveAs(filePath);
await artifact.delete();
await this._connection.localUtils().zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
await localUtils.zip({ zipFile: filePath, entries: [], mode: 'append', stacksId: this._stacksId, includeSources: this._includeSources });
}
_resetStackCounter() {

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ChannelOwner } from './channelOwner';
import { Connection } from './connection';
import type { HeadersArray } from './types';
import type * as channels from '@protocol/channels';
export async function connectOverWebSocket(parentConnection: Connection, params: channels.LocalUtilsConnectParams): Promise<Connection> {
const localUtils = parentConnection.localUtils();
const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
const connectHeaders = await transport.connect(params);
const connection = new Connection(localUtils, parentConnection.platform, parentConnection._instrumentation, connectHeaders);
connection.markAsRemote();
connection.on('close', () => transport.close());
let closeError: string | undefined;
const onTransportClosed = (reason?: string) => {
connection.close(reason || closeError);
};
transport.onClose(reason => onTransportClosed(reason));
connection.onmessage = message => transport.send(message).catch(() => onTransportClosed());
transport.onMessage(message => {
try {
connection!.dispatch(message);
} catch (e) {
closeError = String(e);
transport.close();
}
});
return connection;
}
interface Transport {
connect(params: channels.LocalUtilsConnectParams): Promise<HeadersArray>;
send(message: any): Promise<void>;
onMessage(callback: (message: object) => void): void;
onClose(callback: (reason?: string) => void): void;
close(): Promise<void>;
}
class JsonPipeTransport implements Transport {
private _pipe: channels.JsonPipeChannel | undefined;
private _owner: ChannelOwner<channels.LocalUtilsChannel>;
constructor(owner: ChannelOwner<channels.LocalUtilsChannel>) {
this._owner = owner;
}
async connect(params: channels.LocalUtilsConnectParams) {
const { pipe, headers: connectHeaders } = await this._owner._wrapApiCall(async () => {
return await this._owner._channel.connect(params);
}, /* isInternal */ true);
this._pipe = pipe;
return connectHeaders;
}
async send(message: object) {
this._owner._wrapApiCall(async () => {
await this._pipe!.send({ message });
}, /* isInternal */ true);
}
onMessage(callback: (message: object) => void) {
this._pipe!.on('message', ({ message }) => callback(message));
}
onClose(callback: (reason?: string) => void) {
this._pipe!.on('closed', ({ reason }) => callback(reason));
}
async close() {
await this._owner._wrapApiCall(async () => {
await this._pipe!.close().catch(() => {});
}, /* isInternal */ true);
}
}
class WebSocketTransport implements Transport {
private _ws: WebSocket | undefined;
async connect(params: channels.LocalUtilsConnectParams) {
this._ws = new window.WebSocket(params.wsEndpoint);
return [];
}
async send(message: object) {
this._ws!.send(JSON.stringify(message));
}
onMessage(callback: (message: object) => void) {
this._ws!.addEventListener('message', event => callback(JSON.parse(event.data)));
}
onClose(callback: (reason?: string) => void) {
this._ws!.addEventListener('close', () => callback());
}
async close() {
this._ws!.close();
}
}

View File

@ -1,5 +1,2 @@
[*]
../utils/
../utils/isomorphic/
../utilsBundle.ts
../zipBundle.ts

View File

@ -14,12 +14,10 @@
* limitations under the License.
*/
import * as crypto from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { webColors, noColors } from '../utils/isomorphic/colors';
import type * as fs from 'fs';
import type * as path from 'path';
import type { Colors } from '../utils/isomorphic/colors';
export type Zone = {
@ -37,6 +35,8 @@ const noopZone: Zone = {
};
export type Platform = {
name: 'node' | 'web' | 'empty';
calculateSha1(text: string): Promise<string>;
colors: Colors;
createGuid: () => string;
@ -46,21 +46,22 @@ export type Platform = {
log(name: 'api' | 'channel', message: string | Error | object): void;
path: () => typeof path;
pathSeparator: string;
ws?: (url: string) => WebSocket;
zones: { empty: Zone, current: () => Zone; };
};
export const webPlatform: Platform = {
name: 'web',
calculateSha1: async (text: string) => {
const bytes = new TextEncoder().encode(text);
const hashBuffer = await crypto.subtle.digest('SHA-1', bytes);
const hashBuffer = await window.crypto.subtle.digest('SHA-1', bytes);
return Array.from(new Uint8Array(hashBuffer), b => b.toString(16).padStart(2, '0')).join('');
},
colors: webColors,
createGuid: () => {
return Array.from(crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('');
return Array.from(window.crypto.getRandomValues(new Uint8Array(16)), b => b.toString(16).padStart(2, '0')).join('');
},
fs: () => {
@ -82,12 +83,12 @@ export const webPlatform: Platform = {
pathSeparator: '/',
ws: (url: string) => new WebSocket(url),
zones: { empty: noopZone, current: () => noopZone },
};
export const emptyPlatform: Platform = {
name: 'empty',
calculateSha1: async () => {
throw new Error('Not implemented');
},

View File

@ -1,23 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Progress {
log(message: string): void;
timeUntilDeadline(): number;
isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void;
throwIfAborted(): void;
}

View File

@ -1,4 +1,3 @@
[*]
../common/
../utils/
../utils/isomorphic

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
import { isUnderTest } from '../utils';
import { isUnderTest } from '../utils/isomorphic/debug';
export class ValidationError extends Error {}
export type Validator = (arg: any, path: string, context: ValidatorContext) => any;

View File

@ -16,8 +16,7 @@
import { Dispatcher } from './dispatcher';
import { SdkObject } from '../../server/instrumentation';
import * as localUtils from '../../common/localUtils';
import { nodePlatform } from '../utils/nodePlatform';
import * as localUtils from '../localUtils';
import { getUserAgent } from '../utils/userAgent';
import { deviceDescriptors as descriptors } from '../deviceDescriptors';
import { JsonPipeDispatcher } from '../dispatchers/jsonPipeDispatcher';
@ -26,7 +25,7 @@ import { SocksInterceptor } from '../socksInterceptor';
import { WebSocketTransport } from '../transport';
import { fetchData } from '../utils/network';
import type { HarBackend } from '../../common/harBackend';
import type { HarBackend } from '../harBackend';
import type { CallMetadata } from '../instrumentation';
import type { Playwright } from '../playwright';
import type { RootDispatcher } from './dispatcher';
@ -50,11 +49,11 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
}
async zip(params: channels.LocalUtilsZipParams): Promise<void> {
return await localUtils.zip(nodePlatform, this._stackSessions, params);
return await localUtils.zip(this._stackSessions, params);
}
async harOpen(params: channels.LocalUtilsHarOpenParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarOpenResult> {
return await localUtils.harOpen(nodePlatform, this._harBackends, params);
return await localUtils.harOpen(this._harBackends, params);
}
async harLookup(params: channels.LocalUtilsHarLookupParams, metadata: CallMetadata): Promise<channels.LocalUtilsHarLookupResult> {
@ -74,7 +73,7 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels.
}
async traceDiscarded(params: channels.LocalUtilsTraceDiscardedParams, metadata?: CallMetadata | undefined): Promise<void> {
return await localUtils.traceDiscarded(nodePlatform, this._stackSessions, params);
return await localUtils.traceDiscarded(this._stackSessions, params);
}
async addStackToTracingNoReply(params: channels.LocalUtilsAddStackToTracingNoReplyParams, metadata?: CallMetadata | undefined): Promise<void> {

View File

@ -18,7 +18,6 @@ import * as fs from 'fs';
import * as path from 'path';
import { assert } from '../utils/isomorphic/debug';
import { fileUploadSizeLimit } from '../common/fileUtils';
import { mime } from '../utilsBundle';
import type { WritableStreamDispatcher } from './dispatchers/writableStreamDispatcher';
@ -27,6 +26,9 @@ import type { Frame } from './frames';
import type * as types from './types';
import type * as channels from '@protocol/channels';
// Keep in sync with the client.
export const fileUploadSizeLimit = 50 * 1024 * 1024;
async function filesExceedUploadLimit(files: string[]) {
const sizes = await Promise.all(files.map(async file => (await fs.promises.stat(file)).size));
return sizes.reduce((total, size) => total + size, 0) >= fileUploadSizeLimit;

View File

@ -14,11 +14,14 @@
* limitations under the License.
*/
import { ZipFile } from '../utils/zipFile';
import * as fs from 'fs';
import * as path from 'path';
import type { HeadersArray } from './types';
import { createGuid } from './utils/crypto';
import { ZipFile } from './utils/zipFile';
import type { HeadersArray } from '../common/types';
import type * as har from '@trace/har';
import type { Platform } from './platform';
const redirectStatus = [301, 302, 303, 307, 308];
@ -27,11 +30,9 @@ export class HarBackend {
private _harFile: har.HARFile;
private _zipFile: ZipFile | null;
private _baseDir: string | null;
private _platform: Platform;
constructor(platform: Platform, harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) {
this._platform = platform;
this.id = platform.createGuid();
constructor(harFile: har.HARFile, baseDir: string | null, zipFile: ZipFile | null) {
this.id = createGuid();
this._harFile = harFile;
this._baseDir = baseDir;
this._zipFile = zipFile;
@ -79,7 +80,7 @@ export class HarBackend {
if (this._zipFile)
buffer = await this._zipFile.read(file);
else
buffer = await this._platform.fs().promises.readFile(this._platform.path().resolve(this._baseDir!, file));
buffer = await fs.promises.readFile(path.resolve(this._baseDir!, file));
} else {
buffer = Buffer.from(content.text || '', content.encoding === 'base64' ? 'base64' : 'utf-8');
}

View File

@ -18,15 +18,15 @@ import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { removeFolders } from './fileUtils';
import { calculateSha1 } from './utils/crypto';
import { HarBackend } from './harBackend';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import { ZipFile } from '../utils/zipFile';
import { ZipFile } from './utils/zipFile';
import { yauzl, yazl } from '../zipBundle';
import { serializeClientSideCallMetadata } from '../utils/isomorphic/traceUtils';
import { assert } from '../utils/isomorphic/debug';
import { removeFolders } from './utils/fileUtils';
import type { Platform } from './platform';
import type * as channels from '@protocol/channels';
import type * as har from '@trace/har';
import type EventEmitter from 'events';
@ -39,7 +39,7 @@ export type StackSession = {
callStacks: channels.ClientSideCallMetadata[];
};
export async function zip(platform: Platform, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsZipParams): Promise<void> {
export async function zip(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsZipParams): Promise<void> {
const promise = new ManualPromise<void>();
const zipFile = new yazl.ZipFile();
(zipFile as any as EventEmitter).on('error', error => promise.reject(error));
@ -77,7 +77,7 @@ export async function zip(platform: Platform, stackSessions: Map<string, StackSe
sourceFiles.add(file);
}
for (const sourceFile of sourceFiles)
addFile(sourceFile, 'resources/src@' + await platform.calculateSha1(sourceFile) + '.txt');
addFile(sourceFile, 'resources/src@' + await calculateSha1(sourceFile) + '.txt');
}
if (params.mode === 'write') {
@ -89,7 +89,7 @@ export async function zip(platform: Platform, stackSessions: Map<string, StackSe
.on('error', error => promise.reject(error));
});
await promise;
await deleteStackSession(platform, stackSessions, params.stacksId);
await deleteStackSession(stackSessions, params.stacksId);
return;
}
@ -124,20 +124,20 @@ export async function zip(platform: Platform, stackSessions: Map<string, StackSe
});
});
await promise;
await deleteStackSession(platform, stackSessions, params.stacksId);
await deleteStackSession(stackSessions, params.stacksId);
}
async function deleteStackSession(platform: Platform, stackSessions: Map<string, StackSession>, stacksId?: string) {
async function deleteStackSession(stackSessions: Map<string, StackSession>, stacksId?: string) {
const session = stacksId ? stackSessions.get(stacksId) : undefined;
if (!session)
return;
await session.writer;
if (session.tmpDir)
await removeFolders(platform, [session.tmpDir]);
await removeFolders([session.tmpDir]);
stackSessions.delete(stacksId!);
}
export async function harOpen(platform: Platform, harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
export async function harOpen(harBackends: Map<string, HarBackend>, params: channels.LocalUtilsHarOpenParams): Promise<channels.LocalUtilsHarOpenResult> {
let harBackend: HarBackend;
if (params.file.endsWith('.zip')) {
const zipFile = new ZipFile(params.file);
@ -147,10 +147,10 @@ export async function harOpen(platform: Platform, harBackends: Map<string, HarBa
return { error: 'Specified archive does not have a .har file' };
const har = await zipFile.read(harEntryName);
const harFile = JSON.parse(har.toString()) as har.HARFile;
harBackend = new HarBackend(platform, harFile, null, zipFile);
harBackend = new HarBackend(harFile, null, zipFile);
} else {
const harFile = JSON.parse(await fs.promises.readFile(params.file, 'utf-8')) as har.HARFile;
harBackend = new HarBackend(platform, harFile, path.dirname(params.file), null);
harBackend = new HarBackend(harFile, path.dirname(params.file), null);
}
harBackends.set(harBackend.id, harBackend);
return { harId: harBackend.id };
@ -194,8 +194,8 @@ export async function tracingStarted(stackSessions: Map<string, StackSession>, p
return { stacksId: traceStacksFile };
}
export async function traceDiscarded(platform: Platform, stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
await deleteStackSession(platform, stackSessions, params.stacksId);
export async function traceDiscarded(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsTraceDiscardedParams): Promise<void> {
await deleteStackSession(stackSessions, params.stacksId);
}
export async function addStackToTracingNoReply(stackSessions: Map<string, StackSession>, params: channels.LocalUtilsAddStackToTracingNoReplyParams): Promise<void> {

View File

@ -19,10 +19,14 @@ import { assert, monotonicTime } from '../utils';
import { ManualPromise } from '../utils/isomorphic/manualPromise';
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
import type { Progress as CommonProgress } from '../common/progress';
import type { LogName } from './utils/debugLogger';
export interface Progress extends CommonProgress {
export interface Progress {
log(message: string): void;
timeUntilDeadline(): number;
isRunning(): boolean;
cleanupWhenAborted(cleanup: () => any): void;
throwIfAborted(): void;
metadata: CallMetadata;
}

View File

@ -45,16 +45,14 @@ class NodeZone implements Zone {
return this._zone.run(func);
}
runIgnoreCurrent<R>(func: () => R): R {
return emptyZone.run(func);
}
data<T>(): T | undefined {
return this._zone.data('apiZone');
}
}
export const nodePlatform: Platform = {
name: 'node',
calculateSha1: (text: string) => {
const sha1 = crypto.createHash('sha1');
sha1.update(text);

View File

@ -14,9 +14,9 @@
* limitations under the License.
*/
import { yauzl } from '../zipBundle';
import { yauzl } from '../../zipBundle';
import type { Entry, UnzipFile } from '../zipBundle';
import type { Entry, UnzipFile } from '../../zipBundle';
export class ZipFile {
private _fileName: string;

View File

@ -29,7 +29,6 @@ export * from './utils/isomorphic/urlMatch';
export * from './utils/isomorphic/headers';
export * from './utils/isomorphic/semaphore';
export * from './utils/isomorphic/stackTrace';
export * from './utils/zipFile';
export * from './server/utils/ascii';
export * from './server/utils/comparators';
@ -50,6 +49,7 @@ export * from './server/utils/spawnAsync';
export * from './server/utils/task';
export * from './server/utils/userAgent';
export * from './server/utils/wsServer';
export * from './server/utils/zipFile';
export * from './server/utils/zones';
export { colors } from './utilsBundle';

View File

@ -1,14 +1,14 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* Licensed under the Apache License, Version 2.0 (the 'License');
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* distributed under the License is distributed on an 'AS IS' BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
@ -21,3 +21,426 @@ export function isJsonMimeType(mimeType: string) {
export function isTextualMimeType(mimeType: string) {
return !!mimeType.match(/^(text\/.*?|application\/(json|(x-)?javascript|xml.*?|ecmascript|graphql|x-www-form-urlencoded)|image\/svg(\+xml)?|application\/.*?(\+json|\+xml))(;\s*charset=.*)?$/);
}
export function getMimeTypeForPath(path: string): string | null {
const dotIndex = path.lastIndexOf('.');
if (dotIndex === -1)
return null;
const extension = path.substring(dotIndex + 1);
return types.get(extension) || null;
}
const types: Map<string, string> = new Map([
['ez', 'application/andrew-inset'],
['aw', 'application/applixware'],
['atom', 'application/atom+xml'],
['atomcat', 'application/atomcat+xml'],
['atomdeleted', 'application/atomdeleted+xml'],
['atomsvc', 'application/atomsvc+xml'],
['dwd', 'application/atsc-dwd+xml'],
['held', 'application/atsc-held+xml'],
['rsat', 'application/atsc-rsat+xml'],
['bdoc', 'application/bdoc'],
['xcs', 'application/calendar+xml'],
['ccxml', 'application/ccxml+xml'],
['cdfx', 'application/cdfx+xml'],
['cdmia', 'application/cdmi-capability'],
['cdmic', 'application/cdmi-container'],
['cdmid', 'application/cdmi-domain'],
['cdmio', 'application/cdmi-object'],
['cdmiq', 'application/cdmi-queue'],
['cu', 'application/cu-seeme'],
['mpd', 'application/dash+xml'],
['davmount', 'application/davmount+xml'],
['dbk', 'application/docbook+xml'],
['dssc', 'application/dssc+der'],
['xdssc', 'application/dssc+xml'],
['ecma', 'application/ecmascript'],
['es', 'application/ecmascript'],
['emma', 'application/emma+xml'],
['emotionml', 'application/emotionml+xml'],
['epub', 'application/epub+zip'],
['exi', 'application/exi'],
['exp', 'application/express'],
['fdt', 'application/fdt+xml'],
['pfr', 'application/font-tdpfr'],
['geojson', 'application/geo+json'],
['gml', 'application/gml+xml'],
['gpx', 'application/gpx+xml'],
['gxf', 'application/gxf'],
['gz', 'application/gzip'],
['hjson', 'application/hjson'],
['stk', 'application/hyperstudio'],
['ink', 'application/inkml+xml'],
['inkml', 'application/inkml+xml'],
['ipfix', 'application/ipfix'],
['its', 'application/its+xml'],
['ear', 'application/java-archive'],
['jar', 'application/java-archive'],
['war', 'application/java-archive'],
['ser', 'application/java-serialized-object'],
['class', 'application/java-vm'],
['js', 'application/javascript'],
['mjs', 'application/javascript'],
['json', 'application/json'],
['map', 'application/json'],
['json5', 'application/json5'],
['jsonml', 'application/jsonml+json'],
['jsonld', 'application/ld+json'],
['lgr', 'application/lgr+xml'],
['lostxml', 'application/lost+xml'],
['hqx', 'application/mac-binhex40'],
['cpt', 'application/mac-compactpro'],
['mads', 'application/mads+xml'],
['webmanifest', 'application/manifest+json'],
['mrc', 'application/marc'],
['mrcx', 'application/marcxml+xml'],
['ma', 'application/mathematica'],
['mb', 'application/mathematica'],
['nb', 'application/mathematica'],
['mathml', 'application/mathml+xml'],
['mbox', 'application/mbox'],
['mscml', 'application/mediaservercontrol+xml'],
['metalink', 'application/metalink+xml'],
['meta4', 'application/metalink4+xml'],
['mets', 'application/mets+xml'],
['maei', 'application/mmt-aei+xml'],
['musd', 'application/mmt-usd+xml'],
['mods', 'application/mods+xml'],
['m21', 'application/mp21'],
['mp21', 'application/mp21'],
['m4p', 'application/mp4'],
['mp4s', 'application/mp4'],
['doc', 'application/msword'],
['dot', 'application/msword'],
['mxf', 'application/mxf'],
['nq', 'application/n-quads'],
['nt', 'application/n-triples'],
['cjs', 'application/node'],
['bin', 'application/octet-stream'],
['bpk', 'application/octet-stream'],
['buffer', 'application/octet-stream'],
['deb', 'application/octet-stream'],
['deploy', 'application/octet-stream'],
['dist', 'application/octet-stream'],
['distz', 'application/octet-stream'],
['dll', 'application/octet-stream'],
['dmg', 'application/octet-stream'],
['dms', 'application/octet-stream'],
['dump', 'application/octet-stream'],
['elc', 'application/octet-stream'],
['exe', 'application/octet-stream'],
['img', 'application/octet-stream'],
['iso', 'application/octet-stream'],
['lrf', 'application/octet-stream'],
['mar', 'application/octet-stream'],
['msi', 'application/octet-stream'],
['msm', 'application/octet-stream'],
['msp', 'application/octet-stream'],
['pkg', 'application/octet-stream'],
['so', 'application/octet-stream'],
['oda', 'application/oda'],
['opf', 'application/oebps-package+xml'],
['ogx', 'application/ogg'],
['omdoc', 'application/omdoc+xml'],
['onepkg', 'application/onenote'],
['onetmp', 'application/onenote'],
['onetoc', 'application/onenote'],
['onetoc2', 'application/onenote'],
['oxps', 'application/oxps'],
['relo', 'application/p2p-overlay+xml'],
['xer', 'application/patch-ops-error+xml'],
['pdf', 'application/pdf'],
['pgp', 'application/pgp-encrypted'],
['asc', 'application/pgp-signature'],
['sig', 'application/pgp-signature'],
['prf', 'application/pics-rules'],
['p10', 'application/pkcs10'],
['p7c', 'application/pkcs7-mime'],
['p7m', 'application/pkcs7-mime'],
['p7s', 'application/pkcs7-signature'],
['p8', 'application/pkcs8'],
['ac', 'application/pkix-attr-cert'],
['cer', 'application/pkix-cert'],
['crl', 'application/pkix-crl'],
['pkipath', 'application/pkix-pkipath'],
['pki', 'application/pkixcmp'],
['pls', 'application/pls+xml'],
['ai', 'application/postscript'],
['eps', 'application/postscript'],
['ps', 'application/postscript'],
['provx', 'application/provenance+xml'],
['pskcxml', 'application/pskc+xml'],
['raml', 'application/raml+yaml'],
['owl', 'application/rdf+xml'],
['rdf', 'application/rdf+xml'],
['rif', 'application/reginfo+xml'],
['rnc', 'application/relax-ng-compact-syntax'],
['rl', 'application/resource-lists+xml'],
['rld', 'application/resource-lists-diff+xml'],
['rs', 'application/rls-services+xml'],
['rapd', 'application/route-apd+xml'],
['sls', 'application/route-s-tsid+xml'],
['rusd', 'application/route-usd+xml'],
['gbr', 'application/rpki-ghostbusters'],
['mft', 'application/rpki-manifest'],
['roa', 'application/rpki-roa'],
['rsd', 'application/rsd+xml'],
['rss', 'application/rss+xml'],
['rtf', 'application/rtf'],
['sbml', 'application/sbml+xml'],
['scq', 'application/scvp-cv-request'],
['scs', 'application/scvp-cv-response'],
['spq', 'application/scvp-vp-request'],
['spp', 'application/scvp-vp-response'],
['sdp', 'application/sdp'],
['senmlx', 'application/senml+xml'],
['sensmlx', 'application/sensml+xml'],
['setpay', 'application/set-payment-initiation'],
['setreg', 'application/set-registration-initiation'],
['shf', 'application/shf+xml'],
['sieve', 'application/sieve'],
['siv', 'application/sieve'],
['smi', 'application/smil+xml'],
['smil', 'application/smil+xml'],
['rq', 'application/sparql-query'],
['srx', 'application/sparql-results+xml'],
['gram', 'application/srgs'],
['grxml', 'application/srgs+xml'],
['sru', 'application/sru+xml'],
['ssdl', 'application/ssdl+xml'],
['ssml', 'application/ssml+xml'],
['swidtag', 'application/swid+xml'],
['tei', 'application/tei+xml'],
['teicorpus', 'application/tei+xml'],
['tfi', 'application/thraud+xml'],
['tsd', 'application/timestamped-data'],
['toml', 'application/toml'],
['trig', 'application/trig'],
['ttml', 'application/ttml+xml'],
['ubj', 'application/ubjson'],
['rsheet', 'application/urc-ressheet+xml'],
['td', 'application/urc-targetdesc+xml'],
['vxml', 'application/voicexml+xml'],
['wasm', 'application/wasm'],
['wgt', 'application/widget'],
['hlp', 'application/winhlp'],
['wsdl', 'application/wsdl+xml'],
['wspolicy', 'application/wspolicy+xml'],
['xaml', 'application/xaml+xml'],
['xav', 'application/xcap-att+xml'],
['xca', 'application/xcap-caps+xml'],
['xdf', 'application/xcap-diff+xml'],
['xel', 'application/xcap-el+xml'],
['xns', 'application/xcap-ns+xml'],
['xenc', 'application/xenc+xml'],
['xht', 'application/xhtml+xml'],
['xhtml', 'application/xhtml+xml'],
['xlf', 'application/xliff+xml'],
['rng', 'application/xml'],
['xml', 'application/xml'],
['xsd', 'application/xml'],
['xsl', 'application/xml'],
['dtd', 'application/xml-dtd'],
['xop', 'application/xop+xml'],
['xpl', 'application/xproc+xml'],
['*xsl', 'application/xslt+xml'],
['xslt', 'application/xslt+xml'],
['xspf', 'application/xspf+xml'],
['mxml', 'application/xv+xml'],
['xhvml', 'application/xv+xml'],
['xvm', 'application/xv+xml'],
['xvml', 'application/xv+xml'],
['yang', 'application/yang'],
['yin', 'application/yin+xml'],
['zip', 'application/zip'],
['*3gpp', 'audio/3gpp'],
['adp', 'audio/adpcm'],
['amr', 'audio/amr'],
['au', 'audio/basic'],
['snd', 'audio/basic'],
['kar', 'audio/midi'],
['mid', 'audio/midi'],
['midi', 'audio/midi'],
['rmi', 'audio/midi'],
['mxmf', 'audio/mobile-xmf'],
['*mp3', 'audio/mp3'],
['m4a', 'audio/mp4'],
['mp4a', 'audio/mp4'],
['m2a', 'audio/mpeg'],
['m3a', 'audio/mpeg'],
['mp2', 'audio/mpeg'],
['mp2a', 'audio/mpeg'],
['mp3', 'audio/mpeg'],
['mpga', 'audio/mpeg'],
['oga', 'audio/ogg'],
['ogg', 'audio/ogg'],
['opus', 'audio/ogg'],
['spx', 'audio/ogg'],
['s3m', 'audio/s3m'],
['sil', 'audio/silk'],
['wav', 'audio/wav'],
['*wav', 'audio/wave'],
['weba', 'audio/webm'],
['xm', 'audio/xm'],
['ttc', 'font/collection'],
['otf', 'font/otf'],
['ttf', 'font/ttf'],
['woff', 'font/woff'],
['woff2', 'font/woff2'],
['exr', 'image/aces'],
['apng', 'image/apng'],
['avif', 'image/avif'],
['bmp', 'image/bmp'],
['cgm', 'image/cgm'],
['drle', 'image/dicom-rle'],
['emf', 'image/emf'],
['fits', 'image/fits'],
['g3', 'image/g3fax'],
['gif', 'image/gif'],
['heic', 'image/heic'],
['heics', 'image/heic-sequence'],
['heif', 'image/heif'],
['heifs', 'image/heif-sequence'],
['hej2', 'image/hej2k'],
['hsj2', 'image/hsj2'],
['ief', 'image/ief'],
['jls', 'image/jls'],
['jp2', 'image/jp2'],
['jpg2', 'image/jp2'],
['jpe', 'image/jpeg'],
['jpeg', 'image/jpeg'],
['jpg', 'image/jpeg'],
['jph', 'image/jph'],
['jhc', 'image/jphc'],
['jpm', 'image/jpm'],
['jpf', 'image/jpx'],
['jpx', 'image/jpx'],
['jxr', 'image/jxr'],
['jxra', 'image/jxra'],
['jxrs', 'image/jxrs'],
['jxs', 'image/jxs'],
['jxsc', 'image/jxsc'],
['jxsi', 'image/jxsi'],
['jxss', 'image/jxss'],
['ktx', 'image/ktx'],
['ktx2', 'image/ktx2'],
['png', 'image/png'],
['sgi', 'image/sgi'],
['svg', 'image/svg+xml'],
['svgz', 'image/svg+xml'],
['t38', 'image/t38'],
['tif', 'image/tiff'],
['tiff', 'image/tiff'],
['tfx', 'image/tiff-fx'],
['webp', 'image/webp'],
['wmf', 'image/wmf'],
['disposition-notification', 'message/disposition-notification'],
['u8msg', 'message/global'],
['u8dsn', 'message/global-delivery-status'],
['u8mdn', 'message/global-disposition-notification'],
['u8hdr', 'message/global-headers'],
['eml', 'message/rfc822'],
['mime', 'message/rfc822'],
['3mf', 'model/3mf'],
['gltf', 'model/gltf+json'],
['glb', 'model/gltf-binary'],
['iges', 'model/iges'],
['igs', 'model/iges'],
['mesh', 'model/mesh'],
['msh', 'model/mesh'],
['silo', 'model/mesh'],
['mtl', 'model/mtl'],
['obj', 'model/obj'],
['stpx', 'model/step+xml'],
['stpz', 'model/step+zip'],
['stpxz', 'model/step-xml+zip'],
['stl', 'model/stl'],
['vrml', 'model/vrml'],
['wrl', 'model/vrml'],
['*x3db', 'model/x3d+binary'],
['x3dbz', 'model/x3d+binary'],
['x3db', 'model/x3d+fastinfoset'],
['*x3dv', 'model/x3d+vrml'],
['x3dvz', 'model/x3d+vrml'],
['x3d', 'model/x3d+xml'],
['x3dz', 'model/x3d+xml'],
['x3dv', 'model/x3d-vrml'],
['appcache', 'text/cache-manifest'],
['manifest', 'text/cache-manifest'],
['ics', 'text/calendar'],
['ifb', 'text/calendar'],
['coffee', 'text/coffeescript'],
['litcoffee', 'text/coffeescript'],
['css', 'text/css'],
['csv', 'text/csv'],
['htm', 'text/html'],
['html', 'text/html'],
['shtml', 'text/html'],
['jade', 'text/jade'],
['jsx', 'text/jsx'],
['less', 'text/less'],
['markdown', 'text/markdown'],
['md', 'text/markdown'],
['mml', 'text/mathml'],
['mdx', 'text/mdx'],
['n3', 'text/n3'],
['conf', 'text/plain'],
['def', 'text/plain'],
['in', 'text/plain'],
['ini', 'text/plain'],
['list', 'text/plain'],
['log', 'text/plain'],
['text', 'text/plain'],
['txt', 'text/plain'],
['rtx', 'text/richtext'],
['*rtf', 'text/rtf'],
['sgm', 'text/sgml'],
['sgml', 'text/sgml'],
['shex', 'text/shex'],
['slim', 'text/slim'],
['slm', 'text/slim'],
['spdx', 'text/spdx'],
['styl', 'text/stylus'],
['stylus', 'text/stylus'],
['tsv', 'text/tab-separated-values'],
['man', 'text/troff'],
['me', 'text/troff'],
['ms', 'text/troff'],
['roff', 'text/troff'],
['t', 'text/troff'],
['tr', 'text/troff'],
['ttl', 'text/turtle'],
['uri', 'text/uri-list'],
['uris', 'text/uri-list'],
['urls', 'text/uri-list'],
['vcard', 'text/vcard'],
['vtt', 'text/vtt'],
['*xml', 'text/xml'],
['yaml', 'text/yaml'],
['yml', 'text/yaml'],
['3gp', 'video/3gpp'],
['3gpp', 'video/3gpp'],
['3g2', 'video/3gpp2'],
['h261', 'video/h261'],
['h263', 'video/h263'],
['h264', 'video/h264'],
['m4s', 'video/iso.segment'],
['jpgv', 'video/jpeg'],
['jpm', 'video/jpm'],
['jpgm', 'video/jpm'],
['mj2', 'video/mj2'],
['mjp2', 'video/mj2'],
['ts', 'video/mp2t'],
['mp4', 'video/mp4'],
['mp4v', 'video/mp4'],
['mpg4', 'video/mp4'],
['m1v', 'video/mpeg'],
['m2v', 'video/mpeg'],
['mpe', 'video/mpeg'],
['mpeg', 'video/mpeg'],
['mpg', 'video/mpeg'],
['ogv', 'video/ogg'],
['mov', 'video/quicktime'],
['qt', 'video/quicktime'],
['webm', 'video/webm']
]);

View File

@ -15,7 +15,7 @@
*/
import type { Frame, Page } from 'playwright-core';
import { ZipFile } from '../../packages/playwright-core/lib/utils/zipFile';
import { ZipFile } from '../../packages/playwright-core/lib/server/utils/zipFile';
import type { TraceModelBackend } from '../../packages/trace-viewer/src/sw/traceModel';
import type { StackFrame } from '../../packages/protocol/src/channels';
import { parseClientSideCallMetadata } from '../../packages/playwright-core/lib/utils/isomorphic/traceUtils';