@patdx/pwa-asset-generator
Version:
Automates PWA asset generation and image declaration. Automatically generates icon and splash screen images, favicons and mstile images. Updates manifest.json and index.html files with the generated images according to Web App Manifest specs and Apple Hum
1,998 lines (1,982 loc) • 48.7 kB
JavaScript
var __require = /* @__PURE__ */ ((x) =>
typeof require !== 'undefined'
? require
: typeof Proxy !== 'undefined'
? new Proxy(x, {
get: (a, b) => (typeof require !== 'undefined' ? require : a)[b],
})
: x)(function (x) {
if (typeof require !== 'undefined') return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/helpers/meta.ts
import * as cheerio from 'cheerio';
import pretty from 'pretty';
import { lookup as lookup2 } from 'mime-types';
import path2 from 'path';
// src/config/apple-fallback-data.json
var apple_fallback_data_default = [
{
device: '12.9\u201D iPad Pro',
portrait: {
width: 2048,
height: 2732,
},
landscape: {
width: 2732,
height: 2048,
},
scaleFactor: 2,
},
{
device: '11\u201D iPad Pro',
portrait: {
width: 1668,
height: 2388,
},
landscape: {
width: 2388,
height: 1668,
},
scaleFactor: 2,
},
{
device: '10.5\u201D iPad Pro',
portrait: {
width: 1668,
height: 2388,
},
landscape: {
width: 2388,
height: 1668,
},
scaleFactor: 2,
},
{
device: '9.7\u201D iPad Pro',
portrait: {
width: 1536,
height: 2048,
},
landscape: {
width: 2048,
height: 1536,
},
scaleFactor: 2,
},
{
device: '8.3\u201D iPad mini',
portrait: {
width: 1488,
height: 2266,
},
landscape: {
width: 2266,
height: 1488,
},
scaleFactor: 2,
},
{
device: '7.9\u201D iPad mini',
portrait: {
width: 1536,
height: 2048,
},
landscape: {
width: 2048,
height: 1536,
},
scaleFactor: 2,
},
{
device: '10.9\u201D iPad Air',
portrait: {
width: 1640,
height: 2360,
},
landscape: {
width: 2360,
height: 1640,
},
scaleFactor: 2,
},
{
device: '10.5\u201D iPad Air',
portrait: {
width: 1668,
height: 2224,
},
landscape: {
width: 2224,
height: 1668,
},
scaleFactor: 2,
},
{
device: '9.7\u201D iPad Air',
portrait: {
width: 1536,
height: 2048,
},
landscape: {
width: 2048,
height: 1536,
},
scaleFactor: 2,
},
{
device: '10.2\u201D iPad',
portrait: {
width: 1620,
height: 2160,
},
landscape: {
width: 2160,
height: 1620,
},
scaleFactor: 2,
},
{
device: '9.7\u201D iPad',
portrait: {
width: 1536,
height: 2048,
},
landscape: {
width: 2048,
height: 1536,
},
scaleFactor: 2,
},
{
device: 'iPhone 16 Pro Max',
portrait: {
width: 1320,
height: 2868,
},
landscape: {
width: 2868,
height: 1320,
},
scaleFactor: 3,
},
{
device: 'iPhone 16 Pro',
portrait: {
width: 1206,
height: 2622,
},
landscape: {
width: 2622,
height: 1206,
},
scaleFactor: 3,
},
{
device: 'iPhone 16 Plus',
portrait: {
width: 1290,
height: 2796,
},
landscape: {
width: 2796,
height: 1290,
},
scaleFactor: 3,
},
{
device: 'iPhone 16',
portrait: {
width: 1179,
height: 2556,
},
landscape: {
width: 2556,
height: 1179,
},
scaleFactor: 3,
},
{
device: 'iPhone 15 Pro Max',
portrait: {
width: 1290,
height: 2796,
},
landscape: {
width: 2796,
height: 1290,
},
scaleFactor: 3,
},
{
device: 'iPhone 15 Pro',
portrait: {
width: 1179,
height: 2556,
},
landscape: {
width: 2556,
height: 1179,
},
scaleFactor: 3,
},
{
device: 'iPhone 15 Plus',
portrait: {
width: 1290,
height: 2796,
},
landscape: {
width: 2796,
height: 1290,
},
scaleFactor: 3,
},
{
device: 'iPhone 15',
portrait: {
width: 1179,
height: 2556,
},
landscape: {
width: 2556,
height: 1179,
},
scaleFactor: 3,
},
{
device: 'iPhone 14 Pro Max',
portrait: {
width: 1290,
height: 2796,
},
landscape: {
width: 2796,
height: 1290,
},
scaleFactor: 3,
},
{
device: 'iPhone 14 Pro',
portrait: {
width: 1179,
height: 2556,
},
landscape: {
width: 2556,
height: 1179,
},
scaleFactor: 3,
},
{
device: 'iPhone 14 Plus',
portrait: {
width: 1284,
height: 2778,
},
landscape: {
width: 2778,
height: 1284,
},
scaleFactor: 3,
},
{
device: 'iPhone 14',
portrait: {
width: 1170,
height: 2532,
},
landscape: {
width: 2532,
height: 1170,
},
scaleFactor: 3,
},
{
device: 'iPhone 13 Pro Max',
portrait: {
width: 1284,
height: 2778,
},
landscape: {
width: 2778,
height: 1284,
},
scaleFactor: 3,
},
{
device: 'iPhone 13 Pro',
portrait: {
width: 1170,
height: 2532,
},
landscape: {
width: 2532,
height: 1170,
},
scaleFactor: 3,
},
{
device: 'iPhone 13',
portrait: {
width: 1170,
height: 2532,
},
landscape: {
width: 2532,
height: 1170,
},
scaleFactor: 3,
},
{
device: 'iPhone 13 mini',
portrait: {
width: 1125,
height: 2436,
},
landscape: {
width: 2436,
height: 1125,
},
scaleFactor: 3,
},
{
device: 'iPhone 12 Pro Max',
portrait: {
width: 1284,
height: 2778,
},
landscape: {
width: 2778,
height: 1284,
},
scaleFactor: 3,
},
{
device: 'iPhone 12 Pro',
portrait: {
width: 1170,
height: 2532,
},
landscape: {
width: 2532,
height: 1170,
},
scaleFactor: 3,
},
{
device: 'iPhone 12',
portrait: {
width: 1170,
height: 2532,
},
landscape: {
width: 2532,
height: 1170,
},
scaleFactor: 3,
},
{
device: 'iPhone 12 mini',
portrait: {
width: 1125,
height: 2436,
},
landscape: {
width: 2436,
height: 1125,
},
scaleFactor: 3,
},
{
device: 'iPhone 11 Pro Max',
portrait: {
width: 1242,
height: 2688,
},
landscape: {
width: 2688,
height: 1242,
},
scaleFactor: 3,
},
{
device: 'iPhone 11 Pro',
portrait: {
width: 1125,
height: 2436,
},
landscape: {
width: 2436,
height: 1125,
},
scaleFactor: 3,
},
{
device: 'iPhone 11',
portrait: {
width: 828,
height: 1792,
},
landscape: {
width: 1792,
height: 828,
},
scaleFactor: 2,
},
{
device: 'iPhone XS Max',
portrait: {
width: 1242,
height: 2688,
},
landscape: {
width: 2688,
height: 1242,
},
scaleFactor: 3,
},
{
device: 'iPhone XS',
portrait: {
width: 1125,
height: 2436,
},
landscape: {
width: 2436,
height: 1125,
},
scaleFactor: 3,
},
{
device: 'iPhone XR',
portrait: {
width: 828,
height: 1792,
},
landscape: {
width: 1792,
height: 828,
},
scaleFactor: 2,
},
{
device: 'iPhone X',
portrait: {
width: 1125,
height: 2436,
},
landscape: {
width: 2436,
height: 1125,
},
scaleFactor: 3,
},
{
device: 'iPhone 8 Plus',
portrait: {
width: 1242,
height: 2208,
},
landscape: {
width: 2208,
height: 1242,
},
scaleFactor: 3,
},
{
device: 'iPhone 8',
portrait: {
width: 750,
height: 1334,
},
landscape: {
width: 1334,
height: 750,
},
scaleFactor: 2,
},
{
device: 'iPhone 7 Plus',
portrait: {
width: 1242,
height: 2208,
},
landscape: {
width: 2208,
height: 1242,
},
scaleFactor: 3,
},
{
device: 'iPhone 7',
portrait: {
width: 750,
height: 1334,
},
landscape: {
width: 1334,
height: 750,
},
scaleFactor: 2,
},
{
device: 'iPhone 6s Plus',
portrait: {
width: 1242,
height: 2208,
},
landscape: {
width: 2208,
height: 1242,
},
scaleFactor: 3,
},
{
device: 'iPhone 6s',
portrait: {
width: 750,
height: 1334,
},
landscape: {
width: 1334,
height: 750,
},
scaleFactor: 2,
},
{
device: 'iPhone 6 Plus',
portrait: {
width: 1242,
height: 2208,
},
landscape: {
width: 2208,
height: 1242,
},
scaleFactor: 3,
},
{
device: 'iPhone 6',
portrait: {
width: 750,
height: 1334,
},
landscape: {
width: 1334,
height: 750,
},
scaleFactor: 2,
},
{
device: '4.7\u201D iPhone SE',
portrait: {
width: 750,
height: 1334,
},
landscape: {
width: 1334,
height: 750,
},
scaleFactor: 2,
},
{
device: '4\u201D iPhone SE',
portrait: {
width: 640,
height: 1136,
},
landscape: {
width: 1136,
height: 640,
},
scaleFactor: 2,
},
{
device: 'iPod touch 5th generation and later',
portrait: {
width: 640,
height: 1136,
},
landscape: {
width: 1136,
height: 640,
},
scaleFactor: 2,
},
];
// src/config/constants.ts
var HTML_META_ORDERED_SELECTOR_LIST = [
{
name: 'favicon' /* favicon */,
selector: 'link[rel="icon"]',
},
{
name: 'msTileImage' /* msTileImage */,
selector: 'meta[name*="msapplication-"]',
},
{
name: 'appleTouchIcon' /* appleTouchIcon */,
selector: 'link[rel="apple-touch-icon"]',
},
{
name: 'appleMobileWebAppCapable' /* appleMobileWebAppCapable */,
selector: 'meta[name="apple-mobile-web-app-capable"]',
},
{
name: 'appleLaunchImage' /* appleLaunchImage */,
selector:
'link[rel="apple-touch-startup-image"]:not([media^="(prefers-color-scheme: dark)"])',
},
{
name: 'appleLaunchImageDarkMode' /* appleLaunchImageDarkMode */,
selector:
'link[rel="apple-touch-startup-image"][media^="(prefers-color-scheme: dark)"]',
},
];
var constants_default = {
FLAGS: {
background: {
type: 'string',
shortFlag: 'b',
default: 'transparent',
},
manifest: {
type: 'string',
shortFlag: 'm',
},
index: {
type: 'string',
shortFlag: 'i',
},
path: {
type: 'string',
shortFlag: 'a',
},
pathOverride: {
type: 'string',
shortFlag: 'v',
},
opaque: {
type: 'boolean',
shortFlag: 'o',
default: true,
},
scrape: {
type: 'boolean',
shortFlag: 's',
default: true,
},
padding: {
type: 'string',
shortFlag: 'p',
default: '10%',
},
type: {
type: 'string',
shortFlag: 't',
default: 'jpg',
},
quality: {
type: 'number',
shortFlag: 'q',
default: 70,
},
splashOnly: {
type: 'boolean',
shortFlag: 'h',
default: false,
},
iconOnly: {
type: 'boolean',
shortFlag: 'c',
default: false,
},
landscapeOnly: {
type: 'boolean',
shortFlag: 'l',
default: false,
},
portraitOnly: {
type: 'boolean',
shortFlag: 'r',
default: false,
},
log: {
type: 'boolean',
shortFlag: 'g',
default: true,
},
singleQuotes: {
type: 'boolean',
shortFlag: 'u',
default: false,
},
xhtml: {
type: 'boolean',
shortFlag: 'x',
default: false,
},
favicon: {
type: 'boolean',
shortFlag: 'f',
default: false,
},
mstile: {
type: 'boolean',
shortFlag: 'w',
default: false,
},
maskable: {
type: 'boolean',
shortFlag: 'e',
default: true,
},
darkMode: {
type: 'boolean',
shortFlag: 'd',
default: false,
},
noSandbox: {
type: 'boolean',
shortFlag: 'n',
default: false,
},
},
CHROME_LAUNCH_ARGS: [
'--disable-dev-shm-usage',
'--log-level=3',
// Fatal only
'--no-default-browser-check',
'--disable-infobars',
'--no-experiments',
'--ignore-gpu-blacklist',
'--disable-gpu',
'--disable-default-apps',
'--enable-features=NetworkService',
'--disable-features=TranslateUI',
'--disable-extensions',
'--disable-component-extensions-with-background-pages',
'--disable-background-networking',
'--disable-backgrounding-occluded-windows',
'--disable-background-timer-throttling',
'--disable-renderer-backgrounding',
'--disable-file-system',
'--disable-permissions-api',
'--incognito',
'--disable-sync',
'--metrics-recording-only',
'--mute-audio',
'--no-first-run',
'--headless',
'--force-color-profile=srgb',
],
CHROME_LAUNCHER_DEBUG_PORT: 9222,
CHROME_LAUNCHER_MAX_CONN_RETRIES: 10,
EMULATED_USER_AGENT:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15',
APPLE_HIG_SPLASH_SCR_SPECS_URL:
'https://developer.apple.com/design/human-interface-guidelines/layout/',
// Apple platform specs: https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/
// https://web.dev/apple-touch-icon/
APPLE_ICON_SIZES: [180],
// Android platform specs: https://developers.google.com/web/fundamentals/web-app-manifest/#icons
// Windows platform specs: https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps/get-started
MANIFEST_ICON_SIZES: [192, 512],
// MSDN static tiles specs: https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/samples/dn455106(v=vs.85)?redirectedfrom=MSDN#static-tiles
MS_ICON_SIZES: [128, 270, 558, { width: 558, height: 270 }],
FAVICON_SIZES: [196],
HTML_META_ORDERED_SELECTOR_LIST,
FAVICON_FILENAME_PREFIX: 'favicon',
APPLE_ICON_FILENAME_PREFIX: 'apple-icon',
APPLE_SPLASH_FILENAME_PREFIX: 'apple-splash',
APPLE_SPLASH_FILENAME_DARK_MODE_POSTFIX: '-dark',
MANIFEST_ICON_FILENAME_PREFIX: 'manifest-icon',
MS_ICON_FILENAME_PREFIX: 'mstile-icon',
APPLE_HIG_SPLASH_SCR_SPECS_DATA_GRID_SELECTOR: 'table tbody tr',
WAIT_FOR_SELECTOR_TIMEOUT: 1e3,
BROWSER_TIMEOUT: 1e4,
FAVICON_META_HTML: (
size,
url2,
mimeType,
xhtml,
) => `<link rel="icon" type="${mimeType}" sizes="${size}x${size}" href="${url2}"${
xhtml ? ' /' : ''
}>
`,
MSTILE_SIZE_ELEMENT_NAME_MAP: {
'128x128': 'square70x70logo',
'270x270': 'square150x150logo',
'558x558': 'square310x310logo',
'558x270': 'wide310x150logo',
},
MSTILE_IMAGE_META_HTML: (
tileName,
url2,
xhtml,
) => `<meta name="msapplication-${tileName}" content="${url2}"${
xhtml ? ' /' : ''
}>
`,
SHELL_HTML_FOR_LOGO: (
imgPath,
padding,
backgroundColor = 'transparent',
) => `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
background: ${backgroundColor};
height: 100vh;
padding: ${padding};
box-sizing: border-box;
}
img {
width: 100%;
height: 100%;
margin: 0 auto;
display: block;
object-fit: contain;
}
</style>
</head>
<body>
<img src="${imgPath}">
</body>
</html>`,
APPLE_TOUCH_ICON_META_HTML: (
url2,
xhtml,
) => `<link rel="apple-touch-icon" href="${url2}"${xhtml ? ' /' : ''}>
`,
APPLE_LAUNCH_SCREEN_META_HTML: (
width,
height,
url2,
scaleFactor,
orientation,
darkMode,
xhtml,
) => {
if (orientation === 'portrait') {
return `<link rel="apple-touch-startup-image" href="${url2}" media="${
darkMode ? '(prefers-color-scheme: dark) and ' : ''
}(device-width: ${width / scaleFactor}px) and (device-height: ${
height / scaleFactor
}px) and (-webkit-device-pixel-ratio: ${scaleFactor}) and (orientation: ${orientation})"${
xhtml ? ' /' : ''
}>
`;
}
return `<link rel="apple-touch-startup-image" href="${url2}" media="${
darkMode ? '(prefers-color-scheme: dark) and ' : ''
}(device-width: ${height / scaleFactor}px) and (device-height: ${
width / scaleFactor
}px) and (-webkit-device-pixel-ratio: ${scaleFactor}) and (orientation: ${orientation})"${
xhtml ? ' /' : ''
}>
`;
},
APPLE_HIG_SPLASH_SCREEN_FALLBACK_DATA: apple_fallback_data_default,
};
// src/helpers/file.ts
import fs from 'fs';
import path from 'path';
import slash from 'slash';
import { lookup } from 'mime-types';
import { promisify } from 'util';
var getExtension = (file) => {
return path.extname(file).replace('.', '');
};
var isImageFile = (file) => {
return [
'apng',
'bmp',
'gif',
'ico',
'cur',
'jpg',
'jpeg',
'jfif',
'pjpeg',
'pjp',
'png',
'svg',
'webp',
].includes(getExtension(file));
};
var isHtmlFile = (file) => {
return ['html', 'htm'].includes(getExtension(file));
};
var convertBackslashPathToSlashPath = (backSlashPath) => {
return slash(backSlashPath);
};
var getAppDir = () => {
let appPath;
try {
appPath = __require.resolve('pwa-asset-generator');
} catch (e) {
appPath = __require.main.filename;
}
return path.join(path.dirname(appPath), '..');
};
var getShellHtmlFilePath = () => {
return `${getAppDir()}/static/shell.html`;
};
var getImageSavePath = (
imageName,
outputFolder,
ext,
maskable,
isMaskableIcon,
) => {
const getMaskablePrefix = () => {
if (!isMaskableIcon) {
return '';
}
if (maskable) {
return '.maskable';
}
return '';
};
return convertBackslashPathToSlashPath(
path.join(outputFolder, `${imageName}${getMaskablePrefix()}.${ext}`),
);
};
var fileUrl = (filePath) => {
let pathName = filePath;
pathName = pathName.replace(/\\/g, '/');
if (pathName[0] !== '/') {
pathName = `/${pathName}`;
}
return encodeURI(`file://${pathName}`).replace(/[?#]/g, encodeURIComponent);
};
var getFileUrlOfPath = (source) => {
return fileUrl(path.resolve(source));
};
var getRelativeFilePath = (referenceFilePath, filePath) => {
return path.relative(
path.dirname(path.resolve(referenceFilePath)),
path.resolve(filePath),
);
};
var getRelativeImagePath = (outputFilePath, imagePath) => {
if (outputFilePath) {
return convertBackslashPathToSlashPath(
getRelativeFilePath(outputFilePath, imagePath),
);
}
return convertBackslashPathToSlashPath(imagePath);
};
var getImageBase64Url = (imagePath) => {
return `data:${lookup(imagePath)};base64,${fs.readFileSync(imagePath, {
encoding: 'base64',
})}`;
};
var getHtmlShell = (imagePath, options, isUrl2) => {
const imageUrl = isUrl2 ? imagePath : getImageBase64Url(imagePath);
return `${constants_default.SHELL_HTML_FOR_LOGO(
imageUrl,
options.padding,
options.background,
)}`;
};
var isPathAccessible = (filePath, mode) =>
promisify(fs.access)(filePath, mode).then(() => true);
var makeDirRecursiveSync = (dirPath) => {
fs.mkdirSync(dirPath, { recursive: true });
return dirPath;
};
var file_default = {
convertBackslashPathToSlashPath,
getRelativeImagePath,
getHtmlShell,
isHtmlFile,
isImageFile,
getImageBase64Url,
getShellHtmlFilePath,
getImageSavePath,
getFileUrlOfPath,
isPathAccessible,
getRelativeFilePath,
getAppDir,
getExtension,
getFilesInDir: promisify(fs.readdir),
readFile: promisify(fs.readFile),
readFileSync: fs.readFileSync,
writeFile: promisify(fs.writeFile),
writeFileSync: fs.writeFileSync,
makeDir: promisify(fs.mkdir),
makeDirSync: fs.mkdirSync,
copyFileSync: fs.copyFileSync,
exists: promisify(fs.exists),
makeDirRecursiveSync,
READ_ACCESS: fs.constants.R_OK,
WRITE_ACCESS: fs.constants.W_OK,
};
// src/helpers/meta.ts
var generateOutputPath = (options, imagePath, isManifest = false) => {
const {
path: pathPrefix,
pathOverride,
index: indexHtmlPath,
manifest: manifestJsonPath,
} = options;
const outputFilePath = isManifest ? manifestJsonPath : indexHtmlPath;
if (pathOverride !== void 0) {
return `${pathOverride}/${path2.parse(imagePath).base}`;
}
if (pathPrefix && !isManifest) {
return `${pathPrefix}/${file_default.getRelativeImagePath(
outputFilePath,
imagePath,
)}`;
}
return file_default.getRelativeImagePath(outputFilePath, imagePath);
};
var generateIconsContentForManifest = (savedImages, options) => {
return savedImages
.filter((image) =>
image.name.startsWith(constants_default.MANIFEST_ICON_FILENAME_PREFIX),
)
.reduce((curr, { path: imagePath, width, height }) => {
const icon = {
src: generateOutputPath(options, imagePath),
sizes: `${width}x${height}`,
type: `image/${file_default.getExtension(imagePath)}`,
};
if (!options.maskable) {
return curr.concat(icon);
}
return curr.concat([
{ ...icon, purpose: 'any' },
{ ...icon, purpose: 'maskable' },
]);
}, []);
};
var generateAppleTouchIconHtml = (savedImages, options) => {
return savedImages
.filter((image) =>
image.name.startsWith(constants_default.APPLE_ICON_FILENAME_PREFIX),
)
.map(({ path: imagePath }) =>
constants_default.APPLE_TOUCH_ICON_META_HTML(
generateOutputPath(options, imagePath),
options.xhtml,
),
)
.join('');
};
var generateFaviconHtml = (savedImages, options) => {
return savedImages
.filter((image) =>
image.name.startsWith(constants_default.FAVICON_FILENAME_PREFIX),
)
.map(({ width, path: imagePath }) =>
constants_default.FAVICON_META_HTML(
width,
generateOutputPath(options, imagePath),
lookup2(imagePath),
options.xhtml,
),
)
.join('');
};
var generateMsTileImageHtml = (savedImages, options) => {
return savedImages
.filter((image) =>
image.name.startsWith(constants_default.MS_ICON_FILENAME_PREFIX),
)
.map(({ width, height, path: imagePath }) =>
constants_default.MSTILE_IMAGE_META_HTML(
constants_default.MSTILE_SIZE_ELEMENT_NAME_MAP[`${width}x${height}`],
generateOutputPath(options, imagePath),
options.xhtml,
),
)
.join('');
};
var generateAppleLaunchImageHtml = (savedImages, options, darkMode) => {
return savedImages
.filter((image) =>
image.name.startsWith(constants_default.APPLE_SPLASH_FILENAME_PREFIX),
)
.map(({ width, height, path: imagePath, scaleFactor, orientation }) =>
constants_default.APPLE_LAUNCH_SCREEN_META_HTML(
width,
height,
generateOutputPath(options, imagePath),
scaleFactor,
orientation,
darkMode,
options.xhtml,
),
)
.join('');
};
var generateHtmlForIndexPage = (savedImages, options) => {
const htmlMeta = {
['appleMobileWebAppCapable' /* appleMobileWebAppCapable */]: `<meta name="apple-mobile-web-app-capable" content="yes"${
options.xhtml ? ' /' : ''
}>
`,
};
if (!options.splashOnly) {
if (options.favicon) {
htmlMeta['favicon' /* favicon */] = `${generateFaviconHtml(
savedImages,
options,
)}`;
}
htmlMeta[
'appleTouchIcon' /* appleTouchIcon */
] = `${generateAppleTouchIconHtml(savedImages, options)}`;
}
if (!options.iconOnly) {
if (options.darkMode) {
htmlMeta[
'appleLaunchImageDarkMode' /* appleLaunchImageDarkMode */
] = `${generateAppleLaunchImageHtml(savedImages, options, true)}`;
} else {
htmlMeta[
'appleLaunchImage' /* appleLaunchImage */
] = `${generateAppleLaunchImageHtml(savedImages, options, false)}`;
}
}
if (options.mstile) {
htmlMeta['msTileImage' /* msTileImage */] = `${generateMsTileImageHtml(
savedImages,
options,
)}`;
}
if (options.singleQuotes) {
Object.keys(htmlMeta).forEach((metaKey) => {
const metaContent = htmlMeta[metaKey];
if (metaContent) {
metaContent.replace(/"/gm, "'");
}
});
return htmlMeta;
}
return htmlMeta;
};
var addIconsToManifest = async (manifestContent, manifestJsonFilePath) => {
if (
!(await file_default.isPathAccessible(
manifestJsonFilePath,
file_default.WRITE_ACCESS,
))
) {
throw Error(`Cannot write to manifest json file ${manifestJsonFilePath}`);
}
const manifestJson = JSON.parse(
await file_default.readFile(manifestJsonFilePath),
);
const newManifestContent = {
...manifestJson,
icons: [...manifestContent],
};
if (manifestJson.icons) {
newManifestContent.icons = [
...newManifestContent.icons,
...manifestJson.icons.filter(
(icon) => !manifestContent.some((man) => man.sizes === icon.sizes),
),
];
}
return file_default.writeFile(
manifestJsonFilePath,
JSON.stringify(newManifestContent, null, 2),
);
};
var formatMetaTags = (htmlMeta) => {
return constants_default.HTML_META_ORDERED_SELECTOR_LIST.reduce(
(acc, meta) => {
if (htmlMeta.hasOwnProperty(meta.name)) {
return `${acc}
${htmlMeta[meta.name]}`;
}
return acc;
},
'',
);
};
var addMetaTagsToIndexPage = async (htmlMeta, indexHtmlFilePath, xhtml) => {
if (
!(await file_default.isPathAccessible(
indexHtmlFilePath,
file_default.WRITE_ACCESS,
))
) {
throw Error(`Cannot write to index html file ${indexHtmlFilePath}`);
}
const indexHtmlFile = await file_default.readFile(indexHtmlFilePath);
const $ = cheerio.load(indexHtmlFile, {
// decodeEntities: false,
xmlMode: xhtml,
});
const HEAD_SELECTOR = 'head';
const hasElement = (selector) => {
return $(selector).length > 0;
};
const hasDarkModeElement = () => {
const darkModeMeta = constants_default.HTML_META_ORDERED_SELECTOR_LIST.find(
(m) =>
m.name === 'appleLaunchImageDarkMode' /* appleLaunchImageDarkMode */,
);
if (darkModeMeta) {
return $(darkModeMeta.selector).length > 0;
}
return false;
};
constants_default.HTML_META_ORDERED_SELECTOR_LIST.forEach((meta) => {
if (htmlMeta.hasOwnProperty(meta.name) && htmlMeta[meta.name] !== '') {
const content = `${htmlMeta[meta.name]}`;
if (hasElement(meta.selector)) {
$(meta.selector).remove();
}
if (
meta.name === 'appleLaunchImage' /* appleLaunchImage */ &&
hasDarkModeElement()
) {
$(HEAD_SELECTOR).prepend(`
${content}`);
} else {
$(HEAD_SELECTOR).append(`${content}
`);
}
}
});
return file_default.writeFile(
indexHtmlFilePath,
pretty($.html(), { ocd: true }),
);
};
var meta_default = {
formatMetaTags,
addIconsToManifest,
addMetaTagsToIndexPage,
generateHtmlForIndexPage,
generateBrowserConfigXml: generateMsTileImageHtml,
generateIconsContentForManifest,
};
// src/helpers/url.ts
import url from 'url';
import dns from 'dns';
// src/helpers/logger.ts
import chalk from 'chalk';
var testMode = !!+process.env.PAG_TEST_MODE;
var logger = (prefix, options) => {
const isLogEnabled =
options && options.hasOwnProperty('log') ? options.log : true;
const getTime = () => {
return chalk.inverse(/* @__PURE__ */ new Date().toLocaleTimeString());
};
const getPrefix = () => {
return prefix ? chalk.gray(prefix) : '';
};
const raw = (...args) => {
if (!isLogEnabled) return;
console.log(...args);
};
const log = (...args) => {
if (testMode || !isLogEnabled) return;
console.log(getTime(), getPrefix(), ...args);
};
const warn = (...args) => {
if (testMode || !isLogEnabled) return;
console.warn(getTime(), getPrefix(), chalk.yellow(...args), '\u{1F914}');
};
const trace = (...args) => {
if (testMode || !isLogEnabled) return;
console.trace(getTime(), getPrefix(), ...args);
};
const error = (...args) => {
console.error(getTime(), getPrefix(), chalk.red(...args), '\u{1F62D}');
};
const success = (...args) => {
if (testMode || !isLogEnabled) return;
console.log(getTime(), getPrefix(), chalk.green(...args), '\u{1F64C}');
};
return {
raw,
log,
warn,
trace,
error,
success,
};
};
var logger_default = logger;
// src/helpers/url.ts
var isUrl = (val) => {
const parsedUrl = url.parse(val);
return ['http:', 'https:'].includes(parsedUrl.protocol);
};
var isUrlExists = (source) => {
return new Promise((resolve, reject) => {
try {
dns.resolve(url.parse(source).hostname, (err) => {
if (err) {
return resolve(false);
}
return resolve(true);
});
} catch (e) {
reject(e);
}
});
};
var getAddress = async (source, options) => {
const logger2 = logger_default(getAddress.name, options);
if (isUrl(source)) {
if (!(await isUrlExists(source))) {
throw Error(
`Cannot resolve ${source}. Please check your internet connection`,
);
}
logger2.log('Providing url source as navigation address');
return source;
}
if (file_default.isHtmlFile(source)) {
logger2.log('Providing html file path as navigation address');
return file_default.getFileUrlOfPath(source);
}
return source;
};
var getShellHtml = async (source, options) => {
const logger2 = logger_default(getShellHtml.name, options);
const useShell = async (isSourceUrl = false) => {
logger2.log('Providing shell html as page content');
return file_default.getHtmlShell(source, options, isSourceUrl);
};
if (isUrl(source)) {
if (!(await isUrlExists(source))) {
throw Error(
`Cannot resolve ${source}. Please check your internet connection`,
);
}
logger2.log('Generating shell html with provided image url');
return useShell(true);
}
if (
!(await file_default.isPathAccessible(source, file_default.READ_ACCESS))
) {
throw Error(`Cannot find path ${source}. Please check if file exists`);
}
logger2.log('Generating shell html with provided image source');
return useShell();
};
var url_default = { isUrl, isUrlExists, getAddress, getShellHtml };
// src/helpers/images.ts
import uniqWith from 'lodash.uniqwith';
import isEqual from 'lodash.isequal';
var mapToIconImageFileObj = (fileNamePrefix, width, height) => ({
name: `${fileNamePrefix}-${width}${height ? `-${height}` : ''}`,
width,
height: height ?? width,
orientation: null,
scaleFactor: 1,
});
var mapToImageFileObj = (
fileNamePrefix,
width,
height,
scaleFactor,
orientation,
) => ({
name: `${fileNamePrefix}-${width}-${height}`,
width,
height,
scaleFactor,
orientation,
});
var getIconImages = (options) => {
let icons = [
...constants_default.APPLE_ICON_SIZES.map((size) =>
mapToIconImageFileObj(constants_default.APPLE_ICON_FILENAME_PREFIX, size),
),
...constants_default.MANIFEST_ICON_SIZES.map((size) =>
mapToIconImageFileObj(
constants_default.MANIFEST_ICON_FILENAME_PREFIX,
size,
),
),
];
if (options.favicon) {
icons = [
...icons,
...constants_default.FAVICON_SIZES.map((size) =>
mapToIconImageFileObj(constants_default.FAVICON_FILENAME_PREFIX, size),
),
];
}
if (options.mstile) {
icons = [
...icons,
...constants_default.MS_ICON_SIZES.map((size) => {
if (typeof size === 'object') {
return mapToIconImageFileObj(
constants_default.MS_ICON_FILENAME_PREFIX,
size.width,
size.height,
);
}
return mapToIconImageFileObj(
constants_default.MS_ICON_FILENAME_PREFIX,
size,
);
}),
];
}
return uniqWith(icons, isEqual);
};
var getSplashScreenImages = (splashScreenData, options) => {
let appleSplashFilenamePrefix =
constants_default.APPLE_SPLASH_FILENAME_PREFIX;
if (options.darkMode) {
appleSplashFilenamePrefix +=
constants_default.APPLE_SPLASH_FILENAME_DARK_MODE_POSTFIX;
}
return uniqWith(
splashScreenData.reduce((acc, curr) => {
let images = acc;
if (!options.landscapeOnly) {
images = [
...images,
mapToImageFileObj(
appleSplashFilenamePrefix,
curr.portrait.width,
curr.portrait.height,
curr.scaleFactor,
'portrait',
),
];
}
if (!options.portraitOnly) {
images = [
...images,
mapToImageFileObj(
appleSplashFilenamePrefix,
curr.landscape.width,
curr.landscape.height,
curr.scaleFactor,
'landscape',
),
];
}
return images;
}, []),
isEqual,
);
};
var images_default = {
getIconImages,
getSplashScreenImages,
};
// src/helpers/browser.ts
import puppeteer from 'puppeteer-core';
import { launch } from 'chrome-launcher';
import find from 'find-process';
import { get } from 'http';
var getLocalBrowserInstance = async (launchArgs, noSandbox) => {
return puppeteer.launch({
...launchArgs,
...(noSandbox && {
args: [
...(launchArgs.args ?? []),
'--no-sandbox',
'--disable-setuid-sandbox',
],
}),
});
};
var launchSystemBrowser = () => {
const launchOptions = {
chromeFlags: constants_default.CHROME_LAUNCH_ARGS,
logLevel: 'silent',
maxConnectionRetries: constants_default.CHROME_LAUNCHER_MAX_CONN_RETRIES,
};
return launch(launchOptions);
};
var getLaunchedChromeVersionInfo = (chrome) => {
return new Promise((resolve, reject) => {
get(`http://localhost:${chrome.port}/json/version`, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
resolve(JSON.parse(data));
});
}).on('error', (err) => reject(err));
});
};
var getSystemBrowserInstance = async (chrome, launchArgs) => {
const chromeVersionInfo = await getLaunchedChromeVersionInfo(chrome);
return puppeteer.connect({
...launchArgs,
browserWSEndpoint: chromeVersionInfo.webSocketDebuggerUrl,
});
};
var getBrowserInstance = async (launchArgs, noSandbox) => {
const LAUNCHER_CONNECTION_REFUSED_ERROR_CODE = 'ECONNREFUSED';
const LAUNCHER_NOT_INSTALLED_ERROR_CODE = 'ERR_LAUNCHER_NOT_INSTALLED';
const logger2 = logger_default(getBrowserInstance.name);
let browser;
let chrome;
try {
chrome = await launchSystemBrowser();
browser = await getSystemBrowserInstance(chrome, launchArgs);
} catch (e) {
const error = e;
if (error.code === LAUNCHER_CONNECTION_REFUSED_ERROR_CODE) {
logger2.warn(
`Chrome launcher could not connect to your system browser. Is your port ${error.port} accessible?`,
);
const prc = await find('port', error.port);
prc.forEach((pr) => {
logger2.log(
`Killing incompletely launched system chrome instance on pid ${pr.pid}`,
);
process.kill(pr.pid);
});
}
if (error.code === LAUNCHER_NOT_INSTALLED_ERROR_CODE) {
logger2.warn('Looks like Chrome is not installed on your system');
}
browser = await getLocalBrowserInstance(launchArgs, noSandbox);
}
return { browser, chrome };
};
var killBrowser = async (browser, chrome) => {
if (chrome) {
await browser.disconnect();
await chrome.kill();
} else {
await browser.close();
}
};
var browser_default = {
getBrowserInstance,
killBrowser,
};
// src/helpers/puppets.ts
import PQueue from 'p-queue';
var getAppleSplashScreenData = async (browser, options) => {
const logger2 = logger_default(getAppleSplashScreenData.name, options);
const page = await browser.newPage();
await page.setUserAgent(constants_default.EMULATED_USER_AGENT);
logger2.log(
`Navigating to Apple Human Interface Guidelines website - ${constants_default.APPLE_HIG_SPLASH_SCR_SPECS_URL}`,
);
await page.goto(constants_default.APPLE_HIG_SPLASH_SCR_SPECS_URL, {
waitUntil: 'networkidle0',
});
logger2.log('Waiting for the data table to be loaded');
try {
await page.waitForSelector('table', {
timeout: constants_default.WAIT_FOR_SELECTOR_TIMEOUT,
});
} catch (e) {
logger2.error(
`Could not find the table on the page within timeout ${constants_default.WAIT_FOR_SELECTOR_TIMEOUT}ms`,
);
throw e;
}
const splashScreenData = await page.evaluate(() => {
const scrapeSplashScreenDataFromHIGPage = () => {
return Array.from(
document
.querySelectorAll(
`#iOS-iPadOS-device-screen-dimensions + .table-wrapper > table`,
)?.[0]
.querySelectorAll('tbody tr'),
).map((tr) => {
const dimensionRegex = /(\d+)x(\d+)\spt\s\((\d+)x(\d+)\spx\s@(\d)x\)/gm;
const getParsedSpecs = (val) => {
const regexMatch = dimensionRegex.exec(val);
if (!regexMatch?.length) {
throw Error('Regex match failed while scraping the specs');
}
const widthInPoints = parseInt(regexMatch[1], 10);
const heightInPoints = parseInt(regexMatch[2], 10);
const scaleFactor = parseInt(regexMatch[5], 10);
if (
widthInPoints === 0 ||
Number.isNaN(widthInPoints) ||
heightInPoints === 0 ||
Number.isNaN(heightInPoints) ||
scaleFactor === 0 ||
Number.isNaN(scaleFactor)
) {
throw Error('Got unexpected dimensions while scraping the specs');
}
return {
width: widthInPoints * scaleFactor,
height: heightInPoints * scaleFactor,
scaleFactor,
};
};
const tableColumns = ['device', 'portrait'];
const columns = Array.from(tr.querySelectorAll('td'));
if (columns.length !== tableColumns.length) {
throw Error(
'Table columns on the page do not match with the scraper',
);
}
return columns.reduce(
(acc, curr, index) => {
if (index === 0) {
return {
...acc,
device: curr.innerText,
};
}
const specs = getParsedSpecs(curr.innerText.trim());
return {
...acc,
portrait: { width: specs.width, height: specs.height },
landscape: { width: specs.height, height: specs.width },
scaleFactor: specs.scaleFactor,
};
},
{
device: '',
portrait: { width: 0, height: 0 },
landscape: { width: 0, height: 0 },
scaleFactor: 0,
},
);
});
};
return scrapeSplashScreenDataFromHIGPage();
});
if (!splashScreenData.length) {
const err = `Failed scraping the data on web page ${constants_default.APPLE_HIG_SPLASH_SCR_SPECS_URL}`;
logger2.error(err);
throw Error(err);
}
logger2.log('Retrieved splash screen data');
await page.close();
return splashScreenData;
};
var getSplashScreenMetaData = async (options, browser) => {
const logger2 = logger_default(getSplashScreenMetaData.name, options);
if (!options.scrape) {
logger2.log(`Skipped scraping - using static data`);
return constants_default.APPLE_HIG_SPLASH_SCREEN_FALLBACK_DATA;
}
logger2.log(
'Initialising puppeteer to load latest splash screen metadata',
'\u{1F916}',
);
let splashScreenMetaData;
try {
splashScreenMetaData = await getAppleSplashScreenData(browser, options);
logger2.success('Loaded metadata for iOS platform');
} catch (e) {
const error = e;
logger2.error(error);
logger2.warn(
`Failed to fetch latest specs from Apple Human Interface guidelines - using static fallback data`,
);
throw error;
}
return splashScreenMetaData;
};
var canNavigateTo = (source) =>
(url_default.isUrl(source) && !file_default.isImageFile(source)) ||
file_default.isHtmlFile(source);
var saveImages = async (imageList, source, output, options, browser) => {
let address;
let shellHtml;
const logger2 = logger_default(saveImages.name, options);
logger2.log('Initialising puppeteer to take screenshots', '\u{1F916}');
if (canNavigateTo(source)) {
address = await url_default.getAddress(source, options);
} else {
shellHtml = await url_default.getShellHtml(source, options);
}
const queue = new PQueue({ concurrency: 1 });
const result = await queue.addAll(
imageList.map(
({ name, width, height, scaleFactor, orientation }) =>
async () => {
const { quality } = options;
const isIcon = name.includes('icon');
const isManifestIcon = name.includes('manifest-icon');
const type = isIcon ? 'png' : options.type;
const path3 = file_default.getImageSavePath(
name,
output,
type,
options.maskable,
isManifestIcon,
);
try {
const page = await browser.newPage();
await page.emulate({
userAgent: constants_default.EMULATED_USER_AGENT,
viewport: {
width: width / scaleFactor,
height: height / scaleFactor,
deviceScaleFactor: scaleFactor,
isLandscape: orientation === 'landscape',
},
});
if (address) {
if (options.darkMode) {
await page.emulateMediaFeatures([
{
name: 'prefers-color-scheme',
value: 'dark',
},
]);
}
await page.goto(address, { waitUntil: 'networkidle0' });
} else {
await page.setContent(shellHtml);
}
await page.screenshot({
path: path3,
omitBackground: !options.opaque,
...(type !== 'png' ? { quality } : {}),
});
await page.close();
logger2.success(`Saved image ${name}`);
return {
name,
width,
height,
scaleFactor,
path: path3,
orientation,
};
} catch (e) {
const error = e;
logger2.error(error.message);
throw Error(`Failed to save image ${name}`);
}
},
),
);
return result;
};
var generateImages = async (source, output, options) => {
const logger2 = logger_default(generateImages.name, options);
const isHtmlInput = canNavigateTo(source);
if (isHtmlInput) {
logger2.warn(
'noSandbox option is disabled for HTML inputs, use an image input instead',
);
}
const { browser, chrome } = await browser_default.getBrowserInstance(
{
timeout: constants_default.BROWSER_TIMEOUT,
args: constants_default.CHROME_LAUNCH_ARGS,
},
isHtmlInput ? false : options.noSandbox,
);
let splashScreenMetaData;
try {
splashScreenMetaData = await getSplashScreenMetaData(options, browser);
} catch (e) {
splashScreenMetaData =
constants_default.APPLE_HIG_SPLASH_SCREEN_FALLBACK_DATA;
}
const allImages = [
...(!options.iconOnly
? images_default.getSplashScreenImages(splashScreenMetaData, options)
: []),
...(!options.splashOnly ? images_default.getIconImages(options) : []),
];
if (
!(
(await file_default.exists(output)) &&
(await file_default.isPathAccessible(output, file_default.WRITE_ACCESS))
)
) {
file_default.makeDirRecursiveSync(output);
logger2.warn(
`Looks like folder ${output} doesn't exist. Created one for you`,
);
}
const savedImages = await saveImages(
allImages,
source,
output,
options,
browser,
);
try {
await browser_default.killBrowser(browser, chrome);
} catch (e) {}
return savedImages;
};
var puppets_default = {
getSplashScreenMetaData,
saveImages,
generateImages,
};
// src/helpers/flags.ts
import os from 'os';
var normalizeOnlyFlagPairs = (flag1Key, flag2Key, opts, logger2) => {
const stripOnly = (key) => key.replace('Only', '');
if (opts[flag1Key] && opts[flag2Key]) {
logger2.warn(
`Hmm, you want to _only_ generate both ${stripOnly(
flag1Key,
)} and ${stripOnly(
flag2Key,
)} set. Ignoring --x-only settings as this is default behavior`,
);
return {
[flag1Key]: false,
[flag2Key]: false,
};
}
return {};
};
var normalizeOutput = (output) => {
if (!output) {
return '.';
}
return output;
};
var getDefaultOptions = () => {
const flags = constants_default.FLAGS;
return Object.keys(flags)
.filter((flagKey) => flags[flagKey].hasOwnProperty('default'))
.reduce((acc, curr) => {
return {
...acc,
[curr]: flags[curr].default,
};
}, {});
};
var normalizeSandboxOption = (noSandbox, logger2) => {
let sandboxDisabled = false;
if (noSandbox) {
if (os.platform() !== 'linux') {
logger2.warn(
'Disabling sandbox is only relevant on Linux platforms, request declined!',
);
} else {
sandboxDisabled = true;
}
}
return {
noSandbox: sandboxDisabled,
};
};
var flags_default = {
normalizeOnlyFlagPairs,
normalizeOutput,
getDefaultOptions,
normalizeSandboxOption,
};
// src/main.ts
async function generateImages2(source, outputFolderPath, options, loggerFn) {
let modOptions;
const logger2 = loggerFn || logger_default(generateImages2.name, options);
if (!source) {
throw Error('Please specify a URL or file path as a source');
}
if (options) {
modOptions = {
...flags_default.getDefaultOptions(),
...options,
...flags_default.normalizeOnlyFlagPairs(
'splashOnly',
'iconOnly',
options,
logger2,
),
...flags_default.normalizeOnlyFlagPairs(
'landscapeOnly',
'portraitOnly',
options,
logger2,
),
...flags_default.normalizeSandboxOption(options.noSandbox, logger2),
};
} else {
modOptions = {
...flags_default.getDefaultOptions(),
};
}
const output = flags_default.normalizeOutput(outputFolderPath);
const savedImages = await puppets_default.generateImages(
source,
output,
modOptions,
);
const manifestJsonContent = meta_default.generateIconsContentForManifest(
savedImages,
modOptions,
);
const htmlMeta = meta_default.generateHtmlForIndexPage(
savedImages,
modOptions,
);
if (!modOptions.splashOnly) {
if (modOptions.manifest) {
await meta_default.addIconsToManifest(
manifestJsonContent,
modOptions.manifest,
);
logger2.success(
`Icons are saved to Web App Manifest file ${modOptions.manifest}`,
);
} else if (!modOptions.splashOnly) {
logger2.warn(
'Web App Manifest file is not specified, printing out the content to console instead',
);
logger2.success(
'Below is the icons content for your manifest.json file. You can copy/paste it manually',
);
logger2.raw(`
${JSON.stringify(manifestJsonContent, null, 2)}
`);
}
}
if (modOptions.index) {
await meta_default.addMetaTagsToIndexPage(
htmlMeta,
modOptions.index,
modOptions.xhtml,
);
logger2.success(
`iOS meta tags are saved to index html file ${modOptions.index}`,
);
} else {
logger2.warn(
'Index html file is not specified, printing out the content to console instead',
);
logger2.success(
'Below is the iOS meta tags content for your index.html file. You can copy/paste it manually',
);
logger2.raw(`
${meta_default.formatMetaTags(htmlMeta)}
`);
}
return {
savedImages,
htmlMeta,
manifestJsonContent,
};
}
var appleDeviceSpecsForLaunchImages =
constants_default.APPLE_HIG_SPLASH_SCREEN_FALLBACK_DATA;
export {
constants_default,
logger_default,
generateImages2 as generateImages,
appleDeviceSpecsForLaunchImages,
};