feat(infra): electron app can hot update renderer layer (#1209)
* ci Signed-off-by: Innei <i@innei.in> * feat: hotupdate renderer impl * fix: skip dev Signed-off-by: Innei <i@innei.in> * update logci Signed-off-by: Innei <i@innei.in> * update logci Signed-off-by: Innei <i@innei.in> * feat: tracker Signed-off-by: Innei <tukon479@gmail.com> * log Signed-off-by: Innei <tukon479@gmail.com> * fix: hash Signed-off-by: Innei <tukon479@gmail.com> * fix: build Signed-off-by: Innei <tukon479@gmail.com> * fix: deps Signed-off-by: Innei <tukon479@gmail.com> * Update nightly.yml * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update minimunm Signed-off-by: Innei <tukon479@gmail.com> * calc main hash Signed-off-by: Innei <tukon479@gmail.com> * add main Hash logic Signed-off-by: Innei <tukon479@gmail.com> * remove Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> * log Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * test Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix: update Signed-off-by: Innei <tukon479@gmail.com> * debug Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix: only one Signed-off-by: Innei <tukon479@gmail.com> * try fix cookie Signed-off-by: Innei <tukon479@gmail.com> * update Signed-off-by: Innei <tukon479@gmail.com> * fix cookie Signed-off-by: Innei <tukon479@gmail.com> * fix: domain Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> * fix: remove test code Signed-off-by: Innei <tukon479@gmail.com> * fix Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <i@innei.in> Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
59709f0200
commit
ca4751acd2
|
|
@ -0,0 +1,75 @@
|
|||
name: Build Electron Render
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
VITE_WEB_URL: ${{ vars.VITE_WEB_URL }}
|
||||
VITE_API_URL: ${{ vars.VITE_API_URL }}
|
||||
VITE_IMGPROXY_URL: ${{ vars.VITE_IMGPROXY_URL }}
|
||||
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
|
||||
VITE_OPENPANEL_CLIENT_ID: ${{ vars.VITE_OPENPANEL_CLIENT_ID }}
|
||||
VITE_OPENPANEL_API_URL: ${{ vars.VITE_OPENPANEL_API_URL }}
|
||||
VITE_FIREBASE_CONFIG: ${{ vars.VITE_FIREBASE_CONFIG }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
jobs:
|
||||
build-render:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
attestations: write
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: true
|
||||
|
||||
- name: Cache pnpm modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.pnpm-store
|
||||
key: ${{ matrix.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ matrix.os }}-
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
- name: Build
|
||||
run: pnpm build:render
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
|
||||
- name: Setup Version
|
||||
id: version
|
||||
uses: ./.github/actions/setup-version
|
||||
|
||||
- name: Create Release Draft
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: v${{ steps.version.outputs.APP_VERSION }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
tag_name: v${{ steps.version.outputs.APP_VERSION }}
|
||||
files: |
|
||||
dist/manifest.yml
|
||||
dist/*.tar.gz
|
||||
|
|
@ -103,7 +103,8 @@ jobs:
|
|||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
- name: Update main hash
|
||||
run: pnpm update:main-hash
|
||||
- name: Build
|
||||
if: matrix.os != 'macos-latest'
|
||||
run: npm exec turbo run //#build
|
||||
|
|
|
|||
|
|
@ -72,25 +72,20 @@ jobs:
|
|||
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
|
||||
PP_PATH=$RUNNER_TEMP/build_pp.provisionprofile
|
||||
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
|
||||
|
||||
# import certificate and provisioning profile from secrets
|
||||
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
|
||||
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
|
||||
|
||||
# create temporary keychain
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
|
||||
# import certificate to keychain
|
||||
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
|
||||
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
|
||||
security list-keychain -d user -s $KEYCHAIN_PATH
|
||||
|
||||
# apply provisioning profile
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm i
|
||||
|
||||
|
|
@ -115,6 +110,9 @@ jobs:
|
|||
fi
|
||||
echo "Updated version to $NIGHTLY_VERSION"
|
||||
|
||||
- name: Update main hash
|
||||
run: pnpm update:main-hash
|
||||
|
||||
- name: Build
|
||||
if: matrix.os != 'macos-latest'
|
||||
run: pnpm build
|
||||
|
|
@ -130,6 +128,10 @@ jobs:
|
|||
KEYCHAIN_PATH: ${{ runner.temp }}/app-signing.keychain-db
|
||||
run: pnpm build:macos
|
||||
|
||||
- name: Build (Render)
|
||||
run: pnpm build:render
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
|
@ -140,6 +142,9 @@ jobs:
|
|||
out/make/**/*.AppImage
|
||||
out/make/**/*.yml
|
||||
out/make/**/*.dmg
|
||||
dist/manifest.yml
|
||||
dist/*.tar.gz
|
||||
|
||||
retention-days: 7
|
||||
|
||||
- name: Generate artifact attestation
|
||||
|
|
@ -152,6 +157,8 @@ jobs:
|
|||
out/make/**/*.exe
|
||||
out/make/**/*.AppImage
|
||||
out/make/**/*.yml
|
||||
dist/manifest.yml
|
||||
dist/*.tar.gz
|
||||
|
||||
- name: Create Nightly Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
|
|
@ -159,16 +166,19 @@ jobs:
|
|||
name: Nightly ${{ env.NIGHTLY_VERSION }}
|
||||
draft: false
|
||||
prerelease: true
|
||||
tag_name: nightly-${{ env.NIGHTLY_VERSION }}
|
||||
tag_name: ${{ env.NIGHTLY_VERSION }}
|
||||
files: |
|
||||
out/make/**/*.dmg
|
||||
out/make/**/*.zip
|
||||
out/make/**/*.exe
|
||||
out/make/**/*.AppImage
|
||||
out/make/**/*.yml
|
||||
dist/manifest.yml
|
||||
dist/*.tar.gz
|
||||
|
||||
body: |
|
||||
This is an automated nightly release for testing purposes.
|
||||
Version: 0.0.0-nightly.${{ env.NIGHTLY_VERSION }}
|
||||
Version: ${{ env.NIGHTLY_VERSION }}
|
||||
|
||||
**Warning:** This build may be unstable and is not recommended for production use.
|
||||
env:
|
||||
|
|
|
|||
|
|
@ -1,2 +1,7 @@
|
|||
import "../../types/vite"
|
||||
import "../../types/authjs"
|
||||
|
||||
declare global {
|
||||
const GIT_COMMIT_HASH: string
|
||||
}
|
||||
export {}
|
||||
|
|
|
|||
|
|
@ -28,23 +28,29 @@
|
|||
"@eneris/push-receiver": "4.3.0",
|
||||
"@follow/shared": "workspace:*",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@openpanel/web": "1.0.1",
|
||||
"@sentry/electron": "5.7.0",
|
||||
"builder-util-runtime": "9.2.10",
|
||||
"electron-context-menu": "4.0.4",
|
||||
"electron-log": "5.2.2",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
"electron-updater": "^6.3.9",
|
||||
"es-toolkit": "1.26.1",
|
||||
"fast-folder-size": "2.3.0",
|
||||
"font-list": "1.5.1",
|
||||
"i18next": "^24.0.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"linkedom": "^0.18.5",
|
||||
"lowdb": "7.0.1",
|
||||
"msedge-tts": "1.3.4",
|
||||
"node-machine-id": "1.1.12",
|
||||
"ofetch": "1.4.1",
|
||||
"semver": "7.6.3",
|
||||
"tar": "7.4.3",
|
||||
"vscode-languagedetection": "npm:@vscode/vscode-languagedetection@^1.0.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "^22.9.3",
|
||||
"electron": "33.2.0",
|
||||
"electron-devtools-installer": "3.2.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
// 5min
|
||||
import path from "node:path"
|
||||
|
||||
import { app } from "electron"
|
||||
|
||||
export const UNREAD_BACKGROUND_POLLING_INTERVAL = 1000 * 60 * 5
|
||||
|
||||
export const HOTUPDATE_RENDER_ENTRY_DIR = path.resolve(app.getPath("userData"), "render")
|
||||
|
||||
export const GITHUB_OWNER = process.env.GITHUB_OWNER || "RSSNext"
|
||||
export const GITHUB_REPO = process.env.GITHUB_REPO || "follow"
|
||||
|
||||
// https://github.com/electron/electron/issues/25081
|
||||
export const START_IN_TRAY_ARGS = "--start-in-tray"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
import { machineIdSync } from "node-machine-id"
|
||||
|
||||
export const DEVICE_ID = machineIdSync()
|
||||
|
|
@ -4,8 +4,10 @@ import { APP_PROTOCOL } from "@follow/shared/constants"
|
|||
import { env } from "@follow/shared/env"
|
||||
import { imageRefererMatches, selfRefererMatches } from "@follow/shared/image"
|
||||
import { app, BrowserWindow, session } from "electron"
|
||||
import type { Cookie } from "electron/main"
|
||||
import squirrelStartup from "electron-squirrel-startup"
|
||||
|
||||
import { DEVICE_ID } from "./constants/system"
|
||||
import { isDev, isMacOS } from "./env"
|
||||
import { initializeAppStage0, initializeAppStage1 } from "./init"
|
||||
import { updateProxy } from "./lib/proxy"
|
||||
|
|
@ -14,6 +16,7 @@ import { store } from "./lib/store"
|
|||
import { registerAppTray } from "./lib/tray"
|
||||
import { setAuthSessionToken, updateNotificationsToken } from "./lib/user"
|
||||
import { registerUpdater } from "./updater"
|
||||
import { cleanupOldRender } from "./updater/hot-updater"
|
||||
import {
|
||||
createMainWindow,
|
||||
getMainWindow,
|
||||
|
|
@ -23,6 +26,9 @@ import {
|
|||
|
||||
if (isDev) console.info("[main] env loaded:", env)
|
||||
|
||||
const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
|
||||
|
||||
console.info("[main] device id:", DEVICE_ID)
|
||||
if (squirrelStartup) {
|
||||
app.quit()
|
||||
}
|
||||
|
|
@ -56,7 +62,7 @@ function bootstrap() {
|
|||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(() => {
|
||||
app.whenReady().then(async () => {
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
|
|
@ -69,6 +75,28 @@ function bootstrap() {
|
|||
|
||||
mainWindow = createMainWindow()
|
||||
|
||||
// restore cookies
|
||||
const cookies = store.get("cookies") as Cookie[]
|
||||
if (cookies) {
|
||||
Promise.all(
|
||||
cookies.map((cookie) => {
|
||||
const setCookieDetails: Electron.CookiesSetDetails = {
|
||||
url: apiURL,
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
domain: cookie.domain,
|
||||
path: cookie.path,
|
||||
secure: cookie.secure,
|
||||
httpOnly: cookie.httpOnly,
|
||||
expirationDate: cookie.expirationDate,
|
||||
sameSite: cookie.sameSite as "unspecified" | "no_restriction" | "lax" | "strict",
|
||||
}
|
||||
|
||||
return mainWindow.webContents.session.cookies.set(setCookieDetails)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
updateProxy()
|
||||
registerUpdater()
|
||||
registerAppTray()
|
||||
|
|
@ -135,7 +163,7 @@ function bootstrap() {
|
|||
}
|
||||
})
|
||||
|
||||
app.on("before-quit", () => {
|
||||
app.on("before-quit", async () => {
|
||||
// store window pos when before app quit
|
||||
const window = getMainWindow()
|
||||
if (!window || window.isDestroyed()) return
|
||||
|
|
@ -147,6 +175,12 @@ function bootstrap() {
|
|||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
})
|
||||
await session.defaultSession.cookies.flushStore()
|
||||
|
||||
const cookies = await session.defaultSession.cookies.get({})
|
||||
store.set("cookies", cookies)
|
||||
|
||||
await cleanupOldRender()
|
||||
})
|
||||
|
||||
const handleOpen = async (url: string) => {
|
||||
|
|
@ -158,7 +192,6 @@ function bootstrap() {
|
|||
const token = urlObj.searchParams.get("token")
|
||||
const userId = urlObj.searchParams.get("userId")
|
||||
|
||||
const apiURL = process.env["VITE_API_URL"] || import.meta.env.VITE_API_URL
|
||||
if (token && apiURL) {
|
||||
setAuthSessionToken(token)
|
||||
mainWindow.webContents.session.cookies.set({
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ export const initializeAppStage1 = () => {
|
|||
// code. You can also put them in separate files and require them here.
|
||||
|
||||
registerMenuAndContextMenu()
|
||||
|
||||
registerPushNotifications()
|
||||
clearCacheCronJob()
|
||||
checkAndCleanCodeCache()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
import { env } from "@follow/shared/env"
|
||||
import { OpenPanel } from "@openpanel/web"
|
||||
import { app } from "electron"
|
||||
|
||||
import { DEVICE_ID } from "~/constants/system"
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: env.VITE_OPENPANEL_CLIENT_ID ?? "",
|
||||
trackScreenViews: false,
|
||||
trackOutgoingLinks: false,
|
||||
trackAttributes: false,
|
||||
trackHashChanges: false,
|
||||
apiUrl: env.VITE_OPENPANEL_API_URL,
|
||||
})
|
||||
|
||||
op.setGlobalProperties({
|
||||
device_id: DEVICE_ID,
|
||||
app_version: app.getVersion(),
|
||||
build: "electron",
|
||||
})
|
||||
|
|
@ -1 +1,13 @@
|
|||
import type { BrowserWindow } from "electron"
|
||||
|
||||
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
// To solve the vibrancy losing issue when leaving full screen mode
|
||||
// @see https://github.com/toeverything/AFFiNE/blob/280e24934a27557529479a70ab38c4f5fc65cb00/packages/frontend/electron/src/main/windows-manager/main-window.ts:L157
|
||||
export function refreshBound(window: BrowserWindow, timeout = 0) {
|
||||
setTimeout(() => {
|
||||
// FIXME: workaround for theme bug in full screen mode
|
||||
const size = window?.getSize()
|
||||
window?.setSize(size[0] + 1, size[1] + 1)
|
||||
window?.setSize(size[0], size[1])
|
||||
}, timeout)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { isDev, isMacOS } from "./env"
|
|||
import { clearAllDataAndConfirm } from "./lib/cleaner"
|
||||
import { t } from "./lib/i18n"
|
||||
import { revealLogFile } from "./logger"
|
||||
import { checkForUpdates, quitAndInstall } from "./updater"
|
||||
import { checkForAppUpdates, quitAndInstall } from "./updater"
|
||||
import { createSettingWindow, createWindow, getMainWindow } from "./window"
|
||||
|
||||
export const registerAppMenu = () => {
|
||||
|
|
@ -184,7 +184,7 @@ export const registerAppMenu = () => {
|
|||
label: t("menu.checkForUpdates"),
|
||||
click: async () => {
|
||||
getMainWindow()?.show()
|
||||
await checkForUpdates()
|
||||
await checkForAppUpdates()
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import * as Sentry from "@sentry/electron/main"
|
||||
import { app } from "electron"
|
||||
import { FetchError } from "ofetch"
|
||||
|
||||
import { DEVICE_ID } from "./constants/system"
|
||||
|
||||
export const initializeSentry = () => {
|
||||
Sentry.init({
|
||||
dsn: process.env.VITE_SENTRY_DSN,
|
||||
|
|
@ -32,4 +35,7 @@ export const initializeSentry = () => {
|
|||
return event
|
||||
},
|
||||
})
|
||||
Sentry.setTag("device_id", DEVICE_ID)
|
||||
Sentry.setTag("app_version", app.getVersion())
|
||||
Sentry.setTag("build", "electron")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,26 @@
|
|||
/* eslint-disable @typescript-eslint/require-await */
|
||||
import fs from "node:fs"
|
||||
import fsp from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import { getRendererHandlers } from "@egoist/tipc/main"
|
||||
import { callWindowExpose } from "@follow/shared/bridge"
|
||||
import type { BrowserWindow } from "electron"
|
||||
import { app, clipboard, dialog, screen, shell } from "electron"
|
||||
import pkg from "@pkg"
|
||||
import { app, BrowserWindow, clipboard, dialog, screen, shell } from "electron"
|
||||
|
||||
import { registerMenuAndContextMenu } from "~/init"
|
||||
import { clearAllData, getCacheSize } from "~/lib/cleaner"
|
||||
import { store, StoreKey } from "~/lib/store"
|
||||
import { registerAppTray } from "~/lib/tray"
|
||||
import { logger } from "~/logger"
|
||||
import {
|
||||
cleanupOldRender,
|
||||
getCurrentRenderManifest,
|
||||
loadDynamicRenderEntry,
|
||||
} from "~/updater/hot-updater"
|
||||
|
||||
import { isWindows11 } from "../env"
|
||||
import { isDev, isWindows11 } from "../env"
|
||||
import { downloadFile } from "../lib/download"
|
||||
import { i18n } from "../lib/i18n"
|
||||
import { cleanAuthSessionToken, cleanUser } from "../lib/user"
|
||||
|
|
@ -272,6 +279,34 @@ ${content}
|
|||
}
|
||||
}),
|
||||
|
||||
getRenderVersion: t.procedure.action(async () => {
|
||||
const manifest = getCurrentRenderManifest()
|
||||
return manifest?.version || pkg.version
|
||||
}),
|
||||
rendererUpdateReload: t.procedure.action(async () => {
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url))
|
||||
const allWindows = BrowserWindow.getAllWindows()
|
||||
const dynamicRenderEntry = loadDynamicRenderEntry()
|
||||
|
||||
const appLoadEntry = dynamicRenderEntry || path.resolve(__dirname, "../../renderer/index.html")
|
||||
logger.info("appLoadEntry", appLoadEntry)
|
||||
const mainWindow = getMainWindow()
|
||||
|
||||
for (const window of allWindows) {
|
||||
if (window === mainWindow) {
|
||||
if (isDev) {
|
||||
logger.verbose("[rendererUpdateReload]: skip reload in dev")
|
||||
break
|
||||
}
|
||||
window.loadFile(appLoadEntry)
|
||||
} else window.destroy()
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
cleanupOldRender()
|
||||
}, 1000)
|
||||
}),
|
||||
|
||||
getCacheSize: t.procedure.action(async () => {
|
||||
return getCacheSize()
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import { op } from "~/lib/op"
|
||||
|
||||
const PREFIX = "app:"
|
||||
export const hotUpdateDownloadTrack = (version: string) => {
|
||||
op.track(`${PREFIX}hot-update-download`, { version })
|
||||
}
|
||||
export const hotUpdateAppNotSupportTriggerTrack = (data: {
|
||||
appVersion: string
|
||||
manifestVersion: string
|
||||
}) => {
|
||||
op.track(`${PREFIX}hot-update-app-not-support-trigger`, data)
|
||||
}
|
||||
|
||||
export const hotUpdateRenderSuccessTrack = (version: string) => {
|
||||
op.track(`${PREFIX}hot-update-render-success`, { version })
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { version as appVersion } from "@pkg"
|
||||
|
||||
import { isDev } from "~/env"
|
||||
|
||||
const isNightlyBuild = appVersion.includes("nightly")
|
||||
|
||||
export const appUpdaterConfig = {
|
||||
// Disable renderer hot update will trigger app update when available
|
||||
enableRenderHotUpdate: !isDev && isNightlyBuild,
|
||||
// Disable app update will also disable renderer hot update
|
||||
// enableAppUpdate: true,
|
||||
enableAppUpdate: !isDev,
|
||||
|
||||
app: {
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: true,
|
||||
checkUpdateInterval: 15 * 60 * 1000,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* @description This file handles hot updates for the electron renderer layer
|
||||
*/
|
||||
import { createHash } from "node:crypto"
|
||||
import { existsSync, readFileSync } from "node:fs"
|
||||
import { mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
import { callWindowExpose } from "@follow/shared/bridge"
|
||||
import { mainHash, version as appVersion } from "@pkg"
|
||||
import log from "electron-log"
|
||||
import { load } from "js-yaml"
|
||||
import { x } from "tar"
|
||||
|
||||
import { GITHUB_OWNER, GITHUB_REPO, HOTUPDATE_RENDER_ENTRY_DIR } from "~/constants/app"
|
||||
import { hotUpdateDownloadTrack, hotUpdateRenderSuccessTrack } from "~/tracker"
|
||||
import { getMainWindow } from "~/window"
|
||||
|
||||
import { appUpdaterConfig } from "./configs"
|
||||
import type { GitHubReleasesItem } from "./types"
|
||||
|
||||
const logger = log.scope("hot-updater")
|
||||
|
||||
const isNightlyBuild = appVersion.includes("nightly")
|
||||
|
||||
const url = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}`
|
||||
const releasesUrl = `${url}/releases`
|
||||
const releaseApiUrl = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases`
|
||||
|
||||
const getLatestReleaseTag = async () => {
|
||||
if (!isNightlyBuild) {
|
||||
const res = await fetch(`${releaseApiUrl}/latest`)
|
||||
const json = await res.json()
|
||||
|
||||
return json.tag_name
|
||||
} else {
|
||||
const res = await fetch(releaseApiUrl)
|
||||
const json = (await res.json()) as GitHubReleasesItem[]
|
||||
|
||||
// Search the top nightly release
|
||||
const nightlyRelease = json.find((item) => item.prerelease)
|
||||
if (!nightlyRelease) return json[0].tag_name
|
||||
return nightlyRelease.tag_name
|
||||
}
|
||||
}
|
||||
|
||||
const getFileDownloadUrl = async (filename: string) => {
|
||||
const tag = await getLatestReleaseTag()
|
||||
|
||||
return `${releasesUrl}/download/${tag}/${filename}`
|
||||
}
|
||||
|
||||
type Manifest = {
|
||||
/** Render version */
|
||||
version: string
|
||||
hash: string
|
||||
commit: string
|
||||
filename: string
|
||||
/** Only electron main hash equal to this value, renderer will can be updated */
|
||||
mainHash: string
|
||||
}
|
||||
const getLatestReleaseManifest = async () => {
|
||||
const url = await getFileDownloadUrl("manifest.yml")
|
||||
logger.info(`Fetching manifest from ${url}`)
|
||||
const res = await fetch(url)
|
||||
const text = await res.text()
|
||||
const manifest = load(text) as Manifest
|
||||
if (typeof manifest !== "object") {
|
||||
logger.error("Invalid manifest", text)
|
||||
return null
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
const downloadTempDir = path.resolve(os.tmpdir(), "follow-render-update")
|
||||
|
||||
export const canUpdateRender = async () => {
|
||||
const manifest = await getLatestReleaseManifest()
|
||||
|
||||
logger.info("fetched manifest", manifest)
|
||||
|
||||
if (!manifest) return false
|
||||
|
||||
// const isAppShouldUpdate = shouldUpdateApp(appVersion, manifest.version)
|
||||
// if (isAppShouldUpdate) {
|
||||
// logger.info(
|
||||
// "app should update, skip render update, app version: ",
|
||||
// appVersion,
|
||||
// ", the manifest version: ",
|
||||
// manifest.version,
|
||||
// )
|
||||
// return false
|
||||
// }
|
||||
|
||||
const appSupport = mainHash === manifest.mainHash
|
||||
if (!appSupport) {
|
||||
logger.info("app not support, should trigger app force update, app version: ", appVersion)
|
||||
// hotUpdateAppNotSupportTriggerTrack({
|
||||
// appVersion,
|
||||
// manifestVersion: manifest.version,
|
||||
// })
|
||||
// // Trigger app force update
|
||||
// checkForAppUpdates().then(() => {
|
||||
// downloadAppUpdate()
|
||||
// })
|
||||
return false
|
||||
}
|
||||
|
||||
const isVersionEqual = appVersion === manifest.version
|
||||
if (isVersionEqual) {
|
||||
logger.info("version is equal, skip update")
|
||||
return false
|
||||
}
|
||||
const isCommitEqual = GIT_COMMIT_HASH === manifest.commit
|
||||
if (isCommitEqual) {
|
||||
logger.info("commit is equal, skip update")
|
||||
return false
|
||||
}
|
||||
|
||||
const manifestFilePath = path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "manifest.yml")
|
||||
const manifestExist = existsSync(manifestFilePath)
|
||||
|
||||
const oldManifest: Manifest | null = manifestExist
|
||||
? (load(readFileSync(manifestFilePath, "utf-8")) as Manifest)
|
||||
: null
|
||||
|
||||
if (oldManifest) {
|
||||
if (oldManifest.version === manifest.version) {
|
||||
logger.info("manifest version is equal, skip update")
|
||||
return false
|
||||
}
|
||||
if (oldManifest.commit === manifest.commit) {
|
||||
logger.info("manifest commit is equal, skip update")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return manifest
|
||||
}
|
||||
const downloadRenderAsset = async (manifest: Manifest) => {
|
||||
hotUpdateDownloadTrack(manifest.version)
|
||||
const { filename } = manifest
|
||||
const url = await getFileDownloadUrl(filename)
|
||||
|
||||
logger.info(`Downloading ${url}`)
|
||||
const res = await fetch(url)
|
||||
const arrayBuffer = await res.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
const filePath = path.resolve(downloadTempDir, filename)
|
||||
await mkdir(downloadTempDir, { recursive: true })
|
||||
await writeFile(filePath, buffer)
|
||||
|
||||
const sha256 = createHash("sha256")
|
||||
sha256.update(buffer)
|
||||
const hash = sha256.digest("hex")
|
||||
if (hash !== manifest.hash) {
|
||||
logger.error("Hash mismatch", hash, manifest.hash)
|
||||
return false
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
export const hotUpdateRender = async (manifest: Manifest) => {
|
||||
if (!appUpdaterConfig.enableRenderHotUpdate) return false
|
||||
|
||||
if (!manifest) return false
|
||||
|
||||
const filePath = await downloadRenderAsset(manifest)
|
||||
if (!filePath) return false
|
||||
|
||||
// Extract the tar.gz file
|
||||
await mkdir(HOTUPDATE_RENDER_ENTRY_DIR, { recursive: true })
|
||||
await x({
|
||||
f: filePath,
|
||||
cwd: HOTUPDATE_RENDER_ENTRY_DIR,
|
||||
})
|
||||
|
||||
// Rename `renderer` folder to `manifest.version`
|
||||
await rename(
|
||||
path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "renderer"),
|
||||
path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, manifest.version),
|
||||
)
|
||||
|
||||
await writeFile(
|
||||
path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "manifest.yml"),
|
||||
JSON.stringify(manifest),
|
||||
)
|
||||
logger.info(`Hot update render success, update to ${manifest.version}`)
|
||||
hotUpdateRenderSuccessTrack(manifest.version)
|
||||
const mainWindow = getMainWindow()
|
||||
if (!mainWindow) return false
|
||||
const caller = callWindowExpose(mainWindow)
|
||||
caller.readyToUpdate()
|
||||
return true
|
||||
}
|
||||
|
||||
export const getCurrentRenderManifest = () => {
|
||||
const manifestFilePath = path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, "manifest.yml")
|
||||
const manifestExist = existsSync(manifestFilePath)
|
||||
if (!manifestExist) return null
|
||||
return load(readFileSync(manifestFilePath, "utf-8")) as Manifest
|
||||
}
|
||||
export const cleanupOldRender = async () => {
|
||||
const manifest = getCurrentRenderManifest()
|
||||
if (!manifest) {
|
||||
// Empty the directory
|
||||
await rm(HOTUPDATE_RENDER_ENTRY_DIR, { recursive: true, force: true })
|
||||
return
|
||||
}
|
||||
|
||||
const currentRenderVersion = manifest.version
|
||||
// Clean all not current version
|
||||
const dirs = await readdir(HOTUPDATE_RENDER_ENTRY_DIR)
|
||||
for (const dir of dirs) {
|
||||
const isDir = (await stat(path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, dir))).isDirectory()
|
||||
if (!isDir) continue
|
||||
if (dir === currentRenderVersion) continue
|
||||
await rm(path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, dir), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
export const loadDynamicRenderEntry = () => {
|
||||
const manifest = getCurrentRenderManifest()
|
||||
if (!manifest) return
|
||||
const currentRenderVersion = manifest.version
|
||||
const dir = path.resolve(HOTUPDATE_RENDER_ENTRY_DIR, currentRenderVersion)
|
||||
const entryFile = path.resolve(dir, "index.html")
|
||||
const entryFileExists = existsSync(entryFile)
|
||||
if (!entryFileExists) return
|
||||
return entryFile
|
||||
}
|
||||
|
|
@ -1,15 +1,20 @@
|
|||
import { getRendererHandlers } from "@egoist/tipc/main"
|
||||
import { autoUpdater as defaultAutoUpdater } from "electron-updater"
|
||||
|
||||
import { GITHUB_OWNER, GITHUB_REPO } from "~/constants/app"
|
||||
import { canUpdateRender, hotUpdateRender } from "~/updater/hot-updater"
|
||||
|
||||
import { channel, isDev, isWindows } from "../env"
|
||||
import { logger } from "../logger"
|
||||
import type { RendererHandlers } from "../renderer-handlers"
|
||||
import { destroyMainWindow, getMainWindow } from "../window"
|
||||
import { appUpdaterConfig } from "./configs"
|
||||
import { CustomGitHubProvider } from "./custom-github-provider"
|
||||
import { WindowsUpdater } from "./windows-updater"
|
||||
|
||||
// skip auto update in dev mode
|
||||
const disabled = isDev
|
||||
// const disabled = isDev
|
||||
const disabled = !appUpdaterConfig.enableAppUpdate
|
||||
|
||||
const autoUpdater = isWindows ? new WindowsUpdater() : defaultAutoUpdater
|
||||
|
||||
|
|
@ -28,32 +33,29 @@ export const quitAndInstall = () => {
|
|||
let downloading = false
|
||||
let checkingUpdate = false
|
||||
|
||||
export type UpdaterConfig = {
|
||||
autoCheckUpdate: boolean
|
||||
autoDownloadUpdate: boolean
|
||||
checkUpdateInterval: number
|
||||
}
|
||||
|
||||
const config: UpdaterConfig = {
|
||||
autoCheckUpdate: true,
|
||||
autoDownloadUpdate: true,
|
||||
checkUpdateInterval: 15 * 60 * 1000,
|
||||
}
|
||||
|
||||
export const checkForUpdates = async () => {
|
||||
export const checkForAppUpdates = async () => {
|
||||
if (disabled || checkingUpdate) {
|
||||
return
|
||||
}
|
||||
|
||||
checkingUpdate = true
|
||||
try {
|
||||
const info = await autoUpdater.checkForUpdates()
|
||||
return info
|
||||
if (appUpdaterConfig.enableRenderHotUpdate) {
|
||||
const manifest = await canUpdateRender()
|
||||
if (manifest) {
|
||||
await hotUpdateRender(manifest)
|
||||
return
|
||||
}
|
||||
}
|
||||
return autoUpdater.checkForUpdates()
|
||||
} catch (e) {
|
||||
logger.error("Error checking for updates", e)
|
||||
} finally {
|
||||
checkingUpdate = false
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadUpdate = async () => {
|
||||
export const downloadAppUpdate = async () => {
|
||||
if (disabled || downloading) {
|
||||
return
|
||||
}
|
||||
|
|
@ -71,8 +73,7 @@ export const registerUpdater = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
const allowAutoUpdate = true
|
||||
|
||||
// Disable there, control this in event
|
||||
autoUpdater.autoDownload = false
|
||||
autoUpdater.allowPrerelease = channel !== "stable"
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
|
|
@ -82,8 +83,8 @@ export const registerUpdater = async () => {
|
|||
channel,
|
||||
// hack for custom provider
|
||||
provider: "custom" as "github",
|
||||
repo: "follow",
|
||||
owner: "RSSNext",
|
||||
repo: GITHUB_REPO,
|
||||
owner: GITHUB_OWNER,
|
||||
releaseType: channel === "stable" ? "release" : "prerelease",
|
||||
// @ts-expect-error hack for custom provider
|
||||
updateProvider: CustomGitHubProvider,
|
||||
|
|
@ -100,10 +101,22 @@ export const registerUpdater = async () => {
|
|||
autoUpdater.on("checking-for-update", () => {
|
||||
logger.info("Checking for update")
|
||||
})
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
autoUpdater.on("update-available", async (info) => {
|
||||
logger.info("Update available", info)
|
||||
if (config.autoDownloadUpdate && allowAutoUpdate) {
|
||||
downloadUpdate().catch((err) => {
|
||||
|
||||
// The app hotfix strategy is as follows:
|
||||
// Determine whether the app should be updated in full or only the renderer layer based on the version number.
|
||||
// https://www.notion.so/rss3/Follow-Hotfix-Electron-Renderer-layer-RFC-fe2444b9ac194c2cb38f9fa0bb1ef3c1?pvs=4#12e35ea049b480f1b268f1e605d86a62
|
||||
if (appUpdaterConfig.enableRenderHotUpdate) {
|
||||
const manifest = await canUpdateRender()
|
||||
if (manifest) {
|
||||
await hotUpdateRender(manifest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (appUpdaterConfig.app.autoDownloadUpdate) {
|
||||
downloadAppUpdate().catch((err) => {
|
||||
logger.error(err)
|
||||
})
|
||||
}
|
||||
|
|
@ -130,14 +143,14 @@ export const registerUpdater = async () => {
|
|||
autoUpdater.forceDevUpdateConfig = isDev
|
||||
|
||||
setInterval(() => {
|
||||
if (config.autoCheckUpdate) {
|
||||
checkForUpdates().catch((err) => {
|
||||
if (appUpdaterConfig.app.autoCheckUpdate) {
|
||||
checkForAppUpdates().catch((err) => {
|
||||
logger.error("Error checking for updates", err)
|
||||
})
|
||||
}
|
||||
}, config.checkUpdateInterval)
|
||||
if (config.autoCheckUpdate) {
|
||||
checkForUpdates().catch((err) => {
|
||||
}, appUpdaterConfig.app.checkUpdateInterval)
|
||||
if (appUpdaterConfig.app.autoCheckUpdate) {
|
||||
checkForAppUpdates().catch((err) => {
|
||||
logger.error("Error checking for updates", err)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
export interface GitHubReleasesItem {
|
||||
url: string
|
||||
assets_url: string
|
||||
upload_url: string
|
||||
html_url: string
|
||||
id: number
|
||||
author: Author
|
||||
node_id: string
|
||||
tag_name: string
|
||||
target_commitish: string
|
||||
name: string
|
||||
draft: boolean
|
||||
prerelease: boolean
|
||||
created_at: string
|
||||
published_at: string
|
||||
assets: AssetsItem[]
|
||||
tarball_url: string
|
||||
zipball_url: string
|
||||
body: string
|
||||
reactions?: Reactions
|
||||
mentions_count?: number
|
||||
}
|
||||
interface Author {
|
||||
login: string
|
||||
id: number
|
||||
node_id: string
|
||||
avatar_url: string
|
||||
gravatar_id: string
|
||||
url: string
|
||||
html_url: string
|
||||
followers_url: string
|
||||
following_url: string
|
||||
gists_url: string
|
||||
starred_url: string
|
||||
subscriptions_url: string
|
||||
organizations_url: string
|
||||
repos_url: string
|
||||
events_url: string
|
||||
received_events_url: string
|
||||
type: string
|
||||
user_view_type: string
|
||||
site_admin: boolean
|
||||
}
|
||||
interface AssetsItem {
|
||||
url: string
|
||||
id: number
|
||||
node_id: string
|
||||
name: string
|
||||
label: string | null
|
||||
uploader: Uploader
|
||||
content_type: string
|
||||
state: string
|
||||
size: number
|
||||
download_count: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
browser_download_url: string
|
||||
}
|
||||
interface Uploader {
|
||||
login: string
|
||||
id: number
|
||||
node_id: string
|
||||
avatar_url: string
|
||||
gravatar_id: string
|
||||
url: string
|
||||
html_url: string
|
||||
followers_url: string
|
||||
following_url: string
|
||||
gists_url: string
|
||||
starred_url: string
|
||||
subscriptions_url: string
|
||||
organizations_url: string
|
||||
repos_url: string
|
||||
events_url: string
|
||||
received_events_url: string
|
||||
type: string
|
||||
user_view_type: string
|
||||
site_admin: boolean
|
||||
}
|
||||
interface Reactions {
|
||||
url: string
|
||||
total_count: number
|
||||
"+1": number
|
||||
"-1": number
|
||||
laugh: number
|
||||
hooray: number
|
||||
confused: number
|
||||
heart: number
|
||||
rocket: number
|
||||
eyes: number
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import fs from "node:fs"
|
|||
import path from "node:path"
|
||||
|
||||
import { app } from "electron"
|
||||
import { major, minor } from "semver"
|
||||
|
||||
let _isSquirrelBuild: boolean | null = null
|
||||
export function isSquirrelBuild() {
|
||||
|
|
@ -16,3 +17,39 @@ export function isSquirrelBuild() {
|
|||
|
||||
return _isSquirrelBuild
|
||||
}
|
||||
|
||||
// The following scenario only requires updating the renderer, so the app update is skipped:
|
||||
// In x.y.z, the update of z will only trigger renderer hotfix, while the update of y requires updating the entire app.
|
||||
// The hotfix version of x.y.z-beta.0 adds a suffix number. It triggers renderer update. If the main code is modified and the entire app update needs to be triggered, the hotfix version adds a suffix like x.y.z-beta.0.app.
|
||||
// For subsequent minor versions that require updating the main code, the suffix .app needs to be added.
|
||||
|
||||
export const shouldUpdateApp = (currentVersion: string, nextVersion: string) => {
|
||||
if (nextVersion.includes("app")) {
|
||||
return true
|
||||
}
|
||||
// x.y.z 's y or x not equal, need update app
|
||||
const [x1, x2] = [safeMajor(currentVersion), safeMajor(nextVersion)]
|
||||
const [y1, y2] = [safeMinor(currentVersion), safeMinor(nextVersion)]
|
||||
|
||||
// Here, it is not determined whether it is a problem of version number downgrade; the updater will handle it automatically.
|
||||
if (x1 !== x2 || y1 !== y2) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const safeMajor = (version: string) => {
|
||||
try {
|
||||
return major(version)
|
||||
} catch {
|
||||
return "0.0.0"
|
||||
}
|
||||
}
|
||||
const safeMinor = (version: string) => {
|
||||
try {
|
||||
return minor(version)
|
||||
} catch {
|
||||
return "0.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,8 +14,10 @@ import { getIconPath } from "./helper"
|
|||
import { t } from "./lib/i18n"
|
||||
import { store } from "./lib/store"
|
||||
import { getTrayConfig } from "./lib/tray"
|
||||
import { refreshBound } from "./lib/utils"
|
||||
import { logger } from "./logger"
|
||||
import { cancelPollingUpdateUnreadCount, pollingUpdateUnreadCount } from "./tipc/dock"
|
||||
import { loadDynamicRenderEntry } from "./updater/hot-updater"
|
||||
|
||||
const windows = {
|
||||
settingWindow: null as BrowserWindow | null,
|
||||
|
|
@ -87,18 +89,10 @@ export function createWindow(
|
|||
})
|
||||
|
||||
window.on("leave-html-full-screen", () => {
|
||||
function refreshBound(timeout = 0) {
|
||||
setTimeout(() => {
|
||||
// FIXME: workaround for theme bug in full screen mode
|
||||
const size = window?.getSize()
|
||||
window?.setSize(size[0] + 1, size[1] + 1)
|
||||
window?.setSize(size[0], size[1])
|
||||
}, timeout)
|
||||
}
|
||||
// To solve the vibrancy losing issue when leaving full screen mode
|
||||
// @see https://github.com/toeverything/AFFiNE/blob/280e24934a27557529479a70ab38c4f5fc65cb00/packages/frontend/electron/src/main/windows-manager/main-window.ts:L157
|
||||
refreshBound()
|
||||
refreshBound(1000)
|
||||
refreshBound(window)
|
||||
refreshBound(window, 1000)
|
||||
})
|
||||
|
||||
window.on("ready-to-show", () => {
|
||||
|
|
@ -149,11 +143,15 @@ export function createWindow(
|
|||
|
||||
logger.log(process.env["ELECTRON_RENDERER_URL"] + (options?.extraPath || ""))
|
||||
} else {
|
||||
const openPath = path.resolve(__dirname, "../renderer/index.html")
|
||||
window.loadFile(openPath, {
|
||||
// Production entry
|
||||
const dynamicRenderEntry = loadDynamicRenderEntry()
|
||||
logger.info("load dynamic render entry", dynamicRenderEntry)
|
||||
const appLoadEntry = dynamicRenderEntry || path.resolve(__dirname, "../renderer/index.html")
|
||||
|
||||
window.loadFile(appLoadEntry, {
|
||||
hash: options?.extraPath,
|
||||
})
|
||||
logger.log(openPath, {
|
||||
logger.log(appLoadEntry, {
|
||||
hash: options?.extraPath,
|
||||
})
|
||||
}
|
||||
|
|
@ -282,7 +280,7 @@ export const createMainWindow = () => {
|
|||
const caller = callWindowExpose(window)
|
||||
const settings = await caller.getUISettings()
|
||||
|
||||
if (settings.showDockBadge) {
|
||||
if (settings?.showDockBadge) {
|
||||
pollingUpdateUnreadCount()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { getStorageNS } from "@follow/utils/ns"
|
||||
import { atomWithStorage } from "jotai/utils"
|
||||
import { atom } from "jotai"
|
||||
|
||||
import { createAtomHooks } from "~/lib/jotai"
|
||||
|
||||
export const [, , useUpdaterStatus, , , setUpdaterStatus] = createAtomHooks(
|
||||
atomWithStorage(getStorageNS("updater"), false, undefined, {
|
||||
getOnInit: true,
|
||||
}),
|
||||
export type UpdaterStatus = "ready"
|
||||
export type UpdaterStatusAtom = {
|
||||
type: "app" | "renderer"
|
||||
status: UpdaterStatus
|
||||
} | null
|
||||
export const [, , useUpdaterStatus, , getUpdaterStatus, setUpdaterStatus] = createAtomHooks(
|
||||
atom(null as UpdaterStatusAtom),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useEffect } from "react"
|
|||
import { createRoutesFromChildren, matchRoutes, useLocation, useNavigationType } from "react-router"
|
||||
|
||||
import { whoami } from "~/atoms/user"
|
||||
import { isElectronBuild } from "~/constants"
|
||||
|
||||
import { SentryConfig } from "./sentry.config"
|
||||
|
||||
|
|
@ -48,5 +49,6 @@ export const initSentry = async () => {
|
|||
}
|
||||
|
||||
Sentry.setTag("session_trace_id", appSessionTraceId)
|
||||
Sentry.setTag("appVersion", version)
|
||||
Sentry.setTag("app_version", version)
|
||||
Sentry.setTag("build", isElectronBuild ? "electron" : "web")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useCallback, useEffect } from "react"
|
|||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { useAudioPlayerAtomSelector } from "~/atoms/player"
|
||||
import { setUpdaterStatus, useUpdaterStatus } from "~/atoms/updater"
|
||||
import { getUpdaterStatus, setUpdaterStatus, useUpdaterStatus } from "~/atoms/updater"
|
||||
import { softBouncePreset } from "~/components/ui/constants/spring"
|
||||
import { tipcClient } from "~/lib/client"
|
||||
import { handlers } from "~/tipc"
|
||||
|
|
@ -15,16 +15,31 @@ export const AutoUpdater = () => {
|
|||
|
||||
useEffect(() => {
|
||||
const unlisten = handlers?.updateDownloaded.listen(() => {
|
||||
setUpdaterStatus(true)
|
||||
setUpdaterStatus({
|
||||
type: "app",
|
||||
status: "ready",
|
||||
})
|
||||
})
|
||||
return unlisten
|
||||
}, [])
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setUpdaterStatus(false)
|
||||
window.analytics?.capture("update_restart")
|
||||
|
||||
tipcClient?.quitAndInstall()
|
||||
const status = getUpdaterStatus()
|
||||
if (!status) return
|
||||
window.analytics?.capture("update_restart", {
|
||||
type: status.type,
|
||||
})
|
||||
switch (status.type) {
|
||||
case "app": {
|
||||
tipcClient?.quitAndInstall()
|
||||
break
|
||||
}
|
||||
case "renderer": {
|
||||
tipcClient?.rendererUpdateReload()
|
||||
break
|
||||
}
|
||||
}
|
||||
setUpdaterStatus(null)
|
||||
}, [])
|
||||
|
||||
const playerIsShow = useAudioPlayerAtomSelector((s) => s.show)
|
||||
|
|
@ -68,7 +83,9 @@ export const AutoUpdater = () => {
|
|||
}
|
||||
/>
|
||||
<div className="font-medium">{t("notify.update_info", { app_name: APP_NAME })}</div>
|
||||
<div className="text-xs text-zinc-500">{t("notify.update_info_1")}</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
{updaterStatus.type === "app" ? t("notify.update_info_1") : t("notify.update_info_2")}
|
||||
</div>
|
||||
</m.div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,21 @@ import { styledButtonVariant } from "@follow/components/ui/button/variants.js"
|
|||
import { Divider } from "@follow/components/ui/divider/index.js"
|
||||
import { getCurrentEnvironment } from "@follow/utils/environment"
|
||||
import { license, repository } from "@pkg"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { Trans, useTranslation } from "react-i18next"
|
||||
|
||||
import { CopyButton } from "~/components/ui/code-highlighter"
|
||||
import { SocialMediaLinks } from "~/constants/social"
|
||||
import { tipcClient } from "~/lib/client"
|
||||
import { getNewIssueUrl } from "~/lib/issues"
|
||||
|
||||
export const SettingAbout = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
const currentEnvironment = getCurrentEnvironment().join("\n")
|
||||
const { data: renderVersion } = useQuery({
|
||||
queryKey: ["renderVersion"],
|
||||
queryFn: () => tipcClient?.getRenderVersion() || "",
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -24,10 +30,19 @@ export const SettingAbout = () => {
|
|||
<div className="text-lg font-bold">
|
||||
{APP_NAME} {!import.meta.env.PROD ? `(${import.meta.env.MODE})` : ""}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">{APP_VERSION}</span>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">app: {APP_VERSION}</span>
|
||||
{renderVersion && (
|
||||
<span className="rounded bg-muted px-2 py-1 text-xs">
|
||||
renderer: {renderVersion}
|
||||
</span>
|
||||
)}
|
||||
<CopyButton
|
||||
value={currentEnvironment}
|
||||
value={
|
||||
renderVersion
|
||||
? `${currentEnvironment}\n**Renderer**: ${renderVersion}`
|
||||
: currentEnvironment
|
||||
}
|
||||
className="border-0 bg-transparent p-1 text-foreground/80 hover:bg-theme-item-hover hover:text-foreground active:bg-theme-item-active [&_i]:size-3"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { toast } from "sonner"
|
|||
|
||||
import { getGeneralSettings } from "~/atoms/settings/general"
|
||||
import { getUISettings, useToggleZenMode } from "~/atoms/settings/ui"
|
||||
import { setUpdaterStatus } from "~/atoms/updater"
|
||||
import { useDialog, useModalStack } from "~/components/ui/modal/stacked/hooks"
|
||||
import { useDiscoverRSSHubRouteModal } from "~/hooks/biz/useDiscoverRSSHubRoute"
|
||||
import { useFollow } from "~/hooks/biz/useFollow"
|
||||
|
|
@ -40,6 +41,12 @@ export const ExtensionExposeProvider = () => {
|
|||
clearIfLoginOtherAccount(newUserId: string) {
|
||||
clearDataIfLoginOtherAccount(newUserId)
|
||||
},
|
||||
readyToUpdate() {
|
||||
setUpdaterStatus({
|
||||
type: "renderer",
|
||||
status: "ready",
|
||||
})
|
||||
},
|
||||
})
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ export const initSentry = async () => {
|
|||
...SentryConfig,
|
||||
})
|
||||
|
||||
Sentry.setTag("appVersion", APP_VERSION)
|
||||
Sentry.setTag("app_version", APP_VERSION)
|
||||
Sentry.setTag("build", "external-pages")
|
||||
}
|
||||
|
||||
const ERROR_PATTERNS = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
import { dirname, resolve } from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
|
||||
import type { UserConfig } from "vite"
|
||||
|
||||
import { createPlatformSpecificImportPlugin } from "../plugins/vite/specific-import"
|
||||
import { viteRenderBaseConfig } from "./vite.render.config"
|
||||
|
||||
const root = resolve(fileURLToPath(dirname(import.meta.url)), "..")
|
||||
|
||||
export default {
|
||||
...viteRenderBaseConfig,
|
||||
|
||||
plugins: [...viteRenderBaseConfig.plugins, createPlatformSpecificImportPlugin(true)],
|
||||
|
||||
root: resolve(root, "apps/renderer"),
|
||||
build: {
|
||||
outDir: resolve(root, "dist/renderer"),
|
||||
sourcemap: !!process.env.CI,
|
||||
target: "esnext",
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(root, "apps/renderer/index.html"),
|
||||
},
|
||||
},
|
||||
minify: true,
|
||||
},
|
||||
define: {
|
||||
...viteRenderBaseConfig.define,
|
||||
ELECTRON: "true",
|
||||
},
|
||||
} satisfies UserConfig
|
||||
|
|
@ -5,6 +5,7 @@ import { defineConfig } from "electron-vite"
|
|||
import { viteRenderBaseConfig } from "./configs/vite.render.config"
|
||||
import { cleanupUnnecessaryFilesPlugin } from "./plugins/vite/cleanup"
|
||||
import { createPlatformSpecificImportPlugin } from "./plugins/vite/specific-import"
|
||||
import { getGitHash } from "./scripts/lib"
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
|
|
@ -24,6 +25,7 @@ export default defineConfig({
|
|||
},
|
||||
define: {
|
||||
ELECTRON: "true",
|
||||
GIT_COMMIT_HASH: JSON.stringify(getGitHash()),
|
||||
},
|
||||
},
|
||||
preload: {
|
||||
|
|
@ -43,6 +45,19 @@ export default defineConfig({
|
|||
renderer: {
|
||||
...viteRenderBaseConfig,
|
||||
|
||||
root: "apps/renderer",
|
||||
build: {
|
||||
outDir: "dist/renderer",
|
||||
sourcemap: !!process.env.CI,
|
||||
target: "esnext",
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve("./apps/renderer/index.html"),
|
||||
},
|
||||
},
|
||||
minify: true,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
...viteRenderBaseConfig.plugins,
|
||||
createPlatformSpecificImportPlugin(true),
|
||||
|
|
@ -61,18 +76,6 @@ export default defineConfig({
|
|||
]),
|
||||
],
|
||||
|
||||
root: "apps/renderer",
|
||||
build: {
|
||||
outDir: "dist/renderer",
|
||||
sourcemap: !!process.env.CI,
|
||||
target: "esnext",
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve("./apps/renderer/index.html"),
|
||||
},
|
||||
},
|
||||
minify: true,
|
||||
},
|
||||
define: {
|
||||
...viteRenderBaseConfig.define,
|
||||
ELECTRON: "true",
|
||||
|
|
|
|||
|
|
@ -263,6 +263,7 @@
|
|||
"notify.unfollow_feed_many": "All selected feeds have been unfollowed.",
|
||||
"notify.update_info": "{{app_name}} is ready to update!",
|
||||
"notify.update_info_1": "Click to restart",
|
||||
"notify.update_info_2": "Click to reload page",
|
||||
"player.back_10s": "Back 10s",
|
||||
"player.close": "Close",
|
||||
"player.download": "Download",
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -17,6 +17,7 @@
|
|||
"analyze:web": "analyzer=1 vite build",
|
||||
"build": "electron-vite build && electron-forge make",
|
||||
"build:macos": "electron-vite build && electron-forge make --arch=x64 --platform=darwin && electron-forge make --arch=arm64 --platform=darwin && tsx scripts/merge-yml.ts",
|
||||
"build:render": "vite build -c vite.config.electron-render.ts",
|
||||
"build:web": "rm -rf out/web && cross-env WEB_BUILD=1 vite build",
|
||||
"bump": "vv",
|
||||
"dedupe:locales": "eslint --fix locales/**/*.json",
|
||||
|
|
@ -37,7 +38,8 @@
|
|||
"start": "electron-vite preview",
|
||||
"sync:ab": "tsx scripts/pull-ab-flags.ts",
|
||||
"test": "CI=1 pnpm --recursive run test",
|
||||
"typecheck": "turbo typecheck"
|
||||
"typecheck": "turbo typecheck",
|
||||
"update:main-hash": "tsx plugins/vite/generate-main-hash.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/generator": "7.26.2",
|
||||
|
|
@ -65,6 +67,7 @@
|
|||
"@tailwindcss/container-queries": "0.1.1",
|
||||
"@tailwindcss/typography": "0.5.15",
|
||||
"@types/html-minifier-terser": "7.0.2",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "^22.9.3",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
|
|
@ -104,6 +107,7 @@
|
|||
"tailwindcss-animate": "1.0.7",
|
||||
"tailwindcss-motion": "0.4.3-beta",
|
||||
"tailwindcss-safe-area": "0.6.0",
|
||||
"tar": "7.4.3",
|
||||
"tsup": "8.3.5",
|
||||
"tsx": "4.19.2",
|
||||
"turbo": "2.3.1",
|
||||
|
|
@ -149,7 +153,11 @@
|
|||
"before": [
|
||||
"git pull --rebase",
|
||||
"tsx scripts/apply-changelog.ts ${NEW_VERSION}",
|
||||
"git add changelog"
|
||||
"git add changelog",
|
||||
"tsx plugins/vite/generate-main-hash.ts",
|
||||
"eslint --fix package.json",
|
||||
"prettier --ignore-unknown --write package.json",
|
||||
"git add package.json"
|
||||
],
|
||||
"after": [
|
||||
"gh pr create --title 'chore: Release v${NEW_VERSION}' --body 'v${NEW_VERSION}' --base main --head dev"
|
||||
|
|
@ -161,5 +169,6 @@
|
|||
"dev"
|
||||
]
|
||||
},
|
||||
"productName": "Follow"
|
||||
"productName": "Follow",
|
||||
"mainHash": "1"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ interface RenderGlobalContext {
|
|||
|
||||
/// Utils
|
||||
toast: typeof toast
|
||||
|
||||
readyToUpdate: () => void
|
||||
dialog: typeof dialog
|
||||
// URL
|
||||
getWebUrl: () => string
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
import { execSync } from "node:child_process"
|
||||
import { createHash } from "node:crypto"
|
||||
import fs from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
|
||||
import * as tar from "tar"
|
||||
import type { Plugin } from "vite"
|
||||
|
||||
import { calculateMainHash } from "./generate-main-hash"
|
||||
|
||||
async function compressDirectory(sourceDir: string, outputFile: string) {
|
||||
await tar.c(
|
||||
{
|
||||
gzip: true,
|
||||
file: outputFile,
|
||||
cwd: sourceDir,
|
||||
},
|
||||
["renderer"],
|
||||
)
|
||||
}
|
||||
|
||||
function compressAndFingerprintPlugin(outDir: string): Plugin {
|
||||
return {
|
||||
name: "compress-and-fingerprint",
|
||||
apply: "build",
|
||||
async closeBundle() {
|
||||
const outputFile = path.join(outDir, "render-asset.tar.gz")
|
||||
const manifestFile = path.join(outDir, "manifest.yml")
|
||||
|
||||
console.info("Compressing and fingerprinting...")
|
||||
// Compress the entire output directory
|
||||
await compressDirectory(outDir, outputFile)
|
||||
console.info("Compressing and fingerprinting", outDir, "done")
|
||||
|
||||
// Calculate the file hash
|
||||
const fileBuffer = await fs.readFile(outputFile)
|
||||
const hashSum = createHash("sha256")
|
||||
hashSum.update(fileBuffer)
|
||||
const hex = hashSum.digest("hex")
|
||||
|
||||
// Calculate main hash
|
||||
const mainHash = await calculateMainHash(path.resolve(process.cwd(), "apps/main"))
|
||||
|
||||
// Get the current git tag version
|
||||
let version = "unknown"
|
||||
try {
|
||||
version = execSync("git describe --tags").toString().trim()
|
||||
} catch (error) {
|
||||
console.warn("Could not retrieve git tag version:", error)
|
||||
}
|
||||
|
||||
// Write the manifest file
|
||||
const manifestContent = `
|
||||
version: ${version.startsWith("v") ? version.slice(1) : version}
|
||||
hash: ${hex}
|
||||
mainHash: ${mainHash}
|
||||
commit: ${execSync("git rev-parse HEAD").toString().trim()}
|
||||
filename: ${path.basename(outputFile)}
|
||||
`
|
||||
console.info("Writing manifest file", manifestContent)
|
||||
await fs.writeFile(manifestFile, manifestContent.trim())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default compressAndFingerprintPlugin
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import { createHash } from "node:crypto"
|
||||
import fs from "node:fs/promises"
|
||||
import { createRequire } from "node:module"
|
||||
import path from "node:path"
|
||||
|
||||
const require = createRequire(import.meta.url)
|
||||
const glob = require("glob") as typeof import("glob")
|
||||
|
||||
export async function calculateMainHash(mainDir: string): Promise<string> {
|
||||
// Get all TypeScript files in the main directory recursively
|
||||
const files = glob.sync("**/*.{ts,tsx}", {
|
||||
cwd: mainDir,
|
||||
ignore: ["node_modules/**", "dist/**"],
|
||||
})
|
||||
|
||||
// Sort files for consistent hash
|
||||
|
||||
files.sort()
|
||||
files.push("package.json")
|
||||
|
||||
const hashSum = createHash("sha256")
|
||||
|
||||
// Read and update hash for each file
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(path.join(mainDir, file))
|
||||
const normalizedContent = content.toString("utf-8").replaceAll("\r\n", "\n")
|
||||
hashSum.update(Buffer.from(normalizedContent))
|
||||
}
|
||||
|
||||
return hashSum.digest("hex")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const hash = await calculateMainHash(path.resolve(process.cwd(), "apps/main"))
|
||||
|
||||
const packageJson = JSON.parse(
|
||||
await fs.readFile(path.resolve(process.cwd(), "package.json"), "utf-8"),
|
||||
)
|
||||
packageJson.mainHash = hash
|
||||
await fs.writeFile(
|
||||
path.resolve(process.cwd(), "package.json"),
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
@ -119,6 +119,9 @@ importers:
|
|||
'@types/html-minifier-terser':
|
||||
specifier: 7.0.2
|
||||
version: 7.0.2
|
||||
'@types/js-yaml':
|
||||
specifier: 4.0.9
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: ^22.9.3
|
||||
version: 22.9.3
|
||||
|
|
@ -236,6 +239,9 @@ importers:
|
|||
tailwindcss-safe-area:
|
||||
specifier: 0.6.0
|
||||
version: 0.6.0(tailwindcss@3.4.15(ts-node@10.9.1(@types/node@22.9.3)(typescript@5.7.2)))
|
||||
tar:
|
||||
specifier: 7.4.3
|
||||
version: 7.4.3
|
||||
tsup:
|
||||
specifier: 8.3.5
|
||||
version: 8.3.5(jiti@2.3.3)(postcss@8.4.49)(tsx@4.19.2)(typescript@5.7.2)(yaml@2.6.1)
|
||||
|
|
@ -290,6 +296,9 @@ importers:
|
|||
'@mozilla/readability':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(patch_hash=43niildbdafdxi7qfcwhpkkxwa)
|
||||
'@openpanel/web':
|
||||
specifier: 1.0.1
|
||||
version: 1.0.1
|
||||
'@sentry/electron':
|
||||
specifier: 5.7.0
|
||||
version: 5.7.0
|
||||
|
|
@ -308,6 +317,9 @@ importers:
|
|||
electron-updater:
|
||||
specifier: ^6.3.9
|
||||
version: 6.3.9
|
||||
es-toolkit:
|
||||
specifier: 1.26.1
|
||||
version: 1.26.1
|
||||
fast-folder-size:
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
|
|
@ -317,6 +329,9 @@ importers:
|
|||
i18next:
|
||||
specifier: ^24.0.0
|
||||
version: 24.0.0(typescript@5.7.2)
|
||||
js-yaml:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
linkedom:
|
||||
specifier: ^0.18.5
|
||||
version: 0.18.5
|
||||
|
|
@ -326,16 +341,25 @@ importers:
|
|||
msedge-tts:
|
||||
specifier: 1.3.4
|
||||
version: 1.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
|
||||
node-machine-id:
|
||||
specifier: 1.1.12
|
||||
version: 1.1.12
|
||||
ofetch:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1
|
||||
semver:
|
||||
specifier: 7.6.3
|
||||
version: 7.6.3
|
||||
tar:
|
||||
specifier: 7.4.3
|
||||
version: 7.4.3
|
||||
vscode-languagedetection:
|
||||
specifier: npm:@vscode/vscode-languagedetection@^1.0.22
|
||||
version: '@vscode/vscode-languagedetection@1.0.22'
|
||||
devDependencies:
|
||||
'@types/js-yaml':
|
||||
specifier: 4.0.9
|
||||
version: 4.0.9
|
||||
'@types/node':
|
||||
specifier: ^22.9.3
|
||||
version: 22.9.3
|
||||
|
|
@ -2840,6 +2864,10 @@ packages:
|
|||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.5':
|
||||
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
|
@ -4471,6 +4499,9 @@ packages:
|
|||
'@types/http-cache-semantics@4.0.4':
|
||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||
|
||||
'@types/js-yaml@4.0.9':
|
||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
|
|
@ -5297,6 +5328,10 @@ packages:
|
|||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
chrome-trace-event@1.0.4:
|
||||
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
@ -6275,6 +6310,9 @@ packages:
|
|||
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-toolkit@1.26.1:
|
||||
resolution: {integrity: sha512-E3H14lHWk8JpupVpIRA1gfNF4r953abHTFW+X1Rp7zl7eG37ksuthfEA4FinyVF/Y807vzzfQS1nubeZk2LTVA==}
|
||||
|
||||
es-toolkit@1.27.0:
|
||||
resolution: {integrity: sha512-ETSFA+ZJArcuSCpzD2TjAy6UHpx4E4uqFsoDg9F/nTLogrLmVVZQ+zNxco5h7cWnA1nNak07IXsLcaSMih+ZPQ==}
|
||||
|
||||
|
|
@ -8496,6 +8534,10 @@ packages:
|
|||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
minizlib@3.0.1:
|
||||
resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
|
|
@ -8508,6 +8550,11 @@ packages:
|
|||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
mlly@1.7.2:
|
||||
resolution: {integrity: sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==}
|
||||
|
||||
|
|
@ -8618,6 +8665,9 @@ packages:
|
|||
engines: {node: ^12.13 || ^14.13 || >=16}
|
||||
hasBin: true
|
||||
|
||||
node-machine-id@1.1.12:
|
||||
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
|
||||
|
||||
node-releases@2.0.18:
|
||||
resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==}
|
||||
|
||||
|
|
@ -9872,6 +9922,10 @@ packages:
|
|||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rimraf@5.0.10:
|
||||
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
|
||||
hasBin: true
|
||||
|
||||
rimraf@6.0.1:
|
||||
resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
|
@ -10404,6 +10458,10 @@ packages:
|
|||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
tar@7.4.3:
|
||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
temp-dir@2.0.0:
|
||||
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -11262,6 +11320,10 @@ packages:
|
|||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yallist@5.0.0:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yaml@2.5.1:
|
||||
resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==}
|
||||
engines: {node: '>= 14'}
|
||||
|
|
@ -13972,6 +14034,10 @@ snapshots:
|
|||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
|
|
@ -15812,6 +15878,8 @@ snapshots:
|
|||
|
||||
'@types/http-cache-semantics@4.0.4': {}
|
||||
|
||||
'@types/js-yaml@4.0.9': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/katex@0.16.7': {}
|
||||
|
|
@ -16979,6 +17047,8 @@ snapshots:
|
|||
|
||||
chownr@2.0.0: {}
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
chrome-trace-event@1.0.4: {}
|
||||
|
||||
chromium-pickle-js@0.2.0: {}
|
||||
|
|
@ -18033,6 +18103,8 @@ snapshots:
|
|||
is-date-object: 1.0.5
|
||||
is-symbol: 1.0.4
|
||||
|
||||
es-toolkit@1.26.1: {}
|
||||
|
||||
es-toolkit@1.27.0: {}
|
||||
|
||||
es6-error@4.1.1:
|
||||
|
|
@ -20867,6 +20939,11 @@ snapshots:
|
|||
minipass: 3.3.6
|
||||
yallist: 4.0.0
|
||||
|
||||
minizlib@3.0.1:
|
||||
dependencies:
|
||||
minipass: 7.1.2
|
||||
rimraf: 5.0.10
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mkdirp@0.5.6:
|
||||
|
|
@ -20875,6 +20952,8 @@ snapshots:
|
|||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mlly@1.7.2:
|
||||
dependencies:
|
||||
acorn: 8.14.0
|
||||
|
|
@ -21003,6 +21082,8 @@ snapshots:
|
|||
- bluebird
|
||||
- supports-color
|
||||
|
||||
node-machine-id@1.1.12: {}
|
||||
|
||||
node-releases@2.0.18: {}
|
||||
|
||||
nopt@5.0.0:
|
||||
|
|
@ -22285,6 +22366,10 @@ snapshots:
|
|||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
rimraf@5.0.10:
|
||||
dependencies:
|
||||
glob: 10.4.5
|
||||
|
||||
rimraf@6.0.1:
|
||||
dependencies:
|
||||
glob: 11.0.0
|
||||
|
|
@ -22956,6 +23041,15 @@ snapshots:
|
|||
mkdirp: 1.0.4
|
||||
yallist: 4.0.0
|
||||
|
||||
tar@7.4.3:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.2
|
||||
minizlib: 3.0.1
|
||||
mkdirp: 3.0.1
|
||||
yallist: 5.0.0
|
||||
|
||||
temp-dir@2.0.0: {}
|
||||
|
||||
temp-file@3.4.0:
|
||||
|
|
@ -23920,6 +24014,8 @@ snapshots:
|
|||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yaml@2.5.1: {}
|
||||
|
||||
yaml@2.6.1: {}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json"
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
import { resolve } from "node:path"
|
||||
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
import config from "./configs/vite.electron-render.config"
|
||||
import compressAndFingerprintPlugin from "./plugins/vite/compress"
|
||||
|
||||
export default defineConfig({
|
||||
...config,
|
||||
base: "./",
|
||||
plugins: [...config.plugins, compressAndFingerprintPlugin(resolve(import.meta.dirname, "dist"))],
|
||||
})
|
||||
Loading…
Reference in New Issue