Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b61fd1716e | |||
| d584b5cc7e | |||
| faf9cedfbd | |||
| 3a38ac9339 | |||
| 7ea36a3cfc | |||
| 3577c0b47a | |||
| d0bef42186 | |||
| d9f10c62ad | |||
| 35d3c03e4b | |||
| fdd38a1580 | |||
| 3018801f06 | |||
| 8e96bfef06 | |||
| 31071bd11e | |||
| f5619f6be1 | |||
| adad1b5a2c | |||
| a6c4e19a5d | |||
| af6f74c344 | |||
| ce45cb8a89 | |||
| 14d4aa202d | |||
| 4c35798dba | |||
| 656a9b365f | |||
| 67fa3db921 | |||
| f2d3061e97 | |||
| 760c34e416 | |||
| 48398b315c | |||
| 45d0d9d7f1 | |||
| b0f4ac8221 |
@@ -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:
|
||||
|
||||
@@ -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 ++]
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
```
|
||||
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 |