pake-cli 3.6.0 → 3.6.2
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/dist/cli.js +402 -308
- package/dist/dev.js +1100 -215
- package/dist/dev.js.map +1 -1
- package/package.json +4 -3
- package/src-tauri/.pake/pake.json +43 -0
- package/src-tauri/.pake/tauri.conf.json +27 -0
- package/src-tauri/.pake/tauri.linux.conf.json +13 -0
- package/src-tauri/.pake/tauri.macos.conf.json +14 -0
- package/src-tauri/.pake/tauri.windows.conf.json +15 -0
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/app/invoke.rs +10 -5
- package/src-tauri/src/app/menu.rs +281 -0
- package/src-tauri/src/app/mod.rs +1 -0
- package/src-tauri/src/lib.rs +2 -231
- package/src-tauri/assets/com-tw93-weekly.desktop +0 -10
package/dist/dev.js
CHANGED
|
@@ -12,44 +12,13 @@ import prompts from 'prompts';
|
|
|
12
12
|
import ora from 'ora';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
import * as psl from 'psl';
|
|
15
|
+
import os from 'os';
|
|
15
16
|
import { execa, execaSync } from 'execa';
|
|
16
17
|
import dns from 'dns';
|
|
17
18
|
import http from 'http';
|
|
18
19
|
import { promisify } from 'util';
|
|
19
20
|
import fs from 'fs';
|
|
20
|
-
|
|
21
|
-
const DEFAULT_PAKE_OPTIONS = {
|
|
22
|
-
icon: '',
|
|
23
|
-
height: 780,
|
|
24
|
-
width: 1200,
|
|
25
|
-
fullscreen: false,
|
|
26
|
-
resizable: true,
|
|
27
|
-
hideTitleBar: false,
|
|
28
|
-
alwaysOnTop: false,
|
|
29
|
-
appVersion: '1.0.0',
|
|
30
|
-
darkMode: false,
|
|
31
|
-
disabledWebShortcuts: false,
|
|
32
|
-
activationShortcut: '',
|
|
33
|
-
userAgent: '',
|
|
34
|
-
showSystemTray: false,
|
|
35
|
-
multiArch: false,
|
|
36
|
-
targets: 'deb',
|
|
37
|
-
useLocalFile: false,
|
|
38
|
-
systemTrayIcon: '',
|
|
39
|
-
proxyUrl: '',
|
|
40
|
-
debug: false,
|
|
41
|
-
inject: [],
|
|
42
|
-
installerLanguage: 'en-US',
|
|
43
|
-
hideOnClose: true,
|
|
44
|
-
incognito: false,
|
|
45
|
-
};
|
|
46
|
-
// Just for cli development
|
|
47
|
-
const DEFAULT_DEV_PAKE_OPTIONS = {
|
|
48
|
-
...DEFAULT_PAKE_OPTIONS,
|
|
49
|
-
url: 'https://weekly.tw93.fun/',
|
|
50
|
-
name: 'Weekly',
|
|
51
|
-
hideTitleBar: true,
|
|
52
|
-
};
|
|
21
|
+
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
53
22
|
|
|
54
23
|
const logger = {
|
|
55
24
|
info(...msg) {
|
|
@@ -113,20 +82,90 @@ const IS_MAC = platform$2 === 'darwin';
|
|
|
113
82
|
const IS_WIN = platform$2 === 'win32';
|
|
114
83
|
const IS_LINUX = platform$2 === 'linux';
|
|
115
84
|
|
|
116
|
-
|
|
85
|
+
function generateSafeFilename(name) {
|
|
86
|
+
return name
|
|
87
|
+
.replace(/[<>:"/\\|?*]/g, '_')
|
|
88
|
+
.replace(/\s+/g, '_')
|
|
89
|
+
.replace(/\.+$/g, '')
|
|
90
|
+
.slice(0, 255);
|
|
91
|
+
}
|
|
92
|
+
function getSafeAppName(name) {
|
|
93
|
+
return generateSafeFilename(name).toLowerCase();
|
|
94
|
+
}
|
|
95
|
+
function generateLinuxPackageName(name) {
|
|
96
|
+
return name
|
|
97
|
+
.toLowerCase()
|
|
98
|
+
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-')
|
|
99
|
+
.replace(/^-+|-+$/g, '')
|
|
100
|
+
.replace(/-+/g, '-');
|
|
101
|
+
}
|
|
102
|
+
function generateIdentifierSafeName(name) {
|
|
103
|
+
const cleaned = name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '').toLowerCase();
|
|
104
|
+
if (cleaned === '') {
|
|
105
|
+
const fallback = Array.from(name)
|
|
106
|
+
.map((char) => {
|
|
107
|
+
const code = char.charCodeAt(0);
|
|
108
|
+
if ((code >= 48 && code <= 57) ||
|
|
109
|
+
(code >= 65 && code <= 90) ||
|
|
110
|
+
(code >= 97 && code <= 122)) {
|
|
111
|
+
return char.toLowerCase();
|
|
112
|
+
}
|
|
113
|
+
return code.toString(16);
|
|
114
|
+
})
|
|
115
|
+
.join('')
|
|
116
|
+
.slice(0, 50);
|
|
117
|
+
return fallback || 'pake-app';
|
|
118
|
+
}
|
|
119
|
+
return cleaned;
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
const ICON_CONFIG = {
|
|
118
123
|
minFileSize: 100,
|
|
119
|
-
|
|
120
|
-
supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp'],
|
|
124
|
+
supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp', 'icns'],
|
|
121
125
|
whiteBackground: { r: 255, g: 255, b: 255 },
|
|
126
|
+
transparentBackground: { r: 255, g: 255, b: 255, alpha: 0 },
|
|
127
|
+
downloadTimeout: {
|
|
128
|
+
ci: 5000,
|
|
129
|
+
default: 15000,
|
|
130
|
+
},
|
|
122
131
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
const PLATFORM_CONFIG = {
|
|
133
|
+
win: { format: '.ico', sizes: [16, 32, 48, 64, 128, 256] },
|
|
134
|
+
linux: { format: '.png', size: 512 },
|
|
135
|
+
macos: { format: '.icns', sizes: [16, 32, 64, 128, 256, 512, 1024] },
|
|
136
|
+
};
|
|
137
|
+
const API_KEYS = {
|
|
126
138
|
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
|
127
|
-
// cspell:disable-next-line
|
|
128
139
|
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
|
129
140
|
};
|
|
141
|
+
function generateIconPath(appName, isDefault = false) {
|
|
142
|
+
const safeName = isDefault
|
|
143
|
+
? 'icon'
|
|
144
|
+
: generateSafeFilename(appName).toLowerCase();
|
|
145
|
+
const baseName = safeName;
|
|
146
|
+
if (IS_WIN) {
|
|
147
|
+
return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);
|
|
148
|
+
}
|
|
149
|
+
if (IS_LINUX) {
|
|
150
|
+
return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_512.png`);
|
|
151
|
+
}
|
|
152
|
+
return path.join(npmDirectory, 'src-tauri', 'icons', `${baseName}.icns`);
|
|
153
|
+
}
|
|
154
|
+
async function copyWindowsIconIfNeeded(convertedPath, appName) {
|
|
155
|
+
if (!IS_WIN || !convertedPath.endsWith('.ico')) {
|
|
156
|
+
return convertedPath;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const finalIconPath = generateIconPath(appName);
|
|
160
|
+
await fsExtra.ensureDir(path.dirname(finalIconPath));
|
|
161
|
+
await fsExtra.copy(convertedPath, finalIconPath);
|
|
162
|
+
return finalIconPath;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
logger.warn(`Failed to copy Windows icon: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
166
|
+
return convertedPath;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
130
169
|
/**
|
|
131
170
|
* Adds white background to transparent icons only
|
|
132
171
|
*/
|
|
@@ -141,8 +180,8 @@ async function preprocessIcon(inputPath) {
|
|
|
141
180
|
create: {
|
|
142
181
|
width: metadata.width || 512,
|
|
143
182
|
height: metadata.height || 512,
|
|
144
|
-
channels:
|
|
145
|
-
background: ICON_CONFIG.whiteBackground,
|
|
183
|
+
channels: 4,
|
|
184
|
+
background: { ...ICON_CONFIG.whiteBackground, alpha: 1 },
|
|
146
185
|
},
|
|
147
186
|
})
|
|
148
187
|
.composite([{ input: inputPath }])
|
|
@@ -151,7 +190,57 @@ async function preprocessIcon(inputPath) {
|
|
|
151
190
|
return outputPath;
|
|
152
191
|
}
|
|
153
192
|
catch (error) {
|
|
154
|
-
|
|
193
|
+
if (error instanceof Error) {
|
|
194
|
+
logger.warn(`Failed to add background to icon: ${error.message}`);
|
|
195
|
+
}
|
|
196
|
+
return inputPath;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Applies macOS squircle mask to icon
|
|
201
|
+
*/
|
|
202
|
+
async function applyMacOSMask(inputPath) {
|
|
203
|
+
try {
|
|
204
|
+
const { path: tempDir } = await dir();
|
|
205
|
+
const outputPath = path.join(tempDir, 'icon-macos-rounded.png');
|
|
206
|
+
// 1. Create a 1024x1024 rounded rect mask
|
|
207
|
+
// rx="224" is closer to the smooth Apple squircle look for 1024px
|
|
208
|
+
const mask = Buffer.from('<svg width="1024" height="1024"><rect x="0" y="0" width="1024" height="1024" rx="224" ry="224" fill="white"/></svg>');
|
|
209
|
+
// 2. Load input, resize to 1024, apply mask
|
|
210
|
+
const maskedBuffer = await sharp(inputPath)
|
|
211
|
+
.resize(1024, 1024, {
|
|
212
|
+
fit: 'contain',
|
|
213
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
214
|
+
})
|
|
215
|
+
.composite([
|
|
216
|
+
{
|
|
217
|
+
input: mask,
|
|
218
|
+
blend: 'dest-in',
|
|
219
|
+
},
|
|
220
|
+
])
|
|
221
|
+
.png()
|
|
222
|
+
.toBuffer();
|
|
223
|
+
// 3. Resize to 840x840 (~18% padding) to solve "too big" visual issue
|
|
224
|
+
// Native MacOS icons often leave some breathing room
|
|
225
|
+
await sharp(maskedBuffer)
|
|
226
|
+
.resize(840, 840, {
|
|
227
|
+
fit: 'contain',
|
|
228
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
229
|
+
})
|
|
230
|
+
.extend({
|
|
231
|
+
top: 92,
|
|
232
|
+
bottom: 92,
|
|
233
|
+
left: 92,
|
|
234
|
+
right: 92,
|
|
235
|
+
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
236
|
+
})
|
|
237
|
+
.toFile(outputPath);
|
|
238
|
+
return outputPath;
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error instanceof Error) {
|
|
242
|
+
logger.warn(`Failed to apply macOS mask: ${error.message}`);
|
|
243
|
+
}
|
|
155
244
|
return inputPath;
|
|
156
245
|
}
|
|
157
246
|
}
|
|
@@ -166,104 +255,155 @@ async function convertIconFormat(inputPath, appName) {
|
|
|
166
255
|
const platformOutputDir = path.join(outputDir, 'converted-icons');
|
|
167
256
|
await fsExtra.ensureDir(platformOutputDir);
|
|
168
257
|
const processedInputPath = await preprocessIcon(inputPath);
|
|
169
|
-
const iconName = appName.toLowerCase();
|
|
258
|
+
const iconName = generateSafeFilename(appName).toLowerCase();
|
|
170
259
|
// Generate platform-specific format
|
|
171
260
|
if (IS_WIN) {
|
|
261
|
+
// Support multiple sizes for better Windows compatibility
|
|
172
262
|
await icongen(processedInputPath, platformOutputDir, {
|
|
173
263
|
report: false,
|
|
174
|
-
ico: {
|
|
264
|
+
ico: {
|
|
265
|
+
name: `${iconName}_256`,
|
|
266
|
+
sizes: PLATFORM_CONFIG.win.sizes,
|
|
267
|
+
},
|
|
175
268
|
});
|
|
176
|
-
return path.join(platformOutputDir, `${iconName}_256.
|
|
269
|
+
return path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
|
|
177
270
|
}
|
|
178
271
|
if (IS_LINUX) {
|
|
179
|
-
const outputPath = path.join(platformOutputDir, `${iconName}
|
|
180
|
-
|
|
272
|
+
const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
|
|
273
|
+
// Ensure we convert to proper PNG format with correct size
|
|
274
|
+
await sharp(processedInputPath)
|
|
275
|
+
.resize(PLATFORM_CONFIG.linux.size, PLATFORM_CONFIG.linux.size, {
|
|
276
|
+
fit: 'contain',
|
|
277
|
+
background: ICON_CONFIG.transparentBackground,
|
|
278
|
+
})
|
|
279
|
+
.ensureAlpha()
|
|
280
|
+
.png()
|
|
281
|
+
.toFile(outputPath);
|
|
181
282
|
return outputPath;
|
|
182
283
|
}
|
|
183
284
|
// macOS
|
|
184
|
-
await
|
|
285
|
+
const macIconPath = await applyMacOSMask(processedInputPath);
|
|
286
|
+
await icongen(macIconPath, platformOutputDir, {
|
|
185
287
|
report: false,
|
|
186
|
-
icns: { name: iconName, sizes:
|
|
288
|
+
icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },
|
|
187
289
|
});
|
|
188
|
-
const outputPath = path.join(platformOutputDir, `${iconName}.
|
|
290
|
+
const outputPath = path.join(platformOutputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
|
|
189
291
|
return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
|
|
190
292
|
}
|
|
191
293
|
catch (error) {
|
|
192
|
-
|
|
294
|
+
if (error instanceof Error) {
|
|
295
|
+
logger.warn(`Icon format conversion failed: ${error.message}`);
|
|
296
|
+
}
|
|
193
297
|
return null;
|
|
194
298
|
}
|
|
195
299
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return
|
|
300
|
+
/**
|
|
301
|
+
* Processes downloaded or local icon for platform-specific format
|
|
302
|
+
*/
|
|
303
|
+
async function processIcon(iconPath, appName) {
|
|
304
|
+
if (!iconPath || !appName)
|
|
305
|
+
return iconPath;
|
|
306
|
+
// Check if already in correct platform format
|
|
307
|
+
const ext = path.extname(iconPath).toLowerCase();
|
|
308
|
+
const isCorrectFormat = (IS_WIN && ext === '.ico') ||
|
|
309
|
+
(IS_LINUX && ext === '.png') ||
|
|
310
|
+
(!IS_WIN && !IS_LINUX && ext === '.icns');
|
|
311
|
+
if (isCorrectFormat) {
|
|
312
|
+
return await copyWindowsIconIfNeeded(iconPath, appName);
|
|
202
313
|
}
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return faviconPath;
|
|
314
|
+
// Convert to platform format
|
|
315
|
+
const convertedPath = await convertIconFormat(iconPath, appName);
|
|
316
|
+
if (convertedPath) {
|
|
317
|
+
return await copyWindowsIconIfNeeded(convertedPath, appName);
|
|
208
318
|
}
|
|
319
|
+
return iconPath;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Gets default icon with platform-specific fallback logic
|
|
323
|
+
*/
|
|
324
|
+
async function getDefaultIcon() {
|
|
209
325
|
logger.info('✼ No icon provided, using default icon.');
|
|
210
|
-
// For Windows, ensure we have proper fallback handling
|
|
211
326
|
if (IS_WIN) {
|
|
212
|
-
const defaultIcoPath =
|
|
327
|
+
const defaultIcoPath = generateIconPath('icon', true);
|
|
213
328
|
const defaultPngPath = path.join(npmDirectory, 'src-tauri/png/icon_512.png');
|
|
214
|
-
//
|
|
329
|
+
// Try default ico first
|
|
215
330
|
if (await fsExtra.pathExists(defaultIcoPath)) {
|
|
216
331
|
return defaultIcoPath;
|
|
217
332
|
}
|
|
218
|
-
//
|
|
333
|
+
// Convert from png if ico doesn't exist
|
|
219
334
|
if (await fsExtra.pathExists(defaultPngPath)) {
|
|
220
335
|
logger.info('✼ Default ico not found, converting from png...');
|
|
221
336
|
try {
|
|
222
337
|
const convertedPath = await convertIconFormat(defaultPngPath, 'icon');
|
|
223
338
|
if (convertedPath && (await fsExtra.pathExists(convertedPath))) {
|
|
224
|
-
return convertedPath;
|
|
339
|
+
return await copyWindowsIconIfNeeded(convertedPath, 'icon');
|
|
225
340
|
}
|
|
226
341
|
}
|
|
227
342
|
catch (error) {
|
|
228
|
-
logger.warn(`Failed to convert default png to ico: ${error.message}`);
|
|
343
|
+
logger.warn(`Failed to convert default png to ico: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
229
344
|
}
|
|
230
345
|
}
|
|
231
|
-
//
|
|
346
|
+
// Fallback to png or empty
|
|
232
347
|
if (await fsExtra.pathExists(defaultPngPath)) {
|
|
233
348
|
logger.warn('✼ Using png as fallback for Windows (may cause issues).');
|
|
234
349
|
return defaultPngPath;
|
|
235
350
|
}
|
|
236
|
-
|
|
237
|
-
|
|
351
|
+
logger.warn('✼ No default icon found, will use pake default.');
|
|
352
|
+
return '';
|
|
238
353
|
}
|
|
354
|
+
// Linux and macOS defaults
|
|
239
355
|
const iconPath = IS_LINUX
|
|
240
356
|
? 'src-tauri/png/icon_512.png'
|
|
241
357
|
: 'src-tauri/icons/icon.icns';
|
|
242
358
|
return path.join(npmDirectory, iconPath);
|
|
243
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Main icon handling function with simplified logic flow
|
|
362
|
+
*/
|
|
363
|
+
async function handleIcon(options, url) {
|
|
364
|
+
// Handle custom icon (local file or remote URL)
|
|
365
|
+
if (options.icon) {
|
|
366
|
+
if (options.icon.startsWith('http')) {
|
|
367
|
+
const downloadedPath = await downloadIcon(options.icon);
|
|
368
|
+
if (downloadedPath) {
|
|
369
|
+
const result = await processIcon(downloadedPath, options.name || '');
|
|
370
|
+
if (result)
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
return '';
|
|
374
|
+
}
|
|
375
|
+
// Local file path
|
|
376
|
+
const resolvedPath = path.resolve(options.icon);
|
|
377
|
+
const result = await processIcon(resolvedPath, options.name || '');
|
|
378
|
+
return result || resolvedPath;
|
|
379
|
+
}
|
|
380
|
+
// Try favicon from website
|
|
381
|
+
if (url && options.name) {
|
|
382
|
+
const faviconPath = await tryGetFavicon(url, options.name);
|
|
383
|
+
if (faviconPath)
|
|
384
|
+
return faviconPath;
|
|
385
|
+
}
|
|
386
|
+
// Use default icon
|
|
387
|
+
return await getDefaultIcon();
|
|
388
|
+
}
|
|
244
389
|
/**
|
|
245
390
|
* Generates icon service URLs for a domain
|
|
246
391
|
*/
|
|
247
392
|
function generateIconServiceUrls(domain) {
|
|
248
|
-
const logoDevUrls =
|
|
393
|
+
const logoDevUrls = API_KEYS.logoDev
|
|
249
394
|
.sort(() => Math.random() - 0.5)
|
|
250
395
|
.map((token) => `https://img.logo.dev/${domain}?token=${token}&format=png&size=256`);
|
|
251
|
-
const brandfetchUrls =
|
|
396
|
+
const brandfetchUrls = API_KEYS.brandfetch
|
|
252
397
|
.sort(() => Math.random() - 0.5)
|
|
253
398
|
.map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);
|
|
254
399
|
return [
|
|
255
400
|
...logoDevUrls,
|
|
256
401
|
...brandfetchUrls,
|
|
257
402
|
`https://logo.clearbit.com/${domain}?size=256`,
|
|
258
|
-
`https://logo.uplead.com/${domain}`,
|
|
259
403
|
`https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
|
|
260
404
|
`https://favicon.is/${domain}`,
|
|
261
|
-
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
|
|
262
|
-
`https://icon.horse/icon/${domain}`,
|
|
263
405
|
`https://${domain}/favicon.ico`,
|
|
264
406
|
`https://www.${domain}/favicon.ico`,
|
|
265
|
-
`https://${domain}/apple-touch-icon.png`,
|
|
266
|
-
`https://${domain}/apple-touch-icon-precomposed.png`,
|
|
267
407
|
];
|
|
268
408
|
}
|
|
269
409
|
/**
|
|
@@ -274,9 +414,10 @@ async function tryGetFavicon(url, appName) {
|
|
|
274
414
|
const domain = new URL(url).hostname;
|
|
275
415
|
const spinner = getSpinner(`Fetching icon from ${domain}...`);
|
|
276
416
|
const serviceUrls = generateIconServiceUrls(domain);
|
|
277
|
-
// Use shorter timeout for CI environments
|
|
278
417
|
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
279
|
-
const downloadTimeout = isCI
|
|
418
|
+
const downloadTimeout = isCI
|
|
419
|
+
? ICON_CONFIG.downloadTimeout.ci
|
|
420
|
+
: ICON_CONFIG.downloadTimeout.default;
|
|
280
421
|
for (const serviceUrl of serviceUrls) {
|
|
281
422
|
try {
|
|
282
423
|
const faviconPath = await downloadIcon(serviceUrl, false, downloadTimeout);
|
|
@@ -284,23 +425,33 @@ async function tryGetFavicon(url, appName) {
|
|
|
284
425
|
continue;
|
|
285
426
|
const convertedPath = await convertIconFormat(faviconPath, appName);
|
|
286
427
|
if (convertedPath) {
|
|
428
|
+
const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
|
|
287
429
|
spinner.succeed(chalk.green('Icon fetched and converted successfully!'));
|
|
288
|
-
return
|
|
430
|
+
return finalPath;
|
|
289
431
|
}
|
|
290
432
|
}
|
|
291
433
|
catch (error) {
|
|
292
|
-
|
|
293
|
-
if (isCI) {
|
|
434
|
+
if (error instanceof Error) {
|
|
294
435
|
logger.debug(`Icon service ${serviceUrl} failed: ${error.message}`);
|
|
295
436
|
}
|
|
437
|
+
// Network error handling
|
|
438
|
+
if ((IS_LINUX || IS_WIN) && error.code === 'ENOTFOUND') {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
// Icon generation error on Windows
|
|
442
|
+
if (IS_WIN && error instanceof Error && error.message.includes('icongen')) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
296
445
|
continue;
|
|
297
446
|
}
|
|
298
447
|
}
|
|
299
|
-
spinner.warn(
|
|
448
|
+
spinner.warn(`No favicon found for ${domain}. Using default.`);
|
|
300
449
|
return null;
|
|
301
450
|
}
|
|
302
451
|
catch (error) {
|
|
303
|
-
|
|
452
|
+
if (error instanceof Error) {
|
|
453
|
+
logger.warn(`Failed to fetch favicon: ${error.message}`);
|
|
454
|
+
}
|
|
304
455
|
return null;
|
|
305
456
|
}
|
|
306
457
|
}
|
|
@@ -311,7 +462,7 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
|
|
|
311
462
|
try {
|
|
312
463
|
const response = await axios.get(iconUrl, {
|
|
313
464
|
responseType: 'arraybuffer',
|
|
314
|
-
timeout: customTimeout ||
|
|
465
|
+
timeout: customTimeout || 10000,
|
|
315
466
|
});
|
|
316
467
|
const iconData = response.data;
|
|
317
468
|
if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize)
|
|
@@ -325,7 +476,7 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
|
|
|
325
476
|
}
|
|
326
477
|
catch (error) {
|
|
327
478
|
if (showSpinner && !(error.response?.status === 404)) {
|
|
328
|
-
|
|
479
|
+
logger.error('Icon download failed!');
|
|
329
480
|
}
|
|
330
481
|
return null;
|
|
331
482
|
}
|
|
@@ -335,15 +486,11 @@ async function downloadIcon(iconUrl, showSpinner = true, customTimeout) {
|
|
|
335
486
|
*/
|
|
336
487
|
async function saveIconFile(iconData, extension) {
|
|
337
488
|
const buffer = Buffer.from(iconData);
|
|
338
|
-
if (IS_LINUX) {
|
|
339
|
-
const iconPath = 'png/linux_temp.png';
|
|
340
|
-
await fsExtra.outputFile(`${npmDirectory}/src-tauri/${iconPath}`, buffer);
|
|
341
|
-
return iconPath;
|
|
342
|
-
}
|
|
343
489
|
const { path: tempPath } = await dir();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
490
|
+
// Always save with the original extension first
|
|
491
|
+
const originalIconPath = path.join(tempPath, `icon.${extension}`);
|
|
492
|
+
await fsExtra.outputFile(originalIconPath, buffer);
|
|
493
|
+
return originalIconPath;
|
|
347
494
|
}
|
|
348
495
|
|
|
349
496
|
// Extracts the domain from a given URL.
|
|
@@ -364,6 +511,27 @@ function getDomain(inputUrl) {
|
|
|
364
511
|
return null;
|
|
365
512
|
}
|
|
366
513
|
}
|
|
514
|
+
// Appends 'https://' protocol to the URL if not present.
|
|
515
|
+
function appendProtocol(inputUrl) {
|
|
516
|
+
try {
|
|
517
|
+
new URL(inputUrl);
|
|
518
|
+
return inputUrl;
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
return `https://${inputUrl}`;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// Normalizes the URL by ensuring it has a protocol and is valid.
|
|
525
|
+
function normalizeUrl(urlToNormalize) {
|
|
526
|
+
const urlWithProtocol = appendProtocol(urlToNormalize);
|
|
527
|
+
try {
|
|
528
|
+
new URL(urlWithProtocol);
|
|
529
|
+
return urlWithProtocol;
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
throw new Error(`Your url "${urlWithProtocol}" is invalid: ${err.message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
367
535
|
|
|
368
536
|
function resolveAppName(name, platform) {
|
|
369
537
|
const domain = getDomain(name) || 'pake';
|
|
@@ -371,8 +539,8 @@ function resolveAppName(name, platform) {
|
|
|
371
539
|
}
|
|
372
540
|
function isValidName(name, platform) {
|
|
373
541
|
const platformRegexMapping = {
|
|
374
|
-
linux: /^[a-z0-9][a-z0-9-]*$/,
|
|
375
|
-
default: /^[a-zA-Z0-9][a-zA-Z0-9- ]*$/,
|
|
542
|
+
linux: /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/,
|
|
543
|
+
default: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff- ]*$/,
|
|
376
544
|
};
|
|
377
545
|
const reg = platformRegexMapping[platform] || platformRegexMapping.default;
|
|
378
546
|
return !!name && reg.test(name);
|
|
@@ -388,12 +556,10 @@ async function handleOptions(options, url) {
|
|
|
388
556
|
const namePrompt = await promptText(promptMessage, defaultName);
|
|
389
557
|
name = namePrompt || defaultName;
|
|
390
558
|
}
|
|
391
|
-
// Handle platform-specific name formatting
|
|
392
559
|
if (name && platform === 'linux') {
|
|
393
|
-
|
|
394
|
-
name = name.toLowerCase().replace(/\s+/g, '-');
|
|
560
|
+
name = generateLinuxPackageName(name);
|
|
395
561
|
}
|
|
396
|
-
if (!isValidName(name, platform)) {
|
|
562
|
+
if (name && !isValidName(name, platform)) {
|
|
397
563
|
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.`;
|
|
398
564
|
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.`;
|
|
399
565
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
|
@@ -411,7 +577,8 @@ async function handleOptions(options, url) {
|
|
|
411
577
|
name,
|
|
412
578
|
identifier: getIdentifier(url),
|
|
413
579
|
};
|
|
414
|
-
|
|
580
|
+
const iconPath = await handleIcon(appOptions, url);
|
|
581
|
+
appOptions.icon = iconPath || '';
|
|
415
582
|
return appOptions;
|
|
416
583
|
}
|
|
417
584
|
|
|
@@ -447,7 +614,9 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
447
614
|
try {
|
|
448
615
|
const { exitCode } = await execa(command, {
|
|
449
616
|
cwd: npmDirectory,
|
|
450
|
-
|
|
617
|
+
// Use 'inherit' to show all output directly to user in real-time.
|
|
618
|
+
// This ensures linuxdeploy and other tool outputs are visible during builds.
|
|
619
|
+
stdio: 'inherit',
|
|
451
620
|
shell: true,
|
|
452
621
|
timeout,
|
|
453
622
|
env: env ? { ...process.env, ...env } : process.env,
|
|
@@ -460,7 +629,38 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
460
629
|
if (error.timedOut) {
|
|
461
630
|
throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
|
|
462
631
|
}
|
|
463
|
-
|
|
632
|
+
let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;
|
|
633
|
+
// Provide helpful guidance for common Linux AppImage build failures
|
|
634
|
+
// caused by strip tool incompatibility with modern glibc (2.38+)
|
|
635
|
+
const lowerError = errorMessage.toLowerCase();
|
|
636
|
+
if (process.platform === 'linux' &&
|
|
637
|
+
(lowerError.includes('linuxdeploy') ||
|
|
638
|
+
lowerError.includes('appimage') ||
|
|
639
|
+
lowerError.includes('strip'))) {
|
|
640
|
+
errorMsg +=
|
|
641
|
+
'\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n' +
|
|
642
|
+
'Linux AppImage Build Failed\n' +
|
|
643
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' +
|
|
644
|
+
'Cause: Strip tool incompatibility with glibc 2.38+\n' +
|
|
645
|
+
' (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
|
|
646
|
+
'Quick fix:\n' +
|
|
647
|
+
' NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
|
|
648
|
+
'Alternatives:\n' +
|
|
649
|
+
' • Use DEB format: pake <url> --targets deb\n' +
|
|
650
|
+
' • Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
|
|
651
|
+
' • Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
|
|
652
|
+
'━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
|
|
653
|
+
if (lowerError.includes('fuse') ||
|
|
654
|
+
lowerError.includes('operation not permitted') ||
|
|
655
|
+
lowerError.includes('/dev/fuse')) {
|
|
656
|
+
errorMsg +=
|
|
657
|
+
'\n\nDocker / Container hint:\n' +
|
|
658
|
+
' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' +
|
|
659
|
+
' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' +
|
|
660
|
+
' or run on the host directly.';
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
throw new Error(errorMsg);
|
|
464
664
|
}
|
|
465
665
|
}
|
|
466
666
|
|
|
@@ -509,6 +709,52 @@ async function isChinaIP(ip, domain) {
|
|
|
509
709
|
}
|
|
510
710
|
}
|
|
511
711
|
|
|
712
|
+
function normalizePathForComparison(targetPath) {
|
|
713
|
+
const normalized = path.normalize(targetPath);
|
|
714
|
+
return IS_WIN ? normalized.toLowerCase() : normalized;
|
|
715
|
+
}
|
|
716
|
+
function getCargoHomeCandidates() {
|
|
717
|
+
const candidates = new Set();
|
|
718
|
+
if (process.env.CARGO_HOME) {
|
|
719
|
+
candidates.add(process.env.CARGO_HOME);
|
|
720
|
+
}
|
|
721
|
+
const homeDir = os.homedir();
|
|
722
|
+
if (homeDir) {
|
|
723
|
+
candidates.add(path.join(homeDir, '.cargo'));
|
|
724
|
+
}
|
|
725
|
+
if (IS_WIN && process.env.USERPROFILE) {
|
|
726
|
+
candidates.add(path.join(process.env.USERPROFILE, '.cargo'));
|
|
727
|
+
}
|
|
728
|
+
return Array.from(candidates).filter(Boolean);
|
|
729
|
+
}
|
|
730
|
+
function ensureCargoBinOnPath() {
|
|
731
|
+
const currentPath = process.env.PATH || '';
|
|
732
|
+
const segments = currentPath.split(path.delimiter).filter(Boolean);
|
|
733
|
+
const normalizedSegments = new Set(segments.map((segment) => normalizePathForComparison(segment)));
|
|
734
|
+
const additions = [];
|
|
735
|
+
let cargoHomeSet = Boolean(process.env.CARGO_HOME);
|
|
736
|
+
for (const cargoHome of getCargoHomeCandidates()) {
|
|
737
|
+
const binDir = path.join(cargoHome, 'bin');
|
|
738
|
+
if (fsExtra.pathExistsSync(binDir) &&
|
|
739
|
+
!normalizedSegments.has(normalizePathForComparison(binDir))) {
|
|
740
|
+
additions.push(binDir);
|
|
741
|
+
normalizedSegments.add(normalizePathForComparison(binDir));
|
|
742
|
+
}
|
|
743
|
+
if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {
|
|
744
|
+
process.env.CARGO_HOME = cargoHome;
|
|
745
|
+
cargoHomeSet = true;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (additions.length) {
|
|
749
|
+
const prefix = additions.join(path.delimiter);
|
|
750
|
+
process.env.PATH = segments.length
|
|
751
|
+
? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`
|
|
752
|
+
: prefix;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function ensureRustEnv() {
|
|
756
|
+
ensureCargoBinOnPath();
|
|
757
|
+
}
|
|
512
758
|
async function installRust() {
|
|
513
759
|
const isActions = process.env.GITHUB_ACTIONS;
|
|
514
760
|
const isInChina = await isChinaDomain('sh.rustup.rs');
|
|
@@ -518,16 +764,23 @@ async function installRust() {
|
|
|
518
764
|
const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
|
|
519
765
|
const spinner = getSpinner('Downloading Rust...');
|
|
520
766
|
try {
|
|
521
|
-
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
|
|
767
|
+
await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined);
|
|
522
768
|
spinner.succeed(chalk.green('✔ Rust installed successfully!'));
|
|
769
|
+
ensureRustEnv();
|
|
523
770
|
}
|
|
524
771
|
catch (error) {
|
|
525
772
|
spinner.fail(chalk.red('✕ Rust installation failed!'));
|
|
526
|
-
|
|
773
|
+
if (error instanceof Error) {
|
|
774
|
+
console.error(error.message);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
console.error(error);
|
|
778
|
+
}
|
|
527
779
|
process.exit(1);
|
|
528
780
|
}
|
|
529
781
|
}
|
|
530
782
|
function checkRustInstalled() {
|
|
783
|
+
ensureCargoBinOnPath();
|
|
531
784
|
try {
|
|
532
785
|
execaSync('rustc', ['--version']);
|
|
533
786
|
return true;
|
|
@@ -539,12 +792,16 @@ function checkRustInstalled() {
|
|
|
539
792
|
|
|
540
793
|
async function combineFiles(files, output) {
|
|
541
794
|
const contents = files.map((file) => {
|
|
542
|
-
const fileContent = fs.readFileSync(file);
|
|
543
795
|
if (file.endsWith('.css')) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
796
|
+
const fileContent = fs.readFileSync(file, 'utf-8');
|
|
797
|
+
return `window.addEventListener('DOMContentLoaded', (_event) => {
|
|
798
|
+
const css = ${JSON.stringify(fileContent)};
|
|
799
|
+
const style = document.createElement('style');
|
|
800
|
+
style.innerHTML = css;
|
|
801
|
+
document.head.appendChild(style);
|
|
802
|
+
});`;
|
|
547
803
|
}
|
|
804
|
+
const fileContent = fs.readFileSync(file);
|
|
548
805
|
return ("window.addEventListener('DOMContentLoaded', (_event) => { " +
|
|
549
806
|
fileContent +
|
|
550
807
|
' });');
|
|
@@ -573,31 +830,42 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
573
830
|
await fsExtra.copy(sourcePath, destPath);
|
|
574
831
|
}
|
|
575
832
|
}));
|
|
576
|
-
const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, } = options;
|
|
833
|
+
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, startToTray, forceInternalNavigation, zoom, minWidth, minHeight, ignoreCertificateErrors, } = options;
|
|
577
834
|
const { platform } = process;
|
|
578
|
-
|
|
835
|
+
const platformHideOnClose = hideOnClose ?? platform === 'darwin';
|
|
579
836
|
const tauriConfWindowOptions = {
|
|
580
837
|
width,
|
|
581
838
|
height,
|
|
582
839
|
fullscreen,
|
|
840
|
+
maximize,
|
|
583
841
|
resizable,
|
|
584
842
|
hide_title_bar: hideTitleBar,
|
|
585
843
|
activation_shortcut: activationShortcut,
|
|
586
844
|
always_on_top: alwaysOnTop,
|
|
587
845
|
dark_mode: darkMode,
|
|
588
846
|
disabled_web_shortcuts: disabledWebShortcuts,
|
|
589
|
-
hide_on_close:
|
|
847
|
+
hide_on_close: platformHideOnClose,
|
|
590
848
|
incognito: incognito,
|
|
591
849
|
title: title || null,
|
|
850
|
+
enable_wasm: wasm,
|
|
851
|
+
enable_drag_drop: enableDragDrop,
|
|
852
|
+
start_to_tray: startToTray && showSystemTray,
|
|
853
|
+
force_internal_navigation: forceInternalNavigation,
|
|
854
|
+
zoom,
|
|
855
|
+
min_width: minWidth,
|
|
856
|
+
min_height: minHeight,
|
|
857
|
+
ignore_certificate_errors: ignoreCertificateErrors,
|
|
592
858
|
};
|
|
593
859
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
594
860
|
tauriConf.productName = name;
|
|
595
861
|
tauriConf.identifier = identifier;
|
|
596
862
|
tauriConf.version = appVersion;
|
|
863
|
+
if (platform === 'linux') {
|
|
864
|
+
tauriConf.mainBinaryName = `pake-${generateIdentifierSafeName(name)}`;
|
|
865
|
+
}
|
|
597
866
|
if (platform == 'win32') {
|
|
598
867
|
tauriConf.bundle.windows.wix.language[0] = installerLanguage;
|
|
599
868
|
}
|
|
600
|
-
//Judge the type of URL, whether it is a file or a website.
|
|
601
869
|
const pathExists = await fsExtra.pathExists(url);
|
|
602
870
|
if (pathExists) {
|
|
603
871
|
logger.warn('✼ Your input might be a local file.');
|
|
@@ -639,64 +907,87 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
639
907
|
// Remove hardcoded desktop files and regenerate with correct app name
|
|
640
908
|
delete tauriConf.bundle.linux.deb.files;
|
|
641
909
|
// Generate correct desktop file configuration
|
|
642
|
-
const
|
|
643
|
-
const identifier = `com.pake.${
|
|
910
|
+
const appNameSafe = getSafeAppName(name);
|
|
911
|
+
const identifier = `com.pake.${appNameSafe}`;
|
|
644
912
|
const desktopFileName = `${identifier}.desktop`;
|
|
645
913
|
// Create desktop file content
|
|
914
|
+
// Determine if title contains Chinese characters for Name[zh_CN]
|
|
915
|
+
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
646
916
|
const desktopContent = `[Desktop Entry]
|
|
647
917
|
Version=1.0
|
|
648
918
|
Type=Application
|
|
649
919
|
Name=${name}
|
|
920
|
+
${chineseName ? `Name[zh_CN]=${chineseName}` : ''}
|
|
650
921
|
Comment=${name}
|
|
651
|
-
Exec
|
|
652
|
-
Icon=${
|
|
922
|
+
Exec=pake-${appNameSafe}
|
|
923
|
+
Icon=${appNameSafe}_512
|
|
653
924
|
Categories=Network;WebBrowser;
|
|
654
925
|
MimeType=text/html;text/xml;application/xhtml_xml;
|
|
655
926
|
StartupNotify=true
|
|
656
927
|
`;
|
|
657
|
-
// Write desktop file to assets directory
|
|
658
|
-
const
|
|
659
|
-
const
|
|
660
|
-
await fsExtra.ensureDir(
|
|
661
|
-
await fsExtra.writeFile(
|
|
928
|
+
// Write desktop file to src-tauri/assets directory where Tauri expects it
|
|
929
|
+
const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
|
|
930
|
+
const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
|
|
931
|
+
await fsExtra.ensureDir(srcAssetsDir);
|
|
932
|
+
await fsExtra.writeFile(srcDesktopFilePath, desktopContent);
|
|
662
933
|
// Set up desktop file in bundle configuration
|
|
934
|
+
// Use absolute path from src-tauri directory to assets
|
|
663
935
|
tauriConf.bundle.linux.deb.files = {
|
|
664
936
|
[`/usr/share/applications/${desktopFileName}`]: `assets/${desktopFileName}`,
|
|
665
937
|
};
|
|
666
|
-
const validTargets = [
|
|
938
|
+
const validTargets = [
|
|
939
|
+
'deb',
|
|
940
|
+
'appimage',
|
|
941
|
+
'rpm',
|
|
942
|
+
'deb-arm64',
|
|
943
|
+
'appimage-arm64',
|
|
944
|
+
'rpm-arm64',
|
|
945
|
+
];
|
|
946
|
+
const baseTarget = options.targets.includes('-arm64')
|
|
947
|
+
? options.targets.replace('-arm64', '')
|
|
948
|
+
: options.targets;
|
|
667
949
|
if (validTargets.includes(options.targets)) {
|
|
668
|
-
tauriConf.bundle.targets = [
|
|
950
|
+
tauriConf.bundle.targets = [baseTarget];
|
|
669
951
|
}
|
|
670
952
|
else {
|
|
671
953
|
logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`);
|
|
672
954
|
}
|
|
673
955
|
}
|
|
956
|
+
// Set macOS bundle targets (for app vs dmg)
|
|
957
|
+
if (platform === 'darwin') {
|
|
958
|
+
const validMacTargets = ['app', 'dmg'];
|
|
959
|
+
if (validMacTargets.includes(options.targets)) {
|
|
960
|
+
tauriConf.bundle.targets = [options.targets];
|
|
961
|
+
}
|
|
962
|
+
}
|
|
674
963
|
// Set icon.
|
|
964
|
+
const safeAppName = getSafeAppName(name);
|
|
675
965
|
const platformIconMap = {
|
|
676
966
|
win32: {
|
|
677
967
|
fileExt: '.ico',
|
|
678
|
-
path: `png/${
|
|
968
|
+
path: `png/${safeAppName}_256.ico`,
|
|
679
969
|
defaultIcon: 'png/icon_256.ico',
|
|
680
970
|
message: 'Windows icon must be .ico and 256x256px.',
|
|
681
971
|
},
|
|
682
972
|
linux: {
|
|
683
973
|
fileExt: '.png',
|
|
684
|
-
path: `png/${
|
|
974
|
+
path: `png/${safeAppName}_512.png`,
|
|
685
975
|
defaultIcon: 'png/icon_512.png',
|
|
686
976
|
message: 'Linux icon must be .png and 512x512px.',
|
|
687
977
|
},
|
|
688
978
|
darwin: {
|
|
689
979
|
fileExt: '.icns',
|
|
690
|
-
path: `icons/${
|
|
980
|
+
path: `icons/${safeAppName}.icns`,
|
|
691
981
|
defaultIcon: 'icons/icon.icns',
|
|
692
982
|
message: 'macOS icon must be .icns type.',
|
|
693
983
|
},
|
|
694
984
|
};
|
|
695
985
|
const iconInfo = platformIconMap[platform];
|
|
696
|
-
const
|
|
986
|
+
const resolvedIconPath = options.icon ? path.resolve(options.icon) : null;
|
|
987
|
+
const exists = resolvedIconPath && (await fsExtra.pathExists(resolvedIconPath));
|
|
697
988
|
if (exists) {
|
|
698
989
|
let updateIconPath = true;
|
|
699
|
-
let customIconExt = path.extname(
|
|
990
|
+
let customIconExt = path.extname(resolvedIconPath).toLowerCase();
|
|
700
991
|
if (customIconExt !== iconInfo.fileExt) {
|
|
701
992
|
updateIconPath = false;
|
|
702
993
|
logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`);
|
|
@@ -705,10 +996,14 @@ StartupNotify=true
|
|
|
705
996
|
else {
|
|
706
997
|
const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path);
|
|
707
998
|
tauriConf.bundle.resources = [iconInfo.path];
|
|
708
|
-
|
|
999
|
+
// Avoid copying if source and destination are the same
|
|
1000
|
+
const absoluteDestPath = path.resolve(iconPath);
|
|
1001
|
+
if (resolvedIconPath !== absoluteDestPath) {
|
|
1002
|
+
await fsExtra.copy(resolvedIconPath, iconPath);
|
|
1003
|
+
}
|
|
709
1004
|
}
|
|
710
1005
|
if (updateIconPath) {
|
|
711
|
-
tauriConf.bundle.icon = [
|
|
1006
|
+
tauriConf.bundle.icon = [iconInfo.path];
|
|
712
1007
|
}
|
|
713
1008
|
else {
|
|
714
1009
|
logger.warn(`✼ Icon will remain as default.`);
|
|
@@ -726,8 +1021,8 @@ StartupNotify=true
|
|
|
726
1021
|
// 需要判断图标格式,默认只支持ico和png两种
|
|
727
1022
|
let iconExt = path.extname(systemTrayIcon).toLowerCase();
|
|
728
1023
|
if (iconExt == '.png' || iconExt == '.ico') {
|
|
729
|
-
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${
|
|
730
|
-
trayIconPath = `png/${
|
|
1024
|
+
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
|
|
1025
|
+
trayIconPath = `png/${safeAppName}${iconExt}`;
|
|
731
1026
|
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
|
732
1027
|
}
|
|
733
1028
|
else {
|
|
@@ -746,11 +1041,13 @@ StartupNotify=true
|
|
|
746
1041
|
const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`);
|
|
747
1042
|
// inject js or css files
|
|
748
1043
|
if (inject?.length > 0) {
|
|
749
|
-
|
|
1044
|
+
// Ensure inject is an array before calling .every()
|
|
1045
|
+
const injectArray = Array.isArray(inject) ? inject : [inject];
|
|
1046
|
+
if (!injectArray.every((item) => item.endsWith('.css') || item.endsWith('.js'))) {
|
|
750
1047
|
logger.error('The injected file must be in either CSS or JS format.');
|
|
751
1048
|
return;
|
|
752
1049
|
}
|
|
753
|
-
const files =
|
|
1050
|
+
const files = injectArray.map((filepath) => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath));
|
|
754
1051
|
tauriConf.pake.inject = files;
|
|
755
1052
|
await combineFiles(files, injectFilePath);
|
|
756
1053
|
}
|
|
@@ -759,6 +1056,16 @@ StartupNotify=true
|
|
|
759
1056
|
await fsExtra.writeFile(injectFilePath, '');
|
|
760
1057
|
}
|
|
761
1058
|
tauriConf.pake.proxy_url = proxyUrl || '';
|
|
1059
|
+
tauriConf.pake.multi_instance = multiInstance;
|
|
1060
|
+
// Configure WASM support with required HTTP headers
|
|
1061
|
+
if (wasm) {
|
|
1062
|
+
tauriConf.app.security = {
|
|
1063
|
+
headers: {
|
|
1064
|
+
'Cross-Origin-Opener-Policy': 'same-origin',
|
|
1065
|
+
'Cross-Origin-Embedder-Policy': 'require-corp',
|
|
1066
|
+
},
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
762
1069
|
// Save config file.
|
|
763
1070
|
const platformConfigPaths = {
|
|
764
1071
|
win32: 'tauri.windows.conf.json',
|
|
@@ -794,10 +1101,34 @@ class BaseBuilder {
|
|
|
794
1101
|
: undefined;
|
|
795
1102
|
}
|
|
796
1103
|
getInstallTimeout() {
|
|
797
|
-
|
|
1104
|
+
// Windows needs more time due to native compilation and antivirus scanning
|
|
1105
|
+
return process.platform === 'win32' ? 900000 : 600000;
|
|
798
1106
|
}
|
|
799
1107
|
getBuildTimeout() {
|
|
800
|
-
return 900000;
|
|
1108
|
+
return 900000;
|
|
1109
|
+
}
|
|
1110
|
+
async detectPackageManager() {
|
|
1111
|
+
if (BaseBuilder.packageManagerCache) {
|
|
1112
|
+
return BaseBuilder.packageManagerCache;
|
|
1113
|
+
}
|
|
1114
|
+
const { execa } = await import('execa');
|
|
1115
|
+
try {
|
|
1116
|
+
await execa('pnpm', ['--version'], { stdio: 'ignore' });
|
|
1117
|
+
logger.info('✺ Using pnpm for package management.');
|
|
1118
|
+
BaseBuilder.packageManagerCache = 'pnpm';
|
|
1119
|
+
return 'pnpm';
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
try {
|
|
1123
|
+
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
1124
|
+
logger.info('✺ pnpm not available, using npm for package management.');
|
|
1125
|
+
BaseBuilder.packageManagerCache = 'npm';
|
|
1126
|
+
return 'npm';
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
801
1132
|
}
|
|
802
1133
|
async prepare() {
|
|
803
1134
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
@@ -807,6 +1138,7 @@ class BaseBuilder {
|
|
|
807
1138
|
logger.warn('✼ The first use requires installing system dependencies.');
|
|
808
1139
|
logger.warn('✼ See more in https://tauri.app/start/prerequisites/.');
|
|
809
1140
|
}
|
|
1141
|
+
ensureRustEnv();
|
|
810
1142
|
if (!checkRustInstalled()) {
|
|
811
1143
|
const res = await prompts({
|
|
812
1144
|
type: 'confirm',
|
|
@@ -826,24 +1158,56 @@ class BaseBuilder {
|
|
|
826
1158
|
const rustProjectDir = path.join(tauriSrcPath, '.cargo');
|
|
827
1159
|
const projectConf = path.join(rustProjectDir, 'config.toml');
|
|
828
1160
|
await fsExtra.ensureDir(rustProjectDir);
|
|
829
|
-
//
|
|
830
|
-
const packageManager =
|
|
831
|
-
const registryOption =
|
|
832
|
-
|
|
833
|
-
: '';
|
|
834
|
-
const legacyPeerDeps = ' --legacy-peer-deps'; // 解决dependency conflicts
|
|
1161
|
+
// Detect available package manager
|
|
1162
|
+
const packageManager = await this.detectPackageManager();
|
|
1163
|
+
const registryOption = ' --registry=https://registry.npmmirror.com';
|
|
1164
|
+
const peerDepsOption = packageManager === 'npm' ? ' --legacy-peer-deps' : '';
|
|
835
1165
|
const timeout = this.getInstallTimeout();
|
|
836
1166
|
const buildEnv = this.getBuildEnvironment();
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1167
|
+
// Show helpful message for first-time users
|
|
1168
|
+
if (!tauriTargetPathExists) {
|
|
1169
|
+
logger.info(process.platform === 'win32'
|
|
1170
|
+
? '✺ First-time setup may take 10-15 minutes on Windows (compiling dependencies)...'
|
|
1171
|
+
: '✺ First-time setup may take 5-10 minutes (installing dependencies)...');
|
|
842
1172
|
}
|
|
843
|
-
|
|
844
|
-
|
|
1173
|
+
let usedMirror = isChina;
|
|
1174
|
+
try {
|
|
1175
|
+
if (isChina) {
|
|
1176
|
+
logger.info(`✺ Located in China, using ${packageManager}/rsProxy CN mirror.`);
|
|
1177
|
+
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
1178
|
+
await fsExtra.copy(projectCnConf, projectConf);
|
|
1179
|
+
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, buildEnv);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, buildEnv);
|
|
1183
|
+
}
|
|
1184
|
+
spinner.succeed(chalk.green('Package installed!'));
|
|
1185
|
+
}
|
|
1186
|
+
catch (error) {
|
|
1187
|
+
// If installation times out and we haven't tried the mirror yet, retry with mirror
|
|
1188
|
+
if (error instanceof Error &&
|
|
1189
|
+
error.message.includes('timed out') &&
|
|
1190
|
+
!usedMirror) {
|
|
1191
|
+
spinner.fail(chalk.yellow('Installation timed out, retrying with CN mirror...'));
|
|
1192
|
+
logger.info('✺ Retrying installation with CN mirror for better speed...');
|
|
1193
|
+
const retrySpinner = getSpinner('Retrying installation...');
|
|
1194
|
+
usedMirror = true;
|
|
1195
|
+
try {
|
|
1196
|
+
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
1197
|
+
await fsExtra.copy(projectCnConf, projectConf);
|
|
1198
|
+
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, buildEnv);
|
|
1199
|
+
retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
|
|
1200
|
+
}
|
|
1201
|
+
catch (retryError) {
|
|
1202
|
+
retrySpinner.fail(chalk.red('Installation failed'));
|
|
1203
|
+
throw retryError;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
spinner.fail(chalk.red('Installation failed'));
|
|
1208
|
+
throw error;
|
|
1209
|
+
}
|
|
845
1210
|
}
|
|
846
|
-
spinner.succeed(chalk.green('Package installed!'));
|
|
847
1211
|
if (!tauriTargetPathExists) {
|
|
848
1212
|
logger.warn('✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.');
|
|
849
1213
|
}
|
|
@@ -852,45 +1216,146 @@ class BaseBuilder {
|
|
|
852
1216
|
await this.buildAndCopy(url, this.options.targets);
|
|
853
1217
|
}
|
|
854
1218
|
async start(url) {
|
|
1219
|
+
logger.info('Pake dev server starting...');
|
|
855
1220
|
await mergeConfig(url, this.options, tauriConfig);
|
|
1221
|
+
const packageManager = await this.detectPackageManager();
|
|
1222
|
+
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
|
1223
|
+
const features = this.getBuildFeatures();
|
|
1224
|
+
const featureArgs = features.length > 0 ? `--features ${features.join(',')}` : '';
|
|
1225
|
+
const argSeparator = packageManager === 'npm' ? ' --' : '';
|
|
1226
|
+
const command = `cd "${npmDirectory}" && ${packageManager} run tauri${argSeparator} dev --config "${configPath}" ${featureArgs}`;
|
|
1227
|
+
await shellExec(command);
|
|
856
1228
|
}
|
|
857
1229
|
async buildAndCopy(url, target) {
|
|
858
|
-
const { name } = this.options;
|
|
1230
|
+
const { name = 'pake-app' } = this.options;
|
|
859
1231
|
await mergeConfig(url, this.options, tauriConfig);
|
|
1232
|
+
// Detect available package manager
|
|
1233
|
+
const packageManager = await this.detectPackageManager();
|
|
860
1234
|
// Build app
|
|
861
1235
|
const buildSpinner = getSpinner('Building app...');
|
|
862
|
-
// Let spinner run for a moment so user can see it, then stop before
|
|
1236
|
+
// Let spinner run for a moment so user can see it, then stop before package manager command
|
|
863
1237
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
864
1238
|
buildSpinner.stop();
|
|
865
1239
|
// Show static message to keep the status visible
|
|
866
1240
|
logger.warn('✸ Building app...');
|
|
867
|
-
const
|
|
868
|
-
|
|
1241
|
+
const baseEnv = this.getBuildEnvironment();
|
|
1242
|
+
let buildEnv = {
|
|
1243
|
+
...(baseEnv ?? {}),
|
|
1244
|
+
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
1245
|
+
};
|
|
1246
|
+
const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined;
|
|
1247
|
+
// Warn users about potential AppImage build failures on modern Linux systems.
|
|
1248
|
+
// The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't
|
|
1249
|
+
// recognize the .relr.dyn section introduced in glibc 2.38+.
|
|
1250
|
+
if (process.platform === 'linux' && this.options.targets === 'appimage') {
|
|
1251
|
+
if (!buildEnv.NO_STRIP) {
|
|
1252
|
+
logger.warn('⚠ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+');
|
|
1253
|
+
logger.warn('⚠ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage');
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
|
|
1257
|
+
const buildTimeout = this.getBuildTimeout();
|
|
1258
|
+
try {
|
|
1259
|
+
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
const shouldRetryWithoutStrip = process.platform === 'linux' &&
|
|
1263
|
+
this.options.targets === 'appimage' &&
|
|
1264
|
+
!buildEnv.NO_STRIP &&
|
|
1265
|
+
this.isLinuxDeployStripError(error);
|
|
1266
|
+
if (shouldRetryWithoutStrip) {
|
|
1267
|
+
logger.warn('⚠ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
|
|
1268
|
+
buildEnv = {
|
|
1269
|
+
...buildEnv,
|
|
1270
|
+
NO_STRIP: '1',
|
|
1271
|
+
};
|
|
1272
|
+
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
throw error;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
869
1278
|
// Copy app
|
|
870
1279
|
const fileName = this.getFileName();
|
|
871
1280
|
const fileType = this.getFileType(target);
|
|
872
1281
|
const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType);
|
|
873
1282
|
const distPath = path.resolve(`${name}.${fileType}`);
|
|
874
1283
|
await fsExtra.copy(appPath, distPath);
|
|
1284
|
+
// Copy raw binary if requested
|
|
1285
|
+
if (this.options.keepBinary) {
|
|
1286
|
+
await this.copyRawBinary(npmDirectory, name);
|
|
1287
|
+
}
|
|
875
1288
|
await fsExtra.remove(appPath);
|
|
876
1289
|
logger.success('✔ Build success!');
|
|
877
1290
|
logger.success('✔ App installer located in', distPath);
|
|
1291
|
+
// Log binary location if preserved
|
|
1292
|
+
if (this.options.keepBinary) {
|
|
1293
|
+
const binaryPath = this.getRawBinaryPath(name);
|
|
1294
|
+
logger.success('✔ Raw binary located in', path.resolve(binaryPath));
|
|
1295
|
+
}
|
|
878
1296
|
}
|
|
879
1297
|
getFileType(target) {
|
|
880
1298
|
return target;
|
|
881
1299
|
}
|
|
882
|
-
|
|
1300
|
+
isLinuxDeployStripError(error) {
|
|
1301
|
+
if (!(error instanceof Error) || !error.message) {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
const message = error.message.toLowerCase();
|
|
1305
|
+
return (message.includes('linuxdeploy') ||
|
|
1306
|
+
message.includes('failed to run linuxdeploy') ||
|
|
1307
|
+
message.includes('strip:') ||
|
|
1308
|
+
message.includes('unable to recognise the format of the input file') ||
|
|
1309
|
+
message.includes('appimage tool failed') ||
|
|
1310
|
+
message.includes('strip tool'));
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* 解析目标架构
|
|
1314
|
+
*/
|
|
1315
|
+
resolveTargetArch(requestedArch) {
|
|
1316
|
+
if (requestedArch === 'auto' || !requestedArch) {
|
|
1317
|
+
return process.arch;
|
|
1318
|
+
}
|
|
1319
|
+
return requestedArch;
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* 获取Tauri构建目标
|
|
1323
|
+
*/
|
|
1324
|
+
getTauriTarget(arch, platform = process.platform) {
|
|
1325
|
+
const platformMappings = BaseBuilder.ARCH_MAPPINGS[platform];
|
|
1326
|
+
if (!platformMappings)
|
|
1327
|
+
return null;
|
|
1328
|
+
return platformMappings[arch] || null;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* 获取架构显示名称(用于文件名)
|
|
1332
|
+
*/
|
|
1333
|
+
getArchDisplayName(arch) {
|
|
1334
|
+
return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* 构建基础构建命令
|
|
1338
|
+
*/
|
|
1339
|
+
buildBaseCommand(packageManager, configPath, target) {
|
|
883
1340
|
const baseCommand = this.options.debug
|
|
884
|
-
?
|
|
885
|
-
:
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
if (IS_MAC && this.options.targets === 'app') {
|
|
891
|
-
fullCommand += ' --bundles app';
|
|
1341
|
+
? `${packageManager} run build:debug`
|
|
1342
|
+
: `${packageManager} run build`;
|
|
1343
|
+
const argSeparator = packageManager === 'npm' ? ' --' : '';
|
|
1344
|
+
let fullCommand = `${baseCommand}${argSeparator} -c "${configPath}"`;
|
|
1345
|
+
if (target) {
|
|
1346
|
+
fullCommand += ` --target ${target}`;
|
|
892
1347
|
}
|
|
893
|
-
//
|
|
1348
|
+
// Enable verbose output in debug mode to help diagnose build issues.
|
|
1349
|
+
// This provides detailed logs from Tauri CLI and bundler tools.
|
|
1350
|
+
if (this.options.debug) {
|
|
1351
|
+
fullCommand += ' --verbose';
|
|
1352
|
+
}
|
|
1353
|
+
return fullCommand;
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* 获取构建特性列表
|
|
1357
|
+
*/
|
|
1358
|
+
getBuildFeatures() {
|
|
894
1359
|
const features = ['cli-build'];
|
|
895
1360
|
// Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
|
|
896
1361
|
if (IS_MAC) {
|
|
@@ -899,6 +1364,18 @@ class BaseBuilder {
|
|
|
899
1364
|
features.push('macos-proxy');
|
|
900
1365
|
}
|
|
901
1366
|
}
|
|
1367
|
+
return features;
|
|
1368
|
+
}
|
|
1369
|
+
getBuildCommand(packageManager = 'pnpm') {
|
|
1370
|
+
// Use temporary config directory to avoid modifying source files
|
|
1371
|
+
const configPath = path.join(npmDirectory, 'src-tauri', '.pake', 'tauri.conf.json');
|
|
1372
|
+
let fullCommand = this.buildBaseCommand(packageManager, configPath);
|
|
1373
|
+
// For macOS, use app bundles by default unless DMG is explicitly requested
|
|
1374
|
+
if (IS_MAC && this.options.targets === 'app') {
|
|
1375
|
+
fullCommand += ' --bundles app';
|
|
1376
|
+
}
|
|
1377
|
+
// Add features
|
|
1378
|
+
const features = this.getBuildFeatures();
|
|
902
1379
|
if (features.length > 0) {
|
|
903
1380
|
fullCommand += ` --features ${features.join(',')}`;
|
|
904
1381
|
}
|
|
@@ -924,96 +1401,250 @@ class BaseBuilder {
|
|
|
924
1401
|
const bundleDir = fileType.toLowerCase() === 'app' ? 'macos' : fileType.toLowerCase();
|
|
925
1402
|
return path.join(npmDirectory, this.getBasePath(), bundleDir, `${fileName}.${fileType}`);
|
|
926
1403
|
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Copy raw binary file to output directory
|
|
1406
|
+
*/
|
|
1407
|
+
async copyRawBinary(npmDirectory, appName) {
|
|
1408
|
+
const binaryPath = this.getRawBinarySourcePath(npmDirectory, appName);
|
|
1409
|
+
const outputPath = this.getRawBinaryPath(appName);
|
|
1410
|
+
if (await fsExtra.pathExists(binaryPath)) {
|
|
1411
|
+
await fsExtra.copy(binaryPath, outputPath);
|
|
1412
|
+
// Make binary executable on Unix-like systems
|
|
1413
|
+
if (process.platform !== 'win32') {
|
|
1414
|
+
await fsExtra.chmod(outputPath, 0o755);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
logger.warn(`✼ Raw binary not found at ${binaryPath}, skipping...`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Get the source path of the raw binary file in the build directory
|
|
1423
|
+
*/
|
|
1424
|
+
getRawBinarySourcePath(npmDirectory, appName) {
|
|
1425
|
+
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1426
|
+
const binaryName = this.getBinaryName(appName);
|
|
1427
|
+
// Handle cross-platform builds
|
|
1428
|
+
if (this.options.multiArch || this.hasArchSpecificTarget()) {
|
|
1429
|
+
return path.join(npmDirectory, this.getArchSpecificPath(), basePath, binaryName);
|
|
1430
|
+
}
|
|
1431
|
+
return path.join(npmDirectory, 'src-tauri/target', basePath, binaryName);
|
|
1432
|
+
}
|
|
1433
|
+
/**
|
|
1434
|
+
* Get the output path for the raw binary file
|
|
1435
|
+
*/
|
|
1436
|
+
getRawBinaryPath(appName) {
|
|
1437
|
+
const extension = process.platform === 'win32' ? '.exe' : '';
|
|
1438
|
+
const suffix = process.platform === 'win32' ? '' : '-binary';
|
|
1439
|
+
return `${appName}${suffix}${extension}`;
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Get the binary name based on app name and platform
|
|
1443
|
+
*/
|
|
1444
|
+
getBinaryName(appName) {
|
|
1445
|
+
const extension = process.platform === 'win32' ? '.exe' : '';
|
|
1446
|
+
// Linux uses the unique binary name we set in merge.ts
|
|
1447
|
+
if (process.platform === 'linux') {
|
|
1448
|
+
return `pake-${generateIdentifierSafeName(appName)}${extension}`;
|
|
1449
|
+
}
|
|
1450
|
+
// Windows and macOS use 'pake' as binary name
|
|
1451
|
+
return `pake${extension}`;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Check if this build has architecture-specific target
|
|
1455
|
+
*/
|
|
1456
|
+
hasArchSpecificTarget() {
|
|
1457
|
+
return false; // Override in subclasses if needed
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Get architecture-specific path for binary
|
|
1461
|
+
*/
|
|
1462
|
+
getArchSpecificPath() {
|
|
1463
|
+
return 'src-tauri/target'; // Override in subclasses if needed
|
|
1464
|
+
}
|
|
927
1465
|
}
|
|
1466
|
+
BaseBuilder.packageManagerCache = null;
|
|
1467
|
+
// 架构映射配置
|
|
1468
|
+
BaseBuilder.ARCH_MAPPINGS = {
|
|
1469
|
+
darwin: {
|
|
1470
|
+
arm64: 'aarch64-apple-darwin',
|
|
1471
|
+
x64: 'x86_64-apple-darwin',
|
|
1472
|
+
universal: 'universal-apple-darwin',
|
|
1473
|
+
},
|
|
1474
|
+
win32: {
|
|
1475
|
+
arm64: 'aarch64-pc-windows-msvc',
|
|
1476
|
+
x64: 'x86_64-pc-windows-msvc',
|
|
1477
|
+
},
|
|
1478
|
+
linux: {
|
|
1479
|
+
arm64: 'aarch64-unknown-linux-gnu',
|
|
1480
|
+
x64: 'x86_64-unknown-linux-gnu',
|
|
1481
|
+
},
|
|
1482
|
+
};
|
|
1483
|
+
// 架构名称映射(用于文件名生成)
|
|
1484
|
+
BaseBuilder.ARCH_DISPLAY_NAMES = {
|
|
1485
|
+
arm64: 'aarch64',
|
|
1486
|
+
x64: 'x64',
|
|
1487
|
+
universal: 'universal',
|
|
1488
|
+
};
|
|
928
1489
|
|
|
929
1490
|
class MacBuilder extends BaseBuilder {
|
|
930
1491
|
constructor(options) {
|
|
931
1492
|
super(options);
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1493
|
+
const validArchs = ['intel', 'apple', 'universal', 'auto', 'x64', 'arm64'];
|
|
1494
|
+
this.buildArch = validArchs.includes(options.targets || '')
|
|
1495
|
+
? options.targets
|
|
1496
|
+
: 'auto';
|
|
1497
|
+
if (options.iterativeBuild || process.env.PAKE_CREATE_APP === '1') {
|
|
1498
|
+
this.buildFormat = 'app';
|
|
936
1499
|
}
|
|
937
1500
|
else {
|
|
938
|
-
this.
|
|
1501
|
+
this.buildFormat = 'dmg';
|
|
939
1502
|
}
|
|
1503
|
+
this.options.targets = this.buildFormat;
|
|
940
1504
|
}
|
|
941
1505
|
getFileName() {
|
|
942
|
-
const { name } = this.options;
|
|
943
|
-
|
|
944
|
-
if (this.options.targets === 'app') {
|
|
1506
|
+
const { name = 'pake-app' } = this.options;
|
|
1507
|
+
if (this.buildFormat === 'app') {
|
|
945
1508
|
return name;
|
|
946
1509
|
}
|
|
947
|
-
// For DMG files, use versioned filename
|
|
948
1510
|
let arch;
|
|
949
|
-
if (this.options.multiArch) {
|
|
1511
|
+
if (this.buildArch === 'universal' || this.options.multiArch) {
|
|
950
1512
|
arch = 'universal';
|
|
951
1513
|
}
|
|
1514
|
+
else if (this.buildArch === 'apple') {
|
|
1515
|
+
arch = 'aarch64';
|
|
1516
|
+
}
|
|
1517
|
+
else if (this.buildArch === 'intel') {
|
|
1518
|
+
arch = 'x64';
|
|
1519
|
+
}
|
|
952
1520
|
else {
|
|
953
|
-
arch =
|
|
1521
|
+
arch = this.getArchDisplayName(this.resolveTargetArch(this.buildArch));
|
|
954
1522
|
}
|
|
955
1523
|
return `${name}_${tauriConfig.version}_${arch}`;
|
|
956
1524
|
}
|
|
957
|
-
|
|
958
|
-
if (this.options.multiArch) {
|
|
959
|
-
|
|
960
|
-
? 'npm run tauri build -- --debug'
|
|
961
|
-
: 'npm run tauri build --';
|
|
962
|
-
// Use temporary config directory to avoid modifying source files
|
|
963
|
-
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
|
|
964
|
-
let fullCommand = `${baseCommand} --target universal-apple-darwin -c "${configPath}"`;
|
|
965
|
-
// Add features
|
|
966
|
-
const features = ['cli-build'];
|
|
967
|
-
// Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
|
|
968
|
-
const macOSVersion = this.getMacOSMajorVersion();
|
|
969
|
-
if (macOSVersion >= 23) {
|
|
970
|
-
features.push('macos-proxy');
|
|
971
|
-
}
|
|
972
|
-
if (features.length > 0) {
|
|
973
|
-
fullCommand += ` --features ${features.join(',')}`;
|
|
974
|
-
}
|
|
975
|
-
return fullCommand;
|
|
1525
|
+
getActualArch() {
|
|
1526
|
+
if (this.buildArch === 'universal' || this.options.multiArch) {
|
|
1527
|
+
return 'universal';
|
|
976
1528
|
}
|
|
977
|
-
|
|
1529
|
+
else if (this.buildArch === 'apple') {
|
|
1530
|
+
return 'arm64';
|
|
1531
|
+
}
|
|
1532
|
+
else if (this.buildArch === 'intel') {
|
|
1533
|
+
return 'x64';
|
|
1534
|
+
}
|
|
1535
|
+
return this.resolveTargetArch(this.buildArch);
|
|
1536
|
+
}
|
|
1537
|
+
getBuildCommand(packageManager = 'pnpm') {
|
|
1538
|
+
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
|
|
1539
|
+
const actualArch = this.getActualArch();
|
|
1540
|
+
const buildTarget = this.getTauriTarget(actualArch, 'darwin');
|
|
1541
|
+
if (!buildTarget) {
|
|
1542
|
+
throw new Error(`Unsupported architecture: ${actualArch} for macOS`);
|
|
1543
|
+
}
|
|
1544
|
+
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1545
|
+
const features = this.getBuildFeatures();
|
|
1546
|
+
if (features.length > 0) {
|
|
1547
|
+
fullCommand += ` --features ${features.join(',')}`;
|
|
1548
|
+
}
|
|
1549
|
+
return fullCommand;
|
|
978
1550
|
}
|
|
979
1551
|
getBasePath() {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1552
|
+
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1553
|
+
const actualArch = this.getActualArch();
|
|
1554
|
+
const target = this.getTauriTarget(actualArch, 'darwin');
|
|
1555
|
+
return `src-tauri/target/${target}/${basePath}/bundle`;
|
|
1556
|
+
}
|
|
1557
|
+
hasArchSpecificTarget() {
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
getArchSpecificPath() {
|
|
1561
|
+
const actualArch = this.getActualArch();
|
|
1562
|
+
const target = this.getTauriTarget(actualArch, 'darwin');
|
|
1563
|
+
return `src-tauri/target/${target}`;
|
|
983
1564
|
}
|
|
984
1565
|
}
|
|
985
1566
|
|
|
986
1567
|
class WinBuilder extends BaseBuilder {
|
|
987
1568
|
constructor(options) {
|
|
988
1569
|
super(options);
|
|
989
|
-
this.
|
|
1570
|
+
this.buildFormat = 'msi';
|
|
1571
|
+
const validArchs = ['x64', 'arm64', 'auto'];
|
|
1572
|
+
this.buildArch = validArchs.includes(options.targets || '')
|
|
1573
|
+
? this.resolveTargetArch(options.targets)
|
|
1574
|
+
: this.resolveTargetArch('auto');
|
|
1575
|
+
this.options.targets = this.buildFormat;
|
|
990
1576
|
}
|
|
991
1577
|
getFileName() {
|
|
992
1578
|
const { name } = this.options;
|
|
993
|
-
const { arch } = process;
|
|
994
1579
|
const language = tauriConfig.bundle.windows.wix.language[0];
|
|
995
|
-
|
|
1580
|
+
const targetArch = this.getArchDisplayName(this.buildArch);
|
|
1581
|
+
return `${name}_${tauriConfig.version}_${targetArch}_${language}`;
|
|
1582
|
+
}
|
|
1583
|
+
getBuildCommand(packageManager = 'pnpm') {
|
|
1584
|
+
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
|
|
1585
|
+
const buildTarget = this.getTauriTarget(this.buildArch, 'win32');
|
|
1586
|
+
if (!buildTarget) {
|
|
1587
|
+
throw new Error(`Unsupported architecture: ${this.buildArch} for Windows`);
|
|
1588
|
+
}
|
|
1589
|
+
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1590
|
+
const features = this.getBuildFeatures();
|
|
1591
|
+
if (features.length > 0) {
|
|
1592
|
+
fullCommand += ` --features ${features.join(',')}`;
|
|
1593
|
+
}
|
|
1594
|
+
return fullCommand;
|
|
1595
|
+
}
|
|
1596
|
+
getBasePath() {
|
|
1597
|
+
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1598
|
+
const target = this.getTauriTarget(this.buildArch, 'win32');
|
|
1599
|
+
return `src-tauri/target/${target}/${basePath}/bundle/`;
|
|
1600
|
+
}
|
|
1601
|
+
hasArchSpecificTarget() {
|
|
1602
|
+
return true;
|
|
1603
|
+
}
|
|
1604
|
+
getArchSpecificPath() {
|
|
1605
|
+
const target = this.getTauriTarget(this.buildArch, 'win32');
|
|
1606
|
+
return `src-tauri/target/${target}`;
|
|
996
1607
|
}
|
|
997
1608
|
}
|
|
998
1609
|
|
|
999
1610
|
class LinuxBuilder extends BaseBuilder {
|
|
1000
1611
|
constructor(options) {
|
|
1001
1612
|
super(options);
|
|
1613
|
+
const target = options.targets || 'deb';
|
|
1614
|
+
if (target.includes('-arm64')) {
|
|
1615
|
+
this.buildFormat = target.replace('-arm64', '');
|
|
1616
|
+
this.buildArch = 'arm64';
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
this.buildFormat = target;
|
|
1620
|
+
this.buildArch = this.resolveTargetArch('auto');
|
|
1621
|
+
}
|
|
1622
|
+
this.options.targets = this.buildFormat;
|
|
1002
1623
|
}
|
|
1003
1624
|
getFileName() {
|
|
1004
|
-
const { name, targets } = this.options;
|
|
1625
|
+
const { name = 'pake-app', targets } = this.options;
|
|
1005
1626
|
const version = tauriConfig.version;
|
|
1006
|
-
let arch
|
|
1007
|
-
if (
|
|
1008
|
-
arch = 'aarch64';
|
|
1627
|
+
let arch;
|
|
1628
|
+
if (this.buildArch === 'arm64') {
|
|
1629
|
+
arch = targets === 'rpm' || targets === 'appimage' ? 'aarch64' : 'arm64';
|
|
1630
|
+
}
|
|
1631
|
+
else {
|
|
1632
|
+
if (this.buildArch === 'x64') {
|
|
1633
|
+
arch = targets === 'rpm' ? 'x86_64' : 'amd64';
|
|
1634
|
+
}
|
|
1635
|
+
else {
|
|
1636
|
+
arch = this.buildArch;
|
|
1637
|
+
if (this.buildArch === 'arm64' &&
|
|
1638
|
+
(targets === 'rpm' || targets === 'appimage')) {
|
|
1639
|
+
arch = 'aarch64';
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1009
1642
|
}
|
|
1010
|
-
// The RPM format uses different separators and version number formats
|
|
1011
1643
|
if (targets === 'rpm') {
|
|
1012
1644
|
return `${name}-${version}-1.${arch}`;
|
|
1013
1645
|
}
|
|
1014
1646
|
return `${name}_${version}_${arch}`;
|
|
1015
1647
|
}
|
|
1016
|
-
// Customize it, considering that there are all targets.
|
|
1017
1648
|
async build(url) {
|
|
1018
1649
|
const targetTypes = ['deb', 'appimage', 'rpm'];
|
|
1019
1650
|
for (const target of targetTypes) {
|
|
@@ -1022,12 +1653,49 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1022
1653
|
}
|
|
1023
1654
|
}
|
|
1024
1655
|
}
|
|
1656
|
+
getBuildCommand(packageManager = 'pnpm') {
|
|
1657
|
+
const configPath = path.join('src-tauri', '.pake', 'tauri.conf.json');
|
|
1658
|
+
const buildTarget = this.buildArch === 'arm64'
|
|
1659
|
+
? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
|
|
1660
|
+
: undefined;
|
|
1661
|
+
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1662
|
+
const features = this.getBuildFeatures();
|
|
1663
|
+
if (features.length > 0) {
|
|
1664
|
+
fullCommand += ` --features ${features.join(',')}`;
|
|
1665
|
+
}
|
|
1666
|
+
// Enable verbose output for AppImage builds when debugging or PAKE_VERBOSE is set.
|
|
1667
|
+
// AppImage builds often fail with minimal error messages from linuxdeploy,
|
|
1668
|
+
// so verbose mode helps diagnose issues like strip failures and missing dependencies.
|
|
1669
|
+
if (this.options.targets === 'appimage' &&
|
|
1670
|
+
(this.options.debug || process.env.PAKE_VERBOSE)) {
|
|
1671
|
+
fullCommand += ' --verbose';
|
|
1672
|
+
}
|
|
1673
|
+
return fullCommand;
|
|
1674
|
+
}
|
|
1675
|
+
getBasePath() {
|
|
1676
|
+
const basePath = this.options.debug ? 'debug' : 'release';
|
|
1677
|
+
if (this.buildArch === 'arm64') {
|
|
1678
|
+
const target = this.getTauriTarget(this.buildArch, 'linux');
|
|
1679
|
+
return `src-tauri/target/${target}/${basePath}/bundle/`;
|
|
1680
|
+
}
|
|
1681
|
+
return super.getBasePath();
|
|
1682
|
+
}
|
|
1025
1683
|
getFileType(target) {
|
|
1026
1684
|
if (target === 'appimage') {
|
|
1027
1685
|
return 'AppImage';
|
|
1028
1686
|
}
|
|
1029
1687
|
return super.getFileType(target);
|
|
1030
1688
|
}
|
|
1689
|
+
hasArchSpecificTarget() {
|
|
1690
|
+
return this.buildArch === 'arm64';
|
|
1691
|
+
}
|
|
1692
|
+
getArchSpecificPath() {
|
|
1693
|
+
if (this.buildArch === 'arm64') {
|
|
1694
|
+
const target = this.getTauriTarget(this.buildArch, 'linux');
|
|
1695
|
+
return `src-tauri/target/${target}`;
|
|
1696
|
+
}
|
|
1697
|
+
return super.getArchSpecificPath();
|
|
1698
|
+
}
|
|
1031
1699
|
}
|
|
1032
1700
|
|
|
1033
1701
|
const { platform } = process;
|
|
@@ -1046,13 +1714,230 @@ class BuilderProvider {
|
|
|
1046
1714
|
}
|
|
1047
1715
|
}
|
|
1048
1716
|
|
|
1049
|
-
|
|
1717
|
+
var version = "3.6.1";
|
|
1718
|
+
var packageJson = {
|
|
1719
|
+
version: version};
|
|
1720
|
+
|
|
1721
|
+
const DEFAULT_PAKE_OPTIONS = {
|
|
1722
|
+
icon: '',
|
|
1723
|
+
height: 780,
|
|
1724
|
+
width: 1200,
|
|
1725
|
+
fullscreen: false,
|
|
1726
|
+
maximize: false,
|
|
1727
|
+
hideTitleBar: false,
|
|
1728
|
+
alwaysOnTop: false,
|
|
1729
|
+
appVersion: '1.0.0',
|
|
1730
|
+
darkMode: false,
|
|
1731
|
+
disabledWebShortcuts: false,
|
|
1732
|
+
activationShortcut: '',
|
|
1733
|
+
userAgent: '',
|
|
1734
|
+
showSystemTray: false,
|
|
1735
|
+
multiArch: false,
|
|
1736
|
+
targets: (() => {
|
|
1737
|
+
switch (process.platform) {
|
|
1738
|
+
case 'linux':
|
|
1739
|
+
return 'deb';
|
|
1740
|
+
case 'darwin':
|
|
1741
|
+
return 'dmg';
|
|
1742
|
+
case 'win32':
|
|
1743
|
+
return 'msi';
|
|
1744
|
+
default:
|
|
1745
|
+
return 'deb';
|
|
1746
|
+
}
|
|
1747
|
+
})(),
|
|
1748
|
+
useLocalFile: false,
|
|
1749
|
+
systemTrayIcon: '',
|
|
1750
|
+
proxyUrl: '',
|
|
1751
|
+
debug: false,
|
|
1752
|
+
inject: [],
|
|
1753
|
+
installerLanguage: 'en-US',
|
|
1754
|
+
hideOnClose: undefined, // Platform-specific: true for macOS, false for others
|
|
1755
|
+
incognito: false,
|
|
1756
|
+
wasm: false,
|
|
1757
|
+
enableDragDrop: false,
|
|
1758
|
+
keepBinary: false,
|
|
1759
|
+
multiInstance: false,
|
|
1760
|
+
startToTray: false,
|
|
1761
|
+
forceInternalNavigation: false,
|
|
1762
|
+
iterativeBuild: false,
|
|
1763
|
+
zoom: 100,
|
|
1764
|
+
minWidth: 0,
|
|
1765
|
+
minHeight: 0,
|
|
1766
|
+
ignoreCertificateErrors: false,
|
|
1767
|
+
};
|
|
1768
|
+
|
|
1769
|
+
function validateNumberInput(value) {
|
|
1770
|
+
const parsedValue = Number(value);
|
|
1771
|
+
if (isNaN(parsedValue)) {
|
|
1772
|
+
throw new InvalidArgumentError('Not a number.');
|
|
1773
|
+
}
|
|
1774
|
+
return parsedValue;
|
|
1775
|
+
}
|
|
1776
|
+
function validateUrlInput(url) {
|
|
1777
|
+
const isFile = fs.existsSync(url);
|
|
1778
|
+
if (!isFile) {
|
|
1779
|
+
try {
|
|
1780
|
+
return normalizeUrl(url);
|
|
1781
|
+
}
|
|
1782
|
+
catch (error) {
|
|
1783
|
+
if (error instanceof Error) {
|
|
1784
|
+
throw new InvalidArgumentError(error.message);
|
|
1785
|
+
}
|
|
1786
|
+
throw error;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
return url;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function getCliProgram() {
|
|
1793
|
+
const { green, yellow } = chalk;
|
|
1794
|
+
const logo = `${chalk.green(' ____ _')}
|
|
1795
|
+
${green('| _ \\ __ _| | _____')}
|
|
1796
|
+
${green('| |_) / _` | |/ / _ \\')}
|
|
1797
|
+
${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')}
|
|
1798
|
+
${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')}
|
|
1799
|
+
`;
|
|
1800
|
+
return program$1
|
|
1801
|
+
.addHelpText('beforeAll', logo)
|
|
1802
|
+
.usage(`[url] [options]`)
|
|
1803
|
+
.showHelpAfterError()
|
|
1804
|
+
.argument('[url]', 'The web URL you want to package', validateUrlInput)
|
|
1805
|
+
.option('--name <string>', 'Application name')
|
|
1806
|
+
.option('--icon <string>', 'Application icon', DEFAULT_PAKE_OPTIONS.icon)
|
|
1807
|
+
.option('--width <number>', 'Window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
|
|
1808
|
+
.option('--height <number>', 'Window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
|
|
1809
|
+
.option('--use-local-file', 'Use local file packaging', DEFAULT_PAKE_OPTIONS.useLocalFile)
|
|
1810
|
+
.option('--fullscreen', 'Start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
|
|
1811
|
+
.option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT_PAKE_OPTIONS.hideTitleBar)
|
|
1812
|
+
.option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch)
|
|
1813
|
+
.option('--inject <files>', 'Inject local CSS/JS files into the page', (val, previous) => {
|
|
1814
|
+
if (!val)
|
|
1815
|
+
return DEFAULT_PAKE_OPTIONS.inject;
|
|
1816
|
+
// Split by comma and trim whitespace, filter out empty strings
|
|
1817
|
+
const files = val
|
|
1818
|
+
.split(',')
|
|
1819
|
+
.map((item) => item.trim())
|
|
1820
|
+
.filter((item) => item.length > 0);
|
|
1821
|
+
// If previous values exist (from multiple --inject options), merge them
|
|
1822
|
+
return previous ? [...previous, ...files] : files;
|
|
1823
|
+
}, DEFAULT_PAKE_OPTIONS.inject)
|
|
1824
|
+
.option('--debug', 'Debug build and more output', DEFAULT_PAKE_OPTIONS.debug)
|
|
1825
|
+
.addOption(new Option('--proxy-url <url>', 'Proxy URL for all network requests (http://, https://, socks5://)')
|
|
1826
|
+
.default(DEFAULT_PAKE_OPTIONS.proxyUrl)
|
|
1827
|
+
.hideHelp())
|
|
1828
|
+
.addOption(new Option('--user-agent <string>', 'Custom user agent')
|
|
1829
|
+
.default(DEFAULT_PAKE_OPTIONS.userAgent)
|
|
1830
|
+
.hideHelp())
|
|
1831
|
+
.addOption(new Option('--targets <string>', 'Build target format for your system').default(DEFAULT_PAKE_OPTIONS.targets))
|
|
1832
|
+
.addOption(new Option('--app-version <string>', 'App version, the same as package.json version')
|
|
1833
|
+
.default(DEFAULT_PAKE_OPTIONS.appVersion)
|
|
1834
|
+
.hideHelp())
|
|
1835
|
+
.addOption(new Option('--always-on-top', 'Always on the top level')
|
|
1836
|
+
.default(DEFAULT_PAKE_OPTIONS.alwaysOnTop)
|
|
1837
|
+
.hideHelp())
|
|
1838
|
+
.addOption(new Option('--maximize', 'Start window maximized')
|
|
1839
|
+
.default(DEFAULT_PAKE_OPTIONS.maximize)
|
|
1840
|
+
.hideHelp())
|
|
1841
|
+
.addOption(new Option('--dark-mode', 'Force Mac app to use dark mode')
|
|
1842
|
+
.default(DEFAULT_PAKE_OPTIONS.darkMode)
|
|
1843
|
+
.hideHelp())
|
|
1844
|
+
.addOption(new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts')
|
|
1845
|
+
.default(DEFAULT_PAKE_OPTIONS.disabledWebShortcuts)
|
|
1846
|
+
.hideHelp())
|
|
1847
|
+
.addOption(new Option('--activation-shortcut <string>', 'Shortcut key to active App')
|
|
1848
|
+
.default(DEFAULT_PAKE_OPTIONS.activationShortcut)
|
|
1849
|
+
.hideHelp())
|
|
1850
|
+
.addOption(new Option('--show-system-tray', 'Show system tray in app')
|
|
1851
|
+
.default(DEFAULT_PAKE_OPTIONS.showSystemTray)
|
|
1852
|
+
.hideHelp())
|
|
1853
|
+
.addOption(new Option('--system-tray-icon <string>', 'Custom system tray icon')
|
|
1854
|
+
.default(DEFAULT_PAKE_OPTIONS.systemTrayIcon)
|
|
1855
|
+
.hideHelp())
|
|
1856
|
+
.addOption(new Option('--hide-on-close [boolean]', 'Hide window on close instead of exiting (default: true for macOS, false for others)')
|
|
1857
|
+
.default(DEFAULT_PAKE_OPTIONS.hideOnClose)
|
|
1858
|
+
.argParser((value) => {
|
|
1859
|
+
if (value === undefined)
|
|
1860
|
+
return true; // --hide-on-close without value
|
|
1861
|
+
if (value === 'true')
|
|
1862
|
+
return true;
|
|
1863
|
+
if (value === 'false')
|
|
1864
|
+
return false;
|
|
1865
|
+
throw new Error('--hide-on-close must be true or false');
|
|
1866
|
+
})
|
|
1867
|
+
.hideHelp())
|
|
1868
|
+
.addOption(new Option('--title <string>', 'Window title').hideHelp())
|
|
1869
|
+
.addOption(new Option('--incognito', 'Launch app in incognito/private mode')
|
|
1870
|
+
.default(DEFAULT_PAKE_OPTIONS.incognito)
|
|
1871
|
+
.hideHelp())
|
|
1872
|
+
.addOption(new Option('--wasm', 'Enable WebAssembly support (Flutter Web, etc.)')
|
|
1873
|
+
.default(DEFAULT_PAKE_OPTIONS.wasm)
|
|
1874
|
+
.hideHelp())
|
|
1875
|
+
.addOption(new Option('--enable-drag-drop', 'Enable drag and drop functionality')
|
|
1876
|
+
.default(DEFAULT_PAKE_OPTIONS.enableDragDrop)
|
|
1877
|
+
.hideHelp())
|
|
1878
|
+
.addOption(new Option('--keep-binary', 'Keep raw binary file alongside installer')
|
|
1879
|
+
.default(DEFAULT_PAKE_OPTIONS.keepBinary)
|
|
1880
|
+
.hideHelp())
|
|
1881
|
+
.addOption(new Option('--multi-instance', 'Allow multiple app instances')
|
|
1882
|
+
.default(DEFAULT_PAKE_OPTIONS.multiInstance)
|
|
1883
|
+
.hideHelp())
|
|
1884
|
+
.addOption(new Option('--start-to-tray', 'Start app minimized to tray')
|
|
1885
|
+
.default(DEFAULT_PAKE_OPTIONS.startToTray)
|
|
1886
|
+
.hideHelp())
|
|
1887
|
+
.addOption(new Option('--force-internal-navigation', 'Keep every link inside the Pake window instead of opening external handlers')
|
|
1888
|
+
.default(DEFAULT_PAKE_OPTIONS.forceInternalNavigation)
|
|
1889
|
+
.hideHelp())
|
|
1890
|
+
.addOption(new Option('--installer-language <string>', 'Installer language')
|
|
1891
|
+
.default(DEFAULT_PAKE_OPTIONS.installerLanguage)
|
|
1892
|
+
.hideHelp())
|
|
1893
|
+
.addOption(new Option('--zoom <number>', 'Initial page zoom level (50-200)')
|
|
1894
|
+
.default(DEFAULT_PAKE_OPTIONS.zoom)
|
|
1895
|
+
.argParser((value) => {
|
|
1896
|
+
const zoom = parseInt(value);
|
|
1897
|
+
if (isNaN(zoom) || zoom < 50 || zoom > 200) {
|
|
1898
|
+
throw new Error('--zoom must be a number between 50 and 200');
|
|
1899
|
+
}
|
|
1900
|
+
return zoom;
|
|
1901
|
+
})
|
|
1902
|
+
.hideHelp())
|
|
1903
|
+
.addOption(new Option('--min-width <number>', 'Minimum window width')
|
|
1904
|
+
.default(DEFAULT_PAKE_OPTIONS.minWidth)
|
|
1905
|
+
.argParser(validateNumberInput)
|
|
1906
|
+
.hideHelp())
|
|
1907
|
+
.addOption(new Option('--min-height <number>', 'Minimum window height')
|
|
1908
|
+
.default(DEFAULT_PAKE_OPTIONS.minHeight)
|
|
1909
|
+
.argParser(validateNumberInput)
|
|
1910
|
+
.hideHelp())
|
|
1911
|
+
.addOption(new Option('--ignore-certificate-errors', 'Ignore certificate errors (for self-signed certificates)')
|
|
1912
|
+
.default(DEFAULT_PAKE_OPTIONS.ignoreCertificateErrors)
|
|
1913
|
+
.hideHelp())
|
|
1914
|
+
.addOption(new Option('--iterative-build', 'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging')
|
|
1915
|
+
.default(DEFAULT_PAKE_OPTIONS.iterativeBuild)
|
|
1916
|
+
.hideHelp())
|
|
1917
|
+
.version(packageJson.version, '-v, --version')
|
|
1918
|
+
.configureHelp({
|
|
1919
|
+
sortSubcommands: true,
|
|
1920
|
+
optionTerm: (option) => {
|
|
1921
|
+
if (option.flags === '-v, --version' || option.flags === '-h, --help')
|
|
1922
|
+
return '';
|
|
1923
|
+
return option.flags;
|
|
1924
|
+
},
|
|
1925
|
+
optionDescription: (option) => {
|
|
1926
|
+
if (option.flags === '-v, --version' || option.flags === '-h, --help')
|
|
1927
|
+
return '';
|
|
1928
|
+
return option.description;
|
|
1929
|
+
},
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
const program = getCliProgram();
|
|
1934
|
+
program.action(async (url, options) => {
|
|
1050
1935
|
log.setDefaultLevel('debug');
|
|
1051
|
-
const appOptions = await handleOptions(
|
|
1936
|
+
const appOptions = await handleOptions(options, url);
|
|
1052
1937
|
log.debug('PakeAppOptions', appOptions);
|
|
1053
1938
|
const builder = BuilderProvider.create(appOptions);
|
|
1054
1939
|
await builder.prepare();
|
|
1055
|
-
await builder.start(
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1940
|
+
await builder.start(url);
|
|
1941
|
+
});
|
|
1942
|
+
program.parse();
|
|
1058
1943
|
//# sourceMappingURL=dev.js.map
|