Skip to content

Commit

Permalink
feat!: refactor api (#25)
Browse files Browse the repository at this point in the history
* feat!: refactor api

* chore: remove log from generate-assets

* chore: include original image name in the instruction

* chore: cleanup generate-assets

* chore: include pwa manifest icons + move some tests

* chore: split instructor resolver

* chore: rename icons resolver module

* chore: add manifest CLI option
  • Loading branch information
userquin committed Dec 12, 2023
1 parent 1182fcd commit f0a73cf
Show file tree
Hide file tree
Showing 21 changed files with 1,217 additions and 543 deletions.
4 changes: 4 additions & 0 deletions build.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export default defineBuildConfig({
{ input: 'src/presets/minimal', name: 'presets/minimal' },
{ input: 'src/presets/minimal-2023', name: 'presets/minimal-2023' },
{ input: 'src/api/index', name: 'api' },
{ input: 'src/api/instructions', name: 'api/instructions' },
{ input: 'src/api/generate-assets', name: 'api/generate-assets' },
{ input: 'src/api/generate-html-markup', name: 'api/generate-html-markup' },
{ input: 'src/api/generate-manifest-icons-entry', name: 'api/generate-manifest-icons-entry' },
{ input: 'src/cli', name: 'cli' },
],
clean: true,
Expand Down
26 changes: 21 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./api": {
"import": "./dist/api.mjs",
"require": "./dist/api.cjs"
},
"./config": {
"import": "./dist/config.mjs",
"require": "./dist/config.cjs"
Expand All @@ -46,6 +42,26 @@
"import": "./dist/presets/minimal-2023.mjs",
"require": "./dist/presets/minimal-2023.cjs"
},
"./api": {
"import": "./dist/api.mjs",
"require": "./dist/api.cjs"
},
"./api/instructions": {
"import": "./dist/api/instructions.mjs",
"require": "./dist/api/instructions.cjs"
},
"./api/generate-html-markup": {
"import": "./dist/api/generate-html-markup.mjs",
"require": "./dist/api/generate-html-markup.cjs"
},
"./api/generate-manifest-icons-entry": {
"import": "./dist/api/generate-manifest-icons-entry.mjs",
"require": "./dist/api/generate-manifest-icons-entry.cjs"
},
"./api/generate-assets": {
"import": "./dist/api/generate-assets.mjs",
"require": "./dist/api/generate-assets.cjs"
},
"./*": "./*"
},
"main": "dist/index.cjs",
Expand Down Expand Up @@ -76,7 +92,7 @@
"prepublishOnly": "npm run build",
"release": "bumpp && npm publish",
"run-playground": "pnpm -C playground run build",
"test": "vitest"
"test": "vitest run"
},
"dependencies": {
"cac": "^6.7.14",
Expand Down
7 changes: 6 additions & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{
"private": true,
"scripts": {
"build": "pwa-assets-generator"
"build": "pwa-assets-generator",
"build-no-override": "pwa-assets-generator --override=false",
"build-no-manifest": "pwa-assets-generator --manifest=false",
"build-n-override": "pwa-assets-generator -o=false",
"build-n-manifest": "pwa-assets-generator -m=false",
"help": "pwa-assets-generator -h"
},
"devDependencies": {
"@vite-pwa/assets-generator": "workspace:*"
Expand Down
201 changes: 201 additions & 0 deletions src/api/apple-icons-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import type { PngOptions, ResizeOptions } from 'sharp'
import type { AppleDeviceSize, AppleSplashScreens, ResolvedAppleSplashScreens } from '../types.ts'
import { defaultSplashScreenName } from '../splash.ts'
import { createPngCompressionOptions, createResizeOptions } from './defaults.ts'
import type { ImageAssets, ImageAssetsInstructions } from './types.ts'
import { generateMaskableAsset } from './maskable.ts'
import { createAppleSplashScreenHtmlLink } from './html.ts'

export function resolveAppleSplashScreensInstructions(
imageAssets: ImageAssets,
instructions: ImageAssetsInstructions,
useAppleSplashScreens?: AppleSplashScreens,
) {
const appleSplashScreens = resolveAppleSplashScreens(useAppleSplashScreens)
if (!appleSplashScreens || !appleSplashScreens.sizes.length)
return

const { linkMediaOptions, name: resolveName, sizes, png } = appleSplashScreens

const sizesMap = new Map<number, number>()
const splashScreens: SplashScreenData[] = sizes.reduce((acc, size) => {
// cleanup duplicates screen dimensions:
// should we add the links (maybe scaleFactor is different)?
if (sizesMap.get(size.width) === size.height)
return acc

sizesMap.set(size.width, size.height)
const { width: height, height: width, ...restSize } = size
const {
width: lheight,
height: lwidth,
...restResizeOptions
} = size.resizeOptions || {}
const landscapeSize: AppleDeviceSize = {
...restSize,
width,
height,
resizeOptions: {
...restResizeOptions,
width: lwidth,
height: lheight,
},
}
acc.push({
size,
landscape: false,
dark: size.darkResizeOptions ? false : undefined,
resizeOptions: size.resizeOptions,
padding: size.padding ?? 0.3,
png: size.png ?? png,
})
acc.push({
size: landscapeSize,
landscape: true,
dark: size.darkResizeOptions ? false : undefined,
resizeOptions: landscapeSize.resizeOptions,
padding: size.padding ?? 0.3,
png: size.png ?? png,
})
if (size.darkResizeOptions) {
const {
width: dlheight,
height: dlwidth,
...restDarkResizeOptions
} = size.darkResizeOptions
const landscapeDarkResizeOptions: ResizeOptions = { ...restDarkResizeOptions, width: dlwidth, height: dlheight }
const landscapeDarkSize: AppleDeviceSize = {
...restSize,
width,
height,
resizeOptions: landscapeDarkResizeOptions,
darkResizeOptions: undefined,
}
acc.push({
size,
landscape: false,
dark: true,
resizeOptions: size.darkResizeOptions,
padding: size.padding ?? 0.3,
png: size.png ?? png,
})
acc.push({
size: landscapeDarkSize,
landscape: true,
dark: true,
resizeOptions: landscapeDarkResizeOptions,
padding: size.padding ?? 0.3,
png: size.png ?? png,
})
}
return acc
}, [] as SplashScreenData[])

sizesMap.clear()

for (const size of splashScreens) {
const name = resolveName(size.landscape, size.size, size.dark)
const url = `${imageAssets.basePath}${name}`
const promise = () => generateMaskableAsset('png', imageAssets.imageName, size.size, {
padding: size.padding,
resizeOptions: {
...size.resizeOptions,
background: size.resizeOptions?.background ?? (size.dark ? 'black' : 'white'),
},
outputOptions: size.png,
})
instructions.appleSplashScreen[url] = {
name,
url,
width: size.size.width,
height: size.size.height,
mimeType: 'image/png',
link: createAppleSplashScreenHtmlLink('string', {
size: size.size,
landscape: size.landscape,
addMediaScreen: linkMediaOptions.addMediaScreen,
xhtml: linkMediaOptions.xhtml,
name: resolveName,
basePath: linkMediaOptions.basePath,
dark: size.dark,
includeId: imageAssets.htmlLinks.includeId,
}),
linkObject: createAppleSplashScreenHtmlLink('link', {
size: size.size,
landscape: size.landscape,
addMediaScreen: linkMediaOptions.addMediaScreen,
xhtml: linkMediaOptions.xhtml,
name: resolveName,
basePath: linkMediaOptions.basePath,
dark: size.dark,
includeId: imageAssets.htmlLinks.includeId,
}),
buffer: () => promise().then(m => m.toBuffer()),
}
}
}

interface SplashScreenData {
size: AppleDeviceSize
landscape: boolean
dark?: boolean
resizeOptions?: ResizeOptions
padding: number
png: PngOptions
}

function resolveAppleSplashScreens(
useAppleSplashScreens?: AppleSplashScreens,
) {
let appleSplashScreens: ResolvedAppleSplashScreens | undefined
if (useAppleSplashScreens) {
const {
padding = 0.3,
resizeOptions: useResizeOptions = {},
darkResizeOptions: useDarkResizeOptions = {},
linkMediaOptions: useLinkMediaOptions = {},
sizes,
name = defaultSplashScreenName,
png: usePng = {},
} = useAppleSplashScreens

// Initialize defaults
const resizeOptions = createResizeOptions(false, useResizeOptions)
const darkResizeOptions = createResizeOptions(true, useDarkResizeOptions)
const png: PngOptions = createPngCompressionOptions(usePng)

for (const size of sizes) {
if (typeof size.padding === 'undefined')
size.padding = padding

if (typeof size.png === 'undefined')
size.png = png

if (typeof size.resizeOptions === 'undefined')
size.resizeOptions = resizeOptions

if (typeof size.darkResizeOptions === 'undefined')
size.darkResizeOptions = darkResizeOptions
}
const {
log = true,
addMediaScreen = true,
basePath = '/',
xhtml = false,
} = useLinkMediaOptions
appleSplashScreens = {
padding,
sizes,
linkMediaOptions: {
log,
addMediaScreen,
basePath,
xhtml,
},
name,
png,
}
}

return appleSplashScreens
}
43 changes: 43 additions & 0 deletions src/api/generate-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { resolve } from 'node:path'
import { existsSync } from 'node:fs'
import { writeFile } from 'node:fs/promises'
import type { IconAsset, ImageAssetsInstructions } from './types.ts'

export async function generateAssets(
instruction: ImageAssetsInstructions,
overrideAssets: boolean,
folder: string,
log?: (message: string, ignored: boolean) => void,
) {
const transparent = Array.from(Object.values(instruction.transparent))
await Promise.all(transparent.map(icon => generateAsset('PNG', icon, folder, overrideAssets, log)))
const maskable = Array.from(Object.values(instruction.maskable))
await Promise.all(maskable.map(icon => generateAsset('PNG', icon, folder, overrideAssets, log)))
const apple = Array.from(Object.values(instruction.apple))
await Promise.all(apple.map(icon => generateAsset('PNG', icon, folder, overrideAssets, log)))
const favicon = Array.from(Object.values(instruction.favicon))
await Promise.all(favicon.filter(icon => !icon.name.endsWith('.svg')).map(icon => generateAsset('ICO', icon, folder, overrideAssets, log)))
const appleSplashScreen = Array.from(Object.values(instruction.appleSplashScreen))
await Promise.all(appleSplashScreen.map(icon => generateAsset('PNG', icon, folder, overrideAssets, log)))
}

async function generateAsset(
type: 'ICO' | 'PNG',
icon: IconAsset<any>,
folder: string,
overrideAssets: boolean,
log?: (message: string, ignored: boolean) => void,
) {
const filePath = resolve(folder, icon.name)
if (!overrideAssets && existsSync(filePath)) {
log?.(`Skipping, ${type} file already exists: ${filePath}`, true)
return
}

await icon
.buffer()
.then(b => writeFile(resolve(folder, icon.name), b))
.then(() => {})

log?.(`Generated ${type} file: ${filePath}`, false)
}
13 changes: 13 additions & 0 deletions src/api/generate-html-markup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ImageAssetsInstructions } from './types.ts'

export function generateHtmlMarkup(instruction: ImageAssetsInstructions) {
const apple = Array.from(Object.values(instruction.apple))
const favicon = Array.from(Object.values(instruction.favicon))
const appleSplashScreen = Array.from(Object.values(instruction.appleSplashScreen))
const links: string[] = []
favicon.forEach(icon => icon.link?.length && links.push(icon.link))
apple.forEach(icon => icon.link?.length && links.push(icon.link))
appleSplashScreen.forEach(icon => icon.link?.length && links.push(icon.link))

return links
}
30 changes: 30 additions & 0 deletions src/api/generate-manifest-icons-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ImageAssetsInstructions, ManifestIcons, ManifestIconsOptionsType, ManifestIconsType } from './types.ts'

export function generateManifestIconsEntry<Format extends ManifestIconsType>(
format: Format,
instruction: ImageAssetsInstructions,
): ManifestIconsOptionsType<Format> {
const icons: ManifestIcons = { icons: [] }

for (const icon of Object.values(instruction.transparent)) {
icons.icons.push({
src: icon.name,
sizes: `${icon.width}x${icon.height}`,
type: icon.mimeType,
})
}
for (const icon of Object.values(instruction.maskable)) {
icons.icons.push({
src: icon.name,
sizes: `${icon.width}x${icon.height}`,
type: icon.mimeType,
purpose: 'maskable',
})
}

return (
format === 'string'
? JSON.stringify(icons, null, 2)
: icons
) as ManifestIconsOptionsType<Format>
}

0 comments on commit f0a73cf

Please sign in to comment.