feat: Allow customizing entrypoint options per browser (#154)

This commit is contained in:
Aaron
2023-10-05 17:12:20 -05:00
committed by GitHub
parent d9fdcb5b33
commit 868fd27804
9 changed files with 208 additions and 34 deletions
+64
View File
@@ -44,3 +44,67 @@ wxt build --mv2
```
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
## Customizing Entrypoints
There are several ways to customize entrypoint definitions per browser.
First, you can use either the `include` or `exclude` option to include or exclude the entrypoint from specific browsers. Here are some examples
:::code-group
```ts [Background]
export default defineBackground({
// Only include a background script when targeting chrome
include: ['chrome'],
});
```
```ts [Content Script]
export default defineContentScript({
// Do not add this content script to the manifest when targeting firefox
exclude: ['firefox'],
});
```
```html [HTML page]
<!-- entrypoints/options.html -->
<html>
<head>
<!-- Don't include the options page for safari -->
<meta name="manifest.exclude" content="['safari']" />
</head>
</html>
```
:::
Second, you can change individual options per-browser:
:::code-group
```ts [Background]
export default defineBackground({
persistent: {
// Use a non-persistent background script for just safari
safari: false,
},
});
```
```ts [Content Script]
export default defineContentScript({
matches: {
// Run the content script on different pages for each browser
chrome: ['*://*.google.com/*'],
firefox: ['*://*.duckduckgo.com/*'],
edge: ['*://*.bing.com/*'],
},
});
```
:::
:::warning
Only `defineBackground` and `defineContentScript` support per-browser options right now.
:::
+29
View File
@@ -508,4 +508,33 @@ describe('Manifest Content', () => {
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"author\\":\\"Custom Author\\"}"
`);
});
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
persistent: {
firefox: true,
safari: false,
},
main: () => {},
})`,
);
await project.build({ browser });
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
});
+9 -2
View File
@@ -17,7 +17,10 @@ import { parseHTML } from 'linkedom';
import JSON5 from 'json5';
import { importEntrypointFile } from '../utils/importEntrypointFile';
import glob from 'fast-glob';
import { getEntrypointName } from '../utils/entrypoints';
import {
getEntrypointName,
resolvePerBrowserOption,
} from '../utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
import { CSS_EXTENSIONS_PATTERN } from '../utils/paths';
@@ -300,7 +303,11 @@ async function getBackgroundEntrypoint(
name: 'background',
inputPath: path,
outputDir: config.outDir,
options: options,
options: {
...options,
type: resolvePerBrowserOption(options.type, config.browser),
persistent: resolvePerBrowserOption(options.persistent, config.browser),
},
};
}
+15 -6
View File
@@ -6,7 +6,10 @@ import {
} from './types';
import * as vite from 'vite';
import { Scripting } from 'webextension-polyfill';
import { getEntrypointBundlePath } from './utils/entrypoints';
import {
getEntrypointBundlePath,
resolvePerBrowserOption,
} from './utils/entrypoints';
import {
getContentScriptCssFiles,
getContentScriptsCssMap,
@@ -106,12 +109,18 @@ export function reloadContentScripts(
const css = getContentScriptCssFiles([entry], cssMap);
server.reloadContentScript({
allFrames: entry.options.allFrames,
excludeMatches: entry.options.excludeMatches,
matches: entry.options.matches,
runAt: entry.options.runAt,
allFrames: resolvePerBrowserOption(
entry.options.allFrames,
config.browser,
),
excludeMatches: resolvePerBrowserOption(
entry.options.excludeMatches,
config.browser,
),
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
world: entry.options.world,
world: resolvePerBrowserOption(entry.options.world, config.browser),
js,
css,
});
+16 -12
View File
@@ -365,47 +365,49 @@ export type Entrypoint =
export type OnContentScriptStopped = (cb: () => void) => void;
export interface ContentScriptDefinition extends ExcludableEntrypoint {
matches: Manifest.ContentScript['matches'];
matches: PerBrowserOption<Manifest.ContentScript['matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default "documentIdle"
*/
runAt?: Manifest.ContentScript['run_at'];
runAt?: PerBrowserOption<Manifest.ContentScript['run_at']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchAboutBlank?: Manifest.ContentScript['match_about_blank'];
matchAboutBlank?: PerBrowserOption<
Manifest.ContentScript['match_about_blank']
>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeMatches?: Manifest.ContentScript['exclude_matches'];
excludeMatches?: PerBrowserOption<Manifest.ContentScript['exclude_matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
includeGlobs?: Manifest.ContentScript['include_globs'];
includeGlobs?: PerBrowserOption<Manifest.ContentScript['include_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeGlobs?: Manifest.ContentScript['exclude_globs'];
excludeGlobs?: PerBrowserOption<Manifest.ContentScript['exclude_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
allFrames?: Manifest.ContentScript['all_frames'];
allFrames?: PerBrowserOption<Manifest.ContentScript['all_frames']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchOriginAsFallback?: boolean;
matchOriginAsFallback?: PerBrowserOption<boolean>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default "ISOLATED"
*/
world?: 'ISOLATED' | 'MAIN';
world?: PerBrowserOption<'ISOLATED' | 'MAIN'>;
/**
* Customize how imported/generated styles are injected with the content script. Regardless of the
* mode selected, CSS will always be built and included in the output directory.
@@ -417,7 +419,7 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
*
* @default "manifest"
*/
cssInjectionMode?: 'manifest' | 'manual' | 'ui';
cssInjectionMode?: PerBrowserOption<'manifest' | 'manual' | 'ui'>;
/**
* Main function executed when the content script is loaded.
*/
@@ -425,11 +427,13 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
}
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
type?: 'module';
persistent?: boolean;
type?: PerBrowserOption<'module'>;
persistent?: PerBrowserOption<boolean>;
main(): void;
}
export type PerBrowserOption<T> = T | { [browser: TargetBrowser]: T };
export interface ExcludableEntrypoint {
/**
* List of target browsers to include this entrypoint in. Defaults to being included in all
+26 -1
View File
@@ -1,5 +1,9 @@
import { describe, it, expect } from 'vitest';
import { getEntrypointName, getEntrypointOutputFile } from '../entrypoints';
import {
getEntrypointName,
getEntrypointOutputFile,
resolvePerBrowserOption,
} from '../entrypoints';
import { Entrypoint } from '../../types';
import { resolve } from 'path';
@@ -49,4 +53,25 @@ describe('Entrypoint Utils', () => {
expect(actual).toBe(expected);
});
});
describe('resolvePerBrowserOption', () => {
it('should return the value directly', () => {
expect(resolvePerBrowserOption('some-string', '')).toEqual('some-string');
expect(resolvePerBrowserOption(false, '')).toEqual(false);
expect(resolvePerBrowserOption([1], '')).toEqual([1]);
expect(resolvePerBrowserOption(['string'], '')).toEqual(['string']);
});
it('should return the value for the specific browser', () => {
expect(resolvePerBrowserOption({ a: 'one', b: 'two' }, 'a')).toEqual(
'one',
);
expect(resolvePerBrowserOption({ c: ['one'], d: ['two'] }, 'c')).toEqual([
'one',
]);
expect(resolvePerBrowserOption({ c: false, d: true }, 'e')).toEqual(
undefined,
);
});
});
});
+26 -9
View File
@@ -1,5 +1,6 @@
import { Manifest } from 'webextension-polyfill';
import { ContentScriptEntrypoint } from '../types';
import { ContentScriptEntrypoint, InternalConfig } from '../types';
import { resolvePerBrowserOption } from './entrypoints';
/**
* Returns a unique and consistent string hash based on a content scripts options.
@@ -35,18 +36,34 @@ export function hashContentScriptOptions(
export function mapWxtOptionsToContentScript(
options: ContentScriptEntrypoint['options'],
config: InternalConfig,
): Omit<Manifest.ContentScript, 'js' | 'css'> {
return {
matches: options.matches,
all_frames: options.allFrames,
match_about_blank: options.matchAboutBlank,
exclude_globs: options.excludeGlobs,
exclude_matches: options.excludeMatches,
include_globs: options.includeGlobs,
run_at: options.runAt,
matches: resolvePerBrowserOption(options.matches, config.browser),
all_frames: resolvePerBrowserOption(options.allFrames, config.browser),
match_about_blank: resolvePerBrowserOption(
options.matchAboutBlank,
config.browser,
),
exclude_globs: resolvePerBrowserOption(
options.excludeGlobs,
config.browser,
),
exclude_matches: resolvePerBrowserOption(
options.excludeMatches,
config.browser,
),
include_globs: resolvePerBrowserOption(
options.includeGlobs,
config.browser,
),
run_at: resolvePerBrowserOption(options.runAt, config.browser),
// @ts-expect-error: untyped chrome options
match_origin_as_fallback: options.matchOriginAsFallback,
match_origin_as_fallback: resolvePerBrowserOption(
options.matchOriginAsFallback,
config.browser,
),
world: options.world,
};
}
+13 -1
View File
@@ -1,4 +1,4 @@
import { Entrypoint } from '../types';
import { Entrypoint, PerBrowserOption, TargetBrowser } from '../types';
import path, { relative, resolve } from 'node:path';
import { normalizePath } from './paths';
@@ -34,3 +34,15 @@ export function getEntrypointBundlePath(
relative(outDir, getEntrypointOutputFile(entrypoint, ext)),
);
}
/**
* Given an entrypoint option, resolve it's value based on a target browser.
*/
export function resolvePerBrowserOption<T>(
option: PerBrowserOption<T>,
browser: TargetBrowser,
): T {
if (typeof option === 'object' && !Array.isArray(option))
return (option as any)[browser];
return option;
}
+10 -3
View File
@@ -10,7 +10,10 @@ import {
} from '../types';
import fs from 'fs-extra';
import { resolve } from 'path';
import { getEntrypointBundlePath } from './entrypoints';
import {
getEntrypointBundlePath,
resolvePerBrowserOption,
} from './entrypoints';
import { ContentSecurityPolicy } from './ContentSecurityPolicy';
import {
hashContentScriptOptions,
@@ -295,7 +298,11 @@ function addEntrypoints(
if (config.command === 'serve' && config.manifestVersion === 3) {
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
contentScripts.forEach((script) => {
script.options.matches.forEach((matchPattern) => {
const matches = resolvePerBrowserOption(
script.options.matches,
config.browser,
);
matches.forEach((matchPattern) => {
hostPermissions.add(matchPattern);
});
});
@@ -312,7 +319,7 @@ function addEntrypoints(
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
([, scripts]) => ({
...mapWxtOptionsToContentScript(scripts[0].options),
...mapWxtOptionsToContentScript(scripts[0].options, config),
// TOOD: Sorting css and js arrays here so we get consistent test results... but we
// shouldn't have to. Where is the inconsistency coming from?
css: getContentScriptCssFiles(scripts, cssMap)?.sort(),