diff --git a/eslint.config.mjs b/eslint.config.mjs index b40c721eec..a162786886 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -366,7 +366,6 @@ export default [ { files: [ "packages/injected/src/**/*.ts", - "packages/playwright-core/src/server/storageScript.ts", ], languageOptions: languageOptionsWithTsConfig, rules: { diff --git a/packages/playwright-core/src/server/storageScript.ts b/packages/injected/src/storageScript.ts similarity index 99% rename from packages/playwright-core/src/server/storageScript.ts rename to packages/injected/src/storageScript.ts index 62bcaee0da..b91eaf0c02 100644 --- a/packages/playwright-core/src/server/storageScript.ts +++ b/packages/injected/src/storageScript.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { parseEvaluationResultValue, serializeAsCallArgument } from '../utils/isomorphic/utilityScriptSerializers'; +import { parseEvaluationResultValue, serializeAsCallArgument } from '@isomorphic/utilityScriptSerializers'; import type * as channels from '@protocol/channels'; diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 7b83d49b59..ca404a436e 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -45,7 +45,7 @@ import type { CallMetadata } from './instrumentation'; import type { Progress, ProgressController } from './progress'; import type { Selectors } from './selectors'; import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; -import type { SerializedStorage } from './storageScript'; +import type { SerializedStorage } from '@injected/storageScript'; import type * as types from './types'; import type * as channels from '@protocol/channels'; diff --git a/packages/playwright-core/src/server/javascript.ts b/packages/playwright-core/src/server/javascript.ts index e395051122..fffb5e2408 100644 --- a/packages/playwright-core/src/server/javascript.ts +++ b/packages/playwright-core/src/server/javascript.ts @@ -104,7 +104,7 @@ export class ExecutionContext extends SdkObject { } async evaluateWithArguments(expression: string, returnByValue: boolean, values: any[], handles: JSHandle[]): Promise { - const utilityScript = await this._utilityScript(); + const utilityScript = await this.utilityScript(); return this._raceAgainstContextDestroyed(this.delegate.evaluateWithArguments(expression, returnByValue, utilityScript, values, handles)); } @@ -120,7 +120,7 @@ export class ExecutionContext extends SdkObject { return null; } - private _utilityScript(): Promise> { + utilityScript(): Promise> { if (!this._utilityScriptPromise) { const source = ` (() => { diff --git a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts index 0151d82b4f..1273453959 100644 --- a/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts +++ b/packages/playwright-core/src/utils/isomorphic/utilityScriptSerializers.ts @@ -37,256 +37,248 @@ type VisitorInfo = { lastId: number; }; -function source() { - - function isRegExp(obj: any): obj is RegExp { - try { - return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; - } catch (error) { - return false; - } +function isRegExp(obj: any): obj is RegExp { + try { + return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; + } catch (error) { + return false; } - - function isDate(obj: any): obj is Date { - try { - return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]'; - } catch (error) { - return false; - } - } - - function isURL(obj: any): obj is URL { - try { - return obj instanceof URL || Object.prototype.toString.call(obj) === '[object URL]'; - } catch (error) { - return false; - } - } - - function isError(obj: any): obj is Error { - try { - return obj instanceof Error || (obj && Object.getPrototypeOf(obj)?.name === 'Error'); - } catch (error) { - return false; - } - } - - function isTypedArray(obj: any, constructor: Function): boolean { - try { - return obj instanceof constructor || Object.prototype.toString.call(obj) === `[object ${constructor.name}]`; - } catch (error) { - return false; - } - } - - const typedArrayConstructors: Record = { - i8: Int8Array, - ui8: Uint8Array, - ui8c: Uint8ClampedArray, - i16: Int16Array, - ui16: Uint16Array, - i32: Int32Array, - ui32: Uint32Array, - // TODO: add Float16Array once it's in baseline - f32: Float32Array, - f64: Float64Array, - bi64: BigInt64Array, - bui64: BigUint64Array, - }; - - function typedArrayToBase64(array: any) { - /** - * Firefox does not support iterating over typed arrays, so we use `.toBase64`. - * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().' - */ - if ('toBase64' in array) - return array.toBase64(); - const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join(''); - return btoa(binary); - } - - function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) - bytes[i] = binary.charCodeAt(i); - return new TypedArrayConstructor(bytes.buffer); - } - - function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map = new Map()): any { - if (Object.is(value, undefined)) - return undefined; - if (typeof value === 'object' && value) { - if ('ref' in value) - return refs.get(value.ref); - if ('v' in value) { - if (value.v === 'undefined') - return undefined; - if (value.v === 'null') - return null; - if (value.v === 'NaN') - return NaN; - if (value.v === 'Infinity') - return Infinity; - if (value.v === '-Infinity') - return -Infinity; - if (value.v === '-0') - return -0; - return undefined; - } - if ('d' in value) - return new Date(value.d); - if ('u' in value) - return new URL(value.u); - if ('bi' in value) - return BigInt(value.bi); - if ('e' in value) { - const error = new Error(value.e.m); - error.name = value.e.n; - error.stack = value.e.s; - return error; - } - if ('r' in value) - return new RegExp(value.r.p, value.r.f); - if ('a' in value) { - const result: any[] = []; - refs.set(value.id, result); - for (const a of value.a) - result.push(parseEvaluationResultValue(a, handles, refs)); - return result; - } - if ('o' in value) { - const result: any = {}; - refs.set(value.id, result); - for (const { k, v } of value.o) - result[k] = parseEvaluationResultValue(v, handles, refs); - return result; - } - if ('h' in value) - return handles[value.h]; - if ('ta' in value) - return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]); - } - return value; - } - - function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue { - return serialize(value, handleSerializer, { visited: new Map(), lastId: 0 }); - } - - function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { - if (value && typeof value === 'object') { - // eslint-disable-next-line no-restricted-globals - if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window) - return 'ref: '; - // eslint-disable-next-line no-restricted-globals - if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document) - return 'ref: '; - // eslint-disable-next-line no-restricted-globals - if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node) - return 'ref: '; - } - return innerSerialize(value, handleSerializer, visitorInfo); - } - - function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { - const result = handleSerializer(value); - if ('fallThrough' in result) - value = result.fallThrough; - else - return result; - - if (typeof value === 'symbol') - return { v: 'undefined' }; - if (Object.is(value, undefined)) - return { v: 'undefined' }; - if (Object.is(value, null)) - return { v: 'null' }; - if (Object.is(value, NaN)) - return { v: 'NaN' }; - if (Object.is(value, Infinity)) - return { v: 'Infinity' }; - if (Object.is(value, -Infinity)) - return { v: '-Infinity' }; - if (Object.is(value, -0)) - return { v: '-0' }; - - if (typeof value === 'boolean') - return value; - if (typeof value === 'number') - return value; - if (typeof value === 'string') - return value; - if (typeof value === 'bigint') - return { bi: value.toString() }; - - if (isError(value)) { - let stack; - if (value.stack?.startsWith(value.name + ': ' + value.message)) { - // v8 - stack = value.stack; - } else { - stack = `${value.name}: ${value.message}\n${value.stack}`; - } - return { e: { n: value.name, m: value.message, s: stack } }; - } - if (isDate(value)) - return { d: value.toJSON() }; - if (isURL(value)) - return { u: value.toJSON() }; - if (isRegExp(value)) - return { r: { p: value.source, f: value.flags } }; - for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) { - if (isTypedArray(value, ctor)) - return { ta: { b: typedArrayToBase64(value), k } }; - } - - const id = visitorInfo.visited.get(value); - if (id) - return { ref: id }; - - if (Array.isArray(value)) { - const a = []; - const id = ++visitorInfo.lastId; - visitorInfo.visited.set(value, id); - for (let i = 0; i < value.length; ++i) - a.push(serialize(value[i], handleSerializer, visitorInfo)); - return { a, id }; - } - - if (typeof value === 'object') { - const o: { k: string, v: SerializedValue }[] = []; - const id = ++visitorInfo.lastId; - visitorInfo.visited.set(value, id); - for (const name of Object.keys(value)) { - let item; - try { - item = value[name]; - } catch (e) { - continue; // native bindings will throw sometimes - } - if (name === 'toJSON' && typeof item === 'function') - o.push({ k: name, v: { o: [], id: 0 } }); - else - o.push({ k: name, v: serialize(item, handleSerializer, visitorInfo) }); - } - - let jsonWrapper; - try { - // If Object.keys().length === 0 we fall back to toJSON if it exists - if (o.length === 0 && value.toJSON && typeof value.toJSON === 'function') - jsonWrapper = { value: value.toJSON() }; - } catch (e) { - } - if (jsonWrapper) - return innerSerialize(jsonWrapper.value, handleSerializer, visitorInfo); - - return { o, id }; - } - } - - return { parseEvaluationResultValue, serializeAsCallArgument }; } -const { parseEvaluationResultValue, serializeAsCallArgument } = source(); -export { parseEvaluationResultValue, serializeAsCallArgument }; +function isDate(obj: any): obj is Date { + try { + return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]'; + } catch (error) { + return false; + } +} + +function isURL(obj: any): obj is URL { + try { + return obj instanceof URL || Object.prototype.toString.call(obj) === '[object URL]'; + } catch (error) { + return false; + } +} + +function isError(obj: any): obj is Error { + try { + return obj instanceof Error || (obj && Object.getPrototypeOf(obj)?.name === 'Error'); + } catch (error) { + return false; + } +} + +function isTypedArray(obj: any, constructor: Function): boolean { + try { + return obj instanceof constructor || Object.prototype.toString.call(obj) === `[object ${constructor.name}]`; + } catch (error) { + return false; + } +} + +const typedArrayConstructors: Record = { + i8: Int8Array, + ui8: Uint8Array, + ui8c: Uint8ClampedArray, + i16: Int16Array, + ui16: Uint16Array, + i32: Int32Array, + ui32: Uint32Array, + // TODO: add Float16Array once it's in baseline + f32: Float32Array, + f64: Float64Array, + bi64: BigInt64Array, + bui64: BigUint64Array, +}; + +function typedArrayToBase64(array: any) { + /** + * Firefox does not support iterating over typed arrays, so we use `.toBase64`. + * Error: 'Accessing TypedArray data over Xrays is slow, and forbidden in order to encourage performant code. To copy TypedArrays across origin boundaries, consider using Components.utils.cloneInto().' + */ + if ('toBase64' in array) + return array.toBase64(); + const binary = Array.from(new Uint8Array(array.buffer, array.byteOffset, array.byteLength)).map(b => String.fromCharCode(b)).join(''); + return btoa(binary); +} + +function base64ToTypedArray(base64: string, TypedArrayConstructor: any) { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) + bytes[i] = binary.charCodeAt(i); + return new TypedArrayConstructor(bytes.buffer); +} + +export function parseEvaluationResultValue(value: SerializedValue, handles: any[] = [], refs: Map = new Map()): any { + if (Object.is(value, undefined)) + return undefined; + if (typeof value === 'object' && value) { + if ('ref' in value) + return refs.get(value.ref); + if ('v' in value) { + if (value.v === 'undefined') + return undefined; + if (value.v === 'null') + return null; + if (value.v === 'NaN') + return NaN; + if (value.v === 'Infinity') + return Infinity; + if (value.v === '-Infinity') + return -Infinity; + if (value.v === '-0') + return -0; + return undefined; + } + if ('d' in value) + return new Date(value.d); + if ('u' in value) + return new URL(value.u); + if ('bi' in value) + return BigInt(value.bi); + if ('e' in value) { + const error = new Error(value.e.m); + error.name = value.e.n; + error.stack = value.e.s; + return error; + } + if ('r' in value) + return new RegExp(value.r.p, value.r.f); + if ('a' in value) { + const result: any[] = []; + refs.set(value.id, result); + for (const a of value.a) + result.push(parseEvaluationResultValue(a, handles, refs)); + return result; + } + if ('o' in value) { + const result: any = {}; + refs.set(value.id, result); + for (const { k, v } of value.o) + result[k] = parseEvaluationResultValue(v, handles, refs); + return result; + } + if ('h' in value) + return handles[value.h]; + if ('ta' in value) + return base64ToTypedArray(value.ta.b, typedArrayConstructors[value.ta.k]); + } + return value; +} + +export function serializeAsCallArgument(value: any, handleSerializer: (value: any) => HandleOrValue): SerializedValue { + return serialize(value, handleSerializer, { visited: new Map(), lastId: 0 }); +} + +function serialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { + if (value && typeof value === 'object') { + // eslint-disable-next-line no-restricted-globals + if (typeof globalThis.Window === 'function' && value instanceof globalThis.Window) + return 'ref: '; + // eslint-disable-next-line no-restricted-globals + if (typeof globalThis.Document === 'function' && value instanceof globalThis.Document) + return 'ref: '; + // eslint-disable-next-line no-restricted-globals + if (typeof globalThis.Node === 'function' && value instanceof globalThis.Node) + return 'ref: '; + } + return innerSerialize(value, handleSerializer, visitorInfo); +} + +function innerSerialize(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue { + const result = handleSerializer(value); + if ('fallThrough' in result) + value = result.fallThrough; + else + return result; + + if (typeof value === 'symbol') + return { v: 'undefined' }; + if (Object.is(value, undefined)) + return { v: 'undefined' }; + if (Object.is(value, null)) + return { v: 'null' }; + if (Object.is(value, NaN)) + return { v: 'NaN' }; + if (Object.is(value, Infinity)) + return { v: 'Infinity' }; + if (Object.is(value, -Infinity)) + return { v: '-Infinity' }; + if (Object.is(value, -0)) + return { v: '-0' }; + + if (typeof value === 'boolean') + return value; + if (typeof value === 'number') + return value; + if (typeof value === 'string') + return value; + if (typeof value === 'bigint') + return { bi: value.toString() }; + + if (isError(value)) { + let stack; + if (value.stack?.startsWith(value.name + ': ' + value.message)) { + // v8 + stack = value.stack; + } else { + stack = `${value.name}: ${value.message}\n${value.stack}`; + } + return { e: { n: value.name, m: value.message, s: stack } }; + } + if (isDate(value)) + return { d: value.toJSON() }; + if (isURL(value)) + return { u: value.toJSON() }; + if (isRegExp(value)) + return { r: { p: value.source, f: value.flags } }; + for (const [k, ctor] of Object.entries(typedArrayConstructors) as [TypedArrayKind, Function][]) { + if (isTypedArray(value, ctor)) + return { ta: { b: typedArrayToBase64(value), k } }; + } + + const id = visitorInfo.visited.get(value); + if (id) + return { ref: id }; + + if (Array.isArray(value)) { + const a = []; + const id = ++visitorInfo.lastId; + visitorInfo.visited.set(value, id); + for (let i = 0; i < value.length; ++i) + a.push(serialize(value[i], handleSerializer, visitorInfo)); + return { a, id }; + } + + if (typeof value === 'object') { + const o: { k: string, v: SerializedValue }[] = []; + const id = ++visitorInfo.lastId; + visitorInfo.visited.set(value, id); + for (const name of Object.keys(value)) { + let item; + try { + item = value[name]; + } catch (e) { + continue; // native bindings will throw sometimes + } + if (name === 'toJSON' && typeof item === 'function') + o.push({ k: name, v: { o: [], id: 0 } }); + else + o.push({ k: name, v: serialize(item, handleSerializer, visitorInfo) }); + } + + let jsonWrapper; + try { + // If Object.keys().length === 0 we fall back to toJSON if it exists + if (o.length === 0 && value.toJSON && typeof value.toJSON === 'function') + jsonWrapper = { value: value.toJSON() }; + } catch (e) { + } + if (jsonWrapper) + return innerSerialize(jsonWrapper.value, handleSerializer, visitorInfo); + + return { o, id }; + } +} diff --git a/utils/build/build.js b/utils/build/build.js index ed2a2a0963..3faa844700 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -375,7 +375,6 @@ onChanges.push({ 'packages/playwright-core/src/third_party/**', 'packages/playwright-ct-core/src/injected/**', 'packages/playwright-core/src/utils/isomorphic/**', - 'packages/playwright-core/src/server/storageScript.ts', 'utils/generate_injected_builtins.js', 'utils/generate_injected.js', ], diff --git a/utils/generate_injected.js b/utils/generate_injected.js index e91d2cd566..0750703052 100644 --- a/utils/generate_injected.js +++ b/utils/generate_injected.js @@ -51,7 +51,7 @@ const injectedScripts = [ true, ], [ - path.join(ROOT, 'packages', 'playwright-core', 'src', 'server', 'storageScript.ts'), + path.join(ROOT, 'packages', 'injected', 'src', 'storageScript.ts'), path.join(ROOT, 'packages', 'injected', 'lib'), path.join(ROOT, 'packages', 'playwright-core', 'src', 'generated'), true,