Skip to content
2 changes: 1 addition & 1 deletion packages/wxt-demo/wxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default defineConfig({
},
example: {
a: 'a',
// @ts-expect-error: c is not defined, this should error out
// @ts-expect-error: c is not defined, this should be a type error, but it should show up in the module
c: 'c',
},
unocss: {
Expand Down
2 changes: 1 addition & 1 deletion packages/wxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@
"fast-glob": "^3.3.2",
"filesize": "^10.1.6",
"fs-extra": "^11.2.0",
"get-port": "^7.1.0",
"get-port-please": "^3.1.2",
"giget": "^1.2.3",
"hookable": "^5.5.3",
"is-wsl": "^3.1.0",
Expand Down
6 changes: 4 additions & 2 deletions packages/wxt/src/core/builders/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { installSourcemapsSupport } from 'vite-node/source-map';
export async function createViteBuilder(
wxtConfig: ResolvedConfig,
hooks: Hookable<WxtHooks>,
server?: WxtDevServer,
getWxtDevServer?: () => WxtDevServer | undefined,
): Promise<WxtBuilder> {
const vite = await import('vite');

Expand Down Expand Up @@ -65,6 +65,8 @@ export async function createViteBuilder(
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],
};

const server = getWxtDevServer?.();

config.plugins ??= [];
config.plugins.push(
wxtPlugins.download(wxtConfig),
Expand Down Expand Up @@ -193,7 +195,7 @@ export async function createViteBuilder(
};

/**
* Return the basic config for building a sinlge CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
* Return the basic config for building a single CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
*/
const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
return {
Expand Down
158 changes: 88 additions & 70 deletions packages/wxt/src/core/create-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { createExtensionRunner } from './runners';
import { Mutex } from 'async-mutex';
import pc from 'picocolors';
import { relative } from 'node:path';
import { registerWxt, wxt } from './wxt';
import { deinitWxtModules, initWxtModules, registerWxt, wxt } from './wxt';
import { unnormalizePath } from './utils/paths';
import {
getContentScriptJs,
Expand All @@ -40,64 +40,102 @@ import {
export async function createServer(
inlineConfig?: InlineConfig,
): Promise<WxtDevServer> {
await registerWxt('serve', inlineConfig, async (config) => {
const { port, hostname } = config.dev.server!;
const serverInfo: ServerInfo = {
await registerWxt('serve', inlineConfig);

return (wxt.server = await createServerInternal());
}

async function createServerInternal(): Promise<WxtDevServer> {
const getServerInfo = (): ServerInfo => {
const { port, hostname } = wxt.config.dev.server!;
return {
port,
hostname,
origin: `http://${hostname}:${port}`,
};
};

// Server instance must be created first so its reference can be added to the internal config used
// to pre-render entrypoints
const server: WxtDevServer = {
...serverInfo,
get watcher() {
return builderServer.watcher;
},
get ws() {
return builderServer.ws;
},
currentOutput: undefined,
async start() {
await builderServer.listen();
wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
await buildAndOpenBrowser();
},
async stop() {
await runner.closeBrowser();
await builderServer.close();
},
async restart() {
await closeAndRecreateRunner();
await buildAndOpenBrowser();
},
transformHtml(url, html, originalUrl) {
return builderServer.transformHtml(url, html, originalUrl);
},
reloadContentScript(payload) {
server.ws.send('wxt:reload-content-script', payload);
},
reloadPage(path) {
server.ws.send('wxt:reload-page', path);
},
reloadExtension() {
server.ws.send('wxt:reload-extension');
},
async restartBrowser() {
await closeAndRecreateRunner();
await runner.openBrowser();
},
};
return server;
});

const server = wxt.server!;
let [runner, builderServer] = await Promise.all([
createExtensionRunner(),
wxt.builder.createServer(server),
wxt.builder.createServer(getServerInfo()),
]);

// Used to track if modules need to be re-initialized
let wasStopped = false;

// Server instance must be created first so its reference can be added to the internal config used
// to pre-render entrypoints
const server: WxtDevServer = {
get hostname() {
return getServerInfo().hostname;
},
get port() {
return getServerInfo().port;
},
get origin() {
return getServerInfo().origin;
},
get watcher() {
return builderServer.watcher;
},
get ws() {
return builderServer.ws;
},
currentOutput: undefined,
async start() {
if (wasStopped) {
await wxt.reloadConfig();
runner = await createExtensionRunner();
builderServer = await wxt.builder.createServer(getServerInfo());
await initWxtModules();
}

await builderServer.listen();
wxt.logger.success(`Started dev server @ ${server.origin}`);
await buildAndOpenBrowser();

// Register content scripts for the first time after the background starts up since they're not
// listed in the manifest
server.ws.on('wxt:background-initialized', () => {
if (server.currentOutput == null) return;
reloadContentScripts(server.currentOutput.steps, server);
});

// Listen for file changes and reload different parts of the extension accordingly
const reloadOnChange = createFileReloader(server);
server.watcher.on('all', reloadOnChange);
},
async stop() {
wasStopped = true;
await runner.closeBrowser();
await builderServer.close();
deinitWxtModules();
server.currentOutput = undefined;
},
async restart() {
await server.stop();
await server.start();
},
transformHtml(url, html, originalUrl) {
return builderServer.transformHtml(url, html, originalUrl);
},
reloadContentScript(payload) {
server.ws.send('wxt:reload-content-script', payload);
},
reloadPage(path) {
server.ws.send('wxt:reload-page', path);
},
reloadExtension() {
server.ws.send('wxt:reload-extension');
},
async restartBrowser() {
await runner.closeBrowser();
await wxt.reloadConfig();
runner = await createExtensionRunner();
await runner.openBrowser();
},
};

const buildAndOpenBrowser = async () => {
// Build after starting the dev server so it can be used to transform HTML files
server.currentOutput = await internalBuild();
Expand All @@ -114,26 +152,6 @@ export async function createServer(
await runner.openBrowser();
};

/**
* Stops the previous runner, grabs the latest config, and recreates the runner.
*/
const closeAndRecreateRunner = async () => {
await runner.closeBrowser();
await wxt.reloadConfig();
runner = await createExtensionRunner();
};

// Register content scripts for the first time after the background starts up since they're not
// listed in the manifest
server.ws.on('wxt:background-initialized', () => {
if (server.currentOutput == null) return;
reloadContentScripts(server.currentOutput.steps, server);
});

// Listen for file changes and reload different parts of the extension accordingly
const reloadOnChange = createFileReloader(server);
server.watcher.on('all', reloadOnChange);

return server;
}

Expand Down
12 changes: 9 additions & 3 deletions packages/wxt/src/core/resolve-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { builtinModules } from '../builtin-modules';
import { getEslintVersion } from './utils/eslint';
import { safeStringToNumber } from './utils/number';
import { loadEnv } from './utils/env';
import { getPort } from 'get-port-please';

/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
Expand Down Expand Up @@ -137,14 +138,19 @@ export async function resolveConfig(

let devServerConfig: ResolvedConfig['dev']['server'];
if (command === 'serve') {
const hostname = mergedConfig.dev?.server?.hostname ?? 'localhost';
let port = mergedConfig.dev?.server?.port;
if (port == null || !isFinite(port)) {
const { default: getPort, portNumbers } = await import('get-port');
port = await getPort({ port: portNumbers(3000, 3010) });
port = await getPort({
port: 3000,
portRange: [3001, 3010],
// Passing host required for Mac, unsure of Windows/Linux
host: hostname,
});
}
devServerConfig = {
port,
hostname: mergedConfig.dev?.server?.hostname ?? 'localhost',
hostname,
watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ describe('Detect Dev Changes', () => {
});
});

describe('modules/*', () => {
it("should return 'full-restart' when one of the changed files is in the WXT modules folder", () => {
const modulesDir = '/root/modules';
setFakeWxt({
config: {
modulesDir,
},
});
const changes = [
'/root/src/public/image.svg',
`${modulesDir}/example.ts`,
];
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [],
};
const expected: DevModeChange = {
type: 'full-restart',
};

const actual = detectDevChanges(changes, currentOutput);

expect(actual).toEqual(expected);
});
});

describe('web-ext.config.ts', () => {
it("should return 'browser-restart' when one of the changed files is the config file", () => {
const runnerFile = '/root/web-ext.config.ts';
Expand Down
11 changes: 10 additions & 1 deletion packages/wxt/src/core/utils/building/detect-dev-changes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import { wxt } from '../../wxt';
* - Background script is changed
* - Manifest is different
* - Restart browser
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
* - web-ext.config.ts (runner config changes)
* - Full dev server restart
* - wxt.config.ts (main config file)
* - modules/* (any file related to WXT modules)
* - .env (environment variable changed could effect build)
*/
export function detectDevChanges(
changedFiles: string[],
Expand All @@ -38,6 +42,11 @@ export function detectDevChanges(
);
if (isConfigChange) return { type: 'full-restart' };

const isWxtModuleChange = some(changedFiles, (file) =>
file.startsWith(wxt.config.modulesDir),
);
if (isWxtModuleChange) return { type: 'full-restart' };

const isRunnerChange = some(
changedFiles,
(file) => file === wxt.config.runnerConfig.configFile,
Expand Down
Loading