forked from umijs/qiankun
-
Notifications
You must be signed in to change notification settings - Fork 0
/
proxySandbox.ts
347 lines (293 loc) · 12.3 KB
/
proxySandbox.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
/* eslint-disable no-param-reassign */
/**
* @author Kuitos
* @since 2020-3-31
*/
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';
import { nextTask } from '../utils';
import { getTargetValue, setCurrentRunningSandboxProxy } from './common';
/**
* fastest(at most time) unique array method
* @see https://jsperf.com/array-filter-unique/30
*/
function uniq(array: Array<string | symbol>) {
return array.filter(function filter(this: PropertyKey[], element) {
return element in this ? false : ((this as any)[element] = true);
}, Object.create(null));
}
// zone.js will overwrite Object.defineProperty
const rawObjectDefineProperty = Object.defineProperty;
const variableWhiteListInDev =
process.env.NODE_ENV === 'development' || window.__QIANKUN_DEVELOPMENT__
? [
// for react hot reload
// see https://github.com/facebook/create-react-app/blob/66bf7dfc43350249e2f09d138a20840dae8a0a4a/packages/react-error-overlay/src/index.js#L180
'__REACT_ERROR_OVERLAY_GLOBAL_HOOK__',
]
: [];
// who could escape the sandbox
const variableWhiteList: PropertyKey[] = [
// FIXME System.js used a indirect call with eval, which would make it scope escape to global
// To make System.js works well, we write it back to global window temporary
// see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/evaluate.js#L106
'System',
// see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/instantiate.js#L357
'__cjsWrapper',
...variableWhiteListInDev,
];
/*
variables who are impossible to be overwrite need to be escaped from proxy sandbox for performance reasons
*/
const unscopables = {
undefined: true,
Array: true,
Object: true,
String: true,
Boolean: true,
Math: true,
Number: true,
Symbol: true,
parseFloat: true,
Float32Array: true,
};
type SymbolTarget = 'target' | 'rawWindow';
type FakeWindow = Window & Record<PropertyKey, any>;
function createFakeWindow(global: Window) {
// map always has the fastest performance in has check scenario
// see https://jsperf.com/array-indexof-vs-set-has/23
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow;
/*
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.
*/
Object.getOwnPropertyNames(global)
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(global, p);
return !descriptor?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(global, p);
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
/*
make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return.
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
> The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property.
*/
if (
p === 'top' ||
p === 'parent' ||
p === 'self' ||
p === 'window' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
descriptor.configurable = true;
/*
The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was
Example:
Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false}
Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false}
*/
if (!hasGetter) {
descriptor.writable = true;
}
}
if (hasGetter) propertiesWithGetter.set(p, true);
// freeze the descriptor to avoid being modified by zone.js
// see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
}
});
return {
fakeWindow,
propertiesWithGetter,
};
}
let activeSandboxCount = 0;
/**
* 基于 Proxy 实现的沙箱
*/
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>();
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true;
latestSetProp: PropertyKey | null = null;
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
...this.updatedValueSet.keys(),
]);
}
if (--activeSandboxCount === 0) {
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
// @ts-ignore
delete window[p];
}
});
}
this.sandboxRunning = false;
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;
const rawWindow = window;
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
// We must kept its description while the property existed in rawWindow before
if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// @ts-ignore
target[p] = value;
}
if (variableWhiteList.indexOf(p) !== -1) {
// @ts-ignore
rawWindow[p] = value;
}
updatedValueSet.add(p);
this.latestSetProp = p;
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
get(target: FakeWindow, p: PropertyKey): any {
setCurrentRunningSandboxProxy(proxy);
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTask(() => setCurrentRunningSandboxProxy(null));
if (p === Symbol.unscopables) return unscopables;
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') {
return proxy;
}
// hijack global accessing with globalThis keyword
if (p === 'globalThis') {
return proxy;
}
if (
p === 'top' ||
p === 'parent' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
// if your master app in an iframe context, allow these props escape the sandbox
if (rawWindow === rawWindow.parent) {
return proxy;
}
return (rawWindow as any)[p];
}
// proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty
if (p === 'hasOwnProperty') {
return hasOwnProperty;
}
// mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher
if (p === 'document' || p === 'eval') {
switch (p) {
case 'document':
return document;
case 'eval':
// eslint-disable-next-line no-eval
return eval;
// no default
}
}
// eslint-disable-next-line no-nested-ternary
const value = propertiesWithGetter.has(p)
? (rawWindow as any)[p]
: p in target
? (target as any)[p]
: (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
},
// 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 rawWindow;
},
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.
*/
if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p);
descriptorTargetMap.set(p, 'target');
return descriptor;
}
if (rawWindow.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
descriptorTargetMap.set(p, 'rawWindow');
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
}
return undefined;
},
// trap to support iterator with sandbox
ownKeys(target: FakeWindow): ArrayLike<string | symbol> {
return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
},
defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
const from = descriptorTargetMap.get(p);
/*
Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
otherwise it would cause a TypeError with illegal invocation.
*/
switch (from) {
case 'rawWindow':
return Reflect.defineProperty(rawWindow, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
},
deleteProperty(target: FakeWindow, p: string | number | symbol): boolean {
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p);
return true;
}
return true;
},
// makes sure `window instanceof Window` returns truthy in micro app
getPrototypeOf() {
return Reflect.getPrototypeOf(rawWindow);
},
});
this.proxy = proxy;
activeSandboxCount++;
}
}