242 lines
7.2 KiB
TypeScript
242 lines
7.2 KiB
TypeScript
/* eslint-disable no-extend-native -- 该文件用于集中安装旧浏览器缺失的原生 API。 */
|
|
|
|
import { deserialize, serialize } from '@ungap/structured-clone';
|
|
|
|
import 'wicg-inert';
|
|
|
|
type AtFunction = (index: number) => unknown;
|
|
type StructuredCloneFunction = <T>(
|
|
value: T,
|
|
options?: StructuredSerializeOptions,
|
|
) => T;
|
|
type WithResolversResult<T> = {
|
|
promise: Promise<T>;
|
|
reject: (reason?: unknown) => void;
|
|
resolve: (value: PromiseLike<T> | T) => void;
|
|
};
|
|
|
|
const objectConstructor = Object as ObjectConstructor & {
|
|
hasOwn?: (object: unknown, property: PropertyKey) => boolean;
|
|
};
|
|
const arrayPrototype = Array.prototype as unknown[] & {
|
|
at?: AtFunction;
|
|
toReversed?: () => unknown[];
|
|
toSorted?: (
|
|
compareFn?: (left: unknown, right: unknown) => number,
|
|
) => unknown[];
|
|
toSpliced?: (
|
|
start: number,
|
|
deleteCount?: number,
|
|
...items: unknown[]
|
|
) => unknown[];
|
|
};
|
|
const stringPrototype = String.prototype as string & {
|
|
at?: AtFunction;
|
|
};
|
|
const promiseConstructor = Promise as PromiseConstructor & {
|
|
withResolvers?: <T>() => WithResolversResult<T>;
|
|
};
|
|
const urlConstructor = URL as typeof URL & {
|
|
canParse?: (url: string | URL, base?: string | URL) => boolean;
|
|
};
|
|
const browserGlobal = globalThis as typeof globalThis & {
|
|
structuredClone?: StructuredCloneFunction;
|
|
};
|
|
|
|
/**
|
|
* 将索引转换为 Array.prototype.at 与 String.prototype.at 使用的整数。
|
|
*/
|
|
function toIntegerOrInfinity(index: number): number {
|
|
const numericIndex = Number(index);
|
|
if (Number.isNaN(numericIndex) || numericIndex === 0) return 0;
|
|
return numericIndex > 0 ? Math.floor(numericIndex) : Math.ceil(numericIndex);
|
|
}
|
|
|
|
/**
|
|
* 安装 Object.hasOwn 兼容实现。
|
|
*/
|
|
export function installObjectHasOwnPolyfill() {
|
|
if (typeof objectConstructor.hasOwn === 'function') return;
|
|
|
|
Object.defineProperty(Object, 'hasOwn', {
|
|
configurable: true,
|
|
value(object: unknown, property: PropertyKey) {
|
|
if (object === null || object === undefined) {
|
|
throw new TypeError('Cannot convert undefined or null to object');
|
|
}
|
|
// eslint-disable-next-line unicorn/new-for-builtins -- Object() 保留 ToObject 语义。
|
|
return Object.prototype.hasOwnProperty.call(Object(object), property);
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 安装 Array.prototype.at 兼容实现。
|
|
*/
|
|
export function installArrayAtPolyfill() {
|
|
if (typeof arrayPrototype.at === 'function') return;
|
|
|
|
Object.defineProperty(Array.prototype, 'at', {
|
|
configurable: true,
|
|
value(this: ArrayLike<unknown> | null | undefined, index: number): unknown {
|
|
if (this === null || this === undefined) {
|
|
throw new TypeError('Array.prototype.at called on null or undefined');
|
|
}
|
|
|
|
const target = this as ArrayLike<unknown>;
|
|
const numericLength = Number(target.length);
|
|
const length =
|
|
Number.isNaN(numericLength) || numericLength <= 0
|
|
? 0
|
|
: Math.min(Math.floor(numericLength), Number.MAX_SAFE_INTEGER);
|
|
const relativeIndex = toIntegerOrInfinity(index);
|
|
const actualIndex =
|
|
relativeIndex >= 0 ? relativeIndex : length + relativeIndex;
|
|
|
|
if (actualIndex < 0 || actualIndex >= length) return undefined;
|
|
return target[actualIndex];
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 安装 String.prototype.at 兼容实现。
|
|
*/
|
|
export function installStringAtPolyfill() {
|
|
if (typeof stringPrototype.at === 'function') return;
|
|
|
|
Object.defineProperty(String.prototype, 'at', {
|
|
configurable: true,
|
|
value(this: unknown, index: number): string | undefined {
|
|
if (this === null || this === undefined) {
|
|
throw new TypeError('String.prototype.at called on null or undefined');
|
|
}
|
|
|
|
const target = String(this);
|
|
const relativeIndex = toIntegerOrInfinity(index);
|
|
const actualIndex =
|
|
relativeIndex >= 0 ? relativeIndex : target.length + relativeIndex;
|
|
return actualIndex < 0 || actualIndex >= target.length
|
|
? undefined
|
|
: target[actualIndex];
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 安装不会修改原数组的数组复制方法。
|
|
*/
|
|
export function installArrayCopyingPolyfills() {
|
|
if (typeof arrayPrototype.toReversed !== 'function') {
|
|
Object.defineProperty(Array.prototype, 'toReversed', {
|
|
configurable: true,
|
|
value() {
|
|
return Array.prototype.slice.call(this).reverse();
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
if (typeof arrayPrototype.toSorted !== 'function') {
|
|
Object.defineProperty(Array.prototype, 'toSorted', {
|
|
configurable: true,
|
|
value(compareFn?: (left: unknown, right: unknown) => number) {
|
|
return Array.prototype.slice.call(this).sort(compareFn);
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
if (typeof arrayPrototype.toSpliced !== 'function') {
|
|
Object.defineProperty(Array.prototype, 'toSpliced', {
|
|
configurable: true,
|
|
value(start: number, deleteCount?: number, ...items: unknown[]) {
|
|
const copy = Array.prototype.slice.call(this);
|
|
if (arguments.length === 1) {
|
|
copy.splice(start);
|
|
} else {
|
|
copy.splice(start, deleteCount as number, ...items);
|
|
}
|
|
return copy;
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 安装 Promise.withResolvers 兼容实现。
|
|
*/
|
|
export function installPromiseWithResolversPolyfill() {
|
|
// eslint-disable-next-line n/no-unsupported-features/es-syntax -- 此处专门检测并补齐该新 API。
|
|
if (typeof promiseConstructor.withResolvers === 'function') return;
|
|
|
|
Object.defineProperty(Promise, 'withResolvers', {
|
|
configurable: true,
|
|
value<T>(this: PromiseConstructor): WithResolversResult<T> {
|
|
let reject!: (reason?: unknown) => void;
|
|
let resolve!: (value: PromiseLike<T> | T) => void;
|
|
const promise = new this<T>((resolvePromise, rejectPromise) => {
|
|
resolve = resolvePromise;
|
|
reject = rejectPromise;
|
|
});
|
|
return { promise, reject, resolve };
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 安装可处理循环引用、Map、Set 与日期的 structuredClone 兼容实现。
|
|
*/
|
|
export function installStructuredClonePolyfill() {
|
|
if (typeof browserGlobal.structuredClone === 'function') return;
|
|
|
|
Object.defineProperty(globalThis, 'structuredClone', {
|
|
configurable: true,
|
|
value: <T>(value: T) => deserialize(serialize(value)) as T,
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 安装 URL.canParse 兼容实现。
|
|
*/
|
|
export function installUrlCanParsePolyfill() {
|
|
if (typeof urlConstructor.canParse === 'function') return;
|
|
|
|
Object.defineProperty(URL, 'canParse', {
|
|
configurable: true,
|
|
value(url: string | URL, base?: string | URL) {
|
|
try {
|
|
const parsedUrl =
|
|
arguments.length > 1
|
|
? new URL(String(url), String(base))
|
|
: new URL(String(url));
|
|
return parsedUrl instanceof URL;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
writable: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 在业务模块加载前安装 Chrome 90 缺失的运行时能力。
|
|
*/
|
|
export function installLegacyBrowserPolyfills() {
|
|
installObjectHasOwnPolyfill();
|
|
installArrayAtPolyfill();
|
|
installStringAtPolyfill();
|
|
installArrayCopyingPolyfills();
|
|
installPromiseWithResolversPolyfill();
|
|
installStructuredClonePolyfill();
|
|
installUrlCanParsePolyfill();
|
|
}
|
|
|
|
installLegacyBrowserPolyfills();
|