Compare commits

...

15 Commits

34 changed files with 341 additions and 212 deletions
@@ -29,6 +29,7 @@ const chromeExtensionIds = [
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+8 -2
View File
@@ -15,7 +15,14 @@ npx wxt@latest init <project-name>
```
```sh [bun]
bunx wxt@latest init <project-name>
# The "wxt init" command currently fails when ran with bunx.
# Use NPX as a workaround, and select "bun" as your package
# manager. To stay up to date with this issue, follow
# https://github.com/wxt-dev/wxt/issues/707
#
# bunx wxt@latest init <project-name>
npx wxt@latest init <project-name>
```
:::
@@ -96,7 +103,6 @@ pnpm dev
```
:::tip 🎉&ensp;Well done!
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
:::
@@ -12,8 +12,8 @@ Firefox does not support sandboxed pages.
:patterns="[
['sandbox.html', 'sandbox.html'],
['sandbox/index.html', 'sandbox.html'],
['<name>.sandbox.html', '<name>.html` '],
['<name>.sandbox/index.html', '<name>.html` '],
['<name>.sandbox.html', '<name>.html'],
['<name>.sandbox/index.html', '<name>.html'],
]"
/>
+1 -1
View File
@@ -18,7 +18,7 @@
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
},
"devDependencies": {
"@aklinker1/buildc": "^1.0.5",
"@aklinker1/buildc": "^1.0.7",
"@aklinker1/check": "^1.3.1",
"@types/fs-extra": "^11.0.4",
"@vitest/coverage-v8": "^1.6.0",
+1 -1
View File
@@ -25,7 +25,7 @@
"check": "buildc --deps-only -- check"
},
"peerDependencies": {
"wxt": ">=0.18.5"
"wxt": ">=0.18.6"
},
"dependencies": {
"@vitejs/plugin-react": "^4.3.1"
+1 -1
View File
@@ -25,7 +25,7 @@
"check": "buildc --deps-only -- check"
},
"peerDependencies": {
"wxt": ">=0.18.5"
"wxt": ">=0.18.6"
},
"dependencies": {
"vite-plugin-solid": "^2.10.2"
+1 -1
View File
@@ -25,7 +25,7 @@
"check": "buildc --deps-only -- check"
},
"peerDependencies": {
"wxt": ">=0.18.5"
"wxt": ">=0.18.6"
},
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.1"
+1 -1
View File
@@ -25,7 +25,7 @@
"check": "buildc --deps-only -- check"
},
"peerDependencies": {
"wxt": ">=0.18.5"
"wxt": ">=0.18.6"
},
"dependencies": {
"@vitejs/plugin-vue": "^5.0.5"
+2 -2
View File
@@ -4,8 +4,8 @@
"private": true,
"type": "module",
"scripts": {
"dev": "buildc -- wxt",
"build": "buildc --deps-only -- wxt build",
"dev": "buildc --deps-only -- wxt",
"build": "buildc -- wxt build",
"build:all": "buildc --deps-only -- run-s -s 'build:all:*'",
"build:all:chrome-mv3": "wxt build",
"build:all:chrome-mv2": "wxt build --mv2",
@@ -13,7 +13,7 @@ export default defineContentScript({
anchor: 'form[role=search]',
onMount: (container) => {
const app = document.createElement('div');
app.textContent = 'Custom content script UI';
app.textContent = browser.i18n.getMessage('prompt_for_name');
container.append(app);
},
});
+4 -1
View File
@@ -19,7 +19,10 @@ export default defineConfig({
open: true,
},
experimental: {
viteRuntime: true,
entrypointImporter: 'vite-node',
},
runner: {
startUrls: ['https://duckduckgo.com'],
},
example: {
a: 'a',
+27
View File
@@ -1,5 +1,32 @@
# Changelog
## v0.18.9
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.8...wxt-v0.18.9)
### 🚀 Enhancements
- **experimental:** Replace `viteRuntime` option with `entrypointImporter` option, and implement `vite-node` importer ([#761](https://github.com/wxt-dev/wxt/pull/761))
## v0.18.8
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.7...wxt-v0.18.8)
### 🚀 Enhancements
- **dev:** Reload extension when public files change ([#752](https://github.com/wxt-dev/wxt/pull/752))
### 🩹 Fixes
- Don't load plugins twice in HTML pages ([#746](https://github.com/wxt-dev/wxt/pull/746))
- Ignore .wxt file changes in dev ([#755](https://github.com/wxt-dev/wxt/pull/755))
- **modules:** `addViteConfig` ignored user vite config ([#760](https://github.com/wxt-dev/wxt/pull/760))
### 🏡 Chore
- Refactor client web socket util ([#753](https://github.com/wxt-dev/wxt/pull/753))
- Add E2E test for `addImportPreset` ([9fc6408](https://github.com/wxt-dev/wxt/commit/9fc6408))
## v0.18.7
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.6...wxt-v0.18.7)
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe.each([true, false])(
describe.each(['jiti', 'vite-runtime', 'vite-node'] as const)(
'Manifest Content (Vite runtime? %s)',
(viteRuntime) => {
(entrypointImporter) => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
@@ -24,7 +24,7 @@ describe.each([true, false])(
main: () => {},
})`,
);
await project.build({ browser, experimental: { viteRuntime } });
await project.build({ browser, experimental: { entrypointImporter } });
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
+24
View File
@@ -287,5 +287,29 @@ describe('Module Helpers', () => {
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
it('should add preset', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground(() => {
customImport();
});`,
);
project.addFile(
'modules/test.ts',
`import { defineWxtModule, addImportPreset } from 'wxt/modules';
export default defineWxtModule((wxt) => {
addImportPreset(wxt, "vue");
})`,
);
await project.build();
await expect(
project.serializeFile('.wxt/types/imports.d.ts'),
).resolves.toContain("const ref: typeof import('vue')['ref']");
});
});
});
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.18.7",
"version": "0.18.9",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -138,6 +138,7 @@
"publish-browser-extension": "^2.1.3",
"unimport": "^3.7.2",
"vite": "^5.3.0",
"vite-node": "^1.6.0",
"web-ext-run": "^0.2.0",
"webextension-polyfill": "^0.10.0"
},
+3 -3
View File
@@ -13,10 +13,10 @@ describe('Module Utilities', () => {
const userConfig = {};
const moduleConfig = { build: { sourcemap: true } };
wxt.config.vite = () => userConfig;
wxt.config.vite = () => Promise.resolve(userConfig);
addViteConfig(wxt, () => moduleConfig);
await wxt.hooks.callHook('ready', wxt);
const actual: any = wxt.config.vite(wxt.config.env);
const actual = await wxt.config.vite(wxt.config.env);
expect(actual).toEqual(expected);
});
@@ -32,7 +32,7 @@ describe('Module Utilities', () => {
wxt.config.vite = () => userConfig;
addViteConfig(wxt, () => moduleConfig);
await wxt.hooks.callHook('ready', wxt);
const actual: any = wxt.config.vite(wxt.config.env);
const actual = await wxt.config.vite(wxt.config.env);
expect(actual).toEqual(expected);
});
@@ -1,22 +0,0 @@
import { describe, it, expect } from 'vitest';
import { createViteBuilder } from '../index';
import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects';
import { createHooks } from 'hookable';
describe('Vite Builder', () => {
describe('importEntrypoint', () => {
it('should import entrypoints, removing runtime values (like the main function)', async () => {
const {
default: { main: _, ...expected },
} = await import('./fixtures/module');
const builder = await createViteBuilder(
fakeResolvedConfig({ root: __dirname }),
createHooks(),
);
const actual = await builder.importEntrypoint<{ default: any }>(
'./fixtures/module.ts',
);
expect(actual).toEqual(expected);
});
});
});
+63 -15
View File
@@ -20,6 +20,10 @@ import {
import { Hookable } from 'hookable';
import { toArray } from '~/core/utils/arrays';
import { safeVarName } from '~/core/utils/strings';
import { importEntrypointFile } from '~/core/utils/building';
import { ViteNodeServer } from 'vite-node/server';
import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map';
export async function createViteBuilder(
wxtConfig: ResolvedConfig,
@@ -204,21 +208,65 @@ export async function createViteBuilder(
return {
name: 'Vite',
version: vite.version,
async importEntrypoint(url) {
const baseConfig = await getBaseConfig();
const envConfig: vite.InlineConfig = {
plugins: [
wxtPlugins.webextensionPolyfillMock(wxtConfig),
wxtPlugins.removeEntrypointMainFunction(wxtConfig, url),
],
};
const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config);
await server.listen();
const runtime = await vite.createViteRuntime(server, { hmr: false });
const module = await runtime.executeUrl(url);
await server.close();
return module.default;
async importEntrypoint(path) {
switch (wxtConfig.experimental.entrypointImporter) {
default:
case 'jiti': {
return await importEntrypointFile(path);
}
case 'vite-runtime': {
const baseConfig = await getBaseConfig();
const envConfig: vite.InlineConfig = {
plugins: [
wxtPlugins.webextensionPolyfillMock(wxtConfig),
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
],
};
const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config);
await server.listen();
const runtime = await vite.createViteRuntime(server, { hmr: false });
const module = await runtime.executeUrl(path);
await server.close();
return module.default;
}
case 'vite-node': {
const baseConfig = await getBaseConfig();
// Disable dep optimization, as recommended by vite-node's README
baseConfig.optimizeDeps ??= {};
baseConfig.optimizeDeps.noDiscovery = true;
baseConfig.optimizeDeps.include = [];
const envConfig: vite.InlineConfig = {
plugins: [
wxtPlugins.webextensionPolyfillMock(wxtConfig),
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
],
};
const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config);
await server.pluginContainer.buildStart({});
const node = new ViteNodeServer(server);
installSourcemapsSupport({
getSourceMap: (source) => node.getSourceMap(source),
});
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
// when having the server and runner in a different context,
// you will need to handle the communication between them
// and pass to this function
fetchModule(id) {
return node.fetchModule(id);
},
resolveId(id, importer) {
return node.resolveId(id, importer);
},
});
const res = await runner.executeFile(path);
await server.close();
return res.default;
}
}
},
async build(group) {
let entryConfig;
@@ -46,13 +46,19 @@ try {
// Use "pre" so the new script is added before vite bundles all the scripts
order: 'pre',
handler(html, _ctx) {
const { document } = parseHTML(html);
const script = document.createElement('script');
script.type = 'module';
script.src =
const src =
config.command === 'serve'
? `http://${config.dev.server?.hostname}:${config.dev.server?.port}/@id/${virtualHtmlModuleId}`
: virtualHtmlModuleId;
const { document } = parseHTML(html);
const existing = document.querySelector(`script[src='${src}']`);
if (existing) return;
const script = document.createElement('script');
script.type = 'module';
script.src = src;
if (document.head == null) {
const newHead = document.createElement('head');
document.documentElement.prepend(newHead);
+1
View File
@@ -153,6 +153,7 @@ function createFileReloader(server: WxtDevServer) {
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
if (path.startsWith(wxt.config.outBaseDir)) return;
if (path.startsWith(wxt.config.wxtDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
@@ -100,6 +100,11 @@ describe('Detect Dev Changes', () => {
describe('Public Assets', () => {
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
const changes = ['/root/src/public/image.svg'];
setFakeWxt({
config: {
publicDir: '/root/src/public',
},
});
const asset1 = fakeOutputAsset({
fileName: 'image.svg',
});
@@ -114,10 +119,7 @@ describe('Detect Dev Changes', () => {
const expected: DevModeChange = {
type: 'extension-reload',
rebuildGroups: [],
cachedOutput: {
...currentOutput,
publicAssets: [asset2],
},
cachedOutput: currentOutput,
};
const actual = detectDevChanges(changes, currentOutput);
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import {
BackgroundEntrypoint,
BackgroundEntrypointOptions,
@@ -12,16 +12,13 @@ import {
import { resolve } from 'path';
import { findEntrypoints } from '../find-entrypoints';
import fs from 'fs-extra';
import { importEntrypointFile } from '../import-entrypoint';
import glob from 'fast-glob';
import {
fakeResolvedConfig,
setFakeWxt,
} from '~/core/utils/testing/fake-objects';
import { unnormalizePath } from '~/core/utils/paths';
vi.mock('../import-entrypoint');
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
import { wxt } from '~/core/wxt';
vi.mock('fast-glob');
const globMock = vi.mocked(glob);
@@ -39,9 +36,11 @@ describe('findEntrypoints', () => {
outDir: resolve('.output'),
command: 'build',
});
let importEntrypointMock: Mock;
beforeEach(() => {
setFakeWxt({ config });
importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint);
});
it.each<[string, string, PopupEntrypoint]>([
@@ -211,13 +210,13 @@ describe('findEntrypoints', () => {
matches: ['<all_urls>'],
};
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
},
);
@@ -249,13 +248,13 @@ describe('findEntrypoints', () => {
type: 'module',
};
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
},
);
@@ -338,12 +337,13 @@ describe('findEntrypoints', () => {
config: {
manifestVersion: 2,
},
builder: wxt.builder,
});
const options: BackgroundEntrypointOptions = {
type: 'module',
};
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue(options);
importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
@@ -355,12 +355,13 @@ describe('findEntrypoints', () => {
config: {
manifestVersion: 3,
},
builder: wxt.builder,
});
const options: BackgroundEntrypointOptions = {
type: 'module',
};
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue(options);
importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
@@ -373,6 +374,7 @@ describe('findEntrypoints', () => {
...config,
command: 'serve',
},
builder: wxt.builder,
});
globMock.mockResolvedValueOnce(['popup.html']);
@@ -410,13 +412,13 @@ describe('findEntrypoints', () => {
};
const options: BaseEntrypointOptions = {};
globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options);
importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
},
);
@@ -701,7 +703,7 @@ describe('findEntrypoints', () => {
describe('include option', () => {
it("should filter out the background when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue({
importEntrypointMock.mockResolvedValue({
include: ['not' + config.browser],
});
@@ -712,7 +714,7 @@ describe('findEntrypoints', () => {
it("should filter out content scripts when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointFileMock.mockResolvedValue({
importEntrypointMock.mockResolvedValue({
include: ['not' + config.browser],
});
@@ -776,7 +778,7 @@ describe('findEntrypoints', () => {
describe('exclude option', () => {
it('should filter out the background when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue({
importEntrypointMock.mockResolvedValue({
exclude: [config.browser],
});
@@ -787,7 +789,7 @@ describe('findEntrypoints', () => {
it('should filter out content scripts when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointFileMock.mockResolvedValue({
importEntrypointMock.mockResolvedValue({
exclude: [config.browser],
});
@@ -850,7 +852,6 @@ describe('findEntrypoints', () => {
'ui.content/index.ts',
'injected.content/index.ts',
]);
importEntrypointFileMock.mockResolvedValue({});
const filterEntrypoints = ['popup', 'ui'];
setFakeWxt({
config: {
@@ -860,8 +861,11 @@ describe('findEntrypoints', () => {
command: 'build',
filterEntrypoints: new Set(filterEntrypoints),
},
builder: wxt.builder,
});
importEntrypointMock.mockResolvedValue({});
const entrypoints = await findEntrypoints();
const names = entrypoints.map((item) => item.name);
expect(names).toHaveLength(2);
@@ -2,7 +2,6 @@ import {
BuildOutput,
BuildStepOutput,
EntrypointGroup,
OutputAsset,
OutputFile,
} from '~/types';
import { every, some } from '~/core/utils/arrays';
@@ -50,12 +49,25 @@ export function detectDevChanges(
findEffectedSteps(changedFile, currentOutput),
),
);
if (changedSteps.size === 0) return { type: 'no-change' };
if (changedSteps.size === 0) {
const hasPublicChange = some(changedFiles, (file) =>
file.startsWith(wxt.config.publicDir),
);
if (hasPublicChange) {
return {
type: 'extension-reload',
rebuildGroups: [],
cachedOutput: currentOutput,
};
} else {
return { type: 'no-change' };
}
}
const unchangedOutput: BuildOutput = {
manifest: currentOutput.manifest,
steps: [],
publicAssets: [],
publicAssets: [...currentOutput.publicAssets],
};
const changedOutput: BuildOutput = {
manifest: currentOutput.manifest,
@@ -70,13 +82,6 @@ export function detectDevChanges(
unchangedOutput.steps.push(step);
}
}
for (const asset of currentOutput.publicAssets) {
if (changedSteps.has(asset)) {
changedOutput.publicAssets.push(asset);
} else {
unchangedOutput.publicAssets.push(asset);
}
}
const isOnlyHtmlChanges =
changedFiles.length > 0 &&
@@ -117,8 +122,8 @@ export function detectDevChanges(
function findEffectedSteps(
changedFile: string,
currentOutput: BuildOutput,
): DetectedChange[] {
const changes: DetectedChange[] = [];
): BuildStepOutput[] {
const changes: BuildStepOutput[] = [];
const changedPath = normalizePath(changedFile);
const isChunkEffected = (chunk: OutputFile): boolean =>
@@ -135,11 +140,6 @@ function findEffectedSteps(
if (effectedChunk) changes.push(step);
}
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
isChunkEffected(chunk),
);
if (effectedAsset) changes.push(effectedAsset);
return changes;
}
@@ -194,10 +194,3 @@ interface ContentScriptReload extends RebuildChange {
type: 'content-script-reload';
changedSteps: BuildStepOutput[];
}
/**
* When figuring out what needs reloaded, this stores the step that was changed, or the public
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
* intermediate type.
*/
type DetectedChange = BuildStepOutput | OutputAsset;
@@ -28,7 +28,6 @@ import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
import pc from 'picocolors';
import { wxt } from '../../wxt';
import { importEntrypointFile } from './import-entrypoint';
/**
* Return entrypoints and their configuration by looking through the project's files.
@@ -290,7 +289,7 @@ async function getUnlistedScriptEntrypoint({
skipped,
}: EntrypointInfo): Promise<GenericEntrypoint> {
const defaultExport =
await importEntrypoint<UnlistedScriptDefinition>(inputPath);
await wxt.builder.importEntrypoint<UnlistedScriptDefinition>(inputPath);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
@@ -315,7 +314,7 @@ async function getBackgroundEntrypoint({
let options: Omit<BackgroundDefinition, 'main'> = {};
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
const defaultExport =
await importEntrypoint<BackgroundDefinition>(inputPath);
await wxt.builder.importEntrypoint<BackgroundDefinition>(inputPath);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
@@ -344,8 +343,15 @@ async function getContentScriptEntrypoint({
name,
skipped,
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
const { main: _, ...options } =
await importEntrypoint<ContentScriptDefinition>(inputPath);
const defaultExport =
await wxt.builder.importEntrypoint<ContentScriptDefinition>(inputPath);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
);
}
const { main: _, ...options } = defaultExport;
if (options == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
@@ -488,9 +494,3 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {
};
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
function importEntrypoint<T>(path: string) {
return wxt.config.experimental.viteRuntime
? wxt.builder.importEntrypoint<T>(path)
: importEntrypointFile<T>(path);
}
@@ -167,7 +167,7 @@ export async function resolveConfig(
alias,
experimental: defu(mergedConfig.experimental, {
includeBrowserPolyfill: true,
viteRuntime: false,
entrypointImporter: 'jiti' as const,
}),
dev: {
server: devServerConfig,
@@ -297,7 +297,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
alias: {},
experimental: {
includeBrowserPolyfill: true,
viteRuntime: false,
entrypointImporter: 'jiti',
},
dev: {
reloadCommand: 'Alt+R',
+5 -7
View File
@@ -104,13 +104,11 @@ export function addViteConfig(
): void {
wxt.hooks.hook('ready', (wxt) => {
const userVite = wxt.config.vite;
wxt.config.vite = (env) =>
vite.mergeConfig(
// Use config added by module as base
viteConfig(env) ?? {},
// Overwrite module config with user config
userVite(env),
);
wxt.config.vite = async (env) => {
const fromUser = await userVite(env);
const fromModule = viteConfig(env) ?? {};
return vite.mergeConfig(fromModule, fromUser);
};
});
}
@@ -0,0 +1,85 @@
import { logger } from './utils/logger';
interface WebSocketMessage {
type: string;
event: string;
data?: any;
}
export interface WxtWebSocket extends WebSocket {
addWxtEventListener(
type: 'wxt:reload-extension',
callback: (event: CustomEvent<undefined>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addWxtEventListener(
type: 'wxt:reload-content-script',
callback?: (event: CustomEvent<ReloadContentScriptPayload>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addWxtEventListener(
type: 'wxt:reload-page',
callback?: (event: CustomEvent<string>) => void,
options?: AddEventListenerOptions | boolean,
): void;
sendCustom(event: string, payload?: any): void;
}
let ws: WxtWebSocket | undefined;
/**
* Connect to the websocket and listen for messages.
*
* @param onMessage Optional callback that is called when a message is recieved and we've verified
* it's structure is what we expect.
*/
export function getDevServerWebSocket(): WxtWebSocket {
if (import.meta.env.COMMAND !== 'serve')
throw Error(
'Must be running WXT dev command to connect to call getDevServerWebSocket()',
);
if (ws == null) {
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
logger.debug('Connecting to dev server @', serverUrl);
ws = new WebSocket(serverUrl, 'vite-hmr') as WxtWebSocket;
ws.addWxtEventListener = ws.addEventListener.bind(ws);
ws.sendCustom = (event, payload) =>
ws?.send(JSON.stringify({ type: 'custom', event, payload }));
ws.addEventListener('open', () => {
logger.debug('Connected to dev server');
});
ws.addEventListener('close', () => {
logger.debug('Disconnected from dev server');
});
ws.addEventListener('error', (event) => {
logger.error('Failed to connect to dev server', event);
});
ws.addEventListener('message', (e) => {
try {
const message = JSON.parse(e.data) as WebSocketMessage;
if (message.type === 'custom') {
ws?.dispatchEvent(
new CustomEvent(message.event, { detail: message.data }),
);
}
} catch (err) {
logger.error('Failed to handle message', err);
}
});
}
return ws;
}
export interface ReloadContentScriptPayload {
registration?: 'manifest' | 'runtime';
contentScript: {
matches: string[];
js?: string[];
css?: string[];
};
}
+9 -6
View File
@@ -317,14 +317,17 @@ export interface InlineConfig {
*/
includeBrowserPolyfill?: boolean;
/**
* When set to `true`, use the Vite Runtime API to load entrypoint options instead of the default, `jiti`.
* Method used to import entrypoint files during the build process to extract their options.
*
* Lets you use imported variables and leverage your Vite config to add support for non-standard APIs/syntax.
* - "jiti": Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function
* - "vite-runtime" (unstable): Uses Vite 5.3's new runtime API to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
* - "vite-node" (unstable): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
*
* @experimental Early access to try out the feature before it becomes the default.
* @default false
* @see {@link https://wxt.dev/guide/go-further/entrypoint-side-effects.html|Entrypoint Side-effect Docs}
*
* @default "jiti"
*/
viteRuntime?: boolean;
entrypointImporter?: 'jiti' | 'vite-runtime' | 'vite-node';
};
/**
* Config effecting dev mode only.
@@ -1174,7 +1177,7 @@ export interface ResolvedConfig {
alias: Record<string, string>;
experimental: {
includeBrowserPolyfill: boolean;
viteRuntime: boolean;
entrypointImporter: 'jiti' | 'vite-runtime' | 'vite-node';
};
dev: {
/** Only defined during dev command */
@@ -1,6 +1,6 @@
import definition from 'virtual:user-background-entrypoint';
import { initPlugins } from 'virtual:wxt-plugins';
import { setupWebSocket } from './utils/setup-web-socket';
import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
import { logger } from '../sandbox/utils/logger';
import { browser } from 'wxt/browser';
import { keepServiceWorkerAlive } from './utils/keep-service-worker-alive';
@@ -8,18 +8,19 @@ import { reloadContentScript } from './utils/reload-content-scripts';
if (import.meta.env.COMMAND === 'serve') {
try {
const ws = setupWebSocket((message) => {
if (message.event === 'wxt:reload-extension') browser.runtime.reload();
if (message.event === 'wxt:reload-content-script' && message.data != null)
reloadContentScript(message.data);
const ws = getDevServerWebSocket();
ws.addWxtEventListener('wxt:reload-extension', () => {
browser.runtime.reload();
});
ws.addWxtEventListener('wxt:reload-content-script', (event) => {
reloadContentScript(event.detail);
});
if (import.meta.env.MANIFEST_VERSION === 3) {
// Tell the server the background script is loaded and ready to go
ws.addEventListener('open', () => {
const msg = { type: 'custom', event: 'wxt:background-initialized' };
ws.send(JSON.stringify(msg));
});
ws.addEventListener('open', () =>
ws.sendCustom('wxt:background-initialized'),
);
// Web Socket will disconnect if the service worker is killed
keepServiceWorkerAlive();
+5 -9
View File
@@ -1,16 +1,12 @@
import { logger } from '../sandbox/utils/logger';
import { setupWebSocket } from './utils/setup-web-socket';
import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
if (import.meta.env.COMMAND === 'serve') {
try {
setupWebSocket((message) => {
if (message.event === 'wxt:reload-page') {
// We need to remove the initial slash from the path to compare correctly
// "popup.html" === "/popup.html".substring(1)
if (message.data === location.pathname.substring(1)) {
location.reload();
}
}
const ws = getDevServerWebSocket();
ws.addWxtEventListener('wxt:reload-page', (event) => {
// "popup.html" === "/popup.html".substring(1)
if (event.detail === location.pathname.substring(1)) location.reload();
});
} catch (err) {
logger.error('Failed to setup web socket connection with dev server', err);
@@ -1,16 +1,7 @@
import { browser } from 'wxt/browser';
import { logger } from '../../sandbox/utils/logger';
import { MatchPattern } from 'wxt/sandbox';
interface ContentScript {
matches: string[];
js?: string[];
css?: string[];
}
interface ReloadContentScriptPayload {
registration?: 'manifest' | 'runtime';
contentScript: ContentScript;
}
import type { ReloadContentScriptPayload } from '../../sandbox/dev-server-websocket';
export function reloadContentScript(payload: ReloadContentScriptPayload) {
const manifest = browser.runtime.getManifest();
@@ -32,6 +23,8 @@ export async function reloadContentScriptMv3({
}
}
type ContentScript = ReloadContentScriptPayload['contentScript'];
export async function reloadManifestContentScriptMv3(
contentScript: ContentScript,
) {
@@ -1,44 +0,0 @@
import { logger } from '../../sandbox/utils/logger';
interface WebSocketMessage {
type: string;
event: string;
data?: any;
}
/**
* Connect to the websocket and listen for messages.
*
* @param onMessage Optional callback that is called when a message is recieved and we've verified
* it's structure is what we expect.
*/
export function setupWebSocket(
onMessage?: (message: WebSocketMessage) => void,
) {
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
logger.debug('Connecting to dev server @', serverUrl);
const ws = new WebSocket(serverUrl, 'vite-hmr');
ws.addEventListener('open', () => {
logger.debug('Connected to dev server');
});
ws.addEventListener('close', () => {
logger.debug('Disconnected from dev server');
});
ws.addEventListener('error', (event) => {
logger.error('Failed to connect to dev server', event);
});
ws.addEventListener('message', (e) => {
try {
const message = JSON.parse(e.data) as WebSocketMessage;
if (message.type === 'custom' && message.event?.startsWith?.('wxt:')) {
onMessage?.(message);
}
} catch (err) {
logger.error('Failed to handle message', err);
}
});
return ws;
}
+10 -7
View File
@@ -9,8 +9,8 @@ importers:
.:
devDependencies:
'@aklinker1/buildc':
specifier: ^1.0.5
version: 1.0.5(typescript@5.4.5)
specifier: ^1.0.7
version: 1.0.7(typescript@5.4.5)
'@aklinker1/check':
specifier: ^1.3.1
version: 1.3.1(typescript@5.4.5)
@@ -291,6 +291,9 @@ importers:
vite:
specifier: ^5.3.0
version: 5.3.0(@types/node@20.14.2)
vite-node:
specifier: ^1.6.0
version: 1.6.0(@types/node@20.14.2)
web-ext-run:
specifier: ^0.2.0
version: 0.2.0
@@ -377,8 +380,8 @@ importers:
packages:
/@aklinker1/buildc@1.0.5(typescript@5.4.5):
resolution: {integrity: sha512-xM9tA6VpkHh2qTCVBwuXUGdBpgxtnj35zrDT0hMBWBLAoAvNXjQrGf4DaFN+gnBsd32TtFPDg1IfcW2ZcUF0ZQ==}
/@aklinker1/buildc@1.0.7(typescript@5.4.5):
resolution: {integrity: sha512-8njwYe/uxeeyqfj1ryOOwWolNaoJpVjFMoR8muNezo4D6IL6NBNN2XNhqaCShlDBjHM5NG7mdsd/5rueqsKReg==}
hasBin: true
peerDependencies:
typescript: ^5.0.0
@@ -741,6 +744,7 @@ packages:
/@babel/highlight@7.24.7:
resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
engines: {node: '>=6.9.0'}
requiresBuild: true
dependencies:
'@babel/helper-validator-identifier': 7.24.7
chalk: 2.4.2
@@ -7028,7 +7032,7 @@ packages:
spdx-expression-parse: 3.0.1
dev: true
/vite-node@1.6.0:
/vite-node@1.6.0(@types/node@20.14.2):
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
engines: {node: ^18.0.0 || >=20.0.0}
hasBin: true
@@ -7047,7 +7051,6 @@ packages:
- sugarss
- supports-color
- terser
dev: true
/vite-plugin-solid@2.10.2(solid-js@1.8.17)(vite@5.3.0):
resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==}
@@ -7235,7 +7238,7 @@ packages:
tinybench: 2.5.1
tinypool: 0.8.4
vite: 5.3.0(@types/node@20.14.2)
vite-node: 1.6.0
vite-node: 1.6.0(@types/node@20.14.2)
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less