Compare commits

..

27 Commits

Author SHA1 Message Date
github-actions[bot] b61fd1716e chore(release): wxt v0.20.6
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-30 16:22:57 +00:00
Aaron d584b5cc7e chore(deps): Upgrade web-ext-run to v0.2.3 (#1634) 2025-04-30 11:18:30 -05:00
Bang·_· faf9cedfbd docs: Added "[DesignPicker]" to the homepage (#1624) 2025-04-27 07:24:00 -05:00
Aaron 3a38ac9339 Fix changelog contributors 2025-04-26 18:07:17 -05:00
github-actions[bot] 7ea36a3cfc chore(release): wxt v0.20.5
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-26 13:53:46 +00:00
Aaron 3577c0b47a fix: Don't use crypto.randUUID for shadow root UIs 2025-04-26 08:48:42 -05:00
Aaron d0bef42186 fix: Standardize locale codes and warn about unsupported ones (#1617) 2025-04-26 08:03:22 -05:00
github-actions[bot] d9f10c62ad chore(release): wxt v0.20.4
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-25 03:03:08 +00:00
aklinker1 35d3c03e4b fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-04-25 01:52:10 +00:00
Yunsup Sim fdd38a1580 fix: Fix CORS error in Firefox (#1607) 2025-04-24 19:01:30 -05:00
ТΞNSΛI 3018801f06 feat: add {{packageVersion}} as template variable (#1604) 2025-04-24 12:06:50 -05:00
Anh71me 8e96bfef06 fix: fix typescript error on defineItem fallback (#1601) 2025-04-24 07:31:48 -05:00
ТΞNSΛI 31071bd11e feat: ignore elements with a vite-ignore or wxt-ignore attribute (#1603)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-04-24 00:34:49 -05:00
Yuan f5619f6be1 chore: Move the public folder outside of src for the default Svelte template (#1602) 2025-04-24 00:26:34 -05:00
Aaron adad1b5a2c chore: Fix formatting 2025-04-24 00:24:49 -05:00
Nishu a6c4e19a5d fix: adding missing "" to PublicPath and browser.runtime.getUrl (#1597) 2025-04-24 00:18:06 -05:00
Jack af6f74c344 docs: Added "[Always Light Mode]" to the homepage (#1608) 2025-04-24 00:05:37 -05:00
Aaron ce45cb8a89 docs: Add warning about --load-extension deprecation 2025-04-23 23:35:28 -05:00
aklinker1 14d4aa202d fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-04-23 01:51:01 +00:00
Nishu 4c35798dba feat: Ignore popup/index.ts instead of erroring (#1520)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-04-20 21:17:05 -05:00
Aaron 656a9b365f docs(Content Script UI): Add additional details about when onRemove is called 2025-04-20 21:15:08 -05:00
Namu 67fa3db921 docs: Fix import in unit-testing.md (#1598) 2025-04-20 20:36:06 -05:00
github-actions[bot] f2d3061e97 chore(release): wxt v0.20.3
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-19 17:17:39 +00:00
Aaron 760c34e416 feat: Automatically place document-level CSS outside shadow root (#1594) 2025-04-19 12:12:04 -05:00
Bang·_· 48398b315c docs: Added "[Blens - Time Tracker and AI Insight]" to the homepage (#1587) 2025-04-18 21:53:19 -05:00
techlism 45d0d9d7f1 docs: Fix entrypoints.md examples (#1586) 2025-04-18 21:11:04 -05:00
Aaron b0f4ac8221 fix: Fix double hashing of inline script keys
Additionally, update the vite plugin name to better align with purpose
2025-04-18 20:50:02 -05:00
38 changed files with 754 additions and 567 deletions
@@ -71,6 +71,9 @@ const chromeExtensionIds = [
'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager
'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder
'gdjampjdgjmbifnhldgcnccdjkcoicmg', // radiofrance - news & broadcasts (French), music (international)
'jlnhphlghikichhgbnkepenehbmloenb', // Blens - Time Tracker and AI Insight
'njnammmpdodmfkodnfpammnpdcbhnlcm', // Always Light Mode - Setting website always in light mode
'lblmfclcfniabobmamfkdogcgdagbhhb', // DesignPicker - Color Picker & Font Detector
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
@@ -8,6 +8,16 @@ outline: deep
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
:::danger
Chrome 137 removed support for the `--load-extension` CLI flag, which WXT relied on to open the browser with an extension installed. So this feature will not work for Chrome.
You have two options:
1. Install [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (which still supports the `--load-extension` flag) and [point the `chrome` binary to it](#set-browser-binaries), or
2. [Disable this feature](#disable-opening-browser) and manually load your extension
:::
## Config Files
You can configure browser startup in 3 places:
+2 -2
View File
@@ -211,12 +211,12 @@ When defining your background entrypoint, keep in mind that WXT will import this
<!-- prettier-ignore -->
```ts
browser.action.onClick.addListener(() => { // [!code --]
browser.action.onClicked.addListener(() => { // [!code --]
// ... // [!code --]
}); // [!code --]
export default defineBackground(() => {
browser.action.onClick.addListener(() => { // [!code ++]
browser.action.onClicked.addListener(() => { // [!code ++]
// ... // [!code ++]
}); // [!code ++]
});
+1 -1
View File
@@ -85,7 +85,7 @@ import { injectScript, createShadowRootUi } from '#imports';
But Vitest sees this:
```ts
import { injectScript } from 'wxt/browser';
import { injectScript } from 'wxt/utils/inject-script';
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
```
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/browser",
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
"version": "0.0.315",
"version": "0.0.317",
"type": "module",
"main": "src/index.mjs",
"types": "src/index.d.ts",
@@ -23,7 +23,7 @@
"src"
],
"devDependencies": {
"@types/chrome": "0.0.315",
"@types/chrome": "0.0.317",
"fs-extra": "catalog:",
"nano-spawn": "catalog:",
"tsx": "catalog:",
+67 -5
View File
@@ -8958,7 +8958,7 @@ export namespace Browser {
default_popup?: string | undefined;
}
// Source: https://developer.Browser.com/docs/extensions/mv3/declare_permissions/
/** Source: https://developer.Browser.com/docs/extensions/reference/permissions-list */
export type ManifestPermissions =
| "accessibilityFeatures.modify"
| "accessibilityFeatures.read"
@@ -9015,11 +9015,11 @@ export namespace Browser {
| "privacy"
| "processes"
| "proxy"
| "readingList"
| "scripting"
| "search"
| "sessions"
| "sidePanel"
| "signedInDevices"
| "storage"
| "system.cpu"
| "system.display"
@@ -9042,6 +9042,23 @@ export namespace Browser {
| "webRequestBlocking"
| "webRequestAuthProvider";
/** Source : https://developer.Browser.com/docs/extensions/reference/api/permissions */
export type ManifestOptionalPermissions = Exclude<
ManifestPermissions,
| "debugger"
| "declarativeNetRequest"
| "devtools"
| "experimental"
| "fontSettings"
| "geolocation"
| "proxy"
| "tts"
| "ttsEngine"
| "unlimitedStorage"
| "wallpaper"
| "webAuthenticationProxy"
>;
export interface SearchProvider {
name?: string | undefined;
keyword?: string | undefined;
@@ -9261,8 +9278,8 @@ export namespace Browser {
}
| undefined;
content_security_policy?: string | undefined;
optional_permissions?: string[] | undefined;
permissions?: string[] | undefined;
optional_permissions?: ManifestOptionalPermissions[] | string[] | undefined;
permissions?: ManifestPermissions[] | string[] | undefined;
web_accessible_resources?: string[] | undefined;
}
@@ -9297,7 +9314,7 @@ export namespace Browser {
sandbox?: string;
};
host_permissions?: string[] | undefined;
optional_permissions?: ManifestPermissions[] | undefined;
optional_permissions?: ManifestOptionalPermissions[] | undefined;
optional_host_permissions?: string[] | undefined;
permissions?: ManifestPermissions[] | undefined;
web_accessible_resources?: Array<{ resources: string[]; matches: string[] }> | undefined;
@@ -14845,6 +14862,18 @@ export namespace Browser {
*/
export type ExecutionWorld = "MAIN" | "USER_SCRIPT";
/** @since Chrome 135 */
export interface InjectionResult {
/** The document associated with the injection. */
documentId: string;
/** The error, if any. `error` and `result` are mutually exclusive. */
error?: string;
/** The frame associated with the injection. */
frameId: number;
/** The result of the script execution. */
result: any;
}
export interface WorldProperties {
/** Specifies the world csp. The default is the `ISOLATED` world csp. */
csp?: string;
@@ -14861,6 +14890,18 @@ export namespace Browser {
ids?: string[];
}
/** @since Chrome 135 */
export interface InjectionTarget {
/** Whether the script should inject into all frames within the tab. Defaults to false. This must not be true if `frameIds` is specified. */
allFrames?: boolean;
/** The IDs of specific documentIds to inject into. This must not be set if `frameIds` is set. */
documentIds?: string[];
/** The IDs of specific frames to inject into. */
frameIds?: number[];
/** The ID of the tab into which to inject. */
tabId: number;
}
export interface RegisteredUserScript {
/** If true, it will inject into all frames, even if the frame is not the top-most frame in the tab. Each frame is checked independently for URL requirements; it will not inject into child frames if the URL requirements are not met. Defaults to false, meaning that only the top frame is matched. */
allFrames?: boolean;
@@ -14887,6 +14928,20 @@ export namespace Browser {
worldId?: string;
}
/** @since Chrome 135 */
export interface UserScriptInjection {
/** Whether the injection should be triggered in the target as soon as possible. Note that this is not a guarantee that injection will occur prior to page load, as the page may have already loaded by the time the script reaches the target. */
injectImmediately?: boolean;
/** The list of ScriptSource objects defining sources of scripts to be injected into the target. */
js: ScriptSource[];
/** Details specifying the target into which to inject the script. */
target: InjectionTarget;
/** The JavaScript "world" to run the script in. The default is `USER_SCRIPT`. */
world?: ExecutionWorld;
/** Specifies the user script world ID to execute in. If omitted, the script will execute in the default user script world. Only valid if `world` is omitted or is `USER_SCRIPT`. Values with leading underscores (`_`) are reserved. */
worldId?: string;
}
/**
* Properties for a script source.
*/
@@ -14939,6 +14994,13 @@ export namespace Browser {
export function getWorldConfigurations(): Promise<WorldProperties[]>;
export function getWorldConfigurations(callback: (worlds: WorldProperties[]) => void): void;
/**
* Injects a script into a target context. By default, the script will be run at `document_idle`, or immediately if the page has already loaded. If the `injectImmediately` property is set, the script will inject without waiting, even if the page has not finished loading. If the script evaluates to a promise, the browser will wait for the promise to settle and return the resulting value.
* @since Chrome 135
*/
export function execute(injection: UserScriptInjection): Promise<InjectionResult[]>;
export function execute(injection: UserScriptInjection, callback: (result: InjectionResult[]) => void): void;
/**
* Registers one or more user scripts for this extension.
*
+32 -2
View File
@@ -1,10 +1,14 @@
import { describe, it, expect } from 'vitest';
import { ChromeMessage } from '../build';
import { applyChromeMessagePlaceholders, getSubstitutionCount } from '../utils';
import {
applyChromeMessagePlaceholders,
getSubstitutionCount,
standardizeLocale,
} from '../utils';
describe('Utils', () => {
describe('applyChromeMessagePlaceholders', () => {
it('should return the combined stirng', () => {
it('should return the combined string', () => {
const input = {
message: 'Hello $username$, welcome to $appName$',
placeholders: {
@@ -60,4 +64,30 @@ describe('Utils', () => {
expect(getSubstitutionCount('Hello $10')).toBe(1);
});
});
describe('standardizeLocale', () => {
it('should convert two-letter locale codes to lowercase', () => {
expect(standardizeLocale('en')).toEqual('en');
expect(standardizeLocale('EN')).toEqual('en');
});
it('should convert locale code extensions to uppercase', () => {
expect(standardizeLocale('en_US')).toEqual('en_US');
expect(standardizeLocale('en_us')).toEqual('en_US');
expect(standardizeLocale('es_419')).toEqual('es_419');
});
it('should convert dashes to underscores', () => {
expect(standardizeLocale('en_US')).toEqual('en_US');
expect(standardizeLocale('en-US')).toEqual('en_US');
});
it('should return the input string as-is for unknown formats', () => {
expect(standardizeLocale('en_USSS')).toEqual('en_USSS');
expect(standardizeLocale('en-')).toEqual('en-');
expect(standardizeLocale('------')).toEqual('------');
expect(standardizeLocale('test')).toEqual('test');
expect(standardizeLocale('hello-world')).toEqual('hello-world');
});
});
});
+2
View File
@@ -10,6 +10,8 @@ import { parseYAML, parseJSON5, parseTOML } from 'confbox';
import { dirname, extname } from 'node:path';
import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';
export { SUPPORTED_LOCALES } from './supported-locales';
//
// TYPES
//
+18 -4
View File
@@ -16,12 +16,14 @@ import {
generateChromeMessagesText,
parseMessagesFile,
generateTypeText,
SUPPORTED_LOCALES,
} from './build';
import glob from 'fast-glob';
import { basename, extname, join, resolve } from 'node:path';
import { watch } from 'chokidar';
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
import { writeFile } from 'node:fs/promises';
import { standardizeLocale } from './utils';
export default defineWxtModule<I18nOptions>({
name: '@wxt-dev/i18n',
@@ -46,10 +48,22 @@ export default defineWxtModule<I18nOptions>({
cwd: localesDir,
absolute: true,
});
return files.map((file) => ({
file,
locale: basename(file).replace(extname(file), ''),
}));
const unsupportedLocales: string[] = [];
const res = files.map((file) => {
const rawLocale = basename(file).replace(extname(file), '');
const locale = standardizeLocale(rawLocale);
if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale);
return { file, locale };
});
if (unsupportedLocales.length > 0)
wxt.logger.warn(
`Unsupported locales: [${unsupportedLocales.join(', ')}].\n\nWeb extensions only support a limited set of locales as described here: https://developer.chrome.com/docs/extensions/reference/api/i18n#locales`,
);
return res;
};
const generateOutputJsonFiles = async (): Promise<
+58
View File
@@ -0,0 +1,58 @@
/** From https://developer.chrome.com/docs/extensions/reference/api/i18n#locales */
export const SUPPORTED_LOCALES = new Set([
'ar',
'am',
'bg',
'bn',
'ca',
'cs',
'da',
'de',
'el',
'en',
'en_AU',
'en_GB',
'en_US',
'es',
'es_419',
'et',
'fa',
'fi',
'fil',
'fr',
'gu',
'he',
'hi',
'hr',
'hu',
'id',
'it',
'ja',
'kn',
'ko',
'lt',
'lv',
'ml',
'mr',
'ms',
'nl',
'no',
'pl',
'pt_BR',
'pt_PT',
'ro',
'ru',
'sk',
'sl',
'sr',
'sv',
'sw',
'ta',
'te',
'th',
'tr',
'uk',
'vi',
'zh_CN',
'zh_TW',
]);
+13
View File
@@ -21,3 +21,16 @@ export function getSubstitutionCount(message: string): number {
}
const MAX_SUBSTITUTIONS = 9;
/** Given a string, standardize it to the format `xx_YY`. */
export function standardizeLocale(locale: string): string {
if (locale.length === 2) return locale.toLowerCase();
const [is_match, prefix, suffix] =
locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? [];
if (is_match) {
return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`;
}
return locale;
}
@@ -1281,6 +1281,19 @@ describe('Storage Utils', () => {
it('should define a nullable value when options are not passed', () => {
const item = storage.defineItem<number>(`local:test`);
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
const item2 = storage.defineItem<number>(`local:test`, {});
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
const item3 = storage.defineItem<number>(`local:test`, {
fallback: undefined,
});
expectTypeOf(item3).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
const item4 = storage.defineItem<number>(`local:test`, {
defaultValue: undefined,
});
expectTypeOf(item4).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
});
it('should define a non-null value when options are passed with a nullish default value', () => {
@@ -1288,6 +1301,11 @@ describe('Storage Utils', () => {
defaultValue: 123,
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
const item2 = storage.defineItem(`local:test`, {
fallback: 123,
});
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();
});
it('should define a nullable value when options are passed with null default value', () => {
+9 -1
View File
@@ -750,8 +750,16 @@ export interface WxtStorage {
): WxtStorageItem<TValue | null, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
options: WxtStorageItemOptions<TValue> & { fallback: TValue },
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue> & { defaultValue: TValue },
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
): WxtStorageItem<TValue | null, TMetadata>;
}
interface WxtStorageDriver {
+58
View File
@@ -1,5 +1,63 @@
# Changelog
## v0.20.6
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.5...wxt-v0.20.6)
## v0.20.5
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.4...wxt-v0.20.5)
### 🩹 Fixes
- Don't use crypto.randUUID for shadow root UIs ([3577c0b](https://github.com/wxt-dev/wxt/commit/3577c0b))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.4
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.3...wxt-v0.20.4)
### 🚀 Enhancements
- Ignore popup/index.ts instead of erroring ([#1520](https://github.com/wxt-dev/wxt/pull/1520))
- Ignore elements with a `vite-ignore` or `wxt-ignore` attribute ([#1603](https://github.com/wxt-dev/wxt/pull/1603))
- Add `{{packageVersion}}` as template variable ([#1604](https://github.com/wxt-dev/wxt/pull/1604))
### 🩹 Fixes
- Adding missing `""` to `PublicPath` and `browser.runtime.getUrl` ([#1597](https://github.com/wxt-dev/wxt/pull/1597))
- Fix CORS error in Firefox ([#1607](https://github.com/wxt-dev/wxt/pull/1607))
### 📖 Documentation
- **Content Script UI:** Add additional details about when `onRemove` is called ([656a9b3](https://github.com/wxt-dev/wxt/commit/656a9b3))
### ❤️ Contributors
- Yunsup Sim ([@SimYunSup](https://github.com/SimYunSup))
- ТΞNSΛI ([@Tensai75](https://github.com/Tensai75))
- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.3
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.2...wxt-v0.20.3)
### 🚀 Enhancements
- Automatically place document-level CSS outside shadow root ([#1594](https://github.com/wxt-dev/wxt/pull/1594))
### 🩹 Fixes
- Fix double hashing of inline script keys ([b0f4ac8](https://github.com/wxt-dev/wxt/commit/b0f4ac8))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.2
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.1...wxt-v0.20.2)
@@ -48,6 +48,7 @@ describe('TypeScript Project', () => {
declare module "wxt/browser" {
export type PublicPath =
| ""
| "/"
| "/options.html"
| "/popup.html"
+5 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.2",
"version": "0.20.6",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
@@ -167,6 +167,10 @@
"types": "./dist/utils/match-patterns.d.ts",
"default": "./dist/utils/match-patterns.mjs"
},
"./utils/split-shadow-root-css": {
"types": "./dist/utils/split-shadow-root-css.d.ts",
"default": "./dist/utils/split-shadow-root-css.mjs"
},
"./utils/storage": {
"types": "./dist/utils/storage.d.ts",
"default": "./dist/utils/storage.mjs"
@@ -85,13 +85,13 @@ export function devHtmlPrerender(
inlineScripts.forEach((script) => {
// Save the text content for later
const textContent = script.textContent ?? '';
const textHash = hash(textContent);
inlineScriptContents[textHash] = textContent;
const key = hash(textContent);
inlineScriptContents[key] = textContent;
// Replace unsafe inline script
const virtualScript = document.createElement('script');
virtualScript.type = 'module';
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${textHash}`;
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${key}`;
script.replaceWith(virtualScript);
});
@@ -111,7 +111,7 @@ export function devHtmlPrerender(
},
},
{
name: 'wxt:virtualize-react-refresh',
name: 'wxt:virtualize-inline-scripts',
apply: 'serve',
resolveId(id) {
// Resolve inline scripts
@@ -127,9 +127,9 @@ export function devHtmlPrerender(
load(id) {
// Resolve virtualized inline scripts
if (id.startsWith(resolvedVirtualInlineScript)) {
// id="virtual:wxt-inline-script?<hash>"
const newHash = hash(id.substring(id.indexOf('?')));
return inlineScriptContents[newHash];
// id="virtual:wxt-inline-script?<key>"
const key = id.substring(id.indexOf('?') + 1);
return inlineScriptContents[key];
}
// Ignore chunks during HTML file pre-rendering
@@ -150,6 +150,13 @@ export function pointToDevServer(
attr: string,
) {
document.querySelectorAll(querySelector).forEach((element) => {
if (
element.hasAttribute('vite-ignore') ||
element.hasAttribute('wxt-ignore')
) {
element.removeAttribute('wxt-ignore');
return;
}
const src = element.getAttribute(attr);
if (!src || isUrl(src)) return;
+8 -6
View File
@@ -73,16 +73,18 @@ async function getPathsDeclarationEntry(
isHtmlEntrypoint(entry) ? '.html' : '.js',
),
)
.concat([''])
.concat(await getPublicFiles());
await wxt.hooks.callHook('prepare:publicPaths', wxt, paths);
const unions = paths
.map(normalizePath)
.sort()
.map((path) => ` | "/${path}"`)
.join('\n');
const unions = [
` | ""`,
` | "/"`,
...paths
.map(normalizePath)
.sort()
.map((path) => ` | "/${path}"`),
].join('\n');
const template = `// Generated by wxt
import "wxt/browser";
@@ -1503,7 +1503,7 @@ describe('Manifest Utils', () => {
expect(actual).toMatchObject({
content_security_policy:
"script-src 'self' http://localhost:3000; object-src 'self';",
permissions: ['http://localhost:3000/*', 'tabs'],
permissions: ['http://localhost/*', 'tabs'],
});
});
@@ -1535,7 +1535,7 @@ describe('Manifest Utils', () => {
sandbox:
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3000; sandbox allow-scripts allow-forms allow-popups allow-modals; child-src 'self';",
},
host_permissions: ['http://localhost:3000/*'],
host_permissions: ['http://localhost/*'],
permissions: ['tabs', 'scripting'],
});
});
@@ -668,27 +668,46 @@ describe('findEntrypoints', () => {
expect(entrypoints[0]).toEqual(expected);
});
it('should not allow multiple entrypoints with the same name', async () => {
it('should ignore other index files in the same directory when index.html exists', async () => {
globMock.mockResolvedValueOnce([
'content/index.ts',
'popup/index.html',
'popup/index.ts',
'popup/index.css',
]);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(2);
expect(entrypoints[0]).toMatchObject({
type: 'content-script',
name: 'content',
});
expect(entrypoints[1]).toMatchObject({
type: 'popup',
name: 'popup',
});
});
it('should not allow a file entrypoint and directory entrypoint to have the same name', async () => {
globMock.mockResolvedValueOnce([
'options/index.html',
'options/index.jsx',
'popup.html',
'popup/index.html',
'popup/index.ts',
'ui.html',
'other.ts',
'other/index.ts',
]);
await expect(() => findEntrypoints()).rejects.toThrowError(
[
'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
'',
'- options',
` - ${unnormalizePath('src/entrypoints/options/index.html')}`,
` - ${unnormalizePath('src/entrypoints/options/index.jsx')}`,
'- other',
` - ${unnormalizePath('src/entrypoints/other.ts')}`,
` - ${unnormalizePath('src/entrypoints/other/index.ts')}`,
'- popup',
` - ${unnormalizePath('src/entrypoints/popup.html')}`,
` - ${unnormalizePath('src/entrypoints/popup/index.html')}`,
` - ${unnormalizePath('src/entrypoints/popup/index.ts')}`,
].join('\n'),
);
});
@@ -53,20 +53,34 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
relativePaths.sort();
const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
const entrypointInfos: EntrypointInfo[] = relativePaths.reduce<
EntrypointInfo[]
>((results, relativePath) => {
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
const matchingGlob = pathGlobs.find((glob) =>
minimatch(relativePath, glob),
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({ name, inputPath, type });
}
return results;
}, []);
const entrypointInfos: EntrypointInfo[] = relativePaths
.reduce<EntrypointInfo[]>((results, relativePath) => {
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
const matchingGlob = pathGlobs.find((glob) =>
minimatch(relativePath, glob),
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({ name, inputPath, type });
}
return results;
}, [])
.filter(({ name, inputPath }, _, entrypointInfos) => {
// Remove <name>/index.* if <name>/index.html exists
if (inputPath.endsWith('.html')) return true;
const isIndexFile = /index\..+$/.test(inputPath);
if (!isIndexFile) return true;
const hasIndexHtml = entrypointInfos.some(
(entry) =>
entry.name === name && entry.inputPath.endsWith('index.html'),
);
if (hasIndexHtml) return false;
return true;
});
await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);
+7 -1
View File
@@ -459,7 +459,13 @@ function discoverIcons(
}
function addDevModeCsp(manifest: Browser.runtime.Manifest): void {
const permission = `${wxt.server?.origin ?? ''}/*`;
let permissonUrl = wxt.server?.origin;
if (permissonUrl) {
const permissionUrlInstance = new URL(permissonUrl);
permissionUrlInstance.port = '';
permissonUrl = permissionUrlInstance.toString();
}
const permission = `${permissonUrl}*`;
const allowedCsp = wxt.server?.origin ?? 'http://localhost:*';
if (manifest.manifest_version === 3) {
+3 -3
View File
@@ -26,11 +26,10 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
wxt.logger.info('Zipping extension...');
const zipFiles: string[] = [];
const packageJson = await getPackageJson();
const projectName =
wxt.config.zip.name ??
safeFilename(
(await getPackageJson())?.name || path.basename(process.cwd()),
);
safeFilename(packageJson?.name || path.basename(process.cwd()));
const applyTemplate = (template: string): string =>
template
.replaceAll('{{name}}', projectName)
@@ -39,6 +38,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
'{{version}}',
output.manifest.version_name ?? output.manifest.version,
)
.replaceAll('{{packageVersion}}', packageJson?.version)
.replaceAll('{{mode}}', wxt.config.mode)
.replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);
+2
View File
@@ -147,6 +147,7 @@ export interface InlineConfig {
*
* - <span v-pre>`{{name}}`</span> - The project's name converted to kebab-case
* - <span v-pre>`{{version}}`</span> - The version_name or version from the manifest
* - <span v-pre>`{{packageVersion}}`</span> - The version from the package.json
* - <span v-pre>`{{browser}}`</span> - The target browser from the `--browser` CLI flag
* - <span v-pre>`{{mode}}`</span> - The current mode
* - <span v-pre>`{{manifestVersion}}`</span> - Either "2" or "3"
@@ -171,6 +172,7 @@ export interface InlineConfig {
*
* - <span v-pre>`{{name}}`</span> - The project's name converted to kebab-case
* - <span v-pre>`{{version}}`</span> - The version_name or version from the manifest
* - <span v-pre>`{{packageVersion}}`</span> - The version from the package.json
* - <span v-pre>`{{browser}}`</span> - The target browser from the `--browser` CLI flag
* - <span v-pre>`{{mode}}`</span> - The current mode
* - <span v-pre>`{{manifestVersion}}`</span> - Either "2" or "3"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import type { ContentScriptUi, ContentScriptUiOptions } from './types';
import { createIsolatedElement } from '@webext-core/isolated-element';
import { applyPosition, createMountFunctions, mountUi } from './shared';
import { logger } from '../internal/logger';
import { splitShadowRootCss } from '../split-shadow-root-css';
/**
* Create a content script UI inside a [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
@@ -17,6 +18,7 @@ export async function createShadowRootUi<TMounted>(
ctx: ContentScriptContext,
options: ShadowRootContentScriptUiOptions<TMounted>,
): Promise<ShadowRootContentScriptUi<TMounted>> {
const instanceId = Math.random().toString(36).substring(2, 15);
const css: string[] = [];
if (!options.inheritStyles) {
@@ -31,6 +33,9 @@ export async function createShadowRootUi<TMounted>(
css.push(entryCss.replaceAll(':root', ':host'));
}
// Some rules must be applied outside the shadow root, so split the CSS apart
const { shadowCss, documentCss } = splitShadowRootCss(css.join('\n').trim());
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
@@ -38,7 +43,7 @@ export async function createShadowRootUi<TMounted>(
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
textContent: shadowCss,
},
mode: options.mode ?? 'open',
isolateEvents: options.isolateEvents,
@@ -51,6 +56,20 @@ export async function createShadowRootUi<TMounted>(
// Add shadow root element to DOM
mountUi(shadowHost, options);
applyPosition(shadowHost, shadow.querySelector('html'), options);
// Add document CSS
if (
documentCss &&
!document.querySelector(
`style[wxt-shadow-root-document-styles="${instanceId}"]`,
)
) {
const style = document.createElement('style');
style.textContent = documentCss;
style.setAttribute('wxt-shadow-root-document-styles', instanceId);
(document.head ?? document.body).append(style);
}
// Mount UI inside shadow root
mounted = options.onMount(uiContainer, shadow, shadowHost);
};
@@ -58,11 +77,20 @@ export async function createShadowRootUi<TMounted>(
const remove = () => {
// Cleanup mounted state
options.onRemove?.(mounted);
// Detach shadow root from DOM
shadowHost.remove();
// Remove document CSS
const documentStyle = document.querySelector(
`style[wxt-shadow-root-document-styles="${instanceId}"]`,
);
documentStyle?.remove();
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
// Clear mounted value
mounted = undefined;
};
@@ -9,6 +9,9 @@ export type ContentScriptUiOptions<TMounted> = ContentScriptPositioningOptions &
/**
* Callback called before the UI is removed from the webpage. Use to cleanup your UI, like
* unmounting your Vue or React apps.
*
* Note that this callback is called only when `ui.remove` is called - that means it is
* not called automatically when the anchor is removed, unless you use `autoMount`.
*/
onRemove?: (mounted: TMounted | undefined) => void;
};
@@ -0,0 +1,25 @@
/**
* Given a CSS string that will be loaded into a shadow root, split it into two parts:
* - `documentCss`: CSS that needs to be applied to the document (like `@property`)
* - `shadowCss`: CSS that needs to be applied to the shadow root
* @param css
*/
export function splitShadowRootCss(css: string): {
documentCss: string;
shadowCss: string;
} {
let shadowCss = css;
let documentCss = '';
const rulesRegex = /(\s*@property[\s\S]*?{[\s\S]*?})/gm;
let match;
while ((match = rulesRegex.exec(css)) !== null) {
documentCss += match[1];
shadowCss = shadowCss.replace(match[1], '');
}
return {
documentCss: documentCss.trim(),
shadowCss: shadowCss.trim(),
};
}
+1
View File
@@ -14,6 +14,7 @@
"src/utils/define-unlisted-script.ts",
"src/utils/define-wxt-plugin.ts",
"src/utils/match-patterns.ts",
"src/utils/split-shadow-root-css.ts",
"src/utils/storage.ts",
"src/testing/index.ts",
"src/modules.ts"
+238 -506
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -105,5 +105,5 @@ catalog:
vitest-mock-extended: ^3.1.0
vitest-plugin-random-seed: ^1.1.1
vue: ^3.5.13
web-ext-run: ^0.2.2
web-ext-run: ^0.2.3
webextension-polyfill: ^0.12.0

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 559 B

After

Width:  |  Height:  |  Size: 559 B

Before

Width:  |  Height:  |  Size: 916 B

After

Width:  |  Height:  |  Size: 916 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB