🐛 compatible with legacy browser which not support globalThis in speedy mode (#2400)
This commit is contained in:
parent
25c0449690
commit
ab2c29737e
|
|
@ -13,7 +13,11 @@ writeFileSync(
|
|||
globalsFilePath,
|
||||
`// generated from https://github.com/sindresorhus/globals/blob/main/globals.json es2015 part
|
||||
// only init its values while Proxy is supported
|
||||
export const globals = window.Proxy ? ${JSON.stringify(Object.keys(globals.es2015), null, 2)} : [];`,
|
||||
export const globals = window.Proxy ? ${JSON.stringify(
|
||||
Object.keys(globals.es2015),
|
||||
null,
|
||||
2,
|
||||
)}.filter(p => /* just keep the available properties in current window context */ p in window) : [];`,
|
||||
);
|
||||
|
||||
export default {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import type {
|
|||
ObjectType,
|
||||
} from './interfaces';
|
||||
import { createSandboxContainer, css } from './sandbox';
|
||||
import { scopedGlobals } from './sandbox/common';
|
||||
import { cachedGlobals } from './sandbox/proxySandbox';
|
||||
import {
|
||||
Deferred,
|
||||
genAppInstanceIdByName,
|
||||
|
|
@ -345,7 +345,7 @@ export async function loadApp<T extends ObjectType>(
|
|||
|
||||
// get the lifecycle hooks from module exports
|
||||
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
|
||||
scopedGlobalVariables: speedySandbox ? scopedGlobals : [],
|
||||
scopedGlobalVariables: speedySandbox ? cachedGlobals : [],
|
||||
});
|
||||
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
|
||||
scriptExports,
|
||||
|
|
|
|||
72
src/sandbox/__tests__/proxySandbox.speedy.test.ts
Normal file
72
src/sandbox/__tests__/proxySandbox.speedy.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import ProxySandbox from '../proxySandbox';
|
||||
|
||||
it('should never throw errors although globalThis is unavailable in current global context', () => {
|
||||
const { proxy } = new ProxySandbox('globalThis-always-available');
|
||||
// @ts-ignore
|
||||
window.proxy = proxy;
|
||||
|
||||
expect('mockGlobalThis' in window).toBe(false);
|
||||
expect('mockGlobalThis' in proxy).toBe(true);
|
||||
|
||||
const code = `(function() {
|
||||
with (window.proxy) {
|
||||
(function(mockGlobalThis){
|
||||
mockGlobalThis.testName = 'kuitos';
|
||||
})(mockGlobalThis);
|
||||
}
|
||||
})()`;
|
||||
// eslint-disable-next-line no-eval
|
||||
const geval = eval;
|
||||
geval(code);
|
||||
|
||||
// @ts-ignore
|
||||
expect(proxy.testName).toBe('kuitos');
|
||||
// @ts-ignore
|
||||
expect(window.testName).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw errors while variable not existed in current global context', () => {
|
||||
const { proxy } = new ProxySandbox('invalid-throw-error');
|
||||
// @ts-ignore
|
||||
window.proxy = proxy;
|
||||
|
||||
expect('invalidVariable' in window).toBe(false);
|
||||
expect('invalidVariable' in proxy).toBe(false);
|
||||
|
||||
const code = `(function() {
|
||||
with (window.proxy) {
|
||||
(function(mockGlobalThis){
|
||||
(0, invalidVariable);
|
||||
})(mockGlobalThis);
|
||||
}
|
||||
})()`;
|
||||
// eslint-disable-next-line no-eval
|
||||
const geval = eval;
|
||||
try {
|
||||
geval(code);
|
||||
} catch (e: any) {
|
||||
expect(e.message).toBe('invalidVariable is not defined');
|
||||
}
|
||||
});
|
||||
|
||||
it('should never hijack native method of Object.prototype', () => {
|
||||
const { proxy } = new ProxySandbox('native-object-method');
|
||||
// @ts-ignore
|
||||
window.proxy = proxy;
|
||||
|
||||
const code = `(function() {
|
||||
with (window.proxy) {
|
||||
(function(mockGlobalThis){
|
||||
window.nativeHasOwnCheckResult = hasOwnProperty.call({nativeHas: 123}, 'nativeHas');
|
||||
window.proxyHasOwnCheck = window.hasOwnProperty.call({nativeHas: '123'}, 'nativeHas');
|
||||
window.selfCheck = window.hasOwnProperty('nativeHasOwnCheckResult');
|
||||
})(mockGlobalThis);
|
||||
}
|
||||
})()`;
|
||||
// eslint-disable-next-line no-eval
|
||||
const geval = eval;
|
||||
geval(code);
|
||||
expect(window.proxy.nativeHasOwnCheckResult).toBeTruthy();
|
||||
expect(window.proxy.proxyHasOwnCheck).toBeFalsy();
|
||||
expect(window.proxy.selfCheck).toBeTruthy();
|
||||
});
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
*/
|
||||
|
||||
import { isBoundedFunction, isCallable, isConstructable } from '../utils';
|
||||
import { globals } from './globals';
|
||||
|
||||
type AppInstance = { name: string; window: WindowProxy };
|
||||
let currentRunningApp: AppInstance | null = null;
|
||||
|
|
@ -21,9 +20,6 @@ export function setCurrentRunningApp(appInstance: { name: string; window: Window
|
|||
currentRunningApp = appInstance;
|
||||
}
|
||||
|
||||
export const overwrittenGlobals = ['window', 'self', 'globalThis'];
|
||||
export const scopedGlobals = Array.from(new Set([...globals, ...overwrittenGlobals, 'requestAnimationFrame']));
|
||||
|
||||
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
|
||||
|
||||
export function getTargetValue(target: any, value: any): any {
|
||||
|
|
|
|||
|
|
@ -59,5 +59,5 @@ export const globals = window.Proxy
|
|||
'valueOf',
|
||||
'WeakMap',
|
||||
'WeakSet',
|
||||
]
|
||||
].filter((p) => /* just keep the available properties in current window context */ p in window)
|
||||
: [];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { execScripts } from 'import-html-entry';
|
|||
import { isFunction } from 'lodash';
|
||||
import { frameworkConfiguration } from '../../../apis';
|
||||
import { qiankunHeadTagName } from '../../../utils';
|
||||
import { scopedGlobals } from '../../common';
|
||||
import { cachedGlobals } from '../../proxySandbox';
|
||||
import * as css from '../css';
|
||||
|
||||
export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
|
||||
|
|
@ -280,7 +280,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
|
|||
const { fetch } = frameworkConfiguration;
|
||||
const referenceNode = mountDOM.contains(refChild) ? refChild : null;
|
||||
|
||||
const scopedGlobalVariables = speedySandbox ? scopedGlobals : [];
|
||||
const scopedGlobalVariables = speedySandbox ? cachedGlobals : [];
|
||||
|
||||
if (src) {
|
||||
let isRedfinedCurrentScript = false;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { without } from 'lodash';
|
|||
import type { SandBox } from '../interfaces';
|
||||
import { SandBoxType } from '../interfaces';
|
||||
import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils';
|
||||
import { overwrittenGlobals, getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common';
|
||||
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common';
|
||||
import { globals } from './globals';
|
||||
|
||||
type SymbolTarget = 'target' | 'globalContext';
|
||||
|
|
@ -47,17 +47,32 @@ const globalVariableWhiteList: string[] = [
|
|||
...variableWhiteListInDev,
|
||||
];
|
||||
|
||||
// these globals should be recorded in every accessing
|
||||
const accessingSpiedGlobals = ['document', 'top', 'parent', 'hasOwnProperty', 'eval'];
|
||||
const inTest = process.env.NODE_ENV === 'test';
|
||||
const mockSafariTop = 'mockSafariTop';
|
||||
const mockTop = 'mockTop';
|
||||
const mockGlobalThis = 'mockGlobalThis';
|
||||
|
||||
// these globals should be recorded while accessing every time
|
||||
const accessingSpiedGlobals = ['document', 'top', 'parent', 'eval'];
|
||||
const overwrittenGlobals = ['window', 'self', 'globalThis'].concat(inTest ? [mockGlobalThis] : []);
|
||||
export const cachedGlobals = Array.from(
|
||||
new Set(without([...globals, ...overwrittenGlobals, 'requestAnimationFrame'], ...accessingSpiedGlobals)),
|
||||
);
|
||||
|
||||
// transform cachedGlobals to object for faster element check
|
||||
const cachedGlobalObjects = cachedGlobals.reduce((acc, globalProp) => ({ ...acc, [globalProp]: true }), {});
|
||||
|
||||
/*
|
||||
variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons.
|
||||
Variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons.
|
||||
But overwritten globals must not be escaped, otherwise they will be leaked to the global scope.
|
||||
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables
|
||||
*/
|
||||
const unscopables = without(globals, ...accessingSpiedGlobals, ...overwrittenGlobals).reduce(
|
||||
const unscopables = without(cachedGlobals, ...overwrittenGlobals).reduce(
|
||||
// Notes that babel will transpile spread operator to Object.assign({}, ...args), which will keep the prototype of Object in merged object,
|
||||
// while this result used as Symbol.unscopables, it will make properties in Object.prototype always be escaped from proxy sandbox as unscopables check will look up prototype chain as well,
|
||||
// such as hasOwnProperty, toString, valueOf, etc.
|
||||
(acc, key) => ({ ...acc, [key]: true }),
|
||||
{
|
||||
__proto__: null,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
|
||||
|
|
@ -96,7 +111,7 @@ function createFakeWindow(globalContext: Window) {
|
|||
p === 'parent' ||
|
||||
p === 'self' ||
|
||||
p === 'window' ||
|
||||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
|
||||
(inTest && (p === mockTop || p === mockSafariTop))
|
||||
) {
|
||||
descriptor.configurable = true;
|
||||
/*
|
||||
|
|
@ -153,7 +168,7 @@ export default class ProxySandbox implements SandBox {
|
|||
]);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'test' || --activeSandboxCount === 0) {
|
||||
if (inTest || --activeSandboxCount === 0) {
|
||||
// reset the global value to the prev value
|
||||
Object.keys(this.globalWhitelistPrevDescriptor).forEach((p) => {
|
||||
const descriptor = this.globalWhitelistPrevDescriptor[p];
|
||||
|
|
@ -236,15 +251,11 @@ export default class ProxySandbox implements SandBox {
|
|||
}
|
||||
|
||||
// hijack globalWindow accessing with globalThis keyword
|
||||
if (p === 'globalThis') {
|
||||
if (p === 'globalThis' || (inTest && p === mockGlobalThis)) {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
if (
|
||||
p === 'top' ||
|
||||
p === 'parent' ||
|
||||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
|
||||
) {
|
||||
if (p === 'top' || p === 'parent' || (inTest && (p === mockTop || p === mockSafariTop))) {
|
||||
// if your master app in an iframe context, allow these props escape the sandbox
|
||||
if (globalContext === globalContext.parent) {
|
||||
return proxy;
|
||||
|
|
@ -286,14 +297,15 @@ export default class ProxySandbox implements SandBox {
|
|||
// trap in operator
|
||||
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
|
||||
has(target: FakeWindow, p: string | number | symbol): boolean {
|
||||
return p in unscopables || p in target || p in globalContext;
|
||||
// property in cachedGlobalObjects must return true to avoid escape from get trap
|
||||
return p in cachedGlobalObjects || p in target || p in globalContext;
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
|
||||
/*
|
||||
as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
|
||||
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
|
||||
> A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
|
||||
> A property cannot be reported as non-configurable, if it does not existed as an own property of the target object or if it exists as a configurable own property of the target object.
|
||||
*/
|
||||
if (target.hasOwnProperty(p)) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(target, p);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user