Merge pull request #535 from tw93/dev

merge: Cli support inject js/css files to app and set app safe domains
- Inject css/js files to app
- Support setting app safeDomains
- More comfortable development mode for CLI power users
This commit is contained in:
santree
2023-08-03 16:31:16 +08:00
committed by GitHub
29 changed files with 822 additions and 565 deletions

1
.gitattributes vendored
View File

@@ -8,3 +8,4 @@ script/* linguist-vendored
/icns2png.py linguist-vendored /icns2png.py linguist-vendored
/rollup.config.js linguist-vendored /rollup.config.js linguist-vendored
src-tauri/src/inject/* linguist-vendored src-tauri/src/inject/* linguist-vendored
src-tauri/src/.pake/* linguist-vendored

View File

@@ -43,7 +43,7 @@ jobs:
packages: libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra packages: libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra
version: 1.0 version: 1.0
- name: Run unit & integration tests with nextest - name: Run unit & integration tests with nextest
run: cargo hack --feature-powerset nextest run run: cargo hack --feature-powerset --exclude-features cli-build nextest run
# - name: Run documentation tests with cargo test # - name: Run documentation tests with cargo test
# run: cargo hack --feature-powerset test --doc # run: cargo hack --feature-powerset test --doc
@@ -70,7 +70,7 @@ jobs:
packages: libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra packages: libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra
version: 1.0 version: 1.0
- name: Run all-features code quality checks - name: Run all-features code quality checks
run: cargo hack --feature-powerset --no-dev-deps clippy run: cargo hack --feature-powerset --exclude-features cli-build --no-dev-deps clippy
- name: Run normal code quality check - name: Run normal code quality check
run: cargo clippy run: cargo clippy

1
.gitignore vendored
View File

@@ -37,3 +37,4 @@ dist
src-tauri/.cargo/config src-tauri/.cargo/config
src-tauri/.cargo/ src-tauri/.cargo/
.next .next
src-tauri/.pake/

View File

@@ -71,6 +71,10 @@ export default abstract class BaseBuilder {
await this.buildAndCopy(url, this.options.targets); await this.buildAndCopy(url, this.options.targets);
} }
async start(url: string) {
await mergeConfig(url, this.options, tauriConfig);
}
async buildAndCopy(url: string, target: string) { async buildAndCopy(url: string, target: string) {
const { name } = this.options; const { name } = this.options;
await mergeConfig(url, this.options, tauriConfig); await mergeConfig(url, this.options, tauriConfig);
@@ -98,7 +102,8 @@ export default abstract class BaseBuilder {
abstract getFileName(): string; abstract getFileName(): string;
protected getBuildCommand(): string { protected getBuildCommand(): string {
return 'npm run build'; // the debug option should support `--debug` and `--release`
return this.options.debug ? 'npm run build:debug' : 'npm run build';
} }
protected getBasePath(): string { protected getBasePath(): string {

2
bin/cli.ts vendored
View File

@@ -38,6 +38,8 @@ program
.option('--iter-copy-file', 'Copy files when URL is a local file', DEFAULT.iterCopyFile) .option('--iter-copy-file', 'Copy files when URL is a local file', DEFAULT.iterCopyFile)
.option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT.multiArch) .option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT.multiArch)
.option('--targets <string>', 'Only for Linux, option "deb" or "appimage"', DEFAULT.targets) .option('--targets <string>', 'Only for Linux, option "deb" or "appimage"', DEFAULT.targets)
.option('--inject [injects...]', 'Inject .js or .css for this app', DEFAULT.inject)
.option('--safe-domain [domains...]', 'Please enter the security domains that need to be configured', DEFAULT.safeDomain)
.option('--debug', 'Debug mode', DEFAULT.debug) .option('--debug', 'Debug mode', DEFAULT.debug)
.version(packageJson.version, '-v, --version', 'Output the current version') .version(packageJson.version, '-v, --version', 'Output the current version')
.action(async (url: string, options: PakeCliOptions) => { .action(async (url: string, options: PakeCliOptions) => {

9
bin/defaults.ts vendored
View File

@@ -15,4 +15,13 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
iterCopyFile: false, iterCopyFile: false,
systemTrayIcon: '', systemTrayIcon: '',
debug: false, debug: false,
inject: [],
safeDomain: [],
}; };
// just for cli development
export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & {url: string} = {
...DEFAULT_PAKE_OPTIONS,
url: 'https://weread.qq.com',
name: 'Weread',
}

17
bin/dev.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import log from 'loglevel';
import {DEFAULT_DEV_PAKE_OPTIONS} from './defaults';
import handleInputOptions from './options/index';
import BuilderProvider from './builders/BuilderProvider';
async function startBuild() {
log.setDefaultLevel('debug');
const appOptions = await handleInputOptions(DEFAULT_DEV_PAKE_OPTIONS, DEFAULT_DEV_PAKE_OPTIONS.url);
log.debug('PakeAppOptions', appOptions);
const builder = BuilderProvider.create(appOptions);
await builder.prepare();
await builder.start(DEFAULT_DEV_PAKE_OPTIONS.url);
}
startBuild();

75
bin/helpers/merge.ts vendored
View File

@@ -2,8 +2,10 @@ import path from 'path';
import fsExtra from 'fs-extra'; import fsExtra from 'fs-extra';
import { npmDirectory } from '@/utils/dir'; import { npmDirectory } from '@/utils/dir';
import combineFiles from '@/utils/combine';
import logger from '@/options/logger'; import logger from '@/options/logger';
import { PakeAppOptions, PlatformMap } from '@/types'; import { PakeAppOptions, PlatformMap } from '@/types';
import { tauriConfigDirectory } from '../utils/dir';
export async function mergeConfig(url: string, options: PakeAppOptions, tauriConf: any) { export async function mergeConfig(url: string, options: PakeAppOptions, tauriConf: any) {
const { const {
@@ -19,6 +21,8 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon
identifier, identifier,
name, name,
resizable = true, resizable = true,
inject,
safeDomain,
} = options; } = options;
const { platform } = process; const { platform } = process;
@@ -57,9 +61,7 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon
const filesToCopyBack = ['cli.js', 'about_pake.html']; const filesToCopyBack = ['cli.js', 'about_pake.html'];
await Promise.all( await Promise.all(
filesToCopyBack.map(file => filesToCopyBack.map(file => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))),
fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file)),
),
); );
} }
@@ -68,7 +70,24 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon
} else { } else {
tauriConf.pake.windows[0].url_type = 'web'; tauriConf.pake.windows[0].url_type = 'web';
// Set the secure domain for calling window.__TAURI__ to the application domain that has been set. // Set the secure domain for calling window.__TAURI__ to the application domain that has been set.
tauriConf.tauri.security.dangerousRemoteDomainIpcAccess[0].domain = new URL(url).hostname; tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [
{
domain: new URL(url).hostname,
windows: ['pake'],
enableTauriAPI: true,
},
];
}
if (safeDomain.length > 0) {
tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [
...tauriConf.tauri.security.dangerousRemoteDomainIpcAccess,
...safeDomain.map(domain => ({
domain,
windows: ['pake'],
enableTauriAPI: true,
})),
];
} }
const platformMap: PlatformMap = { const platformMap: PlatformMap = {
@@ -90,12 +109,9 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon
delete tauriConf.tauri.bundle.deb.files; delete tauriConf.tauri.bundle.deb.files;
const validTargets = ['all', 'deb', 'appimage']; const validTargets = ['all', 'deb', 'appimage'];
if (validTargets.includes(options.targets)) { if (validTargets.includes(options.targets)) {
tauriConf.tauri.bundle.targets = tauriConf.tauri.bundle.targets = options.targets === 'all' ? ['deb', 'appimage'] : [options.targets];
options.targets === 'all' ? ['deb', 'appimage'] : [options.targets];
} else { } else {
logger.warn( logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`,
);
} }
} }
@@ -154,10 +170,7 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon
// 需要判断图标格式默认只支持ico和png两种 // 需要判断图标格式默认只支持ico和png两种
let iconExt = path.extname(systemTrayIcon).toLowerCase(); let iconExt = path.extname(systemTrayIcon).toLowerCase();
if (iconExt == '.png' || iconExt == '.ico') { if (iconExt == '.png' || iconExt == '.ico') {
const trayIcoPath = path.join( const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
npmDirectory,
`src-tauri/png/${name.toLowerCase()}${iconExt}`,
);
trayIconPath = `png/${name.toLowerCase()}${iconExt}`; trayIconPath = `png/${name.toLowerCase()}${iconExt}`;
await fsExtra.copy(systemTrayIcon, trayIcoPath); await fsExtra.copy(systemTrayIcon, trayIcoPath);
} else { } else {
@@ -171,24 +184,40 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon
} }
tauriConf.tauri.systemTray.iconPath = trayIconPath; tauriConf.tauri.systemTray.iconPath = trayIconPath;
const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
// inject js or css files
if (inject?.length > 0) {
if (!inject.every(item => item.endsWith('.css') || item.endsWith('.js'))) {
logger.error('The injected file must be in either CSS or JS format.');
return;
}
const files = inject.map(filepath => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
tauriConf.pake.inject = files;
await combineFiles(files, injectFilePath);
} else {
tauriConf.pake.inject = [];
await fsExtra.writeFile(injectFilePath, '');
}
// Save config file. // Save config file.
const platformConfigPaths: PlatformMap = { const platformConfigPaths: PlatformMap = {
win32: 'src-tauri/tauri.windows.conf.json', win32: 'tauri.windows.conf.json',
darwin: 'src-tauri/tauri.macos.conf.json', darwin: 'tauri.macos.conf.json',
linux: 'src-tauri/tauri.linux.conf.json', linux: 'tauri.linux.conf.json',
}; };
const configPath = path.join(npmDirectory, platformConfigPaths[platform]); const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]);
const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } }; const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } };
await fsExtra.writeJson(configPath, bundleConf, { spaces: 4 }); await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json'); await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
await fsExtra.writeJson(pakeConfigPath, tauriConf.pake, { spaces: 4 });
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf)); let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake; delete tauriConf2.pake;
delete tauriConf2.tauri.bundle; delete tauriConf2.tauri.bundle;
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json'); if (process.env.NODE_ENV === 'development') {
await fsExtra.writeJson(configJsonPath, tauriConf2, { spaces: 4 }); tauriConf2.tauri.bundle = bundleConf.tauri.bundle;
}
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
} }

6
bin/types.ts vendored
View File

@@ -47,6 +47,12 @@ export interface PakeCliOptions {
// Debug mode, outputs more logs // Debug mode, outputs more logs
debug: boolean; debug: boolean;
/** 需要注入页面的外部脚本 */
inject: string[];
/* the domain that can use ipc or tauri javascript sdk */
safeDomain: string[];
} }
export interface PakeAppOptions extends PakeCliOptions { export interface PakeAppOptions extends PakeCliOptions {

14
bin/utils/combine.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import fs from 'fs';
export default async function combineFiles(files: string[], output: string) {
const contents = files.map(file => {
const fileContent = fs.readFileSync(file);
if (file.endsWith('.css')) {
return "window.addEventListener('DOMContentLoaded', (_event) => { const css = `" + fileContent + "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });";
}
return "window.addEventListener('DOMContentLoaded', (_event) => { " + fileContent + " });";
});
fs.writeFileSync(output, contents.join('\n'));
return files;
}

2
bin/utils/dir.ts vendored
View File

@@ -6,3 +6,5 @@ const currentModulePath = fileURLToPath(import.meta.url);
// Resolve the parent directory of the current module // Resolve the parent directory of the current module
export const npmDirectory = path.join(path.dirname(currentModulePath), '..'); export const npmDirectory = path.join(path.dirname(currentModulePath), '..');
export const tauriConfigDirectory = process.env.NODE_ENV === 'development' ? path.join(npmDirectory, 'src-tauri', '.pake') : path.join(npmDirectory, 'src-tauri');

0
dist/.gitkeep vendored
View File

16
dist/about_pake.html vendored
View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h5>Welcome from Pake!</h5>
<p>version: 1.0.9</p>
<a href="https://github.com/tw93/Pake">Project link</a><br>
<a href="https://github.com/tw93/Pake/discussions">Discussions</a><br>
<a href="https://github.com/tw93/Pake/issues" >Issues</a><br>
<p>LICENSE: MIT</p>
</body>
</html>

779
dist/cli.js vendored
View File

@@ -1,307 +1,307 @@
import chalk from 'chalk'; import chalk from 'chalk';
import { InvalidArgumentError, program } from 'commander'; import { InvalidArgumentError, program } from 'commander';
import log from 'loglevel'; import log from 'loglevel';
import path from 'path'; import path from 'path';
import fsExtra from 'fs-extra'; import fsExtra from 'fs-extra';
import prompts from 'prompts'; import prompts from 'prompts';
import shelljs from 'shelljs'; import shelljs from 'shelljs';
import crypto from 'crypto'; import crypto from 'crypto';
import ora from 'ora'; import ora from 'ora';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import dns from 'dns'; import dns from 'dns';
import http from 'http'; import http from 'http';
import { promisify } from 'util'; import { promisify } from 'util';
import updateNotifier from 'update-notifier'; import updateNotifier from 'update-notifier';
import axios from 'axios'; import axios from 'axios';
import { dir } from 'tmp-promise'; import { dir } from 'tmp-promise';
import { fileTypeFromBuffer } from 'file-type'; import { fileTypeFromBuffer } from 'file-type';
import psl from 'psl'; import psl from 'psl';
import isUrl from 'is-url'; import isUrl from 'is-url';
import fs from 'fs'; import fs from 'fs';
var name = "pake-cli"; var name = "pake-cli";
var version = "2.2.5"; var version = "2.2.5";
var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 很简单的用 Rust 打包网页生成很小的桌面 App。"; var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 很简单的用 Rust 打包网页生成很小的桌面 App。";
var engines = { var engines = {
node: ">=16.0.0" node: ">=16.0.0"
}; };
var bin = { var bin = {
pake: "./cli.js" pake: "./cli.js"
}; };
var repository = { var repository = {
type: "git", type: "git",
url: "https://github.com/tw93/pake.git" url: "https://github.com/tw93/pake.git"
}; };
var author = { var author = {
name: "Tw93", name: "Tw93",
email: "tw93@qq.com" email: "tw93@qq.com"
}; };
var keywords = [ var keywords = [
"pake", "pake",
"pake-cli", "pake-cli",
"rust", "rust",
"tauri", "tauri",
"no-electron", "no-electron",
"productivity" "productivity"
]; ];
var files = [ var files = [
"dist", "dist",
"src-tauri", "src-tauri",
"cli.js" "cli.js"
]; ];
var scripts = { var scripts = {
start: "npm run dev", start: "npm run dev",
dev: "npm run tauri dev", dev: "npm run tauri dev",
build: "npm run tauri build --release", build: "npm run tauri build --release",
"build:mac": "npm run tauri build -- --target universal-apple-darwin", "build:mac": "npm run tauri build -- --target universal-apple-darwin",
"build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh", "build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh",
"build:all-windows": "pwsh ./script/build.ps1", "build:all-windows": "pwsh ./script/build.ps1",
analyze: "cd src-tauri && cargo bloat --release --crates", analyze: "cd src-tauri && cargo bloat --release --crates",
tauri: "tauri", tauri: "tauri",
cli: "rollup -c rollup.config.js --watch", cli: "rollup -c rollup.config.js --watch",
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
prepublishOnly: "npm run cli:build" prepublishOnly: "npm run cli:build"
}; };
var type = "module"; var type = "module";
var exports = "./dist/pake.js"; var exports = "./dist/pake.js";
var license = "MIT"; var license = "MIT";
var dependencies = { var dependencies = {
"@tauri-apps/api": "^1.4.0", "@tauri-apps/api": "^1.4.0",
"@tauri-apps/cli": "^1.4.0", "@tauri-apps/cli": "^1.4.0",
axios: "^1.1.3", axios: "^1.1.3",
chalk: "^5.1.2", chalk: "^5.1.2",
commander: "^11.0.0", commander: "^11.0.0",
"file-type": "^18.0.0", "file-type": "^18.0.0",
"fs-extra": "^11.1.0", "fs-extra": "^11.1.0",
"is-url": "^1.2.4", "is-url": "^1.2.4",
loglevel: "^1.8.1", loglevel: "^1.8.1",
ora: "^6.1.2", ora: "^6.1.2",
prompts: "^2.4.2", prompts: "^2.4.2",
psl: "^1.9.0", psl: "^1.9.0",
shelljs: "^0.8.5", shelljs: "^0.8.5",
"tmp-promise": "^3.0.3", "tmp-promise": "^3.0.3",
"update-notifier": "^6.0.2" "update-notifier": "^6.0.2"
}; };
var devDependencies = { var devDependencies = {
"@rollup/plugin-alias": "^4.0.2", "@rollup/plugin-alias": "^4.0.2",
"@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^5.0.2", "@rollup/plugin-json": "^5.0.2",
"@rollup/plugin-terser": "^0.1.0", "@rollup/plugin-terser": "^0.1.0",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/is-url": "^1.2.30", "@types/is-url": "^1.2.30",
"@types/page-icon": "^0.3.4", "@types/page-icon": "^0.3.4",
"@types/prompts": "^2.4.1", "@types/prompts": "^2.4.1",
"@types/psl": "^1.1.0", "@types/psl": "^1.1.0",
"@types/shelljs": "^0.8.11", "@types/shelljs": "^0.8.11",
"@types/tmp": "^0.2.3", "@types/tmp": "^0.2.3",
"@types/update-notifier": "^6.0.1", "@types/update-notifier": "^6.0.1",
"app-root-path": "^3.1.0", "app-root-path": "^3.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
rollup: "^3.3.0", rollup: "^3.3.0",
"rollup-plugin-typescript2": "^0.34.1", "rollup-plugin-typescript2": "^0.34.1",
tslib: "^2.4.1", tslib: "^2.4.1",
typescript: "^4.9.3" typescript: "^4.9.3"
}; };
var packageJson = { var packageJson = {
name: name, name: name,
version: version, version: version,
description: description, description: description,
engines: engines, engines: engines,
bin: bin, bin: bin,
repository: repository, repository: repository,
author: author, author: author,
keywords: keywords, keywords: keywords,
files: files, files: files,
scripts: scripts, scripts: scripts,
type: type, type: type,
exports: exports, exports: exports,
license: license, license: license,
dependencies: dependencies, dependencies: dependencies,
devDependencies: devDependencies devDependencies: devDependencies
}; };
var windows = [ var windows = [
{ {
url: "https://weread.qq.com/", url: "https://weread.qq.com/",
transparent: true, transparent: true,
fullscreen: false, fullscreen: false,
width: 1200, width: 1200,
height: 780, height: 780,
resizable: true, resizable: true,
url_type: "web" url_type: "web"
} }
]; ];
var user_agent = { var user_agent = {
macos: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15", macos: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
}; };
var menu = { var menu = {
macos: true, macos: true,
linux: false, linux: false,
windows: false windows: false
}; };
var system_tray = { var system_tray = {
macos: false, macos: false,
linux: true, linux: true,
windows: true windows: true
}; };
var pakeConf = { var pakeConf = {
windows: windows, windows: windows,
user_agent: user_agent, user_agent: user_agent,
menu: menu, menu: menu,
system_tray: system_tray system_tray: system_tray
}; };
var tauri$3 = { var tauri$3 = {
security: { security: {
csp: null, csp: null,
dangerousRemoteDomainIpcAccess: [ dangerousRemoteDomainIpcAccess: [
{ {
domain: "weread.qq.com", domain: "weread.qq.com",
windows: [ windows: [
"pake" "pake"
], ],
enableTauriAPI: true enableTauriAPI: true
} }
] ]
}, },
updater: { updater: {
active: false active: false
}, },
systemTray: { systemTray: {
iconPath: "png/weread_512.png", iconPath: "png/weread_512.png",
iconAsTemplate: true iconAsTemplate: true
}, },
allowlist: { allowlist: {
all: true, all: true,
fs: { fs: {
all: true, all: true,
scope: [ scope: [
"$DOWNLOAD/*" "$DOWNLOAD/*"
] ]
} }
} }
}; };
var build = { var build = {
withGlobalTauri: true, withGlobalTauri: true,
devPath: "../dist", devPath: "../dist",
distDir: "../dist", distDir: "../dist",
beforeBuildCommand: "", beforeBuildCommand: "",
beforeDevCommand: "" beforeDevCommand: ""
}; };
var CommonConf = { var CommonConf = {
"package": { "package": {
productName: "WeRead", productName: "WeRead",
version: "1.0.0" version: "1.0.0"
}, },
tauri: tauri$3, tauri: tauri$3,
build: build build: build
}; };
var tauri$2 = { var tauri$2 = {
bundle: { bundle: {
icon: [ icon: [
"png/weread_256.ico", "png/weread_256.ico",
"png/weread_32.ico" "png/weread_32.ico"
], ],
identifier: "com.pake.weread", identifier: "com.pake.weread",
active: true, active: true,
category: "DeveloperTool", category: "DeveloperTool",
copyright: "", copyright: "",
externalBin: [ externalBin: [
], ],
longDescription: "", longDescription: "",
resources: [ resources: [
"png/weread_32.ico" "png/weread_32.ico"
], ],
shortDescription: "", shortDescription: "",
targets: [ targets: [
"msi" "msi"
], ],
windows: { windows: {
certificateThumbprint: null, certificateThumbprint: null,
digestAlgorithm: "sha256", digestAlgorithm: "sha256",
timestampUrl: "", timestampUrl: "",
wix: { wix: {
language: [ language: [
"en-US" "en-US"
], ],
template: "assets/main.wxs" template: "assets/main.wxs"
} }
} }
} }
}; };
var WinConf = { var WinConf = {
tauri: tauri$2 tauri: tauri$2
}; };
var tauri$1 = { var tauri$1 = {
bundle: { bundle: {
icon: [ icon: [
"icons/weread.icns" "icons/weread.icns"
], ],
identifier: "com.pake.weread", identifier: "com.pake.weread",
active: true, active: true,
category: "DeveloperTool", category: "DeveloperTool",
copyright: "", copyright: "",
externalBin: [ externalBin: [
], ],
longDescription: "", longDescription: "",
macOS: { macOS: {
entitlements: null, entitlements: null,
exceptionDomain: "", exceptionDomain: "",
frameworks: [ frameworks: [
], ],
providerShortName: null, providerShortName: null,
signingIdentity: null signingIdentity: null
}, },
resources: [ resources: [
], ],
shortDescription: "", shortDescription: "",
targets: [ targets: [
"dmg" "dmg"
] ]
} }
}; };
var MacConf = { var MacConf = {
tauri: tauri$1 tauri: tauri$1
}; };
var tauri = { var tauri = {
bundle: { bundle: {
icon: [ icon: [
"png/weread_512.png" "png/weread_512.png"
], ],
identifier: "com.pake.weread", identifier: "com.pake.weread",
active: true, active: true,
category: "DeveloperTool", category: "DeveloperTool",
copyright: "", copyright: "",
deb: { deb: {
depends: [ depends: [
"curl", "curl",
"wget" "wget"
], ],
files: { files: {
"/usr/share/applications/com-pake-weread.desktop": "assets/com-pake-weread.desktop" "/usr/share/applications/com-pake-weread.desktop": "assets/com-pake-weread.desktop"
} }
}, },
externalBin: [ externalBin: [
], ],
longDescription: "", longDescription: "",
resources: [ resources: [
], ],
shortDescription: "", shortDescription: "",
targets: [ targets: [
"deb", "deb",
"appimage" "appimage"
] ]
} }
}; };
var LinuxConf = { var LinuxConf = {
tauri: tauri tauri: tauri
}; };
const platformConfigs = { const platformConfigs = {
win32: WinConf, win32: WinConf,
darwin: MacConf, darwin: MacConf,
@@ -318,8 +318,8 @@ let tauriConfig = {
package: CommonConf.package, package: CommonConf.package,
build: CommonConf.build, build: CommonConf.build,
pake: pakeConf, pake: pakeConf,
}; };
// Generates an identifier based on the given URL. // Generates an identifier based on the given URL.
function getIdentifier(url) { function getIdentifier(url) {
const postFixHash = crypto.createHash('md5').update(url).digest('hex').substring(0, 6); const postFixHash = crypto.createHash('md5').update(url).digest('hex').substring(0, 6);
@@ -347,18 +347,19 @@ function getSpinner(text) {
spinner: loadingType, spinner: loadingType,
color: 'cyan', color: 'cyan',
}).start(); }).start();
} }
const { platform: platform$1 } = process; const { platform: platform$1 } = process;
const IS_MAC = platform$1 === 'darwin'; const IS_MAC = platform$1 === 'darwin';
const IS_WIN = platform$1 === 'win32'; const IS_WIN = platform$1 === 'win32';
const IS_LINUX = platform$1 === 'linux'; const IS_LINUX = platform$1 === 'linux';
// Convert the current module URL to a file path // Convert the current module URL to a file path
const currentModulePath = fileURLToPath(import.meta.url); const currentModulePath = fileURLToPath(import.meta.url);
// Resolve the parent directory of the current module // Resolve the parent directory of the current module
const npmDirectory = path.join(path.dirname(currentModulePath), '..'); const npmDirectory = path.join(path.dirname(currentModulePath), '..');
const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri');
function shellExec(command) { function shellExec(command) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
shelljs.exec(command, { async: true, silent: false, cwd: npmDirectory }, code => { shelljs.exec(command, { async: true, silent: false, cwd: npmDirectory }, code => {
@@ -370,8 +371,8 @@ function shellExec(command) {
} }
}); });
}); });
} }
const logger = { const logger = {
info(...msg) { info(...msg) {
log.info(...msg.map(m => chalk.white(m))); log.info(...msg.map(m => chalk.white(m)));
@@ -388,8 +389,8 @@ const logger = {
success(...msg) { success(...msg) {
log.info(...msg.map(m => chalk.green(m))); log.info(...msg.map(m => chalk.green(m)));
}, },
}; };
const resolve = promisify(dns.resolve); const resolve = promisify(dns.resolve);
const ping = async (host) => { const ping = async (host) => {
const lookup = promisify(dns.lookup); const lookup = promisify(dns.lookup);
@@ -433,8 +434,8 @@ async function isChinaIP(ip, domain) {
logger.debug(`ping ${domain} failed!`); logger.debug(`ping ${domain} failed!`);
return true; return true;
} }
} }
async function installRust() { async function installRust() {
const isActions = process.env.GITHUB_ACTIONS; const isActions = process.env.GITHUB_ACTIONS;
const isInChina = await isChinaDomain('sh.rustup.rs'); const isInChina = await isChinaDomain('sh.rustup.rs');
@@ -455,10 +456,22 @@ async function installRust() {
} }
function checkRustInstalled() { function checkRustInstalled() {
return shelljs.exec('rustc --version', { silent: true }).code === 0; return shelljs.exec('rustc --version', { silent: true }).code === 0;
} }
async function combineFiles(files, output) {
const contents = files.map(file => {
const fileContent = fs.readFileSync(file);
if (file.endsWith('.css')) {
return "window.addEventListener('DOMContentLoaded', (_event) => { const css = `" + fileContent + "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });";
}
return "window.addEventListener('DOMContentLoaded', (_event) => { " + fileContent + " });";
});
fs.writeFileSync(output, contents.join('\n'));
return files;
}
async function mergeConfig(url, options, tauriConf) { async function mergeConfig(url, options, tauriConf) {
const { width, height, fullscreen, transparent, userAgent, showMenu, showSystemTray, systemTrayIcon, iterCopyFile, identifier, name, resizable = true, } = options; const { width, height, fullscreen, transparent, userAgent, showMenu, showSystemTray, systemTrayIcon, iterCopyFile, identifier, name, resizable = true, inject, safeDomain, } = options;
const { platform } = process; const { platform } = process;
// Set Windows parameters. // Set Windows parameters.
const tauriConfWindowOptions = { const tauriConfWindowOptions = {
@@ -496,7 +509,23 @@ async function mergeConfig(url, options, tauriConf) {
else { else {
tauriConf.pake.windows[0].url_type = 'web'; tauriConf.pake.windows[0].url_type = 'web';
// Set the secure domain for calling window.__TAURI__ to the application domain that has been set. // Set the secure domain for calling window.__TAURI__ to the application domain that has been set.
tauriConf.tauri.security.dangerousRemoteDomainIpcAccess[0].domain = new URL(url).hostname; tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [
{
domain: new URL(url).hostname,
windows: ['pake'],
enableTauriAPI: true,
},
];
}
if (safeDomain.length > 0) {
tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [
...tauriConf.tauri.security.dangerousRemoteDomainIpcAccess,
...safeDomain.map(domain => ({
domain,
windows: ['pake'],
enableTauriAPI: true,
})),
];
} }
const platformMap = { const platformMap = {
win32: 'windows', win32: 'windows',
@@ -514,8 +543,7 @@ async function mergeConfig(url, options, tauriConf) {
delete tauriConf.tauri.bundle.deb.files; delete tauriConf.tauri.bundle.deb.files;
const validTargets = ['all', 'deb', 'appimage']; const validTargets = ['all', 'deb', 'appimage'];
if (validTargets.includes(options.targets)) { if (validTargets.includes(options.targets)) {
tauriConf.tauri.bundle.targets = tauriConf.tauri.bundle.targets = options.targets === 'all' ? ['deb', 'appimage'] : [options.targets];
options.targets === 'all' ? ['deb', 'appimage'] : [options.targets];
} }
else { else {
logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`); logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
@@ -591,24 +619,39 @@ async function mergeConfig(url, options, tauriConf) {
} }
} }
tauriConf.tauri.systemTray.iconPath = trayIconPath; tauriConf.tauri.systemTray.iconPath = trayIconPath;
const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/_INJECT_.js`);
// inject js or css files
if (inject?.length > 0) {
if (!inject.every(item => item.endsWith('.css') || item.endsWith('.js'))) {
logger.error('The injected file must be in either CSS or JS format.');
return;
}
const files = inject.map(filepath => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
tauriConf.pake.inject = files;
await combineFiles(files, injectFilePath);
}
else {
tauriConf.pake.inject = [];
await fsExtra.writeFile(injectFilePath, '');
}
// Save config file. // Save config file.
const platformConfigPaths = { const platformConfigPaths = {
win32: 'src-tauri/tauri.windows.conf.json', win32: 'tauri.windows.conf.json',
darwin: 'src-tauri/tauri.macos.conf.json', darwin: 'tauri.macos.conf.json',
linux: 'src-tauri/tauri.linux.conf.json', linux: 'tauri.linux.conf.json',
}; };
const configPath = path.join(npmDirectory, platformConfigPaths[platform]); const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]);
const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } }; const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } };
await fsExtra.writeJson(configPath, bundleConf, { spaces: 4 }); await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json'); const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
await fsExtra.writeJson(pakeConfigPath, tauriConf.pake, { spaces: 4 }); await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf)); let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake; delete tauriConf2.pake;
delete tauriConf2.tauri.bundle; delete tauriConf2.tauri.bundle;
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json'); const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
await fsExtra.writeJson(configJsonPath, tauriConf2, { spaces: 4 }); await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
} }
class BaseBuilder { class BaseBuilder {
constructor(options) { constructor(options) {
this.options = options; this.options = options;
@@ -657,6 +700,9 @@ class BaseBuilder {
async build(url) { async build(url) {
await this.buildAndCopy(url, this.options.targets); await this.buildAndCopy(url, this.options.targets);
} }
async start(url) {
await mergeConfig(url, this.options, tauriConfig);
}
async buildAndCopy(url, target) { async buildAndCopy(url, target) {
const { name } = this.options; const { name } = this.options;
await mergeConfig(url, this.options, tauriConfig); await mergeConfig(url, this.options, tauriConfig);
@@ -678,7 +724,8 @@ class BaseBuilder {
return target; return target;
} }
getBuildCommand() { getBuildCommand() {
return 'npm run build'; // the debug option should support `--debug` and `--release`
return this.options.debug ? 'npm run build:debug' : 'npm run build';
} }
getBasePath() { getBasePath() {
return 'src-tauri/target/release/bundle/'; return 'src-tauri/target/release/bundle/';
@@ -686,8 +733,8 @@ class BaseBuilder {
getBuildAppPath(npmDirectory, fileName, fileType) { getBuildAppPath(npmDirectory, fileName, fileType) {
return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`); return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`);
} }
} }
class MacBuilder extends BaseBuilder { class MacBuilder extends BaseBuilder {
constructor(options) { constructor(options) {
super(options); super(options);
@@ -712,8 +759,8 @@ class MacBuilder extends BaseBuilder {
? 'src-tauri/target/universal-apple-darwin/release/bundle' ? 'src-tauri/target/universal-apple-darwin/release/bundle'
: super.getBasePath(); : super.getBasePath();
} }
} }
class WinBuilder extends BaseBuilder { class WinBuilder extends BaseBuilder {
constructor(options) { constructor(options) {
super(options); super(options);
@@ -725,8 +772,8 @@ class WinBuilder extends BaseBuilder {
const language = tauriConfig.tauri.bundle.windows.wix.language[0]; const language = tauriConfig.tauri.bundle.windows.wix.language[0];
return `${name}_${tauriConfig.package.version}_${arch}_${language}`; return `${name}_${tauriConfig.package.version}_${arch}_${language}`;
} }
} }
class LinuxBuilder extends BaseBuilder { class LinuxBuilder extends BaseBuilder {
constructor(options) { constructor(options) {
super(options); super(options);
@@ -751,8 +798,8 @@ class LinuxBuilder extends BaseBuilder {
} }
return super.getFileType(target); return super.getFileType(target);
} }
} }
const { platform } = process; const { platform } = process;
const buildersMap = { const buildersMap = {
darwin: MacBuilder, darwin: MacBuilder,
@@ -767,8 +814,8 @@ class BuilderProvider {
} }
return new Builder(options); return new Builder(options);
} }
} }
const DEFAULT_PAKE_OPTIONS = { const DEFAULT_PAKE_OPTIONS = {
icon: '', icon: '',
height: 780, height: 780,
@@ -784,12 +831,14 @@ const DEFAULT_PAKE_OPTIONS = {
iterCopyFile: false, iterCopyFile: false,
systemTrayIcon: '', systemTrayIcon: '',
debug: false, debug: false,
}; inject: [],
safeDomain: [],
};
async function checkUpdateTips() { async function checkUpdateTips() {
updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({ isGlobal: true }); updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({ isGlobal: true });
} }
async function handleIcon(options) { async function handleIcon(options) {
if (options.icon) { if (options.icon) {
if (options.icon.startsWith('http')) { if (options.icon.startsWith('http')) {
@@ -842,8 +891,8 @@ async function downloadIcon(iconUrl) {
} }
throw error; throw error;
} }
} }
// Extracts the domain from a given URL. // Extracts the domain from a given URL.
function getDomain(inputUrl) { function getDomain(inputUrl) {
try { try {
@@ -881,8 +930,8 @@ function normalizeUrl(urlToNormalize) {
else { else {
throw new Error(`Your url "${urlWithProtocol}" is invalid`); throw new Error(`Your url "${urlWithProtocol}" is invalid`);
} }
} }
function resolveAppName(name, platform) { function resolveAppName(name, platform) {
const domain = getDomain(name) || 'pake'; const domain = getDomain(name) || 'pake';
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain; return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
@@ -926,8 +975,8 @@ async function handleOptions(options, url) {
}; };
appOptions.icon = await handleIcon(appOptions); appOptions.icon = await handleIcon(appOptions);
return appOptions; return appOptions;
} }
function validateNumberInput(value) { function validateNumberInput(value) {
const parsedValue = Number(value); const parsedValue = Number(value);
if (isNaN(parsedValue)) { if (isNaN(parsedValue)) {
@@ -946,14 +995,14 @@ function validateUrlInput(url) {
} }
} }
return url; return url;
} }
const { green, yellow } = chalk; const { green, yellow } = chalk;
const logo = `${chalk.green(' ____ _')} const logo = `${chalk.green(' ____ _')}
${green('| _ \\ __ _| | _____')} ${green('| _ \\ __ _| | _____')}
${green('| |_) / _` | |/ / _ \\')} ${green('| |_) / _` | |/ / _ \\')}
${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')} ${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')}
${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')} ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')}
`; `;
program program
.addHelpText('beforeAll', logo) .addHelpText('beforeAll', logo)
@@ -974,6 +1023,8 @@ program
.option('--iter-copy-file', 'Copy files when URL is a local file', DEFAULT_PAKE_OPTIONS.iterCopyFile) .option('--iter-copy-file', 'Copy files when URL is a local file', DEFAULT_PAKE_OPTIONS.iterCopyFile)
.option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch) .option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch)
.option('--targets <string>', 'Only for Linux, option "deb" or "appimage"', DEFAULT_PAKE_OPTIONS.targets) .option('--targets <string>', 'Only for Linux, option "deb" or "appimage"', DEFAULT_PAKE_OPTIONS.targets)
.option('--inject [injects...]', 'inject .js or .css for this app', DEFAULT_PAKE_OPTIONS.inject)
.option('--safe-domain [domains...]', 'domains that can call window.__TAURI__ and use ipc', DEFAULT_PAKE_OPTIONS.safeDomain)
.option('--debug', 'Debug mode', DEFAULT_PAKE_OPTIONS.debug) .option('--debug', 'Debug mode', DEFAULT_PAKE_OPTIONS.debug)
.version(packageJson.version, '-v, --version', 'Output the current version') .version(packageJson.version, '-v, --version', 'Output the current version')
.action(async (url, options) => { .action(async (url, options) => {
@@ -997,4 +1048,4 @@ program
await builder.prepare(); await builder.prepare();
await builder.build(url); await builder.build(url);
}); });
program.parse(); program.parse();

View File

@@ -33,12 +33,14 @@
"start": "npm run dev", "start": "npm run dev",
"dev": "npm run tauri dev", "dev": "npm run tauri dev",
"build": "npm run tauri build --release", "build": "npm run tauri build --release",
"build:debug": "npm run tauri build -- --debug",
"build:mac": "npm run tauri build -- --target universal-apple-darwin", "build:mac": "npm run tauri build -- --target universal-apple-darwin",
"build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh", "build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh",
"build:all-windows": "pwsh ./script/build.ps1", "build:all-windows": "pwsh ./script/build.ps1",
"analyze": "cd src-tauri && cargo bloat --release --crates", "analyze": "cd src-tauri && cargo bloat --release --crates",
"tauri": "tauri", "tauri": "tauri",
"cli": "rollup -c rollup.config.js --watch", "cli": "rollup -c rollup.config.js --watch",
"cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w",
"cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js",
"prepublishOnly": "npm run cli:build" "prepublishOnly": "npm run cli:build"
}, },
@@ -66,6 +68,7 @@
"@rollup/plugin-alias": "^4.0.2", "@rollup/plugin-alias": "^4.0.2",
"@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^5.0.2", "@rollup/plugin-json": "^5.0.2",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-terser": "^0.1.0", "@rollup/plugin-terser": "^0.1.0",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/is-url": "^1.2.30", "@types/is-url": "^1.2.30",

View File

@@ -0,0 +1,50 @@
import chalk from 'chalk';
import {spawn, exec} from 'child_process';
// just run in development mode
export default function pakeCliDevPlugin() {
let devChildProcess;
let cliChildProcess;
let devHasStarted = false;
return {
name: 'pake-cli-dev-plugin',
buildEnd() {
const command = 'node';
const cliCmdArgs = ['./dist/dev.js'];
cliChildProcess = spawn(command, cliCmdArgs, {detached: true});
cliChildProcess.stdout.on('data', (data) => {
console.log(chalk.green(data.toString()));
});
cliChildProcess.stderr.on('data', (data) => {
console.error(chalk.yellow(data.toString()));
});
cliChildProcess.on('close', async (code) => {
console.log(chalk.yellow(`cli running end with code: ${code}`));
if (devHasStarted) return;
devHasStarted = true;
devChildProcess = await exec('npm run tauri dev -- --config ./src-tauri/.pake/tauri.conf.json --features cli-build');
devChildProcess.stdout.on('data', (data) => {
console.log(chalk.green(data.toString()));
});
devChildProcess.stderr.on('data', (data) => {
console.error(chalk.yellow(data.toString()));
});
devChildProcess.on('close', (code) => {
console.log(chalk.yellow(`dev running end: ${code}`));
process.exit(code);
});
});
}
}
}

21
rollup.config.js vendored
View File

@@ -4,12 +4,23 @@ import typescript from 'rollup-plugin-typescript2';
import alias from '@rollup/plugin-alias'; import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json'; import json from '@rollup/plugin-json';
import replace from '@rollup/plugin-replace';
import pakeCliDevPlugin from './plugins/pakeCliDevPlugin.js';
const isProduction = process.env.NODE_ENV === 'production';
const devPlugins = !isProduction ? [pakeCliDevPlugin()] : [];
export default { export default {
input: 'bin/cli.ts', input: isProduction ? 'bin/cli.ts' : 'bin/dev.ts',
output: { output: {
file: 'dist/cli.js', file: isProduction ? 'dist/cli.js' : 'dist/dev.js',
format: 'es' format: 'es',
sourcemap: !isProduction,
},
watch: {
include: 'bin/**',
exclude: 'node_modules/**',
}, },
plugins: [ plugins: [
json(), json(),
@@ -18,8 +29,12 @@ export default {
clean: true, // 清理缓存 clean: true, // 清理缓存
}), }),
commonjs(), commonjs(),
replace({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
alias({ alias({
entries: [{ find: '@', replacement: path.join(appRootPath.path, 'bin') }], entries: [{ find: '@', replacement: path.join(appRootPath.path, 'bin') }],
}), }),
...devPlugins,
], ],
}; };

View File

@@ -24,6 +24,8 @@ tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-works
cargo-bloat = "0.11.1" cargo-bloat = "0.11.1"
[features] [features]
# this feature is used for development builds from development cli
cli-build = []
# by default Tauri runs in production mode # by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
default = ["custom-protocol"] default = ["custom-protocol"]

BIN
src-tauri/icons/figma.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/zbook.icns Normal file

Binary file not shown.

View File

@@ -1,28 +1,29 @@
{ {
"windows": [ "windows": [
{ {
"url": "https://weread.qq.com/", "url": "https://weread.qq.com/",
"transparent": true, "transparent": false,
"fullscreen": false, "fullscreen": false,
"width": 1200, "width": 1200,
"height": 780, "height": 780,
"resizable": true, "resizable": true,
"url_type": "web" "url_type": "web"
} }
], ],
"user_agent": { "user_agent": {
"macos": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15", "macos": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
"linux": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", "linux": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" "windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
}, },
"menu": { "menu": {
"macos": true, "macos": false,
"linux": false, "linux": false,
"windows": false "windows": false
}, },
"system_tray": { "system_tray": {
"macos": false, "macos": false,
"linux": true, "linux": true,
"windows": true "windows": true
} },
"inject": []
} }

View File

@@ -1,5 +1,5 @@
use crate::util::{check_file_or_append, get_download_message, show_toast, MessageType}; use crate::util::{check_file_or_append, get_download_message, show_toast, MessageType};
use std::fs::File; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use tauri::api::http::{ClientBuilder, HttpRequestBuilder, ResponseType}; use tauri::api::http::{ClientBuilder, HttpRequestBuilder, ResponseType};
use tauri::{api, command, AppHandle, Manager, Window}; use tauri::{api, command, AppHandle, Manager, Window};
@@ -10,6 +10,12 @@ pub struct DownloadFileParams {
filename: String, filename: String,
} }
#[derive(serde::Deserialize)]
pub struct BinaryDownloadParams {
filename: String,
binary: Vec<u8>,
}
#[command] #[command]
pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> { pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> {
let window: Window = app.get_window("pake").unwrap(); let window: Window = app.get_window("pake").unwrap();
@@ -42,3 +48,25 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result
} }
} }
} }
#[command]
pub async fn download_file_by_binary(
app: AppHandle,
params: BinaryDownloadParams,
) -> Result<(), String> {
let window: Window = app.get_window("pake").unwrap();
show_toast(&window, &get_download_message(MessageType::Start));
let output_path = api::path::download_dir().unwrap().join(params.filename);
let file_path = check_file_or_append(output_path.to_str().unwrap());
let download_file_result = fs::write(file_path, &params.binary);
match download_file_result {
Ok(_) => {
show_toast(&window, &get_download_message(MessageType::Success));
Ok(())
}
Err(e) => {
show_toast(&window, &get_download_message(MessageType::Failure));
Err(e.to_string())
}
}
}

View File

@@ -26,10 +26,11 @@ pub fn get_window(app: &mut App, config: PakeConfig, _data_dir: PathBuf) -> Wind
.resizable(window_config.resizable) .resizable(window_config.resizable)
.fullscreen(window_config.fullscreen) .fullscreen(window_config.fullscreen)
.inner_size(window_config.width, window_config.height) .inner_size(window_config.width, window_config.height)
.disable_file_drop_handler() //Very annoying, otherwise dragging files to the window will not work. .disable_file_drop_handler()
.initialization_script(include_str!("../inject/style.js")) .initialization_script(include_str!("../inject/component.js"))
.initialization_script(include_str!("../inject/event.js")) .initialization_script(include_str!("../inject/event.js"))
.initialization_script(include_str!("../inject/component.js")); .initialization_script(include_str!("../inject/style.js"))
.initialization_script(include_str!("../inject/custom.js")); //Very annoying, otherwise dragging files to the window will not work.
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {

2
src-tauri/src/inject/custom.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// This file is used to merge the injected external js and css files
// and you can also directly add script files that you want to attach to the application in this fil

View File

@@ -68,12 +68,90 @@ document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(topDom); document.body.appendChild(topDom);
const domEl = document.getElementById('pack-top-dom'); const domEl = document.getElementById('pack-top-dom');
domEl.addEventListener('mousedown', (e) => {
e.preventDefault(); // Collect blob urls to blob by overriding window.URL.createObjectURL
if (e.buttons === 1 && e.detail !== 2) { function collectUrlToBlobs() {
appWindow.startDragging().then(); const backupCreateObjectURL = window.URL.createObjectURL;
} window.blobToUrlCaches = new Map();
window.URL.createObjectURL = (blob) => {
const url = backupCreateObjectURL.call(window.URL, blob);
window.blobToUrlCaches.set(url, blob);
return url;
};
}
function convertBlobUrlToBinary(blobUrl) {
return new Promise((resolve) => {
const blob = window.blobToUrlCaches.get(blobUrl);
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = () => {
resolve(Array.from(new Uint8Array(reader.result)));
};
}); });
}
function downladFromDataUri(dataURI, filename) {
const byteString = atob(dataURI.split(',')[1]);
// write the bytes of the string to an ArrayBuffer
const bufferArray = new ArrayBuffer(byteString.length);
// create a view into the buffer
const binary = new Uint8Array(bufferArray);
// set the bytes of the buffer to the correct values
for (var i = 0; i < byteString.length; i++) {
binary[i] = byteString.charCodeAt(i);
}
// write the ArrayBuffer to a binary, and you're done
invoke('download_file_by_binary', {
params: {
filename,
binary: Array.from(binary)
},
});
}
function downloadFromBlobUrl(blobUrl, filename) {
convertBlobUrlToBinary(blobUrl).then((binary) => {
invoke('download_file_by_binary', {
params: {
filename,
binary
},
});
});
}
// detect blob download by createElement("a")
function detectDownloadByCreateAnchor() {
const createEle = document.createElement;
document.createElement = (el) => {
if (el !== 'a') return createEle.call(document, el);
const anchorEle = createEle.call(document, el);
// use addEventListener to avoid overriding the original click event.
anchorEle.addEventListener('click', (e) => {
const url = anchorEle.href;
const filename = anchorEle.download || getFilenameFromUrl(url);
if (window.blobToUrlCaches.has(url)) {
downloadFromBlobUrl(url, filename);
// case: downoload from dataURL -> convert dataURL ->
} else if (url.startsWith('data:')) {
downladFromDataUri(url, filename);
} else {
handleExternalLink(e, url);
}
}, true);
return anchorEle;
};
}
domEl.addEventListener('touchstart', () => { domEl.addEventListener('touchstart', () => {
appWindow.startDragging().then(); appWindow.startDragging().then();
@@ -95,6 +173,9 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const isExternalLink = (url, host) => window.location.host !== host; const isExternalLink = (url, host) => window.location.host !== host;
// process special download protocal['data:','blob:']
const isSpecialDownload = (url) => ['blob', 'data'].some(protocal => url.startsWith(protocal));
const isDownloadRequired = (url, anchorElement, e) => const isDownloadRequired = (url, anchorElement, e) =>
anchorElement.download || e.metaKey || e.ctrlKey || isDownloadLink(url); anchorElement.download || e.metaKey || e.ctrlKey || isDownloadLink(url);
@@ -122,7 +203,7 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// Process download links for Rust to handle. // Process download links for Rust to handle.
if (isDownloadRequired(absoluteUrl, anchorElement, e) && !externalDownLoadLink()) { if (isDownloadRequired(absoluteUrl, anchorElement, e) && !externalDownLoadLink() && !isSpecialDownload(absoluteUrl)) {
handleDownloadLink(e, absoluteUrl, filename); handleDownloadLink(e, absoluteUrl, filename);
} }
} }
@@ -177,69 +258,6 @@ function getFilenameFromUrl(url) {
return urlPath.substring(urlPath.lastIndexOf('/') + 1); return urlPath.substring(urlPath.lastIndexOf('/') + 1);
} }
// Collect blob urls to blob by overriding window.URL.createObjectURL
function collectUrlToBlobs() {
const backupCreateObjectURL = window.URL.createObjectURL;
window.blobToUrlCaches = new Map();
window.URL.createObjectURL = (blob) => {
const url = backupCreateObjectURL.call(window.URL, blob);
window.blobToUrlCaches.set(url, blob);
return url;
};
}
function convertBlobUrlToBinary(blobUrl) {
return new Promise((resolve) => {
const blob = window.blobToUrlCaches.get(blobUrl);
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onload = () => {
resolve(reader.result);
};
});
}
async function downloadFromBlobUrl(blobUrl, filename) {
try {
const tauri = window.__TAURI__;
const binary = await convertBlobUrlToBinary(blobUrl);
await tauri.fs.writeBinaryFile(filename, binary, {
dir: tauri.fs.BaseDirectory.Download,
});
const lang = getSystemLanguage();
window.pakeToast(lang === 'en' ? 'Download successful, saved to download directory~' : '下载成功,已保存到下载目录~');
} catch (error) {
console.error('Error downloading from Blob URL:', error);
}
}
// detect blob download by createElement("a")
function detectDownloadByCreateAnchor() {
const originalCreateElement = document.createElement;
document.createElement = function(el, ...args) {
const element = originalCreateElement.call(this, el, ...args);
if (el === 'a') {
element.addEventListener('click', (event) => {
const url = element.href;
if (window.blobToUrlCaches.has(url)) {
// Prevent default 'click' event if a blob URL is detected
event.preventDefault();
const filename = element.download || getFilenameFromUrl(url);
downloadFromBlobUrl(url, filename);
}
});
}
return element;
};
}
// Determine the language of the current system. // Determine the language of the current system.
function getSystemLanguage() { function getSystemLanguage() {

View File

@@ -7,7 +7,7 @@ mod app;
mod util; mod util;
use app::{invoke, menu, window}; use app::{invoke, menu, window};
use invoke::download_file; use invoke::{download_file, download_file_by_binary};
use menu::{get_menu, menu_event_handle}; use menu::{get_menu, menu_event_handle};
use tauri_plugin_window_state::Builder as windowStatePlugin; use tauri_plugin_window_state::Builder as windowStatePlugin;
use util::{get_data_dir, get_pake_config}; use util::{get_data_dir, get_pake_config};
@@ -41,7 +41,10 @@ pub fn run_app() {
tauri_app tauri_app
.plugin(windowStatePlugin::default().build()) .plugin(windowStatePlugin::default().build())
.invoke_handler(tauri::generate_handler![download_file]) .invoke_handler(tauri::generate_handler![
download_file,
download_file_by_binary
])
.setup(|app| { .setup(|app| {
let _window = get_window(app, pake_config, data_dir); let _window = get_window(app, pake_config, data_dir);
// Prevent initial shaking // Prevent initial shaking

View File

@@ -4,9 +4,16 @@ use std::path::PathBuf;
use tauri::{api, Config, Window}; use tauri::{api, Config, Window};
pub fn get_pake_config() -> (PakeConfig, Config) { pub fn get_pake_config() -> (PakeConfig, Config) {
#[cfg(feature = "cli-build")]
let pake_config: PakeConfig = serde_json::from_str(include_str!("../.pake/pake.json"))
.expect("Failed to parse pake config");
#[cfg(not(feature = "cli-build"))]
let pake_config: PakeConfig = let pake_config: PakeConfig =
serde_json::from_str(include_str!("../pake.json")).expect("Failed to parse pake config"); serde_json::from_str(include_str!("../pake.json")).expect("Failed to parse pake config");
#[cfg(feature = "cli-build")]
let tauri_config: Config = serde_json::from_str(include_str!("../.pake/tauri.conf.json"))
.expect("Failed to parse tauri config");
#[cfg(not(feature = "cli-build"))]
let tauri_config: Config = serde_json::from_str(include_str!("../tauri.conf.json")) let tauri_config: Config = serde_json::from_str(include_str!("../tauri.conf.json"))
.expect("Failed to parse tauri config"); .expect("Failed to parse tauri config");

View File

@@ -1,39 +1,43 @@
{ {
"package": { "package": {
"productName": "WeRead", "productName": "WeRead",
"version": "1.0.0" "version": "1.0.0"
}, },
"tauri": { "tauri": {
"security": { "security": {
"csp": null, "csp": null,
"dangerousRemoteDomainIpcAccess": [ "dangerousRemoteDomainIpcAccess": [
{ {
"domain": "weread.qq.com", "domain": "weread.qq.com",
"windows": ["pake"], "windows": [
"enableTauriAPI": true "pake"
],
"enableTauriAPI": true
}
]
},
"updater": {
"active": false
},
"systemTray": {
"iconPath": "png/icon_512.png",
"iconAsTemplate": true
},
"allowlist": {
"all": true,
"fs": {
"all": true,
"scope": [
"$DOWNLOAD/*"
]
}
} }
]
}, },
"updater": { "build": {
"active": false "withGlobalTauri": true,
}, "devPath": "../dist",
"systemTray": { "distDir": "../dist",
"iconPath": "png/weread_512.png", "beforeBuildCommand": "",
"iconAsTemplate": true "beforeDevCommand": ""
},
"allowlist": {
"all": true,
"fs": {
"all": true,
"scope": ["$DOWNLOAD/*"]
}
} }
},
"build": {
"withGlobalTauri": true,
"devPath": "../dist",
"distDir": "../dist",
"beforeBuildCommand": "",
"beforeDevCommand": ""
}
} }

View File

@@ -1,23 +1,25 @@
{ {
"tauri": { "tauri": {
"bundle": { "bundle": {
"icon": ["icons/weread.icns"], "icon": ["icons/weread.icns"],
"identifier": "com.pake.weread", "identifier": "com.pake.weread",
"active": true, "active": true,
"category": "DeveloperTool", "category": "DeveloperTool",
"copyright": "", "copyright": "",
"externalBin": [], "externalBin": [],
"longDescription": "", "longDescription": "",
"macOS": { "macOS": {
"entitlements": null, "entitlements": null,
"exceptionDomain": "", "exceptionDomain": "",
"frameworks": [], "frameworks": [],
"providerShortName": null, "providerShortName": null,
"signingIdentity": null "signingIdentity": null
}, },
"resources": [], "resources": [],
"shortDescription": "", "shortDescription": "",
"targets": ["dmg"] "targets": [
"dmg"
]
}
} }
}
} }