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:
EYHN
2024-09-12 07:42:57 +00:00
parent 7c4eab6cd3
commit cc5a6e6d40
291 changed files with 139 additions and 134 deletions
@@ -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'));
}