pake-cli-kevin
Version:
๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ ็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ
1,315 lines (1,299 loc) โข 81.8 kB
JavaScript
#!/usr/bin/env node
import log from 'loglevel';
import updateNotifier from 'update-notifier';
import path from 'path';
import fsExtra from 'fs-extra';
import { fileURLToPath } from 'url';
import chalk from 'chalk';
import prompts from 'prompts';
import os from 'os';
import { execa, execaSync } from 'execa';
import crypto from 'crypto';
import ora from 'ora';
import dns from 'dns';
import http from 'http';
import { promisify } from 'util';
import fs from 'fs';
import { dir } from 'tmp-promise';
import { fileTypeFromBuffer } from 'file-type';
import icongen from 'icon-gen';
import sharp from 'sharp';
import * as psl from 'psl';
import { InvalidArgumentError, program as program$1, Option } from 'commander';
var name = "pake-cli-kevin";
var version = "3.9.1";
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
var engines = {
node: ">=22.0.0"
};
var packageManager = "pnpm@10.26.2";
var bin = {
pake: "./dist/cli.js"
};
var repository = {
type: "git",
url: "https://github.com/KevinYouu/pake.git"
};
var author = {
name: "KevinYouu",
email: "cool@kevnu.com"
};
var keywords = [
"pake",
"pake-cli",
"rust",
"tauri",
"no-electron",
"productivity"
];
var files = [
"dist",
"src-tauri"
];
var scripts = {
start: "pnpm run dev",
dev: "pnpm run tauri dev",
build: "tauri build",
"build:debug": "tauri build --debug",
"build:mac": "tauri build --target universal-apple-darwin",
analyze: "cd src-tauri && cargo bloat --release --crates",
tauri: "tauri",
cli: "cross-env NODE_ENV=development rollup -c -w",
"cli:build": "cross-env NODE_ENV=production rollup -c",
test: "pnpm run cli:build && cross-env PAKE_CREATE_APP=1 node tests/index.js",
format: "prettier --write . --ignore-unknown && find tests -name '*.js' -exec sed -i '' 's/[[:space:]]*$//' {} \\; && cd src-tauri && cargo fmt --verbose",
"format:check": "prettier --check . --ignore-unknown",
update: "pnpm update --verbose && cd src-tauri && cargo update",
prepublishOnly: "pnpm run cli:build"
};
var type = "module";
var exports$1 = "./dist/cli.js";
var license = "MIT";
var dependencies = {
"@tauri-apps/api": "~2.10.1",
"@tauri-apps/cli": "^2.10.0",
chalk: "^5.6.2",
commander: "^14.0.3",
execa: "^9.6.1",
"file-type": "^21.3.0",
"fs-extra": "^11.3.3",
"icon-gen": "^5.0.0",
loglevel: "^1.9.2",
ora: "^9.3.0",
prompts: "^2.4.2",
psl: "^1.15.0",
sharp: "^0.34.5",
"tmp-promise": "^3.0.3",
"update-notifier": "^7.3.1"
};
var devDependencies = {
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-replace": "^6.0.3",
"@rollup/plugin-terser": "^0.4.4",
"@types/fs-extra": "^11.0.4",
"@types/node": "^25.3.0",
"@types/page-icon": "^0.3.6",
"@types/prompts": "^2.4.9",
"@types/tmp": "^0.2.6",
"@types/update-notifier": "^6.0.8",
"app-root-path": "^3.1.0",
"cross-env": "^10.1.0",
prettier: "^3.8.1",
rollup: "^4.58.0",
"rollup-plugin-typescript2": "^0.36.0",
tslib: "^2.8.1",
typescript: "^5.9.3",
vitest: "^4.0.18"
};
var pnpm = {
overrides: {
sharp: "^0.34.5"
},
onlyBuiltDependencies: [
"esbuild",
"sharp"
]
};
var packageJson = {
name: name,
version: version,
description: description,
engines: engines,
packageManager: packageManager,
bin: bin,
repository: repository,
author: author,
keywords: keywords,
files: files,
scripts: scripts,
type: type,
exports: exports$1,
license: license,
dependencies: dependencies,
devDependencies: devDependencies,
pnpm: pnpm
};
// Convert the current module URL to a file path
const currentModulePath = fileURLToPath(import.meta.url);
// Resolve the parent directory of the current module
const npmDirectory = path.join(path.dirname(currentModulePath), '..');
const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri', '.pake');
// Load configs from npm package directory, not from project source
const tauriSrcDir = path.join(npmDirectory, 'src-tauri');
const pakeConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'pake.json'));
const CommonConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.conf.json'));
const WinConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.windows.conf.json'));
const MacConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.macos.conf.json'));
const LinuxConf = fsExtra.readJSONSync(path.join(tauriSrcDir, 'tauri.linux.conf.json'));
const platformConfigs = {
win32: WinConf,
darwin: MacConf,
linux: LinuxConf,
};
const { platform: platform$2 } = process;
// @ts-ignore
const platformConfig = platformConfigs[platform$2];
let tauriConfig = {
...CommonConf,
bundle: platformConfig.bundle,
app: {
...CommonConf.app,
trayIcon: {
...(platformConfig?.app?.trayIcon ?? {}),
},
},
build: CommonConf.build,
pake: pakeConf,
};
// Generates an identifier based on the given URL.
function getIdentifier(url) {
const postFixHash = crypto
.createHash('md5')
.update(url)
.digest('hex')
.substring(0, 6);
return `com.pake.${postFixHash}`;
}
async function promptText(message, initial) {
const response = await prompts({
type: 'text',
name: 'content',
message,
initial,
});
return response.content;
}
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function getSpinner(text) {
const loadingType = {
interval: 80,
frames: ['โฆ', 'โถ', 'โบ', 'โต', 'โธ', 'โน', 'โบ'],
};
return ora({
text: `${chalk.cyan(text)}\n`,
spinner: loadingType,
color: 'cyan',
}).start();
}
const { platform: platform$1 } = process;
const IS_MAC = platform$1 === 'darwin';
const IS_WIN = platform$1 === 'win32';
const IS_LINUX = platform$1 === 'linux';
async function shellExec(command, timeout = 300000, env) {
try {
const { exitCode } = await execa(command, {
cwd: npmDirectory,
// Use 'inherit' to show all output directly to user in real-time.
// This ensures linuxdeploy and other tool outputs are visible during builds.
stdio: 'inherit',
shell: true,
timeout,
env: env ? { ...process.env, ...env } : process.env,
});
return exitCode;
}
catch (error) {
const exitCode = error.exitCode ?? 'unknown';
const errorMessage = error.message || 'Unknown error occurred';
if (error.timedOut) {
throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
}
let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;
// Provide helpful guidance for common Linux AppImage build failures
// caused by strip tool incompatibility with modern glibc (2.38+)
const lowerError = errorMessage.toLowerCase();
if (process.platform === 'linux' &&
(lowerError.includes('linuxdeploy') ||
lowerError.includes('appimage') ||
lowerError.includes('strip'))) {
errorMsg +=
'\n\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n' +
'Linux AppImage Build Failed\n' +
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n' +
'Cause: Strip tool incompatibility with glibc 2.38+\n' +
' (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
'Quick fix:\n' +
' NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
'Alternatives:\n' +
' โข Use DEB format: pake <url> --targets deb\n' +
' โข Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
' โข Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ';
if (lowerError.includes('fuse') ||
lowerError.includes('operation not permitted') ||
lowerError.includes('/dev/fuse')) {
errorMsg +=
'\n\nDocker / Container hint:\n' +
' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' +
' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' +
' or run on the host directly.';
}
}
throw new Error(errorMsg);
}
}
const logger = {
info(...msg) {
log.info(...msg.map((m) => chalk.white(m)));
},
debug(...msg) {
log.debug(...msg);
},
error(...msg) {
log.error(...msg.map((m) => chalk.red(m)));
},
warn(...msg) {
log.info(...msg.map((m) => chalk.yellow(m)));
},
success(...msg) {
log.info(...msg.map((m) => chalk.green(m)));
},
};
const resolve = promisify(dns.resolve);
const ping = async (host) => {
const lookup = promisify(dns.lookup);
const ip = await lookup(host);
const start = new Date();
// Prevent timeouts from affecting user experience.
const requestPromise = new Promise((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((_, reject) => {
setTimeout(() => {
reject(new Error('Request timed out after 3 seconds'));
}, 1000);
});
return Promise.race([requestPromise, timeoutPromise]);
};
async function isChinaDomain(domain) {
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, domain) {
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;
}
}
function normalizePathForComparison(targetPath) {
const normalized = path.normalize(targetPath);
return IS_WIN ? normalized.toLowerCase() : normalized;
}
function getCargoHomeCandidates() {
const candidates = new Set();
if (process.env.CARGO_HOME) {
candidates.add(process.env.CARGO_HOME);
}
const homeDir = os.homedir();
if (homeDir) {
candidates.add(path.join(homeDir, '.cargo'));
}
if (IS_WIN && process.env.USERPROFILE) {
candidates.add(path.join(process.env.USERPROFILE, '.cargo'));
}
return Array.from(candidates).filter(Boolean);
}
function ensureCargoBinOnPath() {
const currentPath = process.env.PATH || '';
const segments = currentPath.split(path.delimiter).filter(Boolean);
const normalizedSegments = new Set(segments.map((segment) => normalizePathForComparison(segment)));
const additions = [];
let cargoHomeSet = Boolean(process.env.CARGO_HOME);
for (const cargoHome of getCargoHomeCandidates()) {
const binDir = path.join(cargoHome, 'bin');
if (fsExtra.pathExistsSync(binDir) &&
!normalizedSegments.has(normalizePathForComparison(binDir))) {
additions.push(binDir);
normalizedSegments.add(normalizePathForComparison(binDir));
}
if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {
process.env.CARGO_HOME = cargoHome;
cargoHomeSet = true;
}
}
if (additions.length) {
const prefix = additions.join(path.delimiter);
process.env.PATH = segments.length
? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`
: prefix;
}
}
function ensureRustEnv() {
ensureCargoBinOnPath();
}
async function installRust() {
const isActions = process.env.GITHUB_ACTIONS;
const isInChina = await isChinaDomain('sh.rustup.rs');
const rustInstallScriptForMac = isInChina && !isActions
? '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, 300000, undefined);
spinner.succeed(chalk.green('โ Rust installed successfully!'));
ensureRustEnv();
}
catch (error) {
spinner.fail(chalk.red('โ Rust installation failed!'));
if (error instanceof Error) {
console.error(error.message);
}
else {
console.error(error);
}
process.exit(1);
}
}
function checkRustInstalled() {
ensureCargoBinOnPath();
try {
execaSync('rustc', ['--version']);
return true;
}
catch {
return false;
}
}
async function combineFiles(files, output) {
const contents = files.map((file) => {
if (file.endsWith('.css')) {
const fileContent = fs.readFileSync(file, 'utf-8');
return `window.addEventListener('DOMContentLoaded', (_event) => {
const css = ${JSON.stringify(fileContent)};
const style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);
});`;
}
const fileContent = fs.readFileSync(file);
return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
fileContent +
' });');
});
fs.writeFileSync(output, contents.join('\n'));
return files;
}
function generateSafeFilename(name) {
return name
.replace(/[<>:"/\\|?*]/g, '_')
.replace(/\s+/g, '_')
.replace(/\.+$/g, '')
.slice(0, 255);
}
function getSafeAppName(name) {
return generateSafeFilename(name).toLowerCase();
}
function generateLinuxPackageName(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
.replace(/^-+|-+$/g, '')
.replace(/-+/g, '-');
}
function generateIdentifierSafeName(name) {
const cleaned = name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '').toLowerCase();
if (cleaned === '') {
const fallback = Array.from(name)
.map((char) => {
const code = char.charCodeAt(0);
if ((code >= 48 && code <= 57) ||
(code >= 65 && code <= 90) ||
(code >= 97 && code <= 122)) {
return char.toLowerCase();
}
return code.toString(16);
})
.join('')
.slice(0, 50);
return fallback || 'pake-app';
}
return cleaned;
}
async function mergeConfig(url, options, tauriConf) {
// Ensure .pake directory exists and copy source templates if needed
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
await fsExtra.ensureDir(tauriConfigDirectory);
// Copy source config files to .pake directory (as templates)
const sourceFiles = [
'tauri.conf.json',
'tauri.macos.conf.json',
'tauri.windows.conf.json',
'tauri.linux.conf.json',
'pake.json',
];
await Promise.all(sourceFiles.map(async (file) => {
const sourcePath = path.join(srcTauriDir, file);
const destPath = path.join(tauriConfigDirectory, file);
if ((await fsExtra.pathExists(sourcePath)) &&
!(await fsExtra.pathExists(destPath))) {
await fsExtra.copy(sourcePath, destPath);
}
}));
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, startToTray, forceInternalNavigation, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, } = options;
const { platform } = process;
const platformHideOnClose = hideOnClose ?? platform === 'darwin';
const tauriConfWindowOptions = {
width,
height,
fullscreen,
maximize,
resizable,
hide_title_bar: hideTitleBar,
activation_shortcut: activationShortcut,
always_on_top: alwaysOnTop,
dark_mode: darkMode,
disabled_web_shortcuts: disabledWebShortcuts,
hide_on_close: platformHideOnClose,
incognito: incognito,
title: title,
enable_wasm: wasm,
enable_drag_drop: enableDragDrop,
start_to_tray: startToTray && showSystemTray,
force_internal_navigation: forceInternalNavigation,
zoom,
min_width: minWidth,
min_height: minHeight,
ignore_certificate_errors: ignoreCertificateErrors,
new_window: newWindow,
};
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
tauriConf.productName = name;
tauriConf.identifier = identifier;
tauriConf.version = appVersion;
// Always set mainBinaryName to ensure binary uniqueness
const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
tauriConf.mainBinaryName =
platform === 'linux'
? linuxBinaryName
: `pake-${generateIdentifierSafeName(name)}`;
if (platform == 'win32') {
tauriConf.bundle.windows.wix.language[0] = installerLanguage;
}
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 (!useLocalFile) {
const urlPath = path.join(distDir, fileName);
await fsExtra.copy(url, urlPath);
}
else {
fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
fsExtra.copySync(dirName, distDir, { overwrite: true });
// ignore it, because about_pake.html have be erased.
// const filesToCopyBack = ['cli.js', 'about_pake.html'];
const filesToCopyBack = ['cli.js'];
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';
}
const platformMap = {
win32: 'windows',
linux: 'linux',
darwin: 'macos',
};
const currentPlatform = platformMap[platform];
if (userAgent.length > 0) {
tauriConf.pake.user_agent[currentPlatform] = userAgent;
}
tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
// Processing targets are currently only open to Linux.
if (platform === 'linux') {
// Remove hardcoded desktop files and regenerate with correct app name
delete tauriConf.bundle.linux.deb.files;
// Generate correct desktop file configuration
const linuxName = generateLinuxPackageName(name);
const desktopFileName = `com.pake.${linuxName}.desktop`;
const iconName = `${linuxName}_512`;
// Create desktop file content
// Determine if title contains Chinese characters for Name[zh_CN]
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
const desktopContent = `[Desktop Entry]
Version=1.0
Type=Application
Name=${name}
${chineseName ? `Name[zh_CN]=${chineseName}` : ''}
Comment=${name}
Exec=${linuxBinaryName}
Icon=${iconName}
Categories=Network;WebBrowser;Utility;
MimeType=text/html;text/xml;application/xhtml_xml;
StartupNotify=true
Terminal=false
`;
// Write desktop file to src-tauri/assets directory where Tauri expects it
const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
await fsExtra.ensureDir(srcAssetsDir);
await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
// Set up desktop file in bundle configuration
// Use absolute path from src-tauri directory to assets
const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
tauriConf.bundle.linux.deb.files = {
[desktopInstallPath]: `assets/${desktopFileName}`,
};
// Add desktop file support for RPM
if (!tauriConf.bundle.linux.rpm) {
tauriConf.bundle.linux.rpm = {};
}
tauriConf.bundle.linux.rpm.files = {
[desktopInstallPath]: `assets/${desktopFileName}`,
};
const validTargets = [
'deb',
'appimage',
'rpm',
'deb-arm64',
'appimage-arm64',
'rpm-arm64',
];
const baseTarget = options.targets.includes('-arm64')
? options.targets.replace('-arm64', '')
: options.targets;
if (validTargets.includes(options.targets)) {
tauriConf.bundle.targets = [baseTarget];
}
else {
logger.warn(`โผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
}
}
// Set macOS bundle targets (for app vs dmg)
if (platform === 'darwin') {
const validMacTargets = ['app', 'dmg'];
if (validMacTargets.includes(options.targets)) {
tauriConf.bundle.targets = [options.targets];
}
}
// Set icon.
const safeAppName = getSafeAppName(name);
const platformIconMap = {
win32: {
fileExt: '.ico',
path: `png/${safeAppName}_256.ico`,
defaultIcon: 'png/icon_256.ico',
message: 'Windows icon must be .ico and 256x256px.',
},
linux: {
fileExt: '.png',
path: `png/${generateLinuxPackageName(name)}_512.png`,
defaultIcon: 'png/icon_512.png',
message: 'Linux icon must be .png and 512x512px.',
},
darwin: {
fileExt: '.icns',
path: `icons/${safeAppName}.icns`,
defaultIcon: 'icons/icon.icns',
message: 'macOS icon must be .icns type.',
},
};
const iconInfo = platformIconMap[platform];
const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;
const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
if (exists) {
let updateIconPath = true;
let customIconExt = path.extname(resolvedIconPath).toLowerCase();
if (customIconExt !== iconInfo.fileExt) {
updateIconPath = false;
logger.warn(`โผ ${iconInfo.message}, but you give ${customIconExt}`);
tauriConf.bundle.icon = [iconInfo.defaultIcon];
}
else {
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
tauriConf.bundle.resources = [iconInfo.path];
// Avoid copying if source and destination are the same
const absoluteDestPath = path.resolve(iconPath);
if (resolvedIconPath !== absoluteDestPath) {
await fsExtra.copy(resolvedIconPath, iconPath);
}
}
if (updateIconPath) {
tauriConf.bundle.icon = [iconInfo.path];
}
else {
logger.warn(`โผ Icon will remain as default.`);
}
}
else {
logger.warn('โผ Custom icon path may be invalid, default icon will be used instead.');
tauriConf.bundle.icon = [iconInfo.defaultIcon];
}
// Set tray icon path.
let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.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/${safeAppName}${iconExt}`);
trayIconPath = `png/${safeAppName}${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.`);
}
}
// Ensure trayIcon object exists before setting iconPath
if (!tauriConf.app.trayIcon) {
tauriConf.app.trayIcon = {};
}
tauriConf.app.trayIcon.iconPath = trayIconPath;
tauriConf.pake.system_tray_path = trayIconPath;
delete tauriConf.app.trayIcon;
const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
// inject js or css files
if (inject?.length > 0) {
// Ensure inject is an array before calling .every()
const injectArray = Array.isArray(inject) ? inject : [inject];
if (!injectArray.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
logger.error('The injected file must be in either CSS or JS format.');
return;
}
const files = injectArray.map((filepath) => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
tauriConf.pake.inject = files;
await combineFiles(files, injectFilePath);
}
else {
tauriConf.pake.inject = [];
await fsExtra.writeFile(injectFilePath, '');
}
tauriConf.pake.proxy_url = proxyUrl || '';
tauriConf.pake.multi_instance = multiInstance;
// Configure WASM support with required HTTP headers
if (wasm) {
tauriConf.app.security = {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
};
}
// Save config file.
const platformConfigPaths = {
win32: 'tauri.windows.conf.json',
darwin: 'tauri.macos.conf.json',
linux: 'tauri.linux.conf.json',
};
const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]);
const bundleConf = { bundle: tauriConf.bundle };
await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake;
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
}
class BaseBuilder {
constructor(options) {
this.options = options;
}
getBuildEnvironment() {
return IS_MAC
? {
CFLAGS: '-fno-modules',
CXXFLAGS: '-fno-modules',
MACOSX_DEPLOYMENT_TARGET: '14.0',
}
: undefined;
}
getInstallTimeout() {
// Windows needs more time due to native compilation and antivirus scanning
return process.platform === 'win32' ? 900000 : 600000;
}
getBuildTimeout() {
return 900000;
}
async detectPackageManager() {
if (BaseBuilder.packageManagerCache) {
return BaseBuilder.packageManagerCache;
}
const { execa } = await import('execa');
try {
await execa('pnpm', ['--version'], { stdio: 'ignore' });
logger.info('โบ Using pnpm for package management.');
BaseBuilder.packageManagerCache = 'pnpm';
return 'pnpm';
}
catch {
try {
await execa('npm', ['--version'], { stdio: 'ignore' });
logger.info('โบ pnpm not available, using npm for package management.');
BaseBuilder.packageManagerCache = 'npm';
return 'npm';
}
catch {
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
}
}
}
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.warn('โผ The first use requires installing system dependencies.');
logger.warn('โผ See more in https://tauri.app/start/prerequisites/.');
}
ensureRustEnv();
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.toml');
await fsExtra.ensureDir(rustProjectDir);
// Detect available package manager
const packageManager = await this.detectPackageManager();
const registryOption = ' --registry=https://registry.npmmirror.com';
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
const timeout = this.getInstallTimeout();
const buildEnv = this.getBuildEnvironment();
// Show helpful message for first-time users
if (!tauriTargetPathExists) {
logger.info(process.platform === 'win32'
? 'โบ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
: 'โบ First-time setup may take 5-10 minutes (installing dependencies)...');
}
let usedMirror = isChina;
try {
if (isChina) {
logger.info(`โบ Located in China, using ${packageManager}/rsProxy CN mirror.`);
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
await fsExtra.copy(projectCnConf, projectConf);
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
}
else {
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
}
spinner.succeed(chalk.green('Package installed!'));
}
catch (error) {
// If installation times out and we haven't tried the mirror yet, retry with mirror
if (error instanceof Error &&
error.message.includes('timed out') &&
!usedMirror) {
spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
logger.info('โบ Retrying installation with CN mirror for better speed...');
const retrySpinner = getSpinner('Retrying installation...');
usedMirror = true;
try {
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
await fsExtra.copy(projectCnConf, projectConf);
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
}
catch (retryError) {
retrySpinner.fail(chalk.red('Installation failed'));
throw retryError;
}
}
else {
spinner.fail(chalk.red('Installation failed'));
throw error;
}
}
if (!tauriTargetPathExists) {
logger.warn('โผ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
}
}
async build(url) {
await this.buildAndCopy(url, this.options.targets);
}
async start(url) {
logger.info('Pake dev server starting...');
await mergeConfig(url, this.options, tauriConfig);
const packageManager = await this.detectPackageManager();
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
const features = this.getBuildFeatures();
const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
const argSeparator = packageManager === 'npm' ? ' --' : '';
const command = `cd "${npmDirectory}" && ${packageManager} run tauri${argSeparator} dev --config "${configPath}" ${featureArgs}`;
await shellExec(command);
}
async buildAndCopy(url, target) {
const { name = 'pake-app' } = this.options;
await mergeConfig(url, this.options, tauriConfig);
// Detect available package manager
const packageManager = await this.detectPackageManager();
// Build app
const buildSpinner = getSpinner('Building app...');
// Let spinner run for a moment so user can see it, then stop before package manager command
await new Promise((resolve) => setTimeout(resolve, 500));
buildSpinner.stop();
// Show static message to keep the status visible
logger.warn('โธ Building app...');
const baseEnv = this.getBuildEnvironment();
let buildEnv = {
...(baseEnv ?? {}),
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
};
const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined;
// Warn users about potential AppImage build failures on modern Linux systems.
// The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't
// recognize the .relr.dyn section introduced in glibc 2.38+.
if (process.platform === 'linux' && target === 'appimage') {
if (!buildEnv.NO_STRIP) {
logger.warn('โ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+');
logger.warn('โ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage');
}
}
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
const buildTimeout = this.getBuildTimeout();
try {
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
}
catch (error) {
const shouldRetryWithoutStrip = process.platform === 'linux' &&
target === 'appimage' &&
!buildEnv.NO_STRIP &&
this.isLinuxDeployStripError(error);
if (shouldRetryWithoutStrip) {
logger.warn('โ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
buildEnv = {
...buildEnv,
NO_STRIP: '1',
};
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
}
else {
throw error;
}
}
// 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);
// Copy raw binary if requested
if (this.options.keepBinary) {
await this.copyRawBinary(npmDirectory, name);
}
await fsExtra.remove(appPath);
logger.success('โ Build success!');
logger.success('โ App installer located in', distPath);
// Log binary location if preserved
if (this.options.keepBinary) {
const binaryPath = this.getRawBinaryPath(name);
logger.success('โ Raw binary located in', path.resolve(binaryPath));
}
}
getFileType(target) {
return target;
}
isLinuxDeployStripError(error) {
if (!(error instanceof Error) || !error.message) {
return false;
}
const message = error.message.toLowerCase();
return (message.includes('linuxdeploy') ||
message.includes('failed to run linuxdeploy') ||
message.includes('strip:') ||
message.includes('unable to recognise the format of the input file') ||
message.includes('appimage tool failed') ||
message.includes('strip tool'));
}
/**
* ่งฃๆ็ฎๆ ๆถๆ
*/
resolveTargetArch(requestedArch) {
if (requestedArch === 'auto' || !requestedArch) {
return process.arch;
}
return requestedArch;
}
/**
* ่ทๅTauriๆๅปบ็ฎๆ
*/
getTauriTarget(arch, platform = process.platform) {
const platformMappings = BaseBuilder.ARCH_MAPPINGS[platform];
if (!platformMappings)
return null;
return platformMappings[arch] || null;
}
/**
* ่ทๅๆถๆๆพ็คบๅ็งฐ๏ผ็จไบๆไปถๅ๏ผ
*/
getArchDisplayName(arch) {
return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
}
/**
* ๆๅปบๅบ็กๆๅปบๅฝไปค
*/
buildBaseCommand(packageManager, configPath, target) {
const baseCommand = this.options.debug
? `${packageManager} run build:debug`
: `${packageManager} run build`;
const argSeparator = packageManager === 'npm' ? ' --' : '';
let fullCommand = `${baseCommand}${argSeparator} -c "${configPath}"`;
if (target) {
fullCommand += ` --target ${target}`;
}
// Enable verbose output in debug mode to help diagnose build issues.
// This provides detailed logs from Tauri CLI and bundler tools.
if (this.options.debug) {
fullCommand += ' --verbose';
}
return fullCommand;
}
/**
* ่ทๅๆๅปบ็นๆงๅ่กจ
*/
getBuildFeatures() {
const features = ['cli-build'];
// Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
if (IS_MAC) {
const macOSVersion = this.getMacOSMajorVersion();
if (macOSVersion >= 23) {
features.push('macos-proxy');
}
}
return features;
}
getBuildCommand(packageManager = 'pnpm') {
// Use temporary config directory to avoid modifying source files
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
let fullCommand = this.buildBaseCommand(packageManager, configPath);
// For macOS, use app bundles by default unless DMG is explicitly requested
if (IS_MAC && this.options.targets === 'app') {
fullCommand += ' --bundles app';
}
// Add features
const features = this.getBuildFeatures();
if (features.length > 0) {
fullCommand += ` --features ${features.join(',')}`;
}
return fullCommand;
}
getMacOSMajorVersion() {
try {
const os = require('os');
const release = os.release();
const majorVersion = parseInt(release.split('.')[0], 10);
return majorVersion;
}
catch (error) {
return 0; // Disable proxy feature if version detection fails
}
}
getBasePath() {
const basePath = this.options.debug ? 'debug' : 'release';
return `src-tauri/target/${basePath}/bundle/`;
}
getBuildAppPath(npmDirectory, fileName, fileType) {
// For app bundles on macOS, the directory is 'macos', not 'app'
const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
}
/**
* Copy raw binary file to output directory
*/
async copyRawBinary(npmDirectory, appName) {
const binaryPath = this.getRawBinarySourcePath(npmDirectory, appName);
const outputPath = this.getRawBinaryPath(appName);
if (await fsExtra.pathExists(binaryPath)) {
await fsExtra.copy(binaryPath, outputPath);
// Make binary executable on Unix-like systems
if (process.platform !== 'win32') {
await fsExtra.chmod(outputPath, 0o755);
}
}
else {
logger.warn(`โผ Raw binary not found at ${binaryPath}, skipping...`);
}
}
/**
* Get the source path of the raw binary file in the build directory
*/
getRawBinarySourcePath(npmDirectory, appName) {
const basePath = this.options.debug ? 'debug' : 'release';
const binaryName = this.getBinaryName(appName);
// Handle cross-platform builds
if (this.options.multiArch || this.hasArchSpecificTarget()) {
return path.join(npmDirectory, this.getArchSpecificPath(), basePath, binaryName);
}
return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName);
}
/**
* Get the output path for the raw binary file
*/
getRawBinaryPath(appName) {
const extension = process.platform === 'win32' ? '.exe' : '';
const suffix = process.platform === 'win32' ? '' : '-binary';
return `${appName}${suffix}${extension}`;
}
/**
* Get the binary name based on app name and platform
*/
getBinaryName(appName) {
const extension = process.platform === 'win32' ? '.exe' : '';
// Use unique binary name for all platforms to avoid conflicts
const nameToUse = process.platform === 'linux'
? generateLinuxPackageName(appName)
: generateIdentifierSafeName(appName);
return `pake-${nameToUse}${extension}`;
}
/**
* Check if this build has architecture-specific target
*/
hasArchSpecificTarget() {
return false; // Override in subclasses if needed
}
/**
* Get architecture-specific path for binary
*/
getArchSpecificPath() {
return 'src-tauri/target'; // Override in subclasses if needed
}
}
BaseBuilder.packageManagerCache = null;
// ๆถๆๆ ๅฐ้
็ฝฎ
BaseBuilder.ARCH_MAPPINGS = {
darwin: {
arm64: 'aarch64-apple-darwin',
x64: 'x86_64-apple-darwin',
universal: 'universal-apple-darwin',
},
win32: {
arm64: 'aarch64-pc-windows-msvc',
x64: 'x86_64-pc-windows-msvc',
},
linux: {
arm64: 'aarch64-unknown-linux-gnu',
x64: 'x86_64-unknown-linux-gnu',
},
};
// ๆถๆๅ็งฐๆ ๅฐ๏ผ็จไบๆไปถๅ็ๆ๏ผ
BaseBuilder.ARCH_DISPLAY_NAMES = {
arm64: 'aarch64',
x64: 'x64',
universal: 'universal',
};
class MacBuilder extends BaseBuilder {
constructor(options) {
super(options);
const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64'];
this.buildArch = validArchs.includes(options.targets || '')
? options.targets
: 'auto';
if (options.iterativeBuild || process.env.PAKE_CREATE_APP === '1') {
this.buildFormat = 'app';
}
else {
this.buildFormat = 'dmg';
}
this.options.targets = this.buildFormat;
}
getFileName() {
const { name = 'pake-app' } = this.options;
if (this.buildFormat === 'app') {
return name;
}
let arch;
if (this.buildArch === 'universal' || this.options.multiArch) {
arch = 'universal';
}
else if (this.buildArch === 'apple') {
arch = 'aarch64';
}
else if (this.buildArch === 'intel') {
arch = 'x64';
}
else {
arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
}
return `${name}_${tauriConfig.version}_${arch}`;
}
getActualArch() {
if (this.buildArch === 'universal' || this.options.multiArch) {
return 'universal';
}
else if (this.buildArch === 'apple') {
return 'arm64';
}
else if (this.buildArch === 'intel') {
return 'x64';
}
return this.resolveTargetArch(this.buildArch);
}
getBuildCommand(packageManager = 'pnpm') {
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
const actualArch = this.getActualArch();
const buildTarget = this.getTauriTarget(actualArch, 'darwin');
if (!buildTarget) {
throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
}
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
const features = this.getBuildFeatures();
if (features.length > 0) {
fullCommand += ` --features ${features.join(',')}`;
}
return fullCommand;
}
getBasePath() {
const basePath = this.options.debug ? 'debug' : 'release';
const actualArch = this.getActualArch();
const target = this.getTauriTarget(actualArch, 'darwin');
return `src-tauri/target/${target}/${basePath}/bundle`;
}
hasArchSpecificTarget() {
return true;
}
getArchSpecificPath() {
const actualArch = this.getActualArch();
const target = this.getTauriTarget(actualArch, 'darwin');
return `src-tauri/target/${target}`;
}
}
class WinBuilder extends BaseBuilder {
constructor(options) {
super(options);
this.buildFormat = 'msi';
const validArchs = ['x64', 'arm64', 'auto'];
this.buildArch = validArchs.includes(options.targets || '')
? this.resolveTargetArch(options.targets)
: this.resolveTargetArch('auto');
this.options.targets = this.buildFormat;
}
getFileName() {
const { name } = this.options;
const language = tauriConfig.bundle.windows.wix.language[0];
const targetArch = this.getArchDisplayName(this.buildArch);
return `${name}_${tauriConfig.version}_${targetArch}_${language}`;
}
getBuildCommand(packageManager = 'pnpm') {
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
const buildTarget = this.getTauriTarget(this.buildArch, 'win32');
if (!buildTarget) {
throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
}
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
const features = this.getBuildFeatures();
if (features.length > 0) {
fullCommand += ` --features ${features.join(',')}`;
}
return fullCommand;
}
getBasePath() {
const basePath = this.options.debug ? 'debug' : 'release';
const target = this.getTauriTarget(this.buildArch, 'win32');
return `src-tauri/target/${target}/${basePath}/bundle/`;
}
hasArchSpecificTarget() {
return true;
}
getArchSpecificPath() {
const target = this.getTauriTarget(this.buildArch, 'win32');
return `src-tauri/target/${target}`;
}
}
class LinuxBuilder extends BaseBuilder {
constructor(options) {
super(options);
this.currentBuildType = '';
const target = options.targets || 'deb';
if (target.includes('-arm64')) {
this.buildFormat = target.replace('-arm64', '');
this.buildArch = 'arm64';
}
else {
this.buildFormat = target;
this.buildArch = this.resolveTargetArch('auto');
}
this.options.targets = this.buildFormat;
}
getFileName() {
const { name = 'pake-app', targets } = this.options;
const version = tauriConfig.version;
const buildType = this.currentBuildType || targets.split(',').map((t) => t.trim())[0];
let arch;
if (this.buildArch === 'arm64') {
arch =
buildType === 'rpm' || buildType === 'appimage' ? 'aarch64' : 'arm64';
}
else {
if (this.buildArch === 'x64') {
arch = buildType === 'rpm' ? 'x86_64' : 'amd64';
}
else {
arch = this.buildArch;
if (this.buildArch === 'arm64' &&
(buildType === 'rpm' || buildType === 'appimage')) {
arch = 'aarch64';
}
}
}
if (this.currentBuildType === 'rpm') {
return `${name}-${version}-1.${arch}`;
}
return `${name