Add prettier as devDependency to improve development workflow

This commit is contained in:
Tw93
2025-08-16 22:44:31 +08:00
parent 6f9450d598
commit 29b7504109
9 changed files with 152 additions and 657 deletions

262
bin/options/icon.ts vendored
View File

@@ -1,243 +1,73 @@
import path from 'path';
import axios from 'axios';
import fsExtra from 'fs-extra';
import chalk from 'chalk';
import { dir } from 'tmp-promise';
import { fileTypeFromBuffer } from 'file-type';
import icongen from 'icon-gen';
import sharp from 'sharp';
import logger from './logger';
import { npmDirectory, getUserHomeDir } from '@/utils/dir';
import { npmDirectory } from '@/utils/dir';
import { IS_LINUX, IS_WIN } from '@/utils/platform';
import { getSpinner } from '@/utils/info';
import { fileTypeFromBuffer } from 'file-type';
import { PakeAppOptions } from '@/types';
// Constants
const ICON_CONFIG = {
minFileSize: 100,
downloadTimeout: 10000,
supportedFormats: ['png', 'ico', 'jpeg', 'jpg', 'webp'] as const,
whiteBackground: { r: 255, g: 255, b: 255 },
};
// API Configuration
const API_TOKENS = {
// cspell:disable-next-line
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
// cspell:disable-next-line
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
};
/**
* Adds white background to transparent icons only
*/
async function preprocessIcon(inputPath: string): Promise<string> {
try {
const metadata = await sharp(inputPath).metadata();
if (metadata.channels !== 4) return inputPath; // No transparency
const { path: tempDir } = await dir();
const outputPath = path.join(tempDir, 'icon-with-background.png');
await sharp({
create: {
width: metadata.width || 512,
height: metadata.height || 512,
channels: 3,
background: ICON_CONFIG.whiteBackground,
},
})
.composite([{ input: inputPath }])
.png()
.toFile(outputPath);
return outputPath;
} catch {
return inputPath;
}
}
/**
* Converts icon to platform-specific format
*/
async function convertIconFormat(
inputPath: string,
appName: string,
): Promise<string | null> {
try {
if (!(await fsExtra.pathExists(inputPath))) return null;
const { path: outputDir } = await dir();
const platformOutputDir = path.join(outputDir, 'converted-icons');
await fsExtra.ensureDir(platformOutputDir);
const processedInputPath = await preprocessIcon(inputPath);
const iconName = appName.toLowerCase();
// Generate platform-specific format
if (IS_WIN) {
await icongen(processedInputPath, platformOutputDir, {
report: false,
ico: { name: `${iconName}_256`, sizes: [256] },
});
return path.join(platformOutputDir, `${iconName}_256.ico`);
}
if (IS_LINUX) {
const outputPath = path.join(platformOutputDir, `${iconName}_512.png`);
await fsExtra.copy(processedInputPath, outputPath);
return outputPath;
}
// macOS
await icongen(processedInputPath, platformOutputDir, {
report: false,
icns: { name: iconName, sizes: [16, 32, 64, 128, 256, 512, 1024] },
});
const outputPath = path.join(platformOutputDir, `${iconName}.icns`);
return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
} catch (error) {
logger.warn(`Icon format conversion failed: ${error.message}`);
return null;
}
}
export async function handleIcon(options: PakeAppOptions, url?: string) {
export async function handleIcon(options: PakeAppOptions) {
if (options.icon) {
if (options.icon.startsWith('http')) {
return downloadIcon(options.icon);
} else {
return path.resolve(options.icon);
}
return path.resolve(options.icon);
}
// Try to get favicon from website if URL is provided
if (url && url.startsWith('http') && options.name) {
const faviconPath = await tryGetFavicon(url, options.name);
if (faviconPath) return faviconPath;
}
logger.info('✼ No icon provided, using default icon.');
const iconPath = IS_WIN
? 'src-tauri/png/icon_256.ico'
: IS_LINUX
? 'src-tauri/png/icon_512.png'
: 'src-tauri/icons/icon.icns';
return path.join(npmDirectory, iconPath);
}
/**
* Generates icon service URLs for a domain
*/
function generateIconServiceUrls(domain: string): string[] {
const logoDevUrls = API_TOKENS.logoDev
.sort(() => Math.random() - 0.5)
.map(
(token) =>
`https://img.logo.dev/${domain}?token=${token}&format=png&size=256`,
} else {
logger.warn(
'✼ No icon given, default in use. For a custom icon, use --icon option.',
);
const brandfetchUrls = API_TOKENS.brandfetch
.sort(() => Math.random() - 0.5)
.map((key) => `https://cdn.brandfetch.io/${domain}/w/400/h/400?c=${key}`);
return [
...logoDevUrls,
...brandfetchUrls,
`https://logo.clearbit.com/${domain}?size=256`,
`https://logo.uplead.com/${domain}`,
`https://www.google.com/s2/favicons?domain=${domain}&sz=256`,
`https://favicon.is/${domain}`,
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
`https://icon.horse/icon/${domain}`,
`https://${domain}/favicon.ico`,
`https://www.${domain}/favicon.ico`,
`https://${domain}/apple-touch-icon.png`,
`https://${domain}/apple-touch-icon-precomposed.png`,
];
}
/**
* Attempts to fetch favicon from website
*/
async function tryGetFavicon(
url: string,
appName: string,
): Promise<string | null> {
try {
const domain = new URL(url).hostname;
logger.info(`Auto-fetching favicon for ${domain}...`);
const serviceUrls = generateIconServiceUrls(domain);
for (const serviceUrl of serviceUrls) {
try {
const faviconPath = await downloadIcon(serviceUrl, false);
if (!faviconPath) continue;
const convertedPath = await convertIconFormat(faviconPath, appName);
if (convertedPath) {
logger.info(`Favicon ready for ${domain}`);
return convertedPath;
}
} catch {
continue;
}
}
logger.info(`No favicon found for ${domain}. Using default.`);
return null;
} catch {
return null;
const iconPath = IS_WIN
? 'src-tauri/png/icon_256.ico'
: IS_LINUX
? 'src-tauri/png/icon_512.png'
: 'src-tauri/icons/icon.icns';
return path.join(npmDirectory, iconPath);
}
}
/**
* Downloads icon from URL
*/
export async function downloadIcon(
iconUrl: string,
showSpinner = true,
): Promise<string | null> {
export async function downloadIcon(iconUrl: string) {
const spinner = getSpinner('Downloading icon...');
try {
const response = await axios.get(iconUrl, {
const iconResponse = await axios.get(iconUrl, {
responseType: 'arraybuffer',
timeout: ICON_CONFIG.downloadTimeout,
});
const iconData = await iconResponse.data;
const iconData = response.data;
if (!iconData || iconData.byteLength < ICON_CONFIG.minFileSize) return null;
const fileDetails = await fileTypeFromBuffer(iconData);
if (
!fileDetails ||
!ICON_CONFIG.supportedFormats.includes(fileDetails.ext as any)
) {
if (!iconData) {
return null;
}
return await saveIconFile(iconData, fileDetails.ext);
} catch (error) {
if (showSpinner && !(error.response?.status === 404)) {
throw error;
const fileDetails = await fileTypeFromBuffer(iconData);
if (!fileDetails) {
return null;
}
return null;
const { path: tempPath } = await dir();
let iconPath = `${tempPath}/icon.${fileDetails.ext}`;
// Fix this for linux
if (IS_LINUX) {
iconPath = 'png/linux_temp.png';
await fsExtra.outputFile(
`${npmDirectory}/src-tauri/${iconPath}`,
iconData,
);
} else {
await fsExtra.outputFile(iconPath, iconData);
}
await fsExtra.outputFile(iconPath, iconData);
spinner.succeed(chalk.green('Icon downloaded successfully!'));
return iconPath;
} catch (error) {
spinner.fail(chalk.red('Icon download failed!'));
if (error.response && error.response.status === 404) {
return null;
}
throw error;
}
}
/**
* Saves icon file to .pake directory
*/
async function saveIconFile(
iconData: ArrayBuffer,
extension: string,
): Promise<string> {
const buffer = Buffer.from(iconData);
const homeDir = getUserHomeDir();
const pakeDir = path.join(homeDir, '.pake', 'icons');
// Ensure .pake/icons directory exists
await fsExtra.ensureDir(pakeDir);
const iconPath = path.join(pakeDir, `downloaded-icon.${extension}`);
await fsExtra.outputFile(iconPath, buffer);
return iconPath;
}

10
bin/options/index.ts vendored
View File

@@ -43,13 +43,11 @@ export default async function handleOptions(
}
if (!isValidName(name, platform)) {
const LINUX_NAME_ERROR = `✕ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
const DEFAULT_NAME_ERROR = `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
const errorMsg =
platform === 'linux'
? `✕ Name should only include letters, numbers, dashes, and spaces. Spaces will be converted to dashes. Examples: Google Translate → google-translate, 123pan, weread.`
: `✕ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: Google Translate, 123pan, WeRead, we-read.`;
platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
logger.error(errorMsg);
if (isActions) {
name = resolveAppName(url, platform);
logger.warn(`✼ Inside github actions, use the default name: ${name}`);
@@ -64,7 +62,7 @@ export default async function handleOptions(
identifier: getIdentifier(url),
};
appOptions.icon = await handleIcon(appOptions, url);
appOptions.icon = await handleIcon(appOptions);
return appOptions;
}