mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 21:05:19 +00:00
refactor: new project struct (#8199)
packages/frontend/web -> packages/frontend/apps/web packages/frontend/mobile -> packages/frontend/apps/mobile packages/frontend/electron -> packages/frontend/apps/electron
This commit is contained in:
13
packages/frontend/apps/electron/src/preload/bootstrap.ts
Normal file
13
packages/frontend/apps/electron/src/preload/bootstrap.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import '@sentry/electron/preload';
|
||||
|
||||
import { contextBridge } from 'electron';
|
||||
|
||||
import { appInfo, getElectronAPIs } from './electron-api';
|
||||
import { sharedStorage } from './shared-storage';
|
||||
|
||||
const { apis, events } = getElectronAPIs();
|
||||
|
||||
contextBridge.exposeInMainWorld('__appInfo', appInfo);
|
||||
contextBridge.exposeInMainWorld('__apis', apis);
|
||||
contextBridge.exposeInMainWorld('__events', events);
|
||||
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
|
||||
244
packages/frontend/apps/electron/src/preload/electron-api.ts
Normal file
244
packages/frontend/apps/electron/src/preload/electron-api.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
// Please add modules to `external` in `rollupOptions` to avoid wrong bundling.
|
||||
import type { EventBasedChannel } from 'async-call-rpc';
|
||||
import { AsyncCall } from 'async-call-rpc';
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Subject } from 'rxjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
AFFINE_EVENT_CHANNEL_NAME,
|
||||
type ExposedMeta,
|
||||
type HelperToRenderer,
|
||||
type RendererToHelper,
|
||||
} from '../shared/type';
|
||||
|
||||
export function getElectronAPIs() {
|
||||
const mainAPIs = getMainAPIs();
|
||||
const helperAPIs = getHelperAPIs();
|
||||
|
||||
return {
|
||||
apis: {
|
||||
...mainAPIs.apis,
|
||||
...helperAPIs.apis,
|
||||
},
|
||||
events: {
|
||||
...mainAPIs.events,
|
||||
...helperAPIs.events,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type Schema =
|
||||
| 'affine'
|
||||
| 'affine-canary'
|
||||
| 'affine-beta'
|
||||
| 'affine-internal'
|
||||
| 'affine-dev';
|
||||
|
||||
// todo: remove duplicated codes
|
||||
const ReleaseTypeSchema = z.enum(['stable', 'beta', 'canary', 'internal']);
|
||||
const envBuildType = (process.env.BUILD_TYPE || 'canary').trim().toLowerCase();
|
||||
const buildType = ReleaseTypeSchema.parse(envBuildType);
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
let schema =
|
||||
buildType === 'stable' ? 'affine' : (`affine-${envBuildType}` as Schema);
|
||||
schema = isDev ? 'affine-dev' : schema;
|
||||
|
||||
export const appInfo = {
|
||||
electron: true,
|
||||
windowName:
|
||||
process.argv.find(arg => arg.startsWith('--window-name='))?.split('=')[1] ??
|
||||
'unknown',
|
||||
viewId:
|
||||
process.argv.find(arg => arg.startsWith('--view-id='))?.split('=')[1] ??
|
||||
'unknown',
|
||||
schema,
|
||||
};
|
||||
|
||||
function getMainAPIs() {
|
||||
const meta: ExposedMeta = (() => {
|
||||
const val = process.argv
|
||||
.find(arg => arg.startsWith('--main-exposed-meta='))
|
||||
?.split('=')[1];
|
||||
|
||||
return val ? JSON.parse(val) : null;
|
||||
})();
|
||||
|
||||
// main handlers that can be invoked from the renderer process
|
||||
const apis: any = (() => {
|
||||
const { handlers: handlersMeta } = meta;
|
||||
|
||||
const all = handlersMeta.map(([namespace, functionNames]) => {
|
||||
const namespaceApis = functionNames.map(name => {
|
||||
const channel = `${namespace}:${name}`;
|
||||
return [
|
||||
name,
|
||||
(...args: any[]) => {
|
||||
return ipcRenderer.invoke(
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
channel,
|
||||
...args
|
||||
);
|
||||
},
|
||||
];
|
||||
});
|
||||
return [namespace, Object.fromEntries(namespaceApis)];
|
||||
});
|
||||
|
||||
return Object.fromEntries(all);
|
||||
})();
|
||||
|
||||
// main events that can be listened to from the renderer process
|
||||
const events: any = (() => {
|
||||
const { events: eventsMeta } = meta;
|
||||
|
||||
// channel -> callback[]
|
||||
const listenersMap = new Map<string, ((...args: any[]) => void)[]>();
|
||||
|
||||
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, ...args) => {
|
||||
if (typeof channel !== 'string') {
|
||||
console.error('invalid ipc event', channel);
|
||||
return;
|
||||
}
|
||||
const [namespace, name] = channel.split(':');
|
||||
if (!namespace || !name) {
|
||||
console.error('invalid ipc event', channel);
|
||||
return;
|
||||
}
|
||||
const listeners = listenersMap.get(channel) ?? [];
|
||||
listeners.forEach(listener => listener(...args));
|
||||
});
|
||||
|
||||
const all = eventsMeta.map(([namespace, eventNames]) => {
|
||||
const namespaceEvents = eventNames.map(name => {
|
||||
const channel = `${namespace}:${name}`;
|
||||
return [
|
||||
name,
|
||||
(callback: (...args: any[]) => void) => {
|
||||
listenersMap.set(channel, [
|
||||
...(listenersMap.get(channel) ?? []),
|
||||
callback,
|
||||
]);
|
||||
|
||||
return () => {
|
||||
const listeners = listenersMap.get(channel) ?? [];
|
||||
const index = listeners.indexOf(callback);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
},
|
||||
];
|
||||
});
|
||||
return [namespace, Object.fromEntries(namespaceEvents)];
|
||||
});
|
||||
return Object.fromEntries(all);
|
||||
})();
|
||||
|
||||
return { apis, events };
|
||||
}
|
||||
|
||||
const helperPort = new Promise<MessagePort>(resolve =>
|
||||
ipcRenderer.on('helper-connection', e => {
|
||||
console.info('[preload] helper-connection', e);
|
||||
resolve(e.ports[0]);
|
||||
})
|
||||
);
|
||||
|
||||
const createMessagePortChannel = (port: MessagePort): EventBasedChannel => {
|
||||
return {
|
||||
on(listener) {
|
||||
port.onmessage = e => {
|
||||
listener(e.data);
|
||||
};
|
||||
port.start();
|
||||
return () => {
|
||||
port.onmessage = null;
|
||||
port.close();
|
||||
};
|
||||
},
|
||||
send(data) {
|
||||
port.postMessage(data);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
function getHelperAPIs() {
|
||||
const events$ = new Subject<{ channel: string; args: any[] }>();
|
||||
const meta: ExposedMeta | null = (() => {
|
||||
const val = process.argv
|
||||
.find(arg => arg.startsWith('--helper-exposed-meta='))
|
||||
?.split('=')[1];
|
||||
|
||||
return val ? JSON.parse(val) : null;
|
||||
})();
|
||||
|
||||
const rendererToHelperServer: RendererToHelper = {
|
||||
postEvent: (channel, ...args) => {
|
||||
events$.next({ channel, args });
|
||||
},
|
||||
};
|
||||
|
||||
const rpc = AsyncCall<HelperToRenderer>(rendererToHelperServer, {
|
||||
channel: helperPort.then(helperPort =>
|
||||
createMessagePortChannel(helperPort)
|
||||
),
|
||||
log: false,
|
||||
});
|
||||
|
||||
const toHelperHandler = (namespace: string, name: string) => {
|
||||
return rpc[`${namespace}:${name}`];
|
||||
};
|
||||
|
||||
const toHelperEventSubscriber = (namespace: string, name: string) => {
|
||||
return (callback: (...args: any[]) => void) => {
|
||||
const subscription = events$.subscribe(({ channel, args }) => {
|
||||
if (channel === `${namespace}:${name}`) {
|
||||
callback(...args);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const setup = (meta: ExposedMeta) => {
|
||||
const { handlers, events } = meta;
|
||||
|
||||
const helperHandlers = Object.fromEntries(
|
||||
handlers.map(([namespace, functionNames]) => {
|
||||
return [
|
||||
namespace,
|
||||
Object.fromEntries(
|
||||
functionNames.map(name => {
|
||||
return [name, toHelperHandler(namespace, name)];
|
||||
})
|
||||
),
|
||||
];
|
||||
})
|
||||
);
|
||||
|
||||
const helperEvents = Object.fromEntries(
|
||||
events.map(([namespace, eventNames]) => {
|
||||
return [
|
||||
namespace,
|
||||
Object.fromEntries(
|
||||
eventNames.map(name => {
|
||||
return [name, toHelperEventSubscriber(namespace, name)];
|
||||
})
|
||||
),
|
||||
];
|
||||
})
|
||||
);
|
||||
return [helperHandlers, helperEvents];
|
||||
};
|
||||
|
||||
if (meta) {
|
||||
const [apis, events] = setup(meta);
|
||||
return { apis, events };
|
||||
} else {
|
||||
return { apis: {}, events: {} };
|
||||
}
|
||||
}
|
||||
1
packages/frontend/apps/electron/src/preload/index.ts
Normal file
1
packages/frontend/apps/electron/src/preload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
@@ -0,0 +1,96 @@
|
||||
import { MemoryMemento } from '@toeverything/infra';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import {
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
AFFINE_EVENT_CHANNEL_NAME,
|
||||
} from '../shared/type';
|
||||
|
||||
const initialGlobalState = ipcRenderer.sendSync(
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
'sharedStorage:getAllGlobalState'
|
||||
);
|
||||
const initialGlobalCache = ipcRenderer.sendSync(
|
||||
AFFINE_API_CHANNEL_NAME,
|
||||
'sharedStorage:getAllGlobalCache'
|
||||
);
|
||||
|
||||
function invokeWithCatch(key: string, ...args: any[]) {
|
||||
ipcRenderer.invoke(AFFINE_API_CHANNEL_NAME, key, ...args).catch(err => {
|
||||
console.error(`Failed to invoke ${key}`, err);
|
||||
});
|
||||
}
|
||||
|
||||
function createSharedStorageApi(
|
||||
init: Record<string, any>,
|
||||
event: string,
|
||||
api: {
|
||||
del: string;
|
||||
clear: string;
|
||||
set: string;
|
||||
}
|
||||
) {
|
||||
const memory = new MemoryMemento();
|
||||
memory.setAll(init);
|
||||
ipcRenderer.on(AFFINE_EVENT_CHANNEL_NAME, (_event, channel, updates) => {
|
||||
if (channel === `sharedStorage:${event}`) {
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value === undefined) {
|
||||
memory.del(key);
|
||||
} else {
|
||||
memory.set(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
del(key: string) {
|
||||
memory.del(key);
|
||||
invokeWithCatch(`sharedStorage:${api.del}`, key);
|
||||
},
|
||||
clear() {
|
||||
memory.clear();
|
||||
invokeWithCatch(`sharedStorage:${api.clear}`);
|
||||
},
|
||||
get<T>(key: string): T | undefined {
|
||||
return memory.get(key);
|
||||
},
|
||||
keys() {
|
||||
return memory.keys();
|
||||
},
|
||||
set(key: string, value: unknown) {
|
||||
memory.set(key, value);
|
||||
invokeWithCatch(`sharedStorage:${api.set}`, key, value);
|
||||
},
|
||||
watch<T>(key: string, cb: (i: T | undefined) => void): () => void {
|
||||
const subscription = memory.watch(key).subscribe(i => cb(i as T));
|
||||
return () => subscription.unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const globalState = createSharedStorageApi(
|
||||
initialGlobalState,
|
||||
'onGlobalStateChanged',
|
||||
{
|
||||
clear: 'clearGlobalState',
|
||||
del: 'delGlobalState',
|
||||
set: 'setGlobalState',
|
||||
}
|
||||
);
|
||||
|
||||
export const globalCache = createSharedStorageApi(
|
||||
initialGlobalCache,
|
||||
'onGlobalCacheChanged',
|
||||
{
|
||||
clear: 'clearGlobalCache',
|
||||
del: 'delGlobalCache',
|
||||
set: 'setGlobalCache',
|
||||
}
|
||||
);
|
||||
|
||||
export const sharedStorage = {
|
||||
globalState,
|
||||
globalCache,
|
||||
};
|
||||
Reference in New Issue
Block a user