UNPKG

pake-cli-kevin

Version:

๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚

1,315 lines (1,299 loc) โ€ข 81.8 kB
#!/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