Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7b3fc53eb | |||
| 4b44fb849d | |||
| 5d0a266e9c | |||
| 24667861db | |||
| 25cc403886 | |||
| 923312c482 | |||
| adeef6eb73 | |||
| 6f84d45960 | |||
| 938c25cab4 |
@@ -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, {}>>();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.20.12",
|
||||
"version": "0.20.13",
|
||||
"description": "⚡ Next-gen Web Extension Framework",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user