diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e971e4ff..c4fb56806 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ on: - module-vue - storage - unocss + - webextension-polyfill - wxt permissions: diff --git a/.github/workflows/sync-releases.yml b/.github/workflows/sync-releases.yml index 3d0663fa8..7e6ddca04 100644 --- a/.github/workflows/sync-releases.yml +++ b/.github/workflows/sync-releases.yml @@ -15,6 +15,7 @@ on: - module-svelte - module-vue - storage + - webextension-polyfill - wxt permissions: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c625a7b9e..a2db1d168 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -174,3 +174,13 @@ npm i https://pkg.pr.new/@wxt-dev/module-react@main # Install `@wxt-dev/storage` from a specific commit: npm i https://pkg.pr.new/@wxt-dev/module-react@426f907 ``` + +## Blog Posts + +Anyone is welcome to submit a blog post on https://wxt.dev/blog! + +> [!NOTE] +> Before starting on a blog post, please message Aaron on Discord or start a discussion on GitHub to get permission to write about a topic, but most topics are welcome: Major version updates, tutorials, etc. + +- **English only**: Blog posts should be written in English. Unfortunately, our maintainers doesn't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓 +- **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that. diff --git a/docs/.vitepress/components/BlogHome.vue b/docs/.vitepress/components/BlogHome.vue new file mode 100644 index 000000000..35aadf90e --- /dev/null +++ b/docs/.vitepress/components/BlogHome.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/docs/.vitepress/components/BlogLayout.vue b/docs/.vitepress/components/BlogLayout.vue new file mode 100644 index 000000000..3fb56be7a --- /dev/null +++ b/docs/.vitepress/components/BlogLayout.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/docs/.vitepress/components/BlogPostPreview.vue b/docs/.vitepress/components/BlogPostPreview.vue new file mode 100644 index 000000000..405822189 --- /dev/null +++ b/docs/.vitepress/components/BlogPostPreview.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/docs/.vitepress/composables/useBlogDate.ts b/docs/.vitepress/composables/useBlogDate.ts new file mode 100644 index 000000000..d746e9bdb --- /dev/null +++ b/docs/.vitepress/composables/useBlogDate.ts @@ -0,0 +1,15 @@ +import { computed, toValue, MaybeRefOrGetter } from 'vue'; + +const MONTH_FORMATTER = new Intl.DateTimeFormat( + globalThis?.navigator?.language, + { + month: 'long', + }, +); + +export default function (date: MaybeRefOrGetter) { + return computed(() => { + const d = new Date(toValue(date)); + return `${MONTH_FORMATTER.format(d)} ${d.getDate()}, ${d.getFullYear()}`; + }); +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a70a50824..aae104fa9 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -21,14 +21,19 @@ import { groupIconVitePlugin, localIconLoader, } from 'vitepress-plugin-group-icons'; +import { Feed } from 'feed'; +import { writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +const origin = 'https://wxt.dev'; const title = 'Next-gen Web Extension Framework'; const titleSuffix = ' – WXT'; const description = "WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started."; const ogTitle = `${title}${titleSuffix}`; -const ogUrl = 'https://wxt.dev'; -const ogImage = 'https://wxt.dev/social-preview.png'; +const ogUrl = origin; +const ogImage = `${origin}/social-preview.png`; const otherPackages = { analytics: analyticsVersion, @@ -69,7 +74,30 @@ export default defineConfig({ }, lastUpdated: true, sitemap: { - hostname: 'https://wxt.dev', + hostname: origin, + }, + + async buildEnd(site) { + // Only construct the RSS document for production builds + const { default: blogDataLoader } = await import('./loaders/blog.data'); + const posts = await blogDataLoader.load(); + const feed = new Feed({ + copyright: 'MIT', + id: 'wxt', + title: 'WXT Blog', + link: `${origin}/blog`, + }); + posts.forEach((post) => { + feed.addItem({ + date: post.frontmatter.date, + link: new URL(post.url, origin).href, + title: post.frontmatter.title, + description: post.frontmatter.description, + }); + }); + console.log('rss.xml:'); + console.log(feed.rss2()); + await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8'); }, head: [ @@ -126,6 +154,7 @@ export default defineConfig({ navItem('Guide', '/guide/installation'), navItem('Examples', '/examples'), navItem('API', '/api/reference/wxt'), + navItem('Blog', '/blog'), navItem(`v${wxtVersion}`, [ navItem('wxt', [ navItem(`v${wxtVersion}`, '/'), diff --git a/docs/.vitepress/loaders/blog.data.ts b/docs/.vitepress/loaders/blog.data.ts new file mode 100644 index 000000000..980fd820f --- /dev/null +++ b/docs/.vitepress/loaders/blog.data.ts @@ -0,0 +1,3 @@ +import { createContentLoader } from 'vitepress'; + +export default createContentLoader('blog/*.md'); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index b5711598c..e616c58a7 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -3,15 +3,18 @@ import Icon from '../components/Icon.vue'; import EntrypointPatterns from '../components/EntrypointPatterns.vue'; import UsingWxtSection from '../components/UsingWxtSection.vue'; import ExampleSearch from '../components/ExampleSearch.vue'; +import BlogLayout from '../components/BlogLayout.vue'; import './custom.css'; import 'virtual:group-icons.css'; export default { extends: DefaultTheme, enhanceApp(ctx) { - ctx.app.component('Icon', Icon); - ctx.app.component('EntrypointPatterns', EntrypointPatterns); - ctx.app.component('UsingWxtSection', UsingWxtSection); - ctx.app.component('ExampleSearch', ExampleSearch); + ctx.app + .component('Icon', Icon) + .component('EntrypointPatterns', EntrypointPatterns) + .component('UsingWxtSection', UsingWxtSection) + .component('ExampleSearch', ExampleSearch) + .component('blog', BlogLayout); }, }; diff --git a/docs/blog.md b/docs/blog.md new file mode 100644 index 000000000..f49c18e67 --- /dev/null +++ b/docs/blog.md @@ -0,0 +1,9 @@ +--- +layout: page +--- + + + + diff --git a/docs/blog/.drafts/2024-10-19-real-world-messaging.md b/docs/blog/.drafts/2024-10-19-real-world-messaging.md new file mode 100644 index 000000000..f161280f3 --- /dev/null +++ b/docs/blog/.drafts/2024-10-19-real-world-messaging.md @@ -0,0 +1,12 @@ +--- +layout: blog +title: Real World Messaging +description: | + The extension messaging APIs are difficult to learn. Let's go beyond the simple examples from Chrome and Firefox's documentation to build our own simple messaging system from scratch. +authors: + - name: Aaron Klinker + github: aklinker1 +date: 2024-10-20T04:54:23.601Z +--- + +Test content **bold** _italic_ diff --git a/docs/blog/2024-12-06-using-imports-module.md b/docs/blog/2024-12-06-using-imports-module.md new file mode 100644 index 000000000..733ec9238 --- /dev/null +++ b/docs/blog/2024-12-06-using-imports-module.md @@ -0,0 +1,72 @@ +--- +layout: blog +title: Introducing #imports +description: Learn how WXT's new #imports module works and how to use it. +authors: + - name: Aaron Klinker + github: aklinker1 +date: 2024-12-06T14:39:00.000Z +--- + +WXT v0.20 introduced a new way of manually importing its APIs: **the `#imports` module**. This module was introduced to simplify import statements and provide more visibility into all the APIs WXT provides. + + +```ts +import { browser } from 'wxt/browser'; // [!code --] +import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; // [!code --] +import { defineContentScript } from 'wxt/utils/define-content-script'; // [!code --] +import { injectScript } from 'wxt/utils/inject-script'; // [!code --] +import { // [!code ++] + browser, createShadowRootUi, defineContentScript, injectScript // [!code ++] +} from '#imports'; // [!code ++] +``` + +The `#imports` module is considered a "virtual module", because the file doesn't actually exist. At build-time, imports are split into individual statements for each API: + +:::code-group + +```ts [What you write] +import { defineContentScript, injectScript } from '#imports'; +``` + +```ts [What the bundler sees] +import { defineContentScript } from 'wxt/utils/define-content-script'; +import { injectScript } from 'wxt/utils/inject-script'; +``` + +::: + +Think of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size. + +This enables better tree-shaking compared to v0.19 and below. + +:::tip Need to lookup the full import path of an API? +Open up your project's `.wxt/types/imports-module.d.ts` file. +::: + +## Mocking + +When writing tests, you might need to mock APIs from the `#imports` module. While mocking these APIs is very easy, it may not be immediately clear how to accomplish it. + +Let's look at an example using Vitest. When [configured with `wxt/testing`](/guide/essentials/unit-testing#vitest), Vitest sees the same transformed code as the bundler. That means to mock an API from `#imports`, you need to call `vi.mock` with the real import path, not `#imports`: + +```ts +import { injectScript } from '#imports'; +import { vi } from 'vitest'; + +vi.mock('wxt/utils/inject-script') +const injectScriptMock = vi.mocked(injectScript); + +injectScriptMock.mockReturnValueOnce(...); +``` + +## Conclusion + +You don't have to use `#imports` if you don't like - you can continue importing APIs from their submodules. However, using `#imports` is the recommended approach moving forwards. + +- As more APIs are added, you won't have to memorize additional import paths. +- If breaking changes are made to import paths in future major versions, `#imports` won't break. + +Happy Coding 😄 + +> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport). diff --git a/docs/guide/essentials/assets.md b/docs/guide/essentials/assets.md index 98e609369..27f396f62 100644 --- a/docs/guide/essentials/assets.md +++ b/docs/guide/essentials/assets.md @@ -46,7 +46,7 @@ import image from '~/assets/image.png'; ## `/public` Directory -Files inside `/public/` are copied into the output folder as-is, without being processed by WXT's bundler. +Files inside `/public/` are copied into the output folder as-is, without being processed by WXT's bundler. Here's how you access them: @@ -81,6 +81,10 @@ img.src = imageUrl; ::: +:::warning +Assets in the `public/` directory are **_not_** accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's [`web_accessible_resources` array](/api/reference/wxt/type-aliases/UserManifest#web-accessible-resources). +::: + ## Inside Content Scripts Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname. diff --git a/docs/guide/essentials/config/auto-imports.md b/docs/guide/essentials/config/auto-imports.md index 63bcc3dc5..86b29503a 100644 --- a/docs/guide/essentials/config/auto-imports.md +++ b/docs/guide/essentials/config/auto-imports.md @@ -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: - `/components/*` - `/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 should still use `#imports` to import all of WXT's APIs from a single place. diff --git a/docs/guide/essentials/config/browser-startup.md b/docs/guide/essentials/config/browser-startup.md index aba5320d2..e5b5778d5 100644 --- a/docs/guide/essentials/config/browser-startup.md +++ b/docs/guide/essentials/config/browser-startup.md @@ -4,7 +4,7 @@ outline: deep # Browser Startup -> See the [API Reference](/api/reference/wxt/interfaces/ExtensionRunnerConfig) for a full list of config. +> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config. 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. @@ -15,9 +15,9 @@ You can configure browser startup in 3 places: 1. `/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers ```ts - import { defineRunnerConfig } from 'wxt'; + import { defineWebExtConfig } from 'wxt'; - export default defineRunnerConfig({ + export default defineWebExtConfig({ // ... }); ``` @@ -32,7 +32,7 @@ You can configure browser startup in 3 places: To set or customize the browser opened during development: ```ts -export default defineRunnerConfig({ +export default defineWebExtConfig({ binaries: { chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox @@ -54,7 +54,7 @@ To persist data, set the `--user-data-dir` flag: :::code-group ```ts [Mac/Linux] -export default defineRunnerConfig({ +export default defineWebExtConfig({ chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'], }); ``` @@ -62,7 +62,7 @@ export default defineRunnerConfig({ ```ts [Windows] import { resolve } from 'node:path'; -export default defineRunnerConfig({ +export default defineWebExtConfig({ // On Windows, the path must be absolute chromiumProfile: resolve('.wxt/chrome-data'), keepProfileChanges: true, @@ -82,7 +82,7 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c If you prefer to load the extension into your browser manually, you can disable the auto-open behavior: ```ts -export default defineRunnerConfig({ +export default defineWebExtConfig({ disabled: true, }); ``` diff --git a/docs/guide/essentials/config/entrypoint-loaders.md b/docs/guide/essentials/config/entrypoint-loaders.md index 8fecc8777..c33c870ad 100644 --- a/docs/guide/essentials/config/entrypoint-loaders.md +++ b/docs/guide/essentials/config/entrypoint-loaders.md @@ -17,62 +17,3 @@ If you're running into errors while importing entrypoints, run `wxt prepare --de ::: Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export. - -There are two options for loading your entrypoints: - -1. `vite-node` - default as of `v0.19.0` -2. `jiti` (**DEPRECATED, will be removed in `v0.20.0`**) - Default before `v0.19.0` - -## vite-node - -Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files. It re-uses the same vite config used when building your extension, making it the most stable entrypoint loader. - -## jiti - -To enable `jiti`: - -```ts -export default defineConfig({ - entrypointLoader: 'jiti', -}); -``` - -This is the original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, it does one additional pre-processing step: It removes **_ALL_** imports from your code. - -That means you cannot use imported variables outside the `main` function in JS entrypoints, like for content script `matches` or other options: - -```ts [entrypoints/content.ts] -import { GOOGLE_MATCHES } from '~/utils/match-patterns'; - -export default defineContentScript({ - matches: GOOGLE_MATCHES, - main() { - // ... - }, -}); -``` - -``` -$ wxt build -wxt build - -WXT 0.14.1 -ℹ Building chrome-mv3 for production with Vite 5.0.5 -✖ Command failed after 360 ms - -[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. -``` - -Usually, this error occurs when you try to extract options into a shared file or when running code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them: - -```ts -import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --] - -export default defineContentScript({ - matches: GOOGLE_MATCHES, // [!code --] - matches: ['*//*.google.com/*'], // [!code ++] - main() { - // ... - }, -}); -``` diff --git a/docs/guide/essentials/config/runtime.md b/docs/guide/essentials/config/runtime.md index 2bdd8c8af..47f3c6fdb 100644 --- a/docs/guide/essentials/config/runtime.md +++ b/docs/guide/essentials/config/runtime.md @@ -5,10 +5,10 @@ Define runtime configuration in a single place, `/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; diff --git a/docs/guide/essentials/content-scripts.md b/docs/guide/essentials/content-scripts.md index f007e5580..eefbd62c7 100644 --- a/docs/guide/essentials/content-scripts.md +++ b/docs/guide/essentials/content-scripts.md @@ -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/integrated/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/shadow-root/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/shadow-root/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/iframe/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/iframe/functions/createIframeUi) for the complete list of options. ## Isolated World vs Main World @@ -621,7 +621,7 @@ export default defineContentScript({ When the `ui.remove` is called, `autoMount` also stops. ::: -See the [API Reference](/api/reference/wxt/client/interfaces/ContentScriptUi.html#automount) for the complete list of options. +See the [API Reference](/api/reference/wxt/utils/content-script-ui/types/interfaces/ContentScriptUi#automount) for the complete list of options. ## Dealing with SPAs diff --git a/docs/guide/essentials/extension-apis.md b/docs/guide/essentials/extension-apis.md index 898857e09..89b58115d 100644 --- a/docs/guide/essentials/extension-apis.md +++ b/docs/guide/essentials/extension-apis.md @@ -4,61 +4,47 @@ Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc). -WXT simplifies this - always use `browser`: +WXT merges these two into a unified API accessed through the `browser` variable. ```ts +import { browser } from 'wxt/browser'; + browser.action.onClicked.addListener(() => { // ... }); ``` -Other than that, refer to Chrome and Mozilla's documentation for how to use specific APIs. Everything a normal extension can do, WXT can do as well, just via `browser` instead of `chrome`. +:::tip +With auto-imports enabled, you don't even need to import this variable from `wxt/browser`! +::: -## Webextension Polyfill +The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime: -> Since `v0.1.0` +<<< @/../packages/browser/src/index.mjs#snippet -By default, WXT uses the [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers. +This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc). -To access types, you should import the relevant namespace from `wxt/browser`: +## Accessing Types + +All types can be accessed via WXT's `Browser` namespace: ```ts -import { Runtime } from 'wxt/browser'; +import { Browser } from 'wxt/browser'; -function handleMessage(message: any, sender: Runtime.Sender) { +function handleMessage(message: any, sender: Browser.runtime.MessageSender) { // ... } ``` -### Disabling the polyfill - -> Since `v0.19.0` +## Using `webextension-polyfill` -After the release of MV3 and Chrome's official deprecation of MV2 in June 2024, the polyfill isn't really doing anything useful anymore. - -You can disable it with a single line: - -```ts [wxt.config.ts] -export default defineConfig({ - extensionApi: 'chrome', -}); -``` +If you want to use the `webextension-polyfill` when importing `browser`, you can do so by installing the `@wxt-dev/webextension-polyfill` package. -This will change `wxt/browser` to simply export the `browser` or `chrome` globals based on browser at runtime: - -<<< @/../packages/browser/src/index.mjs#snippet - -Accessing types is a little different with the polyfill disabled. They do not need to be imported; they're available on the `browser` object itself: - -```ts -function handleMessage(message: any, sender: browser.runtime.Sender) { - // ... -} -``` +See it's [Installation Guide](https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md) to get started. ## Feature Detection -Depending on the manifest version and browser, some APIs are not available at runtime. If an API is not available, it will be `undefined`. +Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`. :::warning Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not. diff --git a/docs/guide/essentials/project-structure.md b/docs/guide/essentials/project-structure.md index d56341066..75fa077d7 100644 --- a/docs/guide/essentials/project-structure.md +++ b/docs/guide/essentials/project-structure.md @@ -60,14 +60,14 @@ After enabling it, your project structure should look like this: 📂 {rootDir}/ 📁 .output/ 📁 .wxt/ + 📁 modules/ + 📁 public/ 📂 src/ 📁 assets/ 📁 components/ 📁 composables/ 📁 entrypoints/ 📁 hooks/ - 📁 modules/ - 📁 public/ 📁 utils/ 📄 app.config.ts 📄 .env @@ -87,12 +87,12 @@ You can configure the following directories: export default defineConfig({ // Relative to project root srcDir: "src", // default: "." + modulesDir: "wxt-modules", // default: "modules" outDir: "dist", // default: ".output" + publicDir: "static", // default: "public" // Relative to srcDir entrypointsDir: "entries", // default: "entrypoints" - modulesDir: "wxt-modules", // default: "modules" - publicDir: "static", // default: "public" }) ``` diff --git a/docs/guide/essentials/storage.md b/docs/guide/essentials/storage.md index 5213e1158..a39662f06 100644 --- a/docs/guide/essentials/storage.md +++ b/docs/guide/essentials/storage.md @@ -6,7 +6,7 @@ You can use the vanilla APIs (see docs above), use [WXT's built-in storage API]( ## Alternatives -1. [`wxt/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases +1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases 2. DIY: If you're migrating to WXT and already have a storage wrapper, keep using it. In the future, if you want to delete that code, you can use one of these alternatives, but there's no reason to replace working code during a migration. diff --git a/docs/guide/essentials/unit-testing.md b/docs/guide/essentials/unit-testing.md index 43f38b5a4..8209ae405 100644 --- a/docs/guide/essentials/unit-testing.md +++ b/docs/guide/essentials/unit-testing.md @@ -32,7 +32,7 @@ Here are real projects with unit testing setup. Look at the code and tests to se ### Example Tests -This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension! +This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/utils/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension! ```ts import { describe, it, expect } from 'vitest'; @@ -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. diff --git a/docs/guide/essentials/wxt-modules.md b/docs/guide/essentials/wxt-modules.md index cb60cfcce..d359d9694 100644 --- a/docs/guide/essentials/wxt-modules.md +++ b/docs/guide/essentials/wxt-modules.md @@ -17,7 +17,7 @@ There are two ways to add a module to your project: > Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules. 2. **Local**: add a file to your project's `modules/` directory: ``` - / + / modules/ my-module.ts ``` @@ -111,12 +111,12 @@ export default defineWxtModule({ ```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; } diff --git a/docs/guide/resources/upgrading.md b/docs/guide/resources/upgrading.md index d4fccf469..225c37897 100644 --- a/docs/guide/resources/upgrading.md +++ b/docs/guide/resources/upgrading.md @@ -1,3 +1,7 @@ +--- +outline: deep +--- + # Upgrading WXT ## Overview @@ -12,6 +16,243 @@ Listed below are all the breaking changes you should address when upgrading to a Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes. +## v0.19.0 → v0.20.0 + +v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes. + +:::tip +Read through all the changes once before making any code changes. +::: + +### `webextension-polyfill` Removed + +WXT's `browser` no longer uses the `webextension-polyfill`! + +:::details Why? +See https://github.com/wxt-dev/wxt/issues/784 +::: + +To upgrade, you have two options: + +1. **Stop using the polyfill** - No changes necessary, though you may want to do some manual testing to make sure everything continues to work. None of the early testers of this feature reported any runtime issues once they stopped using the polyfill. + - If you're already using `extensionApi: "chrome"`, then you don't need to test anything! You're already using the same `browser` object v0.20 provides by default. +2. **Continue using the polyfill** - If you want to keep using the polyfill, you can! One less thing to worry about during this upgrade. + - Install `webextension-polyfill` and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill): + ```sh + pnpm i webextension-polyfill @wxt-dev/webextension-polyfill + ``` + - Add the WXT module to your config: + ```ts [wxt.config.ts] + export default defineConfig({ + modules: ['@wxt-dev/webextension-polyfill'], + }); + ``` + +The new `browser` object (and types) is backed by WXT's new package: [`@wxt-dev/browser`](https://www.npmjs.com/package/@wxt-dev/browser). This package continues WXT's mission of providing useful packages for the whole community. Just like [`@wxt-dev/storage`](https://www.npmjs.com/package/@wxt-dev/storage), [`@wxt-dev/i18n`](https://www.npmjs.com/package/@wxt-dev/i18n), [`@wxt-dev/analytics`](https://www.npmjs.com/package/@wxt-dev/analytics), it is designed to be easy to use in any web extension project, not just those using WXT, and provides a consistent API across all browsers and manifest versions. + +### `extensionApi` Config Removed + +The `extensionApi` config has been removed. Before, this config provided a way to opt into using the new `browser` object prior to v0.20.0. + +Remove it from your `wxt.config.ts` file if present: + +```ts [wxt.config.ts] +export default defineConfig({ + extensionApi: 'chrome', // [!code --] +}); +``` + +### Extension API Type Changes + +With the new `browser` introduced in v0.20, how you access types has changed. WXT now provides types based on `@types/chrome` instead of `@types/webextension-polyfill`. + +These types are more up-to-date with MV3 APIs, contain less bugs, are better organized, and don't have any auto-generated names. + +To access types, use the new `Browser` namespace from `wxt/browser`: + + +```ts +import type { Runtime } from 'wxt/browser'; // [!code --] +import type { Browser } from 'wxt/browser'; // [!code ++] + +function getMessageSenderUrl(sender: Runtime.MessageSender): string { // [!code --] +function getMessageSenderUrl(sender: Browser.runtime.MessageSender): string { // [!code ++] + // ... +} +``` + +> If you use auto-imports, `Browser` will be available without manually importing it. + +Not all type names will be the same as what `@types/webextension-polyfill` provides. You'll have to find the new type names by looking at the types of the `browser.*` API's you use. + +### `public/` and `modules/` Directories Moved + +The default location for the `public/` and `modules/` directories have changed to better align with standards set by other frameworks (Nuxt, Next, Astro, etc). Now, each path is relative to the project's **root directory**, not the src directory. + +- If you follow the default folder structure, you don't need to make any changes. +- If you set a custom `srcDir`, you have two options: + 1. Move the your `public/` and `modules/` directories to the project root: + ```diff + / + + modules/ + + public/ + src/ + components/ + entrypoints/ + - modules/ + - public/ + utils/ + wxt.config.ts + ``` + 2. Keep the folders in the same place and update your project config: + ```ts [wxt.config.ts] + export default defineConfig({ + srcDir: 'src', + publicDir: 'src/public', // [!code ++] + modulesDir: 'src/modules', // [!code ++] + }); + ``` + +### Import Path Changes and `#imports` + +The APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to individual exports under the `wxt/utils/*` path. + +:::details Why? +As WXT grows and more utilities are added, any helper with side-effects will not be tree-shaken out of your final bundle. + +This can cause problems because not every API used by these side-effects is available in every type of entrypoint. Some APIs can only be used in the background, sandboxed pages can't use any extension API, etc. This was leading to JS throwing errors in the top-level scope, preventing your code from running. + +Splitting each util into it's own module solves this problem, making sure you're only importing APIs and side-effects into entrypoints they can run in. +::: + +Refer to the updated [API Reference](/api/reference/) to see the list of new import paths. + +However, you don't need to memorize or learn the new import paths! v0.20 introduces a new virtual module, `#imports`, that abstracts all this away from developers. See the [blog post](/blog/2024-12-06-using-imports-module) for more details about how this module works. + +So to upgrade, just replace any imports from `wxt/storage`, `wxt/client`, and `wxt/sandbox` with an import to the new `#imports` module: + +```ts +import { storage } from 'wxt/storage'; // [!code --] +import { defineContentScript } from 'wxt/sandbox'; // [!code --] +import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --] +import { storage } from '#imports'; // [!code ++] +import { defineContentScript } from '#imports'; // [!code ++] +import { ContentScriptContext, useAppConfig } from '#imports'; // [!code ++] +``` + +You can combine the imports into a single import statement, but it's easier to just find/replace each statement. + +```ts +import { storage } from 'wxt/storage'; // [!code --] +import { defineContentScript } from 'wxt/sandbox'; // [!code --] +import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --] +import { + // [!code ++] + storage, // [!code ++] + defineContentScript, // [!code ++] + ContentScriptContext, // [!code ++] + useAppConfig, // [!code ++] +} from '#imports'; // [!code ++] +``` + +:::tip +Before types will work, you'll need to run `wxt prepare` after installing v0.20 to generate the new TypeScript declarations. +::: + +### `createShadowRootUi` CSS Changes + +WXT now resets styles inherited from the webpage (`visibility`, `color`, `font-size`, etc.) by setting `all: initial` inside the shadow root. + +:::warning +This doesn't effect `rem` units. You should continue using `postcss-rem-to-px` or an equivalent library if the webpage sets the HTML element's `font-size`. +::: + +If you use `createShadowRootUi`: + +1. Remove any manual CSS overrides that reset the style of specific websites. For example: + + + ```css [entrypoints/reddit.content/style.css] + body { /* [!code --] */ + /* Override Reddit's default "hidden" visibility on elements */ /* [!code --] */ + visibility: visible !important; /* [!code --] */ + } /* [!code --] */ + ``` + +2. Double check that your UI looks the same as before. + +If you run into problems with the new behavior, you can disable it and continue using your current CSS: + +```ts +const ui = await createShadowRootUi({ + inheritStyles: true, // [!code ++] + // ... +}); +``` + +### Default Output Directories Changed + +The default value for the [`outDirTemplate`](/api/reference/wxt/interfaces/InlineConfig#outdirtemplate) config has changed. Now, different build modes are output to different directories: + +- `--mode production` → `.output/chrome-mv3`: Production builds are unchanged +- `--mode development` → `.output/chrome-mv3-dev`: Dev mode now has a `-dev` suffix so it doesn't overwrite production builds +- `--mode custom` → `.output/chrome-mv3-custom`: Other custom modes end with a `-[mode]` suffix + +To use the old behavior, writing all output to the same directory, set the `outDirTemplate` option: + +```ts [wxt.config.ts] +export default defineConfig({ + outDirTemplate: '{{browser}}-mv{{manifestVersion}}', // [!code ++] +}); +``` + +:::warning +If you've previously loaded the extension into your browser manually for development, you'll need to uninstall and re-install it from the new dev output directory. +::: + +### Deprecated APIs Removed + +- `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process. + > This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps. +- `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead: + + ```ts [wxt.config.ts] + export default defineConfig({ + transformManifest(manifest) { // [!code --] + hooks: { // [!code ++] + 'build:manifestGenerated': (_, manifest) => { // [!code ++] + // ... + }, // [!code ++] + }, + }); + ``` + +### New Deprecations + +#### `runner` APIs Renamed + +To improve consistency with the `web-ext.config.ts` filename, the "runner" API and config options have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version: + +1. The `runner` option has been renamed to `webExt`: + ```ts [wxt.config.ts] + export default defineConfig({ + runner: { // [!code --] + webExt: { // [!code ++] + startUrls: ["https://wxt.dev"], + }, + }); + ``` +2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`: + ```ts [web-ext.config.ts] + import { defineRunnerConfig } from 'wxt'; // [!code --] + import { defineWebExtConfig } from 'wxt'; // [!code ++] + ``` +3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig` + ```ts + import type { ExtensionRunnerConfig } from 'wxt'; // [!code --] + import type { WebExtConfig } from 'wxt'; // [!code ++] + ``` + ## v0.18.5 → v0.19.0 ### `vite-node` Entrypoint Loader @@ -19,7 +260,7 @@ Currently, WXT is in pre-release. This means changes to the second digit, `v0.X` The default entrypoint loader has changed to `vite-node`. If you use any NPM packages that depend on the `webextension-polyfill`, you need to add them to Vite's `ssr.noExternal` option: -```ts +```ts [wxt.config.ts] export default defineConfig({ vite: () => ({ // [!code ++] ssr: { // [!code ++] @@ -67,7 +308,7 @@ Basically, you can now import and do things outside the `main` function of the e To continue using the old approach, add the following to your `wxt.config.ts` file: -```ts +```ts [wxt.config.ts] export default defineConfig({ entrypointLoader: 'jiti', // [!code ++] }); diff --git a/docs/storage.md b/docs/storage.md index f427990e5..02f9cc397 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -15,7 +15,7 @@ A simplified wrapper around the extension storage APIs. This module is built-in to WXT, so you don't need to install anything. ```ts -import { storage } from 'wxt/storage'; +import { storage } from '#imports'; ``` If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it! @@ -37,7 +37,7 @@ import { storage } from '@wxt-dev/storage'; ## Storage Permission -To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest: +To use the `@wxt-dev/storage` API, the `"storage"` permission must be added to the manifest: ```ts [wxt.config.ts] export default defineConfig({ @@ -74,7 +74,7 @@ await storage.watch( await storage.getMeta<{ v: number }>('local:installDate'); ``` -For a full list of methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage). +For a full list of methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage). ## Watchers @@ -97,7 +97,7 @@ unwatch(); ## Metadata -`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc. +`@wxt-dev/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc. [Other than versioning](#versioning), you are responsible for managing a field's metadata: @@ -157,7 +157,7 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => { }); ``` -For a full list of properties and methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorageItem). +For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem). ### Versioning @@ -353,4 +353,4 @@ await storage.setItems([ ]); ``` -Refer to the [API Reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs. +Refer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs. diff --git a/package.json b/package.json index 58e928816..ab221e040 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "changelogen": "catalog:", "consola": "catalog:", "fast-glob": "catalog:", + "feed": "catalog:", "fs-extra": "catalog:", "lint-staged": "catalog:", "markdown-it-footnote": "catalog:", diff --git a/packages/analytics/app.config.ts b/packages/analytics/app.config.ts index 757be2844..160fd015f 100644 --- a/packages/analytics/app.config.ts +++ b/packages/analytics/app.config.ts @@ -1,4 +1,4 @@ -import { defineAppConfig } from 'wxt/sandbox'; +import { defineAppConfig } from 'wxt/utils/define-app-config'; import { googleAnalytics4 } from './modules/analytics/providers/google-analytics-4'; import { umami } from './modules/analytics/providers/umami'; diff --git a/packages/analytics/modules/analytics/index.ts b/packages/analytics/modules/analytics/index.ts index a6f76562e..317ee5a75 100644 --- a/packages/analytics/modules/analytics/index.ts +++ b/packages/analytics/modules/analytics/index.ts @@ -1,5 +1,5 @@ import 'wxt'; -import 'wxt/sandbox'; +import 'wxt/utils/define-app-config'; import { addAlias, addViteConfig, @@ -9,7 +9,7 @@ import { import { relative, resolve } from 'node:path'; import type { AnalyticsConfig } from './types'; -declare module 'wxt/sandbox' { +declare module 'wxt/utils/define-app-config' { export interface WxtAppConfig { analytics: AnalyticsConfig; } @@ -44,7 +44,7 @@ export default defineWxtModule({ ? clientModuleId : relative(wxtAnalyticsFolder, clientModuleId) }';`, - `import { useAppConfig } from 'wxt/client';`, + `import { useAppConfig } from '#imports';`, ``, `export const analytics = createAnalytics(useAppConfig().analytics);`, ``, diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 9c5ed5f82..699611fc2 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -48,7 +48,7 @@ "prepare": "buildc --deps-only -- wxt prepare" }, "peerDependencies": { - "wxt": ">=0.19.23" + "wxt": ">=0.20.0" }, "devDependencies": { "@aklinker1/check": "catalog:", diff --git a/packages/i18n/README.md b/packages/i18n/README.md index 9b452dc31..b5c35742c 100644 --- a/packages/i18n/README.md +++ b/packages/i18n/README.md @@ -49,7 +49,7 @@ However, it does have one major downside: helloWorld: Hello world! ``` - > `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `/public/_locale//messages.json`, you don't need to convert them to YAML or refactor them - just move them to `/locales/.json` and they'll just work out of the box! + > `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `/public/_locale//messages.json`, you don't need to convert them to YAML or refactor them - just move them to `/locales/.json` and they'll just work out of the box! 4. To get a translation, use the auto-imported `i18n` object or import it manually: diff --git a/packages/module-react/entrypoints/content/index.tsx b/packages/module-react/entrypoints/content/index.tsx index 34f4ae199..87ac4e917 100644 --- a/packages/module-react/entrypoints/content/index.tsx +++ b/packages/module-react/entrypoints/content/index.tsx @@ -1,5 +1,8 @@ -import { defineContentScript } from 'wxt/sandbox'; -import { ContentScriptContext, createShadowRootUi } from 'wxt/client'; +import { + defineContentScript, + ContentScriptContext, + createShadowRootUi, +} from '#imports'; import React from 'react'; import ReactDOM from 'react-dom/client'; diff --git a/packages/module-react/modules/react.ts b/packages/module-react/modules/react.ts index 09ceaa941..0d2b5951c 100644 --- a/packages/module-react/modules/react.ts +++ b/packages/module-react/modules/react.ts @@ -16,7 +16,8 @@ export default defineWxtModule({ // Enable auto-imports for JSX files wxt.hook('config:resolved', (wxt) => { - if (wxt.config.imports === false) return; + // In older versions of WXT, `wxt.config.imports` could be false + if (!wxt.config.imports) return; wxt.config.imports.dirsScanOptions ??= {}; wxt.config.imports.dirsScanOptions.filePatterns = [ diff --git a/packages/module-solid/entrypoints/content/index.tsx b/packages/module-solid/entrypoints/content/index.tsx index 7c2f0e7cb..ef12a9496 100644 --- a/packages/module-solid/entrypoints/content/index.tsx +++ b/packages/module-solid/entrypoints/content/index.tsx @@ -1,5 +1,8 @@ -import { defineContentScript } from 'wxt/sandbox'; -import { ContentScriptContext, createShadowRootUi } from 'wxt/client'; +import { + defineContentScript, + ContentScriptContext, + createShadowRootUi, +} from '#imports'; import { render } from 'solid-js/web'; export default defineContentScript({ diff --git a/packages/module-solid/modules/solid.ts b/packages/module-solid/modules/solid.ts index 8b9d53bd1..1940d8e7a 100644 --- a/packages/module-solid/modules/solid.ts +++ b/packages/module-solid/modules/solid.ts @@ -19,7 +19,8 @@ export default defineWxtModule({ // Enable auto-imports for JSX files wxt.hook('config:resolved', (wxt) => { - if (wxt.config.imports === false) return; + // In older versions of WXT, `wxt.config.imports` could be false + if (!wxt.config.imports) return; wxt.config.imports.dirsScanOptions ??= {}; wxt.config.imports.dirsScanOptions.filePatterns = [ diff --git a/packages/webextension-polyfill/README.md b/packages/webextension-polyfill/README.md new file mode 100644 index 000000000..8dffa7725 --- /dev/null +++ b/packages/webextension-polyfill/README.md @@ -0,0 +1,18 @@ +# `@wxt-dev/webextension-polyfill` + +Configures `wxt/browser` to import `browser` from [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) instead of using the regular `chrome`/`browser` globals WXT normally provides. + +## Usage + +```sh +pnpm i @wxt-dev/webextension-polyfill webextension-polyfill +``` + +Then add the module to your config: + +```ts +// wxt.config.ts +export default defineConfig({ + modules: ['@wxt-dev/webextension-polyfill'], +}); +``` diff --git a/packages/webextension-polyfill/build.config.ts b/packages/webextension-polyfill/build.config.ts new file mode 100644 index 000000000..64a6c1bfb --- /dev/null +++ b/packages/webextension-polyfill/build.config.ts @@ -0,0 +1,15 @@ +import { defineBuildConfig } from 'unbuild'; +import { resolve } from 'node:path'; + +export default defineBuildConfig({ + rootDir: resolve(__dirname, 'modules/webextension-polyfill'), + outDir: resolve(__dirname, 'dist'), + entries: [ + { input: 'index.ts', name: 'index' }, + { input: 'browser.ts', name: 'browser' }, + ], + replace: { + 'process.env.NPM': 'true', + }, + declaration: true, +}); diff --git a/packages/webextension-polyfill/entrypoints/content/index.ts b/packages/webextension-polyfill/entrypoints/content/index.ts new file mode 100644 index 000000000..87ad9f394 --- /dev/null +++ b/packages/webextension-polyfill/entrypoints/content/index.ts @@ -0,0 +1,6 @@ +export default defineContentScript({ + matches: ['*://*/*'], + async main() { + console.log(browser.runtime.id); + }, +}); diff --git a/packages/webextension-polyfill/entrypoints/popup/index.html b/packages/webextension-polyfill/entrypoints/popup/index.html new file mode 100644 index 000000000..a6d6644fb --- /dev/null +++ b/packages/webextension-polyfill/entrypoints/popup/index.html @@ -0,0 +1,12 @@ + + + + + + Document + + +
+ + + diff --git a/packages/webextension-polyfill/entrypoints/popup/main.ts b/packages/webextension-polyfill/entrypoints/popup/main.ts new file mode 100644 index 000000000..3677c7eda --- /dev/null +++ b/packages/webextension-polyfill/entrypoints/popup/main.ts @@ -0,0 +1,3 @@ +const root = document.getElementById('app')!; + +root.textContent = browser.runtime.id; diff --git a/packages/webextension-polyfill/modules/webextension-polyfill/browser.ts b/packages/webextension-polyfill/modules/webextension-polyfill/browser.ts new file mode 100644 index 000000000..c86e9ce3c --- /dev/null +++ b/packages/webextension-polyfill/modules/webextension-polyfill/browser.ts @@ -0,0 +1 @@ +export { default as browser } from 'webextension-polyfill'; diff --git a/packages/webextension-polyfill/modules/webextension-polyfill/index.ts b/packages/webextension-polyfill/modules/webextension-polyfill/index.ts new file mode 100644 index 000000000..6114cad12 --- /dev/null +++ b/packages/webextension-polyfill/modules/webextension-polyfill/index.ts @@ -0,0 +1,18 @@ +import 'wxt'; +import { addViteConfig, defineWxtModule } from 'wxt/modules'; +import { resolve } from 'node:path'; + +export default defineWxtModule({ + name: '@wxt-dev/webextension-polyfill', + setup(wxt) { + addViteConfig(wxt, () => ({ + resolve: { + alias: { + 'wxt/browser': process.env.NPM + ? '@wxt-dev/webextension-polyfill/browser' + : resolve(__dirname, 'browser.ts'), + }, + }, + })); + }, +}); diff --git a/packages/webextension-polyfill/package.json b/packages/webextension-polyfill/package.json new file mode 100644 index 000000000..fb7c8d5af --- /dev/null +++ b/packages/webextension-polyfill/package.json @@ -0,0 +1,56 @@ +{ + "name": "@wxt-dev/webextension-polyfill", + "description": "Use webextension-polyfill with WXT", + "repository": { + "type": "git", + "url": "git+https://github.com/wxt-dev/wxt.git", + "directory": "packages/webextension-polyfill" + }, + "homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md", + "keywords": [ + "wxt", + "module", + "webextension-polyfill" + ], + "author": { + "name": "Aaron Klinker", + "email": "aaronklinker1+wxt@gmail.com" + }, + "license": "MIT", + "version": "1.0.0", + "type": "module", + "main": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "./browser": { + "types": "./dist/browser.d.mts", + "default": "./dist/browser.mjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "dev": "wxt", + "check": "pnpm build && check", + "build": "buildc -- unbuild", + "prepare": "buildc --deps-only -- wxt prepare" + }, + "peerDependencies": { + "webextension-polyfill": "*", + "wxt": ">=0.20.0" + }, + "devDependencies": { + "@aklinker1/check": "catalog:", + "@types/webextension-polyfill": "catalog:", + "publint": "catalog:", + "typescript": "catalog:", + "unbuild": "catalog:", + "webextension-polyfill": "catalog:", + "wxt": "workspace:*" + } +} diff --git a/packages/webextension-polyfill/public/.keep b/packages/webextension-polyfill/public/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/packages/webextension-polyfill/tsconfig.json b/packages/webextension-polyfill/tsconfig.json new file mode 100644 index 000000000..cde6e0a85 --- /dev/null +++ b/packages/webextension-polyfill/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"], + "exclude": ["node_modules/**", "dist/**"] +} diff --git a/packages/wxt-demo/src/modules/auto-icons.ts b/packages/wxt-demo/modules/auto-icons.ts similarity index 100% rename from packages/wxt-demo/src/modules/auto-icons.ts rename to packages/wxt-demo/modules/auto-icons.ts diff --git a/packages/wxt-demo/src/modules/example.ts b/packages/wxt-demo/modules/example.ts similarity index 100% rename from packages/wxt-demo/src/modules/example.ts rename to packages/wxt-demo/modules/example.ts diff --git a/packages/wxt-demo/src/modules/i18n.ts b/packages/wxt-demo/modules/i18n.ts similarity index 100% rename from packages/wxt-demo/src/modules/i18n.ts rename to packages/wxt-demo/modules/i18n.ts diff --git a/packages/wxt-demo/src/modules/unocss.ts b/packages/wxt-demo/modules/unocss.ts similarity index 100% rename from packages/wxt-demo/src/modules/unocss.ts rename to packages/wxt-demo/modules/unocss.ts diff --git a/packages/wxt-demo/package.json b/packages/wxt-demo/package.json index 773c148d1..cad91efdd 100644 --- a/packages/wxt-demo/package.json +++ b/packages/wxt-demo/package.json @@ -22,7 +22,6 @@ "react-dom": "catalog:" }, "devDependencies": { - "@types/chrome": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@wxt-dev/auto-icons": "workspace:*", diff --git a/packages/wxt-demo/src/app.config.ts b/packages/wxt-demo/src/app.config.ts index b2b1f8b0c..5240edd9b 100644 --- a/packages/wxt-demo/src/app.config.ts +++ b/packages/wxt-demo/src/app.config.ts @@ -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; } diff --git a/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts b/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts index 62f79bed1..c46f84d2b 100644 --- a/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts +++ b/packages/wxt-demo/src/entrypoints/__tests__/background.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import background from '../background'; -chrome.i18n.getMessage = () => 'fake-message'; +browser.i18n.getMessage = () => 'fake-message'; const logMock = vi.fn(); console.log = logMock; @@ -11,7 +11,7 @@ describe('Background Entrypoint', () => { fakeBrowser.reset(); }); - it("should log the extenion's runtime ID", () => { + it("should log the extension's runtime ID", () => { const id = 'some-id'; fakeBrowser.runtime.id = id; diff --git a/packages/wxt-demo/wxt.config.ts b/packages/wxt-demo/wxt.config.ts index 6961eea29..eee9f5d39 100644 --- a/packages/wxt-demo/wxt.config.ts +++ b/packages/wxt-demo/wxt.config.ts @@ -3,7 +3,6 @@ import { presetUno } from 'unocss'; export default defineConfig({ srcDir: 'src', - extensionApi: 'chrome', manifest: { permissions: ['storage'], default_locale: 'en', diff --git a/packages/wxt/build.config.ts b/packages/wxt/build.config.ts index 48fa5ccc6..bd2300a7c 100644 --- a/packages/wxt/build.config.ts +++ b/packages/wxt/build.config.ts @@ -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)), // ./utils/storage => wxt/utils/storage ], })), ]); diff --git a/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap b/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap index 0000ea9c2..86d57de2c 100644 --- a/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap +++ b/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap @@ -5,17 +5,44 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file ---------------------------------------- { "globals": { + "AutoMount": true, + "AutoMountOptions": true, + "Browser": true, + "ContentScriptAnchoredOptions": true, + "ContentScriptAppendMode": true, "ContentScriptContext": true, + "ContentScriptInlinePositioningOptions": true, + "ContentScriptModalPositioningOptions": true, + "ContentScriptOverlayAlignment": true, + "ContentScriptOverlayPositioningOptions": true, + "ContentScriptPositioningOptions": true, + "ContentScriptUi": true, + "ContentScriptUiOptions": true, + "IframeContentScriptUi": true, + "IframeContentScriptUiOptions": true, + "InjectScriptOptions": true, + "IntegratedContentScriptUi": true, + "IntegratedContentScriptUiOptions": true, "InvalidMatchPattern": true, "MatchPattern": true, "MigrationError": true, + "ScriptPublicPath": true, + "ShadowRootContentScriptUi": true, + "ShadowRootContentScriptUiOptions": true, + "StopAutoMount": true, + "StorageArea": true, + "StorageAreaChanges": true, + "StorageItemKey": true, + "WxtAppConfig": true, + "WxtStorage": true, + "WxtStorageItem": true, + "WxtWindowEventMap": true, "browser": true, "createIframeUi": true, "createIntegratedUi": true, "createShadowRootUi": true, "defineAppConfig": true, "defineBackground": true, - "defineConfig": true, "defineContentScript": true, "defineUnlistedScript": true, "defineWxtPlugin": true, @@ -32,17 +59,44 @@ exports[`Auto Imports > eslintrc > "enabled: 9" should output a flat config file ".wxt/eslint-auto-imports.mjs ---------------------------------------- const globals = { + "AutoMount": true, + "AutoMountOptions": true, + "Browser": true, + "ContentScriptAnchoredOptions": true, + "ContentScriptAppendMode": true, "ContentScriptContext": true, + "ContentScriptInlinePositioningOptions": true, + "ContentScriptModalPositioningOptions": true, + "ContentScriptOverlayAlignment": true, + "ContentScriptOverlayPositioningOptions": true, + "ContentScriptPositioningOptions": true, + "ContentScriptUi": true, + "ContentScriptUiOptions": true, + "IframeContentScriptUi": true, + "IframeContentScriptUiOptions": true, + "InjectScriptOptions": true, + "IntegratedContentScriptUi": true, + "IntegratedContentScriptUiOptions": true, "InvalidMatchPattern": true, "MatchPattern": true, "MigrationError": true, + "ScriptPublicPath": true, + "ShadowRootContentScriptUi": true, + "ShadowRootContentScriptUiOptions": true, + "StopAutoMount": true, + "StorageArea": true, + "StorageAreaChanges": true, + "StorageItemKey": true, + "WxtAppConfig": true, + "WxtStorage": true, + "WxtStorageItem": true, + "WxtWindowEventMap": true, "browser": true, "createIframeUi": true, "createIntegratedUi": true, "createShadowRootUi": true, "defineAppConfig": true, "defineBackground": true, - "defineConfig": true, "defineContentScript": true, "defineUnlistedScript": true, "defineWxtPlugin": true, @@ -67,17 +121,44 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f ---------------------------------------- { "globals": { + "AutoMount": true, + "AutoMountOptions": true, + "Browser": true, + "ContentScriptAnchoredOptions": true, + "ContentScriptAppendMode": true, "ContentScriptContext": true, + "ContentScriptInlinePositioningOptions": true, + "ContentScriptModalPositioningOptions": true, + "ContentScriptOverlayAlignment": true, + "ContentScriptOverlayPositioningOptions": true, + "ContentScriptPositioningOptions": true, + "ContentScriptUi": true, + "ContentScriptUiOptions": true, + "IframeContentScriptUi": true, + "IframeContentScriptUiOptions": true, + "InjectScriptOptions": true, + "IntegratedContentScriptUi": true, + "IntegratedContentScriptUiOptions": true, "InvalidMatchPattern": true, "MatchPattern": true, "MigrationError": true, + "ScriptPublicPath": true, + "ShadowRootContentScriptUi": true, + "ShadowRootContentScriptUiOptions": true, + "StopAutoMount": true, + "StorageArea": true, + "StorageAreaChanges": true, + "StorageItemKey": true, + "WxtAppConfig": true, + "WxtStorage": true, + "WxtStorageItem": true, + "WxtWindowEventMap": true, "browser": true, "createIframeUi": true, "createIntegratedUi": true, "createShadowRootUi": true, "defineAppConfig": true, "defineBackground": true, - "defineConfig": true, "defineContentScript": true, "defineUnlistedScript": true, "defineWxtPlugin": true, @@ -95,17 +176,44 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = ` ---------------------------------------- { "globals": { + "AutoMount": "readonly", + "AutoMountOptions": "readonly", + "Browser": "readonly", + "ContentScriptAnchoredOptions": "readonly", + "ContentScriptAppendMode": "readonly", "ContentScriptContext": "readonly", + "ContentScriptInlinePositioningOptions": "readonly", + "ContentScriptModalPositioningOptions": "readonly", + "ContentScriptOverlayAlignment": "readonly", + "ContentScriptOverlayPositioningOptions": "readonly", + "ContentScriptPositioningOptions": "readonly", + "ContentScriptUi": "readonly", + "ContentScriptUiOptions": "readonly", + "IframeContentScriptUi": "readonly", + "IframeContentScriptUiOptions": "readonly", + "InjectScriptOptions": "readonly", + "IntegratedContentScriptUi": "readonly", + "IntegratedContentScriptUiOptions": "readonly", "InvalidMatchPattern": "readonly", "MatchPattern": "readonly", "MigrationError": "readonly", + "ScriptPublicPath": "readonly", + "ShadowRootContentScriptUi": "readonly", + "ShadowRootContentScriptUiOptions": "readonly", + "StopAutoMount": "readonly", + "StorageArea": "readonly", + "StorageAreaChanges": "readonly", + "StorageItemKey": "readonly", + "WxtAppConfig": "readonly", + "WxtStorage": "readonly", + "WxtStorageItem": "readonly", + "WxtWindowEventMap": "readonly", "browser": "readonly", "createIframeUi": "readonly", "createIntegratedUi": "readonly", "createShadowRootUi": "readonly", "defineAppConfig": "readonly", "defineBackground": "readonly", - "defineConfig": "readonly", "defineContentScript": "readonly", "defineUnlistedScript": "readonly", "defineWxtPlugin": "readonly", diff --git a/packages/wxt/e2e/tests/auto-imports.test.ts b/packages/wxt/e2e/tests/auto-imports.test.ts index 144b972e8..8746656ff 100644 --- a/packages/wxt/e2e/tests/auto-imports.test.ts +++ b/packages/wxt/e2e/tests/auto-imports.test.ts @@ -17,24 +17,52 @@ 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 MigrationError: typeof import('wxt/storage')['MigrationError'] + 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 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 storage: typeof import('wxt/storage')['storage'] - const useAppConfig: typeof import('wxt/client')['useAppConfig'] + const injectScript: typeof import('wxt/utils/inject-script')['injectScript'] + const storage: typeof import('wxt/utils/storage')['storage'] + const useAppConfig: typeof import('wxt/utils/app-config')['useAppConfig'] + } + // for type re-export + declare global { + // @ts-ignore + export type { Browser } from 'wxt/browser' + import('wxt/browser') + // @ts-ignore + export type { StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage' + import('wxt/utils/storage') + // @ts-ignore + export type { WxtWindowEventMap } from 'wxt/utils/content-script-context' + import('wxt/utils/content-script-context') + // @ts-ignore + export type { IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe' + import('wxt/utils/content-script-ui/iframe') + // @ts-ignore + export type { IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated' + import('wxt/utils/content-script-ui/integrated') + // @ts-ignore + export type { ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root' + import('wxt/utils/content-script-ui/shadow-root') + // @ts-ignore + export type { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types' + import('wxt/utils/content-script-ui/types') + // @ts-ignore + export type { WxtAppConfig } from 'wxt/utils/define-app-config' + import('wxt/utils/define-app-config') + // @ts-ignore + export type { ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script' + import('wxt/utils/inject-script') } " `); @@ -55,10 +83,53 @@ describe('Auto Imports', () => { /// /// /// + /// /// " `); }); + + it('should generate the #imports module', async () => { + const project = new TestProject(); + project.addFile('entrypoints/popup.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, Browser } from 'wxt/browser'; + export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage'; + export { useAppConfig } from 'wxt/utils/app-config'; + export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context'; + export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe'; + export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated'; + export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root'; + export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types'; + export { defineAppConfig, WxtAppConfig } 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, ScriptPublicPath, InjectScriptOptions } 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 +145,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 +165,55 @@ describe('Auto Imports', () => { /// /// /// + /// " `, ); }); + + it('should only generate the #imports module', async () => { + const project = new TestProject(); + project.setConfigFileConfig({ + imports: false, + }); + project.addFile('entrypoints/popup.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, Browser } from 'wxt/browser'; + export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage'; + export { useAppConfig } from 'wxt/utils/app-config'; + export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context'; + export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe'; + export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated'; + export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root'; + export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types'; + export { defineAppConfig, WxtAppConfig } 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, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script'; + export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns'; + export { fakeBrowser } from 'wxt/testing'; + } + " + `); + }); }); describe('eslintrc', () => { diff --git a/packages/wxt/e2e/tests/hooks.test.ts b/packages/wxt/e2e/tests/hooks.test.ts index 80071e395..9442c4088 100644 --- a/packages/wxt/e2e/tests/hooks.test.ts +++ b/packages/wxt/e2e/tests/hooks.test.ts @@ -180,7 +180,7 @@ describe('Hooks', () => { const server = await project.startServer({ hooks, - runner: { + webExt: { disabled: true, }, }); diff --git a/packages/wxt/e2e/tests/manifest-content.test.ts b/packages/wxt/e2e/tests/manifest-content.test.ts index fcbd97ceb..b9b68f353 100644 --- a/packages/wxt/e2e/tests/manifest-content.test.ts +++ b/packages/wxt/e2e/tests/manifest-content.test.ts @@ -1,36 +1,33 @@ import { describe, it, expect } from 'vitest'; import { TestProject } from '../utils'; -describe.each(['vite-node', 'jiti'] as const)( - 'Manifest Content (Vite runtime? %s)', - (entrypointImporter) => { - it.each([ - { browser: undefined, outDir: 'chrome-mv3', expected: undefined }, - { browser: 'chrome', outDir: 'chrome-mv3', expected: undefined }, - { browser: 'firefox', outDir: 'firefox-mv2', expected: true }, - { browser: 'safari', outDir: 'safari-mv2', expected: false }, - ])( - 'should respect the per-browser entrypoint option with %j', - async ({ browser, expected, outDir }) => { - const project = new TestProject(); +describe('Manifest Content', () => { + it.each([ + { browser: undefined, outDir: 'chrome-mv3', expected: undefined }, + { browser: 'chrome', outDir: 'chrome-mv3', expected: undefined }, + { browser: 'firefox', outDir: 'firefox-mv2', expected: true }, + { browser: 'safari', outDir: 'safari-mv2', expected: false }, + ])( + 'should respect the per-browser entrypoint option with %j', + async ({ browser, expected, outDir }) => { + const project = new TestProject(); - project.addFile( - 'entrypoints/background.ts', - `export default defineBackground({ + project.addFile( + 'entrypoints/background.ts', + `export default defineBackground({ persistent: { firefox: true, safari: false, }, main: () => {}, })`, - ); - await project.build({ browser, experimental: { entrypointImporter } }); + ); + await project.build({ browser }); - const safariManifest = await project.getOutputManifest( - `.output/${outDir}/manifest.json`, - ); - expect(safariManifest.background.persistent).toBe(expected); - }, - ); - }, -); + const safariManifest = await project.getOutputManifest( + `.output/${outDir}/manifest.json`, + ); + expect(safariManifest.background.persistent).toBe(expected); + }, + ); +}); diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index 11d866884..dcbe6c90a 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -191,10 +191,7 @@ describe('Module Helpers', () => { ); const expectedText = addPluginModule(project); - await project.build({ - // reduce build output when comparing test failures - extensionApi: 'chrome', - }); + await project.build(); await expect(project.serializeOutput()).resolves.toContain(expectedText); }); @@ -211,10 +208,7 @@ describe('Module Helpers', () => { ); const expectedText = addPluginModule(project); - await project.build({ - // reduce build output when comparing test failures - extensionApi: 'chrome', - }); + await project.build(); await expect(project.serializeOutput()).resolves.toContain(expectedText); }); @@ -232,10 +226,7 @@ describe('Module Helpers', () => { ); const expectedText = addPluginModule(project); - await project.build({ - // reduce build output when comparing test failures - extensionApi: 'chrome', - }); + await project.build(); await expect(project.serializeOutput()).resolves.toContain(expectedText); }); @@ -248,10 +239,7 @@ describe('Module Helpers', () => { ); const expectedText = addPluginModule(project); - await project.build({ - // reduce build output when comparing test failures - extensionApi: 'chrome', - }); + await project.build(); await expect(project.serializeOutput()).resolves.toContain(expectedText); }); diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 260157ae7..4145eaabe 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -262,9 +262,6 @@ describe('Output Directory Structure', () => { project.addFile('entrypoints/popup/main.ts', `logHello('popup')`); await project.build({ - // Simplify the build output for comparison - extensionApi: 'chrome', - vite: () => ({ build: { // Make output for snapshot readible @@ -289,11 +286,7 @@ describe('Output Directory Structure', () => { logHello("background"); } }); - // @ts-expect-error - ((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) == null ? globalThis.chrome : ( - // @ts-expect-error - globalThis.browser - ); + ((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) ? globalThis.browser : globalThis.chrome; function print(method, ...args) { return; } @@ -347,9 +340,6 @@ describe('Output Directory Structure', () => { project.addFile('entrypoints/popup/main.ts', `logHello('popup')`); await project.build({ - // Simplify the build output for comparison - extensionApi: 'chrome', - vite: () => ({ build: { // Make output for snapshot readible @@ -381,11 +371,7 @@ describe('Output Directory Structure', () => { background; function initPlugins() { } - // @ts-expect-error - ((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) == null ? globalThis.chrome : ( - // @ts-expect-error - globalThis.browser - ); + ((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) ? globalThis.browser : globalThis.chrome; function print(method, ...args) { return; } diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts index ad53c38de..de933ce7d 100644 --- a/packages/wxt/e2e/tests/typescript-project.test.ts +++ b/packages/wxt/e2e/tests/typescript-project.test.ts @@ -239,6 +239,7 @@ describe('TypeScript Project', () => { /// /// /// + /// /// " `); diff --git a/packages/wxt/e2e/tests/user-config.test.ts b/packages/wxt/e2e/tests/user-config.test.ts index b120bf87c..0ee4a969e 100644 --- a/packages/wxt/e2e/tests/user-config.test.ts +++ b/packages/wxt/e2e/tests/user-config.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { TestProject } from '../utils'; -import { InlineConfig } from '../../src/types'; describe('User Config', () => { // Root directory is tested with all tests. @@ -88,24 +87,6 @@ describe('User Config', () => { `); }); - it('should exclude the polyfill when extensionApi="chrome"', async () => { - const buildBackground = async (config?: InlineConfig) => { - const background = `export default defineBackground(() => console.log(browser.runtime.id));`; - const projectWithPolyfill = new TestProject(); - projectWithPolyfill.addFile('entrypoints/background.ts', background); - await projectWithPolyfill.build(config); - return await projectWithPolyfill.serializeFile( - '.output/chrome-mv3/background.js', - ); - }; - - const withPolyfill = await buildBackground(); - const withoutPolyfill = await buildBackground({ - extensionApi: 'chrome', - }); - expect(withoutPolyfill).not.toBe(withPolyfill); - }); - it('should respect changing config files', async () => { const project = new TestProject(); project.addFile( diff --git a/packages/wxt/package.json b/packages/wxt/package.json index 39ca18add..37912eb2a 100644 --- a/packages/wxt/package.json +++ b/packages/wxt/package.json @@ -3,73 +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/index.d.ts", - "default": "./dist/browser/index.mjs" - }, - "./browser/chrome": { - "types": "./dist/browser/chrome.d.ts", - "import": "./dist/browser/chrome.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", @@ -84,11 +18,10 @@ "dependencies": { "@1natsu/wait-element": "catalog:", "@aklinker1/rollup-plugin-visualizer": "catalog:", - "@types/chrome": "catalog:", - "@types/webextension-polyfill": "catalog:", "@webext-core/fake-browser": "catalog:", "@webext-core/isolated-element": "catalog:", "@webext-core/match-patterns": "catalog:", + "@wxt-dev/browser": "workspace:*", "@wxt-dev/storage": "workspace:^1.0.0", "async-mutex": "catalog:", "c12": "catalog:", @@ -108,7 +41,6 @@ "hookable": "catalog:", "import-meta-resolve": "catalog:", "is-wsl": "catalog:", - "jiti": "catalog:", "json5": "catalog:", "jszip": "catalog:", "linkedom": "catalog:", @@ -128,8 +60,7 @@ "unimport": "catalog:", "vite": "catalog:", "vite-node": "catalog:", - "web-ext-run": "catalog:", - "webextension-polyfill": "catalog:" + "web-ext-run": "catalog:" }, "devDependencies": { "@aklinker1/check": "catalog:", @@ -149,9 +80,111 @@ "vitest": "catalog:", "vitest-plugin-random-seed": "catalog:" }, - "peerDependenciesMeta": { - "@types/chrome": { - "optional": true + "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" + }, + "./utils/storage": { + "types": "./dist/utils/storage.d.ts", + "default": "./dist/utils/storage.mjs" + }, + "./browser": { + "types": "./dist/browser.d.ts", + "default": "./dist/browser.mjs" + }, + "./testing": { + "types": "./dist/testing/index.d.ts", + "default": "./dist/testing/index.mjs" + }, + "./vite-builder-env": { + "types": "./dist/vite-builder-env.d.ts" + }, + "./modules": { + "types": "./dist/modules.d.ts", + "default": "./dist/modules.mjs" } } } diff --git a/packages/wxt/src/__tests__/modules.test.ts b/packages/wxt/src/__tests__/modules.test.ts index a0455d0a0..6481e0f7c 100644 --- a/packages/wxt/src/__tests__/modules.test.ts +++ b/packages/wxt/src/__tests__/modules.test.ts @@ -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); - }); }); }); diff --git a/packages/wxt/src/browser.ts b/packages/wxt/src/browser.ts new file mode 100644 index 000000000..0019efede --- /dev/null +++ b/packages/wxt/src/browser.ts @@ -0,0 +1,33 @@ +/** + * 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'; + +/** + * This interface is empty because it is generated per-project when running `wxt prepare`. See: + * - `.wxt/types/paths.d.ts` + */ +export interface WxtRuntime {} + +/** + * This interface is empty because it is generated per-project when running `wxt prepare`. See: + * - `.wxt/types/i18n.d.ts` + */ +export interface WxtI18n {} + +export type WxtBrowser = Omit & { + runtime: WxtRuntime & Omit<(typeof _browser)['runtime'], 'getURL'>; + i18n: WxtI18n & Omit<(typeof _browser)['i18n'], 'getMessage'>; +}; + +export const browser: WxtBrowser = _browser; + +export { Browser }; diff --git a/packages/wxt/src/browser/chrome.ts b/packages/wxt/src/browser/chrome.ts deleted file mode 100644 index d1dad85db..000000000 --- a/packages/wxt/src/browser/chrome.ts +++ /dev/null @@ -1,23 +0,0 @@ -/// -/** - * EXPERIMENTAL - * - * Includes the `chrome` API and types when using `extensionApi: 'chrome'`. - * - * @module wxt/browser/chrome - */ -import type { WxtRuntime, WxtI18n } from './index'; - -export type WxtBrowser = Omit & { - runtime: WxtRuntime & Omit<(typeof chrome)['runtime'], 'getURL'>; - i18n: WxtI18n & Omit<(typeof chrome)['i18n'], 'getMessage'>; -}; - -// #region snippet -export const browser: WxtBrowser = - // @ts-expect-error - globalThis.browser?.runtime?.id == null - ? globalThis.chrome - : // @ts-expect-error - globalThis.browser; -// #endregion snippet diff --git a/packages/wxt/src/browser/index.ts b/packages/wxt/src/browser/index.ts deleted file mode 100644 index 8e21e1fba..000000000 --- a/packages/wxt/src/browser/index.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * Includes the `browser` API and types when using `extensionApi: 'webextension-polyfill'` (the default). - * - * @module wxt/browser - */ - -import originalBrowser, { Browser } from 'webextension-polyfill'; - -export type AugmentedBrowser = Omit & { - runtime: WxtRuntime & Omit; - i18n: WxtI18n & Omit; -}; - -/** - * This interface is empty because it is generated per-project when running `wxt prepare`. See: - * - `.wxt/types/paths.d.ts` - */ -export interface WxtRuntime {} - -/** - * This interface is empty because it is generated per-project when running `wxt prepare`. See: - * - `.wxt/types/i18n.d.ts` - */ -export interface WxtI18n {} - -export const browser: AugmentedBrowser = originalBrowser; - -// re-export all the types from webextension-polyfill -// Because webextension-polyfill uses a weird namespace with "import export", there isn't a good way -// to get these types without re-listing them. -/** @ignore */ -export type { - ActivityLog, - Alarms, - Bookmarks, - Action, - BrowserAction, - BrowserSettings, - BrowsingData, - CaptivePortal, - Clipboard, - Commands, - ContentScripts, - ContextualIdentities, - Cookies, - DeclarativeNetRequest, - Devtools, - Dns, - Downloads, - Events, - Experiments, - Extension, - ExtensionTypes, - Find, - GeckoProfiler, - History, - I18n, - Identity, - Idle, - Management, - Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox. - ContextMenus, - Menus, - NetworkStatus, - NormandyAddonStudy, - Notifications, - Omnibox, - PageAction, - Permissions, - Pkcs11, - Privacy, - Proxy, - Runtime, - Scripting, - Search, - Sessions, - SidebarAction, - Storage, - Tabs, - Theme, - TopSites, - Types, - UserScripts, - WebNavigation, - WebRequest, - Windows, -} from 'webextension-polyfill'; diff --git a/packages/wxt/src/builtin-modules/unimport.ts b/packages/wxt/src/builtin-modules/unimport.ts index 11bf88bae..20972948b 100644 --- a/packages/wxt/src/builtin-modules/unimport.ts +++ b/packages/wxt/src/builtin-modules/unimport.ts @@ -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) => { 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 { - // 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 { + 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, true).replaceAll('\n', '\n ')}`, + '}', + '', + ].join('\n'), + tsReference: true, + }; +} + async function getEslintConfigEntry( unimport: Unimport, version: 8 | 9, diff --git a/packages/wxt/src/cli/index.ts b/packages/wxt/src/cli/index.ts index 29002fd46..d9b7ddfa9 100644 --- a/packages/wxt/src/cli/index.ts +++ b/packages/wxt/src/cli/index.ts @@ -2,9 +2,6 @@ import cli from './commands'; import { version } from '../version'; import { isAliasedCommand } from './cli-utils'; -// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277 -process.env.VITE_CJS_IGNORE_WARNING = 'true'; - // Grab the command that we're trying to run cli.parse(process.argv, { run: false }); diff --git a/packages/wxt/src/client/content-scripts/index.ts b/packages/wxt/src/client/content-scripts/index.ts deleted file mode 100644 index 538b3837f..000000000 --- a/packages/wxt/src/client/content-scripts/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './content-script-context'; -export * from './ui'; diff --git a/packages/wxt/src/client/index.ts b/packages/wxt/src/client/index.ts deleted file mode 100644 index b6818d07e..000000000 --- a/packages/wxt/src/client/index.ts +++ /dev/null @@ -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'; diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index 545c63393..b63843f8a 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -20,7 +20,6 @@ import { import { Hookable } from 'hookable'; import { toArray } from '../../utils/arrays'; import { safeVarName } from '../../utils/strings'; -import { importEntrypointFile } from '../../utils/building'; import { ViteNodeServer } from 'vite-node/server'; import { ViteNodeRunner } from 'vite-node/client'; import { installSourcemapsSupport } from 'vite-node/source-map'; @@ -83,7 +82,6 @@ export async function createViteBuilder( wxtPlugins.tsconfigPaths(wxtConfig), wxtPlugins.noopBackground(), wxtPlugins.globals(wxtConfig), - wxtPlugins.resolveExtensionApi(wxtConfig), wxtPlugins.defineImportMeta(), wxtPlugins.wxtPluginLoader(wxtConfig), wxtPlugins.resolveAppConfig(wxtConfig), @@ -286,44 +284,26 @@ export async function createViteBuilder( version: vite.version, async importEntrypoint(path) { const env = createExtensionEnvironment(); - switch (wxtConfig.entrypointLoader) { - default: - case 'jiti': { - return await env.run(() => importEntrypointFile(path)); - } - case 'vite-node': { - const { runner, server } = await createViteNodeImporter([path]); - const res = await env.run(() => runner.executeFile(path)); - await server.close(); - requireDefaultExport(path, res); - return res.default; - } - } + const { runner, server } = await createViteNodeImporter([path]); + const res = await env.run(() => runner.executeFile(path)); + await server.close(); + requireDefaultExport(path, res); + return res.default; }, async importEntrypoints(paths) { const env = createExtensionEnvironment(); - switch (wxtConfig.entrypointLoader) { - default: - case 'jiti': { - return await env.run(() => - Promise.all(paths.map(importEntrypointFile)), - ); - } - case 'vite-node': { - const { runner, server } = await createViteNodeImporter(paths); - const res = await env.run(() => - Promise.all( - paths.map(async (path) => { - const mod = await runner.executeFile(path); - requireDefaultExport(path, mod); - return mod.default; - }), - ), - ); - await server.close(); - return res; - } - } + const { runner, server } = await createViteNodeImporter(paths); + const res = await env.run(() => + Promise.all( + paths.map(async (path) => { + const mod = await runner.executeFile(path); + requireDefaultExport(path, mod); + return mod.default; + }), + ), + ); + await server.close(); + return res; }, async build(group) { let entryConfig; diff --git a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts index 7724219ec..0e2b18f3a 100644 --- a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts +++ b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts @@ -3,12 +3,7 @@ import type * as vite from 'vite'; import { ResolvedConfig } from '../../../../types'; /** - * Mock `webextension-polyfill`, `wxt/browser`, and `wxt/browser/*` by inlining - * all dependencies that import them and adding a custom alias so that Vite - * resolves to a mocked version of the module. - * - * TODO: Detect non-wxt dependencies (like `@webext-core/*`) that import `webextension-polyfill` via - * `npm list` and inline them automatically. + * Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake version of the extension APIs */ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { const virtualSetupModule = 'virtual:wxt-setup'; @@ -27,14 +22,12 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { }, resolve: { alias: [ - { find: 'webextension-polyfill', replacement }, // wxt/browser, wxt/browser/... - { find: /^wxt\/browser.*/, replacement }, + { find: 'wxt/browser', replacement }, ], }, ssr: { - // Inline all WXT modules so vite processes them so the aliases can - // be resolved + // Inline all WXT modules subdependencies can be mocked noExternal: ['wxt'], }, }; diff --git a/packages/wxt/src/core/builders/vite/plugins/index.ts b/packages/wxt/src/core/builders/vite/plugins/index.ts index f70dde240..5ed13ff81 100644 --- a/packages/wxt/src/core/builders/vite/plugins/index.ts +++ b/packages/wxt/src/core/builders/vite/plugins/index.ts @@ -9,7 +9,6 @@ export * from './cssEntrypoints'; export * from './bundleAnalysis'; export * from './globals'; export * from './extensionApiMock'; -export * from './resolveExtensionApi'; export * from './entrypointGroupGlobals'; export * from './defineImportMeta'; export * from './removeEntrypointMainFunction'; diff --git a/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts b/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts index f6e06cadb..660399e3e 100644 --- a/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts +++ b/packages/wxt/src/core/builders/vite/plugins/noopBackground.ts @@ -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)`; } }, }; diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts b/packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts deleted file mode 100644 index 17ec02064..000000000 --- a/packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ResolvedConfig } from '../../../../types'; -import type * as vite from 'vite'; - -/** - * Apply the experimental config for which extension API is used. This only - * effects the extension API included at RUNTIME - during development, types - * depend on the import. - * - * NOTE: this only works if we import `wxt/browser` instead of using the relative path. - */ -export function resolveExtensionApi(config: ResolvedConfig): vite.Plugin { - return { - name: 'wxt:resolve-extension-api', - config() { - // Only apply the config if we're disabling the polyfill - if (config.extensionApi === 'webextension-polyfill') return; - - return { - resolve: { - alias: [ - { find: /^wxt\/browser$/, replacement: 'wxt/browser/chrome' }, - ], - }, - }; - }, - }; -} diff --git a/packages/wxt/src/core/define-runner-config.ts b/packages/wxt/src/core/define-runner-config.ts deleted file mode 100644 index d931c085a..000000000 --- a/packages/wxt/src/core/define-runner-config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { ExtensionRunnerConfig } from '../types'; - -export function defineRunnerConfig( - config: ExtensionRunnerConfig, -): ExtensionRunnerConfig { - return config; -} diff --git a/packages/wxt/src/core/define-web-ext-config.ts b/packages/wxt/src/core/define-web-ext-config.ts new file mode 100644 index 000000000..7826004fc --- /dev/null +++ b/packages/wxt/src/core/define-web-ext-config.ts @@ -0,0 +1,19 @@ +import consola from 'consola'; +import { WebExtConfig } from '../types'; + +/** + * @deprecated Use `defineWebExtConfig` instead. Same function, different name. + */ +export function defineRunnerConfig(config: WebExtConfig): WebExtConfig { + consola.warn( + '`defineRunnerConfig` is deprecated, use `defineWebExtConfig` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0', + ); + return defineWebExtConfig(config); +} + +/** + * Configure how [`web-ext`](https://github.com/mozilla/web-ext) starts the browser during development. + */ +export function defineWebExtConfig(config: WebExtConfig): WebExtConfig { + return config; +} diff --git a/packages/wxt/src/core/generate-wxt-dir.ts b/packages/wxt/src/core/generate-wxt-dir.ts index e7d35949e..b6871b8c5 100644 --- a/packages/wxt/src/core/generate-wxt-dir.ts +++ b/packages/wxt/src/core/generate-wxt-dir.ts @@ -37,11 +37,6 @@ export async function generateWxtDir(entrypoints: Entrypoint[]): Promise { // import.meta.env.* entries.push(await getGlobalsDeclarationEntry()); - // @types/chrome - if (wxt.config.extensionApi === 'chrome') { - entries.push({ module: '@types/chrome' }); - } - // tsconfig.json entries.push(await getTsConfigEntry()); @@ -237,10 +232,11 @@ async function getTsConfigEntry(): Promise { .flatMap(([alias, absolutePath]) => { const aliasPath = getTsconfigPath(absolutePath); return [ - ` "${alias}": ["${aliasPath}"]`, - ` "${alias}/*": ["${aliasPath}/*"]`, + `"${alias}": ["${aliasPath}"]`, + `"${alias}/*": ["${aliasPath}/*"]`, ]; }) + .map((line) => ` ${line}`) .join(',\n'); const text = `{ diff --git a/packages/wxt/src/core/index.ts b/packages/wxt/src/core/index.ts index 9c2c01cb0..0f4057f0a 100644 --- a/packages/wxt/src/core/index.ts +++ b/packages/wxt/src/core/index.ts @@ -1,7 +1,7 @@ export * from './build'; export * from './clean'; export * from './define-config'; -export * from './define-runner-config'; +export * from './define-web-ext-config'; export * from './create-server'; export * from './initialize'; export * from './prepare'; diff --git a/packages/wxt/src/core/resolve-config.ts b/packages/wxt/src/core/resolve-config.ts index 01c38ca21..4d1681696 100644 --- a/packages/wxt/src/core/resolve-config.ts +++ b/packages/wxt/src/core/resolve-config.ts @@ -7,14 +7,13 @@ import { ConfigEnv, UserManifestFn, UserManifest, - ExtensionRunnerConfig, + WebExtConfig, WxtResolvedUnimportOptions, Logger, WxtCommand, WxtModule, WxtModuleWithMetadata, ResolvedEslintrc, - Eslintrc, } from '../types'; import path from 'node:path'; import { createFsCache } from './utils/cache'; @@ -89,40 +88,47 @@ export async function resolveConfig( srcDir, mergedConfig.entrypointsDir ?? 'entrypoints', ); - const modulesDir = path.resolve(srcDir, mergedConfig.modulesDir ?? 'modules'); if (await isDirMissing(entrypointsDir)) { logMissingDir(logger, 'Entrypoints', entrypointsDir); } + const modulesDir = path.resolve(root, mergedConfig.modulesDir ?? 'modules'); const filterEntrypoints = mergedConfig.filterEntrypoints?.length ? new Set(mergedConfig.filterEntrypoints) : undefined; - const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public'); + const publicDir = path.resolve(root, mergedConfig.publicDir ?? 'public'); const typesDir = path.resolve(wxtDir, 'types'); const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output'); const modeSuffixes: Record = { production: '', development: '-dev', }; + const modeSuffix = modeSuffixes[mode] ?? `-${mode}`; const outDirTemplate = ( - mergedConfig.outDirTemplate ?? `${browser}-mv${manifestVersion}` + mergedConfig.outDirTemplate ?? + `${browser}-mv${manifestVersion}${modeSuffix}` ) // Resolve all variables in the template .replaceAll('{{browser}}', browser) .replaceAll('{{manifestVersion}}', manifestVersion.toString()) - .replaceAll('{{modeSuffix}}', modeSuffixes[mode] ?? `-${mode}`) + .replaceAll('{{modeSuffix}}', modeSuffix) .replaceAll('{{mode}}', mode) .replaceAll('{{command}}', command); const outDir = path.resolve(outBaseDir, outDirTemplate); const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R'; - const runnerConfig = await loadConfig({ + if (inlineConfig.runner != null || userConfig.runner != null) { + logger.warn( + '`InlineConfig#runner` is deprecated, use `InlineConfig#webExt` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0', + ); + } + const runnerConfig = await loadConfig({ name: 'web-ext', cwd: root, globalRc: true, rcFile: '.webextrc', - overrides: inlineConfig.runner, - defaults: userConfig.runner, + overrides: inlineConfig.webExt ?? inlineConfig.runner, + defaults: userConfig.webExt ?? userConfig.runner, }); // Make sure alias are absolute const alias = Object.fromEntries( @@ -171,8 +177,6 @@ export async function resolveConfig( {}, ); - const extensionApi = mergedConfig.extensionApi ?? 'webextension-polyfill'; - return { browser, command, @@ -182,13 +186,7 @@ export async function resolveConfig( filterEntrypoints, env, fsCache: createFsCache(wxtDir), - imports: await getUnimportOptions( - wxtDir, - srcDir, - logger, - extensionApi, - mergedConfig, - ), + imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig), logger, manifest: await resolveManifestConfig(env, mergedConfig.manifest), manifestVersion, @@ -203,12 +201,9 @@ export async function resolveConfig( typesDir, wxtDir, zip: resolveZipConfig(root, browser, outBaseDir, mergedConfig), - transformManifest: mergedConfig.transformManifest, analysis: resolveAnalysisConfig(root, mergedConfig), userConfigMetadata: userConfigMetadata ?? {}, alias, - extensionApi, - entrypointLoader: mergedConfig.entrypointLoader ?? 'vite-node', experimental: defu(mergedConfig.experimental, {}), dev: { server: devServerConfig, @@ -254,12 +249,6 @@ async function mergeInlineConfig( return defu(inline, user); }; - // Merge transformManifest option - const transformManifest: InlineConfig['transformManifest'] = (manifest) => { - userConfig.transformManifest?.(manifest); - inlineConfig.transformManifest?.(manifest); - }; - const merged = defu(inlineConfig, userConfig); // Builders @@ -272,7 +261,6 @@ async function mergeInlineConfig( return { ...merged, // Custom merge values - transformManifest, imports, manifest, ...builderConfig, @@ -342,38 +330,140 @@ async function getUnimportOptions( wxtDir: string, srcDir: string, logger: Logger, - extensionApi: ResolvedConfig['extensionApi'], config: InlineConfig, -): Promise { - if (config.imports === false) return false; +): Promise { + 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 defineImportsAndTypes = (imports: string[], typeImports: string[]) => [ + ...imports, + ...typeImports.map((name) => ({ name, type: true })), + ]; const defaultOptions: WxtResolvedUnimportOptions = { - debugLog: logger.debug, - imports: [ - { name: 'defineConfig', from: 'wxt' }, - { name: 'fakeBrowser', from: 'wxt/testing' }, - ], + imports: [{ name: 'fakeBrowser', from: 'wxt/testing' }], 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'], + from: 'wxt/browser', + imports: defineImportsAndTypes(['browser'], ['Browser']), + }, + { + from: 'wxt/utils/storage', + imports: defineImportsAndTypes( + ['storage'], + [ + 'StorageArea', + 'WxtStorage', + 'WxtStorageItem', + 'StorageArea', + 'StorageItemKey', + 'StorageAreaChanges', + 'MigrationError', + ], + ), + }, + { + from: 'wxt/utils/app-config', + imports: defineImportsAndTypes(['useAppConfig'], []), + }, + { + from: 'wxt/utils/content-script-context', + imports: defineImportsAndTypes( + ['ContentScriptContext'], + ['WxtWindowEventMap'], + ), + }, + { + from: 'wxt/utils/content-script-ui/iframe', + imports: defineImportsAndTypes( + ['createIframeUi'], + ['IframeContentScriptUi', 'IframeContentScriptUiOptions'], + ), + ignore: invalidExports, + }, + { + from: 'wxt/utils/content-script-ui/integrated', + imports: defineImportsAndTypes( + ['createIntegratedUi'], + ['IntegratedContentScriptUi', 'IntegratedContentScriptUiOptions'], + ), + ignore: invalidExports, }, { - package: - extensionApi === 'chrome' ? 'wxt/browser/chrome' : 'wxt/browser', + from: 'wxt/utils/content-script-ui/shadow-root', + imports: defineImportsAndTypes( + ['createShadowRootUi'], + ['ShadowRootContentScriptUi', 'ShadowRootContentScriptUiOptions'], + ), + ignore: invalidExports, + }, + { + from: 'wxt/utils/content-script-ui/types', + imports: defineImportsAndTypes( + [], + [ + 'ContentScriptUi', + 'ContentScriptUiOptions', + 'ContentScriptOverlayAlignment', + 'ContentScriptAppendMode', + 'ContentScriptInlinePositioningOptions', + 'ContentScriptOverlayPositioningOptions', + 'ContentScriptModalPositioningOptions', + 'ContentScriptPositioningOptions', + 'ContentScriptAnchoredOptions', + 'AutoMountOptions', + 'StopAutoMount', + 'AutoMount', + ], + ), + }, + { + from: 'wxt/utils/define-app-config', + imports: defineImportsAndTypes(['defineAppConfig'], ['WxtAppConfig']), + }, + { + from: 'wxt/utils/define-background', + imports: defineImportsAndTypes(['defineBackground'], []), + }, + { + from: 'wxt/utils/define-content-script', + imports: defineImportsAndTypes(['defineContentScript'], []), + }, + { + from: 'wxt/utils/define-unlisted-script', + imports: defineImportsAndTypes(['defineUnlistedScript'], []), + }, + { + from: 'wxt/utils/define-wxt-plugin', + imports: defineImportsAndTypes(['defineWxtPlugin'], []), + }, + { + from: 'wxt/utils/inject-script', + imports: defineImportsAndTypes( + ['injectScript'], + ['ScriptPublicPath', 'InjectScriptOptions'], + ), + ignore: invalidExports, + }, + { + from: 'wxt/utils/match-patterns', + imports: defineImportsAndTypes( + ['InvalidMatchPattern', 'MatchPattern'], + [], + ), }, - { package: 'wxt/sandbox' }, - { package: 'wxt/storage' }, ], + 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( @@ -384,34 +474,34 @@ async function getUnimportOptions( async function getUnimportEslintOptions( wxtDir: string, - options: Eslintrc | undefined, + options: InlineConfig['imports'], ): Promise { - 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, }; diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts index 230f14704..993356915 100644 --- a/packages/wxt/src/core/utils/__tests__/manifest.test.ts +++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts @@ -13,7 +13,6 @@ import { fakeWxtDevServer, setFakeWxt, } from '../testing/fake-objects'; -import { Manifest } from 'webextension-polyfill'; import { BuildOutput, ContentScriptEntrypoint, @@ -22,6 +21,7 @@ import { } from '../../../types'; import { wxt } from '../../wxt'; import { mock } from 'vitest-mock-extended'; +import type { Browser } from '@wxt-dev/browser'; const outDir = '/output'; const contentScriptOutDir = '/output/content-scripts'; @@ -58,7 +58,7 @@ describe('Manifest Utils', () => { outDir, }, }); - const expected: Partial = { + const expected: Partial = { action: { default_icon: popup.options.defaultIcon, default_title: popup.options.defaultTitle, @@ -1091,31 +1091,6 @@ describe('Manifest Utils', () => { }); }); - describe('transformManifest option', () => { - it("should call the transformManifest option after the manifest is generated, but before it's returned", async () => { - const entrypoints: Entrypoint[] = []; - const buildOutput = fakeBuildOutput(); - const newAuthor = 'Custom Author'; - setFakeWxt({ - config: { - transformManifest(manifest: any) { - manifest.author = newAuthor; - }, - }, - }); - const expected = { - author: newAuthor, - }; - - const { manifest: actual } = await generateManifest( - entrypoints, - buildOutput, - ); - - expect(actual).toMatchObject(expected); - }); - }); - describe('version', () => { it.each(['chrome', 'safari', 'edge'] as const)( 'should include version and version_name as is on %s', diff --git a/packages/wxt/src/core/utils/__tests__/transform.test.ts b/packages/wxt/src/core/utils/__tests__/transform.test.ts index 4a59b06d6..4df3d61c2 100644 --- a/packages/wxt/src/core/utils/__tests__/transform.test.ts +++ b/packages/wxt/src/core/utils/__tests__/transform.test.ts @@ -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();`; diff --git a/packages/wxt/src/core/utils/building/__tests__/import-entrypoint.test.ts b/packages/wxt/src/core/utils/building/__tests__/import-entrypoint.test.ts deleted file mode 100644 index 2f7571999..000000000 --- a/packages/wxt/src/core/utils/building/__tests__/import-entrypoint.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { beforeEach, describe, expect, it } from 'vitest'; -import { importEntrypointFile } from '../../../utils/building'; -import { resolve } from 'node:path'; -import { setFakeWxt } from '../../../utils/testing/fake-objects'; - -const entrypointPath = (filename: string) => - resolve(__dirname, 'test-entrypoints', filename); - -describe('importEntrypointFile', () => { - beforeEach(() => { - setFakeWxt({ - config: { - imports: false, - debug: false, - // Run inside the demo folder so that wxt is in the node_modules - // WXT must also be built for these tests to pass - root: 'demo', - }, - }); - }); - - it.each([ - ['background.ts', { main: expect.any(Function) }], - ['content.ts', { main: expect.any(Function), matches: [''] }], - ['unlisted.ts', { main: expect.any(Function) }], - ['react.tsx', { main: expect.any(Function) }], - ['with-named.ts', { main: expect.any(Function) }], - ])( - 'should return the default export of test-entrypoints/%s', - async (file, expected) => { - const actual = await importEntrypointFile(entrypointPath(file)); - - expect(actual).toEqual(expected); - }, - ); - - it('should return undefined when there is no default export', async () => { - const actual = await importEntrypointFile( - entrypointPath('no-default-export.ts'), - ); - - expect(actual).toBeUndefined(); - }); - - it('should throw a custom error message when an imported variable is used before main', async () => { - await expect(() => - importEntrypointFile(entrypointPath('imported-option.ts')), - ).rejects.toThrowError( - `imported-option.ts: Cannot use imported variable "faker" outside the main function.`, - ); - }); -}); diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts index cc7d145ea..d12bdb650 100644 --- a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts +++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/background.ts @@ -1,4 +1,4 @@ -import { defineBackground } from '../../../../../sandbox'; +import { defineBackground } from '../../../../../utils/define-background'; export default defineBackground({ main() {}, diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts index 21d582a9b..2377a6b3c 100644 --- a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts +++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/content.ts @@ -1,4 +1,4 @@ -import { defineContentScript } from '../../../../../sandbox'; +import { defineContentScript } from '../../../../../utils/define-content-script'; export default defineContentScript({ matches: [''], diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts index a5b54951c..efdc50ede 100644 --- a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts +++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/imported-option.ts @@ -1,4 +1,4 @@ -import { defineContentScript } from '../../../../../sandbox'; +import { defineContentScript } from '../../../../../utils/define-content-script'; import { faker } from '@faker-js/faker'; export default defineContentScript({ diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx index b9bf6082b..c0421d45e 100644 --- a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx +++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/react.tsx @@ -1,3 +1,3 @@ -import { defineUnlistedScript } from '../../../../../sandbox'; +import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script'; export default defineUnlistedScript(() => {}); diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts index b9bf6082b..c0421d45e 100644 --- a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts +++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/unlisted.ts @@ -1,3 +1,3 @@ -import { defineUnlistedScript } from '../../../../../sandbox'; +import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script'; export default defineUnlistedScript(() => {}); diff --git a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts index 5b9d8930c..1ed0e7103 100644 --- a/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts +++ b/packages/wxt/src/core/utils/building/__tests__/test-entrypoints/with-named.ts @@ -1,4 +1,4 @@ -import { defineBackground } from '../../../../../sandbox'; +import { defineBackground } from '../../../../../utils/define-background'; export const a = {}; diff --git a/packages/wxt/src/core/utils/building/import-entrypoint.ts b/packages/wxt/src/core/utils/building/import-entrypoint.ts deleted file mode 100644 index 09342bf9e..000000000 --- a/packages/wxt/src/core/utils/building/import-entrypoint.ts +++ /dev/null @@ -1,126 +0,0 @@ -import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti'; -import { createUnimport } from 'unimport'; -import fs from 'fs-extra'; -import { relative, resolve } from 'node:path'; -import { removeProjectImportStatements } from '../../utils/strings'; -import { normalizePath } from '../../utils/paths'; -import { TransformOptions, transformSync } from 'esbuild'; -import { fileURLToPath } from 'node:url'; -import { wxt } from '../../wxt'; - -/** - * Get the value from the default export of a `path`. - * - * It works by: - * - * 1. Reading the file text - * 2. Stripping all imports from it via regex - * 3. Auto-import only the client helper functions - * - * This prevents resolving imports of imports, speeding things up and preventing "xxx is not - * defined" errors. - * - * Downside is that code cannot be executed outside of the main fucntion for the entrypoint, - * otherwise you will see "xxx is not defined" errors for any imports used outside of main function. - */ -export async function importEntrypointFile(path: string): Promise { - wxt.logger.debug('Loading file metadata:', path); - // JITI & Babel uses normalized paths. - const normalPath = normalizePath(path); - - const unimport = createUnimport({ - ...wxt.config.imports, - // Only allow specific imports, not all from the project - dirs: [], - }); - await unimport.init(); - - const text = await fs.readFile(path, 'utf-8'); - const textNoImports = removeProjectImportStatements(text); - const { code } = await unimport.injectImports(textNoImports); - wxt.logger.debug( - ['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'), - ); - - const jiti = createJITI( - typeof __filename !== 'undefined' - ? __filename - : fileURLToPath(import.meta.url), - { - cache: false, - debug: wxt.config.debug, - alias: { - 'webextension-polyfill': resolve( - wxt.config.wxtModuleDir, - 'dist/virtual/mock-browser.mjs', - ), - // TODO: Resolve this virtual module to some file with - // `export default {}` instead of this hack of using another file with - // a default export. - 'virtual:app-config': resolve( - wxt.config.wxtModuleDir, - 'dist/virtual/mock-browser.mjs', - ), - }, - // Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not - // respect the custom transform function when using it's native bun option. - tryNative: false, - // List of extensions to transform with esbuild - extensions: [ - '.ts', - '.cts', - '.mts', - '.tsx', - '.js', - '.cjs', - '.mjs', - '.jsx', - ], - transform(opts) { - const isEntrypoint = opts.filename === normalPath; - return transformSync( - // Use modified source code for entrypoints - isEntrypoint ? code : opts.source, - getEsbuildOptions(opts), - ); - }, - }, - ); - - try { - const res = await jiti(path); - return res.default; - } catch (err) { - const filePath = relative(wxt.config.root, path); - if (err instanceof ReferenceError) { - // "XXX is not defined" - usually due to WXT removing imports - const variableName = err.message.replace(' is not defined', ''); - throw Error( - `${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/go-further/entrypoint-side-effects.html`, - { cause: err }, - ); - } else { - wxt.logger.error(err); - throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err }); - } - } -} - -function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions { - const isJsx = opts.filename?.endsWith('x'); - return { - format: 'cjs', - loader: isJsx ? 'tsx' : 'ts', - define: { - 'import.meta.env.ENTRYPOINT': '"build"', - }, - ...(isJsx - ? { - // `h` and `Fragment` are undefined, but that's OK because JSX is never evaluated while - // grabbing the entrypoint's options. - jsxFactory: 'h', - jsxFragment: 'Fragment', - } - : undefined), - }; -} diff --git a/packages/wxt/src/core/utils/building/index.ts b/packages/wxt/src/core/utils/building/index.ts index 72d74acd5..de8fb0bb2 100644 --- a/packages/wxt/src/core/utils/building/index.ts +++ b/packages/wxt/src/core/utils/building/index.ts @@ -2,6 +2,5 @@ export * from './build-entrypoints'; export * from './detect-dev-changes'; export * from './find-entrypoints'; export * from './group-entrypoints'; -export * from './import-entrypoint'; export * from './internal-build'; export * from './rebuild'; diff --git a/packages/wxt/src/core/utils/building/rebuild.ts b/packages/wxt/src/core/utils/building/rebuild.ts index 7a17e3ff1..b0902f5c8 100644 --- a/packages/wxt/src/core/utils/building/rebuild.ts +++ b/packages/wxt/src/core/utils/building/rebuild.ts @@ -1,9 +1,9 @@ -import type { Manifest } from 'wxt/browser'; import { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types'; import { generateWxtDir } from '../../generate-wxt-dir'; import { buildEntrypoints } from './build-entrypoints'; import { generateManifest, writeManifest } from '../../utils/manifest'; import { wxt } from '../../wxt'; +import type { Browser } from '@wxt-dev/browser'; /** * Given a configuration, list of entrypoints, and an existing, partial output, build the @@ -30,7 +30,7 @@ export async function rebuild( }, ): Promise<{ output: BuildOutput; - manifest: Manifest.WebExtensionManifest; + manifest: Browser.runtime.Manifest; warnings: any[][]; }> { const { default: ora } = await import('ora'); diff --git a/packages/wxt/src/core/utils/content-scripts.ts b/packages/wxt/src/core/utils/content-scripts.ts index 3ba1eb18f..416158255 100644 --- a/packages/wxt/src/core/utils/content-scripts.ts +++ b/packages/wxt/src/core/utils/content-scripts.ts @@ -1,6 +1,7 @@ -import type { Manifest, Scripting } from 'wxt/browser'; +import type { Browser } from '@wxt-dev/browser'; import { ContentScriptEntrypoint, ResolvedConfig } from '../../types'; import { getEntrypointBundlePath } from './entrypoints'; +import { ManifestContentScript } from './types'; /** * Returns a unique and consistent string hash based on a content scripts options. @@ -22,13 +23,14 @@ export function hashContentScriptOptions( if (simplifiedOptions[key] == null) delete simplifiedOptions[key]; }); - const withDefaults: Manifest.ContentScript = { + const withDefaults: ManifestContentScript = { exclude_globs: [], exclude_matches: [], include_globs: [], match_about_blank: false, run_at: 'document_idle', all_frames: false, + // @ts-expect-error: Untyped match_origin_as_fallback: false, world: 'ISOLATED', ...simplifiedOptions, @@ -49,7 +51,7 @@ export function mapWxtOptionsToContentScript( options: ContentScriptEntrypoint['options'], js: string[] | undefined, css: string[] | undefined, -): Manifest.ContentScript { +): ManifestContentScript { return { matches: options.matches ?? [], all_frames: options.allFrames, @@ -60,7 +62,7 @@ export function mapWxtOptionsToContentScript( run_at: options.runAt, css, js, - + // @ts-expect-error: Untyped match_origin_as_fallback: options.matchOriginAsFallback, world: options.world, }; @@ -70,7 +72,7 @@ export function mapWxtOptionsToRegisteredContentScript( options: ContentScriptEntrypoint['options'], js: string[] | undefined, css: string[] | undefined, -): Omit { +): Omit { return { allFrames: options.allFrames, excludeMatches: options.excludeMatches, diff --git a/packages/wxt/src/core/utils/manifest.ts b/packages/wxt/src/core/utils/manifest.ts index 364f749f1..70ff10390 100644 --- a/packages/wxt/src/core/utils/manifest.ts +++ b/packages/wxt/src/core/utils/manifest.ts @@ -1,4 +1,3 @@ -import type { Manifest } from 'wxt/browser'; import { Entrypoint, BackgroundEntrypoint, @@ -21,12 +20,14 @@ import { normalizePath } from './paths'; import { writeFileIfDifferent } from './fs'; import defu from 'defu'; import { wxt } from '../wxt'; +import { ManifestV3WebAccessibleResource } from './types'; +import type { Browser } from '@wxt-dev/browser'; /** * Writes the manifest to the output directory and the build output. */ export async function writeManifest( - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, output: BuildOutput, ): Promise { const str = @@ -49,7 +50,7 @@ export async function writeManifest( export async function generateManifest( allEntrypoints: Entrypoint[], buildOutput: Omit, -): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> { +): Promise<{ manifest: Browser.runtime.Manifest; warnings: any[][] }> { const entrypoints = allEntrypoints.filter((entry) => !entry.skipped); const warnings: any[][] = []; @@ -67,7 +68,7 @@ export async function generateManifest( } const version = wxt.config.manifest.version ?? simplifyVersion(versionName); - const baseManifest: Manifest.WebExtensionManifest = { + const baseManifest: Browser.runtime.Manifest = { manifest_version: wxt.config.manifestVersion, name: pkg?.name, description: pkg?.description, @@ -83,10 +84,7 @@ export async function generateManifest( ); } - let manifest = defu( - userManifest, - baseManifest, - ) as Manifest.WebExtensionManifest; + let manifest = defu(userManifest, baseManifest) as Browser.runtime.Manifest; // Add reload command in dev mode if (wxt.config.command === 'serve' && wxt.config.dev.reloadCommand) { @@ -125,8 +123,6 @@ export async function generateManifest( if (wxt.config.command === 'serve') addDevModeCsp(manifest); if (wxt.config.command === 'serve') addDevModePermissions(manifest); - // TODO: Remove in v1 - wxt.config.transformManifest?.(manifest); await wxt.hooks.callHook('build:manifestGenerated', wxt, manifest); if (wxt.config.manifestVersion === 2) { @@ -177,7 +173,7 @@ function simplifyVersion(versionName: string): string { } function addEntrypoints( - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, entrypoints: Entrypoint[], buildOutput: Omit, ): void { @@ -239,7 +235,6 @@ function addEntrypoints( ); } else { manifest.chrome_url_overrides ??= {}; - // @ts-expect-error: bookmarks is untyped in webextension-polyfill, but supported by chrome manifest.chrome_url_overrides.bookmarks = getEntrypointBundlePath( bookmarks, wxt.config.outDir, @@ -255,7 +250,6 @@ function addEntrypoints( ); } else { manifest.chrome_url_overrides ??= {}; - // @ts-expect-error: history is untyped in webextension-polyfill, but supported by chrome manifest.chrome_url_overrides.history = getEntrypointBundlePath( history, wxt.config.outDir, @@ -279,12 +273,13 @@ function addEntrypoints( wxt.config.outDir, '.html', ); - const options: Manifest.ActionManifest = {}; + const options: Browser.runtime.ManifestAction = {}; if (popup.options.defaultIcon) options.default_icon = popup.options.defaultIcon; if (popup.options.defaultTitle) options.default_title = popup.options.defaultTitle; if (popup.options.browserStyle) + // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox options.browser_style = popup.options.browserStyle; if (manifest.manifest_version === 3) { manifest.action = { @@ -314,6 +309,7 @@ function addEntrypoints( const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html'); manifest.options_ui = { open_in_tab: options.options.openInTab, + // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox browser_style: wxt.config.browser === 'firefox' ? options.options.browserStyle @@ -332,7 +328,6 @@ function addEntrypoints( 'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest', ); } else { - // @ts-expect-error: sandbox not typed manifest.sandbox = { pages: sandboxes.map((entry) => getEntrypointBundlePath(entry, wxt.config.outDir, '.html'), @@ -359,7 +354,6 @@ function addEntrypoints( open_at_install: defaultSidepanel.options.openAtInstall, }; } else if (wxt.config.manifestVersion === 3) { - // @ts-expect-error: Untyped manifest.side_panel = { default_path: page, }; @@ -432,7 +426,7 @@ function addEntrypoints( function discoverIcons( buildOutput: Omit, -): Manifest.WebExtensionManifest['icons'] { +): Browser.runtime.Manifest['icons'] { const icons: [string, string][] = []; // prettier-ignore // #region snippet @@ -464,7 +458,7 @@ function discoverIcons( return icons.length > 0 ? Object.fromEntries(icons) : undefined; } -function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void { +function addDevModeCsp(manifest: Browser.runtime.Manifest): void { const permission = `http://${wxt.server?.hostname ?? ''}/*`; const allowedCsp = wxt.server?.origin ?? 'http://localhost:*'; @@ -499,7 +493,7 @@ function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void { manifest.content_security_policy.sandbox = sandboxCsp.toString(); } -function addDevModePermissions(manifest: Manifest.WebExtensionManifest) { +function addDevModePermissions(manifest: Browser.runtime.Manifest) { // For reloading the page addPermission(manifest, 'tabs'); @@ -543,8 +537,7 @@ export function getContentScriptCssWebAccessibleResources( contentScripts: ContentScriptEntrypoint[], contentScriptCssMap: Record, ): any[] { - const resources: Manifest.WebExtensionManifestWebAccessibleResourcesC2ItemType[] = - []; + const resources: ManifestV3WebAccessibleResource[] = []; contentScripts.forEach((script) => { if (script.options.cssInjectionMode !== 'ui') return; @@ -554,9 +547,10 @@ export function getContentScriptCssWebAccessibleResources( resources.push({ resources: [cssFile], - matches: script.options.matches?.map((matchPattern) => - stripPathFromMatchPattern(matchPattern), - ), + matches: + script.options.matches?.map((matchPattern) => + stripPathFromMatchPattern(matchPattern), + ) ?? [], }); }); @@ -583,16 +577,18 @@ export function getContentScriptsCssMap( } function addPermission( - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, permission: string, ): void { manifest.permissions ??= []; + // @ts-expect-error: Allow using strings for permissions for MV2 support if (manifest.permissions.includes(permission)) return; + // @ts-expect-error: Allow using strings for permissions for MV2 support manifest.permissions.push(permission); } function addHostPermission( - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, hostPermission: string, ): void { manifest.host_permissions ??= []; @@ -618,7 +614,7 @@ export function stripPathFromMatchPattern(pattern: string) { * targeting MV2, automatically convert their definitions down to the basic MV2 array. */ export function convertWebAccessibleResourcesToMv2( - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, ): void { if (manifest.web_accessible_resources == null) return; @@ -633,17 +629,17 @@ export function convertWebAccessibleResourcesToMv2( } function moveHostPermissionsToPermissions( - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, ): void { if (!manifest.host_permissions?.length) return; - manifest.host_permissions.forEach((permission) => + manifest.host_permissions.forEach((permission: string) => addPermission(manifest, permission), ); delete manifest.host_permissions; } -function convertActionToMv2(manifest: Manifest.WebExtensionManifest): void { +function convertActionToMv2(manifest: Browser.runtime.Manifest): void { if ( manifest.action == null || manifest.browser_action != null || @@ -654,7 +650,7 @@ function convertActionToMv2(manifest: Manifest.WebExtensionManifest): void { manifest.browser_action = manifest.action; } -function convertCspToMv2(manifest: Manifest.WebExtensionManifest): void { +function convertCspToMv2(manifest: Browser.runtime.Manifest): void { if ( typeof manifest.content_security_policy === 'string' || manifest.content_security_policy?.extension_pages == null @@ -668,8 +664,8 @@ function convertCspToMv2(manifest: Manifest.WebExtensionManifest): void { /** * Make sure all resources are in MV3 format. If not, add a wanring */ -export function validateMv3WebAccessibleResources( - manifest: Manifest.WebExtensionManifest, +function validateMv3WebAccessibleResources( + manifest: Browser.runtime.Manifest, ): void { if (manifest.web_accessible_resources == null) return; @@ -688,7 +684,7 @@ export function validateMv3WebAccessibleResources( /** * Remove keys from the manifest based on the build target. */ -function stripKeys(manifest: Manifest.WebExtensionManifest): void { +function stripKeys(manifest: Browser.runtime.Manifest): void { let keysToRemove: string[] = []; if (wxt.config.manifestVersion === 2) { keysToRemove.push(...mv3OnlyKeys); @@ -699,7 +695,7 @@ function stripKeys(manifest: Manifest.WebExtensionManifest): void { } keysToRemove.forEach((key) => { - delete manifest[key as keyof Manifest.WebExtensionManifest]; + delete manifest[key as keyof Browser.runtime.Manifest]; }); } diff --git a/packages/wxt/src/core/utils/strings.ts b/packages/wxt/src/core/utils/strings.ts index b48b7995c..bb3f264c4 100644 --- a/packages/wxt/src/core/utils/strings.ts +++ b/packages/wxt/src/core/utils/strings.ts @@ -34,14 +34,3 @@ export function removeImportStatements(text: string): string { '', ); } - -/** - * Removes imports, ensuring that some of WXT's client imports are present, so that entrypoints can be parsed if auto-imports are disabled. - */ -export function removeProjectImportStatements(text: string): string { - const noImports = removeImportStatements(text); - - return `import { defineUnlistedScript, defineContentScript, defineBackground } from 'wxt/sandbox'; - -${noImports}`; -} diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 0586d7fbc..d8527b7d3 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -4,7 +4,6 @@ import { resolve } from 'path'; import { faker } from '@faker-js/faker'; import merge from 'lodash.merge'; -import type { Manifest } from 'wxt/browser'; import { FsCache, ResolvedConfig, @@ -27,6 +26,7 @@ import { import { mock } from 'vitest-mock-extended'; import { vi } from 'vitest'; import { setWxtForTesting } from '../../../core/wxt'; +import type { Browser } from '@wxt-dev/browser'; faker.seed(import.meta.test.SEED); @@ -208,13 +208,11 @@ export function fakeOutputFile(): OutputFile { return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]); } -export const fakeManifest = fakeObjectCreator( - () => ({ - manifest_version: faker.helpers.arrayElement([2, 3]), - name: faker.string.alphanumeric(), - version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`, - }), -); +export const fakeManifest = fakeObjectCreator(() => ({ + manifest_version: faker.helpers.arrayElement([2, 3]), + name: faker.string.alphanumeric(), + version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`, +})); export const fakeUserManifest = fakeObjectCreator(() => ({ name: faker.string.alphanumeric(), @@ -245,6 +243,7 @@ export const fakeResolvedConfig = fakeObjectCreator(() => { env: { browser, command, manifestVersion, mode }, fsCache: mock(), imports: { + disabled: faker.datatype.boolean(), eslintrc: { enabled: faker.helpers.arrayElement([false, 8, 9]), filePath: fakeFile(), @@ -296,11 +295,8 @@ export const fakeResolvedConfig = fakeObjectCreator(() => { compressionLevel: 9, zipSources: false, }, - transformManifest: () => {}, userConfigMetadata: {}, alias: {}, - extensionApi: 'webextension-polyfill', - entrypointLoader: 'vite-node', experimental: {}, dev: { reloadCommand: 'Alt+R', @@ -356,8 +352,8 @@ export const fakeBuildStepOutput = fakeObjectCreator(() => ({ entrypoints: fakeArray(fakeEntrypoint), })); -export const fakeManifestCommand = - fakeObjectCreator(() => ({ +export const fakeManifestCommand = fakeObjectCreator( + () => ({ description: faker.string.sample(), suggested_key: { default: `${faker.helpers.arrayElement(['ctrl', 'alt'])}+${faker.number.int( @@ -367,7 +363,8 @@ export const fakeManifestCommand = }, )}`, }, - })); + }), +); export const fakeDevServer = fakeObjectCreator(() => ({ hostname: 'localhost', diff --git a/packages/wxt/src/core/utils/transform.ts b/packages/wxt/src/core/utils/transform.ts index f6951c614..857fdcff6 100644 --- a/packages/wxt/src/core/utils/transform.ts +++ b/packages/wxt/src/core/utils/transform.ts @@ -5,7 +5,7 @@ import { ProxifiedModule, parseModule } from 'magicast'; * 1. Removes or clears out `main` function from returned object * 2. Removes any unused functions/variables outside the definition that aren't being called/used * 3. Removes unused imports - * 3. Removes value-less, side-effect only imports (like `import "./styles.css"` or `import "webextension-polyfill"`) + * 3. Removes value-less, side-effect only imports (like `import "./styles.css"` or `import "polyfill"`) */ export function removeMainFunctionCode(code: string): { code: string; diff --git a/packages/wxt/src/core/utils/types.ts b/packages/wxt/src/core/utils/types.ts index ed6ff2000..9755088a6 100644 --- a/packages/wxt/src/core/utils/types.ts +++ b/packages/wxt/src/core/utils/types.ts @@ -1,3 +1,5 @@ +import type { Browser } from '@wxt-dev/browser'; + /** * Remove optional from key, but keep undefined if present * @@ -6,3 +8,11 @@ * // type Test = {a: string | undefined, b: number} */ export type NullablyRequired = { [K in keyof Required]: T[K] }; + +export type ManifestContentScript = NonNullable< + Browser.runtime.Manifest['content_scripts'] +>[number]; + +export type ManifestV3WebAccessibleResource = NonNullable< + Browser.runtime.ManifestV3['web_accessible_resources'] +>[number]; diff --git a/packages/wxt/src/index.ts b/packages/wxt/src/index.ts index 06ba588fd..4dcba2c67 100644 --- a/packages/wxt/src/index.ts +++ b/packages/wxt/src/index.ts @@ -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'; diff --git a/packages/wxt/src/modules.ts b/packages/wxt/src/modules.ts index d04c9b6ff..2cb12a071 100644 --- a/packages/wxt/src/modules.ts +++ b/packages/wxt/src/modules.ts @@ -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 ??= []; diff --git a/packages/wxt/src/sandbox/index.ts b/packages/wxt/src/sandbox/index.ts deleted file mode 100644 index f3b339bc4..000000000 --- a/packages/wxt/src/sandbox/index.ts +++ /dev/null @@ -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'; diff --git a/packages/wxt/src/storage.ts b/packages/wxt/src/storage.ts deleted file mode 100644 index cefb3a957..000000000 --- a/packages/wxt/src/storage.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * @module @wxt-dev/storage - */ -export * from '@wxt-dev/storage'; diff --git a/packages/wxt/src/testing/index.ts b/packages/wxt/src/testing/index.ts index ccb6f1f58..6de0fafd8 100644 --- a/packages/wxt/src/testing/index.ts +++ b/packages/wxt/src/testing/index.ts @@ -1,4 +1,5 @@ /** + * Utilities for unit testing WXT extensions. * @module wxt/testing */ export * from './fake-browser'; diff --git a/packages/wxt/src/testing/wxt-vitest-plugin.ts b/packages/wxt/src/testing/wxt-vitest-plugin.ts index de526cbe6..cd59f338a 100644 --- a/packages/wxt/src/testing/wxt-vitest-plugin.ts +++ b/packages/wxt/src/testing/wxt-vitest-plugin.ts @@ -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; } diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 187ecb1f5..e384718cb 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1,12 +1,13 @@ import type * as vite from 'vite'; -import type { Manifest, Scripting } from 'wxt/browser'; 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'; import type * as Nypm from 'nypm'; +import { ManifestContentScript } from './core/utils/types'; +import type { Browser } from '@wxt-dev/browser'; export interface InlineConfig { /** @@ -37,7 +38,7 @@ export interface InlineConfig { */ entrypointsDir?: string; /** - * @default "${config.srcDir}/modules" + * @default "${config.root}/modules" */ modulesDir?: string; /** @@ -62,8 +63,8 @@ export interface InlineConfig { * - `{{modeSuffix}}`: A suffix based on the mode ('-dev' for development, '' for production) * - `{{command}}`: The WXT command being run (e.g., 'build', 'serve') * - * @example "{{browser}}-mv{{manifestVersion}}{{modeSuffix}}" - * @default `"{{browser}}-mv{{manifestVersion}}"` + * @example "{{browser}}-mv{{manifestVersion}}" + * @default `"{{browser}}-mv{{manifestVersion}}{{modeSuffix}}"` */ outDirTemplate?: string; /** @@ -125,9 +126,13 @@ export interface InlineConfig { */ manifest?: UserManifest | Promise | UserManifestFn; /** - * Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file. + * Configure browser startup. Options set here can be overridden in a `web-ext.config.ts` file. */ - runner?: ExtensionRunnerConfig; + webExt?: WebExtConfig; + /** + * @deprecated Use `webExt` instead. Same option, just renamed. + */ + runner?: WebExtConfig; zip?: { /** * Configure the filename output when zipping files. @@ -248,26 +253,6 @@ export interface InlineConfig { */ compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; }; - - /** - * @deprecated Use `hooks.build.manifestGenerated` to modify your manifest instead. This option - * will be removed in v1.0 - * - * Transform the final manifest before it's written to the file system. Edit the `manifest` - * parameter directly, do not return a new object. Return values are ignored. - * - * @example - * defineConfig({ - * // Add a CSS-only content script. - * transformManifest(manifest) { - * manifest.content_scripts.push({ - * matches: ["*://google.com/*"], - * css: ["content-scripts/some-example.css"], - * }); - * } - * }) - */ - transformManifest?: (manifest: Manifest.WebExtensionManifest) => void; analysis?: { /** * Explicitly include bundle analysis when running `wxt build`. This can be overridden by the @@ -325,30 +310,6 @@ export interface InlineConfig { * } */ alias?: Record; - /** - * Which extension API to use. - * - * - `"webextension-polyfill"`: Use `browser` and types from [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill). - * - `"chrome"`: Use the regular `chrome` (or `browser` for Firefox/Safari) globals provided by the browser. Types provided by [`@types/chrome`](https://www.npmjs.com/package/@types/chrome). - * - * @default "webextension-polyfill" - * @since 0.19.0 - */ - extensionApi?: 'webextension-polyfill' | 'chrome'; - /** - * @deprecated Will be removed in v0.20.0, please migrate to using `vite-node`, the new default. - * - * Method used to import entrypoint files during the build process to extract their options. - * - * - `"vite-node"` (default as of 0.19.0): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file - * - `"jiti"`: Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function - * - * @see {@link https://wxt.dev/guide/go-further/entrypoint-importers.html|Entrypoint Importers} - * - * @default "vite-node" - * @since 0.19.0 - */ - entrypointLoader?: 'vite-node' | 'jiti'; /** * Experimental settings - use with caution. */ @@ -444,7 +405,7 @@ export interface WxtHooks { } export interface BuildOutput { - manifest: Manifest.WebExtensionManifest; + manifest: Browser.runtime.Manifest; publicAssets: OutputAsset[]; steps: BuildStepOutput[]; } @@ -537,7 +498,7 @@ export interface WxtDevServer export interface ReloadContentScriptPayload { registration?: BaseContentScriptEntrypointOptions['registration']; - contentScript: Omit; + contentScript: Omit; } export type TargetBrowser = string; @@ -588,39 +549,39 @@ export interface BackgroundEntrypointOptions extends BaseEntrypointOptions { export interface BaseContentScriptEntrypointOptions extends BaseEntrypointOptions { - matches?: PerBrowserOption; + matches?: PerBrowserOption>; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default "documentIdle" */ - runAt?: PerBrowserOption; + runAt?: PerBrowserOption; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default false */ matchAboutBlank?: PerBrowserOption< - Manifest.ContentScript['match_about_blank'] + ManifestContentScript['match_about_blank'] >; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default [] */ - excludeMatches?: PerBrowserOption; + excludeMatches?: PerBrowserOption; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default [] */ - includeGlobs?: PerBrowserOption; + includeGlobs?: PerBrowserOption; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default [] */ - excludeGlobs?: PerBrowserOption; + excludeGlobs?: PerBrowserOption; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default false */ - allFrames?: PerBrowserOption; + allFrames?: PerBrowserOption; /** * See https://developer.chrome.com/docs/extensions/mv3/content_scripts/ * @default false @@ -868,7 +829,7 @@ export type ResolvedPerBrowserOptions = { * here, they are configured inline. */ export type UserManifest = { - [key in keyof chrome.runtime.ManifestV3 as key extends + [key in keyof Browser.runtime.ManifestV3 as key extends | 'action' | 'background' | 'chrome_url_overrides' @@ -880,16 +841,16 @@ export type UserManifest = { | 'sandbox' | 'web_accessible_resources' ? never - : key]?: chrome.runtime.ManifestV3[key]; + : key]?: Browser.runtime.ManifestV3[key]; } & { // Add any Browser-specific or MV2 properties that WXT supports here - action?: chrome.runtime.ManifestV3['action'] & { + action?: Browser.runtime.ManifestV3['action'] & { browser_style?: boolean; }; - browser_action?: chrome.runtime.ManifestV2['browser_action'] & { + browser_action?: Browser.runtime.ManifestV2['browser_action'] & { browser_style?: boolean; }; - page_action?: chrome.runtime.ManifestV2['page_action'] & { + page_action?: Browser.runtime.ManifestV2['page_action'] & { browser_style?: boolean; }; browser_specific_settings?: { @@ -909,12 +870,12 @@ export type UserManifest = { }; }; permissions?: ( - | chrome.runtime.ManifestPermissions + | Browser.runtime.ManifestPermissions | (string & Record) )[]; web_accessible_resources?: | string[] - | chrome.runtime.ManifestV3['web_accessible_resources']; + | Browser.runtime.ManifestV3['web_accessible_resources']; }; export type UserManifestFn = ( @@ -946,9 +907,14 @@ export interface ConfigEnv { export type WxtCommand = 'build' | 'serve'; /** - * Configure how the browser starts up. + * @deprecated Use `WebExtConfig` instead. + */ +export type ExtensionRunnerConfig = WebExtConfig; + +/** + * Options for how [`web-ext`](https://github.com/mozilla/web-ext) starts the browser. */ -export interface ExtensionRunnerConfig { +export interface WebExtConfig { /** * Whether or not to open the browser with the extension installed in dev mode. * @@ -1187,7 +1153,7 @@ export interface WxtHooks { */ 'build:manifestGenerated': ( wxt: Wxt, - manifest: Manifest.WebExtensionManifest, + manifest: Browser.runtime.Manifest, ) => HookResult; /** * Called once the names and paths of all entrypoints have been resolved. @@ -1336,10 +1302,10 @@ export interface ResolvedConfig { manifestVersion: TargetManifestVersion; env: ConfigEnv; logger: Logger; - imports: false | WxtResolvedUnimportOptions; + imports: WxtResolvedUnimportOptions; manifest: UserManifest; fsCache: FsCache; - runnerConfig: C12ResolvedConfig; + runnerConfig: C12ResolvedConfig; zip: { name?: string; artifactTemplate: string; @@ -1356,10 +1322,6 @@ export interface ResolvedConfig { */ zipSources: boolean; }; - /** - * @deprecated Use `build:manifestGenerated` hook instead. - */ - transformManifest?: (manifest: Manifest.WebExtensionManifest) => void; analysis: { enabled: boolean; open: boolean; @@ -1377,8 +1339,6 @@ export interface ResolvedConfig { * Import aliases to absolute paths. */ alias: Record; - extensionApi: 'webextension-polyfill' | 'chrome'; - entrypointLoader: 'vite-node' | 'jiti'; experimental: {}; dev: { /** Only defined during dev command */ @@ -1486,6 +1446,12 @@ export type WxtUnimportOptions = Partial & { }; export type WxtResolvedUnimportOptions = Partial & { + /** + * 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; }; diff --git a/packages/wxt/src/utils/README.md b/packages/wxt/src/utils/README.md new file mode 100644 index 000000000..1d9f1720d --- /dev/null +++ b/packages/wxt/src/utils/README.md @@ -0,0 +1 @@ +This folder is for public utils, not internal utils. Put generic helpers and other utils in the core/utils folder. diff --git a/packages/wxt/src/client/content-scripts/__tests__/content-script-context.test.ts b/packages/wxt/src/utils/__tests__/content-script-context.test.ts similarity index 97% rename from packages/wxt/src/client/content-scripts/__tests__/content-script-context.test.ts rename to packages/wxt/src/utils/__tests__/content-script-context.test.ts index 23ff12e89..f7a5ffc86 100644 --- a/packages/wxt/src/client/content-scripts/__tests__/content-script-context.test.ts +++ b/packages/wxt/src/utils/__tests__/content-script-context.test.ts @@ -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'; /** @@ -24,7 +24,7 @@ describe('Content Script Context', () => { const onInvalidated = vi.fn(); ctx.onInvalidated(onInvalidated); - // @ts-expect-error + // @ts-ignore delete fakeBrowser.runtime.id; const isValid = ctx.isValid; diff --git a/packages/wxt/src/sandbox/__tests__/define-background.test.ts b/packages/wxt/src/utils/__tests__/define-background.test.ts similarity index 100% rename from packages/wxt/src/sandbox/__tests__/define-background.test.ts rename to packages/wxt/src/utils/__tests__/define-background.test.ts diff --git a/packages/wxt/src/sandbox/__tests__/define-content-script.test.ts b/packages/wxt/src/utils/__tests__/define-content-script.test.ts similarity index 100% rename from packages/wxt/src/sandbox/__tests__/define-content-script.test.ts rename to packages/wxt/src/utils/__tests__/define-content-script.test.ts diff --git a/packages/wxt/src/sandbox/__tests__/define-unlisted-script.test.ts b/packages/wxt/src/utils/__tests__/define-unlisted-script.test.ts similarity index 100% rename from packages/wxt/src/sandbox/__tests__/define-unlisted-script.test.ts rename to packages/wxt/src/utils/__tests__/define-unlisted-script.test.ts diff --git a/packages/wxt/src/client/app-config.ts b/packages/wxt/src/utils/app-config.ts similarity index 61% rename from packages/wxt/src/client/app-config.ts rename to packages/wxt/src/utils/app-config.ts index 482d345f0..7a22143fc 100644 --- a/packages/wxt/src/client/app-config.ts +++ b/packages/wxt/src/utils/app-config.ts @@ -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; diff --git a/packages/wxt/src/client/content-scripts/content-script-context.ts b/packages/wxt/src/utils/content-script-context.ts similarity index 95% rename from packages/wxt/src/client/content-scripts/content-script-context.ts rename to packages/wxt/src/utils/content-script-context.ts index 48165b610..5a4880197 100644 --- a/packages/wxt/src/client/content-scripts/content-script-context.ts +++ b/packages/wxt/src/utils/content-script-context.ts @@ -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'); diff --git a/packages/wxt/src/client/content-scripts/ui/__tests__/index.test.ts b/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts similarity index 98% rename from packages/wxt/src/client/content-scripts/ui/__tests__/index.test.ts rename to packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts index 4913265f5..57972a22f 100644 --- a/packages/wxt/src/client/content-scripts/ui/__tests__/index.test.ts +++ b/packages/wxt/src/utils/content-script-ui/__tests__/index.test.ts @@ -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. diff --git a/packages/wxt/src/utils/content-script-ui/iframe.ts b/packages/wxt/src/utils/content-script-ui/iframe.ts new file mode 100644 index 000000000..65968ea38 --- /dev/null +++ b/packages/wxt/src/utils/content-script-ui/iframe.ts @@ -0,0 +1,76 @@ +/** @module wxt/utils/content-script-ui/iframe */ +import { browser } from 'wxt/browser'; +import { ContentScriptContext } from '../content-script-context'; +import type { ContentScriptUi, ContentScriptUiOptions } 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( + ctx: ContentScriptContext, + options: IframeContentScriptUiOptions, +): IframeContentScriptUi { + 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, + }; +} + +export interface IframeContentScriptUi + extends ContentScriptUi { + /** + * The iframe added to the DOM. + */ + iframe: HTMLIFrameElement; + /** + * A wrapper div that assists in positioning. + */ + wrapper: HTMLDivElement; +} + +export type IframeContentScriptUiOptions = + ContentScriptUiOptions & { + /** + * The path to the HTML page that will be shown in the iframe. This string is passed into + * `browser.runtime.getURL`. + */ + // @ts-expect-error: HtmlPublicPath is generated per-project + page: import('wxt/browser').HtmlPublicPath; + /** + * Callback executed when mounting the UI. Use this function to customize the iframe or wrapper + * element's appearance. It is called every time `ui.mount()` is called. + * + * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback. + */ + onMount?: (wrapper: HTMLElement, iframe: HTMLIFrameElement) => TMounted; + }; diff --git a/packages/wxt/src/utils/content-script-ui/integrated.ts b/packages/wxt/src/utils/content-script-ui/integrated.ts new file mode 100644 index 000000000..85574d631 --- /dev/null +++ b/packages/wxt/src/utils/content-script-ui/integrated.ts @@ -0,0 +1,77 @@ +/** @module wxt/utils/content-script-ui/integrated */ +import { ContentScriptContext } from '../content-script-context'; +import type { ContentScriptUi, ContentScriptUiOptions } 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( + ctx: ContentScriptContext, + options: IntegratedContentScriptUiOptions, +): IntegratedContentScriptUi { + 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, + }; +} + +/** + * Shared types for the different `wxt/utils/content-script-ui/*` modules. + * @module wxt/utils/content-script-ui/types + */ +export interface IntegratedContentScriptUi + extends ContentScriptUi { + /** + * A wrapper div that assists in positioning. + */ + wrapper: HTMLElement; +} + +export type IntegratedContentScriptUiOptions = + ContentScriptUiOptions & { + /** + * Tag used to create the wrapper element. + * + * @default "div" + */ + tag?: string; + /** + * Callback executed when mounting the UI. This function should create and append the UI to the + * `wrapper` element. It is called every time `ui.mount()` is called. + * + * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback. + */ + onMount: (wrapper: HTMLElement) => TMounted; + }; diff --git a/packages/wxt/src/utils/content-script-ui/shadow-root.ts b/packages/wxt/src/utils/content-script-ui/shadow-root.ts new file mode 100644 index 000000000..a8041a88f --- /dev/null +++ b/packages/wxt/src/utils/content-script-ui/shadow-root.ts @@ -0,0 +1,182 @@ +/** @module wxt/utils/content-script-ui/shadow-root */ +import { browser } from 'wxt/browser'; +import { ContentScriptContext } from '../content-script-context'; +import type { ContentScriptUi, ContentScriptUiOptions } 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( + ctx: ContentScriptContext, + options: ShadowRootContentScriptUiOptions, +): Promise> { + const css: string[] = []; + + if (!options.inheritStyles) { + css.push(`/* WXT Shadow Root Reset */ body{all:initial;}`); + } + if (options.css) { + css.push(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 { + const url = browser.runtime + // @ts-expect-error: getURL is defined per-project, but not inside the package + .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 ''; + } +} + +export interface ShadowRootContentScriptUi + extends ContentScriptUi { + /** + * The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element + * that get's added to the DOM. This element's style is not isolated from the webpage. + */ + shadowHost: HTMLElement; + /** + * The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted + * inside this `HTMLElement`. + */ + uiContainer: HTMLElement; + /** + * The shadow root performing the isolation. + */ + shadow: ShadowRoot; +} + +export type ShadowRootContentScriptUiOptions = + ContentScriptUiOptions & { + /** + * The name of the custom component used to host the ShadowRoot. Must be kebab-case. + */ + name: string; + /** + * Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've + * set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need + * to pass those styles in here. This is for any additional styles not in the imported CSS. + */ + css?: string; + /** + * ShadowRoot's mode. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode + * @default "open" + */ + mode?: 'open' | 'closed'; + /** + * When enabled, `event.stopPropagation` will be called on events trying to bubble out of the + * shadow root. + * + * - Set to `true` to stop the propagation of a default set of events, + * `["keyup", "keydown", "keypress"]` + * - Set to an array of event names to stop the propagation of a custom list of events + */ + isolateEvents?: boolean | string[]; + /** + * By default, WXT adds `all: initial` to the shadow root before the rest of + * your CSS. This resets any inheritable CSS styles that + * [normally pierce the Shadow DOM](https://open-wc.org/guides/knowledge/styling/styles-piercing-shadow-dom/). + * + * WXT resets everything but: + * - **`rem` Units**: they continue to scale based off the webpage's HTML `font-size`. + * - **CSS Variables/Custom Properties**: CSS variables defined outside the shadow root can be accessed inside it. + * - **`@font-face` Definitions**: Fonts defined outside the shadow root can be used inside it. + * + * To disable this behavior and inherit styles from the webpage, set `inheritStyles: true`. + * + * @default false + */ + inheritStyles?: boolean; + /** + * Callback executed when mounting the UI. This function should create and append the UI to the + * `uiContainer` element. It is called every time `ui.mount()` is called. + * + * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback. + */ + onMount: ( + uiContainer: HTMLElement, + shadow: ShadowRoot, + shadowHost: HTMLElement, + ) => TMounted; + }; diff --git a/packages/wxt/src/client/content-scripts/ui/index.ts b/packages/wxt/src/utils/content-script-ui/shared.ts similarity index 51% rename from packages/wxt/src/client/content-scripts/ui/index.ts rename to packages/wxt/src/utils/content-script-ui/shared.ts index 49182bb40..c23b52eb9 100644 --- a/packages/wxt/src/client/content-scripts/ui/index.ts +++ b/packages/wxt/src/utils/content-script-ui/shared.ts @@ -1,197 +1,20 @@ -import { browser } from 'wxt/browser'; -import { waitElement } from '@1natsu/wait-element'; -import { - isExist as mountDetector, - isNotExist as removeDetector, -} from '@1natsu/wait-element/detectors'; -import { ContentScriptContext } from '..'; -import { +import type { + ContentScriptAnchoredOptions, + ContentScriptPositioningOptions, 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'; - -/** - * Create a content script UI without any isolation. - * - * @see https://wxt.dev/guide/essentials/content-scripts.html#integrated - */ -export function createIntegratedUi( - ctx: ContentScriptContext, - options: IntegratedContentScriptUiOptions, -): IntegratedContentScriptUi { - 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( - ctx: ContentScriptContext, - options: IframeContentScriptUiOptions, -): IframeContentScriptUi { - 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( - ctx: ContentScriptContext, - options: ShadowRootContentScriptUiOptions, -): Promise> { - 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; - }, - }; -} +import { waitElement } from '@1natsu/wait-element'; +import { + isExist as mountDetector, + isNotExist as removeDetector, +} from '@1natsu/wait-element/detectors'; +import { logger } from '../../utils/internal/logger'; -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( +export function createMountFunctions( baseFunctions: BaseMountFunctions, options: ContentScriptUiOptions, ): MountFunctions { @@ -392,23 +217,3 @@ function autoMountUi( return { stopAutoMount: _stopAutoMount }; } - -/** - * Load the CSS for the current entrypoint. - */ -async function loadCss(): Promise { - // @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 ''; - } -} diff --git a/packages/wxt/src/client/content-scripts/ui/types.ts b/packages/wxt/src/utils/content-script-ui/types.ts similarity index 50% rename from packages/wxt/src/client/content-scripts/ui/types.ts rename to packages/wxt/src/utils/content-script-ui/types.ts index 7e4da3ff6..b8fdd9b2f 100644 --- a/packages/wxt/src/client/content-scripts/ui/types.ts +++ b/packages/wxt/src/utils/content-script-ui/types.ts @@ -1,40 +1,4 @@ -export interface IntegratedContentScriptUi - extends ContentScriptUi { - /** - * A wrapper div that assists in positioning. - */ - wrapper: HTMLElement; -} - -export interface IframeContentScriptUi - extends ContentScriptUi { - /** - * The iframe added to the DOM. - */ - iframe: HTMLIFrameElement; - /** - * A wrapper div that assists in positioning. - */ - wrapper: HTMLDivElement; -} - -export interface ShadowRootContentScriptUi - extends ContentScriptUi { - /** - * The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element - * that get's added to the DOM. This element's style is not isolated from the webpage. - */ - shadowHost: HTMLElement; - /** - * The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted - * inside this `HTMLElement`. - */ - uiContainer: HTMLElement; - /** - * The shadow root performing the isolation. - */ - shadow: ShadowRoot; -} +/** @module wxt/utils/content-script-ui/types */ export interface ContentScriptUi extends MountFunctions { mounted: TMounted | undefined; @@ -49,81 +13,6 @@ export type ContentScriptUiOptions = ContentScriptPositioningOptions & onRemove?: (mounted: TMounted | undefined) => void; }; -export type IntegratedContentScriptUiOptions = - ContentScriptUiOptions & { - /** - * Tag used to create the wrapper element. - * - * @default "div" - */ - tag?: string; - /** - * Callback executed when mounting the UI. This function should create and append the UI to the - * `wrapper` element. It is called every time `ui.mount()` is called. - * - * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback. - */ - onMount: (wrapper: HTMLElement) => TMounted; - }; - -export type IframeContentScriptUiOptions = - ContentScriptUiOptions & { - /** - * The path to the HTML page that will be shown in the iframe. This string is passed into - * `browser.runtime.getURL`. - */ - // @ts-expect-error: HtmlPublicPath is generated per-project - page: import('wxt/browser').HtmlPublicPath; - /** - * Callback executed when mounting the UI. Use this function to customize the iframe or wrapper - * element's appearance. It is called every time `ui.mount()` is called. - * - * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback. - */ - onMount?: (wrapper: HTMLElement, iframe: HTMLIFrameElement) => TMounted; - }; - -export type ShadowRootContentScriptUiOptions = - ContentScriptUiOptions & { - /** - * The name of the custom component used to host the ShadowRoot. Must be kebab-case. - */ - name: string; - /** - * Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've - * set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need - * to pass those styles in here. This is for any additional styles not in the imported CSS. - */ - css?: string; - /** - * ShadowRoot's mode. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode - * @default "open" - */ - mode?: 'open' | 'closed'; - /** - * When enabled, `event.stopPropagation` will be called on events trying to bubble out of the - * shadow root. - * - * - Set to `true` to stop the propagation of a default set of events, - * `["keyup", "keydown", "keypress"]` - * - Set to an array of event names to stop the propagation of a custom list of events - */ - isolateEvents?: boolean | string[]; - /** - * Callback executed when mounting the UI. This function should create and append the UI to the - * `uiContainer` element. It is called every time `ui.mount()` is called. - * - * Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback. - */ - onMount: ( - uiContainer: HTMLElement, - shadow: ShadowRoot, - shadowHost: HTMLElement, - ) => TMounted; - }; - export type ContentScriptOverlayAlignment = | 'top-left' | 'top-right' diff --git a/packages/wxt/src/sandbox/define-app-config.ts b/packages/wxt/src/utils/define-app-config.ts similarity index 78% rename from packages/wxt/src/sandbox/define-app-config.ts rename to packages/wxt/src/utils/define-app-config.ts index 164264c02..57db98e8d 100644 --- a/packages/wxt/src/sandbox/define-app-config.ts +++ b/packages/wxt/src/utils/define-app-config.ts @@ -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 * } diff --git a/packages/wxt/src/sandbox/define-background.ts b/packages/wxt/src/utils/define-background.ts similarity index 90% rename from packages/wxt/src/sandbox/define-background.ts rename to packages/wxt/src/utils/define-background.ts index bfc074bd7..18a8c92ee 100644 --- a/packages/wxt/src/sandbox/define-background.ts +++ b/packages/wxt/src/utils/define-background.ts @@ -1,3 +1,4 @@ +/** @module wxt/utils/define-background */ import type { BackgroundDefinition } from '../types'; export function defineBackground(main: () => void): BackgroundDefinition; diff --git a/packages/wxt/src/sandbox/define-content-script.ts b/packages/wxt/src/utils/define-content-script.ts similarity index 79% rename from packages/wxt/src/sandbox/define-content-script.ts rename to packages/wxt/src/utils/define-content-script.ts index de3ac5c15..4d250cc53 100644 --- a/packages/wxt/src/sandbox/define-content-script.ts +++ b/packages/wxt/src/utils/define-content-script.ts @@ -1,3 +1,4 @@ +/** @module wxt/utils/define-content-script */ import type { ContentScriptDefinition } from '../types'; export function defineContentScript( diff --git a/packages/wxt/src/sandbox/define-unlisted-script.ts b/packages/wxt/src/utils/define-unlisted-script.ts similarity index 90% rename from packages/wxt/src/sandbox/define-unlisted-script.ts rename to packages/wxt/src/utils/define-unlisted-script.ts index 4716aaa2c..6ee1dbba4 100644 --- a/packages/wxt/src/sandbox/define-unlisted-script.ts +++ b/packages/wxt/src/utils/define-unlisted-script.ts @@ -1,3 +1,4 @@ +/** @module wxt/utils/define-unlisted-script */ import type { UnlistedScriptDefinition } from '../types'; export function defineUnlistedScript( diff --git a/packages/wxt/src/sandbox/define-wxt-plugin.ts b/packages/wxt/src/utils/define-wxt-plugin.ts similarity index 74% rename from packages/wxt/src/sandbox/define-wxt-plugin.ts rename to packages/wxt/src/utils/define-wxt-plugin.ts index f2f9e8a4a..ed487271a 100644 --- a/packages/wxt/src/sandbox/define-wxt-plugin.ts +++ b/packages/wxt/src/utils/define-wxt-plugin.ts @@ -1,3 +1,4 @@ +/** @module wxt/utils/define-wxt-plugin */ import type { WxtPlugin } from '../types'; export function defineWxtPlugin(plugin: WxtPlugin): WxtPlugin { diff --git a/packages/wxt/src/client/inject-script.ts b/packages/wxt/src/utils/inject-script.ts similarity index 97% rename from packages/wxt/src/client/inject-script.ts rename to packages/wxt/src/utils/inject-script.ts index 989ba2482..2043e2da8 100644 --- a/packages/wxt/src/client/inject-script.ts +++ b/packages/wxt/src/utils/inject-script.ts @@ -1,3 +1,4 @@ +/** @module wxt/utils/inject-script */ import { browser } from 'wxt/browser'; export type ScriptPublicPath = Extract< diff --git a/packages/wxt/src/client/content-scripts/custom-events.ts b/packages/wxt/src/utils/internal/custom-events.ts similarity index 100% rename from packages/wxt/src/client/content-scripts/custom-events.ts rename to packages/wxt/src/utils/internal/custom-events.ts diff --git a/packages/wxt/src/sandbox/dev-server-websocket.ts b/packages/wxt/src/utils/internal/dev-server-websocket.ts similarity index 98% rename from packages/wxt/src/sandbox/dev-server-websocket.ts rename to packages/wxt/src/utils/internal/dev-server-websocket.ts index 91961303f..9e2abae79 100644 --- a/packages/wxt/src/sandbox/dev-server-websocket.ts +++ b/packages/wxt/src/utils/internal/dev-server-websocket.ts @@ -1,4 +1,4 @@ -import { logger } from './utils/logger'; +import { logger } from './logger'; interface WebSocketMessage { type: string; diff --git a/packages/wxt/src/client/content-scripts/location-watcher.ts b/packages/wxt/src/utils/internal/location-watcher.ts similarity index 92% rename from packages/wxt/src/client/content-scripts/location-watcher.ts rename to packages/wxt/src/utils/internal/location-watcher.ts index d0ae27695..085844e8d 100644 --- a/packages/wxt/src/client/content-scripts/location-watcher.ts +++ b/packages/wxt/src/utils/internal/location-watcher.ts @@ -1,4 +1,4 @@ -import { ContentScriptContext } from '.'; +import { ContentScriptContext } from '../content-script-context'; import { WxtLocationChangeEvent } from './custom-events'; /** diff --git a/packages/wxt/src/sandbox/utils/logger.ts b/packages/wxt/src/utils/internal/logger.ts similarity index 100% rename from packages/wxt/src/sandbox/utils/logger.ts rename to packages/wxt/src/utils/internal/logger.ts diff --git a/packages/wxt/src/utils/match-patterns.ts b/packages/wxt/src/utils/match-patterns.ts new file mode 100644 index 000000000..ffe24d5e0 --- /dev/null +++ b/packages/wxt/src/utils/match-patterns.ts @@ -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'; diff --git a/packages/wxt/src/utils/storage.ts b/packages/wxt/src/utils/storage.ts new file mode 100644 index 000000000..ea9bb0691 --- /dev/null +++ b/packages/wxt/src/utils/storage.ts @@ -0,0 +1,5 @@ +/** + * Re-export the [`@wxt-dev/storage` package](https://www.npmjs.com/package/@wxt-dev/storage). + * @module wxt/utils/storage + */ +export * from '@wxt-dev/storage'; diff --git a/packages/wxt/src/virtual/background-entrypoint.ts b/packages/wxt/src/virtual/background-entrypoint.ts index b25bac9af..44c4be0ad 100644 --- a/packages/wxt/src/virtual/background-entrypoint.ts +++ b/packages/wxt/src/virtual/background-entrypoint.ts @@ -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'; diff --git a/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts b/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts index a80399d3e..4bd388331 100644 --- a/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts +++ b/packages/wxt/src/virtual/content-script-isolated-world-entrypoint.ts @@ -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 () => { diff --git a/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts b/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts index 15db16d3c..1241bad64 100644 --- a/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts +++ b/packages/wxt/src/virtual/content-script-main-world-entrypoint.ts @@ -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 () => { diff --git a/packages/wxt/src/virtual/reload-html.ts b/packages/wxt/src/virtual/reload-html.ts index 12fa49471..3f3742bb2 100644 --- a/packages/wxt/src/virtual/reload-html.ts +++ b/packages/wxt/src/virtual/reload-html.ts @@ -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 { diff --git a/packages/wxt/src/virtual/unlisted-script-entrypoint.ts b/packages/wxt/src/virtual/unlisted-script-entrypoint.ts index a2a65bbb9..843d9ac46 100644 --- a/packages/wxt/src/virtual/unlisted-script-entrypoint.ts +++ b/packages/wxt/src/virtual/unlisted-script-entrypoint.ts @@ -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 () => { diff --git a/packages/wxt/src/virtual/utils/reload-content-scripts.ts b/packages/wxt/src/virtual/utils/reload-content-scripts.ts index 11b5e0c87..0caa3570c 100644 --- a/packages/wxt/src/virtual/utils/reload-content-scripts.ts +++ b/packages/wxt/src/virtual/utils/reload-content-scripts.ts @@ -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(); @@ -84,7 +84,7 @@ async function reloadTabsForContentScript(contentScript: ContentScript) { await Promise.all( matchingTabs.map(async (tab) => { try { - await browser.tabs.reload(tab.id); + await browser.tabs.reload(tab.id!); } catch (err) { logger.warn('Failed to reload tab:', err); } diff --git a/packages/wxt/src/virtual/virtual-module-globals.d.ts b/packages/wxt/src/virtual/virtual-module-globals.d.ts index 5199a83a9..0b70bcf19 100644 --- a/packages/wxt/src/virtual/virtual-module-globals.d.ts +++ b/packages/wxt/src/virtual/virtual-module-globals.d.ts @@ -21,7 +21,7 @@ declare module 'virtual:user-unlisted-script-entrypoint' { } declare module 'wxt/browser' { - export const browser: import('webextension-polyfill').Browser; + export { browser, Browser } from '@wxt-dev/browser'; } declare module 'virtual:wxt-plugins' { diff --git a/packages/wxt/typedoc.json b/packages/wxt/typedoc.json index d9edaddfc..25e53c161 100644 --- a/packages/wxt/typedoc.json +++ b/packages/wxt/typedoc.json @@ -1,12 +1,22 @@ { "entryPoints": [ - "src/client/index.ts", - "src/testing/index.ts", - "src/sandbox/index.ts", - "src/browser/index.ts", - "src/browser/chrome.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/utils/storage.ts", + "src/browser.ts", + "src/testing/index.ts", + "src/modules.ts" ] } diff --git a/packages/wxt/vitest.config.ts b/packages/wxt/vitest.config.ts index 8bb1d0dab..0c25b9629 100644 --- a/packages/wxt/vitest.config.ts +++ b/packages/wxt/vitest.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ include: ['src/**'], exclude: ['**/dist', '**/__tests__', 'src/utils/testing'], }, + setupFiles: ['./vitest.setup.ts'], globalSetup: ['./vitest.globalSetup.ts'], }, server: { @@ -22,7 +23,6 @@ export default defineConfig({ resolve: { alias: { 'wxt/testing': path.resolve('src/testing'), - 'webextension-polyfill': path.resolve('src/virtual/mock-browser'), }, }, }); diff --git a/packages/wxt/vitest.setup.ts b/packages/wxt/vitest.setup.ts new file mode 100644 index 000000000..a828762f2 --- /dev/null +++ b/packages/wxt/vitest.setup.ts @@ -0,0 +1,5 @@ +import { fakeBrowser } from '@webext-core/fake-browser'; +import { vi } from 'vitest'; + +vi.stubGlobal('chrome', fakeBrowser); +vi.stubGlobal('browser', fakeBrowser); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d461ccb5e..811011a5d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,7 +59,7 @@ catalogs: version: 0.7.39 '@types/webextension-polyfill': specifier: ^0.12.1 - version: 0.12.1 + version: 0.12.3 '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.3.4 @@ -123,6 +123,9 @@ catalogs: fast-glob: specifier: ^3.3.2 version: 3.3.2 + feed: + specifier: ^4.2.2 + version: 4.2.2 filesize: specifier: ^10.1.6 version: 10.1.6 @@ -147,9 +150,6 @@ catalogs: is-wsl: specifier: ^3.1.0 version: 3.1.0 - jiti: - specifier: ^2.4.2 - version: 2.4.2 json5: specifier: ^2.2.3 version: 2.2.3 @@ -337,6 +337,9 @@ importers: fast-glob: specifier: 'catalog:' version: 3.3.2 + feed: + specifier: 'catalog:' + version: 4.2.2 fs-extra: specifier: 'catalog:' version: 11.2.0 @@ -690,6 +693,30 @@ importers: specifier: workspace:* version: link:../wxt + packages/webextension-polyfill: + devDependencies: + '@aklinker1/check': + specifier: 'catalog:' + version: 1.4.5(typescript@5.6.3) + '@types/webextension-polyfill': + specifier: 'catalog:' + version: 0.12.3 + publint: + specifier: 'catalog:' + version: 0.2.12 + typescript: + specifier: 'catalog:' + version: 5.6.3 + unbuild: + specifier: 'catalog:' + version: 3.5.0(sass@1.80.7)(typescript@5.6.3)(vue@3.5.13(typescript@5.6.3)) + webextension-polyfill: + specifier: 'catalog:' + version: 0.12.0 + wxt: + specifier: workspace:* + version: link:../wxt + packages/wxt: dependencies: '@1natsu/wait-element': @@ -698,12 +725,6 @@ importers: '@aklinker1/rollup-plugin-visualizer': specifier: 'catalog:' version: 5.12.0(rollup@4.34.9) - '@types/chrome': - specifier: 'catalog:' - version: 0.0.280 - '@types/webextension-polyfill': - specifier: 'catalog:' - version: 0.12.1 '@webext-core/fake-browser': specifier: 'catalog:' version: 1.3.1 @@ -713,6 +734,9 @@ importers: '@webext-core/match-patterns': specifier: 'catalog:' version: 1.0.3 + '@wxt-dev/browser': + specifier: workspace:* + version: link:../browser '@wxt-dev/storage': specifier: workspace:^1.0.0 version: link:../storage @@ -770,9 +794,6 @@ importers: is-wsl: specifier: 'catalog:' version: 3.1.0 - jiti: - specifier: 'catalog:' - version: 2.4.2 json5: specifier: 'catalog:' version: 2.2.3 @@ -833,9 +854,6 @@ importers: web-ext-run: specifier: 'catalog:' version: 0.2.1 - webextension-polyfill: - specifier: 'catalog:' - version: 0.12.0 devDependencies: '@aklinker1/check': specifier: 'catalog:' @@ -898,9 +916,6 @@ importers: specifier: 'catalog:' version: 19.0.0(react@19.0.0) devDependencies: - '@types/chrome': - specifier: 'catalog:' - version: 0.0.280 '@types/react': specifier: 'catalog:' version: 19.0.1 @@ -2324,8 +2339,8 @@ packages: '@types/web-bluetooth@0.0.20': resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} - '@types/webextension-polyfill@0.12.1': - resolution: {integrity: sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w==} + '@types/webextension-polyfill@0.12.3': + resolution: {integrity: sha512-F58aDVSeN/MjUGazXo/cPsmR76EvqQhQ1v4x23hFjUX0cfAJYE+JBWwiOGW36/VJGGxoH74sVlRIF3z7SJCKyg==} '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} @@ -3325,6 +3340,10 @@ packages: picomatch: optional: true + feed@4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} + filesize@10.1.6: resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} engines: {node: '>= 10.4.0'} @@ -5441,6 +5460,10 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} + xml-js@1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} engines: {node: '>=4.0.0'} @@ -6664,7 +6687,7 @@ snapshots: '@types/web-bluetooth@0.0.20': {} - '@types/webextension-polyfill@0.12.1': {} + '@types/webextension-polyfill@0.12.3': {} '@types/yauzl@2.10.3': dependencies: @@ -7891,6 +7914,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + feed@4.2.2: + dependencies: + xml-js: 1.6.11 + filesize@10.1.6: {} fill-range@7.1.1: @@ -10156,6 +10183,10 @@ snapshots: xdg-basedir@5.1.0: {} + xml-js@1.6.11: + dependencies: + sax: 1.2.4 + xml2js@0.5.0: dependencies: sax: 1.2.4 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3197cb0d6..c2760417e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -48,6 +48,7 @@ catalog: esbuild: ^0.25.0 extract-zip: ^2.0.1 fast-glob: ^3.3.2 + feed: ^4.2.2 filesize: ^10.1.6 fs-extra: ^11.2.0 get-port-please: ^3.1.2 @@ -56,7 +57,6 @@ catalog: hookable: ^5.5.3 import-meta-resolve: ^4.1.0 is-wsl: ^3.1.0 - jiti: ^2.4.2 json5: ^2.2.3 jszip: ^3.10.1 linkedom: ^0.18.5 diff --git a/templates/react/package.json b/templates/react/package.json index 30b88f035..a8c2bad7c 100644 --- a/templates/react/package.json +++ b/templates/react/package.json @@ -19,7 +19,6 @@ "react-dom": "^19.0.0" }, "devDependencies": { - "@types/chrome": "^0.0.280", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.2", "@wxt-dev/module-react": "^1.1.2", diff --git a/templates/react/wxt.config.ts b/templates/react/wxt.config.ts index d1e353878..429f2b90d 100644 --- a/templates/react/wxt.config.ts +++ b/templates/react/wxt.config.ts @@ -2,6 +2,5 @@ import { defineConfig } from 'wxt'; // See https://wxt.dev/api/config.html export default defineConfig({ - extensionApi: 'chrome', modules: ['@wxt-dev/module-react'], }); diff --git a/templates/solid/package.json b/templates/solid/package.json index ff40bcd95..54e682759 100644 --- a/templates/solid/package.json +++ b/templates/solid/package.json @@ -18,7 +18,6 @@ "solid-js": "^1.9.3" }, "devDependencies": { - "@types/chrome": "^0.0.280", "@wxt-dev/module-solid": "^1.1.2", "typescript": "^5.6.3", "wxt": "^0.19.29" diff --git a/templates/solid/wxt.config.ts b/templates/solid/wxt.config.ts index 0a5a994ff..1cc7596d8 100644 --- a/templates/solid/wxt.config.ts +++ b/templates/solid/wxt.config.ts @@ -2,6 +2,5 @@ import { defineConfig } from 'wxt'; // See https://wxt.dev/api/config.html export default defineConfig({ - extensionApi: 'chrome', modules: ['@wxt-dev/module-solid'], }); diff --git a/templates/svelte/package.json b/templates/svelte/package.json index 94554398c..a0049528d 100644 --- a/templates/svelte/package.json +++ b/templates/svelte/package.json @@ -16,7 +16,6 @@ }, "devDependencies": { "@tsconfig/svelte": "^5.0.4", - "@types/chrome": "^0.0.280", "@wxt-dev/module-svelte": "^2.0.0", "svelte": "^5.1.16", "svelte-check": "^4.0.7", diff --git a/templates/svelte/wxt.config.ts b/templates/svelte/wxt.config.ts index f0e4f9aa1..5749b4e62 100644 --- a/templates/svelte/wxt.config.ts +++ b/templates/svelte/wxt.config.ts @@ -3,6 +3,5 @@ import { defineConfig } from 'wxt'; // See https://wxt.dev/api/config.html export default defineConfig({ srcDir: 'src', - extensionApi: 'chrome', modules: ['@wxt-dev/module-svelte'], }); diff --git a/templates/vanilla/package.json b/templates/vanilla/package.json index 4402233ef..3db2cddf9 100644 --- a/templates/vanilla/package.json +++ b/templates/vanilla/package.json @@ -15,7 +15,6 @@ "postinstall": "wxt prepare" }, "devDependencies": { - "@types/chrome": "^0.0.280", "typescript": "^5.6.3", "wxt": "^0.19.29" } diff --git a/templates/vanilla/wxt.config.ts b/templates/vanilla/wxt.config.ts index 35c7ade7f..1e2f53d37 100644 --- a/templates/vanilla/wxt.config.ts +++ b/templates/vanilla/wxt.config.ts @@ -1,6 +1,4 @@ import { defineConfig } from 'wxt'; // See https://wxt.dev/api/config.html -export default defineConfig({ - extensionApi: 'chrome', -}); +export default defineConfig({}); diff --git a/templates/vue/package.json b/templates/vue/package.json index f4d864a6a..2cdc499d2 100644 --- a/templates/vue/package.json +++ b/templates/vue/package.json @@ -18,7 +18,6 @@ "vue": "^3.5.12" }, "devDependencies": { - "@types/chrome": "^0.0.280", "@wxt-dev/module-vue": "^1.0.1", "typescript": "5.6.3", "vue-tsc": "^2.1.10", diff --git a/templates/vue/wxt.config.ts b/templates/vue/wxt.config.ts index 2ab714b1c..55fbc4af6 100644 --- a/templates/vue/wxt.config.ts +++ b/templates/vue/wxt.config.ts @@ -2,6 +2,5 @@ import { defineConfig } from 'wxt'; // See https://wxt.dev/api/config.html export default defineConfig({ - extensionApi: 'chrome', modules: ['@wxt-dev/module-vue'], });