diff --git a/docs/guide/development.md b/docs/guide/development.md index 5f004906..4bd1fc4c 100644 --- a/docs/guide/development.md +++ b/docs/guide/development.md @@ -57,3 +57,13 @@ export default defineRunnerConfig({ :::tip When configuring browser binaries, it's helpful to put them in `~/web-ext.config.ts` instead of the project directory's `web-ext.config.ts` file. When placed in your home directory (`~/`), this config will be used by all WXT projects, so you only need to configure the binaries once. ::: + +## Reload the Extension + +Normally, to manually reload an extension, you have to visit `chrome://extensions` and click the reload button for your extension. + +When running `wxt` command to start the dev server, WXT adds a keyboard shortcut, `ctrl+E` for Windows/Linux and `cmd+E` for Mac, that reloads the extension when pressed, without visiting `chrome://extensions`. + +:::note +This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`. +::: diff --git a/src/core/utils/__tests__/manifest.test.ts b/src/core/utils/__tests__/manifest.test.ts new file mode 100644 index 00000000..c1515c6d --- /dev/null +++ b/src/core/utils/__tests__/manifest.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { generateMainfest } from '../manifest'; +import { + fakeArray, + fakeBuildOutput, + fakeEntrypoint, + fakeInternalConfig, +} from '../testing/fake-objects'; + +describe('Manifest Utils', () => { + describe('generateManifest', () => { + describe('Development reload command', () => { + const reloadCommandName = 'wxt:reload-extension'; + const reloadCommand = { + suggested_key: { + default: 'Ctrl+E', + }, + }; + + it('should include a command for reloading the extension', async () => { + const config = fakeInternalConfig({ command: 'serve' }); + const output = fakeBuildOutput(); + const entrypoints = fakeArray(fakeEntrypoint); + + const actual = await generateMainfest(entrypoints, output, config); + + expect(actual.commands).toMatchObject({ + [reloadCommandName]: reloadCommand, + }); + }); + + it('should not override any existing commands when adding the one to reload the extension', async () => { + const customCommandName = 'custom-command'; + const customCommand = { + description: 'Some other command', + suggested_key: { + default: 'Ctrl+H', + }, + }; + const config = fakeInternalConfig({ + command: 'serve', + manifest: { + commands: { + [customCommandName]: customCommand, + }, + }, + }); + const output = fakeBuildOutput(); + const entrypoints = fakeArray(fakeEntrypoint); + + const actual = await generateMainfest(entrypoints, output, config); + + expect(actual.commands).toMatchObject({ + [reloadCommandName]: reloadCommand, + [customCommandName]: customCommand, + }); + }); + + it('should not include the command when building an extension', async () => { + const config = fakeInternalConfig({ command: 'build' }); + const output = fakeBuildOutput(); + const entrypoints = fakeArray(fakeEntrypoint); + + const actual = await generateMainfest(entrypoints, output, config); + + expect(actual.commands).toBeUndefined(); + }); + }); + }); +}); diff --git a/src/core/utils/manifest.ts b/src/core/utils/manifest.ts index f43b0890..2b78d258 100644 --- a/src/core/utils/manifest.ts +++ b/src/core/utils/manifest.ts @@ -73,6 +73,16 @@ export async function generateMainfest( short_name: pkg?.shortName, icons: discoverIcons(buildOutput), }; + if (config.command === 'serve') { + baseManifest.commands = { + 'wxt:reload-extension': { + description: 'Reload the extension during development', + suggested_key: { + default: 'Ctrl+E', + }, + }, + }; + } const userManifest = config.manifest; const manifest = defu( diff --git a/src/core/utils/testing/fake-objects.ts b/src/core/utils/testing/fake-objects.ts index f846bd8b..f1b0ce33 100644 --- a/src/core/utils/testing/fake-objects.ts +++ b/src/core/utils/testing/fake-objects.ts @@ -17,6 +17,8 @@ import { OutputChunk, OutputFile, OutputAsset, + BuildOutput, + BuildStepOutput, } from '~/types'; import { mock } from 'vitest-mock-extended'; @@ -39,6 +41,16 @@ export function fakeDir(root = process.cwd()): string { return resolve(root, faker.string.alphanumeric()); } +export const fakeEntrypoint = () => + faker.helpers.arrayElement([ + fakePopupEntrypoint, + fakeGenericEntrypoint, + fakeOptionsEntrypoint, + fakeBackgroundEntrypoint, + fakeContentScriptEntrypoint, + fakeUnlistedScriptEntrypoint, + ])(); + export const fakeContentScriptEntrypoint = fakeObjectCreator(() => ({ type: 'content-script', @@ -222,3 +234,14 @@ export const fakeInternalConfig = fakeObjectCreator(() => { builder: mock(), }; }); + +export const fakeBuildOutput = fakeObjectCreator(() => ({ + manifest: fakeManifest(), + publicAssets: fakeArray(fakeOutputAsset), + steps: fakeArray(fakeBuildStepOutput), +})); + +export const fakeBuildStepOutput = fakeObjectCreator(() => ({ + chunks: fakeArray(fakeOutputChunk), + entrypoints: fakeArray(fakeEntrypoint), +})); diff --git a/src/virtual/background-entrypoint.ts b/src/virtual/background-entrypoint.ts index f69897a0..eb448a22 100644 --- a/src/virtual/background-entrypoint.ts +++ b/src/virtual/background-entrypoint.ts @@ -26,6 +26,12 @@ if (__COMMAND__ === 'serve') { } catch (err) { logger.error('Failed to setup web socket connection with dev server', err); } + + browser.commands.onCommand.addListener((command) => { + if (command === 'wxt:reload-extension') { + browser.runtime.reload(); + } + }); } try {