pake-cli 3.11.3 โ 3.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli.js +372 -238
- package/package.json +3 -3
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/app/setup.rs +39 -34
- package/src-tauri/src/app/window.rs +63 -17
- package/src-tauri/src/inject/{component.js โ fullscreen.js} +7 -41
- package/src-tauri/src/inject/toast.js +22 -0
- package/src-tauri/src/lib.rs +8 -2
- package/src-tauri/src/util.rs +144 -29
- package/src-tauri/tauri.conf.json +1 -1
- package/dist/test-local.html +0 -1
- package/src-tauri/.cargo/config.toml +0 -10
package/dist/cli.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import log from 'loglevel';
|
|
3
|
+
import chalk from 'chalk';
|
|
3
4
|
import updateNotifier from 'update-notifier';
|
|
4
5
|
import path from 'path';
|
|
5
6
|
import fsExtra from 'fs-extra';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import chalk from 'chalk';
|
|
8
8
|
import prompts from 'prompts';
|
|
9
9
|
import os from 'os';
|
|
10
10
|
import { execa, execaSync } from 'execa';
|
|
11
11
|
import crypto from 'crypto';
|
|
12
12
|
import ora from 'ora';
|
|
13
|
-
import dns from 'dns';
|
|
14
|
-
import http from 'http';
|
|
15
|
-
import { promisify } from 'util';
|
|
16
13
|
import fs from 'fs/promises';
|
|
17
14
|
import { dir } from 'tmp-promise';
|
|
18
15
|
import { fileTypeFromBuffer } from 'file-type';
|
|
@@ -23,18 +20,18 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
|
23
20
|
import fs$1 from 'fs';
|
|
24
21
|
|
|
25
22
|
var name = "pake-cli";
|
|
26
|
-
var version = "3.11.
|
|
23
|
+
var version = "3.11.5";
|
|
27
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
28
25
|
var engines = {
|
|
29
26
|
node: ">=18.0.0"
|
|
30
27
|
};
|
|
31
28
|
var packageManager = "pnpm@10.26.2";
|
|
32
29
|
var bin = {
|
|
33
|
-
pake: "
|
|
30
|
+
pake: "dist/cli.js"
|
|
34
31
|
};
|
|
35
32
|
var repository = {
|
|
36
33
|
type: "git",
|
|
37
|
-
url: "https://github.com/tw93/pake.git"
|
|
34
|
+
url: "git+https://github.com/tw93/pake.git"
|
|
38
35
|
};
|
|
39
36
|
var author = {
|
|
40
37
|
name: "Tw93",
|
|
@@ -220,6 +217,12 @@ function getSpinner(text) {
|
|
|
220
217
|
}).start();
|
|
221
218
|
}
|
|
222
219
|
|
|
220
|
+
const TRUE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
221
|
+
const CN_MIRROR_ENV = 'PAKE_USE_CN_MIRROR';
|
|
222
|
+
function isCnMirrorEnabled(value = process.env[CN_MIRROR_ENV]) {
|
|
223
|
+
return TRUE_VALUES.has((value ?? '').trim().toLowerCase());
|
|
224
|
+
}
|
|
225
|
+
|
|
223
226
|
const { platform: platform$1 } = process;
|
|
224
227
|
const IS_MAC = platform$1 === 'darwin';
|
|
225
228
|
const IS_WIN = platform$1 === 'win32';
|
|
@@ -279,69 +282,6 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
284
|
|
|
282
|
-
const logger = {
|
|
283
|
-
info(...msg) {
|
|
284
|
-
log.info(...msg.map((m) => chalk.white(m)));
|
|
285
|
-
},
|
|
286
|
-
debug(...msg) {
|
|
287
|
-
log.debug(...msg);
|
|
288
|
-
},
|
|
289
|
-
error(...msg) {
|
|
290
|
-
log.error(...msg.map((m) => chalk.red(m)));
|
|
291
|
-
},
|
|
292
|
-
warn(...msg) {
|
|
293
|
-
log.warn(...msg.map((m) => chalk.yellow(m)));
|
|
294
|
-
},
|
|
295
|
-
success(...msg) {
|
|
296
|
-
log.info(...msg.map((m) => chalk.green(m)));
|
|
297
|
-
},
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
const resolve = promisify(dns.resolve);
|
|
301
|
-
const ping = async (host) => {
|
|
302
|
-
const lookup = promisify(dns.lookup);
|
|
303
|
-
const ip = await lookup(host);
|
|
304
|
-
const start = new Date();
|
|
305
|
-
// Prevent timeouts from affecting user experience.
|
|
306
|
-
const requestPromise = new Promise((resolve, reject) => {
|
|
307
|
-
const req = http.get(`http://${ip.address}`, (res) => {
|
|
308
|
-
const delay = new Date().getTime() - start.getTime();
|
|
309
|
-
res.resume();
|
|
310
|
-
resolve(delay);
|
|
311
|
-
});
|
|
312
|
-
req.on('error', (err) => {
|
|
313
|
-
reject(err);
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
317
|
-
setTimeout(() => {
|
|
318
|
-
reject(new Error('Request timed out after 3 seconds'));
|
|
319
|
-
}, 1000);
|
|
320
|
-
});
|
|
321
|
-
return Promise.race([requestPromise, timeoutPromise]);
|
|
322
|
-
};
|
|
323
|
-
async function isChinaDomain(domain) {
|
|
324
|
-
try {
|
|
325
|
-
const [ip] = await resolve(domain);
|
|
326
|
-
return await isChinaIP(ip, domain);
|
|
327
|
-
}
|
|
328
|
-
catch (error) {
|
|
329
|
-
logger.debug(`${domain} can't be parse!`);
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
async function isChinaIP(ip, domain) {
|
|
334
|
-
try {
|
|
335
|
-
const delay = await ping(ip);
|
|
336
|
-
logger.debug(`${domain} latency is ${delay} ms`);
|
|
337
|
-
return delay > 1000;
|
|
338
|
-
}
|
|
339
|
-
catch (error) {
|
|
340
|
-
logger.debug(`ping ${domain} failed!`);
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
285
|
function normalizePathForComparison(targetPath) {
|
|
346
286
|
const normalized = path.normalize(targetPath);
|
|
347
287
|
return IS_WIN ? normalized.toLowerCase() : normalized;
|
|
@@ -389,15 +329,13 @@ function ensureRustEnv() {
|
|
|
389
329
|
ensureCargoBinOnPath();
|
|
390
330
|
}
|
|
391
331
|
async function installRust() {
|
|
392
|
-
const
|
|
393
|
-
const isInChina = await isChinaDomain('sh.rustup.rs');
|
|
394
|
-
const rustInstallScriptForMac = isInChina && !isActions
|
|
332
|
+
const rustInstallScriptForUnix = isCnMirrorEnabled()
|
|
395
333
|
? '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'
|
|
396
334
|
: "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
|
|
397
335
|
const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
|
|
398
336
|
const spinner = getSpinner('Downloading Rust...');
|
|
399
337
|
try {
|
|
400
|
-
await shellExec(IS_WIN ? rustInstallScriptForWindows :
|
|
338
|
+
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForUnix, 300000, undefined);
|
|
401
339
|
spinner.succeed(chalk.green('โ Rust installed successfully!'));
|
|
402
340
|
ensureRustEnv();
|
|
403
341
|
}
|
|
@@ -443,6 +381,24 @@ async function combineFiles(files, output) {
|
|
|
443
381
|
return files;
|
|
444
382
|
}
|
|
445
383
|
|
|
384
|
+
const logger = {
|
|
385
|
+
info(...msg) {
|
|
386
|
+
log.info(...msg.map((m) => chalk.white(m)));
|
|
387
|
+
},
|
|
388
|
+
debug(...msg) {
|
|
389
|
+
log.debug(...msg);
|
|
390
|
+
},
|
|
391
|
+
error(...msg) {
|
|
392
|
+
log.error(...msg.map((m) => chalk.red(m)));
|
|
393
|
+
},
|
|
394
|
+
warn(...msg) {
|
|
395
|
+
log.warn(...msg.map((m) => chalk.yellow(m)));
|
|
396
|
+
},
|
|
397
|
+
success(...msg) {
|
|
398
|
+
log.info(...msg.map((m) => chalk.green(m)));
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
|
|
446
402
|
function generateSafeFilename(name) {
|
|
447
403
|
return name
|
|
448
404
|
.replace(/[<>:"/\\|?*]/g, '_')
|
|
@@ -480,6 +436,41 @@ function generateIdentifierSafeName(name) {
|
|
|
480
436
|
return cleaned;
|
|
481
437
|
}
|
|
482
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Pure transform from CLI options to the window-config slice that gets
|
|
441
|
+
* merged into pake.json. Exposed for snapshot testing so option drift
|
|
442
|
+
* (e.g. a new flag added in cli-program.ts but forgotten here) is caught.
|
|
443
|
+
*
|
|
444
|
+
* Keep this function side-effect free.
|
|
445
|
+
*/
|
|
446
|
+
function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
|
|
447
|
+
const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
|
|
448
|
+
return {
|
|
449
|
+
width: options.width,
|
|
450
|
+
height: options.height,
|
|
451
|
+
fullscreen: options.fullscreen,
|
|
452
|
+
maximize: options.maximize,
|
|
453
|
+
resizable: options.resizable ?? true,
|
|
454
|
+
hide_title_bar: options.hideTitleBar,
|
|
455
|
+
activation_shortcut: options.activationShortcut,
|
|
456
|
+
always_on_top: options.alwaysOnTop,
|
|
457
|
+
dark_mode: options.darkMode,
|
|
458
|
+
disabled_web_shortcuts: options.disabledWebShortcuts,
|
|
459
|
+
hide_on_close: platformHideOnClose,
|
|
460
|
+
incognito: options.incognito,
|
|
461
|
+
title: options.title,
|
|
462
|
+
enable_wasm: options.wasm,
|
|
463
|
+
enable_drag_drop: options.enableDragDrop,
|
|
464
|
+
start_to_tray: options.startToTray && options.showSystemTray,
|
|
465
|
+
force_internal_navigation: options.forceInternalNavigation,
|
|
466
|
+
internal_url_regex: options.internalUrlRegex,
|
|
467
|
+
zoom: options.zoom,
|
|
468
|
+
min_width: options.minWidth,
|
|
469
|
+
min_height: options.minHeight,
|
|
470
|
+
ignore_certificate_errors: options.ignoreCertificateErrors,
|
|
471
|
+
new_window: options.newWindow,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
483
474
|
function asSupportedPlatform(platform) {
|
|
484
475
|
if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
|
|
485
476
|
throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
|
|
@@ -735,34 +726,9 @@ async function writeAllConfigs(tauriConf, platform) {
|
|
|
735
726
|
}
|
|
736
727
|
async function mergeConfig(url, options, tauriConf) {
|
|
737
728
|
await copyTemplateConfigs();
|
|
738
|
-
const {
|
|
729
|
+
const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
|
|
739
730
|
const platform = asSupportedPlatform(process.platform);
|
|
740
|
-
const
|
|
741
|
-
const tauriConfWindowOptions = {
|
|
742
|
-
width,
|
|
743
|
-
height,
|
|
744
|
-
fullscreen,
|
|
745
|
-
maximize,
|
|
746
|
-
resizable,
|
|
747
|
-
hide_title_bar: hideTitleBar,
|
|
748
|
-
activation_shortcut: activationShortcut,
|
|
749
|
-
always_on_top: alwaysOnTop,
|
|
750
|
-
dark_mode: darkMode,
|
|
751
|
-
disabled_web_shortcuts: disabledWebShortcuts,
|
|
752
|
-
hide_on_close: platformHideOnClose,
|
|
753
|
-
incognito,
|
|
754
|
-
title,
|
|
755
|
-
enable_wasm: wasm,
|
|
756
|
-
enable_drag_drop: enableDragDrop,
|
|
757
|
-
start_to_tray: startToTray && showSystemTray,
|
|
758
|
-
force_internal_navigation: forceInternalNavigation,
|
|
759
|
-
internal_url_regex: internalUrlRegex,
|
|
760
|
-
zoom,
|
|
761
|
-
min_width: minWidth,
|
|
762
|
-
min_height: minHeight,
|
|
763
|
-
ignore_certificate_errors: ignoreCertificateErrors,
|
|
764
|
-
new_window: newWindow,
|
|
765
|
-
};
|
|
731
|
+
const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
|
|
766
732
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
767
733
|
tauriConf.productName = name;
|
|
768
734
|
tauriConf.identifier = identifier;
|
|
@@ -808,70 +774,138 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
808
774
|
await writeAllConfigs(tauriConf, platform);
|
|
809
775
|
}
|
|
810
776
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const currentPath = process.env.PATH || '';
|
|
820
|
-
const systemToolsPath = '/usr/bin';
|
|
821
|
-
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
822
|
-
? currentPath
|
|
823
|
-
: `${systemToolsPath}:${currentPath}`;
|
|
824
|
-
return {
|
|
825
|
-
CFLAGS: '-fno-modules',
|
|
826
|
-
CXXFLAGS: '-fno-modules',
|
|
827
|
-
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
828
|
-
PATH: buildPath,
|
|
829
|
-
};
|
|
777
|
+
/**
|
|
778
|
+
* Returns build environment variables overrides for macOS, where Rust crates
|
|
779
|
+
* sometimes need explicit C/C++ flags and a deterministic SDK target. Other
|
|
780
|
+
* platforms inherit `process.env` unchanged.
|
|
781
|
+
*/
|
|
782
|
+
function getBuildEnvironment() {
|
|
783
|
+
if (!IS_MAC) {
|
|
784
|
+
return undefined;
|
|
830
785
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
786
|
+
const currentPath = process.env.PATH || '';
|
|
787
|
+
const systemToolsPath = '/usr/bin';
|
|
788
|
+
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
789
|
+
? currentPath
|
|
790
|
+
: `${systemToolsPath}:${currentPath}`;
|
|
791
|
+
return {
|
|
792
|
+
CFLAGS: '-fno-modules',
|
|
793
|
+
CXXFLAGS: '-fno-modules',
|
|
794
|
+
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
795
|
+
PATH: buildPath,
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Windows needs more time due to native compilation and antivirus scanning.
|
|
800
|
+
*/
|
|
801
|
+
function getInstallTimeout() {
|
|
802
|
+
return process.platform === 'win32' ? 900000 : 600000;
|
|
803
|
+
}
|
|
804
|
+
function getBuildTimeout() {
|
|
805
|
+
return 900000;
|
|
806
|
+
}
|
|
807
|
+
let packageManagerCache = null;
|
|
808
|
+
/**
|
|
809
|
+
* Returns 'pnpm' when available, otherwise 'npm'. Throws if neither is found.
|
|
810
|
+
* Cached after the first successful detection so tests can call repeatedly.
|
|
811
|
+
*/
|
|
812
|
+
async function detectPackageManager() {
|
|
813
|
+
if (packageManagerCache) {
|
|
814
|
+
return packageManagerCache;
|
|
834
815
|
}
|
|
835
|
-
|
|
836
|
-
|
|
816
|
+
const { execa } = await import('execa');
|
|
817
|
+
try {
|
|
818
|
+
await execa('pnpm', ['--version'], { stdio: 'ignore' });
|
|
819
|
+
logger.info('โบ Using pnpm for package management.');
|
|
820
|
+
packageManagerCache = 'pnpm';
|
|
821
|
+
return 'pnpm';
|
|
837
822
|
}
|
|
838
|
-
|
|
839
|
-
if (BaseBuilder.packageManagerCache) {
|
|
840
|
-
return BaseBuilder.packageManagerCache;
|
|
841
|
-
}
|
|
842
|
-
const { execa } = await import('execa');
|
|
823
|
+
catch {
|
|
843
824
|
try {
|
|
844
|
-
await execa('
|
|
845
|
-
logger.info('โบ
|
|
846
|
-
|
|
847
|
-
return '
|
|
825
|
+
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
826
|
+
logger.info('โบ pnpm not available, using npm for package management.');
|
|
827
|
+
packageManagerCache = 'npm';
|
|
828
|
+
return 'npm';
|
|
848
829
|
}
|
|
849
830
|
catch {
|
|
850
|
-
|
|
851
|
-
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
852
|
-
logger.info('โบ pnpm not available, using npm for package management.');
|
|
853
|
-
BaseBuilder.packageManagerCache = 'npm';
|
|
854
|
-
return 'npm';
|
|
855
|
-
}
|
|
856
|
-
catch {
|
|
857
|
-
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
858
|
-
}
|
|
831
|
+
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
859
832
|
}
|
|
860
833
|
}
|
|
861
|
-
|
|
862
|
-
|
|
834
|
+
}
|
|
835
|
+
function getInstallCommand(packageManager, useCnMirror) {
|
|
836
|
+
const registryOption = useCnMirror
|
|
837
|
+
? ' --registry=https://registry.npmmirror.com'
|
|
838
|
+
: '';
|
|
839
|
+
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
840
|
+
return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
|
|
841
|
+
}
|
|
842
|
+
async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
|
|
843
|
+
if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
if (error instanceof Error &&
|
|
851
|
+
error.message.includes('Source and destination must not be the same')) {
|
|
863
852
|
return;
|
|
864
853
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
|
|
858
|
+
return projectConfig.trim() === cnMirrorConfig.trim();
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Toggles `.cargo/config.toml` to point at rsproxy.cn when the user opts in
|
|
862
|
+
* via `PAKE_USE_CN_MIRROR=1`, and removes the auto-generated mirror config
|
|
863
|
+
* (or warns about a manual one) when they opt out.
|
|
864
|
+
*/
|
|
865
|
+
async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
|
|
866
|
+
const rustProjectDir = path.join(tauriSrcPath, '.cargo');
|
|
867
|
+
const projectConf = path.join(rustProjectDir, 'config.toml');
|
|
868
|
+
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
869
|
+
if (useCnMirror) {
|
|
870
|
+
await fsExtra.ensureDir(rustProjectDir);
|
|
871
|
+
await copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (!(await fsExtra.pathExists(projectConf))) {
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const [projectConfig, cnMirrorConfig] = await Promise.all([
|
|
878
|
+
fsExtra.readFile(projectConf, 'utf8'),
|
|
879
|
+
fsExtra.readFile(projectCnConf, 'utf8'),
|
|
880
|
+
]);
|
|
881
|
+
if (isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
|
|
882
|
+
await fsExtra.remove(projectConf);
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (projectConfig.includes('rsproxy.cn')) {
|
|
886
|
+
logger.warn(`โผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Returns true when an error string looks like the well-known Tauri+linuxdeploy
|
|
891
|
+
* strip failure that we automatically retry with NO_STRIP=1.
|
|
892
|
+
*/
|
|
893
|
+
function isLinuxDeployStripError(error) {
|
|
894
|
+
if (!(error instanceof Error) || !error.message) {
|
|
895
|
+
return false;
|
|
896
|
+
}
|
|
897
|
+
const message = error.message.toLowerCase();
|
|
898
|
+
return (message.includes('linuxdeploy') ||
|
|
899
|
+
message.includes('failed to run linuxdeploy') ||
|
|
900
|
+
message.includes('strip:') ||
|
|
901
|
+
message.includes('unable to recognise the format of the input file') ||
|
|
902
|
+
message.includes('appimage tool failed') ||
|
|
903
|
+
message.includes('strip tool'));
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
class BaseBuilder {
|
|
907
|
+
constructor(options) {
|
|
908
|
+
this.options = options;
|
|
875
909
|
}
|
|
876
910
|
async prepare() {
|
|
877
911
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
@@ -896,60 +930,34 @@ class BaseBuilder {
|
|
|
896
930
|
process.exit(1);
|
|
897
931
|
}
|
|
898
932
|
}
|
|
899
|
-
const isChina = await isChinaDomain('www.npmjs.com');
|
|
900
933
|
const spinner = getSpinner('Installing package...');
|
|
901
|
-
const
|
|
902
|
-
|
|
903
|
-
await
|
|
904
|
-
|
|
905
|
-
const
|
|
906
|
-
const registryOption = ' --registry=https://registry.npmmirror.com';
|
|
907
|
-
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
908
|
-
const timeout = this.getInstallTimeout();
|
|
909
|
-
const buildEnv = this.getBuildEnvironment();
|
|
934
|
+
const useCnMirror = isCnMirrorEnabled();
|
|
935
|
+
await configureCargoRegistry(tauriSrcPath, useCnMirror);
|
|
936
|
+
const packageManager = await detectPackageManager();
|
|
937
|
+
const timeout = getInstallTimeout();
|
|
938
|
+
const buildEnv = getBuildEnvironment();
|
|
910
939
|
// Show helpful message for first-time users
|
|
911
940
|
if (!tauriTargetPathExists) {
|
|
912
941
|
logger.info(process.platform === 'win32'
|
|
913
942
|
? 'โบ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
|
|
914
943
|
: 'โบ First-time setup may take 5-10 minutes (installing dependencies)...');
|
|
915
944
|
}
|
|
916
|
-
|
|
945
|
+
if (useCnMirror) {
|
|
946
|
+
logger.info(`โบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
|
|
947
|
+
}
|
|
917
948
|
try {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
923
|
-
}
|
|
924
|
-
else {
|
|
925
|
-
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
926
|
-
}
|
|
949
|
+
await shellExec(getInstallCommand(packageManager, useCnMirror), timeout, {
|
|
950
|
+
...buildEnv,
|
|
951
|
+
CI: 'true',
|
|
952
|
+
});
|
|
927
953
|
spinner.succeed(chalk.green('Package installed!'));
|
|
928
954
|
}
|
|
929
955
|
catch (error) {
|
|
930
|
-
|
|
931
|
-
if (
|
|
932
|
-
|
|
933
|
-
!usedMirror) {
|
|
934
|
-
spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
|
|
935
|
-
logger.info('โบ Retrying installation with CN mirror for better speed...');
|
|
936
|
-
const retrySpinner = getSpinner('Retrying installation...');
|
|
937
|
-
usedMirror = true;
|
|
938
|
-
try {
|
|
939
|
-
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
940
|
-
await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
941
|
-
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
942
|
-
retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
|
|
943
|
-
}
|
|
944
|
-
catch (retryError) {
|
|
945
|
-
retrySpinner.fail(chalk.red('Installation failed'));
|
|
946
|
-
throw retryError;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
else {
|
|
950
|
-
spinner.fail(chalk.red('Installation failed'));
|
|
951
|
-
throw error;
|
|
956
|
+
spinner.fail(chalk.red('Installation failed'));
|
|
957
|
+
if (!useCnMirror) {
|
|
958
|
+
logger.info(`โบ If downloads are slow in China, retry with ${CN_MIRROR_ENV}=1 to use CN mirrors.`);
|
|
952
959
|
}
|
|
960
|
+
throw error;
|
|
953
961
|
}
|
|
954
962
|
if (!tauriTargetPathExists) {
|
|
955
963
|
logger.warn('โผ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
|
|
@@ -961,7 +969,7 @@ class BaseBuilder {
|
|
|
961
969
|
async start(url) {
|
|
962
970
|
logger.info('Pake dev server starting...');
|
|
963
971
|
await mergeConfig(url, this.options, tauriConfig);
|
|
964
|
-
const packageManager = await
|
|
972
|
+
const packageManager = await detectPackageManager();
|
|
965
973
|
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
|
966
974
|
const features = this.getBuildFeatures();
|
|
967
975
|
const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
|
|
@@ -972,8 +980,7 @@ class BaseBuilder {
|
|
|
972
980
|
async buildAndCopy(url, target) {
|
|
973
981
|
const { name = 'pake-app' } = this.options;
|
|
974
982
|
await mergeConfig(url, this.options, tauriConfig);
|
|
975
|
-
|
|
976
|
-
const packageManager = await this.detectPackageManager();
|
|
983
|
+
const packageManager = await detectPackageManager();
|
|
977
984
|
// Build app
|
|
978
985
|
const buildSpinner = getSpinner('Building app...');
|
|
979
986
|
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
@@ -981,7 +988,7 @@ class BaseBuilder {
|
|
|
981
988
|
buildSpinner.stop();
|
|
982
989
|
// Show static message to keep the status visible
|
|
983
990
|
logger.warn('โธ Building app...');
|
|
984
|
-
const baseEnv =
|
|
991
|
+
const baseEnv = getBuildEnvironment();
|
|
985
992
|
let buildEnv = {
|
|
986
993
|
...(baseEnv ?? {}),
|
|
987
994
|
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
@@ -997,7 +1004,7 @@ class BaseBuilder {
|
|
|
997
1004
|
}
|
|
998
1005
|
}
|
|
999
1006
|
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
|
|
1000
|
-
const buildTimeout =
|
|
1007
|
+
const buildTimeout = getBuildTimeout();
|
|
1001
1008
|
try {
|
|
1002
1009
|
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1003
1010
|
}
|
|
@@ -1005,7 +1012,7 @@ class BaseBuilder {
|
|
|
1005
1012
|
const shouldRetryWithoutStrip = process.platform === 'linux' &&
|
|
1006
1013
|
target === 'appimage' &&
|
|
1007
1014
|
!buildEnv.NO_STRIP &&
|
|
1008
|
-
|
|
1015
|
+
isLinuxDeployStripError(error);
|
|
1009
1016
|
if (shouldRetryWithoutStrip) {
|
|
1010
1017
|
logger.warn('โ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
|
|
1011
1018
|
buildEnv = {
|
|
@@ -1061,18 +1068,6 @@ class BaseBuilder {
|
|
|
1061
1068
|
getFileType(target) {
|
|
1062
1069
|
return target;
|
|
1063
1070
|
}
|
|
1064
|
-
isLinuxDeployStripError(error) {
|
|
1065
|
-
if (!(error instanceof Error) || !error.message) {
|
|
1066
|
-
return false;
|
|
1067
|
-
}
|
|
1068
|
-
const message = error.message.toLowerCase();
|
|
1069
|
-
return (message.includes('linuxdeploy') ||
|
|
1070
|
-
message.includes('failed to run linuxdeploy') ||
|
|
1071
|
-
message.includes('strip:') ||
|
|
1072
|
-
message.includes('unable to recognise the format of the input file') ||
|
|
1073
|
-
message.includes('appimage tool failed') ||
|
|
1074
|
-
message.includes('strip tool'));
|
|
1075
|
-
}
|
|
1076
1071
|
resolveTargetArch(requestedArch) {
|
|
1077
1072
|
if (requestedArch === 'auto' || !requestedArch) {
|
|
1078
1073
|
return process.arch;
|
|
@@ -1210,7 +1205,6 @@ class BaseBuilder {
|
|
|
1210
1205
|
return 'src-tauri/target'; // Override in subclasses if needed
|
|
1211
1206
|
}
|
|
1212
1207
|
}
|
|
1213
|
-
BaseBuilder.packageManagerCache = null;
|
|
1214
1208
|
BaseBuilder.ARCH_MAPPINGS = {
|
|
1215
1209
|
darwin: {
|
|
1216
1210
|
arm64: 'aarch64-apple-darwin',
|
|
@@ -1546,6 +1540,9 @@ function getIconSourcePriority(url, appName) {
|
|
|
1546
1540
|
const ICO_HEADER_SIZE = 6;
|
|
1547
1541
|
const ICO_DIR_ENTRY_SIZE = 16;
|
|
1548
1542
|
const ICO_TYPE_ICON = 1;
|
|
1543
|
+
// Standard Windows icon sizes covering tray (16/24/32), taskbar (32/48),
|
|
1544
|
+
// shell (48/256) and high-DPI (128/256). Issue #1190.
|
|
1545
|
+
const WIN_STANDARD_ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
|
|
1549
1546
|
function decodeDimension(value) {
|
|
1550
1547
|
return value === 0 ? 256 : value;
|
|
1551
1548
|
}
|
|
@@ -1644,6 +1641,91 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
|
|
|
1644
1641
|
return false;
|
|
1645
1642
|
}
|
|
1646
1643
|
}
|
|
1644
|
+
/**
|
|
1645
|
+
* PNG signature `\x89PNG`. ICO frames may carry either a BMP DIB or an
|
|
1646
|
+
* embedded PNG payload (PNG-in-ICO, supported since Windows Vista).
|
|
1647
|
+
*/
|
|
1648
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
1649
|
+
function frameLooksLikePng(entry) {
|
|
1650
|
+
return (entry.data.length >= PNG_SIGNATURE.length &&
|
|
1651
|
+
entry.data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE));
|
|
1652
|
+
}
|
|
1653
|
+
async function decodeFrameToPng(entry) {
|
|
1654
|
+
if (frameLooksLikePng(entry)) {
|
|
1655
|
+
return Buffer.from(entry.data);
|
|
1656
|
+
}
|
|
1657
|
+
// BMP DIB frames need to go through sharp's ico-to-PNG path, which only
|
|
1658
|
+
// works on the full ICO container. Fall back to letting the caller use a
|
|
1659
|
+
// sharp pipeline against the original ICO for the missing source.
|
|
1660
|
+
return null;
|
|
1661
|
+
}
|
|
1662
|
+
async function pickLargestFrameAsPng(buffer, entries) {
|
|
1663
|
+
const largest = [...entries].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height))[0];
|
|
1664
|
+
if (largest) {
|
|
1665
|
+
const decoded = await decodeFrameToPng(largest);
|
|
1666
|
+
if (decoded) {
|
|
1667
|
+
return decoded;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
// Fallback: let sharp render directly from the ICO buffer. sharp picks the
|
|
1671
|
+
// largest embedded frame on its own.
|
|
1672
|
+
try {
|
|
1673
|
+
return await sharp(buffer).png().toBuffer();
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
return null;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Ensures the produced ICO carries every Windows standard size so the OS
|
|
1681
|
+
* never has to downsample a 256x256 frame to 16x16 for the tray.
|
|
1682
|
+
* Falls back to `writeIcoWithPreferredSize` if rendering fails.
|
|
1683
|
+
*
|
|
1684
|
+
* Issue #1190.
|
|
1685
|
+
*/
|
|
1686
|
+
async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize = 256, desiredSizes = WIN_STANDARD_ICO_SIZES) {
|
|
1687
|
+
try {
|
|
1688
|
+
const sourceBuffer = await fsExtra.readFile(sourcePath);
|
|
1689
|
+
const entries = parseIcoBuffer(sourceBuffer);
|
|
1690
|
+
const sourcePng = await pickLargestFrameAsPng(sourceBuffer, entries);
|
|
1691
|
+
if (!sourcePng) {
|
|
1692
|
+
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
1693
|
+
}
|
|
1694
|
+
const frames = await Promise.all(desiredSizes.map(async (size) => {
|
|
1695
|
+
// Reuse an existing exact-size PNG frame when possible to keep any
|
|
1696
|
+
// hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
|
|
1697
|
+
const exact = entries.find((entry) => entry.width === size && entry.height === size);
|
|
1698
|
+
if (exact && frameLooksLikePng(exact)) {
|
|
1699
|
+
return { size, png: Buffer.from(exact.data) };
|
|
1700
|
+
}
|
|
1701
|
+
const png = await sharp(sourcePng)
|
|
1702
|
+
.resize(size, size, {
|
|
1703
|
+
fit: 'contain',
|
|
1704
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
1705
|
+
})
|
|
1706
|
+
.ensureAlpha()
|
|
1707
|
+
.png()
|
|
1708
|
+
.toBuffer();
|
|
1709
|
+
return { size, png };
|
|
1710
|
+
}));
|
|
1711
|
+
// Order frames so the preferred size lands first (Windows shell uses the
|
|
1712
|
+
// first-listed frame as a quality hint when choosing which to display).
|
|
1713
|
+
frames.sort((a, b) => {
|
|
1714
|
+
const aExact = a.size === preferredSize ? 0 : 1;
|
|
1715
|
+
const bExact = b.size === preferredSize ? 0 : 1;
|
|
1716
|
+
if (aExact !== bExact)
|
|
1717
|
+
return aExact - bExact;
|
|
1718
|
+
return b.size - a.size;
|
|
1719
|
+
});
|
|
1720
|
+
const icoBuffer = buildIcoFromPngBuffers(frames);
|
|
1721
|
+
await fsExtra.ensureDir(path.dirname(outputPath));
|
|
1722
|
+
await fsExtra.outputFile(outputPath, icoBuffer);
|
|
1723
|
+
return true;
|
|
1724
|
+
}
|
|
1725
|
+
catch {
|
|
1726
|
+
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1647
1729
|
/**
|
|
1648
1730
|
* Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
|
|
1649
1731
|
* (supported since Windows Vista). This preserves alpha transparency.
|
|
@@ -1693,7 +1775,7 @@ const ICON_CONFIG = {
|
|
|
1693
1775
|
},
|
|
1694
1776
|
};
|
|
1695
1777
|
const PLATFORM_CONFIG = {
|
|
1696
|
-
win: { format: '.ico', sizes: [
|
|
1778
|
+
win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
|
|
1697
1779
|
linux: { format: '.png', size: 512 },
|
|
1698
1780
|
macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
|
|
1699
1781
|
};
|
|
@@ -1728,10 +1810,15 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
|
|
|
1728
1810
|
try {
|
|
1729
1811
|
const finalIconPath = generateIconPath(appName);
|
|
1730
1812
|
await fsExtra.ensureDir(path.dirname(finalIconPath));
|
|
1731
|
-
//
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1813
|
+
// Re-render ICO so every Windows standard size is present and prefer the
|
|
1814
|
+
// 256px frame as the leading entry; falls back to plain reordering if the
|
|
1815
|
+
// ICO is non-decodable, then to a raw copy. (Issue #1190)
|
|
1816
|
+
const upgraded = await ensureMultiResolutionIco(convertedPath, finalIconPath, 256);
|
|
1817
|
+
if (!upgraded) {
|
|
1818
|
+
const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
|
|
1819
|
+
if (!reordered) {
|
|
1820
|
+
await fsExtra.copy(convertedPath, finalIconPath);
|
|
1821
|
+
}
|
|
1735
1822
|
}
|
|
1736
1823
|
return finalIconPath;
|
|
1737
1824
|
}
|
|
@@ -2188,6 +2275,28 @@ function normalizeUrl(urlToNormalize) {
|
|
|
2188
2275
|
}
|
|
2189
2276
|
}
|
|
2190
2277
|
|
|
2278
|
+
/**
|
|
2279
|
+
* Error class used for user-facing CLI errors.
|
|
2280
|
+
*
|
|
2281
|
+
* The top-level catch in `bin/cli.ts` prints `message` directly without a
|
|
2282
|
+
* stack trace and exits with code 1. Use this for predictable failures
|
|
2283
|
+
* (invalid names, missing files, etc.) so users see a clean message instead
|
|
2284
|
+
* of a Node.js stack dump.
|
|
2285
|
+
*/
|
|
2286
|
+
class PakeError extends Error {
|
|
2287
|
+
constructor(message) {
|
|
2288
|
+
super(message);
|
|
2289
|
+
this.isUserError = true;
|
|
2290
|
+
this.name = 'PakeError';
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
function isPakeError(error) {
|
|
2294
|
+
return (error instanceof PakeError ||
|
|
2295
|
+
(typeof error === 'object' &&
|
|
2296
|
+
error !== null &&
|
|
2297
|
+
error.isUserError === true));
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2191
2300
|
function resolveAppName(name, platform) {
|
|
2192
2301
|
const domain = getDomain(name) || 'pake';
|
|
2193
2302
|
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
|
|
@@ -2230,13 +2339,13 @@ async function handleOptions(options, url) {
|
|
|
2230
2339
|
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.`;
|
|
2231
2340
|
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.`;
|
|
2232
2341
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
|
2233
|
-
logger.error(errorMsg);
|
|
2234
2342
|
if (isActions) {
|
|
2343
|
+
logger.error(errorMsg);
|
|
2235
2344
|
name = resolveAppName(url, platform);
|
|
2236
2345
|
logger.warn(`โผ Inside github actions, use the default name: ${name}`);
|
|
2237
2346
|
}
|
|
2238
2347
|
else {
|
|
2239
|
-
|
|
2348
|
+
throw new PakeError(errorMsg);
|
|
2240
2349
|
}
|
|
2241
2350
|
}
|
|
2242
2351
|
const resolvedName = name || 'pake-app';
|
|
@@ -2494,21 +2603,46 @@ async function checkUpdateTips() {
|
|
|
2494
2603
|
});
|
|
2495
2604
|
}
|
|
2496
2605
|
program.action(async (url, options) => {
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2606
|
+
try {
|
|
2607
|
+
await checkUpdateTips();
|
|
2608
|
+
if (!url) {
|
|
2609
|
+
program.help({
|
|
2610
|
+
error: false,
|
|
2611
|
+
});
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
log.setDefaultLevel('info');
|
|
2615
|
+
log.setLevel('info');
|
|
2616
|
+
if (options.debug) {
|
|
2617
|
+
log.setLevel('debug');
|
|
2618
|
+
}
|
|
2619
|
+
const appOptions = await handleOptions(options, url);
|
|
2620
|
+
const builder = BuilderProvider.create(appOptions);
|
|
2621
|
+
await builder.prepare();
|
|
2622
|
+
await builder.build(url);
|
|
2623
|
+
}
|
|
2624
|
+
catch (error) {
|
|
2625
|
+
if (isPakeError(error)) {
|
|
2626
|
+
console.error(chalk.red(error.message));
|
|
2627
|
+
}
|
|
2628
|
+
else if (error instanceof Error) {
|
|
2629
|
+
console.error(chalk.red(`โ ${error.message}`));
|
|
2630
|
+
if (options?.debug && error.stack) {
|
|
2631
|
+
console.error(chalk.gray(error.stack));
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
else {
|
|
2635
|
+
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
2636
|
+
}
|
|
2637
|
+
process.exit(1);
|
|
2503
2638
|
}
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
if (
|
|
2507
|
-
|
|
2639
|
+
});
|
|
2640
|
+
program.parseAsync().catch((error) => {
|
|
2641
|
+
if (error instanceof Error) {
|
|
2642
|
+
console.error(chalk.red(`โ ${error.message}`));
|
|
2643
|
+
}
|
|
2644
|
+
else {
|
|
2645
|
+
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
2508
2646
|
}
|
|
2509
|
-
|
|
2510
|
-
const builder = BuilderProvider.create(appOptions);
|
|
2511
|
-
await builder.prepare();
|
|
2512
|
-
await builder.build(url);
|
|
2647
|
+
process.exit(1);
|
|
2513
2648
|
});
|
|
2514
|
-
program.parse();
|