mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-26 14:58:55 +08: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:
@@ -0,0 +1,162 @@
|
||||
// credits: migrated from https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/providers/GitHubProvider.ts
|
||||
|
||||
import type { CustomPublishOptions } from 'builder-util-runtime';
|
||||
import { newError } from 'builder-util-runtime';
|
||||
import type {
|
||||
AppUpdater,
|
||||
ResolvedUpdateFileInfo,
|
||||
UpdateFileInfo,
|
||||
UpdateInfo,
|
||||
} from 'electron-updater';
|
||||
import { CancellationToken, Provider } from 'electron-updater';
|
||||
import type { ProviderRuntimeOptions } from 'electron-updater/out/providers/Provider';
|
||||
import {
|
||||
getFileList,
|
||||
parseUpdateInfo,
|
||||
} from 'electron-updater/out/providers/Provider';
|
||||
|
||||
import type { buildType } from '../config';
|
||||
import { isSquirrelBuild } from './utils';
|
||||
|
||||
interface GithubUpdateInfo extends UpdateInfo {
|
||||
tag: string;
|
||||
}
|
||||
|
||||
interface GithubRelease {
|
||||
name: string;
|
||||
tag_name: string;
|
||||
published_at: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface UpdateProviderOptions {
|
||||
feedUrl?: string;
|
||||
channel: typeof buildType;
|
||||
}
|
||||
|
||||
export class AFFiNEUpdateProvider extends Provider<GithubUpdateInfo> {
|
||||
static configFeed(options: UpdateProviderOptions): CustomPublishOptions {
|
||||
return {
|
||||
provider: 'custom',
|
||||
feedUrl: 'https://affine.pro/api/worker/releases',
|
||||
updateProvider: AFFiNEUpdateProvider,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly options: CustomPublishOptions,
|
||||
_updater: AppUpdater,
|
||||
runtimeOptions: ProviderRuntimeOptions
|
||||
) {
|
||||
super(runtimeOptions);
|
||||
}
|
||||
|
||||
get feedUrl(): URL {
|
||||
const url = new URL(this.options.feedUrl);
|
||||
url.searchParams.set('channel', this.options.channel);
|
||||
url.searchParams.set('minimal', 'true');
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
async getLatestVersion(): Promise<GithubUpdateInfo> {
|
||||
const cancellationToken = new CancellationToken();
|
||||
|
||||
const releasesJsonStr = await this.httpRequest(
|
||||
this.feedUrl,
|
||||
{
|
||||
accept: 'application/json',
|
||||
'cache-control': 'no-cache',
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (!releasesJsonStr) {
|
||||
throw new Error(
|
||||
`Failed to get releases from ${this.feedUrl.toString()}, response is empty`
|
||||
);
|
||||
}
|
||||
|
||||
const releases = JSON.parse(releasesJsonStr);
|
||||
|
||||
if (releases.length === 0) {
|
||||
throw new Error(
|
||||
`No published versions in channel ${this.options.channel}`
|
||||
);
|
||||
}
|
||||
|
||||
const latestRelease = releases[0] as GithubRelease;
|
||||
const tag = latestRelease.tag_name;
|
||||
|
||||
const channelFileName = getChannelFilename(this.getDefaultChannelName());
|
||||
const channelFileAsset = latestRelease.assets.find(({ url }) =>
|
||||
url.endsWith(channelFileName)
|
||||
);
|
||||
|
||||
if (!channelFileAsset) {
|
||||
throw newError(
|
||||
`Cannot find ${channelFileName} in the latest release artifacts.`,
|
||||
'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND'
|
||||
);
|
||||
}
|
||||
|
||||
const channelFileUrl = new URL(channelFileAsset.url);
|
||||
const channelFileContent = await this.httpRequest(channelFileUrl);
|
||||
|
||||
const result = parseUpdateInfo(
|
||||
channelFileContent,
|
||||
channelFileName,
|
||||
channelFileUrl
|
||||
);
|
||||
|
||||
const files: UpdateFileInfo[] = [];
|
||||
|
||||
result.files.forEach(file => {
|
||||
const asset = latestRelease.assets.find(({ name }) => name === file.url);
|
||||
if (asset) {
|
||||
file.url = asset.url;
|
||||
}
|
||||
|
||||
// for windows, we need to determine its installer type (nsis or squirrel)
|
||||
if (process.platform === 'win32') {
|
||||
const isSquirrel = isSquirrelBuild();
|
||||
if (isSquirrel && file.url.endsWith('.nsis.exe')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
files.push(file);
|
||||
});
|
||||
|
||||
if (result.releaseName == null) {
|
||||
result.releaseName = latestRelease.name;
|
||||
}
|
||||
|
||||
if (result.releaseNotes == null) {
|
||||
// TODO(@forehalo): add release notes
|
||||
result.releaseNotes = '';
|
||||
}
|
||||
|
||||
return {
|
||||
tag: tag,
|
||||
...result,
|
||||
};
|
||||
}
|
||||
|
||||
resolveFiles(updateInfo: GithubUpdateInfo): Array<ResolvedUpdateFileInfo> {
|
||||
const files = getFileList(updateInfo);
|
||||
|
||||
return files.map(file => ({
|
||||
url: new URL(file.url),
|
||||
info: file,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function getChannelFilename(channel: string): string {
|
||||
return `${channel}.yml`;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { app } from 'electron';
|
||||
import { autoUpdater as defaultAutoUpdater } from 'electron-updater';
|
||||
|
||||
import { buildType } from '../config';
|
||||
import { logger } from '../logger';
|
||||
import { isOfflineModeEnabled } from '../utils';
|
||||
import { AFFiNEUpdateProvider } from './affine-update-provider';
|
||||
import { updaterSubjects } from './event';
|
||||
import { WindowsUpdater } from './windows-updater';
|
||||
|
||||
const mode = process.env.NODE_ENV;
|
||||
const isDev = mode === 'development';
|
||||
|
||||
// skip auto update in dev mode & internal
|
||||
const disabled = buildType === 'internal' || isDev;
|
||||
|
||||
export const autoUpdater =
|
||||
process.platform === 'win32' ? new WindowsUpdater() : defaultAutoUpdater;
|
||||
|
||||
export const quitAndInstall = async () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
};
|
||||
|
||||
let downloading = false;
|
||||
let configured = false;
|
||||
let checkingUpdate = false;
|
||||
|
||||
export type UpdaterConfig = {
|
||||
autoCheckUpdate: boolean;
|
||||
autoDownloadUpdate: boolean;
|
||||
};
|
||||
|
||||
const config: UpdaterConfig = {
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: true,
|
||||
};
|
||||
|
||||
export const getConfig = (): UpdaterConfig => {
|
||||
return { ...config };
|
||||
};
|
||||
|
||||
export const setConfig = (newConfig: Partial<UpdaterConfig> = {}): void => {
|
||||
configured = true;
|
||||
|
||||
Object.assign(config, newConfig);
|
||||
|
||||
logger.info('Updater configured!', config);
|
||||
|
||||
// if config.autoCheckUpdate is true, trigger a check
|
||||
if (config.autoCheckUpdate) {
|
||||
checkForUpdates().catch(err => {
|
||||
logger.error('Error checking for updates', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const checkForUpdates = async () => {
|
||||
if (disabled || checkingUpdate || isOfflineModeEnabled()) {
|
||||
return;
|
||||
}
|
||||
checkingUpdate = true;
|
||||
try {
|
||||
const info = await autoUpdater.checkForUpdates();
|
||||
return info;
|
||||
} finally {
|
||||
checkingUpdate = false;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadUpdate = async () => {
|
||||
if (disabled || downloading) {
|
||||
return;
|
||||
}
|
||||
downloading = true;
|
||||
updaterSubjects.downloadProgress$.next(0);
|
||||
autoUpdater.downloadUpdate().catch(e => {
|
||||
downloading = false;
|
||||
logger.error('Failed to download update', e);
|
||||
});
|
||||
logger.info('Update available, downloading...');
|
||||
return;
|
||||
};
|
||||
|
||||
export const registerUpdater = async () => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allowAutoUpdate = true;
|
||||
|
||||
autoUpdater.logger = logger;
|
||||
autoUpdater.autoDownload = false;
|
||||
autoUpdater.allowPrerelease = buildType !== 'stable';
|
||||
autoUpdater.autoInstallOnAppQuit = false;
|
||||
autoUpdater.autoRunAppAfterInstall = true;
|
||||
|
||||
const feedUrl = AFFiNEUpdateProvider.configFeed({
|
||||
channel: buildType,
|
||||
});
|
||||
|
||||
logger.debug('auto-updater feed config', feedUrl);
|
||||
|
||||
autoUpdater.setFeedURL(feedUrl);
|
||||
|
||||
// register events for checkForUpdates
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
logger.info('Checking for update');
|
||||
});
|
||||
autoUpdater.on('update-available', info => {
|
||||
logger.info('Update available', info);
|
||||
if (config.autoDownloadUpdate && allowAutoUpdate) {
|
||||
downloadUpdate().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
updaterSubjects.updateAvailable$.next({
|
||||
version: info.version,
|
||||
allowAutoUpdate,
|
||||
});
|
||||
});
|
||||
autoUpdater.on('update-not-available', info => {
|
||||
logger.info('Update not available', info);
|
||||
});
|
||||
autoUpdater.on('download-progress', e => {
|
||||
logger.info(`Download progress: ${e.percent}`);
|
||||
updaterSubjects.downloadProgress$.next(e.percent);
|
||||
});
|
||||
autoUpdater.on('update-downloaded', e => {
|
||||
downloading = false;
|
||||
updaterSubjects.updateReady$.next({
|
||||
version: e.version,
|
||||
allowAutoUpdate,
|
||||
});
|
||||
// I guess we can skip it?
|
||||
// updaterSubjects.clientDownloadProgress.next(100);
|
||||
logger.info('Update downloaded, ready to install');
|
||||
});
|
||||
autoUpdater.on('error', e => {
|
||||
logger.error('Error while updating client', e);
|
||||
});
|
||||
autoUpdater.forceDevUpdateConfig = isDev;
|
||||
|
||||
// check update whenever the window is activated
|
||||
let lastCheckTime = 0;
|
||||
app.on('browser-window-focus', () => {
|
||||
(async () => {
|
||||
if (
|
||||
configured &&
|
||||
config.autoCheckUpdate &&
|
||||
lastCheckTime + 1000 * 1800 < Date.now()
|
||||
) {
|
||||
lastCheckTime = Date.now();
|
||||
await checkForUpdates();
|
||||
}
|
||||
})().catch(err => {
|
||||
logger.error('Error checking for updates', err);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
|
||||
import type { MainEventRegister } from '../type';
|
||||
|
||||
export interface UpdateMeta {
|
||||
version: string;
|
||||
allowAutoUpdate: boolean;
|
||||
}
|
||||
|
||||
export const updaterSubjects = {
|
||||
// means it is ready for restart and install the new version
|
||||
updateAvailable$: new Subject<UpdateMeta>(),
|
||||
updateReady$: new Subject<UpdateMeta>(),
|
||||
downloadProgress$: new BehaviorSubject<number>(0),
|
||||
};
|
||||
|
||||
export const updaterEvents = {
|
||||
onUpdateAvailable: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||
const sub = updaterSubjects.updateAvailable$.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onUpdateReady: (fn: (versionMeta: UpdateMeta) => void) => {
|
||||
const sub = updaterSubjects.updateReady$.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
onDownloadProgress: (fn: (progress: number) => void) => {
|
||||
const sub = updaterSubjects.downloadProgress$.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
};
|
||||
},
|
||||
} satisfies Record<string, MainEventRegister>;
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { IpcMainInvokeEvent } from 'electron';
|
||||
import { app } from 'electron';
|
||||
|
||||
import type { NamespaceHandlers } from '../type';
|
||||
import type { UpdaterConfig } from './electron-updater';
|
||||
import {
|
||||
checkForUpdates,
|
||||
downloadUpdate,
|
||||
getConfig,
|
||||
quitAndInstall,
|
||||
setConfig,
|
||||
} from './electron-updater';
|
||||
|
||||
export const updaterHandlers = {
|
||||
currentVersion: async () => {
|
||||
return app.getVersion();
|
||||
},
|
||||
quitAndInstall: async () => {
|
||||
return quitAndInstall();
|
||||
},
|
||||
downloadUpdate: async () => {
|
||||
return downloadUpdate();
|
||||
},
|
||||
getConfig: async (): Promise<UpdaterConfig> => {
|
||||
return getConfig();
|
||||
},
|
||||
setConfig: async (
|
||||
_e: IpcMainInvokeEvent,
|
||||
newConfig: Partial<UpdaterConfig>
|
||||
): Promise<void> => {
|
||||
return setConfig(newConfig);
|
||||
},
|
||||
checkForUpdates: async () => {
|
||||
const res = await checkForUpdates();
|
||||
if (res) {
|
||||
const { updateInfo } = res;
|
||||
return {
|
||||
version: updateInfo.version,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
} satisfies NamespaceHandlers;
|
||||
|
||||
export * from './electron-updater';
|
||||
@@ -0,0 +1,18 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { app } from 'electron';
|
||||
|
||||
let _isSquirrelBuild: boolean | null = null;
|
||||
export function isSquirrelBuild() {
|
||||
if (typeof _isSquirrelBuild === 'boolean') {
|
||||
return _isSquirrelBuild;
|
||||
}
|
||||
|
||||
// if it is squirrel build, there will be 'squirrel.exe'
|
||||
// otherwise it is in nsis web mode
|
||||
const files = fs.readdirSync(path.dirname(app.getPath('exe')));
|
||||
_isSquirrelBuild = files.some(it => it.includes('squirrel.exe'));
|
||||
|
||||
return _isSquirrelBuild;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { app } from 'electron';
|
||||
import { NsisUpdater } from 'electron-updater';
|
||||
import { DownloadedUpdateHelper } from 'electron-updater/out/DownloadedUpdateHelper';
|
||||
|
||||
export class WindowsUpdater extends NsisUpdater {
|
||||
protected override downloadedUpdateHelper: DownloadedUpdateHelper =
|
||||
new DownloadedUpdateHelper(app.getPath('sessionData'));
|
||||
}
|
||||
Reference in New Issue
Block a user