diff --git a/lib/generate.cjs b/lib/generate.cjs index 1f7bacf..9f58a6a 100644 --- a/lib/generate.cjs +++ b/lib/generate.cjs @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const consola = require('consola') -const { remove, mkdirp } = require('fs-extra') -const { join } = require('pathe') +const { remove, mkdirp, existsSync, copy } = require('fs-extra') +const { join, resolve } = require('pathe') const sharp = require('sharp') +const pkgName = require(join(__dirname, '../package.json')).name + const resizeOptions = { fit: 'contain', background: 'transparent', @@ -15,57 +17,66 @@ async function ensureDir (dir) { await mkdirp(dir) } -async function generate ({ input, distDir, sizes, maskableInput, maskablePadding, splash, hash }) { - await ensureDir(distDir) +async function generate ({ input, buildAssetsDir, targetDir, sizes, maskableInput, maskablePadding, splash, hash }) { + const cacheDir = resolve(`node_modules/.cache/${pkgName}/${hash}`) + + if (!existsSync(cacheDir)) { + const iconCacheDir = join(cacheDir, targetDir) + await ensureDir(iconCacheDir) - // Resized icons (purpose: any) - await Promise.all(sizes.map(size => - sharp(input) - .resize(Math.floor(size), Math.floor(size), resizeOptions) - .toFile(join(distDir, `${size}x${size}${hash}.png`)), - )) + // Resized icons (purpose: any) + await Promise.all(sizes.map(size => + sharp(input) + .resize(Math.floor(size), Math.floor(size), resizeOptions) + .toFile(join(iconCacheDir, `${size}x${size}.${hash}.png`)), + )) - // Resized icons (purpose: maskable) - await Promise.all(sizes.map(async (size) => { - await sharp({ - create: { - width: size, - height: size, - channels: 4, - background: { r: 0, g: 0, b: 0, alpha: 0 }, - }, - }).composite([{ - input: await sharp(maskableInput) - .resize(Math.floor(size * (1 - maskablePadding / 100)), Math.floor(size * (1 - maskablePadding / 100)), resizeOptions) - .toBuffer(), - }]) - .toFile(join(distDir, `${size}x${size}.maskable${hash}.png`)) - }, - )) + // Resized icons (purpose: maskable) + await Promise.all(sizes.map(async (size) => { + await sharp({ + create: { + width: size, + height: size, + channels: 4, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }, + }).composite([{ + input: await sharp(maskableInput) + .resize(Math.floor(size * (1 - maskablePadding / 100)), Math.floor(size * (1 - maskablePadding / 100)), resizeOptions) + .toBuffer(), + }]) + .toFile(join(iconCacheDir, `${size}x${size}.maskable.${hash}.png`)) + }, + )) - // Splash - if (splash) { - distDir = join(distDir, `../${splash.targetDir}`) - await ensureDir(distDir) - await Promise.all(splash.devices.map(async (device) => { - try { - await sharp({ - create: { - width: device.width, - height: device.height, - channels: 4, - background: splash.backgroundColor, - }, - }).composite([{ - input: await sharp(input).resize(512, 512, resizeOptions).toBuffer(), - }]) - .toFile(join(distDir, `${device.width}x${device.height}${hash}.png`)) - } - catch (err) { - consola.warn(`[PWA] Failed to generate splash (${device.width}x${device.height}) because icon input has larger dimensions`) - } - })) + // Splash + if (splash) { + const splashCacheDir = join(cacheDir, splash.targetDir) + await ensureDir(splashCacheDir) + + await Promise.all(splash.devices.map(async (device) => { + try { + await sharp({ + create: { + width: device.width, + height: device.height, + channels: 4, + background: splash.backgroundColor, + }, + }).composite([{ + input: await sharp(input).resize(512, 512, resizeOptions).toBuffer(), + }]) + .toFile(join(cacheDir, splash.targetDir, `${device.width}x${device.height}.${hash}.png`)) + } + catch (err) { + consola.warn(`[PWA] Failed to generate splash (${device.width}x${device.height}) because icon input has larger dimensions`) + } + })) + } } + + await ensureDir(buildAssetsDir) + await copy(cacheDir, buildAssetsDir) } generate(JSON.parse(process.argv[2])).then(() => { diff --git a/src/parts/icon/index.ts b/src/parts/icon/index.ts index ada2f90..76497ab 100644 --- a/src/parts/icon/index.ts +++ b/src/parts/icon/index.ts @@ -1,7 +1,7 @@ import { existsSync } from 'node:fs' import { fork } from 'node:child_process' import consola from 'consola' -import { join, relative, resolve } from 'pathe' +import { relative, resolve } from 'pathe' import { provider } from 'std-env' import { joinURL } from 'ufo' import { addTemplate, useNuxt } from '@nuxt/kit' @@ -47,7 +47,7 @@ export default async (pwa: PWAContext) => { options.sizes = [64, 120, 144, 152, 192, 384, 512] // Hash as suffix for production - const hash = nuxt.options.dev ? '' : `.${await getFileHash(options.source)}` + const hash = await getFileHash(options.source) const iconsDir = joinURL( nuxt.options.app.baseURL, @@ -85,7 +85,8 @@ export default async (pwa: PWAContext) => { const generateOptions = JSON.stringify({ input: options.source, - distDir: join(pwa._buildAssetsDir, options.targetDir), + buildAssetsDir: pwa._buildAssetsDir, + targetDir: options.targetDir, sizes: options.sizes, maskableInput: options.maskableSource, maskablePadding: options.maskablePadding, diff --git a/src/parts/icon/splash.ts b/src/parts/icon/splash.ts index 75218b7..c286859 100644 --- a/src/parts/icon/splash.ts +++ b/src/parts/icon/splash.ts @@ -46,7 +46,7 @@ export const metaFromDevice = (device: Device, options: { assetsDir: string; has const { assetsDir, hash } = options return { - href: join(assetsDir, `${width}x${height}${hash}.png`), + href: join(assetsDir, `${width}x${height}.${hash}.png`), media: [ `(device-width: ${width / pixelRatio}px)`, `(device-height: ${height / pixelRatio}px)`, diff --git a/src/parts/icon/utils.ts b/src/parts/icon/utils.ts index a60c56a..2966aff 100644 --- a/src/parts/icon/utils.ts +++ b/src/parts/icon/utils.ts @@ -9,7 +9,7 @@ export async function getFileHash (filePath: string): Promise { export function makeManifestIcon ({ iconsDir, size, purpose, hash }: ManifestIconMakerOptions): ManifestIcon { return { - src: joinURL(iconsDir, `${size}x${size}${purpose === 'maskable' ? '.maskable' : ''}${hash}.png`), + src: joinURL(iconsDir, `${size}x${size}${purpose === 'maskable' ? '.maskable' : ''}.${hash}.png`), type: 'image/png', sizes: `${size}x${size}`, purpose,