pake-cli 3.11.2 โ 3.11.4
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 +4 -5
- package/dist/cli.js +480 -378
- package/package.json +5 -5
- package/src-tauri/Cargo.lock +1 -3
- package/src-tauri/Cargo.toml +3 -3
- package/src-tauri/pake.json +3 -3
- package/src-tauri/src/app/setup.rs +37 -29
- package/src-tauri/src/app/window.rs +32 -10
- package/src-tauri/src/inject/event.js +55 -7
- package/src-tauri/src/inject/style.js +31 -6
- package/src-tauri/src/lib.rs +11 -9
- package/src-tauri/src/util.rs +13 -5
- package/src-tauri/tauri.conf.json +1 -1
- package/dist/code-review-graph.js +0 -283
- package/dist/test-local.html +0 -1
- package/src-tauri/.cargo/config.toml +0 -10
- package/src-tauri/.pake/pake.json +0 -45
- package/src-tauri/.pake/tauri.conf.json +0 -46
- package/src-tauri/.pake/tauri.linux.conf.json +0 -12
- package/src-tauri/.pake/tauri.macos.conf.json +0 -32
- package/src-tauri/.pake/tauri.windows.conf.json +0 -15
- package/src-tauri/gen/schemas/acl-manifests.json +0 -1
- package/src-tauri/gen/schemas/capabilities.json +0 -1
- package/src-tauri/gen/schemas/desktop-schema.json +0 -3331
- package/src-tauri/gen/schemas/macOS-schema.json +0 -3331
package/dist/cli.js
CHANGED
|
@@ -10,30 +10,28 @@ 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
|
|
14
|
-
import http from 'http';
|
|
15
|
-
import { promisify } from 'util';
|
|
16
|
-
import fs from 'fs';
|
|
13
|
+
import fs from 'fs/promises';
|
|
17
14
|
import { dir } from 'tmp-promise';
|
|
18
15
|
import { fileTypeFromBuffer } from 'file-type';
|
|
19
16
|
import icongen from 'icon-gen';
|
|
20
17
|
import sharp from 'sharp';
|
|
21
18
|
import * as psl from 'psl';
|
|
22
19
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
20
|
+
import fs$1 from 'fs';
|
|
23
21
|
|
|
24
22
|
var name = "pake-cli";
|
|
25
|
-
var version = "3.11.
|
|
23
|
+
var version = "3.11.4";
|
|
26
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
27
25
|
var engines = {
|
|
28
26
|
node: ">=18.0.0"
|
|
29
27
|
};
|
|
30
28
|
var packageManager = "pnpm@10.26.2";
|
|
31
29
|
var bin = {
|
|
32
|
-
pake: "
|
|
30
|
+
pake: "dist/cli.js"
|
|
33
31
|
};
|
|
34
32
|
var repository = {
|
|
35
33
|
type: "git",
|
|
36
|
-
url: "https://github.com/tw93/pake.git"
|
|
34
|
+
url: "git+https://github.com/tw93/pake.git"
|
|
37
35
|
};
|
|
38
36
|
var author = {
|
|
39
37
|
name: "Tw93",
|
|
@@ -95,7 +93,6 @@ var devDependencies = {
|
|
|
95
93
|
"@rollup/plugin-terser": "^0.4.4",
|
|
96
94
|
"@types/fs-extra": "^11.0.4",
|
|
97
95
|
"@types/node": "^25.3.2",
|
|
98
|
-
"@types/page-icon": "^0.3.6",
|
|
99
96
|
"@types/prompts": "^2.4.9",
|
|
100
97
|
"@types/tmp": "^0.2.6",
|
|
101
98
|
"@types/update-notifier": "^6.0.8",
|
|
@@ -110,7 +107,8 @@ var devDependencies = {
|
|
|
110
107
|
};
|
|
111
108
|
var pnpm = {
|
|
112
109
|
overrides: {
|
|
113
|
-
sharp: "^0.34.5"
|
|
110
|
+
sharp: "^0.34.5",
|
|
111
|
+
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
|
114
112
|
},
|
|
115
113
|
onlyBuiltDependencies: [
|
|
116
114
|
"esbuild",
|
|
@@ -219,6 +217,12 @@ function getSpinner(text) {
|
|
|
219
217
|
}).start();
|
|
220
218
|
}
|
|
221
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
|
+
|
|
222
226
|
const { platform: platform$1 } = process;
|
|
223
227
|
const IS_MAC = platform$1 === 'darwin';
|
|
224
228
|
const IS_WIN = platform$1 === 'win32';
|
|
@@ -278,69 +282,6 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
278
282
|
}
|
|
279
283
|
}
|
|
280
284
|
|
|
281
|
-
const logger = {
|
|
282
|
-
info(...msg) {
|
|
283
|
-
log.info(...msg.map((m) => chalk.white(m)));
|
|
284
|
-
},
|
|
285
|
-
debug(...msg) {
|
|
286
|
-
log.debug(...msg);
|
|
287
|
-
},
|
|
288
|
-
error(...msg) {
|
|
289
|
-
log.error(...msg.map((m) => chalk.red(m)));
|
|
290
|
-
},
|
|
291
|
-
warn(...msg) {
|
|
292
|
-
log.info(...msg.map((m) => chalk.yellow(m)));
|
|
293
|
-
},
|
|
294
|
-
success(...msg) {
|
|
295
|
-
log.info(...msg.map((m) => chalk.green(m)));
|
|
296
|
-
},
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
const resolve = promisify(dns.resolve);
|
|
300
|
-
const ping = async (host) => {
|
|
301
|
-
const lookup = promisify(dns.lookup);
|
|
302
|
-
const ip = await lookup(host);
|
|
303
|
-
const start = new Date();
|
|
304
|
-
// Prevent timeouts from affecting user experience.
|
|
305
|
-
const requestPromise = new Promise((resolve, reject) => {
|
|
306
|
-
const req = http.get(`http://${ip.address}`, (res) => {
|
|
307
|
-
const delay = new Date().getTime() - start.getTime();
|
|
308
|
-
res.resume();
|
|
309
|
-
resolve(delay);
|
|
310
|
-
});
|
|
311
|
-
req.on('error', (err) => {
|
|
312
|
-
reject(err);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
316
|
-
setTimeout(() => {
|
|
317
|
-
reject(new Error('Request timed out after 3 seconds'));
|
|
318
|
-
}, 1000);
|
|
319
|
-
});
|
|
320
|
-
return Promise.race([requestPromise, timeoutPromise]);
|
|
321
|
-
};
|
|
322
|
-
async function isChinaDomain(domain) {
|
|
323
|
-
try {
|
|
324
|
-
const [ip] = await resolve(domain);
|
|
325
|
-
return await isChinaIP(ip, domain);
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
logger.debug(`${domain} can't be parse!`);
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
async function isChinaIP(ip, domain) {
|
|
333
|
-
try {
|
|
334
|
-
const delay = await ping(ip);
|
|
335
|
-
logger.debug(`${domain} latency is ${delay} ms`);
|
|
336
|
-
return delay > 1000;
|
|
337
|
-
}
|
|
338
|
-
catch (error) {
|
|
339
|
-
logger.debug(`ping ${domain} failed!`);
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
285
|
function normalizePathForComparison(targetPath) {
|
|
345
286
|
const normalized = path.normalize(targetPath);
|
|
346
287
|
return IS_WIN ? normalized.toLowerCase() : normalized;
|
|
@@ -388,15 +329,13 @@ function ensureRustEnv() {
|
|
|
388
329
|
ensureCargoBinOnPath();
|
|
389
330
|
}
|
|
390
331
|
async function installRust() {
|
|
391
|
-
const
|
|
392
|
-
const isInChina = await isChinaDomain('sh.rustup.rs');
|
|
393
|
-
const rustInstallScriptForMac = isInChina && !isActions
|
|
332
|
+
const rustInstallScriptForUnix = isCnMirrorEnabled()
|
|
394
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'
|
|
395
334
|
: "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
|
|
396
335
|
const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
|
|
397
336
|
const spinner = getSpinner('Downloading Rust...');
|
|
398
337
|
try {
|
|
399
|
-
await shellExec(IS_WIN ? rustInstallScriptForWindows :
|
|
338
|
+
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForUnix, 300000, undefined);
|
|
400
339
|
spinner.succeed(chalk.green('โ Rust installed successfully!'));
|
|
401
340
|
ensureRustEnv();
|
|
402
341
|
}
|
|
@@ -423,25 +362,43 @@ function checkRustInstalled() {
|
|
|
423
362
|
}
|
|
424
363
|
|
|
425
364
|
async function combineFiles(files, output) {
|
|
426
|
-
const contents = files.map((file) => {
|
|
365
|
+
const contents = await Promise.all(files.map(async (file) => {
|
|
427
366
|
if (file.endsWith('.css')) {
|
|
428
|
-
const fileContent = fs.
|
|
367
|
+
const fileContent = await fs.readFile(file, 'utf-8');
|
|
429
368
|
return `window.addEventListener('DOMContentLoaded', (_event) => {
|
|
430
369
|
const css = ${JSON.stringify(fileContent)};
|
|
431
370
|
const style = document.createElement('style');
|
|
432
|
-
style.
|
|
371
|
+
style.textContent = css;
|
|
433
372
|
document.head.appendChild(style);
|
|
434
373
|
});`;
|
|
435
374
|
}
|
|
436
|
-
const fileContent = fs.
|
|
375
|
+
const fileContent = await fs.readFile(file);
|
|
437
376
|
return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
|
|
438
377
|
fileContent +
|
|
439
378
|
' });');
|
|
440
|
-
});
|
|
441
|
-
fs.
|
|
379
|
+
}));
|
|
380
|
+
await fs.writeFile(output, contents.join('\n'));
|
|
442
381
|
return files;
|
|
443
382
|
}
|
|
444
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
|
+
|
|
445
402
|
function generateSafeFilename(name) {
|
|
446
403
|
return name
|
|
447
404
|
.replace(/[<>:"/\\|?*]/g, '_')
|
|
@@ -479,11 +436,15 @@ function generateIdentifierSafeName(name) {
|
|
|
479
436
|
return cleaned;
|
|
480
437
|
}
|
|
481
438
|
|
|
482
|
-
|
|
483
|
-
|
|
439
|
+
function asSupportedPlatform(platform) {
|
|
440
|
+
if (platform !== 'win32' && platform !== 'darwin' && platform !== 'linux') {
|
|
441
|
+
throw new Error(`Pake only supports win32, darwin, and linux; detected '${platform}'.`);
|
|
442
|
+
}
|
|
443
|
+
return platform;
|
|
444
|
+
}
|
|
445
|
+
async function copyTemplateConfigs() {
|
|
484
446
|
const srcTauriDir = path.join(npmDirectory, 'src-tauri');
|
|
485
447
|
await fsExtra.ensureDir(tauriConfigDirectory);
|
|
486
|
-
// Copy source config files to .pake directory (as templates)
|
|
487
448
|
const sourceFiles = [
|
|
488
449
|
'tauri.conf.json',
|
|
489
450
|
'tauri.macos.conf.json',
|
|
@@ -499,51 +460,11 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
499
460
|
await fsExtra.copy(sourcePath, destPath);
|
|
500
461
|
}
|
|
501
462
|
}));
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const platformHideOnClose = hideOnClose ?? platform === 'darwin';
|
|
505
|
-
const tauriConfWindowOptions = {
|
|
506
|
-
width,
|
|
507
|
-
height,
|
|
508
|
-
fullscreen,
|
|
509
|
-
maximize,
|
|
510
|
-
resizable,
|
|
511
|
-
hide_title_bar: hideTitleBar,
|
|
512
|
-
activation_shortcut: activationShortcut,
|
|
513
|
-
always_on_top: alwaysOnTop,
|
|
514
|
-
dark_mode: darkMode,
|
|
515
|
-
disabled_web_shortcuts: disabledWebShortcuts,
|
|
516
|
-
hide_on_close: platformHideOnClose,
|
|
517
|
-
incognito: incognito,
|
|
518
|
-
title: title,
|
|
519
|
-
enable_wasm: wasm,
|
|
520
|
-
enable_drag_drop: enableDragDrop,
|
|
521
|
-
start_to_tray: startToTray && showSystemTray,
|
|
522
|
-
force_internal_navigation: forceInternalNavigation,
|
|
523
|
-
internal_url_regex: internalUrlRegex,
|
|
524
|
-
zoom,
|
|
525
|
-
min_width: minWidth,
|
|
526
|
-
min_height: minHeight,
|
|
527
|
-
ignore_certificate_errors: ignoreCertificateErrors,
|
|
528
|
-
new_window: newWindow,
|
|
529
|
-
};
|
|
530
|
-
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
531
|
-
tauriConf.productName = name;
|
|
532
|
-
tauriConf.identifier = identifier;
|
|
533
|
-
tauriConf.version = appVersion;
|
|
534
|
-
// Always set mainBinaryName to ensure binary uniqueness
|
|
535
|
-
const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
|
|
536
|
-
tauriConf.mainBinaryName =
|
|
537
|
-
platform === 'linux'
|
|
538
|
-
? linuxBinaryName
|
|
539
|
-
: `pake-${generateIdentifierSafeName(name)}`;
|
|
540
|
-
if (platform == 'win32') {
|
|
541
|
-
tauriConf.bundle.windows.wix.language[0] = installerLanguage;
|
|
542
|
-
}
|
|
463
|
+
}
|
|
464
|
+
async function handleLocalFile(url, useLocalFile, tauriConf) {
|
|
543
465
|
const pathExists = await fsExtra.pathExists(url);
|
|
544
466
|
if (pathExists) {
|
|
545
467
|
logger.warn('โผ Your input might be a local file.');
|
|
546
|
-
tauriConf.pake.windows[0].url_type = 'local';
|
|
547
468
|
const fileName = path.basename(url);
|
|
548
469
|
const dirName = path.dirname(url);
|
|
549
470
|
const distDir = path.join(npmDirectory, 'dist');
|
|
@@ -555,8 +476,6 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
555
476
|
else {
|
|
556
477
|
fsExtra.moveSync(distDir, distBakDir, { overwrite: true });
|
|
557
478
|
fsExtra.copySync(dirName, distDir, { overwrite: true });
|
|
558
|
-
// ignore it, because about_pake.html have be erased.
|
|
559
|
-
// const filesToCopyBack = ['cli.js', 'about_pake.html'];
|
|
560
479
|
const filesToCopyBack = ['cli.js'];
|
|
561
480
|
await Promise.all(filesToCopyBack.map((file) => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))));
|
|
562
481
|
}
|
|
@@ -566,28 +485,19 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
566
485
|
else {
|
|
567
486
|
tauriConf.pake.windows[0].url_type = 'web';
|
|
568
487
|
}
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
delete tauriConf.bundle.linux.deb.files;
|
|
583
|
-
// Generate correct desktop file configuration
|
|
584
|
-
const linuxName = generateLinuxPackageName(name);
|
|
585
|
-
const desktopFileName = `com.pake.${linuxName}.desktop`;
|
|
586
|
-
const iconName = `${linuxName}_512`;
|
|
587
|
-
// Create desktop file content
|
|
588
|
-
// Determine if title contains Chinese characters for Name[zh_CN]
|
|
589
|
-
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
590
|
-
const desktopContent = `[Desktop Entry]
|
|
488
|
+
}
|
|
489
|
+
async function mergeLinuxConfig(options, name, tauriConf, linuxBinaryName) {
|
|
490
|
+
const linuxBundle = tauriConf.bundle.linux;
|
|
491
|
+
if (!linuxBundle) {
|
|
492
|
+
throw new Error('Linux bundle configuration is missing from tauri.linux.conf.json; cannot build Linux target.');
|
|
493
|
+
}
|
|
494
|
+
delete linuxBundle.deb.files;
|
|
495
|
+
const linuxName = generateLinuxPackageName(name);
|
|
496
|
+
const desktopFileName = `com.pake.${linuxName}.desktop`;
|
|
497
|
+
const iconName = `${linuxName}_512`;
|
|
498
|
+
const { title } = options;
|
|
499
|
+
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
500
|
+
const desktopContent = `[Desktop Entry]
|
|
591
501
|
Version=1.0
|
|
592
502
|
Type=Application
|
|
593
503
|
Name=${name}
|
|
@@ -600,51 +510,39 @@ MimeType=text/html;text/xml;application/xhtml_xml;
|
|
|
600
510
|
StartupNotify=true
|
|
601
511
|
Terminal=false
|
|
602
512
|
`;
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
};
|
|
614
|
-
// Add desktop file support for RPM
|
|
615
|
-
if (!tauriConf.bundle.linux.rpm) {
|
|
616
|
-
tauriConf.bundle.linux.rpm = {};
|
|
617
|
-
}
|
|
618
|
-
tauriConf.bundle.linux.rpm.files = {
|
|
619
|
-
[desktopInstallPath]: `assets/${desktopFileName}`,
|
|
620
|
-
};
|
|
621
|
-
const validTargets = [
|
|
622
|
-
'deb',
|
|
623
|
-
'appimage',
|
|
624
|
-
'rpm',
|
|
625
|
-
'deb-arm64',
|
|
626
|
-
'appimage-arm64',
|
|
627
|
-
'rpm-arm64',
|
|
628
|
-
];
|
|
629
|
-
const baseTarget = options.targets.includes('-arm64')
|
|
630
|
-
? options.targets.replace('-arm64', '')
|
|
631
|
-
: options.targets;
|
|
632
|
-
if (validTargets.includes(options.targets)) {
|
|
633
|
-
tauriConf.bundle.targets = [baseTarget];
|
|
634
|
-
}
|
|
635
|
-
else {
|
|
636
|
-
logger.warn(`โผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
|
|
637
|
-
}
|
|
513
|
+
const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
|
|
514
|
+
const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
|
|
515
|
+
await fsExtra.ensureDir(srcAssetsDir);
|
|
516
|
+
await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
|
|
517
|
+
const desktopInstallPath = `/usr/share/applications/${desktopFileName}`;
|
|
518
|
+
linuxBundle.deb.files = {
|
|
519
|
+
[desktopInstallPath]: `assets/${desktopFileName}`,
|
|
520
|
+
};
|
|
521
|
+
if (!linuxBundle.rpm) {
|
|
522
|
+
linuxBundle.rpm = {};
|
|
638
523
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
524
|
+
linuxBundle.rpm.files = {
|
|
525
|
+
[desktopInstallPath]: `assets/${desktopFileName}`,
|
|
526
|
+
};
|
|
527
|
+
const validTargets = [
|
|
528
|
+
'deb',
|
|
529
|
+
'appimage',
|
|
530
|
+
'rpm',
|
|
531
|
+
'deb-arm64',
|
|
532
|
+
'appimage-arm64',
|
|
533
|
+
'rpm-arm64',
|
|
534
|
+
];
|
|
535
|
+
const baseTarget = options.targets.includes('-arm64')
|
|
536
|
+
? options.targets.replace('-arm64', '')
|
|
537
|
+
: options.targets;
|
|
538
|
+
if (validTargets.includes(options.targets)) {
|
|
539
|
+
tauriConf.bundle.targets = [baseTarget];
|
|
645
540
|
}
|
|
646
|
-
|
|
647
|
-
|
|
541
|
+
else {
|
|
542
|
+
logger.warn(`โผ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
|
|
648
546
|
const platformIconMap = {
|
|
649
547
|
win32: {
|
|
650
548
|
fileExt: '.ico',
|
|
@@ -670,7 +568,7 @@ Terminal=false
|
|
|
670
568
|
const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
|
|
671
569
|
if (exists) {
|
|
672
570
|
let updateIconPath = true;
|
|
673
|
-
|
|
571
|
+
const customIconExt = path.extname(resolvedIconPath).toLowerCase();
|
|
674
572
|
if (customIconExt !== iconInfo.fileExt) {
|
|
675
573
|
updateIconPath = false;
|
|
676
574
|
logger.warn(`โผ ${iconInfo.message}, but you give ${customIconExt}`);
|
|
@@ -679,7 +577,6 @@ Terminal=false
|
|
|
679
577
|
else {
|
|
680
578
|
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
|
|
681
579
|
tauriConf.bundle.resources = [iconInfo.path];
|
|
682
|
-
// Avoid copying if source and destination are the same
|
|
683
580
|
const absoluteDestPath = path.resolve(iconPath);
|
|
684
581
|
if (resolvedIconPath !== absoluteDestPath) {
|
|
685
582
|
try {
|
|
@@ -706,37 +603,32 @@ Terminal=false
|
|
|
706
603
|
}
|
|
707
604
|
// Set tray icon path.
|
|
708
605
|
let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
|
|
709
|
-
if (systemTrayIcon.length > 0) {
|
|
606
|
+
if (options.systemTrayIcon.length > 0) {
|
|
710
607
|
try {
|
|
711
|
-
await fsExtra.pathExists(systemTrayIcon);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
if (iconExt == '.png' || iconExt == '.ico') {
|
|
608
|
+
await fsExtra.pathExists(options.systemTrayIcon);
|
|
609
|
+
const iconExt = path.extname(options.systemTrayIcon).toLowerCase();
|
|
610
|
+
if (iconExt === '.png' || iconExt === '.ico') {
|
|
715
611
|
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
|
|
716
612
|
trayIconPath = `png/${safeAppName}${iconExt}`;
|
|
717
|
-
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
|
613
|
+
await fsExtra.copy(options.systemTrayIcon, trayIcoPath);
|
|
718
614
|
}
|
|
719
615
|
else {
|
|
720
616
|
logger.warn(`โผ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
|
|
721
617
|
logger.warn(`โผ Default system tray icon will be used.`);
|
|
722
618
|
}
|
|
723
619
|
}
|
|
724
|
-
catch {
|
|
725
|
-
logger.warn(`โผ ${systemTrayIcon}
|
|
620
|
+
catch (err) {
|
|
621
|
+
logger.warn(`โผ Failed to apply system tray icon "${options.systemTrayIcon}": ${err instanceof Error ? err.message : String(err)}`);
|
|
726
622
|
logger.warn(`โผ Default system tray icon will remain unchanged.`);
|
|
727
623
|
}
|
|
728
624
|
}
|
|
729
|
-
// Ensure trayIcon object exists before setting iconPath
|
|
730
|
-
if (!tauriConf.app.trayIcon) {
|
|
731
|
-
tauriConf.app.trayIcon = {};
|
|
732
|
-
}
|
|
733
|
-
tauriConf.app.trayIcon.iconPath = trayIconPath;
|
|
734
625
|
tauriConf.pake.system_tray_path = trayIconPath;
|
|
735
626
|
delete tauriConf.app.trayIcon;
|
|
736
|
-
|
|
737
|
-
|
|
627
|
+
}
|
|
628
|
+
async function injectCustomCode(options, tauriConf) {
|
|
629
|
+
const { inject, proxyUrl, multiInstance, multiWindow, wasm } = options;
|
|
630
|
+
const injectFilePath = path.join(npmDirectory, 'src-tauri/src/inject/custom.js');
|
|
738
631
|
if (inject?.length > 0) {
|
|
739
|
-
// Ensure inject is an array before calling .every()
|
|
740
632
|
const injectArray = Array.isArray(inject) ? inject : [inject];
|
|
741
633
|
if (!injectArray.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
|
|
742
634
|
logger.error('The injected file must be in either CSS or JS format.');
|
|
@@ -753,7 +645,6 @@ Terminal=false
|
|
|
753
645
|
tauriConf.pake.proxy_url = proxyUrl || '';
|
|
754
646
|
tauriConf.pake.multi_instance = multiInstance;
|
|
755
647
|
tauriConf.pake.multi_window = multiWindow;
|
|
756
|
-
// Configure WASM support with required HTTP headers
|
|
757
648
|
if (wasm) {
|
|
758
649
|
tauriConf.app.security = {
|
|
759
650
|
headers: {
|
|
@@ -762,16 +653,16 @@ Terminal=false
|
|
|
762
653
|
},
|
|
763
654
|
};
|
|
764
655
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
656
|
+
}
|
|
657
|
+
async function generateMacEntitlements(camera, microphone) {
|
|
658
|
+
const entitlementEntries = [];
|
|
659
|
+
if (camera) {
|
|
660
|
+
entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
|
|
661
|
+
}
|
|
662
|
+
if (microphone) {
|
|
663
|
+
entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
|
|
664
|
+
}
|
|
665
|
+
const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
775
666
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
776
667
|
<plist version="1.0">
|
|
777
668
|
<dict>
|
|
@@ -779,10 +670,10 @@ ${entitlementEntries.join('\n')}
|
|
|
779
670
|
</dict>
|
|
780
671
|
</plist>
|
|
781
672
|
`;
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
673
|
+
const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
|
|
674
|
+
await fsExtra.writeFile(entitlementsPath, entitlementsContent);
|
|
675
|
+
}
|
|
676
|
+
async function writeAllConfigs(tauriConf, platform) {
|
|
786
677
|
const platformConfigPaths = {
|
|
787
678
|
win32: 'tauri.windows.conf.json',
|
|
788
679
|
darwin: 'tauri.macos.conf.json',
|
|
@@ -793,11 +684,85 @@ ${entitlementEntries.join('\n')}
|
|
|
793
684
|
await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 });
|
|
794
685
|
const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json');
|
|
795
686
|
await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 });
|
|
796
|
-
|
|
687
|
+
const tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
|
|
797
688
|
delete tauriConf2.pake;
|
|
798
689
|
const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json');
|
|
799
690
|
await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 });
|
|
800
691
|
}
|
|
692
|
+
async function mergeConfig(url, options, tauriConf) {
|
|
693
|
+
await copyTemplateConfigs();
|
|
694
|
+
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, useLocalFile, identifier, name = 'pake-app', resizable = true, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, camera, microphone, } = options;
|
|
695
|
+
const platform = asSupportedPlatform(process.platform);
|
|
696
|
+
const platformHideOnClose = hideOnClose ?? platform === 'darwin';
|
|
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
|
+
};
|
|
722
|
+
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
723
|
+
tauriConf.productName = name;
|
|
724
|
+
tauriConf.identifier = identifier;
|
|
725
|
+
tauriConf.version = appVersion;
|
|
726
|
+
const linuxBinaryName = `pake-${generateLinuxPackageName(name)}`;
|
|
727
|
+
tauriConf.mainBinaryName =
|
|
728
|
+
platform === 'linux'
|
|
729
|
+
? linuxBinaryName
|
|
730
|
+
: `pake-${generateIdentifierSafeName(name)}`;
|
|
731
|
+
if (platform === 'win32') {
|
|
732
|
+
const windowsBundle = tauriConf.bundle.windows;
|
|
733
|
+
if (!windowsBundle) {
|
|
734
|
+
throw new Error('Windows bundle configuration is missing from tauri.windows.conf.json; cannot build Windows target.');
|
|
735
|
+
}
|
|
736
|
+
windowsBundle.wix.language[0] = installerLanguage;
|
|
737
|
+
}
|
|
738
|
+
await handleLocalFile(url, useLocalFile, tauriConf);
|
|
739
|
+
const platformMap = {
|
|
740
|
+
win32: 'windows',
|
|
741
|
+
linux: 'linux',
|
|
742
|
+
darwin: 'macos',
|
|
743
|
+
};
|
|
744
|
+
const currentPlatform = platformMap[platform];
|
|
745
|
+
if (userAgent.length > 0) {
|
|
746
|
+
tauriConf.pake.user_agent[currentPlatform] = userAgent;
|
|
747
|
+
}
|
|
748
|
+
tauriConf.pake.system_tray[currentPlatform] = showSystemTray;
|
|
749
|
+
if (platform === 'linux') {
|
|
750
|
+
await mergeLinuxConfig(options, name, tauriConf, linuxBinaryName);
|
|
751
|
+
}
|
|
752
|
+
if (platform === 'darwin') {
|
|
753
|
+
const validMacTargets = ['app', 'dmg'];
|
|
754
|
+
if (validMacTargets.includes(options.targets)) {
|
|
755
|
+
tauriConf.bundle.targets = [options.targets];
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const safeAppName = getSafeAppName(name);
|
|
759
|
+
await mergeIcons(options, name, tauriConf, platform, safeAppName);
|
|
760
|
+
await injectCustomCode(options, tauriConf);
|
|
761
|
+
if (platform === 'darwin') {
|
|
762
|
+
await generateMacEntitlements(camera, microphone);
|
|
763
|
+
}
|
|
764
|
+
await writeAllConfigs(tauriConf, platform);
|
|
765
|
+
}
|
|
801
766
|
|
|
802
767
|
class BaseBuilder {
|
|
803
768
|
constructor(options) {
|
|
@@ -864,6 +829,40 @@ class BaseBuilder {
|
|
|
864
829
|
throw error;
|
|
865
830
|
}
|
|
866
831
|
}
|
|
832
|
+
getInstallCommand(packageManager, useCnMirror) {
|
|
833
|
+
const registryOption = useCnMirror
|
|
834
|
+
? ' --registry=https://registry.npmmirror.com'
|
|
835
|
+
: '';
|
|
836
|
+
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
837
|
+
return `cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`;
|
|
838
|
+
}
|
|
839
|
+
isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig) {
|
|
840
|
+
return projectConfig.trim() === cnMirrorConfig.trim();
|
|
841
|
+
}
|
|
842
|
+
async configureCargoRegistry(tauriSrcPath, useCnMirror) {
|
|
843
|
+
const rustProjectDir = path.join(tauriSrcPath, '.cargo');
|
|
844
|
+
const projectConf = path.join(rustProjectDir, 'config.toml');
|
|
845
|
+
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
846
|
+
if (useCnMirror) {
|
|
847
|
+
await fsExtra.ensureDir(rustProjectDir);
|
|
848
|
+
await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
if (!(await fsExtra.pathExists(projectConf))) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
const [projectConfig, cnMirrorConfig] = await Promise.all([
|
|
855
|
+
fsExtra.readFile(projectConf, 'utf8'),
|
|
856
|
+
fsExtra.readFile(projectCnConf, 'utf8'),
|
|
857
|
+
]);
|
|
858
|
+
if (this.isGeneratedCnMirrorConfig(projectConfig, cnMirrorConfig)) {
|
|
859
|
+
await fsExtra.remove(projectConf);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (projectConfig.includes('rsproxy.cn')) {
|
|
863
|
+
logger.warn(`โผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
867
866
|
async prepare() {
|
|
868
867
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
869
868
|
const tauriTargetPath = path.join(tauriSrcPath, 'target');
|
|
@@ -884,18 +883,14 @@ class BaseBuilder {
|
|
|
884
883
|
}
|
|
885
884
|
else {
|
|
886
885
|
logger.error('โ Rust required to package your webapp.');
|
|
887
|
-
process.exit(
|
|
886
|
+
process.exit(1);
|
|
888
887
|
}
|
|
889
888
|
}
|
|
890
|
-
const isChina = await isChinaDomain('www.npmjs.com');
|
|
891
889
|
const spinner = getSpinner('Installing package...');
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
await fsExtra.ensureDir(rustProjectDir);
|
|
890
|
+
const useCnMirror = isCnMirrorEnabled();
|
|
891
|
+
await this.configureCargoRegistry(tauriSrcPath, useCnMirror);
|
|
895
892
|
// Detect available package manager
|
|
896
893
|
const packageManager = await this.detectPackageManager();
|
|
897
|
-
const registryOption = ' --registry=https://registry.npmmirror.com';
|
|
898
|
-
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
899
894
|
const timeout = this.getInstallTimeout();
|
|
900
895
|
const buildEnv = this.getBuildEnvironment();
|
|
901
896
|
// Show helpful message for first-time users
|
|
@@ -904,43 +899,22 @@ class BaseBuilder {
|
|
|
904
899
|
? 'โบ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
|
|
905
900
|
: 'โบ First-time setup may take 5-10 minutes (installing dependencies)...');
|
|
906
901
|
}
|
|
907
|
-
|
|
902
|
+
if (useCnMirror) {
|
|
903
|
+
logger.info(`โบ ${CN_MIRROR_ENV}=1 detected, using ${packageManager}/rsProxy CN mirror.`);
|
|
904
|
+
}
|
|
908
905
|
try {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
914
|
-
}
|
|
915
|
-
else {
|
|
916
|
-
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
917
|
-
}
|
|
906
|
+
await shellExec(this.getInstallCommand(packageManager, useCnMirror), timeout, {
|
|
907
|
+
...buildEnv,
|
|
908
|
+
CI: 'true',
|
|
909
|
+
});
|
|
918
910
|
spinner.succeed(chalk.green('Package installed!'));
|
|
919
911
|
}
|
|
920
912
|
catch (error) {
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
|
|
924
|
-
!usedMirror) {
|
|
925
|
-
spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
|
|
926
|
-
logger.info('โบ Retrying installation with CN mirror for better speed...');
|
|
927
|
-
const retrySpinner = getSpinner('Retrying installation...');
|
|
928
|
-
usedMirror = true;
|
|
929
|
-
try {
|
|
930
|
-
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
931
|
-
await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
932
|
-
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
933
|
-
retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
|
|
934
|
-
}
|
|
935
|
-
catch (retryError) {
|
|
936
|
-
retrySpinner.fail(chalk.red('Installation failed'));
|
|
937
|
-
throw retryError;
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
spinner.fail(chalk.red('Installation failed'));
|
|
942
|
-
throw error;
|
|
913
|
+
spinner.fail(chalk.red('Installation failed'));
|
|
914
|
+
if (!useCnMirror) {
|
|
915
|
+
logger.info(`โบ If downloads are slow in China, retry with ${CN_MIRROR_ENV}=1 to use CN mirrors.`);
|
|
943
916
|
}
|
|
917
|
+
throw error;
|
|
944
918
|
}
|
|
945
919
|
if (!tauriTargetPathExists) {
|
|
946
920
|
logger.warn('โผ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
|
|
@@ -1093,6 +1067,10 @@ class BaseBuilder {
|
|
|
1093
1067
|
if (this.options.debug) {
|
|
1094
1068
|
fullCommand += ' --verbose';
|
|
1095
1069
|
}
|
|
1070
|
+
const features = this.getBuildFeatures();
|
|
1071
|
+
if (features.length > 0) {
|
|
1072
|
+
fullCommand += ` --features ${features.join(',')}`;
|
|
1073
|
+
}
|
|
1096
1074
|
return fullCommand;
|
|
1097
1075
|
}
|
|
1098
1076
|
getBuildFeatures() {
|
|
@@ -1114,11 +1092,6 @@ class BaseBuilder {
|
|
|
1114
1092
|
if (IS_MAC && this.options.targets === 'app') {
|
|
1115
1093
|
fullCommand += ' --bundles app';
|
|
1116
1094
|
}
|
|
1117
|
-
// Add features
|
|
1118
|
-
const features = this.getBuildFeatures();
|
|
1119
|
-
if (features.length > 0) {
|
|
1120
|
-
fullCommand += ` --features ${features.join(',')}`;
|
|
1121
|
-
}
|
|
1122
1095
|
return fullCommand;
|
|
1123
1096
|
}
|
|
1124
1097
|
getMacOSMajorVersion() {
|
|
@@ -1280,12 +1253,7 @@ class MacBuilder extends BaseBuilder {
|
|
|
1280
1253
|
if (!buildTarget) {
|
|
1281
1254
|
throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
|
|
1282
1255
|
}
|
|
1283
|
-
|
|
1284
|
-
const features = this.getBuildFeatures();
|
|
1285
|
-
if (features.length > 0) {
|
|
1286
|
-
fullCommand += ` --features ${features.join(',')}`;
|
|
1287
|
-
}
|
|
1288
|
-
return fullCommand;
|
|
1256
|
+
return this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1289
1257
|
}
|
|
1290
1258
|
getBasePath() {
|
|
1291
1259
|
const basePath = this.options.debug ? 'debug' : 'release';
|
|
@@ -1325,12 +1293,7 @@ class WinBuilder extends BaseBuilder {
|
|
|
1325
1293
|
if (!buildTarget) {
|
|
1326
1294
|
throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
|
|
1327
1295
|
}
|
|
1328
|
-
|
|
1329
|
-
const features = this.getBuildFeatures();
|
|
1330
|
-
if (features.length > 0) {
|
|
1331
|
-
fullCommand += ` --features ${features.join(',')}`;
|
|
1332
|
-
}
|
|
1333
|
-
return fullCommand;
|
|
1296
|
+
return this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1334
1297
|
}
|
|
1335
1298
|
getBasePath() {
|
|
1336
1299
|
const basePath = this.options.debug ? 'debug' : 'release';
|
|
@@ -1410,10 +1373,6 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1410
1373
|
? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
|
|
1411
1374
|
: undefined;
|
|
1412
1375
|
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1413
|
-
const features = this.getBuildFeatures();
|
|
1414
|
-
if (features.length > 0) {
|
|
1415
|
-
fullCommand += ` --features ${features.join(',')}`;
|
|
1416
|
-
}
|
|
1417
1376
|
if (this.currentBuildType) {
|
|
1418
1377
|
fullCommand += ` --bundles ${this.currentBuildType}`;
|
|
1419
1378
|
}
|
|
@@ -1470,6 +1429,85 @@ class BuilderProvider {
|
|
|
1470
1429
|
}
|
|
1471
1430
|
}
|
|
1472
1431
|
|
|
1432
|
+
const LOCAL_HOST_SUFFIXES = [
|
|
1433
|
+
'.local',
|
|
1434
|
+
'.lan',
|
|
1435
|
+
'.internal',
|
|
1436
|
+
'.home',
|
|
1437
|
+
'.localdomain',
|
|
1438
|
+
];
|
|
1439
|
+
const IPV4_ADDRESS_PATTERN = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
1440
|
+
function normalize(value) {
|
|
1441
|
+
return value.trim().toLowerCase();
|
|
1442
|
+
}
|
|
1443
|
+
function simplify(value) {
|
|
1444
|
+
return normalize(value).replace(/[\s._-]+/g, '');
|
|
1445
|
+
}
|
|
1446
|
+
function generateDashboardIconSlugs(appName) {
|
|
1447
|
+
const normalizedName = normalize(appName);
|
|
1448
|
+
if (!normalizedName) {
|
|
1449
|
+
return [];
|
|
1450
|
+
}
|
|
1451
|
+
const slugs = new Set([
|
|
1452
|
+
normalizedName,
|
|
1453
|
+
normalizedName.replace(/\s+/g, '-'),
|
|
1454
|
+
]);
|
|
1455
|
+
return [...slugs].filter(Boolean);
|
|
1456
|
+
}
|
|
1457
|
+
function isLikelyLocalHostname(hostname) {
|
|
1458
|
+
const normalizedHostname = normalize(hostname);
|
|
1459
|
+
if (!normalizedHostname) {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1462
|
+
return (normalizedHostname === 'localhost' ||
|
|
1463
|
+
IPV4_ADDRESS_PATTERN.test(normalizedHostname) ||
|
|
1464
|
+
normalizedHostname.includes(':') ||
|
|
1465
|
+
!normalizedHostname.includes('.') ||
|
|
1466
|
+
LOCAL_HOST_SUFFIXES.some((suffix) => normalizedHostname.endsWith(suffix)));
|
|
1467
|
+
}
|
|
1468
|
+
function shouldPreferDashboardIcons(url, appName) {
|
|
1469
|
+
if (!appName) {
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
try {
|
|
1473
|
+
const hostname = new URL(url).hostname.toLowerCase();
|
|
1474
|
+
if (!hostname) {
|
|
1475
|
+
return false;
|
|
1476
|
+
}
|
|
1477
|
+
if (isLikelyLocalHostname(hostname)) {
|
|
1478
|
+
return true;
|
|
1479
|
+
}
|
|
1480
|
+
const parsed = psl.parse(hostname);
|
|
1481
|
+
if (!('domain' in parsed) || !parsed.domain) {
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
const registrableDomain = parsed.domain.toLowerCase();
|
|
1485
|
+
if (hostname === registrableDomain) {
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
const subdomain = 'subdomain' in parsed && typeof parsed.subdomain === 'string'
|
|
1489
|
+
? parsed.subdomain
|
|
1490
|
+
: '';
|
|
1491
|
+
if (!subdomain) {
|
|
1492
|
+
return false;
|
|
1493
|
+
}
|
|
1494
|
+
const productLabel = subdomain.split('.').pop() || '';
|
|
1495
|
+
const rootLabel = registrableDomain.split('.')[0] || '';
|
|
1496
|
+
const normalizedAppName = simplify(appName);
|
|
1497
|
+
return (normalizedAppName.length > 0 &&
|
|
1498
|
+
simplify(productLabel) === normalizedAppName &&
|
|
1499
|
+
simplify(rootLabel) !== normalizedAppName);
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
function getIconSourcePriority(url, appName) {
|
|
1506
|
+
return shouldPreferDashboardIcons(url, appName)
|
|
1507
|
+
? ['dashboard', 'domain']
|
|
1508
|
+
: ['domain', 'dashboard'];
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1473
1511
|
const ICO_HEADER_SIZE = 6;
|
|
1474
1512
|
const ICO_DIR_ENTRY_SIZE = 16;
|
|
1475
1513
|
const ICO_TYPE_ICON = 1;
|
|
@@ -1571,11 +1609,48 @@ async function writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize)
|
|
|
1571
1609
|
return false;
|
|
1572
1610
|
}
|
|
1573
1611
|
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Builds an ICO file from an array of PNG buffers using the PNG-in-ICO format
|
|
1614
|
+
* (supported since Windows Vista). This preserves alpha transparency.
|
|
1615
|
+
*/
|
|
1616
|
+
function buildIcoFromPngBuffers(frames) {
|
|
1617
|
+
const count = frames.length;
|
|
1618
|
+
const headerSize = ICO_HEADER_SIZE + count * ICO_DIR_ENTRY_SIZE;
|
|
1619
|
+
const totalPayload = frames.reduce((acc, f) => acc + f.png.length, 0);
|
|
1620
|
+
const output = Buffer.alloc(headerSize + totalPayload);
|
|
1621
|
+
output.writeUInt16LE(0, 0);
|
|
1622
|
+
output.writeUInt16LE(ICO_TYPE_ICON, 2);
|
|
1623
|
+
output.writeUInt16LE(count, 4);
|
|
1624
|
+
let currentOffset = headerSize;
|
|
1625
|
+
for (let i = 0; i < count; i++) {
|
|
1626
|
+
const { size, png } = frames[i];
|
|
1627
|
+
const entryOffset = ICO_HEADER_SIZE + i * ICO_DIR_ENTRY_SIZE;
|
|
1628
|
+
const sizeByte = size >= 256 ? 0 : size;
|
|
1629
|
+
output.writeUInt8(sizeByte, entryOffset);
|
|
1630
|
+
output.writeUInt8(sizeByte, entryOffset + 1);
|
|
1631
|
+
output.writeUInt8(0, entryOffset + 2);
|
|
1632
|
+
output.writeUInt8(0, entryOffset + 3);
|
|
1633
|
+
output.writeUInt16LE(1, entryOffset + 4);
|
|
1634
|
+
output.writeUInt16LE(32, entryOffset + 6);
|
|
1635
|
+
output.writeUInt32LE(png.length, entryOffset + 8);
|
|
1636
|
+
output.writeUInt32LE(currentOffset, entryOffset + 12);
|
|
1637
|
+
png.copy(output, currentOffset);
|
|
1638
|
+
currentOffset += png.length;
|
|
1639
|
+
}
|
|
1640
|
+
return output;
|
|
1641
|
+
}
|
|
1574
1642
|
|
|
1575
1643
|
const ICON_CONFIG = {
|
|
1576
1644
|
minFileSize: 100,
|
|
1577
|
-
supportedFormats: [
|
|
1578
|
-
|
|
1645
|
+
supportedFormats: [
|
|
1646
|
+
'png',
|
|
1647
|
+
'ico',
|
|
1648
|
+
'jpeg',
|
|
1649
|
+
'jpg',
|
|
1650
|
+
'webp',
|
|
1651
|
+
'icns',
|
|
1652
|
+
'svg',
|
|
1653
|
+
],
|
|
1579
1654
|
transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },
|
|
1580
1655
|
downloadTimeout: {
|
|
1581
1656
|
ci: 5000,
|
|
@@ -1591,6 +1666,9 @@ const API_KEYS = {
|
|
|
1591
1666
|
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
|
1592
1667
|
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
|
1593
1668
|
};
|
|
1669
|
+
/**
|
|
1670
|
+
* Generates platform-specific icon paths and handles copying for Windows
|
|
1671
|
+
*/
|
|
1594
1672
|
function generateIconPath(appName, isDefault = false) {
|
|
1595
1673
|
const safeName = isDefault ? 'icon' : getIconBaseName(appName);
|
|
1596
1674
|
const baseName = safeName;
|
|
@@ -1605,7 +1683,7 @@ function generateIconPath(appName, isDefault = false) {
|
|
|
1605
1683
|
function getIconBaseName(appName) {
|
|
1606
1684
|
const baseName = IS_LINUX
|
|
1607
1685
|
? generateLinuxPackageName(appName)
|
|
1608
|
-
:
|
|
1686
|
+
: getSafeAppName(appName);
|
|
1609
1687
|
return baseName || 'pake-app';
|
|
1610
1688
|
}
|
|
1611
1689
|
async function copyWindowsIconIfNeeded(convertedPath, appName) {
|
|
@@ -1628,31 +1706,23 @@ async function copyWindowsIconIfNeeded(convertedPath, appName) {
|
|
|
1628
1706
|
}
|
|
1629
1707
|
}
|
|
1630
1708
|
/**
|
|
1631
|
-
*
|
|
1709
|
+
* Normalizes icon inputs to PNG while preserving alpha.
|
|
1632
1710
|
*/
|
|
1633
1711
|
async function preprocessIcon(inputPath) {
|
|
1634
1712
|
try {
|
|
1635
|
-
const
|
|
1636
|
-
|
|
1637
|
-
|
|
1713
|
+
const extension = path.extname(inputPath).toLowerCase();
|
|
1714
|
+
const shouldNormalize = ['.png', '.jpeg', '.jpg', '.webp', '.svg'].includes(extension);
|
|
1715
|
+
if (!shouldNormalize) {
|
|
1716
|
+
return inputPath;
|
|
1717
|
+
}
|
|
1638
1718
|
const { path: tempDir } = await dir();
|
|
1639
|
-
const outputPath = path.join(tempDir, 'icon-
|
|
1640
|
-
await sharp(
|
|
1641
|
-
create: {
|
|
1642
|
-
width: metadata.width || 512,
|
|
1643
|
-
height: metadata.height || 512,
|
|
1644
|
-
channels: 4,
|
|
1645
|
-
background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
|
|
1646
|
-
},
|
|
1647
|
-
})
|
|
1648
|
-
.composite([{ input: inputPath }])
|
|
1649
|
-
.png()
|
|
1650
|
-
.toFile(outputPath);
|
|
1719
|
+
const outputPath = path.join(tempDir, 'icon-normalized.png');
|
|
1720
|
+
await sharp(inputPath).ensureAlpha().png().toFile(outputPath);
|
|
1651
1721
|
return outputPath;
|
|
1652
1722
|
}
|
|
1653
1723
|
catch (error) {
|
|
1654
1724
|
if (error instanceof Error) {
|
|
1655
|
-
logger.warn(`Failed to
|
|
1725
|
+
logger.warn(`Failed to normalize icon: ${error.message}`);
|
|
1656
1726
|
}
|
|
1657
1727
|
return inputPath;
|
|
1658
1728
|
}
|
|
@@ -1719,15 +1789,22 @@ async function convertIconFormat(inputPath, appName) {
|
|
|
1719
1789
|
const iconName = getIconBaseName(appName);
|
|
1720
1790
|
// Generate platform-specific format
|
|
1721
1791
|
if (IS_WIN) {
|
|
1722
|
-
|
|
1723
|
-
await
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1792
|
+
const icoPath = path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
|
|
1793
|
+
const sourceBuffer = await fsExtra.readFile(processedInputPath);
|
|
1794
|
+
const frames = await Promise.all(PLATFORM_CONFIG.win.sizes.map(async (size) => {
|
|
1795
|
+
const png = await sharp(sourceBuffer)
|
|
1796
|
+
.resize(size, size, {
|
|
1797
|
+
fit: 'contain',
|
|
1798
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
1799
|
+
})
|
|
1800
|
+
.ensureAlpha()
|
|
1801
|
+
.png()
|
|
1802
|
+
.toBuffer();
|
|
1803
|
+
return { size, png };
|
|
1804
|
+
}));
|
|
1805
|
+
const icoBuffer = buildIcoFromPngBuffers(frames);
|
|
1806
|
+
await fsExtra.outputFile(icoPath, icoBuffer);
|
|
1807
|
+
return icoPath;
|
|
1731
1808
|
}
|
|
1732
1809
|
if (IS_LINUX) {
|
|
1733
1810
|
const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
|
|
@@ -1882,15 +1959,74 @@ function generateIconServiceUrls(domain) {
|
|
|
1882
1959
|
*/
|
|
1883
1960
|
function generateDashboardIconUrls(appName) {
|
|
1884
1961
|
const baseUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png';
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
.
|
|
1893
|
-
.
|
|
1962
|
+
return generateDashboardIconSlugs(appName).map((slug) => `${baseUrl}/${slug}.png`);
|
|
1963
|
+
}
|
|
1964
|
+
function isSupportedIconFormat(extension) {
|
|
1965
|
+
return ICON_CONFIG.supportedFormats.includes(extension);
|
|
1966
|
+
}
|
|
1967
|
+
function looksLikeSvg(arrayBuffer) {
|
|
1968
|
+
const sample = Buffer.from(arrayBuffer)
|
|
1969
|
+
.toString('utf-8', 0, Math.min(arrayBuffer.byteLength, 512))
|
|
1970
|
+
.trimStart()
|
|
1971
|
+
.toLowerCase();
|
|
1972
|
+
return (sample.startsWith('<svg') ||
|
|
1973
|
+
(sample.startsWith('<?xml') && sample.includes('<svg')));
|
|
1974
|
+
}
|
|
1975
|
+
function getUrlExtension(iconUrl) {
|
|
1976
|
+
try {
|
|
1977
|
+
return path.extname(new URL(iconUrl).pathname).slice(1).toLowerCase();
|
|
1978
|
+
}
|
|
1979
|
+
catch {
|
|
1980
|
+
return path.extname(iconUrl).slice(1).toLowerCase();
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
async function detectDownloadedIconExtension(response, arrayBuffer, iconUrl) {
|
|
1984
|
+
const fileDetails = await fileTypeFromBuffer(arrayBuffer);
|
|
1985
|
+
if (fileDetails && isSupportedIconFormat(fileDetails.ext)) {
|
|
1986
|
+
return fileDetails.ext;
|
|
1987
|
+
}
|
|
1988
|
+
const contentType = response.headers
|
|
1989
|
+
.get('content-type')
|
|
1990
|
+
?.split(';')[0]
|
|
1991
|
+
.trim();
|
|
1992
|
+
if (contentType === 'image/svg+xml' && looksLikeSvg(arrayBuffer)) {
|
|
1993
|
+
return 'svg';
|
|
1994
|
+
}
|
|
1995
|
+
if (getUrlExtension(iconUrl) === 'svg' && looksLikeSvg(arrayBuffer)) {
|
|
1996
|
+
return 'svg';
|
|
1997
|
+
}
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
async function resolveIconFromUrl(iconUrl, appName, downloadTimeout) {
|
|
2001
|
+
const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
|
|
2002
|
+
if (!iconPath) {
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
const convertedPath = await convertIconFormat(iconPath, appName);
|
|
2006
|
+
if (!convertedPath) {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
return await copyWindowsIconIfNeeded(convertedPath, appName);
|
|
2010
|
+
}
|
|
2011
|
+
async function tryResolveIconSource(source, domain, appName, downloadTimeout) {
|
|
2012
|
+
const iconUrls = source === 'dashboard'
|
|
2013
|
+
? generateDashboardIconUrls(appName)
|
|
2014
|
+
: generateIconServiceUrls(domain);
|
|
2015
|
+
for (const iconUrl of iconUrls) {
|
|
2016
|
+
try {
|
|
2017
|
+
const resolvedPath = await resolveIconFromUrl(iconUrl, appName, downloadTimeout);
|
|
2018
|
+
if (resolvedPath) {
|
|
2019
|
+
return resolvedPath;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
catch (error) {
|
|
2023
|
+
if (error instanceof Error) {
|
|
2024
|
+
const label = source === 'dashboard' ? 'Dashboard icon' : 'Icon service';
|
|
2025
|
+
logger.debug(`${label} ${iconUrl} failed: ${error.message}`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
return null;
|
|
1894
2030
|
}
|
|
1895
2031
|
/**
|
|
1896
2032
|
* Attempts to fetch favicon from website
|
|
@@ -1903,49 +2039,16 @@ async function tryGetFavicon(url, appName) {
|
|
|
1903
2039
|
const downloadTimeout = isCI
|
|
1904
2040
|
? ICON_CONFIG.downloadTimeout.ci
|
|
1905
2041
|
: ICON_CONFIG.downloadTimeout.default;
|
|
1906
|
-
const
|
|
1907
|
-
for (const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
if (!faviconPath)
|
|
1911
|
-
continue;
|
|
1912
|
-
const convertedPath = await convertIconFormat(faviconPath, appName);
|
|
1913
|
-
if (convertedPath) {
|
|
1914
|
-
const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
|
|
1915
|
-
spinner.succeed(chalk.green('Icon fetched and converted successfully!'));
|
|
1916
|
-
return finalPath;
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
catch (error) {
|
|
1920
|
-
if (error instanceof Error) {
|
|
1921
|
-
logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
|
|
1922
|
-
}
|
|
2042
|
+
const sourcePriority = getIconSourcePriority(url, appName);
|
|
2043
|
+
for (const source of sourcePriority) {
|
|
2044
|
+
const resolvedIconPath = await tryResolveIconSource(source, domain, appName, downloadTimeout);
|
|
2045
|
+
if (!resolvedIconPath) {
|
|
1923
2046
|
continue;
|
|
1924
2047
|
}
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
const dashboardIconUrls = generateDashboardIconUrls(appName);
|
|
1930
|
-
for (const iconUrl of dashboardIconUrls) {
|
|
1931
|
-
try {
|
|
1932
|
-
const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
|
|
1933
|
-
if (!iconPath)
|
|
1934
|
-
continue;
|
|
1935
|
-
const convertedPath = await convertIconFormat(iconPath, appName);
|
|
1936
|
-
if (convertedPath) {
|
|
1937
|
-
const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
|
|
1938
|
-
spinner.succeed(chalk.green(`Icon found via dashboard-icons fallback for "${appName}"!`));
|
|
1939
|
-
return finalPath;
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
catch (error) {
|
|
1943
|
-
if (error instanceof Error) {
|
|
1944
|
-
logger.debug(`Dashboard icon ${iconUrl} failed: ${error.message}`);
|
|
1945
|
-
}
|
|
1946
|
-
continue;
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
2048
|
+
spinner.succeed(chalk.green(source === 'dashboard'
|
|
2049
|
+
? `Icon found via dashboard-icons for "${appName}"!`
|
|
2050
|
+
: 'Icon fetched and converted successfully!'));
|
|
2051
|
+
return resolvedIconPath;
|
|
1949
2052
|
}
|
|
1950
2053
|
spinner.warn(`No favicon found for ${domain}. Using default.`);
|
|
1951
2054
|
return null;
|
|
@@ -1979,12 +2082,11 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
|
|
|
1979
2082
|
const arrayBuffer = await response.arrayBuffer();
|
|
1980
2083
|
if (!arrayBuffer || arrayBuffer.byteLength < ICON_CONFIG.minFileSize)
|
|
1981
2084
|
return null;
|
|
1982
|
-
const
|
|
1983
|
-
if (!
|
|
1984
|
-
!ICON_CONFIG.supportedFormats.includes(fileDetails.ext)) {
|
|
2085
|
+
const extension = await detectDownloadedIconExtension(response, arrayBuffer, iconUrl);
|
|
2086
|
+
if (!extension) {
|
|
1985
2087
|
return null;
|
|
1986
2088
|
}
|
|
1987
|
-
return await saveIconFile(arrayBuffer,
|
|
2089
|
+
return await saveIconFile(arrayBuffer, extension);
|
|
1988
2090
|
}
|
|
1989
2091
|
catch (error) {
|
|
1990
2092
|
clearTimeout(timeoutId);
|
|
@@ -2068,11 +2170,9 @@ function resolveLocalAppName(filePath, platform) {
|
|
|
2068
2170
|
return normalized || 'pake-app';
|
|
2069
2171
|
}
|
|
2070
2172
|
function isValidName(name, platform) {
|
|
2071
|
-
const
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
};
|
|
2075
|
-
const reg = platformRegexMapping[platform] || platformRegexMapping.default;
|
|
2173
|
+
const reg = platform === 'linux'
|
|
2174
|
+
? /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/
|
|
2175
|
+
: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/;
|
|
2076
2176
|
return !!name && reg.test(name);
|
|
2077
2177
|
}
|
|
2078
2178
|
async function handleOptions(options, url) {
|
|
@@ -2177,7 +2277,7 @@ function validateNumberInput(value) {
|
|
|
2177
2277
|
return parsedValue;
|
|
2178
2278
|
}
|
|
2179
2279
|
function validateUrlInput(url) {
|
|
2180
|
-
const isFile = fs.existsSync(url);
|
|
2280
|
+
const isFile = fs$1.existsSync(url);
|
|
2181
2281
|
if (!isFile) {
|
|
2182
2282
|
try {
|
|
2183
2283
|
return normalizeUrl(url);
|
|
@@ -2327,7 +2427,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2327
2427
|
.addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)')
|
|
2328
2428
|
.default(DEFAULT_PAKE_OPTIONS.newWindow)
|
|
2329
2429
|
.hideHelp())
|
|
2330
|
-
.
|
|
2430
|
+
.addOption(new Option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle')
|
|
2431
|
+
.default(DEFAULT_PAKE_OPTIONS.install)
|
|
2432
|
+
.hideHelp())
|
|
2331
2433
|
.addOption(new Option('--camera', 'Request camera permission on macOS')
|
|
2332
2434
|
.default(DEFAULT_PAKE_OPTIONS.camera)
|
|
2333
2435
|
.hideHelp())
|