Compare commits

...

9 Commits

Author SHA1 Message Date
github-actions[bot] b7b3fc53eb chore(release): wxt v0.20.13
📼 VHS / Create VHS (push) Cancelled after 0s
2025-12-16 23:10:49 +00:00
rxliuli 4b44fb849d fix: Don't return promises from unlisted scripts that do not have an async main function (#1907)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:58:46 -06:00
Dan McGee 5d0a266e9c fix: Add another defineItem signature when init function is passed (#1909) 2025-12-16 16:56:24 -06:00
Sebastian Landwehr 24667861db feat: Support .wxtrc config file (#1833)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:33:37 -06:00
Johan Kiviniemi 25cc403886 refactor: Use script.text instead of innerHTML in injectScript (#1764)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:17:07 -06:00
Johan Kiviniemi 923312c482 feat: Make injectScript return the created script element (#1838)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 16:09:07 -06:00
Johan Kiviniemi adeef6eb73 fix: Make injectScript wait until script is actually loaded (#1763)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 15:54:41 -06:00
Johan Kiviniemi 6f84d45960 feat: Add modifyScript option to injectScript (#1762)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 15:47:40 -06:00
Johan Kiviniemi 938c25cab4 feat: Remove script element immediately in injectScript (#1761)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-12-16 15:42:56 -06:00
10 changed files with 214 additions and 17 deletions
+70
View File
@@ -591,6 +591,76 @@ For MV3, `injectScript` is synchronous and the injected script will be evaluated
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
:::
The `script` element can be modified just before it is added to the DOM by using the `modifyScript` option. This can be used to e.g. modify `script.async`/`script.defer`, add event listeners to the element, or pass data to the script via `script.dataset`. An example:
```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
await injectScript('/example-main-world.js', {
modifyScript(script) {
script.dataset['greeting'] = 'Hello there';
},
});
},
});
```
```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
console.log(document.currentScript?.dataset['greeting']);
});
```
`injectScript` returns the created script element. It can be used to e.g. send messages to the script in the form of custom events. The script can add an event listener for them via `document.currentScript`. An example of bidirectional communication:
```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
const { script } = await injectScript('/example-main-world.js', {
modifyScript(script) {
// Add a listener before the injected script is loaded.
script.addEventListener('from-injected-script', (event) => {
if (event instanceof CustomEvent) {
console.log(`${event.type}:`, event.detail);
}
});
},
});
// Send an event after the injected script is loaded.
script.dispatchEvent(
new CustomEvent('from-content-script', {
detail: 'General Kenobi',
}),
);
},
});
```
```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
const script = document.currentScript;
script?.addEventListener('from-content-script', (event) => {
if (event instanceof CustomEvent) {
console.log(`${event.type}:`, event.detail);
}
});
script?.dispatchEvent(
new CustomEvent('from-injected-script', {
detail: 'Hello there',
}),
);
});
```
## Mounting UI to dynamic element
In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
@@ -1315,6 +1315,18 @@ describe('Storage Utils', () => {
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
});
it('should define a non-null value when options are passed with a non-null init function', () => {
const item = storage.defineItem(`local:test`, {
init: () => 123,
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
const item2 = storage.defineItem(`local:test`, {
init: () => Promise.resolve(123),
});
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();
});
});
});
+6
View File
@@ -752,6 +752,12 @@ export interface WxtStorage {
key: StorageItemKey,
options: WxtStorageItemOptions<TValue> & { defaultValue: TValue },
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue> & {
init: () => TValue | Promise<TValue>;
},
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
+26
View File
@@ -1,5 +1,31 @@
# Changelog
## v0.20.13
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.12...wxt-v0.20.13)
### 🚀 Enhancements
- Remove script element immediately in injectScript ([#1761](https://github.com/wxt-dev/wxt/pull/1761))
- Add `modifyScript` option to `injectScript` ([#1762](https://github.com/wxt-dev/wxt/pull/1762))
- Make `injectScript` return the created script element ([#1838](https://github.com/wxt-dev/wxt/pull/1838))
- Support `.wxtrc` config file ([#1833](https://github.com/wxt-dev/wxt/pull/1833))
### 🩹 Fixes
- Make `injectScript` wait until script is actually loaded ([#1763](https://github.com/wxt-dev/wxt/pull/1763))
- Don't return promises from unlisted scripts that do not have an async `main` function ([#1907](https://github.com/wxt-dev/wxt/pull/1907))
### 💅 Refactors
- Use `script.text` instead of `innerHTML` in `injectScript` ([#1764](https://github.com/wxt-dev/wxt/pull/1764))
### ❤️ Contributors
- Rxliuli ([@rxliuli](https://github.com/rxliuli))
- Sebastian Landwehr <info@sebastianlandwehr.com>
- Johan Kiviniemi ([@ion1](https://github.com/ion1))
## v0.20.12
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.11...wxt-v0.20.12)
+3 -5
View File
@@ -139,17 +139,15 @@ describe('User Config', () => {
).toBe(true);
});
it('should throw error when config file not exist', async () => {
it('should not throw error when config file not exist', async () => {
const project = new TestProject();
project.addFile(
'src/entrypoints/background.ts',
'entrypoints/background.ts',
`export default defineBackground(
() => console.log('Hello background'),
);`,
);
await expect(
project.build({ configFile: 'foo.config.ts' }),
).rejects.toThrowError(/not found/);
await project.build({ configFile: 'foo.config.ts' });
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.12",
"version": "0.20.13",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
-4
View File
@@ -50,11 +50,7 @@ export async function resolveConfig(
configFile: inlineConfig.configFile,
name: 'wxt',
cwd: inlineConfig.root ?? process.cwd(),
rcFile: false,
});
if (inlineConfig.configFile && metadata.layers?.length === 0) {
throw Error(`Config file "${inlineConfig.configFile}" not found`);
}
userConfig = loadedConfig ?? {};
userConfigMetadata = metadata;
}
@@ -21,4 +21,22 @@ describe('defineUnlistedScript', () => {
expect(actual).toEqual({ main });
});
it('should return the result without awaiting for synchronous main functions', () => {
const main = vi.fn(() => 'test');
const actual = defineUnlistedScript(main);
expect(actual).toEqual({ main });
expect(actual.main()).eq('test');
});
it('should return a promise of a result for async main functions', async () => {
const main = vi.fn(() => Promise.resolve('test'));
const actual = defineUnlistedScript(main);
expect(actual).toEqual({ main });
await expect(actual.main()).resolves.toEqual('test');
});
});
+56 -5
View File
@@ -15,28 +15,62 @@ export type ScriptPublicPath = Extract<
*
* Make sure to add the injected script to your manifest's
* `web_accessible_resources`.
*
* @returns A result object containing the created script element.
*/
export async function injectScript(
path: ScriptPublicPath,
options?: InjectScriptOptions,
): Promise<void> {
): Promise<InjectScriptResult> {
// @ts-expect-error: getURL is defined per-project, but not inside the package
const url = browser.runtime.getURL(path);
const script = document.createElement('script');
if (browser.runtime.getManifest().manifest_version === 2) {
// MV2 requires using an inline script
script.innerHTML = await fetch(url).then((res) => res.text());
script.text = await fetch(url).then((res) => res.text());
} else {
// MV3 requires using src
script.src = url;
}
if (!options?.keepInDom) {
script.onload = () => script.remove();
}
const loadedPromise = makeLoadedPromise(script);
await options?.modifyScript?.(script);
(document.head ?? document.documentElement).append(script);
if (!options?.keepInDom) {
script.remove();
}
await loadedPromise;
return {
script,
};
}
function makeLoadedPromise(script: HTMLScriptElement): Promise<void> {
return new Promise((resolve, reject) => {
const onload = () => {
resolve();
cleanup();
};
const onerror = () => {
reject(new Error(`Failed to load script: ${script.src}`));
cleanup();
};
const cleanup = () => {
script.removeEventListener('load', onload);
script.removeEventListener('error', onerror);
};
script.addEventListener('load', onload);
script.addEventListener('error', onerror);
});
}
export interface InjectScriptOptions {
@@ -45,4 +79,21 @@ export interface InjectScriptOptions {
* injected. To disable this behavior, set this flag to true.
*/
keepInDom?: boolean;
/**
* Modify the script element just before it is added to the DOM.
*
* It can be used to e.g. modify `script.async`/`script.defer`, add event
* listeners to the element, or pass data to the script via `script.dataset`
* (which can be accessed by the script via `document.currentScript`).
*/
modifyScript?: (script: HTMLScriptElement) => Promise<void> | void;
}
export interface InjectScriptResult {
/**
* The created script element. It can be used to e.g. send messages to the
* script in the form of custom events. The script can add an event listener
* for them via `document.currentScript`.
*/
script: HTMLScriptElement;
}
@@ -2,10 +2,29 @@ import definition from 'virtual:user-unlisted-script-entrypoint';
import { logger } from '../utils/internal/logger';
import { initPlugins } from 'virtual:wxt-plugins';
const result = (async () => {
const result = (() => {
try {
initPlugins();
return await definition.main();
} catch (err) {
logger.error(
`Failed to initialize plugins for "${import.meta.env.ENTRYPOINT}"`,
err,
);
throw err;
}
let result;
try {
result = definition.main();
if (result instanceof Promise) {
result = (result as Promise<any>).catch((err) => {
logger.error(
`The unlisted script "${import.meta.env.ENTRYPOINT}" crashed on startup!`,
err,
);
throw err;
});
}
} catch (err) {
logger.error(
`The unlisted script "${import.meta.env.ENTRYPOINT}" crashed on startup!`,
@@ -13,6 +32,7 @@ const result = (async () => {
);
throw err;
}
return result;
})();
// Return the main function's result to the background when executed via the