diff --git a/e2e/cases/html/app-icon/index.test.ts b/e2e/cases/html/app-icon/index.test.ts
index 3529573aa1..6a320b7ddd 100644
--- a/e2e/cases/html/app-icon/index.test.ts
+++ b/e2e/cases/html/app-icon/index.test.ts
@@ -1,12 +1,14 @@
import { build } from '@e2e/helper';
import { expect, test } from '@playwright/test';
-test('should emit app icon to dist path', async () => {
+test('should emit apple-touch-icon to dist path', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
html: {
- appIcon: '../../../assets/icon.png',
+ appIcon: {
+ icons: [{ src: '../../../assets/icon.png', size: 180 }],
+ },
},
},
});
@@ -20,16 +22,18 @@ test('should emit app icon to dist path', async () => {
files[Object.keys(files).find((file) => file.endsWith('index.html'))!];
expect(html).toContain(
- '',
+ '',
);
});
-test('should apply asset prefix to app icon URL', async () => {
+test('should apply asset prefix to apple-touch-icon URL', async () => {
const rsbuild = await build({
cwd: __dirname,
rsbuildConfig: {
html: {
- appIcon: '../../../assets/icon.png',
+ appIcon: {
+ icons: [{ src: '../../../assets/icon.png', size: 180 }],
+ },
},
output: {
assetPrefix: 'https://www.example.com',
@@ -48,6 +52,6 @@ test('should apply asset prefix to app icon URL', async () => {
files[Object.keys(files).find((file) => file.endsWith('index.html'))!];
expect(html).toContain(
- '',
+ '',
);
});
diff --git a/e2e/cases/html/combined/html.test.ts b/e2e/cases/html/combined/html.test.ts
index 5cec492492..6470b24c6c 100644
--- a/e2e/cases/html/combined/html.test.ts
+++ b/e2e/cases/html/combined/html.test.ts
@@ -24,7 +24,9 @@ test.describe('should combine multiple html config correctly', () => {
description: 'a description of the page',
},
inject: 'body',
- appIcon: '../../../assets/icon.png',
+ appIcon: {
+ icons: [{ src: '../../../assets/icon.png', size: 180 }],
+ },
favicon: '../../../assets/icon.png',
},
},
@@ -46,7 +48,9 @@ test.describe('should combine multiple html config correctly', () => {
test('appicon', async () => {
const [, iconRelativePath] =
- //.exec(mainContent) || [];
+ //.exec(
+ mainContent,
+ ) || [];
expect(iconRelativePath).toBeDefined();
diff --git a/packages/core/src/helpers/index.ts b/packages/core/src/helpers/index.ts
index 7511d96866..1e0107f29d 100644
--- a/packages/core/src/helpers/index.ts
+++ b/packages/core/src/helpers/index.ts
@@ -143,7 +143,7 @@ export const getPublicPathFromChain = (
};
export const getPublicPathFromCompiler = (
- compiler: Rspack.Compiler,
+ compiler: Rspack.Compiler | Rspack.Compilation,
): string => {
const { publicPath } = compiler.options.output;
diff --git a/packages/core/src/plugins/appIcon.ts b/packages/core/src/plugins/appIcon.ts
index 69c439214c..0987e7bc58 100644
--- a/packages/core/src/plugins/appIcon.ts
+++ b/packages/core/src/plugins/appIcon.ts
@@ -1,91 +1,150 @@
import fs from 'node:fs';
import path from 'node:path';
-import { ensureAssetPrefix, isFileExists } from '../helpers';
-import type { EnvironmentContext, RsbuildPlugin } from '../types';
+import {
+ ensureAssetPrefix,
+ getPublicPathFromCompiler,
+ isFileExists,
+} from '../helpers';
+import type { AppIconItem, HtmlBasicTag, RsbuildPlugin } from '../types';
export const pluginAppIcon = (): RsbuildPlugin => ({
name: 'rsbuild:app-icon',
setup(api) {
- const cache = new Map<
+ const htmlTagsMap = new Map();
+ const iconPathMap = new Map<
string,
- { absolutePath: string; relativePath: string }
+ { absolutePath: string; relativePath: string; requestPath: string }
>();
- const getIconPath = ({ config, name }: EnvironmentContext) => {
- const { appIcon } = config.html;
- if (!appIcon) {
- return;
- }
+ const formatIcon = (
+ icon: AppIconItem,
+ distDir: string,
+ publicPath: string,
+ ) => {
+ const { src, size } = icon;
+ const cached = iconPathMap.get(src);
+ const sizes = `${size}x${size}`;
- const cached = cache.get(name);
if (cached) {
- cached;
+ return {
+ sizes,
+ ...cached,
+ ...icon,
+ };
}
- const distDir = config.output.distPath.image;
- const absolutePath = path.isAbsolute(appIcon)
- ? appIcon
- : path.join(api.context.rootPath, appIcon);
+ const absolutePath = path.isAbsolute(src)
+ ? src
+ : path.join(api.context.rootPath, src);
const relativePath = path.posix.join(
distDir,
path.basename(absolutePath),
);
+ const requestPath = ensureAssetPrefix(relativePath, publicPath);
const paths = {
+ requestPath,
absolutePath,
relativePath,
};
- cache.set(name, paths);
+ iconPathMap.set(src, paths);
- return paths;
+ return {
+ sizes,
+ ...paths,
+ ...icon,
+ };
};
api.processAssets(
{ stage: 'additional' },
async ({ compilation, environment, sources }) => {
- const iconPath = getIconPath(environment);
- if (!iconPath) {
+ const { config } = environment;
+ const { appIcon } = config.html;
+
+ if (!appIcon) {
return;
}
- if (!(await isFileExists(iconPath.absolutePath))) {
- throw new Error(
- `[rsbuild:app-icon] Can not find the app icon, please check if the '${iconPath.relativePath}' file exists'.`,
+ const distDir = config.output.distPath.image;
+ const publicPath = getPublicPathFromCompiler(compilation);
+ const icons = appIcon.icons.map((icon) =>
+ formatIcon(icon, distDir, publicPath),
+ );
+ const tags: HtmlBasicTag[] = [];
+
+ for (const icon of icons) {
+ if (!(await isFileExists(icon.absolutePath))) {
+ throw new Error(
+ `[rsbuild:app-icon] Can not find the app icon, please check if the '${icon.relativePath}' file exists'.`,
+ );
+ }
+
+ const source = await fs.promises.readFile(icon.absolutePath);
+
+ compilation.emitAsset(
+ icon.relativePath,
+ new sources.RawSource(source),
);
+
+ if (icon.size < 200) {
+ tags.push({
+ tag: 'link',
+ attrs: {
+ rel: 'apple-touch-icon',
+ sizes: icon.sizes,
+ href: icon.requestPath,
+ },
+ });
+ }
}
- const source = await fs.promises.readFile(iconPath.absolutePath);
+ if (appIcon.name) {
+ const manifestIcons = icons.map((icon) => ({
+ src: icon.requestPath,
+ sizes: icon.sizes,
+ }));
- compilation.emitAsset(
- iconPath.relativePath,
- new sources.RawSource(source),
- );
- },
- );
+ const manifest = {
+ name: appIcon.name,
+ icons: manifestIcons,
+ };
+
+ const manifestFile = 'manifest.webmanifest';
+
+ compilation.emitAsset(
+ manifestFile,
+ new sources.RawSource(JSON.stringify(manifest)),
+ );
- api.modifyHTMLTags(
- ({ headTags, bodyTags }, { environment, compilation }) => {
- const iconPath = getIconPath(environment);
- if (!iconPath) {
- return { headTags, bodyTags };
+ tags.push({
+ tag: 'link',
+ attrs: {
+ rel: 'manifest',
+ href: ensureAssetPrefix(manifestFile, publicPath),
+ },
+ });
}
- headTags.unshift({
- tag: 'link',
- attrs: {
- rel: 'apple-touch-icon',
- sizes: '180*180',
- href: ensureAssetPrefix(
- iconPath.relativePath,
- compilation.outputOptions.publicPath,
- ),
- },
- });
-
- return { headTags, bodyTags };
+ if (tags.length) {
+ htmlTagsMap.set(environment.name, tags);
+ }
},
);
+
+ api.modifyHTMLTags(({ headTags, bodyTags }, { environment }) => {
+ const tags = htmlTagsMap.get(environment.name);
+ if (tags) {
+ headTags.unshift(...tags);
+ }
+ return { headTags, bodyTags };
+ });
+
+ api.onCloseDevServer(() => {
+ htmlTagsMap.clear();
+ iconPathMap.clear();
+ });
},
});
diff --git a/packages/core/src/types/config/html.ts b/packages/core/src/types/config/html.ts
index 7dd7cbf8eb..3ca43dcdc7 100644
--- a/packages/core/src/types/config/html.ts
+++ b/packages/core/src/types/config/html.ts
@@ -52,6 +52,8 @@ export type HtmlTagDescriptor = HtmlTag | HtmlTagHandler;
type ChainedHtmlOption = ConfigChainMergeContext;
+export type AppIconItem = { src: string; size: number };
+
export interface HtmlConfig {
/**
* Configure the `` tag of the HTML.
@@ -75,8 +77,20 @@ export interface HtmlConfig {
favicon?: ChainedHtmlOption;
/**
* Set the file path of the app icon, which can be a relative path or an absolute path.
+ *
+ * @example
+ * appIcon: {
+ * name: 'My Website',
+ * icons: [
+ * { src: './icon-192.png', size: 192 },
+ * { src: './icon-512.png', size: 512 },
+ * ]
+ * }
*/
- appIcon?: string;
+ appIcon?: {
+ name?: string;
+ icons: AppIconItem[];
+ };
/**
* Set the id of root element.
*/