pake-cli 3.11.4 โ 3.11.6
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 +335 -161
- package/dist/pake-badge-test.html +1 -0
- package/dist/test-local.html +1 -0
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +7 -4
- package/src-tauri/Cargo.toml +6 -1
- package/src-tauri/build.rs +2 -0
- package/src-tauri/gen/schemas/acl-manifests.json +1 -0
- package/src-tauri/gen/schemas/capabilities.json +1 -0
- package/src-tauri/gen/schemas/desktop-schema.json +3331 -0
- package/src-tauri/gen/schemas/macOS-schema.json +3331 -0
- package/src-tauri/pake.json +1 -0
- package/src-tauri/src/app/config.rs +2 -0
- package/src-tauri/src/app/invoke.rs +85 -0
- package/src-tauri/src/app/menu.rs +56 -5
- package/src-tauri/src/app/setup.rs +39 -34
- package/src-tauri/src/app/window.rs +64 -17
- package/src-tauri/src/inject/event.js +98 -12
- package/src-tauri/src/inject/find.js +708 -0
- 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 +13 -5
- package/src-tauri/src/util.rs +131 -24
- package/src-tauri/tauri.conf.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
|
20
20
|
import fs$1 from 'fs';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.11.
|
|
23
|
+
var version = "3.11.6";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -436,6 +436,42 @@ function generateIdentifierSafeName(name) {
|
|
|
436
436
|
return cleaned;
|
|
437
437
|
}
|
|
438
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
|
+
enable_find: options.enableFind,
|
|
468
|
+
zoom: options.zoom,
|
|
469
|
+
min_width: options.minWidth,
|
|
470
|
+
min_height: options.minHeight,
|
|
471
|
+
ignore_certificate_errors: options.ignoreCertificateErrors,
|
|
472
|
+
new_window: options.newWindow,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
439
475
|
function asSupportedPlatform(platform) {
|
|
440
476
|
if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
|
|
441
477
|
throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
|
|
@@ -691,34 +727,9 @@ async function writeAllConfigs(tauriConf, platform) {
|
|
|
691
727
|
}
|
|
692
728
|
async function mergeConfig(url, options, tauriConf) {
|
|
693
729
|
await copyTemplateConfigs();
|
|
694
|
-
const {
|
|
730
|
+
const { appVersion, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', installerLanguage, wasm, camera, microphone, } = options;
|
|
695
731
|
const platform = asSupportedPlatform(process.platform);
|
|
696
|
-
const
|
|
697
|
-
const tauriConfWindowOptions = {
|
|
698
|
-
width,
|
|
699
|
-
height,
|
|
700
|
-
fullscreen,
|
|
701
|
-
maximize,
|
|
702
|
-
resizable,
|
|
703
|
-
hide_title_bar: hideTitleBar,
|
|
704
|
-
activation_shortcut: activationShortcut,
|
|
705
|
-
always_on_top: alwaysOnTop,
|
|
706
|
-
dark_mode: darkMode,
|
|
707
|
-
disabled_web_shortcuts: disabledWebShortcuts,
|
|
708
|
-
hide_on_close: platformHideOnClose,
|
|
709
|
-
incognito,
|
|
710
|
-
title,
|
|
711
|
-
enable_wasm: wasm,
|
|
712
|
-
enable_drag_drop: enableDragDrop,
|
|
713
|
-
start_to_tray: startToTray && showSystemTray,
|
|
714
|
-
force_internal_navigation: forceInternalNavigation,
|
|
715
|
-
internal_url_regex: internalUrlRegex,
|
|
716
|
-
zoom,
|
|
717
|
-
min_width: minWidth,
|
|
718
|
-
min_height: minHeight,
|
|
719
|
-
ignore_certificate_errors: ignoreCertificateErrors,
|
|
720
|
-
new_window: newWindow,
|
|
721
|
-
};
|
|
732
|
+
const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
|
|
722
733
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
723
734
|
tauriConf.productName = name;
|
|
724
735
|
tauriConf.identifier = identifier;
|
|
@@ -764,104 +775,138 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
764
775
|
await writeAllConfigs(tauriConf, platform);
|
|
765
776
|
}
|
|
766
777
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
const currentPath = process.env.PATH || '';
|
|
776
|
-
const systemToolsPath = '/usr/bin';
|
|
777
|
-
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
778
|
-
? currentPath
|
|
779
|
-
: `${systemToolsPath}:${currentPath}`;
|
|
780
|
-
return {
|
|
781
|
-
CFLAGS: '-fno-modules',
|
|
782
|
-
CXXFLAGS: '-fno-modules',
|
|
783
|
-
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
784
|
-
PATH: buildPath,
|
|
785
|
-
};
|
|
778
|
+
/**
|
|
779
|
+
* Returns build environment variables overrides for macOS, where Rust crates
|
|
780
|
+
* sometimes need explicit C/C++ flags and a deterministic SDK target. Other
|
|
781
|
+
* platforms inherit `process.env` unchanged.
|
|
782
|
+
*/
|
|
783
|
+
function getBuildEnvironment() {
|
|
784
|
+
if (!IS_MAC) {
|
|
785
|
+
return undefined;
|
|
786
786
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
787
|
+
const currentPath = process.env.PATH || '';
|
|
788
|
+
const systemToolsPath = '/usr/bin';
|
|
789
|
+
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
790
|
+
? currentPath
|
|
791
|
+
: `${systemToolsPath}:${currentPath}`;
|
|
792
|
+
return {
|
|
793
|
+
CFLAGS: '-fno-modules',
|
|
794
|
+
CXXFLAGS: '-fno-modules',
|
|
795
|
+
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
796
|
+
PATH: buildPath,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Windows needs more time due to native compilation and antivirus scanning.
|
|
801
|
+
*/
|
|
802
|
+
function getInstallTimeout() {
|
|
803
|
+
return process.platform === 'win32' ? 900000 : 600000;
|
|
804
|
+
}
|
|
805
|
+
function getBuildTimeout() {
|
|
806
|
+
return 900000;
|
|
807
|
+
}
|
|
808
|
+
let packageManagerCache = null;
|
|
809
|
+
/**
|
|
810
|
+
* Returns 'pnpm' when available, otherwise 'npm'. Throws if neither is found.
|
|
811
|
+
* Cached after the first successful detection so tests can call repeatedly.
|
|
812
|
+
*/
|
|
813
|
+
async function detectPackageManager() {
|
|
814
|
+
if (packageManagerCache) {
|
|
815
|
+
return packageManagerCache;
|
|
790
816
|
}
|
|
791
|
-
|
|
792
|
-
|
|
817
|
+
const { execa } = await import('execa');
|
|
818
|
+
try {
|
|
819
|
+
await execa('pnpm', ['--version'], { stdio: 'ignore' });
|
|
820
|
+
logger.info('โบ Using pnpm for package management.');
|
|
821
|
+
packageManagerCache = 'pnpm';
|
|
822
|
+
return 'pnpm';
|
|
793
823
|
}
|
|
794
|
-
|
|
795
|
-
if (BaseBuilder.packageManagerCache) {
|
|
796
|
-
return BaseBuilder.packageManagerCache;
|
|
797
|
-
}
|
|
798
|
-
const { execa } = await import('execa');
|
|
824
|
+
catch {
|
|
799
825
|
try {
|
|
800
|
-
await execa('
|
|
801
|
-
logger.info('โบ
|
|
802
|
-
|
|
803
|
-
return '
|
|
826
|
+
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
827
|
+
logger.info('โบ pnpm not available, using npm for package management.');
|
|
828
|
+
packageManagerCache = 'npm';
|
|
829
|
+
return 'npm';
|
|
804
830
|
}
|
|
805
831
|
catch {
|
|
806
|
-
|
|
807
|
-
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
808
|
-
logger.info('โบ pnpm not available, using npm for package management.');
|
|
809
|
-
BaseBuilder.packageManagerCache = 'npm';
|
|
810
|
-
return 'npm';
|
|
811
|
-
}
|
|
812
|
-
catch {
|
|
813
|
-
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
814
|
-
}
|
|
832
|
+
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
815
833
|
}
|
|
816
834
|
}
|
|
817
|
-
|
|
818
|
-
|
|
835
|
+
}
|
|
836
|
+
function getInstallCommand(packageManager, useCnMirror) {
|
|
837
|
+
const registryOption = useCnMirror
|
|
838
|
+
? ' --registry=https://registry.npmmirror.com'
|
|
839
|
+
: '';
|
|
840
|
+
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
841
|
+
return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
|
|
842
|
+
}
|
|
843
|
+
async function copyFileWithSamePathGuard(sourcePath, destinationPath) {
|
|
844
|
+
if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
if (error instanceof Error &&
|
|
852
|
+
error.message.includes('Source and destination must not be the same')) {
|
|
819
853
|
return;
|
|
820
854
|
}
|
|
821
|
-
|
|
822
|
-
await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
|
|
823
|
-
}
|
|
824
|
-
catch (error) {
|
|
825
|
-
if (error instanceof Error &&
|
|
826
|
-
error.message.includes('Source and destination must not be the same')) {
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
throw error;
|
|
830
|
-
}
|
|
855
|
+
throw error;
|
|
831
856
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
857
|
+
}
|
|
858
|
+
function isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
|
|
859
|
+
return projectConfig.trim() === cnMirrorConfig.trim();
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Toggles `.cargo/config.toml` to point at rsproxy.cn when the user opts in
|
|
863
|
+
* via `PAKE_USE_CN_MIRROR=1`, and removes the auto-generated mirror config
|
|
864
|
+
* (or warns about a manual one) when they opt out.
|
|
865
|
+
*/
|
|
866
|
+
async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
|
|
867
|
+
const rustProjectDir = path.join(tauriSrcPath, '.cargo');
|
|
868
|
+
const projectConf = path.join(rustProjectDir, 'config.toml');
|
|
869
|
+
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
870
|
+
if (useCnMirror) {
|
|
871
|
+
await fsExtra.ensureDir(rustProjectDir);
|
|
872
|
+
await copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
873
|
+
return;
|
|
838
874
|
}
|
|
839
|
-
|
|
840
|
-
return
|
|
875
|
+
if (!(await fsExtra.pathExists(projectConf))) {
|
|
876
|
+
return;
|
|
841
877
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
878
|
+
const [projectConfig, cnMirrorConfig] = await Promise.all([
|
|
879
|
+
fsExtra.readFile(projectConf, 'utf8'),
|
|
880
|
+
fsExtra.readFile(projectCnConf, 'utf8'),
|
|
881
|
+
]);
|
|
882
|
+
if (isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
|
|
883
|
+
await fsExtra.remove(projectConf);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (projectConfig.includes('rsproxy.cn')) {
|
|
887
|
+
logger.warn(`โผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Returns true when an error string looks like the well-known Tauri+linuxdeploy
|
|
892
|
+
* strip failure that we automatically retry with NO_STRIP=1.
|
|
893
|
+
*/
|
|
894
|
+
function isLinuxDeployStripError(error) {
|
|
895
|
+
if (!(error instanceof Error) || !error.message) {
|
|
896
|
+
return false;
|
|
897
|
+
}
|
|
898
|
+
const message = error.message.toLowerCase();
|
|
899
|
+
return (message.includes('linuxdeploy') ||
|
|
900
|
+
message.includes('failed to run linuxdeploy') ||
|
|
901
|
+
message.includes('strip:') ||
|
|
902
|
+
message.includes('unable to recognise the format of the input file') ||
|
|
903
|
+
message.includes('appimage tool failed') ||
|
|
904
|
+
message.includes('strip tool'));
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
class BaseBuilder {
|
|
908
|
+
constructor(options) {
|
|
909
|
+
this.options = options;
|
|
865
910
|
}
|
|
866
911
|
async prepare() {
|
|
867
912
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
@@ -888,11 +933,10 @@ class BaseBuilder {
|
|
|
888
933
|
}
|
|
889
934
|
const spinner = getSpinner('Installing package...');
|
|
890
935
|
const useCnMirror = isCnMirrorEnabled();
|
|
891
|
-
await
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
const
|
|
895
|
-
const buildEnv = this.getBuildEnvironment();
|
|
936
|
+
await configureCargoRegistry(tauriSrcPath, useCnMirror);
|
|
937
|
+
const packageManager = await detectPackageManager();
|
|
938
|
+
const timeout = getInstallTimeout();
|
|
939
|
+
const buildEnv = getBuildEnvironment();
|
|
896
940
|
// Show helpful message for first-time users
|
|
897
941
|
if (!tauriTargetPathExists) {
|
|
898
942
|
logger.info(process.platform === 'win32'
|
|
@@ -903,7 +947,7 @@ class BaseBuilder {
|
|
|
903
947
|
logger.info(`โบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
|
|
904
948
|
}
|
|
905
949
|
try {
|
|
906
|
-
await shellExec(
|
|
950
|
+
await shellExec(getInstallCommand(packageManager, useCnMirror), timeout, {
|
|
907
951
|
...buildEnv,
|
|
908
952
|
CI: 'true',
|
|
909
953
|
});
|
|
@@ -926,7 +970,7 @@ class BaseBuilder {
|
|
|
926
970
|
async start(url) {
|
|
927
971
|
logger.info('Pake dev server starting...');
|
|
928
972
|
await mergeConfig(url, this.options, tauriConfig);
|
|
929
|
-
const packageManager = await
|
|
973
|
+
const packageManager = await detectPackageManager();
|
|
930
974
|
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
|
931
975
|
const features = this.getBuildFeatures();
|
|
932
976
|
const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
|
|
@@ -937,8 +981,7 @@ class BaseBuilder {
|
|
|
937
981
|
async buildAndCopy(url, target) {
|
|
938
982
|
const { name = 'pake-app' } = this.options;
|
|
939
983
|
await mergeConfig(url, this.options, tauriConfig);
|
|
940
|
-
|
|
941
|
-
const packageManager = await this.detectPackageManager();
|
|
984
|
+
const packageManager = await detectPackageManager();
|
|
942
985
|
// Build app
|
|
943
986
|
const buildSpinner = getSpinner('Building app...');
|
|
944
987
|
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
@@ -946,7 +989,7 @@ class BaseBuilder {
|
|
|
946
989
|
buildSpinner.stop();
|
|
947
990
|
// Show static message to keep the status visible
|
|
948
991
|
logger.warn('โธ Building app...');
|
|
949
|
-
const baseEnv =
|
|
992
|
+
const baseEnv = getBuildEnvironment();
|
|
950
993
|
let buildEnv = {
|
|
951
994
|
...(baseEnv ?? {}),
|
|
952
995
|
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
@@ -962,7 +1005,7 @@ class BaseBuilder {
|
|
|
962
1005
|
}
|
|
963
1006
|
}
|
|
964
1007
|
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
|
|
965
|
-
const buildTimeout =
|
|
1008
|
+
const buildTimeout = getBuildTimeout();
|
|
966
1009
|
try {
|
|
967
1010
|
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
968
1011
|
}
|
|
@@ -970,7 +1013,7 @@ class BaseBuilder {
|
|
|
970
1013
|
const shouldRetryWithoutStrip = process.platform === 'linux' &&
|
|
971
1014
|
target === 'appimage' &&
|
|
972
1015
|
!buildEnv.NO_STRIP &&
|
|
973
|
-
|
|
1016
|
+
isLinuxDeployStripError(error);
|
|
974
1017
|
if (shouldRetryWithoutStrip) {
|
|
975
1018
|
logger.warn('โ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
|
|
976
1019
|
buildEnv = {
|
|
@@ -1026,18 +1069,6 @@ class BaseBuilder {
|
|
|
1026
1069
|
getFileType(target) {
|
|
1027
1070
|
return target;
|
|
1028
1071
|
}
|
|
1029
|
-
isLinuxDeployStripError(error) {
|
|
1030
|
-
if (!(error instanceof Error) || !error.message) {
|
|
1031
|
-
return false;
|
|
1032
|
-
}
|
|
1033
|
-
const message = error.message.toLowerCase();
|
|
1034
|
-
return (message.includes('linuxdeploy') ||
|
|
1035
|
-
message.includes('failed to run linuxdeploy') ||
|
|
1036
|
-
message.includes('strip:') ||
|
|
1037
|
-
message.includes('unable to recognise the format of the input file') ||
|
|
1038
|
-
message.includes('appimage tool failed') ||
|
|
1039
|
-
message.includes('strip tool'));
|
|
1040
|
-
}
|
|
1041
1072
|
resolveTargetArch(requestedArch) {
|
|
1042
1073
|
if (requestedArch === 'auto' || !requestedArch) {
|
|
1043
1074
|
return process.arch;
|
|
@@ -1175,7 +1206,6 @@ class BaseBuilder {
|
|
|
1175
1206
|
return 'src-tauri/target'; // Override in subclasses if needed
|
|
1176
1207
|
}
|
|
1177
1208
|
}
|
|
1178
|
-
BaseBuilder.packageManagerCache = null;
|
|
1179
1209
|
BaseBuilder.ARCH_MAPPINGS = {
|
|
1180
1210
|
darwin: {
|
|
1181
1211
|
arm64: 'aarch64-apple-darwin',
|
|
@@ -1511,6 +1541,9 @@ function getIconSourcePriority(url, appName) {
|
|
|
1511
1541
|
const ICO_HEADER_SIZE = 6;
|
|
1512
1542
|
const ICO_DIR_ENTRY_SIZE = 16;
|
|
1513
1543
|
const ICO_TYPE_ICON = 1;
|
|
1544
|
+
// Standard Windows icon sizes covering tray (16/24/32), taskbar (32/48),
|
|
1545
|
+
// shell (48/256) and high-DPI (128/256). Issue #1190.
|
|
1546
|
+
const WIN_STANDARD_ICO_SIZES = [16, 24, 32, 48, 64, 128, 256];
|
|
1514
1547
|
function decodeDimension(value) {
|
|
1515
1548
|
return value === 0 ? 256 : value;
|
|
1516
1549
|
}
|
|
@@ -1609,6 +1642,91 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
|
|
|
1609
1642
|
return false;
|
|
1610
1643
|
}
|
|
1611
1644
|
}
|
|
1645
|
+
/**
|
|
1646
|
+
* PNG signature `\x89PNG`. ICO frames may carry either a BMP DIB or an
|
|
1647
|
+
* embedded PNG payload (PNG-in-ICO, supported since Windows Vista).
|
|
1648
|
+
*/
|
|
1649
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
|
|
1650
|
+
function frameLooksLikePng(entry) {
|
|
1651
|
+
return (entry.data.length >= PNG_SIGNATURE.length &&
|
|
1652
|
+
entry.data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE));
|
|
1653
|
+
}
|
|
1654
|
+
async function decodeFrameToPng(entry) {
|
|
1655
|
+
if (frameLooksLikePng(entry)) {
|
|
1656
|
+
return Buffer.from(entry.data);
|
|
1657
|
+
}
|
|
1658
|
+
// BMP DIB frames need to go through sharp's ico-to-PNG path, which only
|
|
1659
|
+
// works on the full ICO container. Fall back to letting the caller use a
|
|
1660
|
+
// sharp pipeline against the original ICO for the missing source.
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
async function pickLargestFrameAsPng(buffer, entries) {
|
|
1664
|
+
const largest = [...entries].sort((a, b) => Math.max(b.width, b.height) - Math.max(a.width, a.height))[0];
|
|
1665
|
+
if (largest) {
|
|
1666
|
+
const decoded = await decodeFrameToPng(largest);
|
|
1667
|
+
if (decoded) {
|
|
1668
|
+
return decoded;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
// Fallback: let sharp render directly from the ICO buffer. sharp picks the
|
|
1672
|
+
// largest embedded frame on its own.
|
|
1673
|
+
try {
|
|
1674
|
+
return await sharp(buffer).png().toBuffer();
|
|
1675
|
+
}
|
|
1676
|
+
catch {
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Ensures the produced ICO carries every Windows standard size so the OS
|
|
1682
|
+
* never has to downsample a 256x256 frame to 16x16 for the tray.
|
|
1683
|
+
* Falls back to `writeIcoWithPreferredSize` if rendering fails.
|
|
1684
|
+
*
|
|
1685
|
+
* Issue #1190.
|
|
1686
|
+
*/
|
|
1687
|
+
async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize = 256, desiredSizes = WIN_STANDARD_ICO_SIZES) {
|
|
1688
|
+
try {
|
|
1689
|
+
const sourceBuffer = await fsExtra.readFile(sourcePath);
|
|
1690
|
+
const entries = parseIcoBuffer(sourceBuffer);
|
|
1691
|
+
const sourcePng = await pickLargestFrameAsPng(sourceBuffer, entries);
|
|
1692
|
+
if (!sourcePng) {
|
|
1693
|
+
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
1694
|
+
}
|
|
1695
|
+
const frames = await Promise.all(desiredSizes.map(async (size) => {
|
|
1696
|
+
// Reuse an existing exact-size PNG frame when possible to keep any
|
|
1697
|
+
// hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
|
|
1698
|
+
const exact = entries.find((entry) => entry.width === size && entry.height === size);
|
|
1699
|
+
if (exact && frameLooksLikePng(exact)) {
|
|
1700
|
+
return { size, png: Buffer.from(exact.data) };
|
|
1701
|
+
}
|
|
1702
|
+
const png = await sharp(sourcePng)
|
|
1703
|
+
.resize(size, size, {
|
|
1704
|
+
fit: 'contain',
|
|
1705
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
1706
|
+
})
|
|
1707
|
+
.ensureAlpha()
|
|
1708
|
+
.png()
|
|
1709
|
+
.toBuffer();
|
|
1710
|
+
return { size, png };
|
|
1711
|
+
}));
|
|
1712
|
+
// Order frames so the preferred size lands first (Windows shell uses the
|
|
1713
|
+
// first-listed frame as a quality hint when choosing which to display).
|
|
1714
|
+
frames.sort((a, b) => {
|
|
1715
|
+
const aExact = a.size === preferredSize ? 0 : 1;
|
|
1716
|
+
const bExact = b.size === preferredSize ? 0 : 1;
|
|
1717
|
+
if (aExact !== bExact)
|
|
1718
|
+
return aExact - bExact;
|
|
1719
|
+
return b.size - a.size;
|
|
1720
|
+
});
|
|
1721
|
+
const icoBuffer = buildIcoFromPngBuffers(frames);
|
|
1722
|
+
await fsExtra.ensureDir(path.dirname(outputPath));
|
|
1723
|
+
await fsExtra.outputFile(outputPath, icoBuffer);
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
catch {
|
|
1727
|
+
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1612
1730
|
/**
|
|
1613
1731
|
* Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
|
|
1614
1732
|
* (supported since Windows Vista). This preserves alpha transparency.
|
|
@@ -1658,7 +1776,7 @@ const ICON_CONFIG = {
|
|
|
1658
1776
|
},
|
|
1659
1777
|
};
|
|
1660
1778
|
const PLATFORM_CONFIG = {
|
|
1661
|
-
win: { format: '.ico', sizes: [
|
|
1779
|
+
win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
|
|
1662
1780
|
linux: { format: '.png', size: 512 },
|
|
1663
1781
|
macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
|
|
1664
1782
|
};
|
|
@@ -1693,10 +1811,15 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
|
|
|
1693
1811
|
try {
|
|
1694
1812
|
const finalIconPath = generateIconPath(appName);
|
|
1695
1813
|
await fsExtra.ensureDir(path.dirname(finalIconPath));
|
|
1696
|
-
//
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1814
|
+
// Re-render ICO so every Windows standard size is present and prefer the
|
|
1815
|
+
// 256px frame as the leading entry; falls back to plain reordering if the
|
|
1816
|
+
// ICO is non-decodable, then to a raw copy. (Issue #1190)
|
|
1817
|
+
const upgraded = await ensureMultiResolutionIco(convertedPath, finalIconPath, 256);
|
|
1818
|
+
if (!upgraded) {
|
|
1819
|
+
const reordered = await writeIcoWithPreferredSize(convertedPath, finalIconPath, 256);
|
|
1820
|
+
if (!reordered) {
|
|
1821
|
+
await fsExtra.copy(convertedPath, finalIconPath);
|
|
1822
|
+
}
|
|
1700
1823
|
}
|
|
1701
1824
|
return finalIconPath;
|
|
1702
1825
|
}
|
|
@@ -2153,6 +2276,28 @@ function normalizeUrl(urlToNormalize) {
|
|
|
2153
2276
|
}
|
|
2154
2277
|
}
|
|
2155
2278
|
|
|
2279
|
+
/**
|
|
2280
|
+
* Error class used for user-facing CLI errors.
|
|
2281
|
+
*
|
|
2282
|
+
* The top-level catch in `bin/cli.ts` prints `message` directly without a
|
|
2283
|
+
* stack trace and exits with code 1. Use this for predictable failures
|
|
2284
|
+
* (invalid names, missing files, etc.) so users see a clean message instead
|
|
2285
|
+
* of a Node.js stack dump.
|
|
2286
|
+
*/
|
|
2287
|
+
class PakeError extends Error {
|
|
2288
|
+
constructor(message) {
|
|
2289
|
+
super(message);
|
|
2290
|
+
this.isUserError = true;
|
|
2291
|
+
this.name = 'PakeError';
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function isPakeError(error) {
|
|
2295
|
+
return (error instanceof PakeError ||
|
|
2296
|
+
(typeof error === 'object' &&
|
|
2297
|
+
error !== null &&
|
|
2298
|
+
error.isUserError === true));
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2156
2301
|
function resolveAppName(name, platform) {
|
|
2157
2302
|
const domain = getDomain(name) || 'pake';
|
|
2158
2303
|
return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain;
|
|
@@ -2195,13 +2340,13 @@ async function handleOptions(options, url) {
|
|
|
2195
2340
|
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.`;
|
|
2196
2341
|
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.`;
|
|
2197
2342
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
|
2198
|
-
logger.error(errorMsg);
|
|
2199
2343
|
if (isActions) {
|
|
2344
|
+
logger.error(errorMsg);
|
|
2200
2345
|
name = resolveAppName(url, platform);
|
|
2201
2346
|
logger.warn(`โผ Inside github actions, use the default name: ${name}`);
|
|
2202
2347
|
}
|
|
2203
2348
|
else {
|
|
2204
|
-
|
|
2349
|
+
throw new PakeError(errorMsg);
|
|
2205
2350
|
}
|
|
2206
2351
|
}
|
|
2207
2352
|
const resolvedName = name || 'pake-app';
|
|
@@ -2258,6 +2403,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2258
2403
|
startToTray: false,
|
|
2259
2404
|
forceInternalNavigation: false,
|
|
2260
2405
|
internalUrlRegex: '',
|
|
2406
|
+
enableFind: false,
|
|
2261
2407
|
iterativeBuild: false,
|
|
2262
2408
|
zoom: 100,
|
|
2263
2409
|
minWidth: 0,
|
|
@@ -2397,6 +2543,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2397
2543
|
.addOption(new Option('--internal-url-regex <string>', 'Regex pattern to match URLs that should be considered internal')
|
|
2398
2544
|
.default(DEFAULT_PAKE_OPTIONS.internalUrlRegex)
|
|
2399
2545
|
.hideHelp())
|
|
2546
|
+
.addOption(new Option('--enable-find', 'Enable in-page Find UI with Cmd/Ctrl+F/G shortcuts')
|
|
2547
|
+
.default(DEFAULT_PAKE_OPTIONS.enableFind)
|
|
2548
|
+
.hideHelp())
|
|
2400
2549
|
.addOption(new Option('--installer-language <string>', 'Installer language')
|
|
2401
2550
|
.default(DEFAULT_PAKE_OPTIONS.installerLanguage)
|
|
2402
2551
|
.hideHelp())
|
|
@@ -2459,21 +2608,46 @@ async function checkUpdateTips() {
|
|
|
2459
2608
|
});
|
|
2460
2609
|
}
|
|
2461
2610
|
program.action(async (url, options) => {
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2611
|
+
try {
|
|
2612
|
+
await checkUpdateTips();
|
|
2613
|
+
if (!url) {
|
|
2614
|
+
program.help({
|
|
2615
|
+
error: false,
|
|
2616
|
+
});
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
log.setDefaultLevel('info');
|
|
2620
|
+
log.setLevel('info');
|
|
2621
|
+
if (options.debug) {
|
|
2622
|
+
log.setLevel('debug');
|
|
2623
|
+
}
|
|
2624
|
+
const appOptions = await handleOptions(options, url);
|
|
2625
|
+
const builder = BuilderProvider.create(appOptions);
|
|
2626
|
+
await builder.prepare();
|
|
2627
|
+
await builder.build(url);
|
|
2628
|
+
}
|
|
2629
|
+
catch (error) {
|
|
2630
|
+
if (isPakeError(error)) {
|
|
2631
|
+
console.error(chalk.red(error.message));
|
|
2632
|
+
}
|
|
2633
|
+
else if (error instanceof Error) {
|
|
2634
|
+
console.error(chalk.red(`โ ${error.message}`));
|
|
2635
|
+
if (options?.debug && error.stack) {
|
|
2636
|
+
console.error(chalk.gray(error.stack));
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
else {
|
|
2640
|
+
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
2641
|
+
}
|
|
2642
|
+
process.exit(1);
|
|
2643
|
+
}
|
|
2644
|
+
});
|
|
2645
|
+
program.parseAsync().catch((error) => {
|
|
2646
|
+
if (error instanceof Error) {
|
|
2647
|
+
console.error(chalk.red(`โ ${error.message}`));
|
|
2468
2648
|
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
if (options.debug) {
|
|
2472
|
-
log.setLevel('debug');
|
|
2649
|
+
else {
|
|
2650
|
+
console.error(chalk.red(`โ Unexpected error: ${String(error)}`));
|
|
2473
2651
|
}
|
|
2474
|
-
|
|
2475
|
-
const builder = BuilderProvider.create(appOptions);
|
|
2476
|
-
await builder.prepare();
|
|
2477
|
-
await builder.build(url);
|
|
2652
|
+
process.exit(1);
|
|
2478
2653
|
});
|
|
2479
|
-
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<!doctype html><html><head><meta charset="utf-8"><title>Pake Badge Test</title></head><body><h1>Pake Badge Test</h1><pre id="log"></pre><script>const log=(msg)=>{document.getElementById("log").textContent+=msg+"\n"};async function run(){await new Promise(r=>setTimeout(r,1500));log("setAppBadge(3)");await navigator.setAppBadge(3);setTimeout(async()=>{log("setAppBadge() dot");await navigator.setAppBadge();},7000);setTimeout(async()=>{log("clearAppBadge()");await navigator.clearAppBadge();},14000);}run().catch(e=>log(String(e)));</script></body></html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<html><body><h1>Hello Pake</h1></body></html>
|