🎨 Simplify usage

This commit is contained in:
Tw93
2023-06-23 11:43:06 +08:00
parent 7809771a79
commit ba65ff21da
14 changed files with 360 additions and 284 deletions

12
bin/README.md vendored
View File

@@ -1,3 +1,5 @@
<h4 align="right"><strong>English</strong> | <a href="https://github.com/tw93/Pake/blob/master/bin/README_CN.md">简体中文</a></h4>
## 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).
@@ -38,14 +40,14 @@ npm install pake-cli -g
## 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**: 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]
The URL is the link to the web page you want to package or the path to a local HTML file. This is mandatory.
@@ -59,8 +61,6 @@ Specify the application name. If not provided, you will be prompted to enter it.
```shell
--name <value>
# or
-n <value>
```
#### [icon]
@@ -95,8 +95,6 @@ Set the width of the application window. Default is `1200px`.
Enable or disable immersive header. Default is `false`. Use the following command to enable this feature.
```
```shell
--transparent
```
@@ -114,7 +112,7 @@ Determine whether the application launches in full screen. Default is `false`. U
Determine whether the window is resizable. Default is `true`. Use the following command to disable window resizing.
```shell
--no-resizable
--resizable
```
#### [multi-arch]

8
bin/README_CN.md vendored
View File

@@ -1,3 +1,5 @@
<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)。
@@ -39,14 +41,14 @@ npm install pake-cli -g
## 使用方法
```bash
pake url [options]
pake [url] [options]
```
应用程序的打包结果将默认保存在当前工作目录。由于首次打包需要配置环境,这可能需要一些时间,请耐心等待。
> **注意**:打包过程需要使用 `Rust` 环境。如果您没有安装 `Rust`,系统会提示您是否要安装。如果遇到安装失败或超时的问题,您可以 [手动安装](https://www.rust-lang.org/tools/install)。
### url
### [url]
`url` 是您需要打包的网页链接 🔗 或本地 HTML 文件的路径,此参数为必填。
@@ -104,7 +106,7 @@ pake url [options]
设置应用窗口是否可以调整大小,默认为 `true`(可调整)。使用以下命令可以禁止调整窗口大小。
```shell
--no-resizable
--resizable
```
#### [fullscreen]

View File

@@ -1,11 +1,12 @@
import path from 'path';
import ora from "ora";
import fsExtra from "fs-extra";
import prompts from 'prompts';
import logger from '@/options/logger';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';
import { getSpinner } from "@/utils/info";
import { npmDirectory } from '@/utils/dir';
import { PakeAppOptions } from '@/types';
import { IS_MAC } from "@/utils/platform";
import { checkRustInstalled, installRust } from '@/helpers/rust';
@@ -14,46 +15,46 @@ 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('The first use requires installing system dependencies.');
logger.info('See more in https://tauri.app/v1/guides/getting-started/prerequisites#installing.');
}
if (checkRustInstalled()) {
return;
if (!checkRustInstalled()) {
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(0);
}
}
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 spinner = ora('Building...').start();
setTimeout(() => spinner.succeed(), 5000);
const isChina = await isChinaDomain("www.npmjs.com");
const spinner = getSpinner('Installing package.');
if (isChina) {
logger.info("Located in China, using npm/Rust CN mirror.");
const rustProjectDir = path.join(directory, 'src-tauri', ".cargo");
logger.info("Located in China, using npm/rsProxy CN mirror.");
const rustProjectDir = path.join(npmDirectory, 'src-tauri', ".cargo");
await fsExtra.ensureDir(rustProjectDir);
const projectCnConf = path.join(directory, "src-tauri", "rust_proxy.toml");
const projectCnConf = path.join(npmDirectory, "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}`);
await shellExec(`cd "${npmDirectory}" && npm install --registry=https://registry.npmmirror.com`);
} else {
await shellExec(`cd "${directory}" && npm install && ${command}`);
await shellExec(`cd "${npmDirectory}" && npm install`);
}
spinner.succeed('Package installed.');
}
protected async runBuildCommand(command: string = "npm run build") {
const spinner = getSpinner('Building app.');
await shellExec(`cd "${npmDirectory}" && ${command}`);
spinner.stop();
}
}

View File

@@ -12,7 +12,7 @@ export default class LinuxBuilder extends BaseBuilder {
async build(url: string, options: PakeAppOptions) {
const { name } = options;
await mergeConfig(url, options, tauriConfig);
await this.runBuildCommand(npmDirectory, 'npm run build');
await this.runBuildCommand();
const arch = process.arch === "x64" ? "amd64" : process.arch;

View File

@@ -14,10 +14,10 @@ export default class MacBuilder extends BaseBuilder {
await mergeConfig(url, options, tauriConfig);
let dmgName: string;
if (options.multiArch) {
await this.runBuildCommand(npmDirectory, 'npm run build:mac');
await this.runBuildCommand('npm run build:mac');
dmgName = `${name}_${tauriConfig.package.version}_universal.dmg`;
} else {
await this.runBuildCommand(npmDirectory, 'npm run build');
await this.runBuildCommand();
let arch = process.arch === "arm64" ? "aarch64" : process.arch;
dmgName = `${name}_${tauriConfig.package.version}_${arch}.dmg`;
}

View File

@@ -12,7 +12,7 @@ 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');
await this.runBuildCommand();
const language = tauriConfig.tauri.bundle.windows.wix.language[0];
const arch = process.arch;

29
bin/cli.ts vendored
View File

@@ -1,5 +1,5 @@
import ora from "ora";
import log from 'loglevel';
import chalk from 'chalk';
import { program } from 'commander';
import { PakeCliOptions } from './types';
@@ -11,8 +11,8 @@ import { validateNumberInput, validateUrlInput } from './utils/validate';
import { DEFAULT_PAKE_OPTIONS as DEFAULT } from './defaults';
program
.version(packageJson.version)
.description('A CLI that can turn any webpage into a desktop app with Rust.')
.description(chalk.green('Pake: A CLI that can turn any webpage into a desktop app with Rust.'))
.usage('[url] [options]')
.showHelpAfterError();
program
@@ -21,7 +21,7 @@ program
.option('--icon <string>', 'Application icon', DEFAULT.icon)
.option('--height <number>', 'Window height', validateNumberInput, DEFAULT.height)
.option('--width <number>', 'Window width', validateNumberInput, DEFAULT.width)
.option('--no-resizable', 'Whether the window can be resizable', DEFAULT.resizable)
.option('--resizable', 'Whether the window can be resizable', DEFAULT.resizable)
.option('--fullscreen', 'Start the packaged app in full screen', DEFAULT.fullscreen)
.option('--transparent', 'Transparent title bar', DEFAULT.transparent)
.option('--user-agent <string>', 'Custom user agent', DEFAULT.userAgent)
@@ -29,17 +29,23 @@ program
.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 to app when URL is a local file', DEFAULT.iterCopyFile)
.option('--multi-arch', 'Available for Mac only, supports both Intel and M1', DEFAULT.multiArch)
.option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT.multiArch)
.option('--targets <string>', 'Only for Linux, option "deb", "appimage" or "all"', DEFAULT.targets)
.option('--debug', 'Debug mode', DEFAULT.debug)
.version(packageJson.version, '-v, --version', 'Output the current version')
.action(async (url: string, options: PakeCliOptions) => {
//Check for update prompt
await checkUpdateTips();
// If no URL is provided, display help information
if (!url) {
program.help();
program.outputHelp((str) => {
const filteredOutput = str
.split('\n')
.filter((line) => !/((-h,|--help)|((-v|-V),|--version))\s+.+$/.test(line))
.join('\n');
return filteredOutput.trim(); // Trim any leading/trailing whitespace
});
process.exit(0);
}
log.setDefaultLevel('info');
@@ -47,14 +53,11 @@ program
log.setLevel('debug');
}
const spinner = ora('Preparing...').start();
const builder = BuilderProvider.create();
await builder.prepare();
const appOptions = await handleInputOptions(options, url);
spinner.succeed();
log.debug('PakeAppOptions', appOptions);
const builder = BuilderProvider.create();
await builder.prepare();
await builder.build(url, appOptions);
});

27
bin/helpers/merge.ts vendored
View File

@@ -37,33 +37,12 @@ export async function mergeConfig(
};
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. 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. Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead.`;
logger.error(errorMsg);
process.exit();
}
tauriConf.package.productName = name;
tauriConf.tauri.bundle.identifier = identifier;
// 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) {
//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';

10
bin/helpers/rust.ts vendored
View File

@@ -1,6 +1,6 @@
import ora from 'ora';
import shelljs from 'shelljs';
import { getSpinner } from "@/utils/info";
import { IS_WIN } from '@/utils/platform';
import { shellExec } from '@/utils/shell';
import { isChinaDomain } from '@/utils/ip';
@@ -12,16 +12,14 @@ export async function installRust() {
: "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();
const spinner = getSpinner('Downloading Rust.');
try {
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
spinner.succeed();
spinner.succeed('Rust installed successfully.');
} catch (error) {
console.error('Error installing Rust:', error.message);
spinner.fail();
//@ts-ignore
spinner.fail('Rust installation failed.');
process.exit(1);
}
}

6
bin/options/icon.ts vendored
View File

@@ -1,3 +1,4 @@
import { getSpinner } from "@/utils/info";
import path from 'path';
import axios from 'axios';
import fsExtra from "fs-extra";
@@ -24,9 +25,9 @@ export async function handleIcon(options: PakeAppOptions) {
}
export async function downloadIcon(iconUrl: string) {
const spinner = getSpinner('Downloading icon.');
try {
const iconResponse = await axios.get(iconUrl, { responseType: 'arraybuffer' });
const iconData = await iconResponse.data;
if (!iconData) {
@@ -41,9 +42,10 @@ export async function downloadIcon(iconUrl: string) {
const { path: tempPath } = await dir();
const iconPath = `${tempPath}/icon.${fileDetails.ext}`;
await fsExtra.outputFile(iconPath, iconData);
spinner.succeed('Icon downloaded successfully.');
return iconPath;
} catch (error) {
spinner.fail('Icon download failed.');
if (error.response && error.response.status === 404) {
return null;
}

53
bin/options/index.ts vendored
View File

@@ -1,24 +1,57 @@
import fsExtra from "fs-extra";
import logger from "@/options/logger";
import { handleIcon } from './icon';
import { getDomain } from '@/utils/url';
import { getIdentifier, promptText } from '@/utils/info';
import { PakeAppOptions, PakeCliOptions } from '@/types';
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 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 = `Package name is invalid. It 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 = `Package name is invalid. It 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 {
process.exit(1);
}
}
const appOptions: PakeAppOptions = {
...options,
name,
identifier: getIdentifier(url),
};
let urlExists = await fsExtra.pathExists(url);
if (!appOptions.name) {
const defaultName = urlExists ? "" : getDomain(url);
const promptMessage = 'Enter your application name';
appOptions.name = await promptText(promptMessage, defaultName);
}
appOptions.icon = await handleIcon(appOptions);
return appOptions;

21
bin/utils/info.ts vendored
View File

@@ -1,5 +1,6 @@
import crypto from 'crypto';
import prompts from "prompts";
import ora from "ora";
// Generates an identifier based on the given URL.
export function getIdentifier(url: string) {
@@ -19,3 +20,23 @@ export async function promptText(message: string, initial?: string): Promise<str
});
return response.content;
}
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function getSpinner(text: string) {
const loadingType = {
"interval": 100,
"frames": [
"✶",
"✵",
"✸",
"✹",
"✺",
"✹",
"✷",
]
}
return ora({ text: `${text}\n`, spinner: loadingType }).start();
}

8
bin/utils/ip.ts vendored
View File

@@ -27,7 +27,7 @@ const ping = async (host: string) => {
const timeoutPromise = new Promise<number>((_, reject) => {
setTimeout(() => {
reject(new Error('Request timed out after 3 seconds'));
}, 3000);
}, 1000);
});
return Promise.race([requestPromise, timeoutPromise]);
@@ -40,7 +40,7 @@ async function isChinaDomain(domain: string): Promise<boolean> {
return await isChinaIP(ip, domain);
} catch (error) {
logger.debug(`${domain} can't be parse!`);
return false;
return true;
}
}
@@ -48,10 +48,10 @@ async function isChinaIP(ip: string, domain: string): Promise<boolean> {
try {
const delay = await ping(ip);
logger.debug(`${domain} latency is ${delay} ms`);
return delay > 500;
return delay > 1000;
} catch (error) {
logger.debug(`ping ${domain} failed!`);
return false;
return true;
}
}