feat!: Individual exports and introduce the #imports module (#1258)

This commit is contained in:
Aaron
2024-12-10 18:05:36 -06:00
parent 894a9fc68b
commit e29fdcbb9d
65 changed files with 718 additions and 461 deletions
+19 -13
View File
@@ -11,19 +11,7 @@ export default defineConfig({
});
```
By default, WXT sets up auto-imports for all of it's own APIs:
- [`browser`](/api/reference/wxt/browser/variables/browser) from `wxt/browser`
- [`defineContentScript`](/api/reference/wxt/sandbox/functions/defineContentScript) from `wxt/sandbox`
- [`defineBackground`](/api/reference/wxt/sandbox/functions/defineBackground) from `wxt/sandbox`
- [`defineUnlistedScript`](/api/reference/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
- [`createIntegratedUi`](/api/reference/wxt/client/functions/createIntegratedUi) from `wxt/client`
- [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) from `wxt/client`
- [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi) from `wxt/client`
- [`fakeBrowser`](/api/reference/wxt/testing/variables/fakeBrowser) from `wxt/testing`
- And more!
WXT also adds some project directories as auto-import sources automatically:
By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:
- `<srcDir>/components/*`
- `<srcDir>/composables/*`
@@ -32,6 +20,8 @@ WXT also adds some project directories as auto-import sources automatically:
All named and default exports from files in these directories are available everywhere else in your project without having to import them.
To see the complete list of auto-imported APIs, run [`wxt prepare`](/api/cli/wxt-prepare) and look at your project's `.wxt/types/imports-module.d.ts` file.
## TypeScript
For TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare).
@@ -110,3 +100,19 @@ export default defineConfig({
imports: false, // [!code ++]
});
```
## Explicit Imports (`#imports`)
You can manually import all of WXT's APIs via the `#imports` module:
```ts
import {
createShadowRootUi,
ContentScriptContext,
MatchPattern,
} from '#imports';
```
To learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module).
If you've disabled auto-imports, you can use `#imports` to import all of WXT's APIs from a single place.
+4 -4
View File
@@ -5,10 +5,10 @@
Define runtime configuration in a single place, `<srcDir>/app.config.ts`:
```ts
import { defineAppConfig } from 'wxt/sandbox';
import { defineAppConfig } from '#imports';
// Define types for your config
declare module 'wxt/sandbox' {
declare module 'wxt/utils/define-app-config' {
export interface WxtAppConfig {
theme?: 'light' | 'dark';
}
@@ -26,7 +26,7 @@ This file is committed to the repo, so don't put any secrets here. Instead, use
To access runtime config, WXT provides the `useAppConfig` function:
```ts
import { useAppConfig } from 'wxt/sandbox';
import { useAppConfig } from '#imports';
console.log(useAppConfig()); // { theme: "dark" }
```
@@ -36,7 +36,7 @@ console.log(useAppConfig()); // { theme: "dark" }
You can use environment variables in the `app.config.ts` file.
```ts
declare module 'wxt/sandbox' {
declare module 'wxt/utils/define-app-config' {
export interface WxtAppConfig {
apiKey?: string;
skipWelcome: boolean;
+5 -5
View File
@@ -256,13 +256,13 @@ export default defineContentScript({
:::
See the [API Reference](/api/reference/wxt/client/functions/createIntegratedUi) for the complete list of options.
See the [API Reference](/api/reference/wxt/utils/content-script-ui/functions/createIntegratedUi) for the complete list of options.
### Shadow Root
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
WXT's [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
WXT's [`createShadowRootUi`](/api/reference/wxt/utils/content-script-ui/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
To use `createShadowRootUi`, follow these steps:
@@ -445,7 +445,7 @@ export default defineContentScript({
:::
See the [API Reference](/api/reference/wxt/client/functions/createShadowRootUi) for the complete list of options.
See the [API Reference](/api/reference/wxt/utils/content-script-ui/functions/createShadowRootUi) for the complete list of options.
Full examples:
@@ -456,7 +456,7 @@ Full examples:
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/functions/createIframeUi), which simplifies setting up the IFrame.
1. Create an HTML page that will be loaded into your IFrame:
```html
@@ -510,7 +510,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/fun
});
```
See the [API Reference](/api/reference/wxt/client/functions/createIframeUi) for the complete list of options.
See the [API Reference](/api/reference/wxt/utils/content-script-ui/functions/createIframeUi) for the complete list of options.
## Isolated World vs Main World
+28
View File
@@ -71,6 +71,34 @@ describe('isLoggedIn', () => {
});
```
### Mocking WXT APIs
First, you need to understand how the `#imports` module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path.
For example, this is what your write in your source code:
```ts
// What you write
import { injectScript, createShadowRootUi } from '#imports';
```
But Vitest sees this:
```ts
import { injectScript } from 'wxt/browser';
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
```
So in this case, if you wanted to mock `injectScript`, you need to pass in `"wxt/utils/inject-script"`, not `"#imports"`.
```ts
vi.mock("wxt/utils/inject-script", () => ({
injectScript: ...
}))
```
Refer to your project's `.wxt/types/imports-module.d.ts` file to lookup real import paths for `#imports`. If the file doesn't exist, run [`wxt prepare`](/guide/essentials/config/typescript).
## Other Testing Frameworks
To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use.
+2 -2
View File
@@ -111,12 +111,12 @@ export default defineWxtModule<AnalyticModuleOptions>({
```ts
import { defineWxtModule } from 'wxt/modules';
import 'wxt/sandbox';
import 'wxt/utils/define-app-config';
export interface MyModuleRuntimeOptions {
// Add your runtime options here...
}
declare module 'wxt/sandbox' {
declare module 'wxt/utils/define-app-config' {
export interface WxtAppConfig {
myModule: MyModuleOptions;
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { defineAppConfig } from 'wxt/sandbox';
import { defineAppConfig } from '#imports';
declare module 'wxt/sandbox' {
declare module 'wxt/utils/define-app-config' {
export interface WxtAppConfig {
example: string;
}
+2 -5
View File
@@ -1,5 +1,5 @@
import { defineBuildConfig } from 'unbuild';
import { version } from './package.json';
import { version, exports } from './package.json';
import { readFile, writeFile } from 'fs/promises';
import {
virtualEntrypointModuleNames,
@@ -32,10 +32,7 @@ export default defineBuildConfig([
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
'virtual:wxt-plugins',
'virtual:app-config',
'wxt/browser',
'wxt/sandbox',
'wxt/client',
'wxt/testing',
...Object.keys(exports).map((path) => 'wxt' + path.slice(1)), // ./storage => wxt/storage
],
})),
]);
@@ -15,7 +15,6 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
@@ -42,7 +41,6 @@ const globals = {
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
@@ -77,7 +75,6 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
@@ -105,7 +102,6 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
"createShadowRootUi": "readonly",
"defineAppConfig": "readonly",
"defineBackground": "readonly",
"defineConfig": "readonly",
"defineContentScript": "readonly",
"defineUnlistedScript": "readonly",
"defineWxtPlugin": "readonly",
+141 -15
View File
@@ -17,24 +17,23 @@ describe('Auto Imports', () => {
// Generated by wxt
export {}
declare global {
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
const ContentScriptContext: typeof import('wxt/utils/content-script-context')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/utils/match-patterns')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/utils/match-patterns')['MatchPattern']
const MigrationError: typeof import('wxt/storage')['MigrationError']
const browser: typeof import('wxt/browser')['browser']
const createIframeUi: typeof import('wxt/client')['createIframeUi']
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
const defineAppConfig: typeof import('wxt/sandbox')['defineAppConfig']
const defineBackground: typeof import('wxt/sandbox')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
const defineWxtPlugin: typeof import('wxt/sandbox')['defineWxtPlugin']
const createIframeUi: typeof import('wxt/utils/content-script-ui/iframe')['createIframeUi']
const createIntegratedUi: typeof import('wxt/utils/content-script-ui/integrated')['createIntegratedUi']
const createShadowRootUi: typeof import('wxt/utils/content-script-ui/shadow-root')['createShadowRootUi']
const defineAppConfig: typeof import('wxt/utils/define-app-config')['defineAppConfig']
const defineBackground: typeof import('wxt/utils/define-background')['defineBackground']
const defineContentScript: typeof import('wxt/utils/define-content-script')['defineContentScript']
const defineUnlistedScript: typeof import('wxt/utils/define-unlisted-script')['defineUnlistedScript']
const defineWxtPlugin: typeof import('wxt/utils/define-wxt-plugin')['defineWxtPlugin']
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
const injectScript: typeof import('wxt/client')['injectScript']
const injectScript: typeof import('wxt/utils/inject-script')['injectScript']
const storage: typeof import('wxt/storage')['storage']
const useAppConfig: typeof import('wxt/client')['useAppConfig']
const useAppConfig: typeof import('wxt/utils/app-config')['useAppConfig']
}
"
`);
@@ -55,10 +54,52 @@ describe('Auto Imports', () => {
/// <reference path="./types/paths.d.ts" />
/// <reference path="./types/i18n.d.ts" />
/// <reference path="./types/globals.d.ts" />
/// <reference path="./types/imports-module.d.ts" />
/// <reference path="./types/imports.d.ts" />
"
`);
});
it('should generate the #imports module', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
// Project auto-imports should also be present
project.addFile(
'utils/time.ts',
`export function startOfDay(date: Date): Date {
throw Error("TODO")
}`,
);
await project.prepare();
expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports-module.d.ts
----------------------------------------
// Generated by wxt
// Types for the #import virtual module
declare module '#imports' {
export { browser } from 'wxt/browser';
export { MigrationError, storage } from 'wxt/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext } from 'wxt/utils/content-script-context';
export { createIframeUi } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi } from 'wxt/utils/content-script-ui/integrated';
export { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
export { defineAppConfig } from 'wxt/utils/define-app-config';
export { defineBackground } from 'wxt/utils/define-background';
export { defineContentScript } from 'wxt/utils/define-content-script';
export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
export { startOfDay } from '../utils/time';
}
"
`);
});
});
describe('imports: false', () => {
@@ -74,7 +115,7 @@ describe('Auto Imports', () => {
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
});
it('should not include imports.d.ts in the type references', async () => {
it('should only include imports-module.d.ts in the the project', async () => {
const project = new TestProject();
project.setConfigFileConfig({
imports: false,
@@ -94,10 +135,95 @@ describe('Auto Imports', () => {
/// <reference path="./types/paths.d.ts" />
/// <reference path="./types/i18n.d.ts" />
/// <reference path="./types/globals.d.ts" />
/// <reference path="./types/imports-module.d.ts" />
"
`,
);
});
it('should only generate the #imports module', async () => {
const project = new TestProject();
project.setConfigFileConfig({
imports: false,
});
project.addFile('entrypoints/popup.html', `<html></html>`);
// Project auto-imports should also be present
project.addFile(
'utils/time.ts',
`export function startOfDay(date: Date): Date {
throw Error("TODO")
}`,
);
await project.prepare();
expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports-module.d.ts
----------------------------------------
// Generated by wxt
// Types for the #import virtual module
declare module '#imports' {
export { browser } from 'wxt/browser';
export { MigrationError, storage } from 'wxt/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext } from 'wxt/utils/content-script-context';
export { createIframeUi } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi } from 'wxt/utils/content-script-ui/integrated';
export { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
export { defineAppConfig } from 'wxt/utils/define-app-config';
export { defineBackground } from 'wxt/utils/define-background';
export { defineContentScript } from 'wxt/utils/define-content-script';
export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
}
"
`);
});
it('should still generate the #imports module', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
// Project auto-imports should also be exported
project.addFile(
'utils/time.ts',
`export function startOfDay(date: Date): Date {
throw Error("TODO")
}`,
);
await project.prepare();
expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports-module.d.ts
----------------------------------------
// Generated by wxt
// Types for the #import virtual module
declare module '#imports' {
export { browser } from 'wxt/browser';
export { MigrationError, storage } from 'wxt/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext } from 'wxt/utils/content-script-context';
export { createIframeUi } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi } from 'wxt/utils/content-script-ui/integrated';
export { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
export { defineAppConfig } from 'wxt/utils/define-app-config';
export { defineBackground } from 'wxt/utils/define-background';
export { defineContentScript } from 'wxt/utils/define-content-script';
export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
export { startOfDay } from '../utils/time';
}
"
`);
});
});
describe('eslintrc', () => {
@@ -239,6 +239,7 @@ describe('TypeScript Project', () => {
/// <reference path="./types/paths.d.ts" />
/// <reference path="./types/i18n.d.ts" />
/// <reference path="./types/globals.d.ts" />
/// <reference path="./types/imports-module.d.ts" />
/// <reference path="./types/imports.d.ts" />
"
`);
+107 -63
View File
@@ -3,69 +3,7 @@
"type": "module",
"version": "0.19.29",
"description": "⚡ Next-gen Web Extension Framework",
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git"
},
"homepage": "https://wxt.dev",
"keywords": [
"vite",
"chrome",
"web",
"extension",
"browser",
"bundler",
"framework"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev",
"files": [
"bin",
"dist"
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./client": {
"types": "./dist/client/index.d.ts",
"default": "./dist/client/index.mjs"
},
"./sandbox": {
"types": "./dist/sandbox/index.d.ts",
"default": "./dist/sandbox/index.mjs"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.mjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"default": "./dist/testing/index.mjs"
},
"./storage": {
"types": "./dist/storage.d.ts",
"default": "./dist/storage.mjs"
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
},
"./modules": {
"types": "./dist/modules.d.ts",
"default": "./dist/modules.mjs"
}
},
"scripts": {
"wxt": "tsx src/cli/index.ts",
"build": "buildc -- unbuild",
@@ -142,5 +80,111 @@
"vitest": "catalog:",
"vitest-plugin-random-seed": "catalog:"
},
"peerDependenciesMeta": {}
"peerDependenciesMeta": {},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git"
},
"homepage": "https://wxt.dev",
"keywords": [
"vite",
"chrome",
"web",
"extension",
"browser",
"bundler",
"framework"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"funding": "https://github.com/sponsors/wxt-dev",
"files": [
"bin",
"dist"
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./utils/app-config": {
"types": "./dist/utils/app-config.d.ts",
"default": "./dist/utils/app-config.mjs"
},
"./utils/inject-script": {
"types": "./dist/utils/inject-script.d.ts",
"default": "./dist/utils/inject-script.mjs"
},
"./utils/content-script-context": {
"types": "./dist/utils/content-script-context.d.ts",
"default": "./dist/utils/content-script-context.mjs"
},
"./utils/content-script-ui/types": {
"types": "./dist/utils/content-script-ui/types.d.ts",
"default": "./dist/utils/content-script-ui/types.mjs"
},
"./utils/content-script-ui/integrated": {
"types": "./dist/utils/content-script-ui/integrated.d.ts",
"default": "./dist/utils/content-script-ui/integrated.mjs"
},
"./utils/content-script-ui/shadow-root": {
"types": "./dist/utils/content-script-ui/shadow-root.d.ts",
"default": "./dist/utils/content-script-ui/shadow-root.mjs"
},
"./utils/content-script-ui/iframe": {
"types": "./dist/utils/content-script-ui/iframe.d.ts",
"default": "./dist/utils/content-script-ui/iframe.mjs"
},
"./utils/define-app-config": {
"types": "./dist/utils/define-app-config.d.ts",
"default": "./dist/utils/define-app-config.mjs"
},
"./utils/define-background": {
"types": "./dist/utils/define-background.d.ts",
"default": "./dist/utils/define-background.mjs"
},
"./utils/define-content-script": {
"types": "./dist/utils/define-content-script.d.ts",
"default": "./dist/utils/define-content-script.mjs"
},
"./utils/define-unlisted-script": {
"types": "./dist/utils/define-unlisted-script.d.ts",
"default": "./dist/utils/define-unlisted-script.mjs"
},
"./utils/define-wxt-plugin": {
"types": "./dist/utils/define-wxt-plugin.d.ts",
"default": "./dist/utils/define-wxt-plugin.mjs"
},
"./utils/match-patterns": {
"types": "./dist/utils/match-patterns.d.ts",
"default": "./dist/utils/match-patterns.mjs"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.mjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"default": "./dist/testing/index.mjs"
},
"./storage": {
"types": "./dist/storage.d.ts",
"default": "./dist/storage.mjs"
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
},
"./modules": {
"types": "./dist/modules.d.ts",
"default": "./dist/modules.mjs"
}
}
}
@@ -67,20 +67,5 @@ describe('Module Utilities', () => {
expect(wxt.config.imports && wxt.config.imports.presets).toHaveLength(2);
});
it("should not enable imports if they've been disabled", async () => {
const preset = 'vue';
const wxt = fakeWxt({
hooks: createHooks(),
config: {
imports: false,
},
});
addImportPreset(wxt, preset);
await wxt.hooks.callHook('config:resolved', wxt);
expect(wxt.config.imports).toBe(false);
});
});
});
+8
View File
@@ -1,4 +1,12 @@
/**
* Contains the `browser` export which you should use to access the extension APIs in your project:
* ```ts
* import { browser } from 'wxt/browser';
*
* browser.runtime.onInstalled.addListener(() => {
* // ...
* })
* ```
* @module wxt/browser
*/
import { browser as _browser, type Browser } from '@wxt-dev/browser';
+40 -13
View File
@@ -1,28 +1,27 @@
import { addViteConfig, defineWxtModule } from '../modules';
import type {
EslintGlobalsPropValue,
Wxt,
WxtDirFileEntry,
WxtModule,
WxtResolvedUnimportOptions,
} from '../types';
import { type Unimport, createUnimport } from 'unimport';
import { type Unimport, createUnimport, toExports } from 'unimport';
import UnimportPlugin from 'unimport/unplugin';
export default defineWxtModule({
name: 'wxt:built-in:unimport',
setup(wxt) {
const options = wxt.config.imports;
if (options === false) return;
let unimport: Unimport;
const isEnabled = () => !wxt.config.imports.disabled;
// Add user module imports to config
wxt.hooks.hook('config:resolved', () => {
const addModuleImports = (module: WxtModule<any>) => {
if (!module.imports) return;
options.imports ??= [];
options.imports.push(...module.imports);
wxt.config.imports.imports ??= [];
wxt.config.imports.imports.push(...module.imports);
};
wxt.config.builtinModules.forEach(addModuleImports);
@@ -33,7 +32,7 @@ export default defineWxtModule({
// config inside "config:resolved" are applied.
wxt.hooks.afterEach((event) => {
if (event.name === 'config:resolved') {
unimport = createUnimport(options);
unimport = createUnimport(wxt.config.imports);
}
});
@@ -42,17 +41,29 @@ export default defineWxtModule({
// Update cache before each rebuild
await unimport.init();
// Always generate the #import module types
entries.push(await getImportsModuleEntry(wxt, unimport));
if (!isEnabled()) return;
// Only create global types when user has enabled auto-imports
entries.push(await getImportsDeclarationEntry(unimport));
if (options.eslintrc.enabled === false) return;
if (wxt.config.imports.eslintrc.enabled === false) return;
// Only generate ESLint config if that feature is enabled
entries.push(
await getEslintConfigEntry(unimport, options.eslintrc.enabled, options),
await getEslintConfigEntry(
unimport,
wxt.config.imports.eslintrc.enabled,
wxt.config.imports,
),
);
});
// Add vite plugin
addViteConfig(wxt, () => ({
plugins: [UnimportPlugin.vite(options)],
plugins: [UnimportPlugin.vite(wxt.config.imports)],
}));
},
});
@@ -60,9 +71,6 @@ export default defineWxtModule({
async function getImportsDeclarationEntry(
unimport: Unimport,
): Promise<WxtDirFileEntry> {
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.init();
return {
path: 'types/imports.d.ts',
text: [
@@ -74,6 +82,25 @@ async function getImportsDeclarationEntry(
};
}
async function getImportsModuleEntry(
wxt: Wxt,
unimport: Unimport,
): Promise<WxtDirFileEntry> {
const imports = await unimport.getImports();
return {
path: 'types/imports-module.d.ts',
text: [
'// Generated by wxt',
'// Types for the #import virtual module',
"declare module '#imports' {",
' ' + toExports(imports, wxt.config.wxtDir).replaceAll('\n', '\n '),
'}',
'',
].join('\n'),
tsReference: true,
};
}
async function getEslintConfigEntry(
unimport: Unimport,
version: 8 | 9,
@@ -1,2 +0,0 @@
export * from './content-script-context';
export * from './ui';
-8
View File
@@ -1,8 +0,0 @@
/**
* Any runtime APIs that use the web extension APIs.
*
* @module wxt/client
*/
export * from './content-scripts';
export * from './app-config';
export * from './inject-script';
@@ -15,7 +15,7 @@ export function noopBackground(): Plugin {
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `import { defineBackground } from 'wxt/sandbox';\nexport default defineBackground(() => void 0)`;
return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`;
}
},
};
+3 -2
View File
@@ -232,10 +232,11 @@ async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
.flatMap(([alias, absolutePath]) => {
const aliasPath = getTsconfigPath(absolutePath);
return [
` "${alias}": ["${aliasPath}"]`,
` "${alias}/*": ["${aliasPath}/*"]`,
`"${alias}": ["${aliasPath}"]`,
`"${alias}/*": ["${aliasPath}/*"]`,
];
})
.map((line) => ` ${line}`)
.join(',\n');
const text = `{
+39 -33
View File
@@ -14,7 +14,6 @@ import {
WxtModule,
WxtModuleWithMetadata,
ResolvedEslintrc,
Eslintrc,
} from '../types';
import path from 'node:path';
import { createFsCache } from './utils/cache';
@@ -332,33 +331,40 @@ async function getUnimportOptions(
srcDir: string,
logger: Logger,
config: InlineConfig,
): Promise<WxtResolvedUnimportOptions | false> {
if (config.imports === false) return false;
): Promise<WxtResolvedUnimportOptions> {
const disabled = config.imports === false;
const eslintrc = await getUnimportEslintOptions(wxtDir, config.imports);
// mlly sometimes picks up things as exports that aren't. That's what this array contains.
const invalidExports = ['options'];
const defaultOptions: WxtResolvedUnimportOptions = {
debugLog: logger.debug,
imports: [
{ name: 'defineConfig', from: 'wxt' },
{ name: 'fakeBrowser', from: 'wxt/testing' },
],
imports: [{ name: 'fakeBrowser', from: 'wxt/testing' }],
// prettier-ignore
presets: [
{
package: 'wxt/client',
// There seems to be a bug in unimport that thinks "options" is an
// export from wxt/client, but it doesn't actually exist... so it's
// ignored.
ignore: ['options'],
},
{ package: 'wxt/browser' },
{ package: 'wxt/sandbox' },
{ package: 'wxt/storage' },
{ package: 'wxt/utils/app-config' },
{ package: 'wxt/utils/content-script-context' },
{ package: 'wxt/utils/content-script-ui/iframe', ignore: invalidExports },
{ package: 'wxt/utils/content-script-ui/integrated', ignore: invalidExports },
{ package: 'wxt/utils/content-script-ui/shadow-root', ignore: invalidExports },
{ package: 'wxt/utils/content-script-ui/types' },
{ package: 'wxt/utils/define-app-config' },
{ package: 'wxt/utils/define-background' },
{ package: 'wxt/utils/define-content-script' },
{ package: 'wxt/utils/define-unlisted-script' },
{ package: 'wxt/utils/define-wxt-plugin' },
{ package: 'wxt/utils/inject-script', ignore: invalidExports },
{ package: 'wxt/utils/match-patterns' },
],
virtualImports: ['#imports'],
debugLog: logger.debug,
warn: logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],
dirsScanOptions: {
cwd: srcDir,
},
eslintrc: await getUnimportEslintOptions(wxtDir, config.imports?.eslintrc),
eslintrc,
dirs: disabled ? [] : ['components', 'composables', 'hooks', 'utils'],
disabled,
};
return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>(
@@ -369,34 +375,34 @@ async function getUnimportOptions(
async function getUnimportEslintOptions(
wxtDir: string,
options: Eslintrc | undefined,
options: InlineConfig['imports'],
): Promise<ResolvedEslintrc> {
const rawEslintEnabled = options?.enabled ?? 'auto';
let eslintEnabled: ResolvedEslintrc['enabled'];
switch (rawEslintEnabled) {
const inlineEnabled =
options === false ? false : (options?.eslintrc?.enabled ?? 'auto');
let enabled: ResolvedEslintrc['enabled'];
switch (inlineEnabled) {
case 'auto':
const version = await getEslintVersion();
let major = parseInt(version[0]);
if (isNaN(major)) eslintEnabled = false;
if (major <= 8) eslintEnabled = 8;
else if (major >= 9) eslintEnabled = 9;
if (isNaN(major)) enabled = false;
if (major <= 8) enabled = 8;
else if (major >= 9) enabled = 9;
// NaN
else eslintEnabled = false;
else enabled = false;
break;
case true:
eslintEnabled = 8;
enabled = 8;
break;
default:
eslintEnabled = rawEslintEnabled;
enabled = inlineEnabled;
}
return {
enabled: eslintEnabled,
enabled,
filePath: path.resolve(
wxtDir,
eslintEnabled === 9
? 'eslint-auto-imports.mjs'
: 'eslintrc-auto-import.json',
enabled === 9 ? 'eslint-auto-imports.mjs' : 'eslintrc-auto-import.json',
),
globalsPropValue: true,
};
@@ -57,13 +57,13 @@ describe('Transform Utils', () => {
it('should remove unused imports', () => {
const input = `
import { defineBackground } from "wxt/sandbox"
import { defineBackground } from "#imports"
import { test1 } from "somewhere1"
import test2 from "somewhere2"
export default defineBackground(() => {})
`;
const expected = `import { defineBackground } from "wxt/sandbox"
const expected = `import { defineBackground } from "#imports"
export default defineBackground();`;
@@ -74,13 +74,13 @@ export default defineBackground();`;
it('should remove explict side-effect imports', () => {
const input = `
import { defineBackground } from "wxt/sandbox"
import { defineBackground } from "#imports"
import "my-polyfill"
import "./style.css"
export default defineBackground(() => {})
`;
const expected = `import { defineBackground } from "wxt/sandbox"
const expected = `import { defineBackground } from "#imports"
export default defineBackground();`;
@@ -1,4 +1,4 @@
import { defineBackground } from '../../../../../sandbox';
import { defineBackground } from '../../../../../utils/define-background';
export default defineBackground({
main() {},
@@ -1,4 +1,4 @@
import { defineContentScript } from '../../../../../sandbox';
import { defineContentScript } from '../../../../../utils/define-content-script';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -1,4 +1,4 @@
import { defineContentScript } from '../../../../../sandbox';
import { defineContentScript } from '../../../../../utils/define-content-script';
import { faker } from '@faker-js/faker';
export default defineContentScript({
@@ -1,3 +1,3 @@
import { defineUnlistedScript } from '../../../../../sandbox';
import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script';
export default defineUnlistedScript(() => {});
@@ -1,3 +1,3 @@
import { defineUnlistedScript } from '../../../../../sandbox';
import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script';
export default defineUnlistedScript(() => {});
@@ -1,4 +1,4 @@
import { defineBackground } from '../../../../../sandbox';
import { defineBackground } from '../../../../../utils/define-background';
export const a = {};
@@ -243,6 +243,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
env: { browser, command, manifestVersion, mode },
fsCache: mock<FsCache>(),
imports: {
disabled: faker.datatype.boolean(),
eslintrc: {
enabled: faker.helpers.arrayElement([false, 8, 9]),
filePath: fakeFile(),
+5
View File
@@ -1,4 +1,9 @@
/**
* This module contains:
* - JS APIs used by the CLI to build extensions or start dev mode.
* - Helper functions for defining project config.
* - Types for building and extension or configuring WXT.
*
* @module wxt
*/
export * from './core';
+2 -1
View File
@@ -1,5 +1,5 @@
/**
* Utilities for creating reusable, build-time modules for WXT.
* Utilities for creating [WXT Modules](https://wxt.dev/guide/essentials/wxt-modules.html).
*
* @module wxt/modules
*/
@@ -167,6 +167,7 @@ export function addImportPreset(
preset: UnimportOptions['presets'][0],
): void {
wxt.hooks.hook('config:resolved', (wxt) => {
// In older versions of WXT, `wxt.config.imports` could be false
if (!wxt.config.imports) return;
wxt.config.imports.presets ??= [];
-11
View File
@@ -1,11 +0,0 @@
/**
* Any runtime APIs that don't use the web extension APIs.
*
* @module wxt/sandbox
*/
export * from './define-unlisted-script';
export * from './define-background';
export * from './define-content-script';
export * from './define-wxt-plugin';
export * from './define-app-config';
export * from '@webext-core/match-patterns';
+2 -1
View File
@@ -1,4 +1,5 @@
/**
* @module @wxt-dev/storage
* Re-export the [`@wxt-dev/storage` package](https://www.npmjs.com/package/@wxt-dev/storage).
* @module wxt/storage
*/
export * from '@wxt-dev/storage';
+1
View File
@@ -1,4 +1,5 @@
/**
* Utilities for unit testing WXT extensions.
* @module wxt/testing
*/
export * from './fake-browser';
@@ -37,9 +37,7 @@ export async function WxtVitest(
resolveAppConfig(wxt.config),
extensionApiMock(wxt.config),
];
if (wxt.config.imports !== false) {
plugins.push(UnimportPlugin.vite(wxt.config.imports));
}
plugins.push(UnimportPlugin.vite(wxt.config.imports));
return plugins;
}
+8 -2
View File
@@ -1,7 +1,7 @@
import type * as vite from 'vite';
import { UnimportOptions, Import } from 'unimport';
import { LogLevel } from 'consola';
import type { ContentScriptContext } from './client/content-scripts/content-script-context';
import type { ContentScriptContext } from './utils/content-script-context';
import type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualizer';
import { ResolvedConfig as C12ResolvedConfig } from 'c12';
import { Hookable, NestedHooks } from 'hookable';
@@ -1297,7 +1297,7 @@ export interface ResolvedConfig {
manifestVersion: TargetManifestVersion;
env: ConfigEnv;
logger: Logger;
imports: false | WxtResolvedUnimportOptions;
imports: WxtResolvedUnimportOptions;
manifest: UserManifest;
fsCache: FsCache;
runnerConfig: C12ResolvedConfig<WebExtConfig>;
@@ -1441,6 +1441,12 @@ export type WxtUnimportOptions = Partial<UnimportOptions> & {
};
export type WxtResolvedUnimportOptions = Partial<UnimportOptions> & {
/**
* Set to `true` when the user disabled auto-imports. We still use unimport for the #imports module, but other features should be disabled.
*
* You don't need to check this value before modifying the auto-import options. Even if `disabled` is `true`, there's no harm in adding imports to the config - they'll just be ignored.
*/
disabled: boolean;
eslintrc: ResolvedEslintrc;
};
+1
View File
@@ -0,0 +1 @@
This folder is for public utils, not internal utils. Put generic helpers and other utils in the core/utils folder.
@@ -1,7 +1,7 @@
/** @vitest-environment happy-dom */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ContentScriptContext } from '..';
import { ContentScriptContext } from '../content-script-context';
import { fakeBrowser } from '@webext-core/fake-browser';
/**
@@ -1,6 +1,7 @@
/** @module wxt/utils/app-config */
// @ts-expect-error: Untyped virtual module
import appConfig from 'virtual:app-config';
import type { WxtAppConfig } from '../sandbox/define-app-config';
import type { WxtAppConfig } from '../utils/define-app-config';
export function useAppConfig(): WxtAppConfig {
return appConfig;
@@ -1,8 +1,12 @@
import { ContentScriptDefinition } from '../../types';
/** @module wxt/utils/content-script-context */
import { ContentScriptDefinition } from '../types';
import { browser } from 'wxt/browser';
import { logger } from '../../sandbox/utils/logger';
import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events';
import { createLocationWatcher } from './location-watcher';
import { logger } from '../utils/internal/logger';
import {
WxtLocationChangeEvent,
getUniqueEventName,
} from './internal/custom-events';
import { createLocationWatcher } from './internal/location-watcher';
/**
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
@@ -14,7 +18,7 @@ import { createLocationWatcher } from './location-watcher';
* To create context for testing, you can use the class's constructor:
*
* ```ts
* import { ContentScriptContext } from 'wxt/client';
* import { ContentScriptContext } from 'wxt/utils/content-scripts-context';
*
* test("storage listener should be removed when context is invalidated", () => {
* const ctx = new ContentScriptContext('test');
@@ -1,12 +1,10 @@
/** @vitest-environment happy-dom */
import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest';
import {
createIntegratedUi,
createIframeUi,
createShadowRootUi,
ContentScriptUi,
} from '..';
import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest';
import { createIntegratedUi } from '../integrated';
import { createIframeUi } from '../iframe';
import { createShadowRootUi } from '../shadow-root';
import { ContentScriptContext } from '../../content-script-context';
import { ContentScriptUi } from '../types';
/**
* Util for floating promise.
@@ -0,0 +1,50 @@
/** @module wxt/utils/content-script-ui/iframe */
import { browser } from 'wxt/browser';
import { ContentScriptContext } from '../content-script-context';
import type {
IframeContentScriptUi,
IframeContentScriptUiOptions,
} from './types';
import { applyPosition, createMountFunctions, mountUi } from './shared';
/**
* Create a content script UI using an iframe.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#iframe
*/
export function createIframeUi<TMounted>(
ctx: ContentScriptContext,
options: IframeContentScriptUiOptions<TMounted>,
): IframeContentScriptUi<TMounted> {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-wxt-iframe', '');
const iframe = document.createElement('iframe');
// @ts-expect-error: getURL is defined per-project, but not inside the package
iframe.src = browser.runtime.getURL(options.page);
wrapper.appendChild(iframe);
let mounted: TMounted | undefined = undefined;
const mount = () => {
applyPosition(wrapper, iframe, options);
mountUi(wrapper, options);
mounted = options.onMount?.(wrapper, iframe);
};
const remove = () => {
options.onRemove?.(mounted);
wrapper.remove();
mounted = undefined;
};
const mountFunctions = createMountFunctions({ mount, remove }, options);
ctx.onInvalidated(remove);
return {
get mounted() {
return mounted;
},
iframe,
wrapper,
...mountFunctions,
};
}
@@ -0,0 +1,51 @@
/** @module wxt/utils/content-script-ui/integrated */
import { ContentScriptContext } from '../content-script-context';
import type {
IntegratedContentScriptUi,
IntegratedContentScriptUiOptions,
} from './types';
import { applyPosition, createMountFunctions, mountUi } from './shared';
/**
* Create a content script UI without any isolation.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#integrated
*/
export function createIntegratedUi<TMounted>(
ctx: ContentScriptContext,
options: IntegratedContentScriptUiOptions<TMounted>,
): IntegratedContentScriptUi<TMounted> {
const wrapper = document.createElement(options.tag || 'div');
wrapper.setAttribute('data-wxt-integrated', '');
let mounted: TMounted | undefined = undefined;
const mount = () => {
applyPosition(wrapper, undefined, options);
mountUi(wrapper, options);
mounted = options.onMount?.(wrapper);
};
const remove = () => {
options.onRemove?.(mounted);
wrapper.replaceChildren();
wrapper.remove();
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
get mounted() {
return mounted;
},
wrapper,
...mountFunctions,
};
}
@@ -0,0 +1,104 @@
/** @module wxt/utils/content-script-ui/shadow-root */
import { ContentScriptContext } from '../content-script-context';
import type {
ShadowRootContentScriptUi,
ShadowRootContentScriptUiOptions,
} from './types';
import { createIsolatedElement } from '@webext-core/isolated-element';
import { applyPosition, createMountFunctions, mountUi } from './shared';
import { logger } from '../internal/logger';
/**
* Create a content script UI inside a [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
*
* > This function is async because it has to load the CSS via a network call.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadow-root
*/
export async function createShadowRootUi<TMounted>(
ctx: ContentScriptContext,
options: ShadowRootContentScriptUiOptions<TMounted>,
): Promise<ShadowRootContentScriptUi<TMounted>> {
const css = [options.css ?? ''];
if (ctx.options?.cssInjectionMode === 'ui') {
const entryCss = await loadCss();
// Replace :root selectors with :host since we're in a shadow root
css.push(entryCss.replaceAll(':root', ':host'));
}
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
shadow,
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
},
mode: options.mode ?? 'open',
isolateEvents: options.isolateEvents,
});
shadowHost.setAttribute('data-wxt-shadow-root', '');
let mounted: TMounted | undefined;
const mount = () => {
// Add shadow root element to DOM
mountUi(shadowHost, options);
applyPosition(shadowHost, shadow.querySelector('html'), options);
// Mount UI inside shadow root
mounted = options.onMount(uiContainer, shadow, shadowHost);
};
const remove = () => {
// Cleanup mounted state
options.onRemove?.(mounted);
// Detach shadow root from DOM
shadowHost.remove();
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
// Clear mounted value
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
shadow,
shadowHost,
uiContainer,
...mountFunctions,
get mounted() {
return mounted;
},
};
}
/**
* Load the CSS for the current entrypoint.
*/
async function loadCss(): Promise<string> {
// @ts-expect-error: getURL is defined per-project, but not inside the package
const url = browser.runtime.getURL(
`/content-scripts/${import.meta.env.ENTRYPOINT}.css`,
);
try {
const res = await fetch(url);
return await res.text();
} catch (err) {
logger.warn(
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
err,
);
return '';
}
}
@@ -1,197 +1,20 @@
import { browser } from 'wxt/browser';
import type {
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
AutoMount,
AutoMountOptions,
BaseMountFunctions,
ContentScriptUiOptions,
MountFunctions,
} from './types';
import { waitElement } from '@1natsu/wait-element';
import {
isExist as mountDetector,
isNotExist as removeDetector,
} from '@1natsu/wait-element/detectors';
import { ContentScriptContext } from '..';
import {
AutoMount,
AutoMountOptions,
BaseMountFunctions,
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
ContentScriptUiOptions,
IframeContentScriptUi,
IframeContentScriptUiOptions,
IntegratedContentScriptUi,
IntegratedContentScriptUiOptions,
MountFunctions,
ShadowRootContentScriptUi,
ShadowRootContentScriptUiOptions,
} from './types';
import { logger } from '../../../sandbox/utils/logger';
import { createIsolatedElement } from '@webext-core/isolated-element';
export * from './types';
import { logger } from '../../utils/internal/logger';
/**
* Create a content script UI without any isolation.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#integrated
*/
export function createIntegratedUi<TMounted>(
ctx: ContentScriptContext,
options: IntegratedContentScriptUiOptions<TMounted>,
): IntegratedContentScriptUi<TMounted> {
const wrapper = document.createElement(options.tag || 'div');
wrapper.setAttribute('data-wxt-integrated', '');
let mounted: TMounted | undefined = undefined;
const mount = () => {
applyPosition(wrapper, undefined, options);
mountUi(wrapper, options);
mounted = options.onMount?.(wrapper);
};
const remove = () => {
options.onRemove?.(mounted);
wrapper.replaceChildren();
wrapper.remove();
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
get mounted() {
return mounted;
},
wrapper,
...mountFunctions,
};
}
/**
* Create a content script UI using an iframe.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#iframe
*/
export function createIframeUi<TMounted>(
ctx: ContentScriptContext,
options: IframeContentScriptUiOptions<TMounted>,
): IframeContentScriptUi<TMounted> {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-wxt-iframe', '');
const iframe = document.createElement('iframe');
// @ts-expect-error: getURL is defined per-project, but not inside the package
iframe.src = browser.runtime.getURL(options.page);
wrapper.appendChild(iframe);
let mounted: TMounted | undefined = undefined;
const mount = () => {
applyPosition(wrapper, iframe, options);
mountUi(wrapper, options);
mounted = options.onMount?.(wrapper, iframe);
};
const remove = () => {
options.onRemove?.(mounted);
wrapper.remove();
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
get mounted() {
return mounted;
},
iframe,
wrapper,
...mountFunctions,
};
}
/**
* Create a content script UI inside a [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
*
* > This function is async because it has to load the CSS via a network call.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadow-root
*/
export async function createShadowRootUi<TMounted>(
ctx: ContentScriptContext,
options: ShadowRootContentScriptUiOptions<TMounted>,
): Promise<ShadowRootContentScriptUi<TMounted>> {
const css = [options.css ?? ''];
if (ctx.options?.cssInjectionMode === 'ui') {
const entryCss = await loadCss();
// Replace :root selectors with :host since we're in a shadow root
css.push(entryCss.replaceAll(':root', ':host'));
}
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
shadow,
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
},
mode: options.mode ?? 'open',
isolateEvents: options.isolateEvents,
});
shadowHost.setAttribute('data-wxt-shadow-root', '');
let mounted: TMounted | undefined;
const mount = () => {
// Add shadow root element to DOM
mountUi(shadowHost, options);
applyPosition(shadowHost, shadow.querySelector('html'), options);
// Mount UI inside shadow root
mounted = options.onMount(uiContainer, shadow, shadowHost);
};
const remove = () => {
// Cleanup mounted state
options.onRemove?.(mounted);
// Detach shadow root from DOM
shadowHost.remove();
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
// Clear mounted value
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
shadow,
shadowHost,
uiContainer,
...mountFunctions,
get mounted() {
return mounted;
},
};
}
function applyPosition(
export function applyPosition(
root: HTMLElement,
positionedElement: HTMLElement | undefined | null,
options: ContentScriptPositioningOptions,
@@ -227,7 +50,9 @@ function applyPosition(
}
}
function getAnchor(options: ContentScriptAnchoredOptions): Element | undefined {
export function getAnchor(
options: ContentScriptAnchoredOptions,
): Element | undefined {
if (options.anchor == null) return document.body;
let resolved =
@@ -254,7 +79,7 @@ function getAnchor(options: ContentScriptAnchoredOptions): Element | undefined {
return resolved ?? undefined;
}
function mountUi(
export function mountUi(
root: HTMLElement,
options: ContentScriptAnchoredOptions,
): void {
@@ -287,7 +112,7 @@ function mountUi(
}
}
function createMountFunctions<TMounted>(
export function createMountFunctions<TMounted>(
baseFunctions: BaseMountFunctions,
options: ContentScriptUiOptions<TMounted>,
): MountFunctions {
@@ -392,23 +217,3 @@ function autoMountUi(
return { stopAutoMount: _stopAutoMount };
}
/**
* Load the CSS for the current entrypoint.
*/
async function loadCss(): Promise<string> {
// @ts-expect-error: getURL is defined per-project, but not inside the package
const url = browser.runtime.getURL(
`/content-scripts/${import.meta.env.ENTRYPOINT}.css`,
);
try {
const res = await fetch(url);
return await res.text();
} catch (err) {
logger.warn(
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
err,
);
return '';
}
}
@@ -1,3 +1,7 @@
/**
* Shared types for the different `wxt/utils/content-script-ui/*` modules.
* @module wxt/utils/content-script-ui/types
*/
export interface IntegratedContentScriptUi<TMounted>
extends ContentScriptUi<TMounted> {
/**
@@ -1,3 +1,4 @@
/** @module wxt/utils/define-app-config */
export interface WxtAppConfig {}
/**
@@ -7,9 +8,9 @@ export interface WxtAppConfig {}
*
* ```ts
* // app.config.ts
* import 'wxt/sandbox';
* import 'wxt/utils/define-app-config';
*
* declare module "wxt/sandbox" {
* declare module "wxt/utils/define-app-config" {
* export interface WxtAppConfig {
* analytics: AnalyticsConfig
* }
@@ -1,3 +1,4 @@
/** @module wxt/utils/define-background */
import type { BackgroundDefinition } from '../types';
export function defineBackground(main: () => void): BackgroundDefinition;
@@ -1,3 +1,4 @@
/** @module wxt/utils/define-content-script */
import type { ContentScriptDefinition } from '../types';
export function defineContentScript(
@@ -1,3 +1,4 @@
/** @module wxt/utils/define-unlisted-script */
import type { UnlistedScriptDefinition } from '../types';
export function defineUnlistedScript(
@@ -1,3 +1,4 @@
/** @module wxt/utils/define-wxt-plugin */
import type { WxtPlugin } from '../types';
export function defineWxtPlugin(plugin: WxtPlugin): WxtPlugin {
@@ -1,3 +1,4 @@
/** @module wxt/utils/inject-script */
import { browser } from 'wxt/browser';
export type ScriptPublicPath = Extract<
@@ -1,4 +1,4 @@
import { logger } from './utils/logger';
import { logger } from './logger';
interface WebSocketMessage {
type: string;
@@ -1,4 +1,4 @@
import { ContentScriptContext } from '.';
import { ContentScriptContext } from '../content-script-context';
import { WxtLocationChangeEvent } from './custom-events';
/**
+5
View File
@@ -0,0 +1,5 @@
/**
* Re-export the [`@webext-core/match-patterns` package](https://www.npmjs.com/package/@webext-core/match-patterns).
* @module wxt/utils/match-patterns
*/
export * from '@webext-core/match-patterns';
@@ -1,7 +1,7 @@
import definition from 'virtual:user-background-entrypoint';
import { initPlugins } from 'virtual:wxt-plugins';
import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
import { logger } from '../sandbox/utils/logger';
import { getDevServerWebSocket } from '../utils/internal/dev-server-websocket';
import { logger } from '../utils/internal/logger';
import { browser } from 'wxt/browser';
import { keepServiceWorkerAlive } from './utils/keep-service-worker-alive';
import { reloadContentScript } from './utils/reload-content-scripts';
@@ -1,6 +1,6 @@
import definition from 'virtual:user-content-script-isolated-world-entrypoint';
import { logger } from '../sandbox/utils/logger';
import { ContentScriptContext } from 'wxt/client';
import { logger } from '../utils/internal/logger';
import { ContentScriptContext } from 'wxt/utils/content-script-context';
import { initPlugins } from 'virtual:wxt-plugins';
const result = (async () => {
@@ -1,5 +1,5 @@
import definition from 'virtual:user-content-script-main-world-entrypoint';
import { logger } from '../sandbox/utils/logger';
import { logger } from '../utils/internal/logger';
import { initPlugins } from 'virtual:wxt-plugins';
const result = (async () => {
+2 -2
View File
@@ -1,5 +1,5 @@
import { logger } from '../sandbox/utils/logger';
import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
import { logger } from '../utils/internal/logger';
import { getDevServerWebSocket } from '../utils/internal/dev-server-websocket';
if (import.meta.env.COMMAND === 'serve') {
try {
@@ -1,5 +1,5 @@
import definition from 'virtual:user-unlisted-script-entrypoint';
import { logger } from '../sandbox/utils/logger';
import { logger } from '../utils/internal/logger';
import { initPlugins } from 'virtual:wxt-plugins';
const result = (async () => {
@@ -1,7 +1,7 @@
import { browser } from 'wxt/browser';
import { logger } from '../../sandbox/utils/logger';
import { MatchPattern } from 'wxt/sandbox';
import type { ReloadContentScriptPayload } from '../../sandbox/dev-server-websocket';
import { logger } from '../../utils/internal/logger';
import { MatchPattern } from 'wxt/utils/match-patterns';
import type { ReloadContentScriptPayload } from '../../utils/internal/dev-server-websocket';
export function reloadContentScript(payload: ReloadContentScriptPayload) {
const manifest = browser.runtime.getManifest();
+17 -6
View File
@@ -1,11 +1,22 @@
{
"entryPoints": [
"src/client/index.ts",
"src/testing/index.ts",
"src/sandbox/index.ts",
"src/browser.ts",
"src/index.ts",
"src/modules.ts",
"src/storage.ts"
"src/utils/app-config.ts",
"src/utils/inject-script.ts",
"src/utils/content-script-context.ts",
"src/utils/content-script-ui/types.ts",
"src/utils/content-script-ui/integrated.ts",
"src/utils/content-script-ui/shadow-root.ts",
"src/utils/content-script-ui/iframe.ts",
"src/utils/define-app-config.ts",
"src/utils/define-background.ts",
"src/utils/define-content-script.ts",
"src/utils/define-unlisted-script.ts",
"src/utils/define-wxt-plugin.ts",
"src/utils/match-patterns.ts",
"src/browser.ts",
"src/testing/index.ts",
"src/storage.ts",
"src/modules.ts"
]
}