Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
jeasonnow
2023-06-25 10:23:51 +08:00
54 changed files with 2346 additions and 5630 deletions

View File

@@ -1,4 +1,3 @@
# http://editorconfig.org
root = true
[*]

View File

@@ -1,4 +1,4 @@
name: Build App with Pake Cli
name: Build App with Pake CLI
on:
workflow_dispatch:
inputs:
@@ -20,29 +20,19 @@ on:
icon:
description: "[Icon, Optional]"
required: false
height:
description: "[Height, Optional]"
required: false
default: "780"
width:
description: "[Width, Optional]"
required: false
default: "1200"
height:
description: "[Height, Optional]"
required: false
default: "780"
transparent:
description: "[Transparent, Optional]"
description: "[Transparent, Optional, MacOS only]"
required: false
type: boolean
default: false
# fullscreen:
# description: "[FullScreen, Optional]"
# required: false
# type: boolean
# default: false
resize:
description: "[Resize, Optional]"
required: false
type: boolean
default: true
multi_arch:
description: "[MultiArch, Optional, MacOS only]"
required: false

View File

@@ -1,4 +1,3 @@
target
src-tauri/target
node_modules
dist/**/*
!dist/twitter.css

15
.prettierrc.json Normal file
View File

@@ -0,0 +1,15 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"endOfLine": "lf",
"bracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

View File

@@ -17,10 +17,6 @@ graph LR
- `master` is the release branch, we will make tag and publish version on this branch.
- If it is a document modification, it can be submitted to this branch.
## Commit Log
please use <https://github.com/tw93/cz-emoji-chinese>
## More
It is a good habit to create a feature request issue to discuss whether the feature is necessary before you implement it. However, it's unnecessary to create an issue to claim that you found a typo or improved the readability of documentation, just create a pull request.

View File

@@ -58,7 +58,7 @@
</td>
</tr>
<tr>
<td><img src=https://cdn.fliggy.com/upic/rdeeLu.jpg width=600/></td>
<td><img src=https://cdn.fliggy.com/upic/Ld5ZCJ.png width=600/></td>
<td><img src=https://cdn.fliggy.com/upic/7QUfi3.png width=600/></td>
</tr>
<tr>
@@ -159,7 +159,7 @@ In addition, double-click the title bar to switch to full-screen mode. For Mac u
![Pake](https://gw.alipayobjects.com/zos/k/zd/pake.gif)
**Pake provides a command line tool, making the flow of package customization quicker and easier. See [documentation](./bin/README_EN.md) for more information.**
**Pake provides a command line tool, making the flow of package customization quicker and easier. See [documentation](./bin/README_CN.md) for more information.**
```bash
# Install with npm
@@ -194,7 +194,7 @@ npm run build
## Advanced Usage
1. You can refer to the [codebase structure](https://github.com/tw93/Pake/wiki/Description-of-Pake's-code-structure) before working on Pake, which will help you much in development.
2. Modify the `url` and `productName` fields in the `pake.json` file under the src-tauri directory, as well as the `icon` and `identifier` fields in the `tauri.xxx.conf.json` file. You can select a `icon` from the `icons` directory or download one from [macOSicons](https://macosicons.com/#/) to match your product needs.
2. Modify the `url` and `productName` fields in the `pake.json` file under the src-tauri directory, the "domain" field in the `tauri.config.json` file needs to be modified synchronously, as well as the `icon` and `identifier` fields in the `tauri.xxx.conf.json` file. You can select a `icon` from the `icons` directory or download one from [macOSicons](https://macosicons.com/#/) to match your product needs.
3. For configurations on window properties, you can modify the `pake.json` file to change the value of `width`, `height`, `fullscreen` (or not), `resizable` (or not) of the `windows` property. To adapt to the immersive header on Mac, change `transparent` to `true`, look for the `Header` element, and add the `padding-top` property.
4. For advanced usages such as style rewriting, advertisement removal, JS injection, container message communication, and user-defined shortcut keys, see [Advanced Usage of Pake](https://github.com/tw93/Pake/wiki/Advanced-Usage-of-Pake).
@@ -269,6 +269,13 @@ Pake's development can not be without these Hackers. They contributed a lot of c
<sub><b>Pake Actions</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jeasonnow">
<img src="https://avatars.githubusercontent.com/u/16950207?v=4" width="90;" alt="jeasonnow"/>
<br />
<sub><b>Santree</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/QingZ11">
<img src="https://avatars.githubusercontent.com/u/38887077?v=4" width="90;" alt="QingZ11"/>
@@ -283,13 +290,6 @@ Pake's development can not be without these Hackers. They contributed a lot of c
<sub><b>孟世博</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jeasonnow">
<img src="https://avatars.githubusercontent.com/u/16950207?v=4" width="90;" alt="jeasonnow"/>
<br />
<sub><b>Santree</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/2nthony">
<img src="https://avatars.githubusercontent.com/u/19513289?v=4" width="90;" alt="2nthony"/>

View File

@@ -57,7 +57,7 @@
</td>
</tr>
<tr>
<td><img src=https://cdn.fliggy.com/upic/rdeeLu.jpg width=600/></td>
<td><img src=https://cdn.fliggy.com/upic/Ld5ZCJ.png width=600/></td>
<td><img src=https://cdn.fliggy.com/upic/7QUfi3.png width=600/></td>
</tr>
<tr>
@@ -162,7 +162,7 @@
</kbd>
<br/><br/>
**Pake 提供了命令行工具,可以更快捷方便地一键自定义打你需要的包,详细可见 [文档](./bin/README.md)。**
**Pake 提供了命令行工具,可以更快捷方便地一键自定义打你需要的包,详细可见 [文档](./bin/README_CN.md)。**
```bash
# 使用 npm 进行安装
@@ -196,7 +196,7 @@ npm run build
## 高级使用
1. 代码结构可参考 [文档](https://github.com/tw93/Pake/wiki/Pake-%E7%9A%84%E4%BB%A3%E7%A0%81%E7%BB%93%E6%9E%84%E8%AF%B4%E6%98%8E),便于你在开发前了解更多。
2. 修改 src-tauri 目录下 `pake.json` 中的 `url``productName` 字段,以及 `tauri.xxx.conf.json` 中的 `icon``identifier` 字段,其中 `icon` 可以从 icons 目录选择一个,也可以去 [macOSicons](https://macosicons.com/#/) 下载符合效果的。
2. 修改 src-tauri 目录下 `pake.json` 中的 `url``productName` 字段,需同步修改下 `tauri.config.json` 中的 `domain` 字段,以及 `tauri.xxx.conf.json` 中的 `icon``identifier` 字段,其中 `icon` 可以从 icons 目录选择一个,也可以去 [macOSicons](https://macosicons.com/#/) 下载符合效果的。
3. 关于窗口属性设置,可以在 `pake.json` 修改 windows 属性对应的 `width/height`fullscreen 是否全屏resizable 是否可以调整大小,假如想适配 Mac 沉浸式头部,可以将 transparent 设置成 `true`,找到 Header 元素加一个 padding-top 样式即可,不想适配改成 `false` 也行。
4. 此外样式改写、屏蔽广告、逻辑代码注入、容器消息通信、自定义快捷键可见 [高级用法](https://github.com/tw93/Pake/wiki/Pake-%E7%9A%84%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95)。
@@ -249,19 +249,26 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢
<sub><b>Essesoul</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/AielloChan">
<img src="https://avatars.githubusercontent.com/u/7900765?v=4" width="90;" alt="AielloChan"/>
<br />
<sub><b>Aiello</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/m1911star">
<img src="https://avatars.githubusercontent.com/u/4948120?v=4" width="90;" alt="m1911star"/>
<br />
<sub><b>Horus</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/AielloChan">
<img src="https://avatars.githubusercontent.com/u/7900765?v=4" width="90;" alt="AielloChan"/>
<a href="https://github.com/Pake-Actions">
<img src="https://avatars.githubusercontent.com/u/126550811?v=4" width="90;" alt="Pake-Actions"/>
<br />
<sub><b>Aiello</b></sub>
<sub><b>Pake Actions</b></sub>
</a>
</td>
<td align="center">
@@ -271,6 +278,20 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢
<sub><b>Steam</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/exposir">
<img src="https://avatars.githubusercontent.com/u/33340988?v=4" width="90;" alt="exposir"/>
<br />
<sub><b>孟世博</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/jeasonnow">
<img src="https://avatars.githubusercontent.com/u/16950207?v=4" width="90;" alt="jeasonnow"/>
<br />
<sub><b>Santree</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/2nthony">
<img src="https://avatars.githubusercontent.com/u/19513289?v=4" width="90;" alt="2nthony"/>
@@ -278,6 +299,14 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢
<sub><b>2nthony</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/ACGNnsj">
<img src="https://avatars.githubusercontent.com/u/22112141?v=4" width="90;" alt="ACGNnsj"/>
<br />
<sub><b>Null</b></sub>
</a>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/nekomeowww">
<img src="https://avatars.githubusercontent.com/u/11081491?v=4" width="90;" alt="nekomeowww"/>
@@ -305,8 +334,7 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢
<br />
<sub><b>Po Chen</b></sub>
</a>
</td></tr>
<tr>
</td>
<td align="center">
<a href="https://github.com/houhoz">
<img src="https://avatars.githubusercontent.com/u/19684376?v=4" width="90;" alt="houhoz"/>
@@ -314,13 +342,21 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢
<sub><b>Hyzhao</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/lakca">
<img src="https://avatars.githubusercontent.com/u/16255922?v=4" width="90;" alt="lakca"/>
<br />
<sub><b>Null</b></sub>
</a>
</td>
<td align="center">
<a href="https://github.com/liusishan">
<img src="https://avatars.githubusercontent.com/u/33129823?v=4" width="90;" alt="liusishan"/>
<br />
<sub><b>Liusishan</b></sub>
</a>
</td>
</td></tr>
<tr>
<td align="center">
<a href="https://github.com/piaoyidage">
<img src="https://avatars.githubusercontent.com/u/5135405?v=4" width="90;" alt="piaoyidage"/>
@@ -333,7 +369,7 @@ Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢
## 支持
1. 我有两只猫,一只叫汤圆,一只叫可乐,假如觉得 Pake 让你生活更美好,可以给汤圆可乐 <a href="https://miaoyan.app/cats.html?name=Pake" target="_blank">喂罐头 🥩</a>。
1. 我有两只猫,一只叫汤圆,一只叫可乐,假如 Pake 让你生活更美好,可以给汤圆可乐 <a href="https://miaoyan.app/cats.html?name=Pake" target="_blank">喂罐头 🥩</a>。
2. 如果你喜欢 Pake可以在 Github Star更欢迎 [推荐](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=%23Pake%20%E4%B8%80%E4%B8%AA%E5%BE%88%E7%AE%80%E5%8D%95%E7%9A%84%E7%94%A8%20Rust%20%E6%89%93%E5%8C%85%E7%BD%91%E9%A1%B5%E7%94%9F%E6%88%90%20Mac%20App%20%E7%9A%84%E5%B7%A5%E5%85%B7%EF%BC%8C%E7%9B%B8%E6%AF%94%E4%BC%A0%E7%BB%9F%E7%9A%84%20Electron%20%E5%A5%97%E5%A3%B3%E6%89%93%E5%8C%85%EF%BC%8C%E5%A4%A7%E5%B0%8F%E8%A6%81%E5%B0%8F%E5%B0%86%E8%BF%91%2040%20%E5%80%8D%EF%BC%8C%E4%B8%80%E8%88%AC%202M%20%E5%B7%A6%E5%8F%B3%EF%BC%8C%E5%BA%95%E5%B1%82%E4%BD%BF%E7%94%A8Tauri%20%EF%BC%8C%E6%80%A7%E8%83%BD%E4%BD%93%E9%AA%8C%E8%BE%83%20JS%20%E6%A1%86%E6%9E%B6%E8%A6%81%E8%BD%BB%E5%BF%AB%E4%B8%8D%E5%B0%91%EF%BC%8C%E5%86%85%E5%AD%98%E5%B0%8F%E5%BE%88%E5%A4%9A%EF%BC%8C%E6%94%AF%E6%8C%81%E5%BE%AE%E4%BF%A1%E8%AF%BB%E4%B9%A6%E3%80%81Twitter%E3%80%81Youtube%E3%80%81RunCode%E3%80%81Flomo%E3%80%81%E8%AF%AD%E9%9B%80%E7%AD%89%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%BE%88%E6%96%B9%E4%BE%BF%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91~) 给你志同道合的朋友使用。
3. 可以关注我的 [Twitter](https://twitter.com/HiTw93) 获取到最新的 Pake 更新消息,也欢迎加入 [Telegram](https://t.me/+GclQS9ZnxyI2ODQ1) 聊天群。
4. 希望大伙玩的过程中有一种学习新技术的喜悦感,假如你发现有很适合做成桌面 App 的网页也很欢迎告诉我。

107
bin/README.md vendored
View File

@@ -1,23 +1,25 @@
## 安装
<h4 align="right"><strong>English</strong> | <a href="https://github.com/tw93/Pake/blob/master/bin/README_CN.md">简体中文</a></h4>
请确保 Node 版本>=16 如 16.8,不要使用 sudo 进行安装,假如 npm 报没有权限可以参考 [How to fix npm throwing error without sudo](https://stackoverflow.com/questions/16151018/how-to-fix-npm-throwing-error-without-sudo)。
## Installation
Ensure that your Node.js version is 16.0 or higher (e.g., 16.8). Avoid using `sudo` for the installation. If you encounter permission issues with npm, refer to [How to fix npm throwing error without sudo](https://stackoverflow.com/questions/16151018/how-to-fix-npm-throwing-error-without-sudo).
```bash
npm install -g pake-cli
npm install pake-cli -g
```
## Windows/Linux 注意点
## Considerations for Windows & Linux Users
- **十分重要** 查看 Tauri 提供的[依赖指南](https://tauri.app/v1/guides/getting-started/prerequisites)
- 对于 windows(至少安装了`Win10 SDK(10.0.19041.0)` `Visual Studio build tool 2022>=17.2`),还需要额外安装:
- **CRITICAL**: Consult [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites) before proceeding.
- For Windows users (ensure that `Win10 SDK (10.0.19041.0)` and `Visual Studio build tool 2022 (>=17.2)` are installed), additional installations are required:
1. Microsoft Visual C++ 2015-2022 Redistributable (x64)
2. Microsoft Visual C++ 2015-2022 Redistributable (x86)
3. Microsoft Visual C++ 2012 Redistributable (x86)(可选)
4. Microsoft Visual C++ 2013 Redistributable (x86)(可选)
5. Microsoft Visual C++ 2008 Redistributable (x86)(可选)
3. Microsoft Visual C++ 2012 Redistributable (x86) (optional)
4. Microsoft Visual C++ 2013 Redistributable (x86) (optional)
5. Microsoft Visual C++ 2008 Redistributable (x86) (optional)
- 此外 Ubuntu 在开始之前可以运行如下命令,安装前期所需依赖。
- For Ubuntu users, execute the following commands to install the required libraries before compiling:
```bash
sudo apt install libdbus-1-dev \
@@ -35,88 +37,71 @@ npm install -g pake-cli
gnome-video-effects-extra
```
## 用法
## Usage
```bash
pake url [options]
pake [url] [options]
```
打包完成后的应用程序默认为当前工作目录,首次打包由于需配置好环境,需要一些时间,请耐心等待即可。
The packaged application will be located in the current working directory by default. The first packaging might take some time due to environment configuration. Please be patient.
> **Note**:
> 打包需要用 `Rust` 环境,如果没有 `Rust`,会提示确认安装。如遇安装失败或超时,可[自行安装](https://www.rust-lang.org/tools/install)。
> **Note**: Packaging requires the Rust environment. If Rust is not installed, you will be prompted for installation confirmation. In case of installation failure or timeout, you can [install it manually](https://www.rust-lang.org/tools/install).
### url
### [url]
url 为你需要打包的网页链接 🔗 或者本地 html 文件,必须提供。
The URL is the link to the web page you want to package or the path to a local HTML file. This is mandatory.
### [options]
提供了一些特定的选项,打包时可以传递对应参数达到定制化的效果。
Various options are available for customization. You can pass corresponding arguments during packaging to achieve the desired configuration.
#### [name]
应用名称,如输入时未指定,会提示你输入,尽量使用英语。
Specify the application name. If not provided, you will be prompted to enter it. It is recommended to use English.
```shell
--name <value>
# 或者
-n <value>
```
#### [icon]
应用 icon支持本地/远程文件,默认为 Pake 自带图标,定制的可以去 [icon-icons](https://icon-icons.com) [macOSicons](https://macosicons.com/#/) 搜索下载。
Specify the application icon. Supports both local and remote files. By default, it uses the Pake brand icon. For custom icons, visit [icon icons](https://icon-icons.com) or [macOSicons](https://macosicons.com/#/).
- MacOS 下必须为 `.icns`
- Windows 下必须为 `.ico`
- Linux 下必须为 `.png`
- For macOS, use `.icns` format.
- For Windows, use `.ico` format.
- For Linux, use `.png` format.
```shell
--icon <path>
# 或者
-i <path>
```
#### [height]
打包后的应用窗口高度,默认 `780px`
Set the height of the application window. Default is `780px`.
```shell
--height <number>
# 或者
-h <number>
```
#### [width]
打包后的应用窗口宽度,默认 `1200px`
Set the width of the application window. Default is `1200px`.
```shell
--width <number>
# 或者
-w <number>
```
#### [transparent]
是否开启沉浸式头部,默认为 `false` 不开启,输入下面的命令则开启沉浸式,推荐 MacOS 用户开启。
Enable or disable immersive header. Default is `false`. Use the following command to enable this feature, macOS only.
```shell
--transparent
```
#### [resize]
是否可以拖动大小,默认为 `true` 可拖动,输入下面的命令则不能对窗口大小进行拉伸。
```shell
--no-resizable
```
#### [fullscreen]
打开应用后是否开启全屏,默认为 `false`,输入下面的命令则会自动全屏。
Determine whether the application launches in full screen. Default is `false`. Use the following command to enable full screen.
```shell
--fullscreen
@@ -124,42 +109,40 @@ url 为你需要打包的网页链接 🔗 或者本地 html 文件,必须提
#### [multi-arch]
打包结果同时支持英特尔和 m1 芯片,仅适用于 MacOS默认为 `false`
Package the application to support both Intel and M1 chips, exclusively for macOS. Default is `false`.
##### 准备工作
##### Prerequisites
- 注意:开启该选项后,需要用 rust 官网的 rustup 安装 rust不支持 brew 安装。
- 对于 intel 芯片用户,需要安装 arm64 跨平台包,使安装包支持 m1 芯片,使用下面命令安装。
- Note: After enabling this option, Rust must be installed using rustup from the official Rust website. Installation via brew is not supported.
- For Intel chip users, install the arm64 cross-platform package to support M1 chips using the following command:
```shell
rustup target add aarch64-apple-darwin
```
- 对于 M1 芯片用户,需要安装 x86 跨平台包,使安装包支持 interl 芯片,使用下面的命令安装。
- For M1 chip users, install the x86 cross-platform package to support Intel chips using the following command:
```shell
rustup target add x86_64-apple-darwin
```
##### 使用方法
##### Usage
```shell
--multi-arch
# 或者
-m
```
#### [targets]
选择输出的包格式,支持 deb/appimage/all如果选择 all,则同时打包 deb 和 appimage该选项仅支持 Linux默认为`all`
Select the output package format for Linux. Options include `deb`, `appimage`, or `all`. If `all` is selected, both `deb` and `appimage` will be packaged. Default is `all`.
```shell
--targets xxx
--targets <format>
```
#### [user-agent]
自定义浏览器请求头, 默认为空。
Customize the browser user agent. Default is empty.
```shell
--user-agent <value>
@@ -167,7 +150,7 @@ rustup target add x86_64-apple-darwin
#### [show-menu]
显示菜单栏, 默认不显示输入下面的命令则会显示推荐MacOS用户开启。
Display the menu bar. Default is not to display. Use the following command to enable the menu bar. Recommended for macOS users.
```shell
--show-menu
@@ -175,7 +158,7 @@ rustup target add x86_64-apple-darwin
#### [show-system-tray]
显示通知栏托盘, 默认不显示,输入下面的命令则会显示。
Display the system tray. Default is not to display. Use the following command to enable the system tray.
```shell
--show-system-tray
@@ -183,17 +166,21 @@ rustup target add x86_64-apple-darwin
#### [system-tray-icon]
通知栏托盘图标,仅当显示通知栏托盘时有效, 图标必须为.ico或者.png格式的32x32~256x256像素的图片。
Specify the system tray icon. This is only effective when the system tray is enabled. The icon must be in `.ico` or `.png` format and should be an image with dimensions ranging from 32x32 to 256x256 pixels.
```shell
--system-tray-icon <value>
--system-tray-icon <path>
```
#### [copy-iter-file]
递归拷贝当url为本地文件路径时候若开启该选项则将url路径文件所在文件夹以及所有子文件都拷贝到pake静态文件夹默认不开启
Enable recursive copying. When the URL is a local file path, enabling this option will copy the folder containing the file specified in the URL, as well as all sub-files, to the Pake static folder. This is disabled by default.
```shell
--copy-iter-file
```
## Conclusion
After completing the above steps, your application should be successfully packaged. Please note that the packaging process may take some time depending on your system configuration and network conditions. Be patient, and once the packaging is complete, you can find the application installer in the specified directory.

190
bin/README_CN.md vendored Normal file
View File

@@ -0,0 +1,190 @@
<h4 align="right"><strong><a href="https://github.com/tw93/Pake/tree/master/bin">English</a></strong> | 简体中文</h4>
## 安装
请确保您的 Node.js 版本为 16 或更高版本(例如 16.8)。请避免使用 `sudo` 进行安装。如果 npm 报告权限问题,请参考 [如何在不使用 sudo 的情况下修复 npm 报错](https://stackoverflow.com/questions/16151018/how-to-fix-npm-throwing-error-without-sudo)。
```bash
npm install pake-cli -g
```
## Windows/Linux 注意事项
- **非常重要**:请参阅 Tauri 的 [依赖项指南](https://tauri.app/v1/guides/getting-started/prerequisites)。
- 对于 Windows 用户,请确保至少安装了 `Win10 SDK(10.0.19041.0)``Visual Studio Build Tools 2022版本 17.2 或更高)`
。此外,还需要安装以下组件:
1. Microsoft Visual C++ 2015-2022 Redistributable (x64)
2. Microsoft Visual C++ 2015-2022 Redistributable (x86)
3. Microsoft Visual C++ 2012 Redistributable (x86)(可选)
4. Microsoft Visual C++ 2013 Redistributable (x86)(可选)
5. Microsoft Visual C++ 2008 Redistributable (x86)(可选)
- 对于 Ubuntu 用户,在开始之前,建议运行以下命令以安装所需的依赖项:
```bash
sudo apt install libdbus-1-dev \
libsoup2.4-dev \
libjavascriptcoregtk-4.0-dev \
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
```
## 使用方法
```bash
pake [url] [options]
```
应用程序的打包结果将默认保存在当前工作目录。由于首次打包需要配置环境,这可能需要一些时间,请耐心等待。
> **注意**:打包过程需要使用 `Rust` 环境。如果您没有安装 `Rust`,系统会提示您是否要安装。如果遇到安装失败或超时的问题,您可以 [手动安装](https://www.rust-lang.org/tools/install)。
### [url]
`url` 是您需要打包的网页链接 🔗 或本地 HTML 文件的路径,此参数为必填。
### [options]
您可以通过传递以下选项来定制打包过程:
#### [name]
指定应用程序的名称。如果在输入时未指定,系统会提示您输入。建议使用英文名称。
```shell
--name <value>
```
#### [icon]
指定应用程序的图标,支持本地或远程文件。默认使用 Pake 的内置图标。您可以访问 [icon-icons](https://icon-icons.com)
或 [macOSicons](https://macosicons.com/#/) 下载自定义图标。
- macOS 要求使用 `.icns` 格式。
- Windows 要求使用 `.ico` 格式。
- Linux 要求使用 `.png` 格式。
```shell
--icon <path>
```
#### [height]
设置应用窗口的高度,默认为 `780px`。
```shell
--height <number>
```
#### [width]
设置应用窗口的宽度,默认为 `1200px`。
```shell
--width <number>
```
#### [transparent]
设置是否启用沉浸式头部,默认为 `false`(不启用)。当前只对 macOS 上有效。
```shell
--transparent
```
#### [fullscreen]
设置应用程序是否在启动时自动全屏,默认为 `false`。使用以下命令可以设置应用程序启动时自动全屏。
```shell
--fullscreen
```
#### [multi-arch]
设置打包结果同时支持 Intel 和 M1 芯片,仅适用于 macOS默认为 `false`。
##### 准备工作
- 注意:启用此选项后,需要使用 rust 官网的 rustup 安装 rust不支持通过 brew 安装。
- 对于 Intel 芯片用户,需要安装 arm64 跨平台包,以使安装包支持 M1 芯片。使用以下命令安装:
```shell
rustup target add aarch64-apple-darwin
```
- 对于 M1 芯片用户,需要安装 x86 跨平台包,以使安装包支持 Intel 芯片。使用以下命令安装:
```shell
rustup target add x86_64-apple-darwin
```
##### 使用方法
```shell
--multi-arch
```
#### [targets]
选择输出的包格式,支持 `deb`、`appimage` 或 `all`。如果选择 `all`,则会同时打包 `deb` 和 `appimage`。此选项仅适用于
Linux默认为 `all`。
```shell
--targets <value>
```
#### [user-agent]
自定义浏览器的用户代理请求头,默认为空。
```shell
--user-agent <value>
```
#### [show-menu]
设置是否显示菜单栏,默认不显示。在 macOS 上推荐启用此选项。
```shell
--show-menu
```
#### [show-system-tray]
设置是否显示通知栏托盘,默认不显示。
```shell
--show-system-tray
```
#### [system-tray-icon]
设置通知栏托盘图标,仅在启用通知栏托盘时有效。图标必须为 `.ico` 或 `.png` 格式,分辨率为 32x32 到 256x256 像素。
```shell
--system-tray-icon <path>
```
#### [copy-iter-file]
当 `url` 为本地文件路径时,如果启用此选项,则会递归地将 `url` 路径文件所在的文件夹及其所有子文件复
制到 Pake 的静态文件夹。默认不启用。
```shell
--copy-iter-file
```
## 结语
完成上述步骤后,您的应用程序应该已经成功打包。请注意,根据您的系统配置和网络状况,打包过程可能需要一些时间。请耐心等待,一旦打包完成,您就可以在指定的目录中找到应用程序安装包。

202
bin/README_EN.md vendored
View File

@@ -1,202 +0,0 @@
## Install
Ensure the version of your installed Node.js is greater than `16.0` such as `16.8`. Do not use `sudo` to install. If you encountered permission issues/problems while installing using npm, see [How to fix npm throwing error without sudo](https://stackoverflow.com/questions/16151018/how-to-fix-npm-throwing-error-without-sudo).
```bash
npm install -g pake-cli
```
## Notes for Windows & Linux users
- **VERY IMPORTANT**: Check out [the Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites) before proceeding.
- For Windows users who had been installed `Win10 SDK (10.0.19041.0)` and `Visual Studio build tool 2022(>=17.2)`, you may need to install these additionally:
1. Microsoft Visual C++ 2015-2022 Redistributable (x64)
2. Microsoft Visual C++ 2015-2022 Redistributable (x86)
3. Microsoft Visual C++ 2012 Redistributable (x86) (optional)
4. Microsoft Visual C++ 2013 Redistributable (x86) (optional)
5. Microsoft Visual C++ 2008 Redistributable (x86) (optional)
- For Ubuntu users, run the following commands to install the required libraries before compiling:
```bash
sudo apt install libdbus-1-dev \
libsoup2.4-dev \
libjavascriptcoregtk-4.0-dev \
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
```
## Usage
```bash
pake url [options]
```
The packaged application will be placed in the current working directory by default. Since the environment needs to be configured for the first packaging, it will take some time. Please wait patiently.
> **Note**:
> The Rust environment is required for packaging. If you have not installed Rust, you will be prompted to confirm the installation. If the installation fails or times out, you can [install](https://www.rust-lang.org/tools/install) by yourself.
### url
The url🔗 is the link to the website you want to package. Required.
### [options]
We provide some options for customization. When packaging, the corresponding arguments can be passed to configure your app.
#### [name]
The name of your application. We will prompt you to enter this if you do not provide it in this phase. Input must be in English.
```shell
--name <value>
# or
-n <value>
```
#### [icon]
The application icon. Supports local and remote files. By default, it is the brand icon of Pake. For customizing the icon of your product, go to [icon icons](https://icon-icons.com) or [macOSicons](https://macosicons.com/#/) to download it.
- macOS must be `.icns`
- Windows must be `.ico`
- Linux must be `.png`
```shell
--icon <path>
# or
-i <path>
```
#### [height]
The height of the packaged application window. The default is `780px`.
```shell
--height <number>
# or
-h <number>
```
#### [width]
The width of the packaged application window. The default is `1200px`.
```shell
--width <number>
# or
-w <number>
```
#### [transparent]
Whether to enable the immersive header. The default is `false`. Use the command below to enable this feature.
```shell
--transparent
```
#### [fullscreen]
Indicates if the window should be full screen on application launch. The default is `false`.
Use the command below to enable this feature.
```shell
--fullscreen
```
#### [resize]
Indicates if the window can be resized. The default value is `true`.
Use the command below to disable this feature.
```shell
--no-resizable
#or
-r
```
#### [multi-arch]
Package results support both Intel and m1 chips, only for MacOS. The default is `false`.
```shell
--targets xxx
```
##### Preparation
- Note: After enabling this option, you need to use rustup on the rust official website to install rust, brew installation is not supported.
- For intel chip users, you need to install the arm64 cross-platform package to make the installation package support the m1 chip, and use the following command to install.
```shell
rustup target add aarch64-apple-darwin
```
- For M1 chip users, you need to install the x86 cross-platform package to make the installation package support the interl chip, and use the following command to install.
```shell
rustup target add x86_64-apple-darwin
```
##### Instructions
```shell
--multi-arch
# or
-m
```
#### [targets]
Select the output package format, support deb/appimage/all, if all is selected, deb and appimage will be packaged at the same time, this option only supports Linux, the default is `all`.
#### [user-agent]
Custom browser user agent, default is empty.
```shell
--user-agent <value>
```
#### [show-menu]
Display the menu bar, not display it by default, enter the following command and it will be displayed. MacOS users are recommended to enable.
```shell
--show-menu
```
#### [show-system-tray]
Display the notification tray, not display it by default, entering the following command will display.
```shell
--show-system-tray <value>
```
#### [system-tray-icon]
The notification tray icon is only valid when the notification tray is displayed. The icon must be a 32x32~256x256 pixel image in .ico or .png format.
```shell
--system-tray-icon <value>
```
#### [copy-iter-file]
Recursive copy, when the url is a local file path, if this option is enabled, the folder where the url path file is located and all sub-files are copied to the pake static folder, which is not enabled by default
```shell
--copy-iter-file
```

111
bin/builders/BaseBuilder.ts vendored Normal file
View File

@@ -0,0 +1,111 @@
import path from 'path';
import fsExtra from 'fs-extra';
import chalk from 'chalk';
import prompts from 'prompts';
import { PakeAppOptions } from '@/types';
import { checkRustInstalled, installRust } from '@/helpers/rust';
import { mergeConfig } from '@/helpers/merge';
import tauriConfig from '@/helpers/tauriConfig';
import { npmDirectory } from '@/utils/dir';
import { getSpinner } from '@/utils/info';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';
import { IS_MAC } from '@/utils/platform';
import logger from '@/options/logger';
export default abstract class BaseBuilder {
protected options: PakeAppOptions;
protected constructor(options: PakeAppOptions) {
this.options = options;
}
async prepare() {
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
const tauriTargetPath = path.join(tauriSrcPath, 'target');
const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath);
if (!IS_MAC && !tauriTargetPathExists) {
logger.info('✺ The first use requires installing system dependencies.');
logger.info('✺ See more in https://tauri.app/v1/guides/getting-started/prerequisites.');
}
if (!checkRustInstalled()) {
const res = await prompts({
type: 'confirm',
message: 'Rust not detected. Install now?',
name: 'value',
});
if (res.value) {
await installRust();
} else {
logger.error('✕ Rust required to package your webapp.');
process.exit(0);
}
}
const isChina = await isChinaDomain('www.npmjs.com');
const spinner = getSpinner('Installing package...');
const rustProjectDir = path.join(tauriSrcPath, '.cargo');
const projectConf = path.join(rustProjectDir, 'config');
await fsExtra.ensureDir(rustProjectDir);
if (isChina) {
logger.info('✺ Located in China, using npm/rsProxy CN mirror.');
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
await fsExtra.copy(projectCnConf, projectConf);
await shellExec(`cd "${npmDirectory}" && npm install --registry=https://registry.npmmirror.com`);
} else {
await shellExec(`cd "${npmDirectory}" && npm install`);
}
spinner.succeed(chalk.green('Package installed!'));
if (!tauriTargetPathExists) {
logger.warn('✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
}
}
async build(url: string) {
await this.buildAndCopy(url, this.options.targets);
}
async buildAndCopy(url: string, target: string) {
const { name } = this.options;
await mergeConfig(url, this.options, tauriConfig);
// Build app
const spinner = getSpinner('Building app...');
setTimeout(() => spinner.stop(), 3000);
await shellExec(`cd ${npmDirectory} && ${this.getBuildCommand()}`);
// Copy app
const fileName = this.getFileName();
const fileType = this.getFileType(target);
const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
const distPath = path.resolve(`${name}.${fileType}`);
await fsExtra.copy(appPath, distPath);
await fsExtra.remove(appPath);
logger.success('✔ Build success!');
logger.success('✔ App installer located in', distPath);
}
protected getFileType(target: string): string {
return target;
}
abstract getFileName(): string;
protected getBuildCommand(): string {
return 'npm run build';
}
protected getBasePath(): string {
return 'src-tauri/target/release/bundle/';
}
protected getBuildAppPath(npmDirectory: string, fileName: string, fileType: string): string {
return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`);
}
}

View File

@@ -1,20 +0,0 @@
import { IS_MAC, IS_WIN, IS_LINUX } from '@/utils/platform.js';
import { IBuilder } from './base.js';
import MacBuilder from './MacBuilder.js';
import WinBuilder from './WinBulider.js';
import LinuxBuilder from './LinuxBuilder.js';
export default class BuilderFactory {
static create(): IBuilder {
if (IS_MAC) {
return new MacBuilder();
}
if (IS_WIN) {
return new WinBuilder();
}
if (IS_LINUX) {
return new LinuxBuilder();
}
throw new Error('The current system does not support!!');
}
}

23
bin/builders/BuilderProvider.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
import BaseBuilder from './BaseBuilder';
import MacBuilder from './MacBuilder';
import WinBuilder from './WinBuilder';
import LinuxBuilder from './LinuxBuilder';
import { PakeAppOptions } from '@/types';
const { platform } = process;
const buildersMap: Record<string, new (options: PakeAppOptions) => BaseBuilder> = {
darwin: MacBuilder,
win32: WinBuilder,
linux: LinuxBuilder,
};
export default class BuilderProvider {
static create(options: PakeAppOptions): BaseBuilder {
const Builder = buildersMap[platform];
if (!Builder) {
throw new Error('The current system is not supported!');
}
return new Builder(options);
}
}

View File

@@ -1,101 +1,32 @@
import fs from 'fs/promises';
import path from 'path';
import prompts from 'prompts';
import { checkRustInstalled, installRust } from '@/helpers/rust.js';
import { PakeAppOptions } from '@/types.js';
import { IBuilder } from './base.js';
import { shellExec } from '@/utils/shell.js';
import {isChinaDomain} from '@/utils/ip_addr.js';
// @ts-expect-error 加上resolveJsonModule rollup会打包报错
// import tauriConf from '../../src-tauri/tauri.windows.conf.json';
import tauriConf from './tauriConf.js';
import BaseBuilder from './BaseBuilder';
import { PakeAppOptions } from '@/types';
import tauriConfig from '@/helpers/tauriConfig';
import { fileURLToPath } from 'url';
import logger from '@/options/logger.js';
import { mergeTauriConfig } from './common.js';
import { npmDirectory } from '@/utils/dir.js';
export default class LinuxBuilder implements IBuilder {
async prepare() {
logger.info(
'To build the Linux app, you need to install Rust and Linux package'
);
logger.info(
'See more in https://tauri.app/v1/guides/getting-started/prerequisites#installing\n'
);
if (checkRustInstalled()) {
return;
}
const res = await prompts({
type: 'confirm',
message: 'We detected that you have not installed Rust. Install it now?',
name: 'value',
});
if (res.value) {
// TODO 国内有可能会超时
await installRust();
} else {
logger.error('Error: Pake needs Rust to package your webapp!!!');
process.exit(2);
}
export default class LinuxBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) {
super(options);
}
async build(url: string, options: PakeAppOptions) {
logger.debug('PakeAppOptions', options);
const { name } = options;
await mergeTauriConfig(url, options, tauriConf);
const isChina = await isChinaDomain("www.npmjs.com");
if (isChina) {
logger.info("it's in China, use npm/rust cn mirror")
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const e1 = fs.access(rust_project_dir);
if (e1) {
await fs.mkdir(rust_project_dir, { recursive: true });
getFileName(): string {
const { name } = this.options;
const arch = process.arch === 'x64' ? 'amd64' : process.arch;
return `${name}_${tauriConfig.package.version}_${arch}`;
}
// Customize it, considering that there are all targets.
async build(url: string) {
const targetTypes = ['deb', 'appimage'];
for (const target of targetTypes) {
if (this.options.targets === target || this.options.targets === 'all') {
await this.buildAndCopy(url, target);
}
const project_cn_conf = path.join(npmDirectory, "src-tauri", "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs.copyFile(project_cn_conf, project_conf);
const _ = await shellExec(
`cd "${npmDirectory}" && npm install --registry=https://registry.npmmirror.com && npm run build`
);
} else {
const _ = await shellExec(`cd "${npmDirectory}" && npm install && npm run build`);
}
let arch: string;
if (process.arch === "x64") {
arch = "amd64";
} else {
arch = process.arch;
}
if (options.targets === "deb" || options.targets === "all") {
const debName = `${name}_${tauriConf.package.version}_${arch}.deb`;
const appPath = this.getBuildAppPath(npmDirectory, "deb", debName);
const distPath = path.resolve(`${name}.deb`);
await fs.copyFile(appPath, distPath);
await fs.unlink(appPath);
logger.success('Build Deb success!');
logger.success('You can find the deb app installer in', distPath);
}
if (options.targets === "appimage" || options.targets === "all") {
const appImageName = `${name}_${tauriConf.package.version}_${arch}.AppImage`;
const appImagePath = this.getBuildAppPath(npmDirectory, "appimage", appImageName);
const distAppPath = path.resolve(`${name}.AppImage`);
await fs.copyFile(appImagePath, distAppPath);
await fs.unlink(appImagePath);
logger.success('Build AppImage success!');
logger.success('You can find the AppImage app installer in', distAppPath);
}
}
getBuildAppPath(npmDirectory: string, packageType: string, packageName: string) {
return path.join(
npmDirectory,
'src-tauri/target/release/bundle/',
packageType,
packageName
);
protected getFileType(target: string): string {
if (target === 'appimage') {
return 'AppImage';
}
return super.getFileType(target);
}
}

View File

@@ -1,99 +1,31 @@
import fs from 'fs/promises';
import path from 'path';
import prompts from 'prompts';
import { checkRustInstalled, installRust } from '@/helpers/rust.js';
import { PakeAppOptions } from '@/types.js';
import { IBuilder } from './base.js';
import { shellExec } from '@/utils/shell.js';
// @ts-expect-error 加上resolveJsonModule rollup会打包报错
// import tauriConf from '../../src-tauri/tauri.macos.conf.json';
import tauriConf from './tauriConf.js';
import log from 'loglevel';
import { mergeTauriConfig } from './common.js';
import { npmDirectory } from '@/utils/dir.js';
import {isChinaDomain} from '@/utils/ip_addr.js';
import logger from '@/options/logger.js';
import tauriConfig from '@/helpers/tauriConfig';
import { PakeAppOptions } from '@/types';
import BaseBuilder from './BaseBuilder';
export default class MacBuilder implements IBuilder {
async prepare() {
if (checkRustInstalled()) {
return;
}
const res = await prompts({
type: 'confirm',
message: 'We detected that you have not installed Rust. Install it now?',
name: 'value',
});
if (res.value) {
// TODO 国内有可能会超时
await installRust();
} else {
log.error('Error: Pake need Rust to package your webapp!!!');
process.exit(2);
}
export default class MacBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) {
super(options);
this.options.targets = 'dmg';
}
async build(url: string, options: PakeAppOptions) {
log.debug('PakeAppOptions', options);
const { name } = options;
await mergeTauriConfig(url, options, tauriConf);
let dmgName: string;
if (options.multiArch) {
const isChina = await isChinaDomain("www.npmjs.com");
if (isChina) {
logger.info("it's in China, use npm/rust cn mirror")
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const e1 = fs.access(rust_project_dir);
if (e1) {
await fs.mkdir(rust_project_dir, { recursive: true });
}
const project_cn_conf = path.join(npmDirectory, "src-tauri", "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs.copyFile(project_cn_conf, project_conf);
const _ = await shellExec(
`cd "${npmDirectory}" && npm install --registry=https://registry.npmmirror.com && npm run build:mac`
);
} else {
const _ = await shellExec(`cd "${npmDirectory}" && npm install && npm run build:mac`);
}
dmgName = `${name}_${tauriConf.package.version}_universal.dmg`;
getFileName(): string {
const { name } = this.options;
let arch: string;
if (this.options.multiArch) {
arch = 'universal';
} else {
const isChina = isChinaDomain("www.npmjs.com")
if (isChina) {
const _ = await shellExec(
`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`
);
} else {
const _ = await shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
let arch = "x64";
if (process.arch === "arm64") {
arch = "aarch64";
} else {
arch = process.arch;
}
dmgName = `${name}_${tauriConf.package.version}_${arch}.dmg`;
arch = process.arch === 'arm64' ? 'aarch64' : process.arch;
}
const appPath = this.getBuildAppPath(npmDirectory, dmgName, options.multiArch);
const distPath = path.resolve(`${name}.dmg`);
await fs.copyFile(appPath, distPath);
await fs.unlink(appPath);
logger.success('Build success!');
logger.success('You can find the app installer in', distPath);
return `${name}_${tauriConfig.package.version}_${arch}`;
}
getBuildAppPath(npmDirectory: string, dmgName: string, multiArch: boolean) {
let dmgPath: string;
if (multiArch) {
dmgPath = 'src-tauri/target/universal-apple-darwin/release/bundle/dmg';
} else {
dmgPath = 'src-tauri/target/release/bundle/dmg';
}
return path.join(npmDirectory, dmgPath, dmgName);
protected getBuildCommand(): string {
return this.options.multiArch ? 'npm run build:mac' : super.getBuildCommand();
}
protected getBasePath(): string {
return this.options.multiArch
? 'src-tauri/target/universal-apple-darwin/release/bundle'
: super.getBasePath();
}
}

17
bin/builders/WinBuilder.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import BaseBuilder from './BaseBuilder';
import { PakeAppOptions } from '@/types';
import tauriConfig from '@/helpers/tauriConfig';
export default class WinBuilder extends BaseBuilder {
constructor(options: PakeAppOptions) {
super(options);
this.options.targets = 'msi';
}
getFileName(): string {
const { name } = this.options;
const { arch } = process;
const language = tauriConfig.tauri.bundle.windows.wix.language[0];
return `${name}_${tauriConfig.package.version}_${arch}_${language}`;
}
}

View File

@@ -1,85 +0,0 @@
import fs from 'fs/promises';
import path from 'path';
import prompts from 'prompts';
import { checkRustInstalled, installRust } from '@/helpers/rust.js';
import { PakeAppOptions } from '@/types.js';
import { IBuilder } from './base.js';
import { shellExec } from '@/utils/shell.js';
// @ts-expect-error 加上resolveJsonModule rollup会打包报错
// import tauriConf from '../../src-tauri/tauri.windows.conf.json';
import tauriConf from './tauriConf.js';
import { fileURLToPath } from 'url';
import logger from '@/options/logger.js';
import { mergeTauriConfig } from './common.js';
import { npmDirectory } from '@/utils/dir.js';
import {isChinaDomain} from '@/utils/ip_addr.js';
export default class WinBuilder implements IBuilder {
async prepare() {
logger.info(
'To build the Windows app, you need to install Rust and VS Build Tools.'
);
logger.info(
'See more in https://tauri.app/v1/guides/getting-started/prerequisites#installing\n'
);
if (checkRustInstalled()) {
return;
}
const res = await prompts({
type: 'confirm',
message: 'We detected that you have not installed Rust. Install it now?',
name: 'value',
});
if (res.value) {
// TODO 国内有可能会超时
await installRust();
} else {
logger.error('Error: Pake needs Rust to package your webapp!!!');
process.exit(2);
}
}
async build(url: string, options: PakeAppOptions) {
logger.debug('PakeAppOptions', options);
const { name } = options;
await mergeTauriConfig(url, options, tauriConf);
const isChina = await isChinaDomain("www.npmjs.com")
if (isChina) {
logger.info("it's in China, use npm/rust cn mirror")
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const e1 = fs.access(rust_project_dir);
if (e1) {
await fs.mkdir(rust_project_dir, { recursive: true });
}
const project_cn_conf = path.join(npmDirectory, "src-tauri", "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs.copyFile(project_cn_conf, project_conf);
const _ = await shellExec(
`cd "${npmDirectory}" && npm install --registry=https://registry.npmmirror.com && npm run build`
);
} else {
const _ = await shellExec(`cd "${npmDirectory}" && npm install && npm run build`);
}
const language = tauriConf.tauri.bundle.windows.wix.language[0];
const arch = process.arch;
const msiName = `${name}_${tauriConf.package.version}_${arch}_${language}.msi`;
const appPath = this.getBuildedAppPath(npmDirectory, msiName);
const distPath = path.resolve(`${name}.msi`);
await fs.copyFile(appPath, distPath);
await fs.unlink(appPath);
logger.success('Build success!');
logger.success('You can find the app installer in', distPath);
}
getBuildedAppPath(npmDirectory: string, dmgName: string) {
return path.join(
npmDirectory,
'src-tauri/target/release/bundle/msi',
dmgName
);
}
}

16
bin/builders/base.ts vendored
View File

@@ -1,16 +0,0 @@
import { PakeAppOptions } from '@/types.js';
/**
* Builder接口
* 不同平台打包过程需要实现 prepare 和 build 方法
*/
export interface IBuilder {
/** 前置检查 */
prepare(): Promise<void>;
/**
* 开始打包
* @param url 打包url
* @param options 配置参数
*/
build(url: string, options: PakeAppOptions): Promise<void>;
}

355
bin/builders/common.ts vendored
View File

@@ -1,355 +0,0 @@
import { PakeAppOptions } from '@/types.js';
import prompts from 'prompts';
import path from 'path';
import fs from 'fs/promises';
import fs2 from 'fs-extra';
import {TauriConfig} from 'tauri/src/types';
import { npmDirectory } from '@/utils/dir.js';
import logger from '@/options/logger.js';
import combineFiles from '@/helpers/combine.js';
type DangerousRemoteDomainIpAccess = {
domain: string;
windows: string[];
enableTauriAPI: boolean;
schema?: string;
plugins?: string[];
}
// https://tauri.app/v1/api/config/#remotedomainaccessscope
type NextTauriConfig = TauriConfig & {
tauri: {
security: {
dangerousRemoteDomainIpcAccess?: DangerousRemoteDomainIpAccess[]
}
}
}
export async function promptText(message: string, initial?: string) {
const response = await prompts({
type: 'text',
name: 'content',
message,
initial,
});
return response.content;
}
function setSecurityConfigWithUrl(tauriConfig: NextTauriConfig, url: string) {
const myURL = new URL(url);
const currentUrlConfig: DangerousRemoteDomainIpAccess = {
domain: myURL.hostname,
windows: ["pake"],
enableTauriAPI: true,
};
tauriConfig.tauri.security.dangerousRemoteDomainIpcAccess = [currentUrlConfig];
}
export async function mergeTauriConfig(
url: string,
options: PakeAppOptions,
tauriConf: any
) {
const {
width,
height,
fullscreen,
transparent,
resizable,
userAgent,
showMenu,
showSystemTray,
systemTrayIcon,
iterCopyFile,
identifier,
name,
inject,
} = options;
const tauriConfWindowOptions = {
width,
height,
fullscreen,
transparent,
resizable,
};
// Package name is valid ?
// for Linux, package name must be a-z, 0-9 or "-", not allow to A-Z and other
if (process.platform === "linux") {
const reg = new RegExp(/[0-9]*[a-z]+[0-9]*\-?[0-9]*[a-z]*[0-9]*\-?[0-9]*[a-z]*[0-9]*/);
if (!reg.test(name) || reg.exec(name)[0].length != name.length) {
logger.error("package name is illegal it must be lowercase letters, numbers, dashes, and it must contain the lowercase letters.")
logger.error("E.g com-123-xxx, 123pan, pan123,weread, we-read");
process.exit();
}
}
if (process.platform === "win32" || process.platform === "darwin" ) {
const reg = new RegExp(/([0-9]*[a-zA-Z]+[0-9]*)+/);
if (!reg.test(name) || reg.exec(name)[0].length != name.length) {
logger.error("package name is illegal it must be letters, numbers, and it must contain the letters")
logger.error("E.g 123pan,123Pan Pan123,weread, WeRead, WERead");
process.exit();
}
}
// logger.warn(JSON.stringify(tauriConf.pake.windows, null, 4));
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
tauriConf.package.productName = name;
tauriConf.tauri.bundle.identifier = identifier;
// 判断一下url类型是文件还是网站
// 如果是文件并且开启了递归拷贝功能则需要将该文件以及所在文件夹下的所有文件拷贝到src目录下否则只拷贝单个文件。
const url_exists = await fs.stat(url)
.then(() => true)
.catch(() => false);
if (url_exists) {
logger.warn("you input may a local file");
tauriConf.pake.windows[0].url_type = "local";
const file_name = path.basename(url);
const dir_name = path.dirname(url);
if (!iterCopyFile) {
const url_path = path.join(npmDirectory,"dist/", file_name);
await fs.copyFile(url, url_path);
} else {
const old_dir = path.join(npmDirectory,"dist/");
const new_dir = path.join(npmDirectory,"dist_bak/");
fs2.moveSync(old_dir, new_dir, {"overwrite": true});
fs2.copySync(dir_name, old_dir, {"overwrite": true});
// logger.warn("dir name", dir_name);
// 将dist_bak里面的cli.js和about_pake.html拷贝回去
const cli_path = path.join(new_dir, "cli.js")
const cli_path_target = path.join(old_dir, "cli.js")
const about_pake_path = path.join(new_dir, "about_pake.html");
const about_pake_path_target = path.join(old_dir, "about_pake.html")
fs.copyFile(cli_path, cli_path_target);
fs.copyFile(about_pake_path, about_pake_path_target);
}
tauriConf.pake.windows[0].url = file_name;
tauriConf.pake.windows[0].url_type = "local";
} else {
tauriConf.pake.windows[0].url_type = "web";
}
// 处理user-agent
if (userAgent.length > 0) {
if (process.platform === "win32") {
tauriConf.pake.user_agent.windows = userAgent;
}
if (process.platform === "linux") {
tauriConf.pake.user_agent.linux = userAgent;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = userAgent;
}
}
// 处理菜单栏
if (showMenu) {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = true;
}
if (process.platform === "linux") {
tauriConf.pake.menu.linux = true;
}
if (process.platform === "darwin") {
tauriConf.pake.menu.macos = true;
}
} else {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = false;
}
if (process.platform === "linux") {
tauriConf.pake.menu.linux = false;
}
if (process.platform === "darwin") {
tauriConf.pake.menu.macos = false;
}
}
// 处理托盘
if (showSystemTray) {
if (process.platform === "win32") {
tauriConf.pake.system_tray.windows = true;
}
if (process.platform === "linux") {
tauriConf.pake.system_tray.linux = true;
}
if (process.platform === "darwin") {
tauriConf.pake.system_tray.macos = true;
}
} else {
if (process.platform === "win32") {
tauriConf.pake.system_tray.windows = false;
}
if (process.platform === "linux") {
tauriConf.pake.system_tray.linux = false;
}
if (process.platform === "darwin") {
tauriConf.pake.system_tray.macos = false;
}
}
// 处理targets 暂时只对linux开放
if (process.platform === "linux") {
delete tauriConf.tauri.bundle.deb.files;
if (["all", "deb", "appimage"].includes(options.targets)) {
if (options.targets === "all") {
tauriConf.tauri.bundle.targets = ["deb", "appimage"];
} else {
tauriConf.tauri.bundle.targets = [options.targets];
}
} else {
logger.warn("targets must be 'all', 'deb', 'appimage', we will use default 'all'");
}
}
// 处理应用图标
const exists = await fs.stat(options.icon)
.then(() => true)
.catch(() => false);
if (exists) {
let updateIconPath = true;
let customIconExt = path.extname(options.icon).toLowerCase();
if (process.platform === "win32") {
if (customIconExt === ".ico") {
const ico_path = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}_32.ico`);
tauriConf.tauri.bundle.resources = [`png/${name.toLowerCase()}_32.ico`];
await fs.copyFile(options.icon, ico_path);
} else {
updateIconPath = false;
logger.warn(`icon file in Windows must be 256 * 256 pix with .ico type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["png/icon_256.ico"];
}
}
if (process.platform === "linux") {
if (customIconExt != ".png") {
updateIconPath = false;
logger.warn(`icon file in Linux must be 512 * 512 pix with .png type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["png/icon_512.png"];
}
}
if (process.platform === "darwin" && customIconExt !== ".icns") {
updateIconPath = false;
logger.warn(`icon file in MacOS must be .icns type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["icons/icon.icns"];
}
if (updateIconPath) {
tauriConf.tauri.bundle.icon = [options.icon];
} else {
logger.warn(`icon file will not change with default.`);
}
} else {
logger.warn("the custom icon path may not exists. we will use default icon to replace it");
if (process.platform === "win32") {
tauriConf.tauri.bundle.icon = ["png/icon_256.ico"];
}
if (process.platform === "linux") {
tauriConf.tauri.bundle.icon = ["png/icon_512.png"];
}
if (process.platform === "darwin") {
tauriConf.tauri.bundle.icon = ["icons/icon.icns"];
}
}
// 处理托盘自定义图标
let useDefaultIcon = true; // 是否使用默认托盘图标
if (systemTrayIcon.length > 0) {
const icon_exists = await fs.stat(systemTrayIcon)
.then(() => true)
.catch(() => false);
if (icon_exists) {
// 需要判断图标格式默认只支持ico和png两种
let iconExt = path.extname(systemTrayIcon).toLowerCase();
if (iconExt == ".png" || iconExt == ".ico") {
useDefaultIcon = false;
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
tauriConf.tauri.systemTray.iconPath = `png/${name.toLowerCase()}${iconExt}`;
await fs.copyFile(systemTrayIcon, trayIcoPath);
} else {
logger.warn(`file type for system tray icon mut be .ico or .png , but you give ${iconExt}`);
logger.warn(`system tray icon file will not change with default.`);
}
} else {
logger.warn(`${systemTrayIcon} not exists!`)
logger.warn(`system tray icon file will not change with default.`);
}
}
// 处理托盘默认图标
if (useDefaultIcon) {
if (process.platform === "linux" || process.platform === "win32") {
tauriConf.tauri.systemTray.iconPath = tauriConf.tauri.bundle.icon[0];
} else {
tauriConf.tauri.systemTray.iconPath = "png/icon_512.png";
}
}
// 设置安全调用 window.__TAURI__ 的安全域名为设置的应用域名
setSecurityConfigWithUrl(tauriConf, url);
let injectFiles: string[] = [];
// 注入外部 js css
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(relativePath => path.join(process.cwd(), relativePath));
injectFiles = injectFiles.concat(...files);
tauriConf.pake.inject = files;
}
combineFiles(injectFiles);
// 保存配置文件
let configPath = "";
switch (process.platform) {
case "win32": {
configPath = path.join(npmDirectory, 'src-tauri/tauri.windows.conf.json');
break;
}
case "darwin": {
configPath = path.join(npmDirectory, 'src-tauri/tauri.macos.conf.json');
break;
}
case "linux": {
configPath = path.join(npmDirectory, 'src-tauri/tauri.linux.conf.json');
break;
}
}
let bundleConf = {tauri: {bundle: tauriConf.tauri.bundle}};
await fs.writeFile(
configPath,
Buffer.from(JSON.stringify(bundleConf, null, 4), 'utf-8')
);
const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json')
await fs.writeFile(
pakeConfigPath,
Buffer.from(JSON.stringify(tauriConf.pake, null, 4), 'utf-8')
);
// logger.info("tauri config", JSON.stringify(tauriConf.build));
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake;
delete tauriConf2.tauri.bundle;
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json')
await fs.writeFile(
configJsonPath,
Buffer.from(JSON.stringify(tauriConf2, null, 4), 'utf-8')
);
}

View File

@@ -1,30 +0,0 @@
import CommonConf from '../../src-tauri/tauri.conf.json';
import pakeConf from '../../src-tauri/pake.json';
import WinConf from '../../src-tauri/tauri.windows.conf.json';
import MacConf from '../../src-tauri/tauri.macos.conf.json';
import LinuxConf from '../../src-tauri/tauri.linux.conf.json';
let tauriConf = {
package: CommonConf.package,
tauri: CommonConf.tauri,
build: CommonConf.build,
pake: pakeConf
}
switch (process.platform) {
case "win32": {
tauriConf.tauri.bundle = WinConf.tauri.bundle;
break;
}
case "darwin": {
tauriConf.tauri.bundle = MacConf.tauri.bundle;
break;
}
case "linux": {
tauriConf.tauri.bundle = LinuxConf.tauri.bundle;
break;
}
}
export default tauriConf;

88
bin/cli.ts vendored
View File

@@ -1,53 +1,49 @@
import chalk from 'chalk';
import { program } from 'commander';
import log from 'loglevel';
import chalk from 'chalk';
import { DEFAULT_PAKE_OPTIONS } from './defaults.js';
import { PakeCliOptions } from './types.js';
import { validateNumberInput, validateUrlInput } from './utils/validate.js';
import handleInputOptions from './options/index.js';
import BuilderFactory from './builders/BuilderFactory.js';
import { checkUpdateTips } from './helpers/updater.js';
// @ts-expect-error
import packageJson from '../package.json';
import logger from './options/logger.js';
import BuilderProvider from './builders/BuilderProvider';
import { DEFAULT_PAKE_OPTIONS as DEFAULT } from './defaults';
import { checkUpdateTips } from './helpers/updater';
import handleInputOptions from './options/index';
program.version(packageJson.version).description('A cli application can package a web page to desktop application.');
import { PakeCliOptions } from './types';
import { validateNumberInput, validateUrlInput } from './utils/validate';
program
.showHelpAfterError()
.argument('[url]', 'the web url you want to package', validateUrlInput)
.option('--name <string>', 'application name')
.option('--icon <string>', 'application icon', DEFAULT_PAKE_OPTIONS.icon)
.option('--height <number>', 'window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
.option('--width <number>', 'window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
.option('--no-resizable', 'whether the window can be resizable', DEFAULT_PAKE_OPTIONS.resizable)
.option('--fullscreen', 'makes the packaged app start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
.option('--transparent', 'transparent title bar', DEFAULT_PAKE_OPTIONS.transparent)
.option('--user-agent <string>', 'custom user agent', DEFAULT_PAKE_OPTIONS.userAgent)
.option('--show-menu', 'show menu in app', DEFAULT_PAKE_OPTIONS.showMenu)
.option('--show-system-tray', 'show system tray in app', DEFAULT_PAKE_OPTIONS.showSystemTray)
.option('--system-tray-icon <string>', 'custom system tray icon', DEFAULT_PAKE_OPTIONS.systemTrayIcon)
.option('--iter-copy-file',
'copy all static file to pake app when url is a local file',
DEFAULT_PAKE_OPTIONS.iterCopyFile)
.option(
'-m, --multi-arch',
"available for Mac only, and supports both Intel and M1",
DEFAULT_PAKE_OPTIONS.multiArch
)
.option(
'--targets <string>',
'only for linux, default is "deb", option "appaimge" or "all"(deb & appimage)',
DEFAULT_PAKE_OPTIONS.targets
)
.option('--debug', 'debug', DEFAULT_PAKE_OPTIONS.transparent)
.option('--inject [injects...]', 'inject .js or .css for this app', DEFAULT_PAKE_OPTIONS.inject)
.description(chalk.green('Pake can turn any webpage into a desktop app with Rust.'))
.usage('[url] [options]')
.showHelpAfterError();
program
.argument('[url]', 'The web URL you want to package', validateUrlInput)
.option('--name <string>', 'Application name')
.option('--icon <string>', 'Application icon', DEFAULT.icon)
.option('--width <number>', 'Window width', validateNumberInput, DEFAULT.width)
.option('--height <number>', 'Window height', validateNumberInput, DEFAULT.height)
.option('--transparent', 'Only for Mac, hide title bar', DEFAULT.transparent)
.option('--fullscreen', 'Start in full screen', DEFAULT.fullscreen)
.option('--user-agent <string>', 'Custom user agent', DEFAULT.userAgent)
.option('--show-menu', 'Show menu in app', DEFAULT.showMenu)
.option('--show-system-tray', 'Show system tray in app', DEFAULT.showSystemTray)
.option('--system-tray-icon <string>', 'Custom system tray icon', DEFAULT.systemTrayIcon)
.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('--targets <string>', 'Only for Linux, option "deb" or "appimage"', DEFAULT.targets)
.option('--inject [injects...]', 'inject .js or .css for this app', DEFAULT.inject)
.option('--debug', 'Debug mode', DEFAULT.debug)
.version(packageJson.version, '-v, --version', 'Output the current version')
.action(async (url: string, options: PakeCliOptions) => {
checkUpdateTips();
await checkUpdateTips();
if (!url) {
// 直接 pake 不需要出现url提示
program.help();
program.outputHelp(str => {
return str
.split('\n')
.filter(line => !/((-h,|--help)|((-v|-V),|--version))\s+.+$/.test(line))
.join('\n');
});
process.exit(0);
}
log.setDefaultLevel('info');
@@ -55,12 +51,12 @@ program
log.setLevel('debug');
}
const builder = BuilderFactory.create();
await builder.prepare();
// logger.warn("you input url is ", url);
const appOptions = await handleInputOptions(options, url);
// logger.info(JSON.stringify(appOptions, null, 4));
builder.build(url, appOptions);
log.debug('PakeAppOptions', appOptions);
const builder = BuilderProvider.create(appOptions);
await builder.prepare();
await builder.build(url);
});
program.parse();

2
bin/defaults.ts vendored
View File

@@ -17,5 +17,3 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
debug: false,
inject: [],
};
export const DEFAULT_APP_NAME = 'Pake';

194
bin/helpers/merge.ts vendored Normal file
View File

@@ -0,0 +1,194 @@
import path from 'path';
import fsExtra from 'fs-extra';
import { npmDirectory } from '@/utils/dir';
import logger from '@/options/logger';
import { PakeAppOptions, PlatformMap } from '@/types';
export async function mergeConfig(url: string, options: PakeAppOptions, tauriConf: any) {
const {
width,
height,
fullscreen,
transparent,
userAgent,
showMenu,
showSystemTray,
systemTrayIcon,
iterCopyFile,
identifier,
name,
resizable = true,
} = options;
const { platform } = process;
// Set Windows parameters.
const tauriConfWindowOptions = {
width,
height,
fullscreen,
transparent,
resizable,
};
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
tauriConf.package.productName = name;
tauriConf.tauri.bundle.identifier = identifier;
//Judge the type of URL, whether it is a file or a website.
const pathExists = await fsExtra.pathExists(url);
if (pathExists) {
logger.warn('✼ Your input might be a local file.');
tauriConf.pake.windows[0].url_type = 'local';
const fileName = path.basename(url);
const dirName = path.dirname(url);
const distDir = path.join(npmDirectory, 'dist');
const distBakDir = path.join(npmDirectory, 'dist_bak');
if (!iterCopyFile) {
const urlPath = path.join(distDir, fileName);
await fsExtra.copy(url, urlPath);
} else {
fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
fsExtra.copySync(dirName, distDir, { overwrite: true });
const filesToCopyBack = ['cli.js', 'about_pake.html'];
await Promise.all(
filesToCopyBack.map(file =>
fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file)),
),
);
}
tauriConf.pake.windows[0].url = fileName;
tauriConf.pake.windows[0].url_type = 'local';
} else {
tauriConf.pake.windows[0].url_type = 'web';
// 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;
}
const platformMap: PlatformMap = {
win32: 'windows',
linux: 'linux',
darwin: 'macos',
};
const currentPlatform = platformMap[platform];
if (userAgent.length > 0) {
tauriConf.pake.user_agent[currentPlatform] = userAgent;
}
tauriConf.pake.menu[currentPlatform] = showMenu;
tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
// Processing targets are currently only open to Linux.
if (platform === 'linux') {
delete tauriConf.tauri.bundle.deb.files;
const validTargets = ['all', 'deb', 'appimage'];
if (validTargets.includes(options.targets)) {
tauriConf.tauri.bundle.targets =
options.targets === 'all' ? ['deb', 'appimage'] : [options.targets];
} else {
logger.warn(
`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`,
);
}
}
// Set icon.
const platformIconMap: PlatformMap = {
win32: {
fileExt: '.ico',
path: `png/${name.toLowerCase()}_256.ico`,
defaultIcon: 'png/icon_256.ico',
message: 'Windows icon must be .ico and 256x256px.',
},
linux: {
fileExt: '.png',
path: `png/${name.toLowerCase()}_512.png`,
defaultIcon: 'png/icon_512.png',
message: 'Linux icon must be .png and 512x512px.',
},
darwin: {
fileExt: '.icns',
path: `icons/${name.toLowerCase()}.icns`,
defaultIcon: 'icons/icon.icns',
message: 'macOS icon must be .icns type.',
},
};
const iconInfo = platformIconMap[platform];
const exists = await fsExtra.pathExists(options.icon);
if (exists) {
let updateIconPath = true;
let customIconExt = path.extname(options.icon).toLowerCase();
if (customIconExt !== iconInfo.fileExt) {
updateIconPath = false;
logger.warn(`${iconInfo.message}, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = [iconInfo.defaultIcon];
} else {
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
tauriConf.tauri.bundle.resources = [iconInfo.path];
await fsExtra.copy(options.icon, iconPath);
}
if (updateIconPath) {
tauriConf.tauri.bundle.icon = [options.icon];
} else {
logger.warn(`✼ Icon will remain as default.`);
}
} else {
logger.warn('✼ Custom icon path may be invalid, default icon will be used instead.');
tauriConf.tauri.bundle.icon = [iconInfo.defaultIcon];
}
// Set tray icon path.
let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.tauri.bundle.icon[0];
if (systemTrayIcon.length > 0) {
try {
await fsExtra.pathExists(systemTrayIcon);
// 需要判断图标格式默认只支持ico和png两种
let iconExt = path.extname(systemTrayIcon).toLowerCase();
if (iconExt == '.png' || iconExt == '.ico') {
const trayIcoPath = path.join(
npmDirectory,
`src-tauri/png/${name.toLowerCase()}${iconExt}`,
);
trayIconPath = `png/${name.toLowerCase()}${iconExt}`;
await fsExtra.copy(systemTrayIcon, trayIcoPath);
} else {
logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
logger.warn(`✼ Default system tray icon will be used.`);
}
} catch {
logger.warn(`${systemTrayIcon} not exists!`);
logger.warn(`✼ Default system tray icon will remain unchanged.`);
}
}
tauriConf.tauri.systemTray.iconPath = trayIconPath;
// Save config file.
const platformConfigPaths: PlatformMap = {
win32: 'src-tauri/tauri.windows.conf.json',
darwin: 'src-tauri/tauri.macos.conf.json',
linux: 'src-tauri/tauri.linux.conf.json',
};
const configPath = path.join(npmDirectory, platformConfigPaths[platform]);
const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } };
await fsExtra.writeJson(configPath, bundleConf, { spaces: 4 });
const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json');
await fsExtra.writeJson(pakeConfigPath, tauriConf.pake, { spaces: 4 });
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake;
delete tauriConf2.tauri.bundle;
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json');
await fsExtra.writeJson(configJsonPath, tauriConf2, { spaces: 4 });
}

41
bin/helpers/rust.ts vendored
View File

@@ -1,31 +1,26 @@
import { IS_WIN } from '@/utils/platform.js';
import ora from 'ora';
import chalk from 'chalk';
import shelljs from 'shelljs';
import logger from '@/options/logger.js';
import { shellExec } from '../utils/shell.js';
import {isChinaDomain} from '@/utils/ip_addr.js'
import { getSpinner } from '@/utils/info';
import { IS_WIN } from '@/utils/platform';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';
export async function installRust() {
const is_china = await isChinaDomain("sh.rustup.rs");
let RustInstallScriptFocMac = "";
if (is_china) {
logger.info("it's in China, use rust cn mirror to install rust");
RustInstallScriptFocMac =
'export RUSTUP_DIST_SERVER="https://rsproxy.cn" && export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup" && curl --proto "=https" --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh';
} else {
RustInstallScriptFocMac =
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
}
const RustInstallScriptForWin = 'winget install --id Rustlang.Rustup';
const spinner = ora('Downloading Rust').start();
try {
await shellExec(IS_WIN ? RustInstallScriptForWin : RustInstallScriptFocMac);
spinner.succeed();
} catch (error) {
console.error('install rust return code', error.message);
spinner.fail();
const isInChina = await isChinaDomain('sh.rustup.rs');
const rustInstallScriptForMac = isInChina
? 'export RUSTUP_DIST_SERVER="https://rsproxy.cn" && export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup" && curl --proto "=https" --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh'
: "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
const spinner = getSpinner('Downloading Rust...');
try {
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
spinner.succeed(chalk.green('Rust installed successfully!'));
} catch (error) {
console.error('Error installing Rust:', error.message);
spinner.fail(chalk.red('Rust installation failed!'));
process.exit(1);
}
}

View File

@@ -1,8 +1,27 @@
import crypto from 'crypto';
import pakeConf from '../../src-tauri/pake.json';
import CommonConf from '../../src-tauri/tauri.conf.json';
import WinConf from '../../src-tauri/tauri.windows.conf.json';
import MacConf from '../../src-tauri/tauri.macos.conf.json';
import LinuxConf from '../../src-tauri/tauri.linux.conf.json';
export function getIdentifier(name: string, url: string) {
const hash = crypto.createHash('md5');
hash.update(url);
const postFixHash = hash.digest('hex').substring(0, 6);
return `pake-${postFixHash}`;
}
const platformConfigs = {
win32: WinConf,
darwin: MacConf,
linux: LinuxConf,
};
const { platform } = process;
// @ts-ignore
const platformConfig = platformConfigs[platform];
let tauriConfig = {
tauri: {
...CommonConf.tauri,
bundle: platformConfig.tauri.bundle,
},
package: CommonConf.package,
build: CommonConf.build,
pake: pakeConf,
};
export default tauriConfig;

View File

@@ -1,7 +1,6 @@
import updateNotifier from 'update-notifier';
// @ts-expect-error
import packageJson from '../../package.json';
export async function checkUpdateTips() {
updateNotifier({ pkg: packageJson }).notify();
updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify();
}

117
bin/options/icon.ts vendored
View File

@@ -1,98 +1,59 @@
import axios from 'axios';
import { fileTypeFromBuffer } from 'file-type';
import { PakeAppOptions } from '../types.js';
import { dir } from 'tmp-promise';
import path from 'path';
import fs from 'fs/promises';
import logger from './logger.js';
import { npmDirectory } from '@/utils/dir.js';
import { IS_LINUX, IS_WIN } from '@/utils/platform.js';
import axios from 'axios';
import fsExtra from 'fs-extra';
import chalk from 'chalk';
import { dir } from 'tmp-promise';
export async function handleIcon(options: PakeAppOptions, url: string) {
import logger from './logger';
import { npmDirectory } from '@/utils/dir';
import { IS_LINUX, IS_WIN } from '@/utils/platform';
import { getSpinner } from '@/utils/info';
import { fileTypeFromBuffer } from 'file-type';
import { PakeAppOptions } from '@/types';
export async function handleIcon(options: PakeAppOptions) {
if (options.icon) {
if (options.icon.startsWith('http')) {
return downloadIcon(options.icon);
} else {
return path.resolve(options.icon);
}
}
if (!options.icon) {
return getDefaultIcon();
} else {
logger.warn('✼ No icon given, default in use. For a custom icon, use --icon option.');
const iconPath = IS_WIN
? 'src-tauri/png/icon_256.ico'
: IS_LINUX
? 'src-tauri/png/icon_512.png'
: 'src-tauri/icons/icon.icns';
return path.join(npmDirectory, iconPath);
}
}
export async function getDefaultIcon() {
logger.info('You have not provided an app icon, use the default icon.(use --icon option to assign an icon)')
let iconPath = 'src-tauri/icons/icon.icns';
if (IS_WIN) {
iconPath = 'src-tauri/png/icon_256.ico';
} else if (IS_LINUX) {
iconPath = 'src-tauri/png/icon_512.png';
}
return path.join(npmDirectory, iconPath);
}
// export async function getIconFromPageUrl(url: string) {
// const icon = await pageIcon(url);
// console.log(icon);
// if (icon.ext === '.ico') {
// const a = await ICO.parse(icon.data);
// icon.data = Buffer.from(a[0].buffer);
// }
// const iconDir = (await dir()).path;
// const iconPath = path.join(iconDir, `/icon.icns`);
// const out = png2icons.createICNS(icon.data, png2icons.BILINEAR, 0);
// await fs.writeFile(iconPath, out);
// return iconPath;
// }
// export async function getIconFromMacosIcons(name: string) {
// const data = {
// query: name,
// filters: 'approved:true',
// hitsPerPage: 10,
// page: 1,
// };
// const res = await axios.post('https://p1txh7zfb3-2.algolianet.com/1/indexes/macOSicons/query?x-algolia-agent=Algolia%20for%20JavaScript%20(4.13.1)%3B%20Browser', data, {
// headers: {
// 'x-algolia-api-key': '0ba04276e457028f3e11e38696eab32c',
// 'x-algolia-application-id': 'P1TXH7ZFB3',
// },
// });
// if (!res.data.hits.length) {
// return '';
// } else {
// return downloadIcon(res.data.hits[0].icnsUrl);
// }
// }
export async function downloadIcon(iconUrl: string) {
let iconResponse;
const spinner = getSpinner('Downloading icon...');
try {
iconResponse = await axios.get(iconUrl, {
responseType: 'arraybuffer',
});
const iconResponse = await axios.get(iconUrl, { responseType: 'arraybuffer' });
const iconData = await iconResponse.data;
if (!iconData) {
return null;
}
const fileDetails = await fileTypeFromBuffer(iconData);
if (!fileDetails) {
return null;
}
const { path: tempPath } = await dir();
const iconPath = `${tempPath}/icon.${fileDetails.ext}`;
await fsExtra.outputFile(iconPath, iconData);
spinner.succeed(chalk.green('Icon downloaded successfully!'));
return iconPath;
} catch (error) {
spinner.fail(chalk.red('Icon download failed!'));
if (error.response && error.response.status === 404) {
return null;
}
throw error;
}
const iconData = await iconResponse.data;
if (!iconData) {
return null;
}
const fileDetails = await fileTypeFromBuffer(iconData);
if (!fileDetails) {
return null;
}
const { path } = await dir();
const iconPath = `${path}/icon.${fileDetails.ext}`;
await fs.writeFile(iconPath, iconData);
return iconPath;
}

70
bin/options/index.ts vendored
View File

@@ -1,29 +1,61 @@
import { promptText } from '@/builders/common.js';
import { getDomain } from '@/utils/url.js';
import { getIdentifier } from '../helpers/tauriConfig.js';
import { PakeAppOptions, PakeCliOptions } from '../types.js';
import { handleIcon } from './icon.js';
import fs from 'fs/promises';
import fsExtra from 'fs-extra';
import logger from '@/options/logger';
export default async function handleOptions(options: PakeCliOptions, url: string): Promise<PakeAppOptions> {
const appOptions: PakeAppOptions = {
...options,
identifier: '',
import { handleIcon } from './icon';
import { getDomain } from '@/utils/url';
import { getIdentifier, promptText, capitalizeFirstLetter } from '@/utils/info';
import { PakeAppOptions, PakeCliOptions, PlatformMap } from '@/types';
function resolveAppName(name: string, platform: NodeJS.Platform): string {
const domain = getDomain(name) || 'pake';
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
}
function isValidName(name: string, platform: NodeJS.Platform): boolean {
const platformRegexMapping: PlatformMap = {
linux: /^[a-z0-9]+(-[a-z0-9]+)*$/,
default: /^[a-zA-Z0-9]+$/,
};
const url_exists = await fs.stat(url)
.then(() => true)
.catch(() => false);
if (!appOptions.name) {
if (!url_exists) {
appOptions.name = await promptText('please input your application name', getDomain(url));
const reg = platformRegexMapping[platform] || platformRegexMapping.default;
return !!name && reg.test(name);
}
export default async function handleOptions(
options: PakeCliOptions,
url: string,
): Promise<PakeAppOptions> {
const { platform } = process;
const isActions = process.env.GITHUB_ACTIONS;
let name = options.name;
const pathExists = await fsExtra.pathExists(url);
if (!options.name) {
const defaultName = pathExists ? '' : resolveAppName(url, platform);
const promptMessage = 'Enter your application name';
const namePrompt = await promptText(promptMessage, defaultName);
name = namePrompt || defaultName;
}
if (!isValidName(name, platform)) {
const LINUX_NAME_ERROR = `✕ name should only include lowercase letters, numbers, and dashes, and must contain at least one lowercase letter. Examples: com-123-xxx, 123pan, pan123, weread, we-read.`;
const DEFAULT_NAME_ERROR = `✕ Name should only include letters and numbers, and must contain at least one letter. Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead.`;
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
logger.error(errorMsg);
if (isActions) {
name = resolveAppName(url, platform);
logger.warn(`✼ Inside github actions, use the default name: ${name}`);
} else {
appOptions.name = await promptText('please input your application name', "");
process.exit(1);
}
}
appOptions.identifier = getIdentifier(appOptions.name, url);
const appOptions: PakeAppOptions = {
...options,
name,
identifier: getIdentifier(url),
};
appOptions.icon = await handleIcon(appOptions, url);
appOptions.icon = await handleIcon(appOptions);
return appOptions;
}

12
bin/options/logger.ts vendored
View File

@@ -1,22 +1,22 @@
import log from 'loglevel';
import chalk from 'chalk';
import log from 'loglevel';
const logger = {
info(...msg: any[]) {
log.info(...msg.map((m) => chalk.blue.bold(m)));
log.info(...msg.map(m => chalk.white(m)));
},
debug(...msg: any[]) {
log.debug(...msg);
},
error(...msg: any[]) {
log.error(...msg.map((m) => chalk.red.bold(m)));
log.error(...msg.map(m => chalk.red(m)));
},
warn(...msg: any[]) {
log.info(...msg.map((m) => chalk.yellow.bold(m)));
log.info(...msg.map(m => chalk.yellow(m)));
},
success(...msg: any[]) {
log.info(...msg.map((m) => chalk.green.bold(m)));
}
log.info(...msg.map(m => chalk.green(m)));
},
};
export default logger;

34
bin/types.ts vendored
View File

@@ -1,47 +1,51 @@
export interface PlatformMap {
[key: string]: any;
}
export interface PakeCliOptions {
/** 应用名称 */
// Application name
name?: string;
/** 应用icon */
// Application icon
icon: string;
/** 应用窗口宽度,默认 1200px */
// Application window width, default 1200px
width: number;
/** 应用窗口高度,默认 780px */
// Application window height, default 780px
height: number;
/** 是否可以拖动默认true */
// Whether the window is resizable, default true
resizable: boolean;
/** 是否可以全屏,默认 false */
// Whether the window can be fullscreen, default false
fullscreen: boolean;
/** 是否开启沉浸式头部,默认为 false 不开启 ƒ*/
// Enable immersive header, default false
transparent: boolean;
/** 自定义UA默认为不开启 ƒ*/
// Custom User-Agent, default off
userAgent: string;
/** 开启菜单栏MacOS默认开启Windows,Linux默认不开启 ƒ*/
// Enable menu bar, default on for macOS, off for Windows and Linux
showMenu: boolean;
/** 开启系统托盘MacOS默认不开启Windows,Linux默认开启 ƒ*/
// Enable system tray, default off for macOS, on for Windows and Linux
showSystemTray: boolean;
/** 托盘图标, Windows、Linux默认和应用图标共用一样的MacOS需要提别提供, 格式为png或者ico */
// Tray icon, default same as app icon for Windows and Linux, macOS requires separate png or ico
systemTrayIcon: string;
// /** 递归拷贝当url为本地文件路径时候若开启该选项则将url路径文件所在文件夹以及所有子文件都拷贝到pake静态文件夹默认不开启 */
// Recursive copy, when url is a local file path, if this option is enabled, the url path file and all its subfiles will be copied to the pake static file folder, default off
iterCopyFile: false;
/** mutli arch, Supports both Intel and m1 chips, only for Mac */
// Multi arch, supports both Intel and M1 chips, only for Mac
multiArch: boolean;
// 包输出产物对linux用户有效默认为deb可选appimage, 或者all即同时输出deb和all;
// Package output, valid for Linux users, default is deb, optional appimage, or all (i.e., output both deb and all);
targets: string;
/** 调试模式,会输出更多日志 */
// Debug mode, outputs more logs
debug: boolean;
/** 需要注入页面的外部脚本 */

8
bin/utils/dir.ts vendored
View File

@@ -1,8 +1,8 @@
import path from 'path';
import { fileURLToPath } from 'url';
// Convert the current module URL to a file path
const currentModulePath = fileURLToPath(import.meta.url);
export const npmDirectory = path.join(
path.dirname(fileURLToPath(import.meta.url)),
'..'
);
// Resolve the parent directory of the current module
export const npmDirectory = path.join(path.dirname(currentModulePath), '..');

36
bin/utils/info.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
import crypto from 'crypto';
import prompts from 'prompts';
import ora from 'ora';
import chalk from 'chalk';
// Generates an identifier based on the given URL.
export function getIdentifier(url: string) {
const postFixHash = crypto.createHash('md5').update(url).digest('hex').substring(0, 6);
return `pake-${postFixHash}`;
}
export async function promptText(message: string, initial?: string): Promise<string> {
const response = await prompts({
type: 'text',
name: 'content',
message,
initial,
});
return response.content;
}
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function getSpinner(text: string) {
const loadingType = {
interval: 80,
frames: ['✦', '✶', '✺', '✵', '✸', '✹', '✺'],
};
return ora({
text: `${chalk.cyan(text)}\n`,
spinner: loadingType,
color: 'cyan',
}).start();
}

57
bin/utils/ip.ts vendored Normal file
View File

@@ -0,0 +1,57 @@
import dns from 'dns';
import http from 'http';
import { promisify } from 'util';
import logger from '@/options/logger';
const resolve = promisify(dns.resolve);
const ping = async (host: string) => {
const lookup = promisify(dns.lookup);
const ip = await lookup(host);
const start = new Date();
// Prevent timeouts from affecting user experience.
const requestPromise = new Promise<number>((resolve, reject) => {
const req = http.get(`http://${ip.address}`, res => {
const delay = new Date().getTime() - start.getTime();
res.resume();
resolve(delay);
});
req.on('error', err => {
reject(err);
});
});
const timeoutPromise = new Promise<number>((_, reject) => {
setTimeout(() => {
reject(new Error('Request timed out after 3 seconds'));
}, 1000);
});
return Promise.race([requestPromise, timeoutPromise]);
};
async function isChinaDomain(domain: string): Promise<boolean> {
try {
const [ip] = await resolve(domain);
return await isChinaIP(ip, domain);
} catch (error) {
logger.debug(`${domain} can't be parse!`);
return true;
}
}
async function isChinaIP(ip: string, domain: string): Promise<boolean> {
try {
const delay = await ping(ip);
logger.debug(`${domain} latency is ${delay} ms`);
return delay > 1000;
} catch (error) {
logger.debug(`ping ${domain} failed!`);
return true;
}
}
export { isChinaDomain, isChinaIP };

54
bin/utils/ip_addr.ts vendored
View File

@@ -1,54 +0,0 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import logger from '@/options/logger.js';
import dns from 'dns';
import http from 'http';
const ping = async (host: string) => {
const lookup = promisify(dns.lookup);
const ip = await lookup(host);
const start = new Date();
return new Promise<number>((resolve, reject) => {
const req = http.get(`http://${ip.address}`, (res) => {
const delay = new Date().getTime() - start.getTime();
res.resume();
resolve(delay);
});
req.on('error', (err) => {
reject(err);
});
});
};
const resolve = promisify(dns.resolve);
async function isChinaDomain(domain: string): Promise<boolean> {
try {
// 解析域名为IP地址
const [ip] = await resolve(domain);
return await isChinaIP(ip, domain);
} catch (error) {
// 域名无法解析返回false
logger.info(`${domain} can't be parse, is not in China!`);
return false;
}
}
async function isChinaIP(ip: string, domain: string): Promise<boolean> {
try {
const delay = await ping(ip);
logger.info(`${domain} latency is ${delay} ms`);
// 判断延迟是否超过500ms
return delay > 500;
} catch (error) {
// 命令执行出错返回false
logger.info(`ping ${domain} failed!, is not in China!`);
return false;
}
}
export { isChinaDomain, isChinaIP };

View File

@@ -1,5 +1,5 @@
export const IS_MAC = process.platform === 'darwin';
const { platform } = process;
export const IS_WIN = process.platform === 'win32';
export const IS_LINUX = process.platform === 'linux';
export const IS_MAC = platform === 'darwin';
export const IS_WIN = platform === 'win32';
export const IS_LINUX = platform === 'linux';

6
bin/utils/shell.ts vendored
View File

@@ -1,9 +1,9 @@
import shelljs from "shelljs";
import { npmDirectory } from "./dir.js";
import shelljs from 'shelljs';
import { npmDirectory } from './dir';
export function shellExec(command: string) {
return new Promise<number>((resolve, reject) => {
shelljs.exec(command, { async: true, silent: false, cwd: npmDirectory}, (code) => {
shelljs.exec(command, { async: true, silent: false, cwd: npmDirectory }, code => {
if (code === 0) {
resolve(0);
} else {

1489
bin/utils/tlds.ts vendored

File diff suppressed because it is too large Load Diff

54
bin/utils/url.ts vendored
View File

@@ -1,45 +1,39 @@
import url from 'url';
import isurl from 'is-url';
import tlds from './tlds.js';
import psl from 'psl';
import isUrl from 'is-url';
export function getDomain(inputUrl: string) {
const parsed = url.parse(inputUrl).host;
var parts = parsed.split('.');
if (parts[0] === 'www' && parts[1] !== 'com') {
parts.shift();
}
var ln = parts.length,
i = ln,
minLength = parts[parts.length - 1].length,
part;
// Extracts the domain from a given URL.
export function getDomain(inputUrl: string): string | null {
try {
const url = new URL(inputUrl);
// Use PSL to parse domain names.
const parsed = psl.parse(url.hostname);
// iterate backwards
while ((part = parts[--i])) {
// stop when we find a non-TLD part
if (
i === 0 || // 'asia.com' (last remaining must be the SLD)
i < ln - 2 || // TLDs only span 2 levels
part.length < minLength || // 'www.cn.com' (valid TLD as second-level domain)
tlds.indexOf(part) < 0 // officialy not a TLD
) {
return part;
// If domain is available, split it and return the SLD.
if ('domain' in parsed && parsed.domain) {
return parsed.domain.split('.')[0];
} else {
return null;
}
} catch (error) {
return null;
}
}
function appendProtocol(inputUrl: string): string {
const parsed = url.parse(inputUrl);
if (!parsed.protocol) {
const urlWithProtocol = `https://${inputUrl}`;
return urlWithProtocol;
// Appends 'https://' protocol to the URL if not present.
export function appendProtocol(inputUrl: string): string {
try {
new URL(inputUrl);
return inputUrl;
} catch {
return `https://${inputUrl}`;
}
return inputUrl;
}
// Normalizes the URL by ensuring it has a protocol and is valid.
export function normalizeUrl(urlToNormalize: string): string {
const urlWithProtocol = appendProtocol(urlToNormalize);
if (isurl(urlWithProtocol)) {
if (isUrl(urlWithProtocol)) {
return urlWithProtocol;
} else {
throw new Error(`Your url "${urlWithProtocol}" is invalid`);

18
bin/utils/validate.ts vendored
View File

@@ -1,23 +1,25 @@
import * as Commander from 'commander';
import { normalizeUrl } from './url.js';
import fs from 'fs';
import { InvalidArgumentError } from 'commander';
import { normalizeUrl } from './url';
export function validateNumberInput(value: string) {
const parsedValue = Number(value);
if (isNaN(parsedValue)) {
throw new Commander.InvalidArgumentError('Not a number.');
throw new InvalidArgumentError('Not a number.');
}
return parsedValue;
}
export function validateUrlInput(url: string) {
if(!fs.existsSync(url)) {
const isFile = fs.existsSync(url);
if (!isFile) {
try {
return normalizeUrl(url)
return normalizeUrl(url);
} catch (error) {
throw new Commander.InvalidArgumentError(error.message);
throw new InvalidArgumentError(error.message);
}
} else {
return url;
}
return url;
}

3370
dist/cli.js vendored

File diff suppressed because it is too large Load Diff

3
icns2png.py vendored
View File

@@ -10,7 +10,6 @@ except ImportError:
os.system("pip install Pillow")
from PIL import Image
if __name__ == "__main__":
now_dir = os.path.dirname(os.path.abspath(__file__))
icons_dir = os.path.join(now_dir, "src-tauri", "icons")
@@ -34,5 +33,3 @@ if __name__ == "__main__":
image_32.save(image_32_path, "ICO")
print("png file write success.")
print(f"There are {len(os.listdir(png_dir))} png picture in ", png_dir)

View File

@@ -1,6 +1,6 @@
{
"name": "pake-cli",
"version": "2.0.6",
"version": "2.1.8",
"description": "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 很简单的用 Rust 打包网页生成很小的桌面 App。",
"engines": {
"node": ">=16.0.0"
@@ -46,17 +46,18 @@
"exports": "./dist/pake.js",
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^1.3.0",
"@tauri-apps/cli": "^1.3.1",
"@tauri-apps/api": "^1.4.0",
"@tauri-apps/cli": "^1.4.0",
"axios": "^1.1.3",
"chalk": "^5.1.2",
"commander": "^9.4.1",
"commander": "^11.0.0",
"file-type": "^18.0.0",
"fs-extra": "^11.1.0",
"is-url": "^1.2.4",
"loglevel": "^1.8.1",
"ora": "^6.1.2",
"prompts": "^2.4.2",
"psl": "^1.9.0",
"shelljs": "^0.8.5",
"tmp-promise": "^3.0.3",
"update-notifier": "^6.0.2"
@@ -64,21 +65,20 @@
"devDependencies": {
"@rollup/plugin-alias": "^4.0.2",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-json": "^5.0.2",
"@rollup/plugin-terser": "^0.1.0",
"@rollup/plugin-typescript": "^9.0.2",
"@types/fs-extra": "^9.0.13",
"@types/is-url": "^1.2.30",
"@types/page-icon": "^0.3.4",
"@types/prompts": "^2.4.1",
"@types/psl": "^1.1.0",
"@types/shelljs": "^0.8.11",
"@types/tmp": "^0.2.3",
"@types/update-notifier": "^6.0.1",
"app-root-path": "^3.1.0",
"concurrently": "^7.5.0",
"cross-env": "^7.0.3",
"rollup": "^3.3.0",
"tauri": "^0.15.0",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.4.1",
"typescript": "^4.9.3"
}

5
rollup.config.js vendored
View File

@@ -1,6 +1,6 @@
import path from 'path';
import appRootPath from 'app-root-path';
import typescript from '@rollup/plugin-typescript';
import typescript from 'rollup-plugin-typescript2';
import alias from '@rollup/plugin-alias';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
@@ -14,7 +14,8 @@ export default {
plugins: [
json(),
typescript({
sourceMap: false,
tsconfig: "tsconfig.json",
clean: true, // 清理缓存
}),
commonjs(),
alias({

4
script/build.bat vendored
View File

@@ -51,7 +51,7 @@ for /f "skip=1 tokens=1-4 delims=," %%i in (app.csv) do (
::echo name is !name! !name_zh! !url!
:: replace url
.\script\sd.exe -s !old_url! !url! src-tauri\pake.json
::replace pacakge name
::replace package name
.\script\sd.exe !old_title! !title! src-tauri\tauri.conf.json
.\script\sd.exe !old_name! !name! src-tauri\tauri.conf.json
.\script\sd.exe !old_name! !name! src-tauri\tauri.windows.conf.json
@@ -96,4 +96,4 @@ echo "output dir is output\windows"
.\script\sd.exe %url% %init_url% src-tauri\pake.json
.\script\sd.exe %title% %init_title% src-tauri\tauri.conf.json
.\script\sd.exe %name% %init_name% src-tauri\tauri.conf.json
.\script\sd.exe %name% %init_name% src-tauri\tauri.windows.conf.json
.\script\sd.exe %name% %init_name% src-tauri\tauri.windows.conf.json

527
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,12 +12,12 @@ rust-version = "1.63.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.3.0", features = [] }
tauri-build = { version = "1.4.0", features = [] }
[dependencies]
serde_json = "1.0.96"
serde = { version = "1.0.163", features = ["derive"] }
tauri = { version = "1.3.0", features = ["api-all", "system-tray"] }
tauri = { version = "1.4.1", features = ["api-all", "system-tray"] }
download_rs = { version = "0.2.0", features = ["sync_download"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }

9
src-tauri/info.plist Normal file
View File

@@ -0,0 +1,9 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSCameraUsageDescription</key>
<string>Request camera access</string>
<key>NSMicrophoneUsageDescription</key>
<string>Request microphone access</string>
</dict>
</plist>

View File

@@ -136,8 +136,7 @@ document.addEventListener('DOMContentLoaded', () => {
document.body.appendChild(m);
setTimeout(function () {
const d = 0.5;
m.style.transition =
'transform ' + d + 's ease-in, opacity ' + d + 's ease-in';
m.style.transition = 'transform ' + d + 's ease-in, opacity ' + d + 's ease-in';
m.style.opacity = '0';
setTimeout(function () {
document.body.removeChild(m);
@@ -146,4 +145,36 @@ document.addEventListener('DOMContentLoaded', () => {
}
window.pakeToast = pakeToast;
// chatgpt supports unlimited times of GPT4-Mobile
if (window.location.hostname === 'chat.openai.com') {
const originFetch = fetch;
window.fetch = (url, options) => {
return originFetch(url, options).then(async response => {
if (url.indexOf('/backend-api/models') === -1) {
return response;
}
const responseClone = response.clone();
let res = await responseClone.json();
res.models = res.models.map(m => {
m.tags = m.tags.filter(t => {
return t !== 'mobile';
});
if (m.slug === 'gpt-4-mobile') {
res.categories.push({
browsing_model: null,
category: 'gpt_4',
code_interpreter_model: null,
default_model: 'gpt-4-mobile',
human_category_name: 'GPT-4-Mobile',
plugins_model: null,
subscription_level: 'plus',
});
}
return m;
});
return new Response(JSON.stringify(res), response);
});
};
}
});

View File

@@ -82,14 +82,14 @@ async function invoke(cmd, args) {
// Judgment of file download.
function isDownloadLink(url) {
const fileExtensions = [
'3gp', '7z', 'ai', 'apk', 'avi', 'bmp', 'csv', 'dmg', 'doc', 'docx', 'fla', 'flv', 'gif', 'gz', 'gzip',
'ico', 'iso', 'indd', 'jar', 'jpeg', 'jpg', 'm3u8', 'mov', 'mp3', 'mp4', 'mpa', 'mpg',
'mpeg', 'msi', 'odt', 'ogg', 'ogv', 'pdf', 'png', 'ppt', 'pptx', 'psd', 'rar', 'raw', 'rss', 'svg',
'swf', 'tar', 'tif', 'tiff', 'ts', 'txt', 'wav', 'webm', 'webp', 'wma', 'wmv', 'xls', 'xlsx', 'xml', 'zip'
];
const downloadLinkPattern = new RegExp(`\\.(${fileExtensions.join('|')})$`, 'i');
return downloadLinkPattern.test(url);
const fileExtensions = [
'3gp', '7z', 'ai', 'apk', 'avi', 'bmp', 'csv', 'dmg', 'doc', 'docx', 'fla', 'flv', 'gif', 'gz', 'gzip',
'ico', 'iso', 'indd', 'jar', 'jpeg', 'jpg', 'm3u8', 'mov', 'mp3', 'mp4', 'mpa', 'mpg',
'mpeg', 'msi', 'odt', 'ogg', 'ogv', 'pdf', 'png', 'ppt', 'pptx', 'psd', 'rar', 'raw', 'rss', 'svg',
'swf', 'tar', 'tif', 'tiff', 'ts', 'txt', 'wav', 'webm', 'webp', 'wma', 'wmv', 'xls', 'xlsx', 'xml', 'zip',
];
const downloadLinkPattern = new RegExp(`\\.(${fileExtensions.join('|')})$`, 'i');
return downloadLinkPattern.test(url);
}
// No need to go to the download link.
@@ -114,17 +114,17 @@ document.addEventListener('DOMContentLoaded', () => {
domEl.addEventListener('mousedown', (e) => {
e.preventDefault();
if (e.buttons === 1 && e.detail !== 2) {
appWindow.startDragging();
appWindow.startDragging().then();
}
});
domEl.addEventListener('touchstart', () => {
appWindow.startDragging();
appWindow.startDragging().then();
});
domEl.addEventListener('dblclick', () => {
appWindow.isFullscreen().then((fullscreen) => {
appWindow.setFullscreen(!fullscreen);
appWindow.setFullscreen(!fullscreen).then();
});
});
@@ -156,6 +156,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);
// Process download links for Rust to handle.
// If the download attribute is set, the download attribute is used as the file name.
if (
@@ -184,7 +185,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Rewrite the window.open function.
const originalWindowOpen = window.open;
window.open = function (url, name, specs) {
window.open = function(url, name, specs) {
// Apple login and google login
if (name === 'AppleAuthentication') {
//do nothing
@@ -261,31 +262,34 @@ function convertBlobUrlToBinary(blobUrl) {
});
}
function downloadFromBlobUrl(blobUrl, filename) {
const tauri = window.__TAURI__;
convertBlobUrlToBinary(blobUrl).then((binary) => {
console.log('binary', binary);
tauri.fs.writeBinaryFile(filename, binary, {
dir: tauri.fs.BaseDirectory.Download,
}).then(() => {
window.pakeToast('Download successful, saved to download directory~');
});
});
}
// detect blob download by createElement("a")
function detectDownloadByCreateAnchor() {
const createEle = document.createElement;
const tauri = window.__TAURI__;
document.createElement = (el) => {
if (el !== "a") return createEle.call(document, el);
if (el !== 'a') return createEle.call(document, el);
const anchorEle = createEle.call(document, el);
const anchorClick = anchorEle.click;
Object.defineProperties(anchorEle, {
click: {
get: () => {
if (anchorEle.href && anchorEle.href.includes('blob:')) {
const url = anchorEle.href;
convertBlobUrlToBinary(url).then((binary) => {
tauri.fs.writeBinaryFile(anchorEle.download || getFilenameFromUrl(url), binary, {
dir: tauri.fs.BaseDirectory.Download,
});
});
}
return anchorClick.bind(anchorEle);
}
// use addEventListener to avoid overriding the original click event.
anchorEle.addEventListener('click', () => {
const url = anchorEle.href;
if (window.blobToUrlCaches.has(url)) {
downloadFromBlobUrl(url, anchorEle.download || getFilenameFromUrl(url));
}
})
});
return anchorEle;
}
};
}

View File

@@ -1,4 +1,4 @@
window.addEventListener('DOMContentLoaded', (_event) => {
window.addEventListener('DOMContentLoaded', _event => {
const css = `
#page #footer-wrapper,
.drawing-board .toolbar .toolbar-action,
@@ -56,6 +56,10 @@ window.addEventListener('DOMContentLoaded', (_event) => {
padding-top: 20px;
}
#__next > div.overflow-hidden.w-full.h-full .min-h-\\[20px\\].items-start.gap-4.whitespace-pre-wrap.break-words {
word-break: break-all;
}
#__next .PageWithSidebarLayout_mainSection__i1yOg {
width: 100%;
max-width: 1000px;
@@ -312,7 +316,8 @@ window.addEventListener('DOMContentLoaded', (_event) => {
}
@media (min-width:1024px){
#__next .text-base.lg\\:max-w-xl, #__next form.stretch.lg\\:max-w-2xl {
#__next .text-base.lg\\:max-w-xl, #__next form.stretch.lg\\:max-w-2xl,
#__next > .w-full.h-full .lg\\:max-w-\\[38rem\\] {
max-width: 44rem;
}
}
@@ -327,6 +332,20 @@ window.addEventListener('DOMContentLoaded', (_event) => {
#__next .overflow-hidden.w-full .max-w-full>.sticky.top-0 {
padding-top: 20px;
}
#__next .overflow-hidden.w-full main.relative.h-full.w-full.flex-1{
padding-bottom: 82px;
}
#__next > div.overflow-hidden.w-full.h-full main.relative.h-full.w-full.flex-1 > .flex-1.overflow-hidden .h-32.md\\:h-48.flex-shrink-0{
height: 0px;
}
}
@media (max-width:565px){
#__next .overflow-hidden.w-full main.relative.h-full.w-full.flex-1{
padding-bottom: 98px;
}
}
#__next .prose ol li p {

View File

@@ -1,34 +1,34 @@
{
"package": {
"productName": "WeRead",
"version": "1.0.0"
"package": {
"productName": "WeRead",
"version": "1.0.0"
},
"tauri": {
"security": {
"csp": null
},
"tauri": {
"security": {
"csp": null
},
"updater": {
"active": false
},
"systemTray": {
"iconPath": "png/weread_512.png",
"iconAsTemplate": true
},
"allowlist": {
"all": true,
"fs": {
"all": true,
"scope": [
"$DOWNLOAD/*"
]
}
}
"updater": {
"active": false
},
"build": {
"withGlobalTauri": true,
"devPath": "../dist",
"distDir": "../dist",
"beforeBuildCommand": "",
"beforeDevCommand": ""
"systemTray": {
"iconPath": "png/weread_512.png",
"iconAsTemplate": true
},
"allowlist": {
"all": true,
"fs": {
"all": true,
"scope": [
"$DOWNLOAD/*"
]
}
}
}
},
"build": {
"withGlobalTauri": true,
"devPath": "../dist",
"distDir": "../dist",
"beforeBuildCommand": "",
"beforeDevCommand": ""
}
}

View File

@@ -8,7 +8,9 @@
"copyright": "",
"deb": {
"depends": ["curl", "wget"],
"files": {"/usr/share/applications/com-tw93-weread.desktop": "assets/com-tw93-weread.desktop"}
"files": {
"/usr/share/applications/com-tw93-weread.desktop": "assets/com-tw93-weread.desktop"
}
},
"externalBin": [],
"longDescription": "",

View File

@@ -1,17 +1,29 @@
{
"compilerOptions": {
"module": "Node16",
"module": "ESNext",
"target": "es2020",
"types": [
"node"
],
"lib": [
"es2020",
"dom"
],
"esModuleInterop": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "Node16",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@/*": ["bin/*"]
"@/*": [
"bin/*"
]
}
},
"include": ["bin/**/*"]
"include": [
"bin/**/*"
]
}