import chalk from 'chalk'; import { InvalidArgumentError, program, Option } from 'commander'; import log from 'loglevel'; import path from 'path'; import fsExtra from 'fs-extra'; import prompts from 'prompts'; import { execa, execaSync } from 'execa'; import crypto from 'crypto'; import ora from 'ora'; import { fileURLToPath } from 'url'; import dns from 'dns'; import http from 'http'; import { promisify } from 'util'; import fs from 'fs'; import updateNotifier from 'update-notifier'; import axios from 'axios'; import { dir } from 'tmp-promise'; import { fileTypeFromBuffer } from 'file-type'; import * as psl from 'psl'; var name = "pake-cli"; var version$1 = "3.2.0-beta1"; var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 利用 Rust 轻松构建轻量级多端桌面应用。"; var engines = { node: ">=16.0.0" }; var bin = { pake: "./cli.js" }; var repository = { type: "git", url: "https://github.com/tw93/pake.git" }; var author = { name: "Tw93", email: "tw93@qq.com" }; var keywords = [ "pake", "pake-cli", "rust", "tauri", "no-electron", "productivity" ]; var files = [ "dist", "src-tauri", "cli.js" ]; var scripts = { start: "npm run dev", dev: "npm run tauri dev", build: "npm run tauri build --release", "build:debug": "npm run tauri build -- --debug", "build:mac": "npm run tauri build -- --target universal-apple-darwin", "build:config": "chmod +x script/app_config.mjs && node script/app_config.mjs", analyze: "cd src-tauri && cargo bloat --release --crates", tauri: "tauri", cli: "rollup -c rollup.config.js --watch", "cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w", "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", format: "npx prettier --write . --ignore-unknown && cd src-tauri && cargo fmt --verbose", prepublishOnly: "npm run cli:build" }; var type = "module"; var exports = "./dist/pake.js"; var license = "MIT"; var dependencies = { "@tauri-apps/api": "^2.7.0", "@tauri-apps/cli": "^2.7.1", axios: "^1.11.0", chalk: "^5.5.0", commander: "^11.1.0", execa: "^9.6.0", "file-type": "^18.7.0", "fs-extra": "^11.3.1", loglevel: "^1.9.2", ora: "^8.2.0", prompts: "^2.4.2", psl: "^1.15.0", "tmp-promise": "^3.0.3", "update-notifier": "^7.3.1" }; var devDependencies = { "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.6", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", "@types/fs-extra": "^11.0.4", "@types/node": "^20.19.10", "@types/page-icon": "^0.3.6", "@types/prompts": "^2.4.9", "@types/psl": "^1.11.0", "@types/tmp": "^0.2.6", "@types/update-notifier": "^6.0.8", "app-root-path": "^3.1.0", "cross-env": "^7.0.3", rollup: "^4.46.2", "rollup-plugin-typescript2": "^0.36.0", tslib: "^2.8.1", typescript: "^5.9.2" }; var packageJson = { name: name, version: version$1, description: description, engines: engines, bin: bin, repository: repository, author: author, keywords: keywords, files: files, scripts: scripts, type: type, exports: exports, license: license, dependencies: dependencies, devDependencies: devDependencies }; var windows = [ { url: "https://weekly.tw93.fun/", url_type: "web", hide_title_bar: true, fullscreen: false, width: 1200, height: 780, resizable: true, always_on_top: false, dark_mode: false, activation_shortcut: "", disabled_web_shortcuts: false, hide_on_close: true } ]; var user_agent = { macos: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15", linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" }; var system_tray = { macos: false, linux: true, windows: true }; var system_tray_path = "icons/icon.png"; var inject = [ ]; var proxy_url = ""; var pakeConf = { windows: windows, user_agent: user_agent, system_tray: system_tray, system_tray_path: system_tray_path, inject: inject, proxy_url: proxy_url }; var productName$1 = "Weekly"; var identifier = "com.pake.weekly"; var version = "1.0.0"; var app = { withGlobalTauri: true, trayIcon: { iconPath: "png/weekly_512.png", iconAsTemplate: false, id: "pake-tray" } }; var build = { frontendDist: "../dist" }; var CommonConf = { productName: productName$1, identifier: identifier, version: version, app: app, build: build }; var bundle$2 = { icon: [ "png/weekly_256.ico", "png/weekly_32.ico" ], active: true, resources: [ "png/weekly_32.ico" ], targets: [ "msi" ], windows: { digestAlgorithm: "sha256", wix: { language: [ "en-US" ], template: "assets/main.wxs" } } }; var WinConf = { bundle: bundle$2 }; var bundle$1 = { icon: [ "icons/weekly.icns" ], active: true, macOS: { }, targets: [ "dmg" ] }; var MacConf = { bundle: bundle$1 }; var productName = "we-read"; var bundle = { icon: [ "png/weekly.png" ], active: true, linux: { deb: { depends: [ "curl", "wget" ], files: { "/usr/share/applications/com-pake-weekly.desktop": "assets/com-pake-weekly.desktop" } } }, targets: [ "deb", "appimage" ] }; var LinuxConf = { productName: productName, bundle: bundle }; 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'; // 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'); async function shellExec(command) { try { const { exitCode } = await execa(command, { cwd: npmDirectory, stdio: 'inherit', shell: true, }); return exitCode; } catch (error) { const exitCode = error.exitCode ?? 'unknown'; const errorMessage = error.message || 'Unknown error occurred'; throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`); } } 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; } } 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); spinner.succeed(chalk.green('Rust installed successfully!')); } catch (error) { console.error('Error installing Rust:', error.message); spinner.fail(chalk.red('Rust installation failed!')); process.exit(1); } } function checkRustInstalled() { try { execaSync('rustc', ['--version']); return true; } catch { return false; } } async function combineFiles(files, output) { const contents = files.map((file) => { const fileContent = fs.readFileSync(file); if (file.endsWith('.css')) { return ("window.addEventListener('DOMContentLoaded', (_event) => { const css = `" + fileContent + "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });"); } return ("window.addEventListener('DOMContentLoaded', (_event) => { " + fileContent + ' });'); }); fs.writeFileSync(output, contents.join('\n')); return files; } async function mergeConfig(url, options, tauriConf) { const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, } = options; const { platform } = process; // Set Windows parameters. const tauriConfWindowOptions = { width, height, fullscreen, resizable, hide_title_bar: hideTitleBar, activation_shortcut: activationShortcut, always_on_top: alwaysOnTop, dark_mode: darkMode, disabled_web_shortcuts: disabledWebShortcuts, hide_on_close: hideOnClose, }; Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions }); tauriConf.productName = name; tauriConf.identifier = identifier; tauriConf.version = appVersion; if (platform == 'win32') { tauriConf.bundle.windows.wix.language[0] = installerLanguage; } //Judge the type of URL, whether it is a file or a website. const pathExists = await fsExtra.pathExists(url); if (pathExists) { logger.warn('✼ Your input might be a local file.'); tauriConf.pake.windows[0].url_type = 'local'; const fileName = path.basename(url); const dirName = path.dirname(url); const distDir = path.join(npmDirectory, 'dist'); const distBakDir = path.join(npmDirectory, 'dist_bak'); if (!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') { delete tauriConf.bundle.linux.deb.files; const validTargets = ['deb', 'appimage', 'rpm']; if (validTargets.includes(options.targets)) { tauriConf.bundle.targets = [options.targets]; } else { logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`); } } // Set icon. const platformIconMap = { win32: { fileExt: '.ico', path: `png/${name.toLowerCase()}_256.ico`, defaultIcon: 'png/icon_256.ico', message: 'Windows icon must be .ico and 256x256px.', }, linux: { fileExt: '.png', path: `png/${name.toLowerCase()}_512.png`, defaultIcon: 'png/icon_512.png', message: 'Linux icon must be .png and 512x512px.', }, darwin: { fileExt: '.icns', path: `icons/${name.toLowerCase()}.icns`, defaultIcon: 'icons/icon.icns', message: 'macOS icon must be .icns type.', }, }; const iconInfo = platformIconMap[platform]; const exists = await fsExtra.pathExists(options.icon); if (exists) { let updateIconPath = true; let customIconExt = path.extname(options.icon).toLowerCase(); if (customIconExt !== iconInfo.fileExt) { updateIconPath = false; logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`); tauriConf.bundle.icon = [iconInfo.defaultIcon]; } else { const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path); tauriConf.bundle.resources = [iconInfo.path]; await fsExtra.copy(options.icon, iconPath); } if (updateIconPath) { tauriConf.bundle.icon = [options.icon]; } else { logger.warn(`✼ Icon will remain as default.`); } } else { logger.warn('✼ Custom icon path may be invalid, default icon will be used instead.'); tauriConf.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/${name.toLowerCase()}${iconExt}`); trayIconPath = `png/${name.toLowerCase()}${iconExt}`; await fsExtra.copy(systemTrayIcon, trayIcoPath); } else { logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`); logger.warn(`✼ Default system tray icon will be used.`); } } catch { logger.warn(`✼ ${systemTrayIcon} not exists!`); logger.warn(`✼ Default system tray icon will remain unchanged.`); } } tauriConf.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) { if (!inject.every((item) => item.endsWith('.css') || item.endsWith('.js'))) { logger.error('The injected file must be in either CSS or JS format.'); return; } const files = inject.map((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 || ''; // 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 }; console.log('pakeConfig', tauriConf.pake); 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; } 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/.'); } 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); // For global CLI installation, always use npm const packageManager = 'npm'; const registryOption = isChina ? ' --registry=https://registry.npmmirror.com' : ''; if (isChina) { logger.info('✺ Located in China, using npm/rsProxy CN mirror.'); const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml'); await fsExtra.copy(projectCnConf, projectConf); await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}`); } else { await shellExec(`cd "${npmDirectory}" && ${packageManager} install`); } spinner.succeed(chalk.green('Package installed!')); if (!tauriTargetPathExists) { logger.warn('✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.'); } } async build(url) { await this.buildAndCopy(url, this.options.targets); } async start(url) { await mergeConfig(url, this.options, tauriConfig); } async buildAndCopy(url, target) { const { name } = this.options; await mergeConfig(url, this.options, tauriConfig); // Build app const spinner = getSpinner('Building app...'); setTimeout(() => spinner.stop(), 3000); await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand()}`); // Copy app const fileName = this.getFileName(); const fileType = this.getFileType(target); const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType); const distPath = path.resolve(`${name}.${fileType}`); await fsExtra.copy(appPath, distPath); await fsExtra.remove(appPath); logger.success('✔ Build success!'); logger.success('✔ App installer located in', distPath); } getFileType(target) { return target; } getBuildCommand() { // the debug option should support `--debug` and `--release` return this.options.debug ? 'npm run build:debug' : 'npm run build'; } getBasePath() { const basePath = this.options.debug ? 'debug' : 'release'; return `src-tauri/target/${basePath}/bundle/`; } getBuildAppPath(npmDirectory, fileName, fileType) { return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`); } } class MacBuilder extends BaseBuilder { constructor(options) { super(options); this.options.targets = 'dmg'; } getFileName() { const { name } = this.options; let arch; if (this.options.multiArch) { arch = 'universal'; } else { arch = process.arch === 'arm64' ? 'aarch64' : process.arch; } return `${name}_${tauriConfig.version}_${arch}`; } getBuildCommand() { return this.options.multiArch ? 'npm run build:mac' : super.getBuildCommand(); } getBasePath() { return this.options.multiArch ? 'src-tauri/target/universal-apple-darwin/release/bundle' : super.getBasePath(); } } class WinBuilder extends BaseBuilder { constructor(options) { super(options); this.options.targets = 'msi'; } getFileName() { const { name } = this.options; const { arch } = process; const language = tauriConfig.bundle.windows.wix.language[0]; return `${name}_${tauriConfig.version}_${arch}_${language}`; } } class LinuxBuilder extends BaseBuilder { constructor(options) { super(options); } getFileName() { const { name, targets } = this.options; const version = tauriConfig.version; let arch = process.arch === 'x64' ? 'amd64' : process.arch; if (arch === 'arm64' && (targets === 'rpm' || targets === 'appimage')) { arch = 'aarch64'; } // The RPM format uses different separators and version number formats if (targets === 'rpm') { return `${name}-${version}-1.${arch}`; } return `${name}_${version}_${arch}`; } // Customize it, considering that there are all targets. async build(url) { const targetTypes = ['deb', 'appimage', 'rpm']; for (const target of targetTypes) { if (this.options.targets === target) { await this.buildAndCopy(url, target); } } } getFileType(target) { if (target === 'appimage') { return 'AppImage'; } return super.getFileType(target); } } const { platform } = process; const buildersMap = { darwin: MacBuilder, win32: WinBuilder, linux: LinuxBuilder, }; class BuilderProvider { static create(options) { const Builder = buildersMap[platform]; if (!Builder) { throw new Error('The current system is not supported!'); } return new Builder(options); } } const DEFAULT_PAKE_OPTIONS = { icon: '', height: 780, width: 1200, fullscreen: false, hideTitleBar: false, alwaysOnTop: false, appVersion: '1.0.0', darkMode: false, disabledWebShortcuts: false, activationShortcut: '', userAgent: '', showSystemTray: false, multiArch: false, targets: 'deb', useLocalFile: false, systemTrayIcon: '', proxyUrl: '', debug: false, inject: [], installerLanguage: 'en-US', hideOnClose: true, }; async function checkUpdateTips() { updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({ isGlobal: true, }); } async function handleIcon(options) { if (options.icon) { if (options.icon.startsWith('http')) { return downloadIcon(options.icon); } else { return path.resolve(options.icon); } } else { logger.warn('✼ No icon given, default in use. For a custom icon, use --icon option.'); const iconPath = IS_WIN ? 'src-tauri/png/icon_256.ico' : IS_LINUX ? 'src-tauri/png/icon_512.png' : 'src-tauri/icons/icon.icns'; return path.join(npmDirectory, iconPath); } } async function downloadIcon(iconUrl) { const spinner = getSpinner('Downloading icon...'); try { const iconResponse = await axios.get(iconUrl, { responseType: 'arraybuffer', }); const iconData = await iconResponse.data; if (!iconData) { return null; } const fileDetails = await fileTypeFromBuffer(iconData); if (!fileDetails) { return null; } const { path: tempPath } = await dir(); let iconPath = `${tempPath}/icon.${fileDetails.ext}`; // Fix this for linux if (IS_LINUX) { iconPath = 'png/linux_temp.png'; await fsExtra.outputFile(`${npmDirectory}/src-tauri/${iconPath}`, iconData); } else { await fsExtra.outputFile(iconPath, iconData); } await fsExtra.outputFile(iconPath, iconData); spinner.succeed(chalk.green('Icon downloaded successfully!')); return iconPath; } catch (error) { spinner.fail(chalk.red('Icon download failed!')); if (error.response && error.response.status === 404) { return null; } throw error; } } // Extracts the domain from a given URL. function getDomain(inputUrl) { try { const url = new URL(inputUrl); // Use PSL to parse domain names. const parsed = psl.parse(url.hostname); // If domain is available, split it and return the SLD. if ('domain' in parsed && parsed.domain) { return parsed.domain.split('.')[0]; } else { return null; } } catch (error) { return null; } } // Appends 'https://' protocol to the URL if not present. function appendProtocol(inputUrl) { try { new URL(inputUrl); return inputUrl; } catch { return `https://${inputUrl}`; } } // Normalizes the URL by ensuring it has a protocol and is valid. function normalizeUrl(urlToNormalize) { const urlWithProtocol = appendProtocol(urlToNormalize); try { new URL(urlWithProtocol); return urlWithProtocol; } catch (err) { throw new Error(`Your url "${urlWithProtocol}" is invalid: ${err.message}`); } } function resolveAppName(name, platform) { const domain = getDomain(name) || 'pake'; return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain; } function isValidName(name, platform) { const platformRegexMapping = { linux: /^[a-z0-9][a-z0-9-]*$/, default: /^[a-zA-Z0-9][a-zA-Z0-9- ]*$/, }; const reg = platformRegexMapping[platform] || platformRegexMapping.default; return !!name && reg.test(name); } async function handleOptions(options, url) { 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; } // Handle platform-specific name formatting if (name && platform === 'linux') { // Convert to lowercase and replace spaces with dashes for Linux name = name.toLowerCase().replace(/\s+/g, '-'); } if (!isValidName(name, platform)) { const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`; const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`; 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 = { ...options, name, identifier: getIdentifier(url), }; appOptions.icon = await handleIcon(appOptions); return appOptions; } function validateNumberInput(value) { const parsedValue = Number(value); if (isNaN(parsedValue)) { throw new InvalidArgumentError('Not a number.'); } return parsedValue; } function validateUrlInput(url) { const isFile = fs.existsSync(url); if (!isFile) { try { return normalizeUrl(url); } catch (error) { throw new InvalidArgumentError(error.message); } } return url; } const { green, yellow } = chalk; const logo = `${chalk.green(' ____ _')} ${green('| _ \\ __ _| | _____')} ${green('| |_) / _` | |/ / _ \\')} ${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')} ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')} `; program .addHelpText('beforeAll', logo) .usage(`[url] [options]`) .showHelpAfterError(); program .argument('[url]', 'The web URL you want to package', validateUrlInput) // Refer to https://github.com/tj/commander.js#custom-option-processing, turn string array into a string connected with custom connectors. // If the platform is Linux, use `-` as the connector, and convert all characters to lowercase. // For example, Google Translate will become google-translate. .option('--name ', 'Application name') .option('--icon ', 'Application icon', DEFAULT_PAKE_OPTIONS.icon) .option('--width ', 'Window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width) .option('--height ', 'Window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height) .option('--use-local-file', 'Use local file packaging', DEFAULT_PAKE_OPTIONS.useLocalFile) .option('--fullscreen', 'Start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen) .option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT_PAKE_OPTIONS.hideTitleBar) .option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch) .option('--inject <./style.css,./script.js,...>', 'Injection of .js or .css files', (val, previous) => { if (!val) return DEFAULT_PAKE_OPTIONS.inject; // Split by comma and trim whitespace, filter out empty strings const files = val .split(',') .map((item) => item.trim()) .filter((item) => item.length > 0); // If previous values exist (from multiple --inject options), merge them return previous ? [...previous, ...files] : files; }, DEFAULT_PAKE_OPTIONS.inject) .option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug) .addOption(new Option('--proxy-url ', 'Proxy URL for all network requests (http://, https://, socks5://)') .default(DEFAULT_PAKE_OPTIONS.proxyUrl) .hideHelp()) .addOption(new Option('--user-agent ', 'Custom user agent') .default(DEFAULT_PAKE_OPTIONS.userAgent) .hideHelp()) .addOption(new Option('--targets ', 'For Linux, option "deb" or "appimage"') .default(DEFAULT_PAKE_OPTIONS.targets) .hideHelp()) .addOption(new Option('--app-version ', 'App version, the same as package.json version') .default(DEFAULT_PAKE_OPTIONS.appVersion) .hideHelp()) .addOption(new Option('--always-on-top', 'Always on the top level') .default(DEFAULT_PAKE_OPTIONS.alwaysOnTop) .hideHelp()) .addOption(new Option('--dark-mode', 'Force Mac app to use dark mode') .default(DEFAULT_PAKE_OPTIONS.darkMode) .hideHelp()) .addOption(new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts') .default(DEFAULT_PAKE_OPTIONS.disabledWebShortcuts) .hideHelp()) .addOption(new Option('--activation-shortcut ', 'Shortcut key to active App') .default(DEFAULT_PAKE_OPTIONS.activationShortcut) .hideHelp()) .addOption(new Option('--show-system-tray', 'Show system tray in app') .default(DEFAULT_PAKE_OPTIONS.showSystemTray) .hideHelp()) .addOption(new Option('--system-tray-icon ', 'Custom system tray icon') .default(DEFAULT_PAKE_OPTIONS.systemTrayIcon) .hideHelp()) .addOption(new Option('--hide-on-close', 'Hide window on close instead of exiting') .default(DEFAULT_PAKE_OPTIONS.hideOnClose) .hideHelp()) .addOption(new Option('--installer-language ', 'Installer language') .default(DEFAULT_PAKE_OPTIONS.installerLanguage) .hideHelp()) .version(packageJson.version, '-v, --version', 'Output the current version') .action(async (url, options) => { await checkUpdateTips(); if (!url) { program.outputHelp((str) => { return str .split('\n') .filter((line) => !/((-h,|--help)|((-v|-V),|--version))\s+.+$/.test(line)) .join('\n'); }); process.exit(0); } log.setDefaultLevel('info'); if (options.debug) { log.setLevel('debug'); } const appOptions = await handleOptions(options, url); log.debug('PakeAppOptions', appOptions); const builder = BuilderProvider.create(appOptions); await builder.prepare(); await builder.build(url); }); program.parse();