diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index e12dcbe..30c12ce 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -5,7 +5,7 @@ import { getDefaultTplWrapper, getWrapperId, getXPathForElement, - isPropertyReadonly, + isPropertyFrozen, nextTask, sleep, validateExportLifecycle, @@ -152,13 +152,13 @@ it('should genAppInstanceIdByName work well', () => { expect(instanceId3).toBe('hello_2'); }); -it('should isPropertyReadonly work well', () => { +it('should isPropertyFrozen work well', () => { const a = { get name() { return 'read only'; }, }; - expect(isPropertyReadonly(a, 'name')).toBeTruthy(); + expect(isPropertyFrozen(a, 'name')).toBeFalsy(); const b = { get name() { @@ -166,19 +166,28 @@ it('should isPropertyReadonly work well', () => { }, set name(_) {}, }; - expect(isPropertyReadonly(b, 'name')).toBeFalsy(); + expect(isPropertyFrozen(b, 'name')).toBeFalsy(); const c = {}; Object.defineProperty(c, 'name', { writable: false }); - expect(isPropertyReadonly(c, 'name')).toBeTruthy(); + expect(isPropertyFrozen(c, 'name')).toBeTruthy(); const d = {}; Object.defineProperty(d, 'name', { configurable: true }); - expect(isPropertyReadonly(d, 'name')).toBeTruthy(); + expect(isPropertyFrozen(d, 'name')).toBeFalsy(); const e = {}; - Object.defineProperty(e, 'name', { writable: true }); - expect(isPropertyReadonly(e, 'name')).toBeFalsy(); + Object.defineProperty(e, 'name', { configurable: false }); + expect(isPropertyFrozen(e, 'name')).toBeTruthy(); - expect(isPropertyReadonly(undefined, 'name')).toBeFalsy(); + const f = {}; + Object.defineProperty(f, 'name', { + get() { + return 'test'; + }, + configurable: false, + }); + expect(isPropertyFrozen(f, 'name')).toBeTruthy(); + + expect(isPropertyFrozen(undefined, 'name')).toBeFalsy(); }); diff --git a/src/sandbox/__tests__/proxySandbox.test.ts b/src/sandbox/__tests__/proxySandbox.test.ts index f18ec9a..009b468 100644 --- a/src/sandbox/__tests__/proxySandbox.test.ts +++ b/src/sandbox/__tests__/proxySandbox.test.ts @@ -3,7 +3,7 @@ * @since 2020-03-31 */ -import { isBoundedFunction, isPropertyReadonly } from '../../utils'; +import { isBoundedFunction } from '../../utils'; import { getCurrentRunningApp } from '../common'; import ProxySandbox from '../proxySandbox'; @@ -26,7 +26,7 @@ beforeAll(() => { }); }); -test('iterator should be worked the same as the raw window', () => { +it('iterator should be worked the same as the raw window', () => { Object.defineProperty(window, 'nonEnumerableValue', { enumerable: false, value: 1, @@ -59,7 +59,7 @@ test('iterator should be worked the same as the raw window', () => { expect(Object.keys(proxy)).toEqual([...Object.keys(window), 'additionalProp']); }); -test('window.self & window.window & window.top & window.parent should equals with sandbox', () => { +it('window.self & window.window & window.top & window.parent should equals with sandbox', () => { const { proxy } = new ProxySandbox('unit-test'); expect(proxy.self).toBe(proxy); @@ -71,12 +71,12 @@ test('window.self & window.window & window.top & window.parent should equals wit expect(proxy.parent).toBe(proxy); }); -test('globalThis should equals with sandbox', () => { +it('globalThis should equals with sandbox', () => { const { proxy } = new ProxySandbox('globalThis'); expect(proxy.globalThis).toBe(proxy); }); -test('allow window.top & window.parent to escape sandbox while in iframe', () => { +it('allow window.top & window.parent to escape sandbox while in iframe', () => { // change window.parent to cheat ProxySandbox is in iframe Object.defineProperty(window, 'parent', { value: 'parent' }); Object.defineProperty(window, 'top', { value: 'top' }); @@ -86,7 +86,7 @@ test('allow window.top & window.parent to escape sandbox while in iframe', () => expect(proxy.parent).toBe('parent'); }); -test('eval should never be represented', () => { +it('eval should never be represented', () => { const { proxy } = new ProxySandbox('eval-test'); // @ts-ignore window.proxy = proxy; @@ -102,7 +102,7 @@ test('eval should never be represented', () => { expect(window.testEval).toBeUndefined(); }); -test('hasOwnProperty should works well', () => { +it('hasOwnProperty should works well', () => { const { proxy } = new ProxySandbox('unit-test'); proxy.testName = 'kuitos'; @@ -120,7 +120,7 @@ test('hasOwnProperty should works well', () => { }); }); -test('descriptor of non-configurable and non-enumerable property existed in raw window should be the same after modified in sandbox', () => { +it('descriptor of non-configurable and non-enumerable property existed in raw window should be the same after modified in sandbox', () => { Object.defineProperty(window, 'nonConfigurableProp', { configurable: false, writable: true }); // eslint-disable-next-line getter-return Object.defineProperty(window, 'nonConfigurablePropWithAccessor', { @@ -161,14 +161,14 @@ test('descriptor of non-configurable and non-enumerable property existed in raw }); }); -test('A property cannot be reported as non-configurable, if it does not exists as an own property of the target object', () => { +it('A property cannot be reported as non-configurable, if it does not exists as an own property of the target object', () => { const { proxy } = new ProxySandbox('non-configurable'); Object.defineProperty(window, 'nonConfigurablePropAfterSandboxCreated', { value: 'test', configurable: false }); const descriptor = Object.getOwnPropertyDescriptor(proxy, 'nonConfigurablePropAfterSandboxCreated'); expect(descriptor?.configurable).toBeTruthy(); }); -test('property added by Object.defineProperty should works as expect', () => { +it('property added by Object.defineProperty should works as expect', () => { const { proxy } = new ProxySandbox('object-define-property-test'); let v: any; @@ -197,7 +197,7 @@ test('property added by Object.defineProperty should works as expect', () => { }); }); -test('defineProperty should added to the target where its descriptor from', () => { +it('defineProperty should added to the target where its descriptor from', () => { Object.defineProperty(window, 'propertyInNativeWindow', { get(this: any) { // distinguish it from internal target or raw window with property length @@ -219,7 +219,7 @@ test('defineProperty should added to the target where its descriptor from', () = expect(proxy.propertyInNativeWindow).toBe('ifAccessByInternalTargetWillCauseIllegalInvocation'); }); -test('hasOwnProperty should always returns same reference', () => { +it('hasOwnProperty should always returns same reference', () => { const proxy = new ProxySandbox('hasOwnProperty-test').proxy as any; proxy.testA = {}; proxy.testB = {}; @@ -227,7 +227,7 @@ test('hasOwnProperty should always returns same reference', () => { expect(proxy.testA.hasOwnProperty).toBe(proxy.testB.hasOwnProperty); }); -test('document and eval accessing should modify the attachDocProxySymbol value every time', () => { +it('document and eval accessing should modify the attachDocProxySymbol value every time', () => { const proxy1 = new ProxySandbox('doc-access-test1').proxy; const proxy2 = new ProxySandbox('doc-access-test2').proxy; const proxy3 = new ProxySandbox('eval-access-test1').proxy; @@ -255,7 +255,7 @@ test('document and eval accessing should modify the attachDocProxySymbol value e expect(eval1).toBe(eval); }); -test('document attachDocProxySymbol mark should be remove before next task', (done) => { +it('document attachDocProxySymbol mark should be remove before next task', (done) => { const { proxy } = new ProxySandbox('doc-symbol'); // just access // @ts-ignore @@ -270,7 +270,7 @@ test('document attachDocProxySymbol mark should be remove before next task', (do }); }); -test('document should work well with MutationObserver', (done) => { +it('document should work well with MutationObserver', (done) => { const docProxy = new ProxySandbox('doc').proxy; const observer = new MutationObserver((mutations) => { @@ -290,7 +290,7 @@ test('document should work well with MutationObserver', (done) => { docProxy.document.body.innerHTML = '
'; }); -test('bounded function should not be rebounded', () => { +it('bounded function should not be rebounded', () => { const proxy = new ProxySandbox('bound-fn-test').proxy as any; const fn = () => {}; const boundedFn = fn.bind(null); @@ -302,25 +302,32 @@ test('bounded function should not be rebounded', () => { expect(isBoundedFunction(proxy.fn1)).toBeTruthy(); }); -test('readonly property should not be overwrite', () => { - const proxy = new ProxySandbox('bound-fn-test').proxy as any; +it('frozen property should not be overwrite', () => { + const globalContext = window; + const fn = () => {}; + Object.defineProperty(globalContext, 'frozenProp', { + value: fn, + configurable: false, + enumerable: true, + writable: false, + }); + const proxy = new ProxySandbox('bound-fn-test', globalContext).proxy as any; + + expect(proxy.frozenProp).toBe(fn); proxy.normalField = 'normalFieldValue'; - - Object.defineProperties(proxy, { - readOnlyField: { - value: 'readOnlyFieldValue', - configurable: false, - enumerable: true, - writable: false, - }, + Object.defineProperty(proxy, 'frozenPropInProxy', { + value: fn, + configurable: false, + enumerable: true, + writable: false, }); - expect(isPropertyReadonly(proxy, 'normalField')).toBeFalsy(); - expect(isPropertyReadonly(proxy, 'readOnlyField')).toBeTruthy(); + expect(proxy.normalField).toBe('normalFieldValue'); + expect(proxy.frozenPropInProxy).toBe(fn); }); -test('the prototype should be kept while we create a function with prototype on proxy', () => { +it('the prototype should be kept while we create a function with prototype on proxy', () => { const proxy = new ProxySandbox('new-function').proxy as any; function test() {} @@ -330,7 +337,7 @@ test('the prototype should be kept while we create a function with prototype on expect(proxy.fn.prototype).toBe(test.prototype); }); -test('some native window property was defined with getter in safari and firefox, and they will check the caller source', () => { +it('some native window property was defined with getter in safari and firefox, and they will check the caller source', () => { Object.defineProperty(window, 'mockSafariGetterProperty', { get(this: Window) { // distinguish it from internal target or raw window with property length @@ -351,7 +358,7 @@ test('some native window property was defined with getter in safari and firefox, expect(proxy.mockSafariGetterProperty).toBe('getterPropertyInSafariWindow'); }); -test('falsy values should return as expected', () => { +it('falsy values should return as expected', () => { const { proxy } = new ProxySandbox('falsy-value-test'); proxy.falsevar = false; proxy.nullvar = null; diff --git a/src/sandbox/common.ts b/src/sandbox/common.ts index bdf6d02..bacf44b 100644 --- a/src/sandbox/common.ts +++ b/src/sandbox/common.ts @@ -3,7 +3,7 @@ * @since 2020-04-13 */ -import { isBoundedFunction, isCallable, isConstructable, isPropertyReadonly } from '../utils'; +import { isBoundedFunction, isCallable, isConstructable } from '../utils'; type AppInstance = { name: string; window: WindowProxy }; let currentRunningApp: AppInstance | null = null; @@ -21,14 +21,15 @@ export function setCurrentRunningApp(appInstance: { name: string; window: Window } const functionBoundedValueMap = new WeakMap(); -export function getTargetValue(target: any, value: any, p?: any): any { + +export function getTargetValue(target: any, value: any): any { /* - 仅绑定 isCallable && !isPropertyReadonly && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常 + 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常 目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断 @warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常) @warning 对于configurable及writable都为false的readonly属性,proxy必须返回原值 */ - if (isCallable(value) && !isPropertyReadonly(target, p) && !isBoundedFunction(value) && !isConstructable(value)) { + if (isCallable(value) && !isBoundedFunction(value) && !isConstructable(value)) { const cachedBoundFunction = functionBoundedValueMap.get(value); if (cachedBoundFunction) { return cachedBoundFunction; diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index e7b9907..7eca70d 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -5,7 +5,7 @@ */ import type { SandBox } from '../interfaces'; import { SandBoxType } from '../interfaces'; -import { nativeGlobal, nextTask } from '../utils'; +import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils'; import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp, unscopedGlobals } from './common'; type SymbolTarget = 'target' | 'globalContext'; @@ -65,7 +65,7 @@ function createFakeWindow(globalContext: Window) { /* copy the non-configurable property of global to fakeWindow 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 exist as an own property of the target object or if it exists as a configurable own property of the target object. */ Object.getOwnPropertyNames(globalContext) .filter((p) => { @@ -250,11 +250,14 @@ export default class ProxySandbox implements SandBox { return eval; } - const value = propertiesWithGetter.has(p) - ? (globalContext as any)[p] - : p in target - ? (target as any)[p] - : (globalContext as any)[p]; + const actualTarget = propertiesWithGetter.has(p) ? globalContext : p in target ? target : globalContext; + const value = actualTarget[p]; + + // frozen value should return directly, see https://github.com/umijs/qiankun/issues/2015 + if (isPropertyFrozen(actualTarget, p)) { + return value; + } + /* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation' See this code: const proxy = new Proxy(window, {}); @@ -262,7 +265,7 @@ export default class ProxySandbox implements SandBox { proxyFetch('https://qiankun.com'); */ const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext; - return getTargetValue(boundTarget, value, p); + return getTargetValue(boundTarget, value); }, // trap in operator diff --git a/src/utils.ts b/src/utils.ts index 213c557..d47e789 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -76,7 +76,7 @@ export function isConstructable(fn: () => any | FunctionConstructor) { */ const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined'; const callableFnCacheMap = new WeakMap(); -export const isCallable = (fn: any) => { +export function isCallable(fn: any) { if (callableFnCacheMap.has(fn)) { return true; } @@ -86,7 +86,7 @@ export const isCallable = (fn: any) => { callableFnCacheMap.set(fn, callable); } return callable; -}; +} /** * isPropertyReadonly @@ -94,28 +94,29 @@ export const isCallable = (fn: any) => { * @param p * @returns boolean */ -const propertyReadonlyCacheMap = new WeakMap>(); -export function isPropertyReadonly(target: any, p?: PropertyKey): boolean { +const frozenPropertyCacheMap = new WeakMap>(); +export function isPropertyFrozen(target: any, p?: PropertyKey): boolean { if (!target || !p) { return false; } - const targetPropertiesFromCache = propertyReadonlyCacheMap.get(target) || {}; + const targetPropertiesFromCache = frozenPropertyCacheMap.get(target) || {}; if (targetPropertiesFromCache[p]) { return targetPropertiesFromCache[p]; } const propertyDescriptor = Object.getOwnPropertyDescriptor(target, p); - const readonly = Boolean( + const frozen = Boolean( propertyDescriptor && + propertyDescriptor.configurable === false && (propertyDescriptor.writable === false || (propertyDescriptor.get && !propertyDescriptor.set)), ); - targetPropertiesFromCache[p] = readonly; - propertyReadonlyCacheMap.set(target, targetPropertiesFromCache); + targetPropertiesFromCache[p] = frozen; + frozenPropertyCacheMap.set(target, targetPropertiesFromCache); - return readonly; + return frozen; } const boundedMap = new WeakMap();