Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/wxt-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "buildc --deps-only -- wxt",
"dev:firefox": "buildc --deps-only -- wxt -b firefox",
"dev:firefox-mv3": "buildc --deps-only -- wxt -b firefox --mv3",
"dev:firefox-android": "buildc --deps-only -- wxt -b firefox-android",
"build": "buildc --deps-only -- wxt build",
"build:all": "buildc --deps-only -- bun run --sequential 'build:*-*'",
"build:chrome-mv3": "wxt build",
Expand Down
110 changes: 110 additions & 0 deletions packages/wxt/src/builtin-modules/adb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { execFile, execFileSync } from 'node:child_process';
import { promisify } from 'node:util';
import { defineWxtModule } from '../modules';
import { Wxt } from '../types';

const execFileAsync = promisify(execFile);

/**
* Fills in `webExt.adbDevice` when a single device is connected and reverses
* the dev server port to it so the extension's `localhost:<port>` URLs (HMR,
* dev-mode pages) work there.
*/
export default defineWxtModule({
name: 'wxt:built-in:adb',
setup(wxt) {
let removeAdbReverse: (() => Promise<void>) | undefined;
let detectedDevice: string | undefined;

wxt.hooks.hook('config:resolved', async (wxt) => {
const config = wxt.config.webExt.config;
if (config?.target !== 'firefox-android' || config.adbDevice) return;
detectedDevice ??= await findSingleAdbDevice(wxt, config.adbBin ?? 'adb');
config.adbDevice = detectedDevice;
});
wxt.hooks.hook('server:started', async (wxt, server) => {
if (wxt.config.webExt.config?.target !== 'firefox-android') return;
removeAdbReverse = await setupAdbReverse(wxt, server.port);
});
wxt.hooks.hook('server:closed', async () => {
await removeAdbReverse?.();
removeAdbReverse = undefined;
});
},
});

/**
* When `adbDevice` isn't configured and exactly one device is connected, use
* it. Otherwise return undefined and let web-ext report the device list.
*/
async function findSingleAdbDevice(
wxt: Wxt,
adbBin: string,
): Promise<string | undefined> {
try {
const { stdout } = await execFileAsync(adbBin, ['devices']);
const devices = stdout
.split('\n')
.slice(1)
.map((line) => line.trim().split(/\s+/))
.filter((parts) => parts.length === 2 && parts[1] === 'device')
.map((parts) => parts[0]);
if (devices.length === 1) {
wxt.logger.info(`Using ADB device: ${devices[0]}`);
return devices[0];
}
wxt.logger.warn(
`Expected exactly 1 connected ADB device, found ${devices.length}. Set \`webExt.adbDevice\` to pick one.`,
);
} catch (err) {
wxt.logger.warn(
`Failed to list ADB devices with "${adbBin} devices", set \`webExt.adbDevice\` manually`,
err,
);
}
return undefined;
}

async function setupAdbReverse(
wxt: Wxt,
port: number,
): Promise<(() => Promise<void>) | undefined> {
const { adbBin = 'adb', adbDevice } = wxt.config.webExt.config ?? {};
// Without `-s`, adb targets the only connected device
const deviceArgs = adbDevice ? ['-s', adbDevice] : [];
const portSpec = `tcp:${port}`;
try {
await execFileAsync(adbBin, [...deviceArgs, 'reverse', portSpec, portSpec]);
wxt.logger.info(`Reversed dev server port ${port} to device for HMR`);
// Ctrl+C doesn't go through server.stop(), clean up on process death too
const removeSync = () => {
try {
execFileSync(adbBin, [...deviceArgs, 'reverse', '--remove', portSpec], {
stdio: 'ignore',
});
} catch {}
};
const onSignal = (signal: NodeJS.Signals) => {
removeSync();
try {
process.kill(process.pid, signal);
} catch {
process.exit(130);
}
};
process.once('exit', removeSync);
process.once('SIGINT', onSignal);
process.once('SIGTERM', onSignal);
return async () => {
process.removeListener('exit', removeSync);
process.removeListener('SIGINT', onSignal);
process.removeListener('SIGTERM', onSignal);
removeSync();
};
} catch (err) {
wxt.logger.warn(
`Failed to run "adb reverse" for port ${port}, hot reload will not work on the device`,
err,
);
}
}
2 changes: 2 additions & 0 deletions packages/wxt/src/builtin-modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { WxtModule } from '../types';
import faviconPermission from './favicon-permission';
import unimport from './unimport';
import escapeUnicode from './escape-unicode';
import adb from './adb';

export const builtinModules: WxtModule<any>[] = [
unimport,
faviconPermission,
escapeUnicode,
adb,
];
41 changes: 27 additions & 14 deletions packages/wxt/src/core/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,24 +72,43 @@ export async function resolveConfig(
const logger = createWxtLogger(mergedConfig.logger ?? consola);
if (debug) logger.level = LogLevels.debug;

const browser = mergedConfig.browser ?? 'chrome';
const root = path.resolve(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
);
const webExt = await loadConfig<WebExtConfig>({
name: 'web-ext',
cwd: root,
globalRc: true,
rcFile: '.webextrc',
overrides: inlineConfig.webExt,
defaults: userConfig.webExt,
});

// "firefox-android" is an alias for the firefox build targeting an Android device
const requestedBrowser = mergedConfig.browser ?? 'chrome';
const isAndroid =
requestedBrowser === 'firefox-android' ||
webExt.config?.target === 'firefox-android';
const browser =
requestedBrowser === 'firefox-android' ? 'firefox' : requestedBrowser;
if (isAndroid) {
webExt.config = { ...webExt.config, target: 'firefox-android' };
}
const targetBrowsers = mergedConfig.targetBrowsers ?? [];
if (targetBrowsers.length > 0 && !targetBrowsers.includes(browser)) {
throw new Error(
`Current target browser \`${browser}\` is not in your \`targetBrowsers\` list!`,
);
}
// Android must be MV3: dev-mode content script reload is MV3-only
const manifestVersion =
mergedConfig.manifestVersion ??
(browser === 'firefox' || browser === 'safari' ? 2 : 3);
(isAndroid || (browser !== 'firefox' && browser !== 'safari') ? 3 : 2);
const mode = mergedConfig.mode ?? COMMAND_MODES[command];
const env: ConfigEnv = { browser, command, manifestVersion, mode };

loadEnv(mode, browser); // Load any environment variables used below

const root = path.resolve(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
);
const wxtDir = path.resolve(root, '.wxt');
const wxtModuleDir = resolveWxtModuleDir();
const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
Expand Down Expand Up @@ -125,14 +144,6 @@ export async function resolveConfig(

const outDir = path.resolve(outBaseDir, outDirTemplate);
const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';
const webExt = await loadConfig<WebExtConfig>({
name: 'web-ext',
cwd: root,
globalRc: true,
rcFile: '.webextrc',
overrides: inlineConfig.webExt,
defaults: userConfig.webExt,
});
// Make sure alias are absolute
const alias = Object.fromEntries(
Object.entries({
Expand All @@ -146,7 +157,9 @@ export async function resolveConfig(

let devServerConfig: ResolvedConfig['dev']['server'];
if (command === 'serve') {
const host = mergedConfig.dev?.server?.host ?? 'localhost';
// Bind IPv4 explicitly for android, "adb reverse" can't reach IPv6-only `::1`
const host =
mergedConfig.dev?.server?.host ?? (isAndroid ? '127.0.0.1' : 'localhost');
let port = mergedConfig.dev?.server?.port;
const origin = mergedConfig.dev?.server?.origin ?? 'localhost';
const strictPort = mergedConfig.dev?.server?.strictPort ?? false;
Expand Down
62 changes: 39 additions & 23 deletions packages/wxt/src/core/runners/web-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,43 +24,59 @@ export function createWebExtRunner(): ExtensionRunner {
};

const wxtUserConfig = wxt.config.webExt.config;
const target =
wxtUserConfig?.target ??
(wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium');

// Options for known targets, anything else passes through to web-ext as-is
let targetConfig: Record<string, unknown> = {};
if (target === 'firefox-desktop') {
targetConfig = {
firefox: wxtUserConfig?.binaries?.firefox,
firefoxProfile: wxtUserConfig?.firefoxProfile,
pref: wxtUserConfig?.firefoxPref,
args: wxtUserConfig?.firefoxArgs,
};
} else if (target === 'firefox-android') {
targetConfig = {
adbDevice: wxtUserConfig?.adbDevice,
adbBin: wxtUserConfig?.adbBin,
firefoxApk: wxtUserConfig?.firefoxApk ?? 'org.mozilla.fenix',
pref: wxtUserConfig?.firefoxPref,
};
} else if (target === 'chromium') {
targetConfig = {
chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],
chromiumProfile: wxtUserConfig?.chromiumProfile,
chromiumPref: defu(
wxtUserConfig?.chromiumPref,
DEFAULT_CHROMIUM_PREFS,
),
args: [
'--unsafely-disable-devtools-self-xss-warnings',
...(wxtUserConfig?.chromiumArgs ?? []),
],
};
}

const userConfig = {
browserConsole: wxtUserConfig?.openConsole,
devtools: wxtUserConfig?.openDevtools,
startUrl: wxtUserConfig?.startUrls,
keepProfileChanges: wxtUserConfig?.keepProfileChanges,
chromiumPort: wxtUserConfig?.chromiumPort,
...(wxt.config.browser === 'firefox'
? {
firefox: wxtUserConfig?.binaries?.firefox,
firefoxProfile: wxtUserConfig?.firefoxProfile,
pref: wxtUserConfig?.firefoxPref,
args: wxtUserConfig?.firefoxArgs,
}
: {
chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser],
chromiumProfile: wxtUserConfig?.chromiumProfile,
chromiumPref: defu(
wxtUserConfig?.chromiumPref,
DEFAULT_CHROMIUM_PREFS,
),
args: [
'--unsafely-disable-devtools-self-xss-warnings',
...(wxtUserConfig?.chromiumArgs ?? []),
],
}),
...targetConfig,
};

const finalConfig = {
...userConfig,
target:
wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
target,
sourceDir: wxt.config.outDir,
// Don't add a "Reload Manager" extension alongside dev extension, WXT
// already handles reloads internally.
noReloadManagerExtension: true,
// WXT handles reloads, so disable auto-reload behaviors in web-ext
noReload: true,
// WXT handles reloads, except on Android where web-ext must re-push
noReload: target !== 'firefox-android',
noInput: true,
};
const options = {
Expand Down
2 changes: 1 addition & 1 deletion packages/wxt/src/core/utils/building/find-entrypoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
.filter((item) => item.skipped)
.map((item) => item.name);
if (skippedEntrypointNames.length) {
wxt.logger.warn(
wxt.logger.warnOnce(
[
'The following entrypoints have been skipped:',
...skippedEntrypointNames.map(
Expand Down
2 changes: 1 addition & 1 deletion packages/wxt/src/core/utils/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ function addEntrypoints(

if (sandboxes?.length) {
if (wxt.config.browser === 'firefox') {
wxt.logger.warn(
wxt.logger.warnOnce(
'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest',
);
} else {
Expand Down
8 changes: 8 additions & 0 deletions packages/wxt/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,14 @@ export interface WebExtConfig {
startUrls?: string[];
/** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#keep-profile-changes */
keepProfileChanges?: boolean;
/** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#target */
target?: 'firefox-desktop' | 'firefox-android' | 'chromium' | (string & {});
/** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#adb-device */
adbDevice?: string;
/** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#adb-bin */
adbBin?: string;
/** @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-apk */
firefoxApk?: string;
}

export interface WxtBuilder {
Expand Down
2 changes: 1 addition & 1 deletion packages/wxt/src/virtual/background-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if (import.meta.env.COMMAND === 'serve') {
logger.error('Failed to setup web socket connection with dev server', err);
}

browser.commands.onCommand.addListener((command) => {
browser.commands?.onCommand.addListener((command) => {
if (command === 'wxt:reload-extension') {
browser.runtime.reload();
}
Expand Down