🐛 compatible with legacy browser which not support globalThis in speedy mode (#2400)

This commit is contained in:
Kuitos 2023-02-16 21:30:23 +08:00 committed by GitHub
parent 25c0449690
commit ab2c29737e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 112 additions and 28 deletions

View File

@ -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 {

View File

@ -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,

View 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();
});

View File

@ -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 {

View File

@ -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)
: [];

View File

@ -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;

View File

@ -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);