diff --git a/e2e/tests/auto-imports.test.ts b/e2e/tests/auto-imports.test.ts
index f2b42b47..62929fa8 100644
--- a/e2e/tests/auto-imports.test.ts
+++ b/e2e/tests/auto-imports.test.ts
@@ -17,6 +17,8 @@ describe('Auto Imports', () => {
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 browser: typeof import('wxt/browser')['browser']
const builtinDrivers: typeof import('wxt/storage')['builtinDrivers']
const createContentScriptIframe: typeof import('wxt/client')['createContentScriptIframe']
diff --git a/package.json b/package.json
index f10ea238..953feea5 100644
--- a/package.json
+++ b/package.json
@@ -86,7 +86,9 @@
"lint": "run-p -c -s lint:*",
"lint:eslint": "echo 'ESLint: TODO'",
"lint:package": "publint",
- "compile": "tsc --noEmit",
+ "compile": "run-s -c compile:*",
+ "compile:wxt": "tsc --noEmit",
+ "compile:virtual": "tsc --noEmit -p src/virtual",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"prepare": "simple-git-hooks",
diff --git a/scripts/build.ts b/scripts/build.ts
index df70e7d9..105d395d 100644
--- a/scripts/build.ts
+++ b/scripts/build.ts
@@ -16,18 +16,17 @@ const startTime = Date.now();
const outDir = 'dist';
await fs.rm(path.join(outDir, '*'), { recursive: true, force: true });
-const preset: tsup.Options = {
+const preset = {
dts: true,
silent: true,
sourcemap: false,
external: [
- 'vite',
'virtual:user-unlisted-script',
'virtual:user-content-script-isolated-world',
'virtual:user-content-script-main-world',
'virtual:user-background',
],
-};
+} satisfies tsup.Options;
function spinnerPMap(configs: tsup.Options[]) {
let completed = 0;
@@ -89,6 +88,7 @@ const config: tsup.Options[] = [
format: ['esm'],
splitting: false,
dts: false,
+ external: [...preset.external, 'wxt'],
},
// CJS-only
{
diff --git a/src/browser.ts b/src/browser.ts
index 61ee5fab..103b19d6 100644
--- a/src/browser.ts
+++ b/src/browser.ts
@@ -17,3 +17,65 @@ export interface WxtI18n extends I18n.Static {
}
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,
+ ContextMenus,
+ Menus,
+ NetworkStatus,
+ NormandyAddonStudy,
+ Notifications,
+ Omnibox,
+ PageAction,
+ Permissions,
+ Pkcs11,
+ Privacy,
+ Proxy,
+ Runtime,
+ Scripting,
+ Search,
+ Sessions,
+ SidebarAction,
+ Storage,
+ Tabs,
+ Theme,
+ TopSites,
+ Types,
+ Urlbar,
+ UserScripts,
+ WebNavigation,
+ WebRequest,
+ Windows,
+} from 'webextension-polyfill';
diff --git a/src/client/content-scripts/content-script-context.ts b/src/client/content-scripts/content-script-context.ts
index 48d925f5..9233113e 100644
--- a/src/client/content-scripts/content-script-context.ts
+++ b/src/client/content-scripts/content-script-context.ts
@@ -1,6 +1,6 @@
import { ContentScriptDefinition } from '~/types';
import { browser } from '~/browser';
-import { logger } from '~/client/utils/logger';
+import { logger } from '~/sandbox/utils/logger';
import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events';
import { createLocationWatcher } from './location-watcher';
diff --git a/src/client/content-scripts/content-script-iframe.ts b/src/client/content-scripts/content-script-iframe.ts
index 8247e13a..2c1ac518 100644
--- a/src/client/content-scripts/content-script-iframe.ts
+++ b/src/client/content-scripts/content-script-iframe.ts
@@ -1,4 +1,4 @@
-import browser from 'webextension-polyfill';
+import { browser } from '~/browser';
import {
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
diff --git a/src/client/content-scripts/content-script-ui.ts b/src/client/content-scripts/content-script-ui.ts
index 281a3c87..c8f38e69 100644
--- a/src/client/content-scripts/content-script-ui.ts
+++ b/src/client/content-scripts/content-script-ui.ts
@@ -1,6 +1,6 @@
import { createIsolatedElement } from '@webext-core/isolated-element';
import { browser } from '~/browser';
-import { logger } from '~/client/utils/logger';
+import { logger } from '~/sandbox/utils/logger';
import { ContentScriptContext } from './content-script-context';
import {
ContentScriptAnchoredOptions,
diff --git a/src/core/builders/vite/plugins/webextensionPolyfillInlineDeps.ts b/src/core/builders/vite/plugins/webextensionPolyfillInlineDeps.ts
index b7023170..cbb1b73f 100644
--- a/src/core/builders/vite/plugins/webextensionPolyfillInlineDeps.ts
+++ b/src/core/builders/vite/plugins/webextensionPolyfillInlineDeps.ts
@@ -9,7 +9,7 @@ export function webextensionPolyfillInlineDeps(): vite.PluginOption {
return {
name: 'wxt:testing-inline-deps',
config() {
- const wxtModules = ['wxt/browser', 'wxt/client'];
+ const wxtModules = ['wxt/browser'];
return {
test: {
server: {
diff --git a/src/core/initialize.ts b/src/core/initialize.ts
index b0410b46..6dd77a47 100644
--- a/src/core/initialize.ts
+++ b/src/core/initialize.ts
@@ -150,14 +150,6 @@ async function cloneProject({
consola.warn('Failed to move _gitignore to .gitignore:', err),
);
- // 3. Add .npmrc for pnpm
- if (packageManager === 'pnpm') {
- await fs.writeFile(
- path.join(directory, '.npmrc'),
- 'shamefully-hoist=true\n',
- );
- }
-
spinner.succeed();
} catch (err) {
spinner.fail();
diff --git a/src/core/utils/building/rebuild.ts b/src/core/utils/building/rebuild.ts
index a8d8cfb3..32ab04cb 100644
--- a/src/core/utils/building/rebuild.ts
+++ b/src/core/utils/building/rebuild.ts
@@ -1,4 +1,4 @@
-import type { Manifest } from 'webextension-polyfill';
+import type { Manifest } from '~/browser';
import { BuildOutput, EntrypointGroup, InternalConfig } from '~/types';
import { findEntrypoints } from './find-entrypoints';
import { generateTypesDir } from './generate-wxt-dir';
diff --git a/src/core/utils/content-scripts.ts b/src/core/utils/content-scripts.ts
index 691feb93..a4f016ca 100644
--- a/src/core/utils/content-scripts.ts
+++ b/src/core/utils/content-scripts.ts
@@ -1,4 +1,4 @@
-import type { Manifest } from 'webextension-polyfill';
+import type { Manifest } from '~/browser';
import { ContentScriptEntrypoint, InternalConfig } from '~/types';
import { resolvePerBrowserOption } from './entrypoints';
diff --git a/src/core/utils/manifest.ts b/src/core/utils/manifest.ts
index a4485a77..f43b0890 100644
--- a/src/core/utils/manifest.ts
+++ b/src/core/utils/manifest.ts
@@ -1,4 +1,4 @@
-import type { Manifest } from 'webextension-polyfill';
+import type { Manifest } from '~/browser';
import {
Entrypoint,
BackgroundEntrypoint,
diff --git a/src/core/utils/testing/fake-objects.ts b/src/core/utils/testing/fake-objects.ts
index 949d053a..f846bd8b 100644
--- a/src/core/utils/testing/fake-objects.ts
+++ b/src/core/utils/testing/fake-objects.ts
@@ -4,7 +4,7 @@
import { resolve } from 'path';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
-import type { Manifest } from 'webextension-polyfill';
+import type { Manifest } from '~/browser';
import {
FsCache,
InternalConfig,
diff --git a/src/sandbox/index.ts b/src/sandbox/index.ts
index 50725005..91b43851 100644
--- a/src/sandbox/index.ts
+++ b/src/sandbox/index.ts
@@ -6,3 +6,4 @@
export * from './define-unlisted-script';
export * from './define-background';
export * from './define-content-script';
+export * from '@webext-core/match-patterns';
diff --git a/src/client/utils/logger.ts b/src/sandbox/utils/logger.ts
similarity index 93%
rename from src/client/utils/logger.ts
rename to src/sandbox/utils/logger.ts
index 66ffcbf6..d2172bd4 100644
--- a/src/client/utils/logger.ts
+++ b/src/sandbox/utils/logger.ts
@@ -1,3 +1,5 @@
+///
+
function print(method: (...args: any[]) => void, ...args: any[]) {
if (import.meta.env.MODE === 'production') return;
diff --git a/src/storage.ts b/src/storage.ts
index 61fe9461..42861467 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -8,7 +8,7 @@ import {
defineDriver,
Storage,
} from 'unstorage';
-import browser, { Storage as BrowserStorage } from 'webextension-polyfill';
+import { browser, Storage as BrowserStorage } from '~/browser';
export interface WebExtensionDriverOptions {
storageArea: 'sync' | 'local' | 'managed' | 'session';
diff --git a/src/types/external.ts b/src/types/external.ts
index 48719ea4..e535ae7f 100644
--- a/src/types/external.ts
+++ b/src/types/external.ts
@@ -1,5 +1,5 @@
import type * as vite from 'vite';
-import type { Manifest, Scripting } from 'webextension-polyfill';
+import type { Manifest, Scripting } from '~/browser';
import { UnimportOptions } from 'unimport';
import { LogLevel } from 'consola';
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts
new file mode 100644
index 00000000..cfa8f0d9
--- /dev/null
+++ b/src/types/globals.d.ts
@@ -0,0 +1,7 @@
+// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts
+declare const __COMMAND__: 'build' | 'serve';
+declare const __DEV_SERVER_PROTOCOL__: string;
+declare const __DEV_SERVER_HOSTNAME__: string;
+declare const __DEV_SERVER_PORT__: string;
+declare const __MANIFEST_VERSION__: 2 | 3;
+declare const __ENTRYPOINT__: string;
diff --git a/src/types/internal.ts b/src/types/internal.ts
index c4e626d0..fc8a631c 100644
--- a/src/types/internal.ts
+++ b/src/types/internal.ts
@@ -11,7 +11,7 @@ import {
} from './external';
import { UnimportOptions } from 'unimport';
import { ResolvedConfig } from 'c12';
-import type { Manifest } from 'webextension-polyfill';
+import type { Manifest } from '~/browser';
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
export interface InternalConfig {
diff --git a/src/virtual/README.md b/src/virtual/README.md
new file mode 100644
index 00000000..b06cebed
--- /dev/null
+++ b/src/virtual/README.md
@@ -0,0 +1,11 @@
+# WXT Virtual Entrypoints
+
+This folder contains scripts that are either loaded as entrypoints to JS files or included in HTML files, just like a project using WXT might load their own scripts.
+
+While they are bundled and shipped inside WXT, Vite considers them apart of your project's source code, not WXT's. This means they cannot import 3rd party modules directly, otherwise `pnpm i --shamefully-hoist=false` and Yarn PnP will fail.
+
+For this reason, the virtual entrypoints get their own TS project to isolate them from the rest of the project. They can only import from `wxt/*` or utils that don't have any imports from node_modules, like the logger.
+
+When bundling WXT for publishing to NPM, all the `wxt/*` imports are marked as external and resolved when building your application. Other imports are added inline.
+
+See https://github.com/wxt-dev/wxt/issues/286#issuecomment-1858888390 for more details.
diff --git a/src/virtual/background-entrypoint.ts b/src/virtual/background-entrypoint.ts
index fac4bfd4..f69897a0 100644
--- a/src/virtual/background-entrypoint.ts
+++ b/src/virtual/background-entrypoint.ts
@@ -1,9 +1,9 @@
import definition from 'virtual:user-background';
-import { setupWebSocket } from '../client/utils/setup-web-socket';
-import { logger } from '../client/utils/logger';
-import browser from 'webextension-polyfill';
-import { keepServiceWorkerAlive } from '../client/utils/keep-service-worker-alive';
-import { reloadContentScript } from '../client/utils/reload-content-scripts';
+import { setupWebSocket } from './utils/setup-web-socket';
+import { logger } from '../sandbox/utils/logger';
+import { browser } from 'wxt/browser';
+import { keepServiceWorkerAlive } from './utils/keep-service-worker-alive';
+import { reloadContentScript } from './utils/reload-content-scripts';
if (__COMMAND__ === 'serve') {
try {
diff --git a/src/virtual/content-script-isolated-world-entrypoint.ts b/src/virtual/content-script-isolated-world-entrypoint.ts
index f881ec84..429238da 100644
--- a/src/virtual/content-script-isolated-world-entrypoint.ts
+++ b/src/virtual/content-script-isolated-world-entrypoint.ts
@@ -1,6 +1,6 @@
import definition from 'virtual:user-content-script-isolated-world';
-import { logger } from '~/client/utils/logger';
-import { ContentScriptContext } from '~/client/content-scripts/content-script-context';
+import { logger } from '../sandbox/utils/logger';
+import { ContentScriptContext } from 'wxt/client';
(async () => {
try {
diff --git a/src/virtual/content-script-main-world-entrypoint.ts b/src/virtual/content-script-main-world-entrypoint.ts
index 5ab4d457..43fc689b 100644
--- a/src/virtual/content-script-main-world-entrypoint.ts
+++ b/src/virtual/content-script-main-world-entrypoint.ts
@@ -1,5 +1,5 @@
import definition from 'virtual:user-content-script-main-world';
-import { logger } from '~/client/utils/logger';
+import { logger } from '../sandbox/utils/logger';
(async () => {
try {
diff --git a/src/virtual/mock-browser.ts b/src/virtual/mock-browser.ts
index f02c0db0..cf37d2f7 100644
--- a/src/virtual/mock-browser.ts
+++ b/src/virtual/mock-browser.ts
@@ -1,3 +1,3 @@
-import { fakeBrowser as mockBrowser } from '~/testing';
+import { fakeBrowser as mockBrowser } from 'wxt/testing';
export default mockBrowser;
diff --git a/src/virtual/reload-html.ts b/src/virtual/reload-html.ts
index 0b48da7c..f2a57a17 100644
--- a/src/virtual/reload-html.ts
+++ b/src/virtual/reload-html.ts
@@ -1,7 +1,5 @@
-///
-
-import { logger } from '~/client/utils/logger';
-import { setupWebSocket } from '~/client/utils/setup-web-socket';
+import { logger } from '../sandbox/utils/logger';
+import { setupWebSocket } from './utils/setup-web-socket';
if (__COMMAND__ === 'serve') {
try {
diff --git a/src/virtual/tsconfig.json b/src/virtual/tsconfig.json
new file mode 100644
index 00000000..929d30b6
--- /dev/null
+++ b/src/virtual/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": ["vite/client", "../types/globals.d.ts"]
+ },
+ "include": ["./*"]
+}
diff --git a/src/virtual/unlisted-script-entrypoint.ts b/src/virtual/unlisted-script-entrypoint.ts
index ea17e2e0..f7c1bead 100644
--- a/src/virtual/unlisted-script-entrypoint.ts
+++ b/src/virtual/unlisted-script-entrypoint.ts
@@ -1,5 +1,5 @@
import definition from 'virtual:user-unlisted-script';
-import { logger } from '~/client/utils/logger';
+import { logger } from '../sandbox/utils/logger';
(async () => {
try {
diff --git a/src/client/utils/keep-service-worker-alive.ts b/src/virtual/utils/keep-service-worker-alive.ts
similarity index 85%
rename from src/client/utils/keep-service-worker-alive.ts
rename to src/virtual/utils/keep-service-worker-alive.ts
index c465b477..6a76d31a 100644
--- a/src/client/utils/keep-service-worker-alive.ts
+++ b/src/virtual/utils/keep-service-worker-alive.ts
@@ -1,4 +1,4 @@
-import browser from 'webextension-polyfill';
+import { browser } from 'wxt/browser';
/**
* https://developer.chrome.com/blog/longer-esw-lifetimes/
diff --git a/src/client/utils/reload-content-scripts.ts b/src/virtual/utils/reload-content-scripts.ts
similarity index 74%
rename from src/client/utils/reload-content-scripts.ts
rename to src/virtual/utils/reload-content-scripts.ts
index 73994819..a623c30d 100644
--- a/src/client/utils/reload-content-scripts.ts
+++ b/src/virtual/utils/reload-content-scripts.ts
@@ -1,8 +1,14 @@
-import browser, { Manifest } from 'webextension-polyfill';
-import { logger } from './logger';
-import { MatchPattern } from '@webext-core/match-patterns';
+import { browser } from 'wxt/browser';
+import { logger } from '../../sandbox/utils/logger';
+import { MatchPattern } from 'wxt/sandbox';
-export function reloadContentScript(contentScript: Manifest.ContentScript) {
+interface ContentScript {
+ matches: string[];
+ js?: string[];
+ css?: string[];
+}
+
+export function reloadContentScript(contentScript: ContentScript) {
const manifest = browser.runtime.getManifest();
if (manifest.manifest_version == 2) {
void reloadContentScriptMv2(contentScript);
@@ -11,9 +17,7 @@ export function reloadContentScript(contentScript: Manifest.ContentScript) {
}
}
-export async function reloadContentScriptMv3(
- contentScript: Manifest.ContentScript,
-) {
+export async function reloadContentScriptMv3(contentScript: ContentScript) {
const id = `wxt:${contentScript.js![0]}`;
logger.log('Reloading content script:', contentScript);
const registered = await browser.scripting.getRegisteredContentScripts();
@@ -41,8 +45,6 @@ export async function reloadContentScriptMv3(
await Promise.all(matchingTabs.map((tab) => browser.tabs.reload(tab.id)));
}
-export async function reloadContentScriptMv2(
- contentScript: Manifest.ContentScript,
-) {
+export async function reloadContentScriptMv2(contentScript: ContentScript) {
throw Error('TODO: reloadContentScriptMv2');
}
diff --git a/src/client/utils/setup-web-socket.ts b/src/virtual/utils/setup-web-socket.ts
similarity index 95%
rename from src/client/utils/setup-web-socket.ts
rename to src/virtual/utils/setup-web-socket.ts
index 55c93214..9e1b58e4 100644
--- a/src/client/utils/setup-web-socket.ts
+++ b/src/virtual/utils/setup-web-socket.ts
@@ -1,4 +1,4 @@
-import { logger } from './logger';
+import { logger } from '../../sandbox/utils/logger';
interface WebSocketMessage {
type: string;
diff --git a/src/virtual/virtual-module-globals.d.ts b/src/virtual/virtual-module-globals.d.ts
index a2523d85..c324452f 100644
--- a/src/virtual/virtual-module-globals.d.ts
+++ b/src/virtual/virtual-module-globals.d.ts
@@ -1,34 +1,39 @@
// Types required to make the virtual modules happy.
-declare module '*?raw' {
- const content: any;
- export default content;
-}
-
declare module 'virtual:user-background' {
- const definition: import('~/types').BackgroundDefinition;
+ const definition: { main: () => void };
export default definition;
}
declare module 'virtual:user-content-script-isolated-world' {
- const definition: import('~/types').ContentScriptIsolatedWorldDefinition;
+ const definition: {
+ main: (
+ ctx: import('wxt/client').ContentScriptContext,
+ ) => void | Promise;
+ };
export default definition;
}
declare module 'virtual:user-content-script-main-world' {
- const definition: import('~/types').ContentScriptMainWorldDefinition;
+ const definition: { main: () => void | Promise };
export default definition;
}
declare module 'virtual:user-unlisted-script' {
- const definition: import('~/types').UnlistedScriptDefinition;
+ const definition: { main: () => void | Promise };
export default definition;
}
-// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts
-declare const __COMMAND__: 'build' | 'serve';
-declare const __DEV_SERVER_PROTOCOL__: string;
-declare const __DEV_SERVER_HOSTNAME__: string;
-declare const __DEV_SERVER_PORT__: string;
-declare const __MANIFEST_VERSION__: 2 | 3;
-declare const __ENTRYPOINT__: string;
+declare module 'wxt/browser' {
+ export const browser: import('webextension-polyfill').Browser;
+}
+
+declare module 'wxt/client' {
+ export class ContentScriptContext {
+ constructor(name: string, options: any);
+ }
+}
+
+declare module 'wxt/testing' {
+ export const fakeBrowser: import('webextension-polyfill').Browser;
+}
diff --git a/tsconfig.json b/tsconfig.json
index de22f577..221324d8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,5 +5,5 @@
"~/*": ["./src/*"]
}
},
- "exclude": ["dist", "demo", "e2e/dist", "templates"]
+ "exclude": ["dist", "demo", "e2e/dist", "templates", "src/virtual"]
}
diff --git a/vitest.workspace.ts b/vitest.workspace.ts
index dafc2686..1a82c8ee 100644
--- a/vitest.workspace.ts
+++ b/vitest.workspace.ts
@@ -20,6 +20,7 @@ const resolve = {
alias: {
'~': path.resolve('src'),
'webextension-polyfill': path.resolve('src/virtual/mock-browser'),
+ 'wxt/testing': path.resolve('src/testing'),
},
};