mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 03:33:06 +08:00
feat(plugin-infra): init permission control (#3461)
This commit is contained in:
@@ -0,0 +1,86 @@
|
|||||||
|
export interface FetchOptions {
|
||||||
|
fetch?: typeof fetch;
|
||||||
|
signal?: AbortSignal;
|
||||||
|
|
||||||
|
normalizeURL?(url: string): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtualize a url
|
||||||
|
* @param url URL to be rewrite
|
||||||
|
* @param direction Direction of this rewrite.
|
||||||
|
* 'in' means the url is from the outside world and should be virtualized.
|
||||||
|
* 'out' means the url is from the inside world and should be de-virtualized to fetch the real target.
|
||||||
|
*/
|
||||||
|
rewriteURL?(url: string, direction: 'in' | 'out'): string;
|
||||||
|
|
||||||
|
replaceRequest?(request: Request): Request | PromiseLike<Request>;
|
||||||
|
|
||||||
|
replaceResponse?(response: Response): Response | PromiseLike<Response>;
|
||||||
|
|
||||||
|
canConnect?(url: string): boolean | PromiseLike<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFetch(options: FetchOptions) {
|
||||||
|
const {
|
||||||
|
fetch: _fetch = fetch,
|
||||||
|
signal,
|
||||||
|
rewriteURL,
|
||||||
|
replaceRequest,
|
||||||
|
replaceResponse,
|
||||||
|
canConnect,
|
||||||
|
normalizeURL,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
return async function fetch(input: RequestInfo, init?: RequestInit) {
|
||||||
|
let request = new Request(input, {
|
||||||
|
...init,
|
||||||
|
signal: getMergedSignal(init?.signal, signal) || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (normalizeURL) request = new Request(normalizeURL(request.url), request);
|
||||||
|
if (canConnect && !(await canConnect(request.url)))
|
||||||
|
throw new TypeError('Failed to fetch');
|
||||||
|
if (rewriteURL)
|
||||||
|
request = new Request(rewriteURL(request.url, 'out'), request);
|
||||||
|
if (replaceRequest) request = await replaceRequest(request);
|
||||||
|
|
||||||
|
let response = await _fetch(request);
|
||||||
|
|
||||||
|
if (rewriteURL) {
|
||||||
|
const { url, redirected, type } = response;
|
||||||
|
// Note: Response constructor does not allow us to set the url of a response.
|
||||||
|
// we have to define the own property on it. This is not a good simulation.
|
||||||
|
// To prevent get the original url by Response.prototype.[[get url]].call(response)
|
||||||
|
// we copy a response and set it's url to empty.
|
||||||
|
response = new Response(response.body, response);
|
||||||
|
Object.defineProperties(response, {
|
||||||
|
url: { value: url, configurable: true },
|
||||||
|
redirected: { value: redirected, configurable: true },
|
||||||
|
type: { value: type, configurable: true },
|
||||||
|
});
|
||||||
|
Object.defineProperty(response, 'url', {
|
||||||
|
configurable: true,
|
||||||
|
value: rewriteURL(url, 'in'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (replaceResponse) response = await replaceResponse(response);
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMergedSignal(
|
||||||
|
signal: AbortSignal | undefined | null,
|
||||||
|
signal2: AbortSignal | undefined | null
|
||||||
|
) {
|
||||||
|
if (!signal) return signal2;
|
||||||
|
if (!signal2) return signal;
|
||||||
|
|
||||||
|
const abortController = new AbortController();
|
||||||
|
signal.addEventListener('abort', () => abortController.abort(), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
signal2.addEventListener('abort', () => abortController.abort(), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
return abortController.signal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
type Handler = (...args: any[]) => void;
|
||||||
|
|
||||||
|
export interface Timers {
|
||||||
|
setTimeout: (handler: Handler, timeout?: number, ...args: any[]) => number;
|
||||||
|
clearTimeout: (handle: number) => void;
|
||||||
|
setInterval: (handler: Handler, timeout?: number, ...args: any[]) => number;
|
||||||
|
clearInterval: (handle: number) => void;
|
||||||
|
requestAnimationFrame: (callback: Handler) => number;
|
||||||
|
cancelAnimationFrame: (handle: number) => void;
|
||||||
|
requestIdleCallback?: typeof window.requestIdleCallback | undefined;
|
||||||
|
cancelIdleCallback?: typeof window.cancelIdleCallback | undefined;
|
||||||
|
queueMicrotask: typeof window.queueMicrotask;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTimers(
|
||||||
|
abortSignal: AbortSignal,
|
||||||
|
originalTimes: Timers = {
|
||||||
|
requestAnimationFrame,
|
||||||
|
cancelAnimationFrame,
|
||||||
|
requestIdleCallback:
|
||||||
|
typeof requestIdleCallback === 'function'
|
||||||
|
? requestIdleCallback
|
||||||
|
: undefined,
|
||||||
|
cancelIdleCallback:
|
||||||
|
typeof cancelIdleCallback === 'function' ? cancelIdleCallback : undefined,
|
||||||
|
setTimeout,
|
||||||
|
clearTimeout,
|
||||||
|
setInterval,
|
||||||
|
clearInterval,
|
||||||
|
queueMicrotask,
|
||||||
|
}
|
||||||
|
): Timers {
|
||||||
|
const {
|
||||||
|
requestAnimationFrame: _requestAnimationFrame,
|
||||||
|
cancelAnimationFrame: _cancelAnimationFrame,
|
||||||
|
setInterval: _setInterval,
|
||||||
|
clearInterval: _clearInterval,
|
||||||
|
setTimeout: _setTimeout,
|
||||||
|
clearTimeout: _clearTimeout,
|
||||||
|
cancelIdleCallback: _cancelIdleCallback,
|
||||||
|
requestIdleCallback: _requestIdleCallback,
|
||||||
|
queueMicrotask: _queueMicrotask,
|
||||||
|
} = originalTimes;
|
||||||
|
|
||||||
|
const interval_timer_id: number[] = [];
|
||||||
|
const idle_id: number[] = [];
|
||||||
|
const raf_id: number[] = [];
|
||||||
|
|
||||||
|
abortSignal.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => {
|
||||||
|
raf_id.forEach(_cancelAnimationFrame);
|
||||||
|
interval_timer_id.forEach(_clearInterval);
|
||||||
|
_cancelIdleCallback && idle_id.forEach(_cancelIdleCallback);
|
||||||
|
},
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// id is a positive number, it never repeats.
|
||||||
|
requestAnimationFrame(callback) {
|
||||||
|
raf_id[raf_id.length] = _requestAnimationFrame(callback);
|
||||||
|
return raf_id.length;
|
||||||
|
},
|
||||||
|
cancelAnimationFrame(handle) {
|
||||||
|
const id = raf_id[handle - 1];
|
||||||
|
if (!id) return;
|
||||||
|
_cancelAnimationFrame(id);
|
||||||
|
},
|
||||||
|
setInterval(handler, timeout) {
|
||||||
|
interval_timer_id[interval_timer_id.length] = (_setInterval as any)(
|
||||||
|
handler,
|
||||||
|
timeout
|
||||||
|
);
|
||||||
|
return interval_timer_id.length;
|
||||||
|
},
|
||||||
|
clearInterval(id) {
|
||||||
|
if (!id) return;
|
||||||
|
const handle = interval_timer_id[id - 1];
|
||||||
|
if (!handle) return;
|
||||||
|
_clearInterval(handle);
|
||||||
|
},
|
||||||
|
setTimeout(handler, timeout) {
|
||||||
|
idle_id[idle_id.length] = (_setTimeout as any)(handler, timeout);
|
||||||
|
return idle_id.length;
|
||||||
|
},
|
||||||
|
clearTimeout(id) {
|
||||||
|
if (!id) return;
|
||||||
|
const handle = idle_id[id - 1];
|
||||||
|
if (!handle) return;
|
||||||
|
_clearTimeout(handle);
|
||||||
|
},
|
||||||
|
requestIdleCallback: _requestIdleCallback
|
||||||
|
? function requestIdleCallback(callback, options) {
|
||||||
|
idle_id[idle_id.length] = _requestIdleCallback(callback, options);
|
||||||
|
return idle_id.length;
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
cancelIdleCallback: _cancelIdleCallback
|
||||||
|
? function cancelIdleCallback(handle) {
|
||||||
|
const id = idle_id[handle - 1];
|
||||||
|
if (!id) return;
|
||||||
|
_cancelIdleCallback(id);
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
queueMicrotask(callback) {
|
||||||
|
_queueMicrotask(() => abortSignal.aborted || callback());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as AFFiNEComponent from '@affine/component';
|
import * as AFFiNEComponent from '@affine/component';
|
||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
import * as BlockSuiteBlocksStd from '@blocksuite/blocks/std';
|
||||||
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
|
import * as BlockSuiteGlobalUtils from '@blocksuite/global/utils';
|
||||||
import * as Icons from '@blocksuite/icons';
|
import * as Icons from '@blocksuite/icons';
|
||||||
@@ -11,6 +12,11 @@ import * as ReactDom from 'react-dom';
|
|||||||
import * as ReactDomClient from 'react-dom/client';
|
import * as ReactDomClient from 'react-dom/client';
|
||||||
import * as SWR from 'swr';
|
import * as SWR from 'swr';
|
||||||
|
|
||||||
|
import { createFetch } from './endowments/fercher';
|
||||||
|
import { createTimers } from './endowments/timer';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('plugins:permission');
|
||||||
|
|
||||||
const setupImportsMap = () => {
|
const setupImportsMap = () => {
|
||||||
importsMap.set('react', new Map(Object.entries(React)));
|
importsMap.set('react', new Map(Object.entries(React)));
|
||||||
importsMap.set('react/jsx-runtime', new Map(Object.entries(ReactJSXRuntime)));
|
importsMap.set('react/jsx-runtime', new Map(Object.entries(ReactJSXRuntime)));
|
||||||
@@ -39,27 +45,61 @@ const importsMap = new Map<string, Map<string, any>>();
|
|||||||
setupImportsMap();
|
setupImportsMap();
|
||||||
export { importsMap };
|
export { importsMap };
|
||||||
|
|
||||||
export const createGlobalThis = () => {
|
const abortController = new AbortController();
|
||||||
return {
|
|
||||||
|
const pluginFetch = createFetch({});
|
||||||
|
const timer = createTimers(abortController.signal);
|
||||||
|
|
||||||
|
const sharedGlobalThis = Object.assign(Object.create(null), timer, {
|
||||||
|
fetch: pluginFetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createGlobalThis = (name: string) => {
|
||||||
|
return Object.assign(Object.create(null), sharedGlobalThis, {
|
||||||
process: Object.freeze({
|
process: Object.freeze({
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// UNSAFE: React will read `window` and `document`
|
// UNSAFE: React will read `window` and `document`
|
||||||
window,
|
window: new Proxy(
|
||||||
document,
|
{},
|
||||||
navigator,
|
{
|
||||||
userAgent: navigator.userAgent,
|
get(_, key) {
|
||||||
// todo(himself65): permission control
|
logger.debug(`${name} is accessing window`, key);
|
||||||
fetch: function (input: RequestInfo, init?: RequestInit) {
|
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
|
||||||
return globalThis.fetch(input, init);
|
const result = Reflect.get(window, key);
|
||||||
},
|
if (typeof result === 'function') {
|
||||||
setTimeout: function (callback: () => void, timeout: number) {
|
return function (...args: any[]) {
|
||||||
return globalThis.setTimeout(callback, timeout);
|
logger.debug(`${name} is calling window`, key, args);
|
||||||
},
|
return result.apply(window, args);
|
||||||
clearTimeout: function (id: number) {
|
};
|
||||||
return globalThis.clearTimeout(id);
|
}
|
||||||
|
logger.debug('window', key, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
document: new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(_, key) {
|
||||||
|
logger.debug(`${name} is accessing document`, key);
|
||||||
|
if (sharedGlobalThis[key]) return sharedGlobalThis[key];
|
||||||
|
const result = Reflect.get(document, key);
|
||||||
|
if (typeof result === 'function') {
|
||||||
|
return function (...args: any[]) {
|
||||||
|
logger.debug(`${name} is calling window`, key, args);
|
||||||
|
return result.apply(document, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
logger.debug('document', key, result);
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
navigator: {
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
},
|
},
|
||||||
|
|
||||||
// safe to use for all plugins
|
// safe to use for all plugins
|
||||||
@@ -97,5 +137,5 @@ export const createGlobalThis = () => {
|
|||||||
IDBIndex: globalThis.IDBIndex,
|
IDBIndex: globalThis.IDBIndex,
|
||||||
IDBCursor: globalThis.IDBCursor,
|
IDBCursor: globalThis.IDBCursor,
|
||||||
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
|
IDBVersionChangeEvent: globalThis.IDBVersionChangeEvent,
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ await Promise.all(
|
|||||||
if (!release && process.env.NODE_ENV === 'production') {
|
if (!release && process.env.NODE_ENV === 'production') {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
const pluginCompartment = new Compartment(createGlobalThis());
|
const pluginCompartment = new Compartment(createGlobalThis(pluginName));
|
||||||
const baseURL = url;
|
const baseURL = url;
|
||||||
const entryURL = `${baseURL}/${core}`;
|
const entryURL = `${baseURL}/${core}`;
|
||||||
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
|
rootStore.set(registeredPluginAtom, prev => [...prev, pluginName]);
|
||||||
|
|||||||
Reference in New Issue
Block a user