Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow use assets in webmanifest icons #397

Closed
wants to merge 10 commits into from
54 changes: 53 additions & 1 deletion docs/guide/static-assets.md
Expand Up @@ -4,10 +4,62 @@ title: Static assets handling | Guide

# Static assets handling

By default, all icons on `PWA Web App Manifest` option found under `Vite's publicDir` option directory, will be included in the service worker *precache*. You can disable this option using `includeManifestIcons: false`.
By default, all icons on `PWA Web App Manifest` option found under Vite's `publicDir` option directory, will be included in the service worker *precache*. You can disable this option using `includeManifestIcons: false`.

You can also add another static assets such as `favicon`, `svg` and `font` files using `includeAssets` option. The `includeAssets` option will be resolved using `fast-glob` found under Vite's `publicDir` option directory, and so you can use regular expressions to include those assets, for example: `includeAssets: ['fonts/*.ttf', 'images/*.png']`. You don't need to configure `PWA Manifest icons` on `includeAssets` option.

## Reusing src/assets images

::: warning
This feature is only available from version `0.13.4+`.
:::

If you are using images in your application via `src/assets` directory (or any other directory), and you want to reuse those images in your `PWA Manifest` icons, you can use them with these 3 limitations:
- any image under `src/assets` directory (or any other directory) must be used in your application via static import or directly on the `src` attribute
- you must reference the images in the `PWA Manifest` icons using the assets directory path relative to the root folder: `./src/assets/logo.png` or `src/assets/logo.png`
- inlined icons cannot be used, in that case you will need to copy/move those images to the Vite's `publicDir` option directory: refer to [Importing Asset as URL](https://vitejs.dev/guide/assets.html#importing-asset-as-url) and [Vite's assetsInlineLimit option](https://vitejs.dev/config/build-options.html#build-assetsinlinelimit)


::: warning
If you're using `PWA Manifest` icons from any asset folder, but you are not using those images in your application (via static import or in src attribute), Vite will not emit those assets, and so missing from the build output:

```shell
Error while trying to use the following icon from the Manifest: https://localhost/src/assets/pwa-192x192.png (Download error or resource isn't a valid image)
```

In that case, you need to copy or move those images to the Vite's `publicDir` option directory (defaults to `public`) and configure the icons properly.
:::

For example, if you have the following image `src/assets/logo-192x192.png` you can add it to your `PWA Manifest` icon using:

```json
{
"src": "./src/assets/logo-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
```

then, in your codebase, you must use it via static import:

```js
// src/main.js or src/main.ts
// can be any js/ts/jsx/tsx module or single file component
import logo from './assets/logo-192x192.png'

document.getElementById('logo-img').src = logo
```

or using the `src` attribute:

```js
// src/main.js or src/main.ts
// can be any js/ts/jsx/tsx module or single file component
document.getElementById('#app').innerHTML = `
<img src="./assets/logo-192x192.png" alt="Logo" width="192" height="192" />
`
```

## globPatterns

If you need to include other assets that are not under Vite's `publicDir` option directory, you can use the `globPatterns` parameter of [workbox](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.generateSW) or [injectManifest](https://developers.google.com/web/tools/workbox/reference-docs/latest/module-workbox-build#.injectManifest) plugin options.
Expand Down
14 changes: 13 additions & 1 deletion examples/vue-router/src/App.vue
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { onBeforeMount, ref } from 'vue'
import { useTimeAgo } from '@vueuse/core'
import MyWorker from './my-worker?worker'
import MyWorker from './my-worker.js?worker'
// DONT'T REMOVE: ASSETS TESTS
// import logo from './assets/pwa-192x192.png'

import ReloadPrompt from './ReloadPrompt.vue'

Expand Down Expand Up @@ -49,5 +51,15 @@ onBeforeMount(() => {
<template v-if="pong">
Response from web worker: <span> Message: {{ pong }} </span>&#160;&#160;<span> Using ENV mode: {{ mode }}</span>
</template>
<br>
<br>
<!-- DONT'T REMOVE: ASSETS TESTS -->
<!--
<img :src="logo" width="192" height="192" alt="Vite PWA logo">
-->
<img src="./assets/pwa-192x192.png" width="192" height="192" alt="Vite PWA logo">
<!--
<img src="./assets/pwa-32x32.png?url" width="32" height="32" alt="Vite PWA logo">
-->
<ReloadPrompt />
</template>
Binary file added examples/vue-router/src/assets/pwa-192x192.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/vue-router/src/assets/pwa-32x32.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions examples/vue-router/vite.config.ts
Expand Up @@ -13,11 +13,25 @@ const pwaOptions: Partial<VitePWAOptions> = {
short_name: 'PWA Router',
theme_color: '#ffffff',
icons: [
/*
{
src: 'src/assets/pwa-32x32.png', // assets test
sizes: '192x192',
type: 'image/png',
},
*/
{
src: 'src/assets/pwa-192x192.png', // assets test
sizes: '192x192',
type: 'image/png',
},
/*
{
src: 'pwa-192x192.png', // <== don't add slash, for testing
sizes: '192x192',
type: 'image/png',
},
*/
{
src: '/pwa-512x512.png', // <== don't remove slash, for testing
sizes: '512x512',
Expand Down
2 changes: 1 addition & 1 deletion src/api.ts
Expand Up @@ -32,7 +32,7 @@ export function _generateBundle({ options, viteConfig, useImportRegister }: PWAP
isAsset: true,
type: 'asset',
name: undefined,
source: generateWebManifestFile(options),
source: generateWebManifestFile(options, bundle),
fileName: options.manifestFilename,
}
}
Expand Down
67 changes: 59 additions & 8 deletions src/assets.ts
Expand Up @@ -4,6 +4,7 @@ import crypto from 'crypto'
import fg from 'fast-glob'
import type { GenerateSWOptions, InjectManifestOptions, ManifestEntry } from 'workbox-build'
import type { ResolvedConfig } from 'vite'
import type { OutputBundle } from 'rollup'
import type { ResolvedVitePWAOptions } from './types'

function buildManifestEntry(
Expand Down Expand Up @@ -120,15 +121,65 @@ export async function configureStaticAssets(
revision: `${cHash.digest('hex')}`,
})
}
if (manifestEntries.length > 0) {
if (useInjectManifest)
injectManifest.additionalManifestEntries = manifestEntries
if (useInjectManifest)
injectManifest.additionalManifestEntries = manifestEntries ?? []
else
workbox.additionalManifestEntries = manifestEntries ?? []
}

else
workbox.additionalManifestEntries = manifestEntries
export function generateWebManifestFile(options: ResolvedVitePWAOptions, bundle?: OutputBundle): string {
const { manifest } = options
if (bundle && manifest && manifest.icons) {
const manifestEntries = options.strategies === 'generateSW'
? options.workbox.additionalManifestEntries!
: options.injectManifest.additionalManifestEntries!
const assets: Map<string, string> = Object.keys(bundle).reduce((acc, key) => {
const { fileName, name } = bundle[key]
if (name)
acc.set(normalizeIconPath(name.startsWith('.') ? name.slice(1) : name), normalizeIconPath(fileName))

return acc
}, new Map())
manifest.icons = manifest.icons.map((icon) => {
const iconSrc = icon.src
if (iconSrc) {
const src = normalizeIconPath(iconSrc.startsWith('.') ? iconSrc.slice(1) : iconSrc)
const url = assets.get(src)
if (url) {
// remove it from the manifest if present
manifestEntries.splice(0, manifestEntries.length, ...manifestEntries.filter((me) => {
if (typeof me === 'string')
return me !== iconSrc
else
return me.url !== iconSrc
}))
// we also need to add it to the sw precache manifest
manifestEntries.push({ url, revision: null })
icon.src = url
}
}

return icon
})
// we also need to recalculate the revision for the new webmanifest
const webManifest = options.manifestFilename
const idx = manifestEntries.findIndex((me) => {
if (typeof me === 'string')
return me === webManifest
else
return me.url === webManifest
})
if (idx > -1) {
const newWebManifest = `${JSON.stringify(manifest, null, options.minify ? 0 : 2)}\n`
const cHash = crypto.createHash('MD5')
cHash.update(newWebManifest)
manifestEntries.splice(idx, 1, {
url: webManifest,
revision: `${cHash.digest('hex')}`,
})
return newWebManifest
}
}
}

export function generateWebManifestFile(options: ResolvedVitePWAOptions): string {
return `${JSON.stringify(options.manifest, null, options.minify ? 0 : 2)}\n`
return `${JSON.stringify(manifest, null, options.minify ? 0 : 2)}\n`
}
2 changes: 1 addition & 1 deletion src/plugins/build.ts
Expand Up @@ -23,7 +23,7 @@ export function BuildPlugin(ctx: PWAPluginContext) {
},
},
generateBundle(_, bundle) {
return _generateBundle(ctx, bundle)
_generateBundle(ctx, bundle)
},
closeBundle: {
sequential: true,
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/dev.ts
Expand Up @@ -155,7 +155,7 @@ export function DevPlugin(ctx: PWAPluginContext): Plugin {
}
return await fs.readFile(swDest, 'utf-8')
}

const key = normalizePath(`${options.base}${id.startsWith('/') ? id.slice(1) : id}`)

if (swDevOptions.workboxPaths.has(key))
Expand Down