diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f798fbc --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +dist-electron +release +*.local + +# Editor directories and files +.vscode/.debug.env +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# lockfile +package-lock.json +pnpm-lock.yaml +yarn.lock \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..16bb32c --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + bracketSpacing: true, + singleQuote: false, + arrowParens: "avoid", + trailingComma: "none" +}; diff --git a/LICENSE b/LICENSE index 1cbdade..8d3fda8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 8473136 +Copyright (c) 2023 刘嘉伟 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/electron-builder.json5 b/electron-builder.json5 new file mode 100644 index 0000000..5ba2076 --- /dev/null +++ b/electron-builder.json5 @@ -0,0 +1,46 @@ +/** + * @see https://www.electron.build/configuration/configuration + */ +{ + "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + "appId": "priv.liujiawei.frpc.desktop", + "asar": true, + "productName": "Frpc-Desktop", + "directories": { + "output": "release/${version}" + }, + "icon": "./public/logo/512x512.png", + "files": [ + "dist", + "dist-electron" + ], + "mac": { + "target": [ + "dmg" + ], + "artifactName": "${productName}-Mac-${version}-Installer.${ext}" + }, + "win": { + "target": [ + { + "target": "nsis", + "arch": [ + "x64" + ] + } + ], + "artifactName": "${productName}-Windows-${version}-Setup.${ext}" + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "deleteAppDataOnUninstall": false + }, + "linux": { + "target": [ + "AppImage" + ], + "artifactName": "${productName}-Linux-${version}.${ext}" + }, +} diff --git a/electron/api/config.ts b/electron/api/config.ts new file mode 100644 index 0000000..44cb054 --- /dev/null +++ b/electron/api/config.ts @@ -0,0 +1,42 @@ +import { ipcMain } from "electron"; +import { getConfig, saveConfig } from "../storage/config"; +import { listVersion } from "../storage/version"; + +export const initConfigApi = () => { + ipcMain.on("config.saveConfig", async (event, args) => { + saveConfig(args, (err, numberOfUpdated, upsert) => { + event.reply("Config.saveConfig.hook", { + err: err, + numberOfUpdated: numberOfUpdated, + upsert: upsert + }); + }); + }); + + ipcMain.on("config.getConfig", async (event, args) => { + getConfig((err, doc) => { + event.reply("Config.getConfig.hook", { + err: err, + data: doc + }); + }); + }); + + ipcMain.on("config.versions", event => { + listVersion((err, doc) => { + event.reply("Config.versions.hook", { + err: err, + data: doc + }); + }); + }); + + ipcMain.on("config.hasConfig", event => { + getConfig((err, doc) => { + event.reply("Config.getConfig.hook", { + err: err, + data: doc + }); + }); + }); +}; diff --git a/electron/api/frpc.ts b/electron/api/frpc.ts new file mode 100644 index 0000000..95c547f --- /dev/null +++ b/electron/api/frpc.ts @@ -0,0 +1,183 @@ +import { app, ipcMain } from "electron"; +import { Config, getConfig } from "../storage/config"; +import { listProxy } from "../storage/proxy"; +import { getVersionById } from "../storage/version"; + +const fs = require("fs"); +const path = require("path"); + +const { exec, spawn } = require("child_process"); +export let frpcProcess = null; + +const runningCmd = { + commandPath: null, + configPath: null +}; + +// const getFrpc = (config: Config) => { +// getVersionById(config.currentVersion, (err, document) => { +// if (!err) { +// } +// }); +// }; + +/** + * 获取选择版本的工作目录 + * @param versionId 版本ID + * @param callback + */ +const getFrpcVersionWorkerPath = ( + versionId: string, + callback: (workerPath: string) => void +) => { + getVersionById(versionId, (err2, version) => { + if (!err2) { + callback(version["frpcVersionPath"]); + } + }); +}; + +/** + * 生成配置文件 + */ +export const generateConfig = ( + config: Config, + callback: (configPath: string) => void +) => { + listProxy((err3, proxys) => { + if (!err3) { + const proxyToml = proxys.map(m => { + let toml = ` +[[proxies]] +name = "${m.name}" +type = "${m.type}" +localIP = "${m.localIp}" +localPort = ${m.localPort} +`; + switch (m.type) { + case "tcp": + toml += `remotePort = ${m.remotePort}`; + break; + case "http": + case "https": + toml += `customDomains=[${m.customDomains.map(m => `"${m}"`)}]`; + break; + default: + break; + } + + return toml; + }); + let toml = ` +serverAddr = "${config.serverAddr}" +serverPort = ${config.serverPort} +auth.method = "${config.authMethod}" +auth.token = "${config.authToken}" +log.to = "frpc.log" +log.level = "debug" +log.maxDays = 3 +webServer.addr = "127.0.0.1" +webServer.port = 57400 + +${proxyToml} + `; + + // const configPath = path.join("frp.toml"); + const filename = "frp.toml"; + fs.writeFile( + path.join(app.getPath("userData"), filename), // 配置文件目录 + toml, // 配置文件内容 + { flag: "w" }, + err => { + if (!err) { + callback(filename); + } + } + ); + } + }); +}; + +/** + * 启动frpc子进程 + * @param cwd + * @param commandPath + * @param configPath + */ +const startFrpcProcess = (commandPath: string, configPath: string) => { + const command = `${commandPath} -c ${configPath}`; + frpcProcess = spawn(command, { + cwd: app.getPath("userData"), + shell: true + }); + runningCmd.commandPath = commandPath; + runningCmd.configPath = configPath; + frpcProcess.stdout.on("data", data => { + console.log(`命令输出: ${data}`); + }); + frpcProcess.stdout.on("error", data => { + console.log(`执行错误: ${data}`); + frpcProcess.kill("SIGINT"); + }); +}; + +export const reloadFrpcProcess = () => { + if (frpcProcess && !frpcProcess.killed) { + getConfig((err1, config) => { + if (!err1) { + if (config) { + generateConfig(config, configPath => { + const command = `${runningCmd.commandPath} reload -c ${configPath}`; + console.log("重启", command); + exec(command, { + cwd: app.getPath("userData"), + shell: true + }); + }); + } + } + }); + } +}; + +export const initFrpcApi = () => { + ipcMain.handle("frpc.running", async (event, args) => { + if (!frpcProcess) { + return false; + } else { + return !frpcProcess.killed; + } + }); + + ipcMain.on("frpc.start", async (event, args) => { + getConfig((err1, config) => { + if (!err1) { + if (config) { + getFrpcVersionWorkerPath( + config.currentVersion, + (frpcVersionPath: string) => { + generateConfig(config, configPath => { + startFrpcProcess( + path.join(frpcVersionPath, "frpc"), + configPath + ); + }); + } + ); + } else { + event.reply( + "Home.frpc.start.error.hook", + "请先前往设置页面,修改配置后再启动" + ); + } + } + }); + }); + + ipcMain.on("frpc.stop", () => { + if (frpcProcess && !frpcProcess.killed) { + console.log("关闭"); + frpcProcess.kill(); + } + }); +}; diff --git a/electron/api/github.ts b/electron/api/github.ts new file mode 100644 index 0000000..fe896ae --- /dev/null +++ b/electron/api/github.ts @@ -0,0 +1,121 @@ +import { app, BrowserWindow, ipcMain, net } from "electron"; +import { insertVersion } from "../storage/version"; + +const fs = require("fs"); +const path = require("path"); +const zlib = require("zlib"); +const { download } = require("electron-dl"); + +const unTarGZ = tarGzPath => { + const tar = require("tar"); + const targetPath = path.resolve(path.join(app.getPath("userData"), "frp")); + const unzip = zlib.createGunzip(); + const readStream = fs.createReadStream(tarGzPath); + if (!fs.existsSync(unzip)) { + fs.mkdirSync(targetPath, { recursive: true }); + } + readStream.pipe(unzip).pipe( + tar.extract({ + cwd: targetPath, + filter: filePath => path.basename(filePath) === "frpc" + }) + ); + return path.join("frp", path.basename(tarGzPath, ".tar.gz")); + // .on("finish", () => { + // console.log("解压完成!"); + // }); +}; +export const initGitHubApi = () => { + // 版本 + let versions = []; + + const getVersion = versionId => { + return versions.find(f => f.id === versionId); + }; + + const getAdaptiveAsset = versionId => { + const version = getVersion(versionId); + const arch = process.arch; + const platform = process.platform; + const { assets } = version; + const asset = assets.find( + f => f.name.indexOf(platform) != -1 && f.name.indexOf(arch) != -1 + ); + return asset; + }; + + /** + * 获取github上的frp所有版本 + */ + ipcMain.on("github.getFrpVersions", async event => { + const request = net.request({ + method: "get", + url: "https://api.github.com/repos/fatedier/frp/releases" + }); + request.on("response", response => { + let responseData: Buffer = Buffer.alloc(0); + response.on("data", (data: Buffer) => { + responseData = Buffer.concat([responseData, data]); + }); + response.on("end", () => { + versions = JSON.parse(responseData.toString()); + // const borderContent: Electron.WebContents = + // BrowserWindow.getFocusedWindow().webContents; + const downloadPath = path.join(app.getPath("userData"), "download"); + const returnVersionsData = versions + .filter(f => getAdaptiveAsset(f.id)) + .map(m => { + const asset = getAdaptiveAsset(m.id); + if (asset) { + // const absPath = `${downloadPath}/${asset.id}_${asset.name}`; + const absPath = `${downloadPath}/${asset.name}`; + m.download_completed = fs.existsSync(absPath); + } else { + console.log("buzhicih"); + } + // m.download_completed = fs.existsSync( + // `${downloadPath}/${asset.id}_${asset.name}` + // ); + return m; + }); + event.reply("Download.frpVersionHook", returnVersionsData); + }); + }); + request.end(); + }); + + /** + * 下载请求 + */ + ipcMain.on("github.download", async (event, args) => { + const version = getVersion(args); + const asset = getAdaptiveAsset(args); + const { browser_download_url } = asset; + // 数据目录 + await download(BrowserWindow.getFocusedWindow(), browser_download_url, { + filename: `${asset.name}`, + directory: path.join(app.getPath("userData"), "download"), + onProgress: progress => { + event.reply("Download.frpVersionDownloadOnProgress", { + id: args, + progress: progress + }); + }, + onCompleted: () => { + const frpcVersionPath = unTarGZ( + path.join( + path.join(app.getPath("userData"), "download"), + `${asset.name}` + ) + ); + version["frpcVersionPath"] = frpcVersionPath; + insertVersion(version, (err, document) => { + if (!err) { + event.reply("Download.frpVersionDownloadOnCompleted", args); + version.download_completed = true; + } + }); + } + }); + }); +}; diff --git a/electron/api/logger.ts b/electron/api/logger.ts new file mode 100644 index 0000000..f76f024 --- /dev/null +++ b/electron/api/logger.ts @@ -0,0 +1,30 @@ +import { app, ipcMain } from "electron"; + +const fs = require("fs"); +const path = require("path"); +export const initLoggerApi = () => { + const logPath = path.join(app.getPath("userData"), "frpc.log"); + + const readLogger = (callback: (content: string) => void) => { + fs.readFile(logPath, "utf-8", (error, data) => { + if (!error) { + callback(data); + } + }); + }; + + ipcMain.on("logger.getLog", async (event, args) => { + readLogger(content => { + event.reply("Logger.getLog.hook", content); + }); + }); + ipcMain.on("logger.update", (event, args) => { + fs.watch(logPath, (eventType, filename) => { + if (eventType === "change") { + readLogger(content => { + event.reply("Logger.update.hook", content); + }); + } + }); + }); +}; diff --git a/electron/api/proxy.ts b/electron/api/proxy.ts new file mode 100644 index 0000000..2ded76c --- /dev/null +++ b/electron/api/proxy.ts @@ -0,0 +1,69 @@ +import { ipcMain } from "electron"; +import { + deleteProxyById, + getProxyById, + insertProxy, + listProxy, + updateProxyById +} from "../storage/proxy"; +import { reloadFrpcProcess } from "./frpc"; + + + +export const initProxyApi = () => { + ipcMain.on("proxy.getProxys", async (event, args) => { + listProxy((err, documents) => { + event.reply("Proxy.getProxys.hook", { + err: err, + data: documents + }); + }); + }); + + ipcMain.on("proxy.insertProxy", async (event, args) => { + delete args["_id"]; + insertProxy(args, (err, documents) => { + if (!err) { + reloadFrpcProcess() + } + event.reply("Proxy.insertProxy.hook", { + err: err, + data: documents + }); + }); + }); + + ipcMain.on("proxy.deleteProxyById", async (event, args) => { + deleteProxyById(args, (err, documents) => { + if (!err) { + reloadFrpcProcess() + } + event.reply("Proxy.deleteProxyById.hook", { + err: err, + data: documents + }); + }); + }); + + ipcMain.on("proxy.getProxyById", async (event, args) => { + getProxyById(args, (err, documents) => { + event.reply("Proxy.getProxyById.hook", { + err: err, + data: documents + }); + }); + }); + + ipcMain.on("proxy.updateProxy", async (event, args) => { + if (!args._id) return; + updateProxyById(args, (err, documents) => { + if (!err) { + reloadFrpcProcess() + } + event.reply("Proxy.updateProxy.hook", { + err: err, + data: documents + }); + }); + }); +}; diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts new file mode 100644 index 0000000..b4c4211 --- /dev/null +++ b/electron/electron-env.d.ts @@ -0,0 +1,11 @@ +/// + +declare namespace NodeJS { + interface ProcessEnv { + VSCODE_DEBUG?: 'true' + DIST_ELECTRON: string + DIST: string + /** /dist/ or /public/ */ + VITE_PUBLIC: string + } +} diff --git a/electron/main/index.ts b/electron/main/index.ts new file mode 100644 index 0000000..a0f03cc --- /dev/null +++ b/electron/main/index.ts @@ -0,0 +1,136 @@ +import { app, BrowserWindow, ipcMain, shell } from "electron"; +import { release } from "node:os"; +import { join } from "node:path"; +import { initGitHubApi } from "../api/github"; +import { initConfigApi } from "../api/config"; +import { initProxyApi } from "../api/proxy"; +import { initFrpcApi } from "../api/frpc"; +import { initLoggerApi } from "../api/logger"; +// The built directory structure +// +// ├─┬ dist-electron +// │ ├─┬ main +// │ │ └── index.js > Electron-Main +// │ └─┬ preload +// │ └── index.js > Preload-Scripts +// ├─┬ dist +// │ └── index.html > Electron-Renderer +// +process.env.DIST_ELECTRON = join(__dirname, ".."); +process.env.DIST = join(process.env.DIST_ELECTRON, "../dist"); +process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL + ? join(process.env.DIST_ELECTRON, "../public") + : process.env.DIST; + +// Disable GPU Acceleration for Windows 7 +if (release().startsWith("6.1")) app.disableHardwareAcceleration(); + +// Set application name for Windows 10+ notifications +if (process.platform === "win32") app.setAppUserModelId(app.getName()); + +if (!app.requestSingleInstanceLock()) { + app.quit(); + process.exit(0); +} + +// Remove electron security warnings +// This warning only shows in development mode +// Read more on https://www.electronjs.org/docs/latest/tutorial/security +// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' + +let win: BrowserWindow | null = null; +// Here, you can also use other preload +const preload = join(__dirname, "../preload/index.js"); +const url = process.env.VITE_DEV_SERVER_URL; +const indexHtml = join(process.env.DIST, "index.html"); + +async function createWindow() { + win = new BrowserWindow({ + title: "Main window", + icon: join(process.env.VITE_PUBLIC, "favicon.ico"), + webPreferences: { + preload, + // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production + // Consider using contextBridge.exposeInMainWorld + // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation + nodeIntegration: true, + contextIsolation: false + } + }); + + if (process.env.VITE_DEV_SERVER_URL) { + // electron-vite-vue#298 + win.loadURL(url); + // Open devTool if the app is not packaged + win.webContents.openDevTools(); + } else { + win.loadFile(indexHtml); + } + + // Test actively push message to the Electron-Renderer + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", new Date().toLocaleString()); + }); + + // Make all links open with the browser, not with the application + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("https:")) shell.openExternal(url); + return { action: "deny" }; + }); + + // 隐藏菜单栏 + const { Menu } = require("electron"); + Menu.setApplicationMenu(null); + // hide menu for Mac + if (process.platform !== "darwin") { + app.dock.hide(); + } + // win.webContents.on('will-navigate', (event, url) => { }) #344 +} + +app.whenReady().then(createWindow); + +app.on("window-all-closed", () => { + win = null; + if (process.platform !== "darwin") app.quit(); +}); + +app.on("second-instance", () => { + if (win) { + // Focus on the main window if the user tried to open another + if (win.isMinimized()) win.restore(); + win.focus(); + } +}); + +app.on("activate", () => { + const allWindows = BrowserWindow.getAllWindows(); + if (allWindows.length) { + allWindows[0].focus(); + } else { + createWindow(); + } +}); + +// New window example arg: new windows url +ipcMain.handle("open-win", (_, arg) => { + const childWindow = new BrowserWindow({ + webPreferences: { + preload, + nodeIntegration: true, + contextIsolation: false + } + }); + + if (process.env.VITE_DEV_SERVER_URL) { + childWindow.loadURL(`${url}#${arg}`); + } else { + childWindow.loadFile(indexHtml, { hash: arg }); + } +}); + +initGitHubApi(); +initConfigApi(); +initProxyApi(); +initFrpcApi(); +initLoggerApi(); diff --git a/electron/main/index111.ts b/electron/main/index111.ts new file mode 100644 index 0000000..71dd9b7 --- /dev/null +++ b/electron/main/index111.ts @@ -0,0 +1,127 @@ +import { app, BrowserWindow, ipcMain, shell } from "electron"; +import { release } from "node:os"; +import { join } from "node:path"; +import { initGitHubApi } from "../api/github"; +import { initConfigApi } from "../api/config"; +import { initProxyApi } from "../api/proxy"; +import { initFrpcApi } from "../api/frpc"; +import { initLoggerApi } from "../api/logger"; + +// The built directory structure +// +// ├─┬ dist-electron +// │ ├─┬ main +// │ │ └── index.js > Electron-Main +// │ └─┬ preload +// │ └── index.js > Preload-Scripts +// ├─┬ dist +// │ └── index.html > Electron-Renderer +// +process.env.DIST_ELECTRON = join(__dirname, ".."); +process.env.DIST = join(process.env.DIST_ELECTRON, "../dist"); +process.env.VITE_PUBLIC = process.env.VITE_DEV_SERVER_URL + ? join(process.env.DIST_ELECTRON, "../public") + : process.env.DIST; + +// Disable GPU Acceleration for Windows 7 +if (release().startsWith("6.1")) app.disableHardwareAcceleration(); + +// Set application name for Windows 10+ notifications +if (process.platform === "win32") app.setAppUserModelId(app.getName()); + +if (!app.requestSingleInstanceLock()) { + app.quit(); + process.exit(0); +} + +// Remove electron security warnings +// This warning only shows in development mode +// Read more on https://www.electronjs.org/docs/latest/tutorial/security +// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' + +let win: BrowserWindow | null = null; +// Here, you can also use other preload +const preload = join(__dirname, '../preload/index.js') +const url = process.env.VITE_DEV_SERVER_URL; +const indexHtml = join(process.env.DIST, "index.html"); + +async function createWindow() { + win = new BrowserWindow({ + width: 800, + height: 600, + resizable: false, + title: "Frpc Desktop", + icon: join(process.env.VITE_PUBLIC, "favicon.ico"), + webPreferences: { + preload, + // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production + // Consider using contextBridge.exposeInMainWorld + // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation + nodeIntegration: true, + contextIsolation: false + } + }); + + if (process.env.VITE_DEV_SERVER_URL) { + // electron-vite-vue#298 + win.loadURL(url); + // Open devTool if the app is not packaged + win.webContents.openDevTools(); + } else { + win.loadFile(indexHtml); + } + + // Make all links open with the browser, not with the application + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("https:")) shell.openExternal(url); + return { action: "deny" }; + }); + // win.webContents.on('will-navigate', (event, url) => { }) #344 +} + +app.whenReady().then(createWindow); + +app.on("window-all-closed", () => { + win = null; + if (process.platform !== "darwin") app.quit(); +}); + +app.on("second-instance", () => { + if (win) { + // Focus on the main window if the user tried to open another + if (win.isMinimized()) win.restore(); + win.focus(); + } +}); + +app.on("activate", () => { + const allWindows = BrowserWindow.getAllWindows(); + if (allWindows.length) { + allWindows[0].focus(); + } else { + createWindow(); + } +}); + +// New window example arg: new windows url +ipcMain.handle('open-win', (_, arg) => { + const childWindow = new BrowserWindow({ + webPreferences: { + preload, + nodeIntegration: true, + contextIsolation: false, + }, + }) + + if (process.env.VITE_DEV_SERVER_URL) { + childWindow.loadURL(`${url}#${arg}`) + } else { + childWindow.loadFile(indexHtml, { hash: arg }) + } +}) + +initGitHubApi(); +initConfigApi(); +initProxyApi(); +initFrpcApi(); +initLoggerApi(); diff --git a/electron/preload/index.ts b/electron/preload/index.ts new file mode 100644 index 0000000..ebf1276 --- /dev/null +++ b/electron/preload/index.ts @@ -0,0 +1,92 @@ +function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { + return new Promise((resolve) => { + if (condition.includes(document.readyState)) { + resolve(true) + } else { + document.addEventListener('readystatechange', () => { + if (condition.includes(document.readyState)) { + resolve(true) + } + }) + } + }) +} + +const safeDOM = { + append(parent: HTMLElement, child: HTMLElement) { + if (!Array.from(parent.children).find(e => e === child)) { + return parent.appendChild(child) + } + }, + remove(parent: HTMLElement, child: HTMLElement) { + if (Array.from(parent.children).find(e => e === child)) { + return parent.removeChild(child) + } + }, +} + +/** + * https://tobiasahlin.com/spinkit + * https://connoratherton.com/loaders + * https://projects.lukehaas.me/css-loaders + * https://matejkustec.github.io/SpinThatShit + */ +function useLoading() { + const className = `loaders-css__square-spin` + const styleContent = ` +@keyframes square-spin { + 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } + 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } + 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } + 100% { transform: perspective(100px) rotateX(0) rotateY(0); } +} +.${className} > div { + animation-fill-mode: both; + width: 50px; + height: 50px; + background: #fff; + animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; +} +.app-loading-wrap { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: #282c34; + z-index: 9; +} + ` + const oStyle = document.createElement('style') + const oDiv = document.createElement('div') + + oStyle.id = 'app-loading-style' + oStyle.innerHTML = styleContent + oDiv.className = 'app-loading-wrap' + oDiv.innerHTML = `
` + + return { + appendLoading() { + safeDOM.append(document.head, oStyle) + safeDOM.append(document.body, oDiv) + }, + removeLoading() { + safeDOM.remove(document.head, oStyle) + safeDOM.remove(document.body, oDiv) + }, + } +} + +// ---------------------------------------------------------------------- + +const { appendLoading, removeLoading } = useLoading() +domReady().then(appendLoading) + +window.onmessage = (ev) => { + ev.data.payload === 'removeLoading' && removeLoading() +} + +setTimeout(removeLoading, 4999) diff --git a/electron/storage/config.ts b/electron/storage/config.ts new file mode 100644 index 0000000..625e427 --- /dev/null +++ b/electron/storage/config.ts @@ -0,0 +1,37 @@ +import Datastore from "nedb"; +import path from "path"; +import { app } from "electron"; + +const configDB = new Datastore({ + autoload: true, + filename: path.join(app.getPath("userData"), "config.db") +}); + +export type Config = { + currentVersion: any; + serverAddr: string; + serverPort: number; + authMethod: string; + authToken: string; +}; + +/** + * 保存 + */ +export const saveConfig = ( + document: Config, + cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void +) => { + document["_id"] = "1"; + configDB.update({ _id: "1" }, document, { upsert: true }, cb); +}; + +/** + * 查找 + * @param cb + */ +export const getConfig = ( + cb: (err: Error | null, document: Config) => void +) => { + configDB.findOne({ _id: "1" }, cb); +}; diff --git a/electron/storage/proxy.ts b/electron/storage/proxy.ts new file mode 100644 index 0000000..4b85855 --- /dev/null +++ b/electron/storage/proxy.ts @@ -0,0 +1,74 @@ +import Datastore from "nedb"; +import path from "path"; +import { app } from "electron"; + +const proxyDB = new Datastore({ + autoload: true, + filename: path.join(app.getPath("userData"), "proxy.db") +}); + +export type Proxy = { + _id: string; + name: string; + type: string; + localIp: string; + localPort: number; + remotePort: number; + customDomains: string[]; +}; +/** + * 新增代理 + * @param proxy + * @param cb + */ +export const insertProxy = ( + proxy: Proxy, + cb?: (err: Error | null, document: Proxy) => void +) => { + console.log("新增", proxy); + proxyDB.insert(proxy, cb); +}; + +/** + * 删除代理 + * @param _id + * @param cb + */ +export const deleteProxyById = ( + _id: string, + cb?: (err: Error | null, n: number) => void +) => { + proxyDB.remove({ _id: _id }, cb); +}; + +/** + * 修改代理 + */ +export const updateProxyById = ( + proxy: Proxy, + cb?: (err: Error | null, numberOfUpdated: number, upsert: boolean) => void +) => { + proxyDB.update({ _id: proxy._id }, proxy, {}, cb); +}; + +/** + * 查找 + * @param cb + */ +export const listProxy = ( + callback: (err: Error | null, documents: Proxy[]) => void +) => { + proxyDB.find({}, callback); +}; + +/** + * 根据id查询 + * @param id + * @param callback + */ +export const getProxyById = ( + id: string, + callback: (err: Error | null, document: Proxy) => void +) => { + proxyDB.findOne({ _id: id }, callback); +}; diff --git a/electron/storage/version.ts b/electron/storage/version.ts new file mode 100644 index 0000000..ce9a80c --- /dev/null +++ b/electron/storage/version.ts @@ -0,0 +1,38 @@ +import Datastore from "nedb"; +import path from "path"; +import { Proxy } from "./proxy"; +import { app } from "electron"; + +const versionDB = new Datastore({ + autoload: true, + filename: path.join(app.getPath("userData"), "version.db") +}); + +/** + * 新增代理 + * @param proxy + * @param cb + */ +export const insertVersion = ( + version: any, + cb?: (err: Error | null, document: any) => void +) => { + versionDB.insert(version, cb); +}; + +/** + * 查找 + * @param cb + */ +export const listVersion = ( + callback: (err: Error | null, documents: any[]) => void +) => { + versionDB.find({}, callback); +}; + +export const getVersionById = ( + id: string, + callback: (err: Error | null, document: any) => void +) => { + versionDB.findOne({ id: id }, callback); +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..d1364fd --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + + Frpc Desktop + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..71d4896 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "Frpc-Desktop", + "version": "1.0.0", + "main": "dist-electron/main/index.js", + "description": "一个frpc桌面客户端", + "author": "刘嘉伟 <8473136@qq.com>", + "license": "MIT", + "private": true, + "keywords": [ + "electron", + "rollup", + "vite", + "vue3", + "vue" + ], + "debug": { + "env": { + "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344/" + } + }, + "scripts": { + "dev": "vite", + "build": "vue-tsc --noEmit && vite build && electron-builder", + "build:win": "vue-tsc --noEmit && vite build && electron-builder -w", + "preview": "vite preview", + "electron:generate-icons": "electron-icon-builder --input=./public/logo.png --output=build --flatten" + }, + "devDependencies": { + "@iconify/vue": "^4.1.1", + "@types/nedb": "^1.8.16", + "@vitejs/plugin-vue": "^4.3.3", + "@vue/eslint-config-prettier": "^7.1.0", + "@vueuse/core": "^9.13.0", + "autoprefixer": "^10.4.15", + "cssnano": "^6.0.1", + "electron": "^26.0.0", + "electron-builder": "^24.6.3", + "element-plus": "^2.4.2", + "eslint-plugin-prettier": "^4.2.1", + "moment": "^2.29.4", + "nedb": "^1.8.0", + "node-cmd": "^5.0.0", + "prettier": "^2.8.8", + "sass": "^1.66.1", + "sass-loader": "^13.3.2", + "tailwindcss": "^3.3.3", + "tree-kill": "^1.2.2", + "typescript": "^5.1.6", + "vite": "^4.4.9", + "vite-plugin-electron": "^0.15.3", + "vite-plugin-electron-renderer": "^0.14.5", + "vue-tsc": "^1.8.8", + "vue-types": "^5.1.1", + "electron-icon-builder": "^2.0.1", + "vue": "^3.3.4", + "vue-router": "^4.2.4" + }, + "dependencies": { + "electron-dl": "^3.5.1", + "tar": "^6.2.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..20d42d8 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,8 @@ +module.exports = { + plugins: { + "postcss-import": {}, + tailwindcss: {}, + autoprefixer: {}, + ...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}) + } +}; diff --git a/public/logo/1024x1024.png b/public/logo/1024x1024.png new file mode 100644 index 0000000..f60b506 Binary files /dev/null and b/public/logo/1024x1024.png differ diff --git a/public/logo/128x128.png b/public/logo/128x128.png new file mode 100644 index 0000000..2c1df6b Binary files /dev/null and b/public/logo/128x128.png differ diff --git a/public/logo/16x16.png b/public/logo/16x16.png new file mode 100644 index 0000000..0941120 Binary files /dev/null and b/public/logo/16x16.png differ diff --git a/public/logo/24x24.png b/public/logo/24x24.png new file mode 100644 index 0000000..ac5941c Binary files /dev/null and b/public/logo/24x24.png differ diff --git a/public/logo/256x256.png b/public/logo/256x256.png new file mode 100644 index 0000000..f66aac8 Binary files /dev/null and b/public/logo/256x256.png differ diff --git a/public/logo/32x32.png b/public/logo/32x32.png new file mode 100644 index 0000000..3500143 Binary files /dev/null and b/public/logo/32x32.png differ diff --git a/public/logo/48x48.png b/public/logo/48x48.png new file mode 100644 index 0000000..ca4cdbd Binary files /dev/null and b/public/logo/48x48.png differ diff --git a/public/logo/512x512.png b/public/logo/512x512.png new file mode 100644 index 0000000..027756d Binary files /dev/null and b/public/logo/512x512.png differ diff --git a/public/logo/64x64.png b/public/logo/64x64.png new file mode 100644 index 0000000..4d31016 Binary files /dev/null and b/public/logo/64x64.png differ diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..080019b --- /dev/null +++ b/src/App.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/assets/logo.png b/src/assets/logo.png new file mode 100644 index 0000000..1f6665c Binary files /dev/null and b/src/assets/logo.png differ diff --git a/src/layout/compoenets/AppMain.vue b/src/layout/compoenets/AppMain.vue new file mode 100644 index 0000000..01b33d9 --- /dev/null +++ b/src/layout/compoenets/AppMain.vue @@ -0,0 +1,22 @@ + + diff --git a/src/layout/compoenets/Breadcrumb.vue b/src/layout/compoenets/Breadcrumb.vue new file mode 100644 index 0000000..48091e8 --- /dev/null +++ b/src/layout/compoenets/Breadcrumb.vue @@ -0,0 +1,28 @@ + + + diff --git a/src/layout/compoenets/LeftMenu.vue b/src/layout/compoenets/LeftMenu.vue new file mode 100644 index 0000000..917bd25 --- /dev/null +++ b/src/layout/compoenets/LeftMenu.vue @@ -0,0 +1,54 @@ + + + diff --git a/src/layout/index.vue b/src/layout/index.vue new file mode 100644 index 0000000..957aa21 --- /dev/null +++ b/src/layout/index.vue @@ -0,0 +1,15 @@ + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a2afdf3 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,13 @@ +import { createApp } from "vue"; +import App from "./App.vue"; +import router from "./router"; +import "./styles/index.scss"; +import ElementPlus from "element-plus"; + +createApp(App) + .use(router) + .use(ElementPlus) + .mount("#app") + .$nextTick(() => { + postMessage({ payload: "removeLoading" }, "*"); + }); diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..af55e5b --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,82 @@ +import { createRouter, createWebHistory, createWebHashHistory, RouteRecordRaw } from "vue-router"; + +const Layout = () => import("@/layout/index.vue"); + +const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "Index", + component: Layout, + redirect: "/home", + children: [ + { + path: "/home", + name: "Home", + meta: { + title: "连接", + icon: "material-symbols:rocket-launch-rounded", + keepAlive: true + }, + component: () => import("@/views/home/index.vue") + }, + { + path: "/proxy", + name: "Proxy", + meta: { + title: "穿透列表", + icon: "material-symbols:cloud", + keepAlive: true + }, + component: () => import("@/views/proxy/index.vue") + }, + { + path: "/download", + name: "Download", + meta: { + title: "版本下载", + icon: "material-symbols:download-2", + keepAlive: true + }, + component: () => import("@/views/download/index.vue") + }, + { + path: "/config", + name: "Config", + meta: { + title: "系统配置", + icon: "material-symbols:settings", + keepAlive: true + }, + component: () => import("@/views/config/index.vue") + }, + { + path: "/logger", + name: "Logger", + meta: { + title: "日志", + icon: "material-symbols:file-copy-sharp", + keepAlive: true, + hidden: false + }, + component: () => import("@/views/logger/index.vue") + } + // { + // path: "/comingSoon", + // name: "ComingSoon", + // meta: { + // title: "敬请期待", + // icon: "material-symbols:file-copy-sharp", + // keepAlive: false, + // hidden: true + // }, + // component: () => import("@/views/comingSoon/index.vue") + // } + ] + } +]; + +const router = createRouter({ + history: createWebHashHistory(), + routes +}); +export default router; diff --git a/src/styles/element.scss b/src/styles/element.scss new file mode 100644 index 0000000..c3f9dad --- /dev/null +++ b/src/styles/element.scss @@ -0,0 +1,9 @@ +@forward 'element-plus/theme-chalk/src/common/var.scss' with ( + $colors: ( + 'primary': ( + 'base': #5F3BB0, + ), + ), +); + +@use "element-plus/theme-chalk/src/index.scss" as *; diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 0000000..892b598 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,21 @@ +@import "reset"; +@import "layout"; +@import "element"; +@import "tailwind"; +/* 自定义全局 CssVar */ +:root { + --pure-transition-duration: 0.016s; +} + +/* 灰色模式 */ +.html-grey { + filter: grayscale(100%); +} + +/* 色弱模式 */ +.html-weakness { + filter: invert(80%); +} + +html { +} diff --git a/src/styles/layout.scss b/src/styles/layout.scss new file mode 100644 index 0000000..8970772 --- /dev/null +++ b/src/styles/layout.scss @@ -0,0 +1,86 @@ +$main-bg: #F3F3F3; +$primary-color: #5F3BB0; + +.main-container { + background: $main-bg; + width: calc(100% - 60px); + height: 100vh; + padding: 20px; + + .main { + height: 100%; + width: 100%; + overflow: hidden; + } + + .app-container-breadcrumb { + height: calc(100% - 50px); + overflow-y: auto; + overflow-x: hidden; + } + + .breadcrumb { + color: $primary-color; + font-size: 36px; + height: 50px; + + svg { + vertical-align: top; + } + + span { + vertical-align: top; + color: black; + font-size: 18px; + font-weight: bold; + transform: translateY(5px); + display: inline-block; + + } + } +} + +.left-menu-container { + background: #fff; + width: 60px; + height: 100vh; + + .menu-container { + + .menu { + display: flex; + height: 40px; + justify-content: center; + align-items: center; + font-size: 20px; + color: #ADADAD; + margin-bottom: 10px; + } + + .menu-selected { + background: #EBE6F7; + color: $primary-color; + } + + .menu:hover { + color: $primary-color; + cursor: pointer; + } + } +} + +.logo-container { + height: 60px; + display: flex; + justify-content: center; + align-items: center; + + .logo { + width: 60%; + } +} + + +.primary-text { + color: $primary-color; +} diff --git a/src/styles/reset.scss b/src/styles/reset.scss new file mode 100644 index 0000000..9d1d252 --- /dev/null +++ b/src/styles/reset.scss @@ -0,0 +1,275 @@ +*, +::before, +::after { + box-sizing: border-box; + border-width: 0; + border-style: solid; + border-color: currentColor; +} + +*{ + -webkit-touch-callout:none; /*系统默认菜单被禁用*/ + -webkit-user-select:none; /*webkit浏览器*/ + -khtml-user-select:none; /*早期浏览器*/ + -moz-user-select:none;/*火狐*/ + -ms-user-select:none; /*IE10*/ + user-select:none; +} + +input{ + -webkit-user-select:auto; /*webkit浏览器*/ +} +textarea{ + -webkit-user-select:auto; /*webkit浏览器*/ +} + +#app { + width: 100%; + height: 100%; +} + +html { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + -moz-tab-size: 4; + tab-size: 4; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +body { + margin: 0; + line-height: inherit; + width: 100%; + height: 100%; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + text-rendering: optimizelegibility; + font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", + "Microsoft YaHei", "微软雅黑", Arial, sans-serif; +} + +hr { + height: 0; + color: inherit; + border-top-width: 1px; +} + +abbr:where([title]) { + text-decoration: underline dotted; +} + +a { + color: inherit; + text-decoration: inherit; +} + +b, +strong { + font-weight: bolder; +} + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, + "Liberation Mono", "Courier New", monospace; + font-size: 1em; +} + +small { + font-size: 80%; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; +} + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: inherit; + color: inherit; + margin: 0; + padding: 0; +} + +button, +select { + text-transform: none; +} + +button, +[type="button"], +[type="reset"], +[type="submit"] { + background-image: none; +} + +:-moz-focusring { + outline: auto; +} + +:-moz-ui-invalid { + box-shadow: none; +} + +progress { + vertical-align: baseline; +} + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +[type="search"] { + outline-offset: -2px; +} + +::-webkit-file-upload-button { + font: inherit; +} + +summary { + display: list-item; +} + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +textarea { + resize: vertical; +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + color: #9ca3af; +} + +button, +[role="button"] { + cursor: pointer; +} + +:disabled { + cursor: default; +} + +//img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; +} + +img, +video { + max-width: 100%; + height: auto; +} + +[hidden] { + display: none; +} + +.dark { + color-scheme: dark; +} + +label { + font-weight: 700; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +a:focus, +a:active { + outline: none; +} + +a, +a:focus, +a:hover { + cursor: pointer; + color: inherit; + text-decoration: none; +} + +div:focus { + outline: none; +} + +.clearfix { + &::after { + visibility: hidden; + display: block; + font-size: 0; + content: " "; + clear: both; + height: 0; + } +} diff --git a/src/styles/tailwind.scss b/src/styles/tailwind.scss new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/styles/tailwind.scss @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..277926a --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,9 @@ +declare module 'element-plus/dist/locale/zh-cn.mjs' { + const zhLocale: any; + export default zhLocale; +} + +declare module 'element-plus/dist/locale/en.mjs' { + const enLocale: any; + export default enLocale; +} diff --git a/src/types/shims-tsx.d.ts b/src/types/shims-tsx.d.ts new file mode 100644 index 0000000..199f979 --- /dev/null +++ b/src/types/shims-tsx.d.ts @@ -0,0 +1,22 @@ +import Vue, { VNode } from "vue"; + +declare module "*.tsx" { + import Vue from "compatible-vue"; + export default Vue; +} + +declare global { + namespace JSX { + interface Element extends VNode {} + interface ElementClass extends Vue {} + interface ElementAttributesProperty { + $props: any; + } + interface IntrinsicElements { + [elem: string]: any; + } + interface IntrinsicAttributes { + [elem: string]: any; + } + } +} diff --git a/src/types/shims-vue.d.ts b/src/types/shims-vue.d.ts new file mode 100644 index 0000000..9fa8db3 --- /dev/null +++ b/src/types/shims-vue.d.ts @@ -0,0 +1,10 @@ +declare module "*.vue" { + import { DefineComponent } from "vue"; + const component: DefineComponent<{}, {}, any>; + export default component; +} + +declare module "*.scss" { + const scss: Record; + export default scss; +} diff --git a/src/utils/clone.ts b/src/utils/clone.ts new file mode 100644 index 0000000..f40201b --- /dev/null +++ b/src/utils/clone.ts @@ -0,0 +1,19 @@ +export function clone(value: T): T { + /** 空 */ + if (!value) return value; + /** 数组 */ + if (Array.isArray(value)) + return value.map(item => clone(item)) as unknown as T; + /** 日期 */ + if (value instanceof Date) return new Date(value) as unknown as T; + /** 普通对象 */ + if (typeof value === "object") { + return Object.fromEntries( + Object.entries(value).map(([k, v]: [string, any]) => { + return [k, clone(v)]; + }) + ) as unknown as T; + } + /** 基本类型 */ + return value; +} diff --git a/src/views/comingSoon/index.vue b/src/views/comingSoon/index.vue new file mode 100644 index 0000000..229688d --- /dev/null +++ b/src/views/comingSoon/index.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/views/config/index.vue b/src/views/config/index.vue new file mode 100644 index 0000000..8f4284c --- /dev/null +++ b/src/views/config/index.vue @@ -0,0 +1,203 @@ + + + + diff --git a/src/views/download/index.vue b/src/views/download/index.vue new file mode 100644 index 0000000..75042a4 --- /dev/null +++ b/src/views/download/index.vue @@ -0,0 +1,116 @@ + + + + diff --git a/src/views/home/index.vue b/src/views/home/index.vue new file mode 100644 index 0000000..7a5526e --- /dev/null +++ b/src/views/home/index.vue @@ -0,0 +1,199 @@ + + + + + diff --git a/src/views/logger/index.vue b/src/views/logger/index.vue new file mode 100644 index 0000000..a24402e --- /dev/null +++ b/src/views/logger/index.vue @@ -0,0 +1,67 @@ + + + + diff --git a/src/views/proxy/index.vue b/src/views/proxy/index.vue new file mode 100644 index 0000000..02a1e99 --- /dev/null +++ b/src/views/proxy/index.vue @@ -0,0 +1,487 @@ + + + + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..11bb37f --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,20 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: "class", + corePlugins: { + preflight: false + }, + content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + bg_color: "var(--el-bg-color)", + primary: "var(--el-color-primary)", + primary_light_9: "var(--el-color-primary-light-9)", + text_color_primary: "var(--el-text-color-primary)", + text_color_regular: "var(--el-text-color-regular)", + text_color_disabled: "var(--el-text-color-disabled)" + } + } + } +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..afca1ed --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,49 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "moduleResolution": "Node", + "strict": false, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": [ + "ESNext", + "DOM" + ], + "skipLibCheck": true, + "noEmit": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "src/*" + ] + }, + "types": [ + "node", + "vite/client", + "element-plus/global" + ], + "typeRoots": [ + "./types", + "./node_modules/@types/", + "./node_modules" + ] + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/**/*.vue", + "types/*.d.ts", + "package.json", + "electron", + "vite.config.ts" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.js" + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..c8b26b9 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,105 @@ +import { rmSync } from "node:fs"; +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import electron from "vite-plugin-electron"; +import renderer from "vite-plugin-electron-renderer"; +import { notBundle } from "vite-plugin-electron/plugin"; +import { resolve } from "path"; + + +import pkg from "./package.json"; + +/** 路径查找 */ +const pathResolve = (dir: string): string => { + return resolve(__dirname, ".", dir); +}; + +// https://vitejs.dev/config/ +export default defineConfig(({ command }) => { + rmSync("dist-electron", { recursive: true, force: true }); + + const isServe = command === "serve"; + const isBuild = command === "build"; + const sourcemap = isServe || !!process.env.VSCODE_DEBUG; + + return { + plugins: [ + vue(), + electron([ + { + // Main process entry file of the Electron App. + entry: "electron/main/index.ts", + onstart({ startup }) { + if (process.env.VSCODE_DEBUG) { + console.log( + /* For `.vscode/.debug.script.mjs` */ "[startup] Electron App" + ); + } else { + startup(); + } + }, + vite: { + build: { + sourcemap, + minify: isBuild, + outDir: "dist-electron/main", + rollupOptions: { + // Some third-party Node.js libraries may not be built correctly by Vite, especially `C/C++` addons, + // we can use `external` to exclude them to ensure they work correctly. + // Others need to put them in `dependencies` to ensure they are collected into `app.asar` after the app is built. + // Of course, this is not absolute, just this way is relatively simple. :) + external: Object.keys( + "dependencies" in pkg ? pkg.dependencies : {} + ) + } + }, + plugins: [ + // This is just an option to improve build performance, it's non-deterministic! + // e.g. `import log from 'electron-log'` -> `const log = require('electron-log')` + isServe && notBundle() + ] + } + }, + { + entry: "electron/preload/index.ts", + onstart({ reload }) { + // Notify the Renderer process to reload the page when the Preload scripts build is complete, + // instead of restarting the entire Electron App. + reload(); + }, + vite: { + build: { + sourcemap: sourcemap ? "inline" : undefined, // #332 + minify: isBuild, + outDir: "dist-electron/preload", + rollupOptions: { + external: Object.keys( + "dependencies" in pkg ? pkg.dependencies : {} + ) + } + }, + plugins: [isServe && notBundle()] + } + } + ]), + // Use Node.js API in the Renderer process + renderer() + ], + resolve: { + alias: { + "@": pathResolve("src"), + "@build": pathResolve("build") + } + }, + server: + process.env.VSCODE_DEBUG && + (() => { + const url = new URL(pkg.debug.env.VITE_DEV_SERVER_URL); + return { + host: url.hostname, + port: +url.port + }; + })(), + clearScreen: false + }; +});