feat: type /_favicon/ paths when favicon permission is declared (#2241)
Co-authored-by: oferitz <oferitz@Ofers-MacBook-Pro.local> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ words:
|
||||
- esbuild
|
||||
- esque
|
||||
- extglob
|
||||
- favicons
|
||||
- fieldtrials
|
||||
- firefoxdeveloperedition
|
||||
- Florian
|
||||
|
||||
@@ -224,6 +224,7 @@ export default defineConfig({
|
||||
menuItem('Storage', 'storage.md'),
|
||||
menuItem('Messaging', 'messaging.md'),
|
||||
menuItem('I18n', 'i18n.md'),
|
||||
menuItem('Favicons', 'favicons.md'),
|
||||
menuItem('Scripting', 'scripting.md'),
|
||||
menuItem('WXT Modules', 'wxt-modules.md'),
|
||||
menuItem('Frontend Frameworks', 'frontend-frameworks.md'),
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Favicons
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/how-to/ui/favicons)
|
||||
|
||||
Chromium-based browsers expose cached favicons through the `_favicon/` URL served from your extension. To use it, declare the `favicon` permission in your manifest and call `browser.runtime.getURL` with a `_favicon/` path:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['favicon'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// Any entrypoint
|
||||
function getFaviconUrl(pageUrl: string, size = 16) {
|
||||
const url = new URL(browser.runtime.getURL('/_favicon/'));
|
||||
url.searchParams.set('pageUrl', pageUrl);
|
||||
url.searchParams.set('size', String(size));
|
||||
return url.toString();
|
||||
}
|
||||
```
|
||||
|
||||
::: warning Chromium only
|
||||
The favicon API is only available on Chromium-based browsers. Firefox has no equivalent, so `/_favicon/` URLs will not resolve at runtime there. If your extension supports both browsers, gate favicon usage behind `import.meta.env.CHROME` (or similar) and declare the permission per-browser.
|
||||
:::
|
||||
|
||||
## Usage from a content script
|
||||
|
||||
WXT does **not** add a `web_accessible_resources` entry for `_favicon/*` — not every extension needs one, and adding it unconditionally would expose internals to sites that don't need them. If you want to load a favicon inside a content script (for example, as an `<img src>`), add your own entry in `wxt.config.ts`:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['favicon'],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['_favicon/*'],
|
||||
matches: ['<all_urls>'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -420,6 +420,48 @@ describe('TypeScript Project', () => {
|
||||
expect(output).toContain('./example.ts');
|
||||
});
|
||||
|
||||
it('should type `/_favicon/` paths when the `favicon` permission is declared', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
|
||||
await project.build({
|
||||
manifest: { permissions: ['favicon'] },
|
||||
});
|
||||
|
||||
const pathsOutput = await project.serializeFile('.wxt/types/paths.d.ts');
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
// The generated type must be a template-literal (backticks), not a
|
||||
// string-literal — otherwise query params (`?pageUrl=...&size=...`)
|
||||
// would not type-check.
|
||||
expect(pathsOutput).toMatchInlineSnapshot(`
|
||||
".wxt/types/paths.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import "wxt/browser";
|
||||
|
||||
declare module "wxt/browser" {
|
||||
export type PublicPath =
|
||||
| ""
|
||||
| "/"
|
||||
| "/popup.html"
|
||||
| \`/_favicon/\${string}\`
|
||||
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
|
||||
export interface WxtRuntime {
|
||||
getURL(path: PublicPath): string;
|
||||
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
|
||||
// Per the review on #1570, WXT intentionally does NOT add a
|
||||
// `web_accessible_resources` entry for `_favicon/*`: that's only
|
||||
// needed for content-script usage, and users who need it can add
|
||||
// it themselves.
|
||||
expect(manifest.web_accessible_resources).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set correct import.meta.env.BROWSER type based on targetBrowsers', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html', '<html></html>');
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defineWxtModule } from '../modules';
|
||||
import type { WxtDirFileEntry } from '../types';
|
||||
|
||||
/**
|
||||
* Adds a template-literal type for the `_favicon/` paths served by Chrome's
|
||||
* favicon API when the `favicon` permission is declared. With this module,
|
||||
* `browser.runtime.getURL('/_favicon/?pageUrl=...')` type-checks without
|
||||
* needing a `@ts-expect-error`.
|
||||
*
|
||||
* Extensions that load favicons from a content script must still add their own
|
||||
* `web_accessible_resources` entry — this module intentionally does not touch
|
||||
* the manifest. See the review thread on #1570 for context.
|
||||
*
|
||||
* @see https://developer.chrome.com/docs/extensions/how-to/ui/favicons
|
||||
*/
|
||||
export default defineWxtModule({
|
||||
name: 'wxt:built-in:favicon-permission',
|
||||
setup(wxt) {
|
||||
wxt.hooks.hook('prepare:types', (_, entries) => {
|
||||
if (!wxt.config.manifest.permissions?.includes('favicon')) return;
|
||||
|
||||
const pathsEntry = entries.find(
|
||||
(entry): entry is WxtDirFileEntry =>
|
||||
'path' in entry && entry.path === 'types/paths.d.ts',
|
||||
);
|
||||
if (!pathsEntry) return;
|
||||
|
||||
// The base generator wraps every public path in double quotes,
|
||||
// producing string-literal union members. Favicon URLs need a
|
||||
// template-literal type so arbitrary query strings type-check, so
|
||||
// we splice a `\`/_favicon/\${string}\`` member into the union
|
||||
// directly. The `HtmlPublicPath` line is a stable anchor emitted
|
||||
// by generate-wxt-dir.ts:getPathsDeclarationEntry.
|
||||
const anchor = ' type HtmlPublicPath';
|
||||
pathsEntry.text = pathsEntry.text.replace(
|
||||
anchor,
|
||||
' | `/_favicon/${string}`\n' + anchor,
|
||||
);
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { WxtModule } from '../types';
|
||||
import faviconPermission from './favicon-permission';
|
||||
import unimport from './unimport';
|
||||
|
||||
export const builtinModules: WxtModule<any>[] = [unimport];
|
||||
export const builtinModules: WxtModule<any>[] = [unimport, faviconPermission];
|
||||
|
||||
Reference in New Issue
Block a user