diff --git a/docs/guide/manifest.md b/docs/guide/manifest.md index eac1c177..d93be882 100644 --- a/docs/guide/manifest.md +++ b/docs/guide/manifest.md @@ -47,9 +47,9 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/ } ``` -## Icon +## `icons` -The [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/) property needs to be set in the config file. The files should be added to WXT's [`public` directory](/get-started/assets#public-directory). +By default, WXT will discover icons in your [`public` directory](/get-started/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/). ``` public/ @@ -60,21 +60,27 @@ public/ └─ icon-128.png ``` +Icon files need to match the following regex to be automatically included in the manifest. Most design software can output icons in one of these formats + +<<< @/../src/core/utils/manifest.ts#snippet + +If you prefer to use filenames in a different format, you can add the icons manually in your `wxt.config.ts` file: + ```ts export default defineConfig({ manifest: { icons: { - 16: '/icon-16.png', - 24: '/icon-24.png', - 48: '/icon-48.png', - 96: '/icon-96.png', - 128: '/icon-128.png', + 16: '/extension-icon-16.png', + 24: '/extension-icon-24.png', + 48: '/extension-icon-48.png', + 96: '/extension-icon-96.png', + 128: '/extension-icon-128.png', }, }, }); ``` -## Permissions +## `permissions` [Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config. diff --git a/e2e/tests/manifest-content.test.ts b/e2e/tests/manifest-content.test.ts index b17ca938..bd31386e 100644 --- a/e2e/tests/manifest-content.test.ts +++ b/e2e/tests/manifest-content.test.ts @@ -133,6 +133,66 @@ describe('Manifest Content', () => { }); }); + describe('icons', () => { + it('should auto-discover icons with the correct name', async () => { + const project = new TestProject(); + project.addFile('public/icon-16.png'); + project.addFile('public/icon/32.jpeg'); + project.addFile('public/icon@48w.jpg'); + project.addFile('public/icon-64x64.gif'); + project.addFile('public/icon@96.bmp'); + project.addFile('public/icon/128x128.ico'); + + await project.build(); + const manifest = await project.getOutputManifest(); + + expect(manifest.icons).toEqual({ + '16': 'icon-16.png', + '32': 'icon/32.jpeg', + '48': 'icon@48w.jpg', + '64': 'icon-64x64.gif', + '96': 'icon@96.bmp', + '128': 'icon/128x128.ico', + }); + }); + + it('should return undefined when no icons are found', async () => { + const project = new TestProject(); + project.addFile('public/logo.png'); + project.addFile('public/icon.jpeg'); + + await project.build(); + const manifest = await project.getOutputManifest(); + + expect(manifest.icons).toBeUndefined(); + }); + + it('should allow icons to be overwritten from the wxt.config.ts file', async () => { + const project = new TestProject(); + project.addFile('public/icon-16.png'); + project.addFile('public/icon-32.png'); + project.addFile('public/logo-16.png'); + project.addFile('public/logo-32.png'); + project.addFile('public/logo-48.png'); + + const icons = { + '16': 'logo-16.png', + '32': 'logo-32.png', + '48': 'logo-48.png', + }; + project.setConfigFileConfig({ + manifest: { + icons, + }, + }); + + await project.build(); + const manifest = await project.getOutputManifest(); + + expect(manifest.icons).toEqual(icons); + }); + }); + it('should group content scripts and styles together based on their matches and run_at', async () => { const project = new TestProject(); project.addFile( diff --git a/src/core/utils/manifest.ts b/src/core/utils/manifest.ts index 401c0176..fa027513 100644 --- a/src/core/utils/manifest.ts +++ b/src/core/utils/manifest.ts @@ -17,6 +17,7 @@ import { mapWxtOptionsToContentScript, } from './content-scripts'; import { getPackageJson } from './package'; +import { normalizePath } from './paths'; /** * Writes the manifest to the output directory and the build output. @@ -65,6 +66,7 @@ export async function generateMainfest( ? pkg?.version : undefined, short_name: pkg?.shortName, + icons: discoverIcons(buildOutput), }, config.manifest, ); @@ -317,6 +319,40 @@ function addEntrypoints( } } +function discoverIcons( + buildOutput: Omit, +): Manifest.WebExtensionManifest['icons'] { + const icons: [string, string][] = []; + // prettier-ignore + // #region snippet + const iconRegex = [ + /^icon-([0-9]+)\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon-16.png + /^icon-([0-9]+)x[0-9]+\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon-16x16.png + /^icon@([0-9]+)w\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon@16w.png + /^icon@([0-9]+)h\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon@16h.png + /^icon@([0-9]+)\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon@16.png + /^icon[\/\\]([0-9]+)\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon/16.png + /^icon[\/\\]([0-9]+)x[0-9]+\.(png|bmp|jpeg|jpg|ico|gif)$/, // icon/16x16.png + ]; + // #endregion snippet + + buildOutput.publicAssets.forEach((asset) => { + let size: string | undefined; + for (const regex of iconRegex) { + const match = asset.fileName.match(regex); + if (match?.[1] != null) { + size = match[1]; + break; + } + } + if (size == null) return; + + icons.push([size, normalizePath(asset.fileName)]); + }); + + return icons.length > 0 ? Object.fromEntries(icons) : undefined; +} + function addDevModeCsp( manifest: Manifest.WebExtensionManifest, config: InternalConfig,