🎨 Refactoring CLI

This commit is contained in:
Tw93
2023-06-22 14:36:02 +08:00
parent 87e91ecbf5
commit b2d0d7a2ae
38 changed files with 1466 additions and 5120 deletions

View File

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

View File

@@ -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

View File

@@ -162,7 +162,7 @@
</kbd>
<br/><br/>
**Pake 提供了命令行工具,可以更快捷方便地一键自定义打你需要的包,详细可见 [文档](./bin/README.md)。**
**Pake 提供了命令行工具,可以更快捷方便地一键自定义打你需要的包,详细可见 [文档](./bin/README_CN.md)。**
```bash
# 使用 npm 进行安装

110
bin/README.md vendored
View File

@@ -1,23 +1,23 @@
## 安装
## Installation
请确保 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)
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,131 +35,124 @@ npm install -g pake-cli
gnome-video-effects-extra
```
## 用法
## Usage
```bash
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 为你需要打包的网页链接 🔗 或者本地 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>
# 或者
# or
-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.
```
```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
```
#### [resize]
Determine whether the window is resizable. Default is `true`. Use the following command to disable window resizing.
```shell
--no-resizable
```
#### [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 +160,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 +168,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,16 +176,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.

196
bin/README_CN.md vendored Normal file
View File

@@ -0,0 +1,196 @@
## 安装
请确保您的 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
```
#### [resize]
设置应用窗口是否可以调整大小,默认为 `true`(可调整)。使用以下命令可以禁止调整窗口大小。
```shell
--no-resizable
```
#### [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
```

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

@@ -0,0 +1,56 @@
import path from 'path';
import fsExtra from "fs-extra";
import prompts from 'prompts';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';
import logger from '@/options/logger';
import { checkRustInstalled, installRust } from '@/helpers/rust';
import { PakeAppOptions } from '@/types';
import { IS_MAC } from "@/utils/platform";
export default abstract class BaseBuilder {
abstract build(url: string, options: PakeAppOptions): Promise<void>;
async prepare() {
// Windows and Linux need to install necessary build tools.
if (!IS_MAC) {
logger.info('Install Rust and required build tools to build the app.');
logger.info('See more in https://tauri.app/v1/guides/getting-started/prerequisites#installing.');
}
if (checkRustInstalled()) {
return;
}
const res = await prompts({
type: 'confirm',
message: 'Rust not detected. Install now?',
name: 'value',
});
if (res.value) {
await installRust();
} else {
logger.error('Error: Rust required to package your webapp!');
process.exit(2);
}
}
protected async runBuildCommand(directory: string, command: string) {
const isChina = await isChinaDomain("www.npmjs.com");
if (isChina) {
logger.info("Located in China, using npm/Rust CN mirror.");
const rustProjectDir = path.join(directory, 'src-tauri', ".cargo");
await fsExtra.ensureDir(rustProjectDir);
const projectCnConf = path.join(directory, "src-tauri", "rust_proxy.toml");
const projectConf = path.join(rustProjectDir, "config");
await fsExtra.copy(projectCnConf, projectConf);
await shellExec(`cd "${directory}" && npm install --registry=https://registry.npmmirror.com && ${command}`);
} else {
await shellExec(`cd "${directory}" && npm install && ${command}`);
}
}
}

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!!');
}
}

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

@@ -0,0 +1,21 @@
import BaseBuilder from './BaseBuilder';
import MacBuilder from './MacBuilder';
import WinBuilder from './WinBuilder';
import LinuxBuilder from './LinuxBuilder';
import { IS_MAC, IS_WIN, IS_LINUX } from '@/utils/platform';
export default class BuilderProvider {
static create(): BaseBuilder {
if (IS_MAC) {
return new MacBuilder();
}
if (IS_WIN) {
return new WinBuilder();
}
if (IS_LINUX) {
return new LinuxBuilder();
}
throw new Error('The current system is not supported!');
}
}

View File

@@ -1,92 +1,39 @@
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 fsExtra from "fs-extra";
import BaseBuilder from './BaseBuilder';
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);
}
}
import logger from '@/options/logger';
import tauriConfig from '@/helpers/tauriConfig';
import { npmDirectory } from '@/utils/dir';
import { PakeAppOptions } from '@/types';
import { mergeConfig } from "@/builders/common";
export default class LinuxBuilder extends BaseBuilder {
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);
await mergeConfig(url, options, tauriConfig);
await this.runBuildCommand(npmDirectory, 'npm run build');
const arch = process.arch === "x64" ? "amd64" : process.arch;
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 debName = `${name}_${tauriConfig.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);
await fsExtra.copy(appPath, distPath);
await fsExtra.remove(appPath);
logger.success('Build Deb success!');
logger.success('You can find the deb app installer in', distPath);
logger.success('Deb app installer located in', distPath);
}
if (options.targets === "appimage" || options.targets === "all") {
const appImageName = `${name}_${tauriConf.package.version}_${arch}.AppImage`;
const appImageName = `${name}_${tauriConfig.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);
await fsExtra.copy(appImagePath, distAppPath);
await fsExtra.remove(appImagePath);
logger.success('Build AppImage success!');
logger.success('You can find the AppImage app installer in', distAppPath);
logger.success('AppImage installer located in', distAppPath);
}
}

View File

@@ -1,99 +1,36 @@
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 fsExtra from "fs-extra";
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);
}
}
import logger from '@/options/logger';
import tauriConfig from '@/helpers/tauriConfig';
import BaseBuilder from './BaseBuilder';
import { npmDirectory } from '@/utils/dir';
import { PakeAppOptions } from '@/types';
import { mergeConfig } from "@/builders/common";
export default class MacBuilder extends BaseBuilder {
async build(url: string, options: PakeAppOptions) {
log.debug('PakeAppOptions', options);
const { name } = options;
await mergeTauriConfig(url, options, tauriConf);
await mergeConfig(url, options, tauriConfig);
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`;
await this.runBuildCommand(npmDirectory, 'npm run build:mac');
dmgName = `${name}_${tauriConfig.package.version}_universal.dmg`;
} 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`;
await this.runBuildCommand(npmDirectory, 'npm run build');
let arch = process.arch === "arm64" ? "aarch64" : process.arch;
dmgName = `${name}_${tauriConfig.package.version}_${arch}.dmg`;
}
const appPath = this.getBuildAppPath(npmDirectory, dmgName, options.multiArch);
const distPath = path.resolve(`${name}.dmg`);
await fs.copyFile(appPath, distPath);
await fs.unlink(appPath);
await fsExtra.copy(appPath, distPath);
await fsExtra.remove(appPath);
logger.success('Build success!');
logger.success('You can find the app installer in', distPath);
logger.success('App installer located in', distPath);
}
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';
}
const dmgPath = multiArch ? 'src-tauri/target/universal-apple-darwin/release/bundle/dmg' : 'src-tauri/target/release/bundle/dmg';
return path.join(npmDirectory, dmgPath, dmgName);
}
}

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

@@ -0,0 +1,35 @@
import path from 'path';
import fsExtra from 'fs-extra';
import BaseBuilder from './BaseBuilder';
import logger from '@/options/logger';
import tauriConfig from '@/helpers/tauriConfig';
import { npmDirectory } from '@/utils/dir';
import { PakeAppOptions } from '@/types';
import { mergeConfig } from '@/builders/common';
export default class WinBuilder extends BaseBuilder {
async build(url: string, options: PakeAppOptions) {
const { name } = options;
await mergeConfig(url, options, tauriConfig);
await this.runBuildCommand(npmDirectory, 'npm run build');
const language = tauriConfig.tauri.bundle.windows.wix.language[0];
const arch = process.arch;
const msiName = `${name}_${tauriConfig.package.version}_${arch}_${language}.msi`;
const appPath = this.getBuildAppPath(npmDirectory, msiName);
const distPath = path.resolve(`${name}.msi`);
await fsExtra.copy(appPath, distPath);
await fsExtra.remove(appPath);
logger.success('Build success!');
logger.success('App installer located in', distPath);
}
getBuildAppPath(npmDirectory: string, msiName: string) {
return path.join(
npmDirectory,
'src-tauri/target/release/bundle/msi',
msiName
);
}
}

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>;
}

388
bin/builders/common.ts vendored
View File

@@ -1,33 +1,13 @@
import { PakeAppOptions,TauriConfig } from '@/types.js';
import prompts from 'prompts';
import path from 'path';
import fs from 'fs/promises';
import fs2 from 'fs-extra';
import fsExtra from 'fs-extra';
import { npmDirectory } from '@/utils/dir.js';
import logger from '@/options/logger.js';
import { PakeAppOptions, PlatformMap } from '@/types.js';
export async function promptText(message: string, initial?: string) {
const response = await prompts({
type: 'text',
name: 'content',
message,
initial,
});
return response.content;
}
function setSecurityConfigWithUrl(tauriConfig: TauriConfig, url: string) {
const myURL = new URL(url);
const hostname = myURL.hostname;
tauriConfig.tauri.security.dangerousRemoteDomainIpcAccess[0].domain = hostname;
}
export async function mergeTauriConfig(
export async function mergeConfig(
url: string,
options: PakeAppOptions,
tauriConf: any
tauriConf: any,
) {
const {
width,
@@ -44,6 +24,9 @@ export async function mergeTauriConfig(
name,
} = options;
const { platform } = process;
// Set Windows parameters.
const tauriConfWindowOptions = {
width,
height,
@@ -51,267 +34,190 @@ export async function mergeTauriConfig(
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 });
// Determine whether the package name is valid.
// for Linux, package name must be a-z, 0-9 or "-", not allow to A-Z and other
const platformRegexMapping: PlatformMap = {
linux: /[0-9]*[a-z]+[0-9]*\-?[0-9]*[a-z]*[0-9]*\-?[0-9]*[a-z]*[0-9]*/,
default: /([0-9]*[a-zA-Z]+[0-9]*)+/,
};
const reg = platformRegexMapping[platform] || platformRegexMapping.default;
const nameCheck = reg.test(name) && reg.exec(name)[0].length === name.length;
if (!nameCheck) {
const errorMsg =
platform === 'linux'
? `Package name is invalid. It should only include lowercase letters, numbers, and dashes, and must contain at least one lowercase letter.\n Examples: com-123-xxx, 123pan, pan123, weread, we-read.`
: `Package name is invalid. It should only include letters and numbers, and must contain at least one letter.\n Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead.`;
logger.error(errorMsg);
process.exit();
}
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);
// Judge the type of URL, whether it is a file or a website.
// If it is a file and the recursive copy function is enabled then the file and all files in its parent folder need to be copied to the "src" directory. Otherwise, only the single file will be copied.
const urlExists = await fsExtra.pathExists(url);
if (urlExists) {
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 url_path = path.join(npmDirectory,"dist/", file_name);
await fs.copyFile(url, url_path);
const urlPath = path.join(distDir, fileName);
await fsExtra.copy(url, urlPath);
} 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);
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 = file_name;
tauriConf.pake.windows[0].url_type = "local";
tauriConf.pake.windows[0].url = fileName;
tauriConf.pake.windows[0].url_type = 'local';
} else {
tauriConf.pake.windows[0].url_type = "web";
tauriConf.pake.windows[0].url_type = 'web';
// Set the secure domain for calling window.__TAURI__ to the application domain that has been set.
tauriConf.tauri.security.dangerousRemoteDomainIpcAccess[0].domain =
new URL(url).hostname;
}
// 处理user-agent
const platformMap: PlatformMap = {
win32: 'windows',
linux: 'linux',
darwin: 'macos',
};
const currentPlatform = platformMap[platform];
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;
}
tauriConf.pake.user_agent[currentPlatform] = userAgent;
}
// 处理菜单栏
if (showMenu) {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = true;
}
tauriConf.pake.menu[currentPlatform] = showMenu;
tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
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") {
// Processing targets are currently only open to Linux.
if (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];
}
const validTargets = ['all', 'deb', 'appimage'];
if (validTargets.includes(options.targets)) {
tauriConf.tauri.bundle.targets = options.targets === 'all' ? ['deb', 'appimage'] : [options.targets];
} else {
logger.warn("targets must be 'all', 'deb', 'appimage', we will use default 'all'");
logger.warn(
`The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`,
);
}
}
// 处理应用图标
const exists = await fs.stat(options.icon)
.then(() => true)
.catch(() => false);
// Set icon.
const platformIconMap: PlatformMap = {
win32: {
fileExt: '.ico',
path: `png/${name.toLowerCase()}_32.ico`,
defaultIcon: 'png/icon_256.ico',
message: 'Windows icon must be .ico and 256x256px.',
},
linux: {
fileExt: '.png',
path: `png/${name.toLowerCase()}_32.png`,
defaultIcon: 'png/icon_512.png',
message: 'Linux icon must be .png and 512x512px.',
},
darwin: {
fileExt: '.icns',
path: `icons/${name.toLowerCase()}_32.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 (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 (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 (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.`);
logger.warn(`Icon will remain as 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"];
}
logger.warn(
'Custom icon path may be invalid. Default icon will be used instead.',
);
tauriConf.tauri.bundle.icon = [iconInfo.defaultIcon];
}
// 处理托盘自定义图标
let useDefaultIcon = true; // 是否使用默认托盘图标
// Set tray icon path.
let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.tauri.bundle.icon[0];
if (systemTrayIcon.length > 0) {
const icon_exists = await fs.stat(systemTrayIcon)
.then(() => true)
.catch(() => false);
if (icon_exists) {
try {
await fsExtra.pathExists(systemTrayIcon);
// 需要判断图标格式默认只支持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);
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(`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.`);
logger.warn(
`System tray icon must be .ico or .png, but you provided ${iconExt}.`,
);
logger.warn(`Default system tray icon will be used.`);
}
} else {
logger.warn(`${systemTrayIcon} not exists!`)
logger.warn(`system tray icon file will not change with default.`);
} catch {
logger.warn(`${systemTrayIcon} not exists!`);
logger.warn(`Default system tray icon will remain unchanged.`);
}
}
// 处理托盘默认图标
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";
}
}
tauriConf.tauri.systemTray.iconPath = trayIconPath;
// 设置安全调用 window.__TAURI__ 的安全域名为设置的应用域名
setSecurityConfigWithUrl(tauriConf, url);
// 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]);
// 保存配置文件
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;
}
}
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 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')
);
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json');
await fsExtra.writeJson(configJsonPath, tauriConf2, { spaces: 4 });
}

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;

86
bin/cli.ts vendored
View File

@@ -1,51 +1,43 @@
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 { program } from 'commander';
program.version(packageJson.version).description('A cli application can package a web page to desktop application.');
import { PakeCliOptions } from './types';
import handleInputOptions from './options/index';
import BuilderProvider from './builders/BuilderProvider';
import { checkUpdateTips } from './helpers/updater';
import packageJson from '../package.json';
import { validateNumberInput, validateUrlInput } from './utils/validate';
import { DEFAULT_PAKE_OPTIONS } from './defaults';
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)
.action(async (url: string, options: PakeCliOptions) => {
checkUpdateTips();
.version(packageJson.version)
.description('A CLI that can turn any webpage into a desktop app with Rust.')
.showHelpAfterError();
program
.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', 'Start the packaged app 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 files to pake app when URL is a local file', DEFAULT_PAKE_OPTIONS.iterCopyFile)
.option('--multi-arch', 'Available for Mac only, supports both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch)
.option('--targets <string>', 'Only for Linux, option "deb", "appimage" or "all"', DEFAULT_PAKE_OPTIONS.targets)
.option('--debug', 'Debug mode', DEFAULT_PAKE_OPTIONS.debug)
.action(async (url: string, options: PakeCliOptions) => {
//Check for update prompt
await checkUpdateTips();
// If no URL is provided, display help information
if (!url) {
// 直接 pake 不需要出现url提示
program.help();
}
@@ -54,12 +46,14 @@ program
log.setLevel('debug');
}
const builder = BuilderFactory.create();
const builder = BuilderProvider.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);
await builder.build(url, appOptions);
});
program.parse();

2
bin/defaults.ts vendored
View File

@@ -16,5 +16,3 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
systemTrayIcon: '',
debug: false,
};
export const DEFAULT_APP_NAME = 'Pake';

30
bin/helpers/rust.ts vendored
View File

@@ -1,31 +1,27 @@
import { IS_WIN } from '@/utils/platform.js';
import ora from 'ora';
import shelljs from 'shelljs';
import logger from '@/options/logger.js';
import { shellExec } from '../utils/shell.js';
import {isChinaDomain} from '@/utils/ip_addr.js'
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 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 = ora('Downloading Rust').start();
try {
await shellExec(IS_WIN ? RustInstallScriptForWin : RustInstallScriptFocMac);
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
spinner.succeed();
} catch (error) {
console.error('install rust return code', error.message);
console.error('Error installing Rust:', error.message);
spinner.fail();
//@ts-ignore
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,5 +1,4 @@
import updateNotifier from 'update-notifier';
// @ts-expect-error
import packageJson from '../../package.json';
export async function checkUpdateTips() {

106
bin/options/icon.ts vendored
View File

@@ -1,98 +1,52 @@
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 { dir } from 'tmp-promise';
import { fileTypeFromBuffer } from 'file-type';
export async function handleIcon(options: PakeAppOptions, url: string) {
import logger from './logger';
import { PakeAppOptions } from '@/types';
import { npmDirectory } from '@/utils/dir';
import { IS_LINUX, IS_WIN } from '@/utils/platform';
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.info('No app icon provided, default icon used. Use --icon option to assign an icon.');
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;
try {
iconResponse = await axios.get(iconUrl, {
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);
return iconPath;
} catch (error) {
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;
}

32
bin/options/index.ts vendored
View File

@@ -1,29 +1,25 @@
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 { handleIcon } from './icon';
import { getDomain } from '@/utils/url';
import { getIdentifier, promptText } from '@/utils/info';
import { PakeAppOptions, PakeCliOptions } from '@/types';
export default async function handleOptions(options: PakeCliOptions, url: string): Promise<PakeAppOptions> {
const appOptions: PakeAppOptions = {
...options,
identifier: '',
identifier: getIdentifier(url),
};
const url_exists = await fs.stat(url)
.then(() => true)
.catch(() => false);
let urlExists = await fsExtra.pathExists(url);
if (!appOptions.name) {
if (!url_exists) {
appOptions.name = await promptText('please input your application name', getDomain(url));
} else {
appOptions.name = await promptText('please input your application name', "");
}
const defaultName = urlExists ? "" : getDomain(url);
const promptMessage = 'Enter your application name';
appOptions.name = await promptText(promptMessage, defaultName);
}
appOptions.identifier = getIdentifier(appOptions.name, url);
appOptions.icon = await handleIcon(appOptions, url);
appOptions.icon = await handleIcon(appOptions);
return appOptions;
}

187
bin/types.ts vendored
View File

@@ -1,202 +1,55 @@
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;
}
export interface PakeAppOptions extends PakeCliOptions {
identifier: string;
}
export interface TauriBuildConfig {
/**
* the path to the app's dist dir
* this path must contain your index.html file
*/
distDir: string
/**
* the app's dev server URL, or the path to the directory containing an index.html to open
*/
devPath: string
/**
* a shell command to run before `tauri dev` kicks in
*/
beforeDevCommand?: string
/**
* a shell command to run before `tauri build` kicks in
*/
beforeBuildCommand?: string
withGlobalTauri?: boolean
}
type DangerousRemoteDomainIpAccess = {
domain: string;
windows: string[];
enableTauriAPI: boolean;
schema?: string;
plugins?: string[];
}
/**
* Tauri configuration
*/
export interface TauriConfig {
/**
* build/dev configuration
*/
build: TauriBuildConfig
/**
* the context of the current `tauri dev` or `tauri build`
*/
ctx: {
/**
* whether we're building for production or not
*/
prod?: boolean
/**
* whether we're running on the dev environment or not
*/
dev?: boolean
/**
* the target of the compilation (see `rustup target list`)
*/
target?: string
/**
* whether the app should be built on debug mode or not
*/
debug?: boolean
/**
* defines we should exit the `tauri dev` process if a Rust code error is found
*/
exitOnPanic?: boolean
}
/**
* tauri root configuration object
*/
tauri: {
/**
* the embedded server configuration
*/
embeddedServer: {
/**
* whether we should use the embedded-server or the no-server mode
*/
active?: boolean
/**
* the embedded server port number or the 'random' string to generate one at runtime
*/
port?: number | 'random' | undefined
}
/**
* tauri bundler configuration
*/
bundle: {
/**
* whether we should build your app with tauri-bundler or plain `cargo build`
*/
active?: boolean
/**
* the bundle targets, currently supports ["deb", "osx", "msi", "appimage", "dmg"] or "all"
*/
targets?: string | string[]
/**
* the app's identifier
*/
identifier: string
/**
* the app's icons
*/
icon: string[]
/**
* app resources to bundle
* each resource is a path to a file or directory
* glob patterns are supported
*/
resources?: string[]
externalBin?: string[]
copyright?: string
category?: string
shortDescription?: string
longDescription?: string
deb?: {
depends?: string[]
useBootstrapper?: boolean
}
osx?: {
frameworks?: string[]
minimumSystemVersion?: string
license?: string
useBootstrapper?: boolean
}
exceptionDomain?: string
}
allowlist: {
all: boolean
[index: string]: boolean
}
window: {
title: string
width?: number
height?: number
resizable?: boolean
fullscreen?: boolean
}
security: {
csp?: string,
dangerousRemoteDomainIpcAccess?: DangerousRemoteDomainIpAccess[]
}
inliner: {
active?: boolean
}
}
plugins?: {
[name: string]: {
[key: string]: any
}
}
/**
* Whether or not to enable verbose logging
*/
verbose?: boolean
}

5
bin/utils/dir.ts vendored
View File

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

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

@@ -0,0 +1,21 @@
import crypto from 'crypto';
import prompts from "prompts";
// 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;
}

View File

@@ -1,9 +1,9 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import logger from '@/options/logger.js';
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);
@@ -23,32 +23,25 @@ const ping = async (host: string) => {
});
};
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!`);
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!`);
return false;
}
try {
const delay = await ping(ip);
logger.info(`${domain} latency is ${delay} ms`);
return delay > 500;
} catch (error) {
logger.info(`ping ${domain} failed!`);
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';

4
bin/utils/shell.ts vendored
View File

@@ -1,9 +1,9 @@
import shelljs from "shelljs";
import { npmDirectory } from "./dir.js";
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

55
bin/utils/url.ts vendored
View File

@@ -1,45 +1,40 @@
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;
}

3087
dist/cli.js vendored

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "pake-cli",
"version": "2.0.7",
"version": "2.1.0",
"description": "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 很简单的用 Rust 打包网页生成很小的桌面 App。",
"engines": {
"node": ">=16.0.0"
@@ -57,6 +57,7 @@
"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,19 +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",
"cross-env": "^7.0.3",
"rollup": "^3.3.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({

2
script/build.bat vendored
View File

@@ -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

View File

@@ -1,17 +1,25 @@
{
"compilerOptions": {
"module": "Node16",
"module": "ESNext",
"target": "es2020",
"types": [
"node"
],
"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/**/*"
]
}